diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 63a4be935f..2f0d27f1ec 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -30,6 +30,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] + [app.main.streams :as ms] [app.render-wasm.api :as wasm.api] [app.render-wasm.shape :as wasm.shape] [beicon.v2.core :as rx] @@ -614,19 +615,27 @@ (filter (fn [[_ {:keys [type]}]] (= type :change-property))))) +;; The live preview state for a drag / resize / rotate gesture lives in +;; behavior-subjects in `app.main.streams` (`wasm-modifiers` and +;; `workspace-selrect`). These were previously the Redux keys +;; `:workspace-wasm-modifiers` / `:workspace-selrect`, but at one +;; React commit per pointermove they were the dominant per-event cost +;; during a drag. Reads now go through RAF-coalesced atoms in +;; `app.main.refs`, which update at most once per animation frame. + (defn set-temporary-selrect [selrect] (ptk/reify ::set-temporary-selrect - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-selrect selrect)))) + ptk/EffectEvent + (effect [_ _ _] + (rx/push! ms/workspace-selrect selrect)))) (defn set-temporary-modifiers [modifiers] (ptk/reify ::set-temporary-modifiers - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-wasm-modifiers modifiers)))) + ptk/EffectEvent + (effect [_ _ _] + (rx/push! ms/wasm-modifiers modifiers)))) (def ^:private xf:map-key (map key)) @@ -658,15 +667,40 @@ objects (dsh/lookup-page-objects state) snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))] + ;; --- DRAG PERF INSTRUMENTATION (remove after investigation) --- + ;; Spans appear in DevTools Performance > Timings track as `swm:*`. + ;; `props=N`/`geom=N` count tags are logged once per call so you can + ;; correlate per-event work to subtree size. + (js/performance.mark "swm:total:start") + (js/console.timeStamp (str "swm props=" (count wasm-props))) + (js/performance.mark "swm:set-props:start") (set-wasm-props! objects prev-wasm-props wasm-props) - (wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree)) - (let [geometry-entries (parse-geometry-modifiers modif-tree) - modifiers (wasm.api/propagate-modifiers geometry-entries snap-pixel?)] - (wasm.api/set-modifiers modifiers) - (let [ids (into [] xf:map-key geometry-entries) - selrect (wasm.api/get-selection-rect ids)] - (rx/of (set-temporary-selrect selrect) - (set-temporary-modifiers modifiers)))))))) + (js/performance.measure "swm:set-props" "swm:set-props:start") + (js/performance.clearMarks "swm:set-props:start") + (let [structure-entries (parse-structure-modifiers modif-tree)] + (js/performance.mark "swm:struct-mods:start") + (wasm.api/set-structure-modifiers structure-entries) + (js/performance.measure "swm:struct-mods" "swm:struct-mods:start") + (js/performance.clearMarks "swm:struct-mods:start") + (let [geometry-entries (parse-geometry-modifiers modif-tree) + _ (js/console.timeStamp (str "swm geom=" (count geometry-entries))) + _ (js/performance.mark "swm:propagate:start") + modifiers (wasm.api/propagate-modifiers geometry-entries snap-pixel?) + _ (js/performance.measure "swm:propagate" "swm:propagate:start") + _ (js/performance.clearMarks "swm:propagate:start")] + (js/performance.mark "swm:set-mods:start") + (wasm.api/set-modifiers modifiers) + (js/performance.measure "swm:set-mods" "swm:set-mods:start") + (js/performance.clearMarks "swm:set-mods:start") + (let [ids (into [] xf:map-key geometry-entries) + _ (js/performance.mark "swm:selrect:start") + selrect (wasm.api/get-selection-rect ids) + _ (js/performance.measure "swm:selrect" "swm:selrect:start") + _ (js/performance.clearMarks "swm:selrect:start")] + (js/performance.measure "swm:total" "swm:total:start") + (js/performance.clearMarks "swm:total:start") + (rx/of (set-temporary-selrect selrect) + (set-temporary-modifiers modifiers))))))))) (defn propagate-structure-modifiers [modif-tree objects] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 58982b33f0..3a05784be3 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -137,9 +137,15 @@ (ptk/reify ::finish-transform ptk/UpdateEvent (update [_ state] - (-> state - (update :workspace-local dissoc :transform :duplicate-move-started?) - (dissoc :workspace-selrect :workspace-wasm-modifiers))))) + (update state :workspace-local dissoc :transform :duplicate-move-started?)) + + ptk/EffectEvent + (effect [_ _ _] + ;; Clear the live-preview streams so React overlays unmount when + ;; the gesture ends. (These streams replaced the redux keys + ;; `:workspace-wasm-modifiers` / `:workspace-selrect`.) + (rx/push! ms/wasm-modifiers nil) + (rx/push! ms/workspace-selrect nil)))) ;; -- Resize -------------------------------------------------------- diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index c61307cf93..b4c511a7af 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -17,8 +17,41 @@ [app.main.data.helpers :as dsh] [app.main.data.workspace.tokens.selected-set :as dwts] [app.main.store :as st] + [app.main.streams :as ms] + [beicon.v2.core :as rx] [okulary.core :as l])) +(defn- raf-coalesced-atom + "Returns a CLJS atom mirroring the latest value pushed to source-stream, + but updated at most once per animation frame for non-nil pushes. + Components that `mf/deref` the atom re-render aligned to RAF instead + of per push, which matters for streams that fire at pointer-event + rate (drag / resize / rotate live preview). + + nil pushes apply synchronously and cancel any pending RAF: a + `finish-transform` clear must take effect in the same reducer pass + that commits the new shape state, otherwise the previous modifier + would double-apply to the already-committed shapes for one frame." + [source-stream] + (let [a (atom nil) + latest (volatile! nil) + raf-id (volatile! nil) + flush! (fn [] + (vreset! raf-id nil) + (reset! a @latest))] + (rx/sub! source-stream + (fn [v] + (vreset! latest v) + (if (nil? v) + (do + (when-some [id @raf-id] + (js/cancelAnimationFrame id) + (vreset! raf-id nil)) + (reset! a nil)) + (when (nil? @raf-id) + (vreset! raf-id (js/requestAnimationFrame flush!)))))) + a)) + ;; ---- Global refs (def route @@ -160,8 +193,14 @@ "All tokens related ephimeral state" (l/derived :workspace-tokens st/state)) +;; Live drag-gesture state. These are intentionally NOT in the Redux +;; state tree — they are short-lived UI values updated on every gesture +;; tick (drag/resize/rotate) and consumed by viewport overlays, the +;; sidebar, and on-canvas widgets. Writing to atoms instead of dispatching +;; ptk events skips the event-pipeline overhead and keeps temp state out +;; of app state. See `app.main.data.workspace.modifiers` for the writers. (def workspace-selrect - (l/derived :workspace-selrect st/state)) + (raf-coalesced-atom ms/workspace-selrect)) ;; WARNING: Don't use directly from components, this is a proxy to ;; improve performance of selected-shapes and @@ -382,7 +421,7 @@ (l/derived :workspace-wasm-editor-styles st/state)) (def workspace-wasm-modifiers - (l/derived :workspace-wasm-modifiers st/state)) + (raf-coalesced-atom ms/wasm-modifiers)) (def ^:private workspace-modifiers-with-objects (l/derived diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index d6ebf074b7..c069f66a19 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -22,6 +22,21 @@ (or ^boolean (kbd/keyboard-event? event) ^boolean (mse/mouse-event? event))) +;; --- Live preview streams (drag / resize / rotate) +;; +;; Carry the in-flight modifier tree and selrect during an interactive +;; transform. Replaces the previous Redux keys +;; `:workspace-wasm-modifiers` and `:workspace-selrect`, which churned +;; React state on every pointermove. Writes happen from +;; `app.main.data.workspace.modifiers/set-temporary-*`; reads happen +;; via the RAF-coalesced atoms in `app.main.refs`. + +(defonce wasm-modifiers + (rx/behavior-subject nil)) + +(defonce workspace-selrect + (rx/behavior-subject nil)) + ;; --- Derived streams (defonce ^:private pointer diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index c7413af9ac..23181957c0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -46,13 +46,10 @@ (mf/defc single-shape-options* {::mf/private true} - [{:keys [shape page-id file-id libraries] :rest props}] + [{:keys [shape page-id file-id libraries wasm-modifiers modifiers] :rest props}] (let [shape-type (dm/get-prop shape :type) shape-id (dm/get-prop shape :id) - wasm-modifiers (mf/deref refs/workspace-wasm-modifiers) - modifiers (mf/deref refs/workspace-modifiers) - shape (if (features/active-feature? @st/state "render-wasm/v1") (let [wasm-modifiers (into {} wasm-modifiers)] @@ -72,17 +69,23 @@ :bool [:> bool/options* {:shape shape :file-id file-id :page-id page-id}] nil))) -(mf/defc shape-options* +;; Throttled inner: re-renders at most every 100ms when its props change. +;; The parent (shape-options*) feeds wasm-modifiers / modifiers as props so +;; this throttle actually applies to drag-time modifier updates. +(mf/defc shape-options-throttled* {::mf/wrap [#(mf/throttle % 100)] ::mf/private true} - [{:keys [shapes shapes-with-children selected page-id file-id libraries]}] + [{:keys [shapes shapes-with-children selected page-id file-id libraries + wasm-modifiers modifiers]}] (if (= 1 (count selected)) [:> single-shape-options* {:page-id page-id :file-id file-id :libraries libraries :shape (first shapes) - :shapes-with-children shapes-with-children}] + :shapes-with-children shapes-with-children + :wasm-modifiers wasm-modifiers + :modifiers modifiers}] [:> multiple/options* {:shapes-with-children shapes-with-children :shapes shapes @@ -90,6 +93,21 @@ :file-id file-id :libraries libraries}])) +(mf/defc shape-options* + {::mf/private true} + [{:keys [shapes shapes-with-children selected page-id file-id libraries]}] + (let [wasm-modifiers (mf/deref refs/workspace-wasm-modifiers) + modifiers (mf/deref refs/workspace-modifiers)] + [:> shape-options-throttled* + {:shapes shapes + :shapes-with-children shapes-with-children + :selected selected + :page-id page-id + :file-id file-id + :libraries libraries + :wasm-modifiers wasm-modifiers + :modifiers modifiers}])) + (mf/defc specialized-panel* {::mf/private true} [{:keys [panel]}]