Belén Albeza fa1b3d4708
⚡ Keep only the shape data the worker reads (#12840)
* ⚡ Keep only the shape data the worker reads

The worker kept a full copy of the current page for its selection and
snap indexes. It now keeps only the attributes the indexes and the
change replay read, and re-indexes only the shapes that changed.

On a 52k-shape page the worker heap goes from 698 MB to 286 MB.

Closes #12740

AI-assisted-by: claude-opus-5-5

* ♻️ Share the worker re-trim with its replay test

The worker and its replay test now share `retrim-objects`.

AI-assisted-by: claude-opus-5-5
2026-11-05 16:25:29 +01:00

161 lines
6.2 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.worker.index-test
(:require
[app.common.files.builder :as fb]
[app.common.files.changes :as ch]
[app.common.geom.rect :as grc]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.worker.impl :as impl]
[app.worker.index :as idx]
[cljs.test :as t :include-macros true]
[clojure.set :as set]))
(t/use-fixtures :each
{:before #(reset! idx/state {:pages-index {}})})
(defn- make-page
[name]
(-> (fb/create-state)
(fb/add-file {:name "Test"})
(fb/add-page {:name name})
(fb/add-shape {:type :rect :x 0 :y 0 :width 100 :height 100})
(fb/get-current-page)))
(defn- initialize
[page]
(impl/handler {:cmd :index/initialize :page page}))
(defn- update-index
[page-id changes]
(impl/handler {:cmd :index/update :page-id page-id :changes changes}))
(defn- indexed-ids
[state]
{:pages (set (keys (:pages-index state)))
:selection (set (keys (::idx/selection state)))
:snap (set (keys (::idx/snap state)))})
(defn- add-rect-change
[page-id shape-id]
(let [shape (cts/setup-shape {:id shape-id :type :rect
:x 200 :y 200 :width 50 :height 50})]
{:type :add-obj
:id shape-id
:page-id page-id
:parent-id uuid/zero
:frame-id uuid/zero
:obj shape}))
(t/deftest finalize-drops-only-that-page
(let [page-a (make-page "Page A")
page-b (make-page "Page B")]
(initialize page-a)
(initialize page-b)
(impl/handler {:cmd :index/finalize :page-id (:id page-a)})
(t/is (= {:pages #{(:id page-b)}
:selection #{(:id page-b)}
:snap #{(:id page-b)}}
(indexed-ids @idx/state)))))
(t/deftest finalize-unknown-page-is-noop
(let [page (make-page "Page 1")]
(initialize page)
(let [before @idx/state]
(impl/handler {:cmd :index/finalize :page-id (uuid/next)})
(t/is (= before @idx/state)))))
(t/deftest update-ignores-pages-not-indexed
(let [page (make-page "Page 1")
page-id (uuid/next)]
(initialize page)
(update-index page-id [(add-rect-change page-id (uuid/next))])
(t/is (= {:pages #{(:id page)}
:selection #{(:id page)}
:snap #{(:id page)}}
(indexed-ids @idx/state)))))
(t/deftest update-drops-indexes-of-deleted-page
(let [page (make-page "Page 1")
page-id (:id page)]
(initialize page)
(update-index page-id [{:type :del-page :id page-id :page-id page-id}])
(t/is (= {:pages #{} :selection #{} :snap #{}}
(indexed-ids @idx/state)))))
(t/deftest update-indexes-new-shapes-of-indexed-page
(let [page (make-page "Page 1")
page-id (:id page)
shape-id (uuid/next)]
(initialize page)
(update-index page-id [(add-rect-change page-id shape-id)])
(t/is (contains? (get-in @idx/state [:pages-index page-id :objects]) shape-id))
(t/is (contains? (set (impl/handler {:cmd :index/query-selection
:page-id page-id
:rect (grc/make-rect 190 190 70 70)}))
shape-id))))
(defn- get-stored-shape
[page-id shape-id]
(get-in @idx/state [:pages-index page-id :objects shape-id]))
(t/deftest initialize-stores-a-trimmed-page
(let [page (make-page "Page 1")
shape-id (-> page :objects (dissoc uuid/zero) keys first)]
(initialize page)
(t/is (some? (get-stored-shape (:id page) shape-id)))
(t/is (nil? (:name (get-stored-shape (:id page) shape-id))))))
(t/deftest update-stores-trimmed-fills
(let [page (make-page "Page 1")
page-id (:id page)
shape-id (-> page :objects (dissoc uuid/zero) keys first)]
(initialize page)
(update-index page-id [{:type :mod-obj :id shape-id :page-id page-id
:operations [{:type :set :attr :fills
:val [{:fill-color "#ff0000" :fill-opacity 1}]}]}])
(t/is (= [{}] (:fills (get-stored-shape page-id shape-id))))))
(t/deftest update-renaming-a-shape-keeps-the-indexes
(let [page (make-page "Page 1")
page-id (:id page)
shape-id (-> page :objects (dissoc uuid/zero) keys first)]
(initialize page)
(let [before @idx/state]
(update-index page-id [{:type :mod-obj :id shape-id :page-id page-id
:operations [{:type :set :attr :name :val "renamed"}]}])
(t/is (identical? (::idx/selection before) (::idx/selection @idx/state)))
(t/is (identical? (::idx/snap before) (::idx/snap @idx/state))))))
(t/deftest update-skips-changes-the-indexes-do-not-read
(let [page (make-page "Page 1")
page-id (:id page)
shape-id (-> page :objects (dissoc uuid/zero) keys first)]
(initialize page)
(let [before (get-in @idx/state [:pages-index page-id])]
(update-index page-id [{:type :set-plugin-data :object-type :shape
:object-id shape-id :page-id page-id
:namespace "ns" :key "k" :value "v"}
{:type :mod-page :id page-id :page-id page-id :name "Renamed"}])
(t/is (= before (get-in @idx/state [:pages-index page-id]))))))
(t/deftest every-change-type-is-replayed-or-skipped
(let [known (set/union idx/index-change-types idx/skipped-change-types)]
(t/is (= #{} (set/difference (set (keys (methods ch/process-change))) known))
"Add the new change type to one set; replaying it may need attributes in `app.worker.trim`")
(t/is (= #{} (set/intersection idx/index-change-types idx/skipped-change-types)))))
(t/deftest update-skips-unknown-change-types
(let [page (make-page "Page 1")
page-id (:id page)
shape-id (-> page :objects (dissoc uuid/zero) keys first)]
(initialize page)
(let [before (get-in @idx/state [:pages-index page-id])]
(update-index page-id [{:type :unknown-change :id shape-id :page-id page-id}])
(t/is (= before (get-in @idx/state [:pages-index page-id]))))))