From d22bff7654fdd190bac8d1b160788216caffc654 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Jul 2026 19:53:57 -0400 Subject: [PATCH] optimization: perf improvements --- tl/src/tl/routes.cljs | 20 ++++++++- tl/src/tl/views.cljs | 99 +++++++++++++++++++++++++++---------------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/tl/src/tl/routes.cljs b/tl/src/tl/routes.cljs index ad62a90..b2bf1fe 100644 --- a/tl/src/tl/routes.cljs +++ b/tl/src/tl/routes.cljs @@ -68,7 +68,7 @@ (defonce ^:private last-replaced (atom nil)) (def ^:private last-url-key "tl/last-url") -(defn replace-project-state! [{:keys [id stack playhead]}] +(defn- do-replace! [{:keys [id stack playhead]}] (when id (let [url (project-url id stack playhead)] (when (not= url @last-replaced) @@ -77,6 +77,24 @@ ;; remember where we were so an installed PWA can come back here (.setItem js/localStorage last-url-key url))))) +;; Playback moves the playhead ~60fps, and set-playhead syncs it into the URL's +;; ?f= param — but rebuilding the URL + history.replaceState + a synchronous +;; localStorage write every frame hitches the main thread. Trailing-throttle so +;; the deep-link / PWA-resume state stays roughly current (leading edge keeps +;; navigation snappy) without per-frame writes. +(defonce ^:private replace-timer (atom nil)) +(defonce ^:private pending-args (atom nil)) + +(defn replace-project-state! [args] + (reset! pending-args args) + (when-not @replace-timer + (do-replace! args) ; leading edge: immediate + (reset! replace-timer + (js/setTimeout (fn [] + (reset! replace-timer nil) + (do-replace! @pending-args)) ; trailing: final position + 400)))) + (defn- standalone? "Running as an installed PWA (added to homescreen), not a browser tab." [] diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 65500e4..92cbebc 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -359,17 +359,30 @@ :ref (fn [n] (when n (reset! video-el n)))}] [drawing-layer]])))) +(defn- build-label-map + "{mark-id → \"Track (occurrence)\"} for every segment, in one pass over `segs`." + [scene segs] + (into {} + (mapcat (fn [[t tsegs]] + (map-indexed (fn [i {m :mark}] + [m (str (get-in scene [:tracks t :name] (name t)) " (" (inc i) ")")]) + (sort-by (comp first :local) tsegs))) + (group-by :track segs)))) + +;; clip-label ran group-by + sort over ALL segments on every call, and it's called +;; once per clip per render → O(segs²) on each timeline re-render (zoom, jump, …). +;; The labels are stable per `segs`, so memoize the whole map on segs identity +;; (::segments returns a stable ref until scene/context changes) → O(1) lookups. +(defonce ^:private label-cache (atom {:segs nil :map nil})) + (defn- clip-label "Track-local clip label for the content segment whose :mark is `mid`." [scene segs mid] - (or (some (fn [[t tsegs]] - (some (fn [[i {m :mark}]] - (when (= m mid) - (str (get-in scene [:tracks t :name] (name t)) - " (" (inc i) ")"))) - (map-indexed vector (sort-by (comp first :local) tsegs)))) - (group-by :track segs)) - "clip")) + (let [c @label-cache + m (if (identical? segs (:segs c)) + (:map c) + (:map (reset! label-cache {:segs segs :map (build-label-map scene segs)})))] + (get m mid "clip"))) (defn- clip-at "Describe local frame `lf` the way the annotation editor does: the underlying @@ -449,6 +462,43 @@ (.addEventListener js/document "mousemove" move) (.addEventListener js/document "mouseup" up))) +(defn- playhead-fx + "Renders nothing. Isolates the playhead-driven side effects — positioning the + (imperative) playhead line and the follow-scroll during playback — so a moving + playhead re-renders only this empty component, not the whole timeline DOM (all + clips + per-clip clip-label) every animation frame. The timeline's hiccup never + depended on the playhead; only these effects did." + [fps zoom segs anns scene] + (let [playhead @(rf/subscribe [::subs/playhead]) + playing? @(rf/subscribe [::subs/playing?])] + (if playing? + (r/after-render + (fn [] + (follow! fps zoom playhead) + ;; first load: if playhead was deep-linked (from query param), scroll to it once + (when (and (nil? @scrolled-on-load?) (> playhead 0) (seq segs)) + (reset! scrolled-on-load? true) + (scroll-to-seg-track! segs playhead)) + ;; clip-following: only while the playhead is inside a bar of an annotation + ;; that opts in (:meta :scroll-to) and is not hidden — scroll each clip into + ;; view as the playhead crosses onto a new bar. Track [ann-id bar-idx] to + ;; avoid scrolling within the same bar. + (when-let [current-bar (some (fn [a] + (when (and (not (get-in (get-in scene [:groups (:id a)]) [:meta :hidden])) + (ann-scroll-to? (get-in scene [:groups (:id a)])) + (some (fn [[lo hi]] (<= lo playhead hi)) (:bars a))) + [(:id a) (first (keep-indexed (fn [j [lo hi]] + (when (<= lo playhead hi) j)) + (:bars a)))])) + anns)] + (when (not= current-bar @played-bar) + (reset! played-bar current-bar) + (scroll-to-seg-track! segs playhead))))) + (do (reset! played-bar nil) + (reset! scrolled-on-load? nil))) + (r/after-render #(position-playhead! fps zoom playhead)) + nil)) + (defn timeline [] (let [content (atom nil) lanes-scroll (atom nil) @@ -462,8 +512,6 @@ tracks @(rf/subscribe [::subs/tracks]) anns @(rf/subscribe [::subs/annotations]) thumbs @(rf/subscribe [::subs/thumbnails]) - playhead @(rf/subscribe [::subs/playhead]) - playing? @(rf/subscribe [::subs/playing?]) authoring? (some? @(rf/subscribe [::subs/draft-group])) linking @(rf/subscribe [::subs/linking]) len @(rf/subscribe [::subs/length]) @@ -471,33 +519,8 @@ lane-h (max 18 (* 18 (count anns))) tracks-h (* row-h (count tracks)) track-y (into {} (map-indexed (fn [i t] [(:id t) i]) tracks))] - (if playing? - (r/after-render - (fn [] - (follow! fps zoom playhead) - ;; first load: if playhead was deep-linked (from query param), scroll to it once - (when (and (nil? @scrolled-on-load?) (> playhead 0) (seq segs)) - (reset! scrolled-on-load? true) - (scroll-to-seg-track! segs playhead)) - ;; clip-following: only while the playhead is actually inside a bar of - ;; an annotation that opts in (:meta :scroll-to) and is not hidden — - ;; scroll each of its clips into view as the playhead crosses onto a new bar (discontinuity). - ;; Track [ann-id bar-idx] to avoid scrolling within the same bar. - (when-let [current-bar (some (fn [a] - (when (and (not (get-in (get-in scene [:groups (:id a)]) [:meta :hidden])) - (ann-scroll-to? (get-in scene [:groups (:id a)])) - (some (fn [[lo hi]] (<= lo playhead hi)) (:bars a))) - [(:id a) (first (keep-indexed (fn [j [lo hi]] - (when (<= lo playhead hi) j)) - (:bars a)))])) - anns)] - (when (not= current-bar @played-bar) - (reset! played-bar current-bar) - (scroll-to-seg-track! segs playhead))))) - (do (reset! played-bar nil) - (reset! scrolled-on-load? nil))) - (r/after-render #(position-playhead! fps zoom playhead)) [:div.timeline {:class (when-not @tracks-open? "tracks-collapsed")} + [playhead-fx fps zoom segs anns scene] [:div.playhead.timeline-playhead {:ref (fn [n] (reset! playhead-el n))} [:div.playhead-handle]] [:div.timeline-head @@ -567,7 +590,9 @@ :on-scroll #(let [sl (.. % -target -scrollLeft)] (when @lanes-scroll (set! (.-scrollLeft @lanes-scroll) sl)) - (position-playhead! fps zoom playhead))} + ;; deref at scroll time (not render) so this + ;; handler needs no render-time playhead dep + (position-playhead! fps zoom @(rf/subscribe [::subs/playhead])))} [:div.content.track-content {:ref (fn [n] (reset! content n)) :on-mouse-down #(scrub! @content fps zoom %) :style {:width width :height tracks-h}}