mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 01:20:38 +00:00
⚡ Improve memory consumption on shape decoding (#12790)
* ⚡ Avoid decoding the opened page twice for its frame index The frame index was built over a with-meta copy, so pages got decoded again. Read it from the page objects themselves and fix the with-meta identity check. * ⚡ Intern keywords when decoding transit on the frontend Each shape is decoded on its own, so every shape had its own key keywords. Cache them in the read handler so all decoded data shares one instance. * ⚡ Use an array map as the wasm shape proxy delegate A hash map with ~35 keys costs ~3 KB of nodes per shape on large pages. An array map holds the same entries in one array, at a small lookup cost.
This commit is contained in:
parent
88cd0933bd
commit
51cbae6ea5
@ -99,6 +99,17 @@
|
||||
|
||||
;; --- HANDLERS
|
||||
|
||||
#?(:cljs
|
||||
(defn- keyword-reader
|
||||
"Return a reader that gives the same keyword instance for each name."
|
||||
[]
|
||||
(let [cache (js/Map.)]
|
||||
(fn [value]
|
||||
(or (.get cache value)
|
||||
(let [kw (keyword value)]
|
||||
(.set cache value kw)
|
||||
kw))))))
|
||||
|
||||
(add-handlers!
|
||||
#?@(:clj
|
||||
[{:id "file"
|
||||
@ -119,6 +130,10 @@
|
||||
{:id "u"
|
||||
:rfn parse-uuid})
|
||||
|
||||
#?(:cljs
|
||||
{:id ":"
|
||||
:rfn (keyword-reader)})
|
||||
|
||||
{:id "ordered-map"
|
||||
:class #?(:clj LinkedMap :cljs lkm/LinkedMap)
|
||||
:wfn vec
|
||||
|
||||
@ -206,7 +206,7 @@
|
||||
|
||||
c/IWithMeta
|
||||
(-with-meta [this new-meta]
|
||||
(if (identical? new-meta meta)
|
||||
(if (identical? new-meta metadata)
|
||||
this
|
||||
(ObjectsMap. new-meta
|
||||
cache
|
||||
|
||||
@ -368,11 +368,17 @@
|
||||
|
||||
(defn start-page-index
|
||||
[objects]
|
||||
(with-meta objects {::index-frames (get-frames (with-meta objects nil))}))
|
||||
(with-meta objects {::index-frames (into [] (get-frames objects {:ignore-index? true}))}))
|
||||
|
||||
(defn update-page-index
|
||||
"Lazy, so pages that are not open are not decoded. The index reads
|
||||
from the returned objects, so decoded shapes are shared with them."
|
||||
[objects]
|
||||
(with-meta objects {::index-frames (get-frames (with-meta objects nil))}))
|
||||
(let [self (volatile! nil)
|
||||
objects (with-meta objects
|
||||
{::index-frames (lazy-seq (get-frames @self {:ignore-index? true}))})]
|
||||
(vreset! self objects)
|
||||
objects))
|
||||
|
||||
(defn update-object-indices
|
||||
[file page-id]
|
||||
|
||||
@ -79,6 +79,16 @@
|
||||
(-count [_]
|
||||
(+ 1 (count delegate)))
|
||||
|
||||
c/IKVReduce
|
||||
(-kv-reduce [_ f init]
|
||||
(let [acc (f init :id id)]
|
||||
(if (reduced? acc)
|
||||
@acc
|
||||
(let [acc (f acc :type type)]
|
||||
(if (reduced? acc)
|
||||
@acc
|
||||
(reduce-kv f acc delegate))))))
|
||||
|
||||
c/ILookup
|
||||
(-lookup [coll k]
|
||||
(-lookup coll k nil))
|
||||
@ -354,6 +364,18 @@
|
||||
(let [id (:id self)]
|
||||
(.set changes id (conj* (.get changes id) k)))))
|
||||
|
||||
(defn- assoc-delegate
|
||||
"Keeps an array map delegate as an array map when adding a new key."
|
||||
[delegate k v]
|
||||
(if (and (instance? PersistentArrayMap delegate)
|
||||
(not (contains? delegate k)))
|
||||
(PersistentArrayMap. (meta delegate)
|
||||
(inc (count delegate))
|
||||
(doto (aclone (.-arr ^PersistentArrayMap delegate))
|
||||
(.push k v))
|
||||
nil)
|
||||
(assoc delegate k v)))
|
||||
|
||||
(defn- impl-assoc
|
||||
[self k v]
|
||||
(track-change! self k)
|
||||
@ -369,7 +391,7 @@
|
||||
(.-delegate ^ShapeProxy self))
|
||||
|
||||
(let [delegate (.-delegate ^ShapeProxy self)
|
||||
delegate' (assoc delegate k v)]
|
||||
delegate' (assoc-delegate delegate k v)]
|
||||
(if (identical? delegate' delegate)
|
||||
self
|
||||
(ShapeProxy. (.-id ^ShapeProxy self)
|
||||
@ -418,18 +440,19 @@
|
||||
(next es))
|
||||
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
|
||||
|
||||
(def ^:private xf:without-id-and-type
|
||||
(remove (fn [kvpair]
|
||||
(let [k (key kvpair)]
|
||||
(or (= k :id)
|
||||
(= k :type))))))
|
||||
|
||||
(defn create-shape
|
||||
"Instantiate a shape from a map"
|
||||
"Instantiate a shape from a map. The delegate is an array map because
|
||||
it takes far less memory than a hash map for shape-sized maps."
|
||||
[attrs]
|
||||
(ShapeProxy. (:id attrs)
|
||||
(:type attrs)
|
||||
(into {} xf:without-id-and-type attrs)))
|
||||
(let [arr (reduce-kv (fn [arr k v]
|
||||
(when-not (or (= k :id) (= k :type))
|
||||
(.push arr k v))
|
||||
arr)
|
||||
#js []
|
||||
attrs)]
|
||||
(ShapeProxy. (:id attrs)
|
||||
(:type attrs)
|
||||
(PersistentArrayMap. nil (/ (alength arr) 2) arr nil))))
|
||||
|
||||
(t/add-handlers!
|
||||
;; We only add a write handler, read handler uses the dynamic dispatch
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user