🐛 Bound component sync retries per component

Count repeated processing of each component instead of total graph width.
A valid dependency graph with 102 outer components now settles and updates
every copy (F108). The non-settling cascade guard remains bounded.

The regression failed at 100 passes before the fix, then passed with 103
components. Common JVM/JS, frontend, backend and exporter suites pass.

Benchmark vs Phase 1e (lower quantile, ms):
world                 baseline     now   ratio
main-10                  1.790   0.993    0.55
main-100                15.415   9.038    0.59
main-1000              153.086  90.145    0.59
three-levels            35.215  18.989    0.54
chain                   68.931  35.045    0.51
library-500             83.083  85.830    1.03
many-shapes              4.837   3.344    0.69
variant-100             32.052  33.586    1.05
All worlds pass the 10% regression threshold.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-22 14:36:01 +02:00
parent c1f10147ab
commit 6002ca5274
2 changed files with 34 additions and 7 deletions

View File

@ -1431,7 +1431,7 @@
(maybe-sync sync-typographies? done? #(generate-sync-file % file-id :typographies asset-id library-id libraries current-file-id early-return?))))))
;; A guard against a sync cascade that never settles.
;; Bound repeated processing of a component, not the width of the graph.
(def ^:private max-sync-passes 100)
(defn changed-components
@ -1449,16 +1449,17 @@
`{:changes :data :component-ids :passes}`, with `component-ids` the
components synced."
[changes data file-id pending libraries current-file-id passes]
(loop [changes changes
data data
pending (vec pending)
synced #{}
passes passes]
(loop [changes changes
data data
pending (vec pending)
synced #{}
attempts {}
passes passes]
(cond
(empty? pending)
{:changes changes :data data :component-ids synced :passes passes}
(>= passes max-sync-passes)
(>= (get attempts (first pending) 0) max-sync-passes)
(throw (ex-info "Component sync did not settle"
{:type ::sync-not-settled
:pending pending
@ -1482,6 +1483,7 @@
data'
(into (subvec pending 1) more)
(conj synced component-id)
(update attempts component-id (fnil inc 0))
(inc passes))))))
(defn generate-sync-components-cascade

View File

@ -8,6 +8,7 @@
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.instances :as cli]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
@ -19,6 +20,30 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest a-wide-component-cascade-settles
(let [labels (mapv #(keyword (str "outer-" %)) (range 102))
file (reduce (fn [file label]
(let [root (keyword (str (name label) "-main"))
head (keyword (str (name label) "-nested"))
copy (keyword (str (name label) "-copy"))]
(-> file
(tho/add-frame root)
(thc/instantiate-component :inner head :parent-label root)
(thc/make-component label root)
(thc/instantiate-component label copy))))
(-> (thf/sample-file :file1)
(tho/add-simple-component :inner :inner-main :inner-child))
labels)
file (ths/update-shape file :inner-child :fills
(ths/sample-fills-color :fill-color "#FF0000"))
result (cll/generate-sync-components-cascade
(pcb/empty-changes) (:id file) #{(thi/id :inner)}
{(:id file) file} (:id file))
synced (thf/apply-changes file (:changes result))]
(t/is (= 103 (count (:component-ids result))))
(doseq [label labels]
(t/is (= "#FF0000" (tho/bottom-fill-color synced (keyword (str (name label) "-copy"))))))))
(defn- first-fill-color [file tag]
(-> (ths/get-shape file tag)
(:fills)