Eva Marco 1962860f04
🐛 Reduce thumbnail re-renders and report throttled update loops (#12610)
* 🐛 Stop thumbnail subscribers re-rendering on every store change

A thumbnail loaded from the server keeps a media id as :uri, and
workspace-thumbnail-by-id resolves it into a new map on each read. The
ref compared values with identical?, so every root frame, every
component in the assets panel and the interactions layer re-rendered
on every store change, mouse moves included. Compare by value instead.

This does not loop by itself, but it adds work to every commit and
brings other update loops closer to React's limit.

Relates to #12580

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

* ✨ Report update loops throttled by the update scheduler

When the scheduler throttles an update loop, it only logs a console
warning, so a loop that no longer crashes leaves no trace. Send an
error report too, with the events in flight and the last events, at
most once per report window.

Relates to #12580

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

* 🐛 Report throttled update loops as unhandled exceptions

The throttled update loop report used the handled-exception event, which
only lands in the audit log: error reports and alerts read
unhandled-exception and exception-page. A throttled loop is an
application defect (its cause has :type :internal), and the point of the
report is to find those loops, so it is now an unhandled exception.

AI-assisted-by: claude-opus-5-5
2026-10-29 09:02:55 +01:00

52 lines
2.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.main.refs-test
(:require
[app.common.uuid :as uuid]
[app.main.refs :as refs]
[app.main.store :as st]
[cljs.test :as t :include-macros true]))
(def ^:private resolved-uri? @#'refs/resolved-uri?)
(t/deftest resolved-uri-detects-blob-and-data-uris
(t/is (true? (resolved-uri? "blob:http://localhost/thumb")))
(t/is (true? (resolved-uri? "data:image/png;base64,iVBOR"))))
(t/deftest resolved-uri-rejects-plain-media-ids
(t/is (false? (resolved-uri? "275c9c60-7f5e-4a2b-9f1c-2d3e4f5a6b7c")))
(t/is (false? (resolved-uri? ""))))
(t/deftest resolved-uri-tolerates-non-string-uris
;; Thumbnail :uri values arrive from the server over transit, so
;; media-ids decode to UUID objects instead of strings. These must
;; fall through to resolve-media instead of raising a TypeError
;; (str.lastIndexOf is not a function) that crashes the workspace.
(t/is (false? (resolved-uri? (uuid/next))))
(t/is (false? (resolved-uri? nil)))
(t/is (false? (resolved-uri? 42))))
(t/deftest thumbnail-ref-ignores-unrelated-state-changes
;; A thumbnail loaded from the server keeps a media id as :uri, and
;; the ref resolves it to a new map on each read. Subscribers (root
;; frames, assets) must not re-render on every unrelated store change.
(let [object-id (str (uuid/next))
ref (refs/workspace-thumbnail-by-id object-id)
calls (atom 0)]
(swap! st/state assoc-in [:thumbnails object-id] {:uri (uuid/next) :rendered-at nil})
(add-watch ref ::probe (fn [_ _ _ _] (swap! calls inc)))
(try
(swap! st/state assoc ::probe (uuid/next))
(t/is (zero? @calls) "unrelated change does not notify")
(swap! st/state assoc-in [:thumbnails object-id :uri] (uuid/next))
(t/is (= 1 @calls) "a new thumbnail uri notifies")
(finally
(remove-watch ref ::probe)
(swap! st/state #(-> %
(dissoc ::probe)
(update :thumbnails dissoc object-id)))))))