You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An investigation into the performance of the Flame Component System (update/render traversal, lifecycle processing, priority changes, hit testing) found that most of the framework overhead came from the children container and from per-frame allocation churn, not from game logic. The parent/children tree structure and the developer experience were to be preserved (this was not an ECS rewrite), but breaking changes were acceptable.
Implementations considered
Children container. Three full replacements for the SplayTreeMap<num, Set<Component>>-backed OrderedSet were implemented, each passing the entire test suite, and benchmarked head-to-head (full comparison table):
Flat sorted array (chosen): one contiguous list sorted by (priority, insertion order), with the container and slot index stored intrusively on the component, null tombstones for removals (compacted once per tick), and a stable sort on priority changes that is skipped when nothing actually reordered. Best or within noise on nearly every benchmark, and no pathological case.
Intrusive doubly-linked list (bench/linked-list): _prevSibling/_nextSibling pointers, O(1) link/unlink. Fastest pure update walk, but pointer-chasing loses on rendering (contiguous pointer arrays prefetch, dependent loads serialize), priority changes cost about 2.7x more than the array, and any structural mutation must invalidate live iterators.
Priority-bucketed arrays (bench/priority-buckets): one dense array per distinct priority, so a priority change is a bucket move instead of a sort. Middling everywhere and pathological under y-sort, where 1000 distinct priorities degenerate into 1000 single-element buckets and it lands slower than the old OrderedSet.
Update traversal. Keeping the recursive updateTree was compared against a root-owned flattened pre-order list. The flat list won once two problems were solved: overridden traversals being silently skipped (solved by making updateTree non-virtual behind a CustomTraversal marker + updateSubtree), and rebuild cost under churn (solved by structure versioning with lazy rebuilds that are fused into the same tick's update pass).
Considered and rejected:
Flattening the render pass: rendering is hierarchical through canvas transforms, clips, decorators, and render contexts, and renderTree has many legitimate overriders; the render pass stays recursive and virtual. Render-order work continues as the RenderLayer proposal in Add a RenderLayer system for global render ordering #3967.
Rewriting engine hit testing as an explicit-stack traversal: componentsAtLocation is an overridden extension point (CameraComponent, Route, ecosystem), so a parallel path would silently skip those overrides. Root-level counters of mounted pointer-event handlers attack the same per-event cost safely instead.
First-class timeScale on Component: rarely used; it stays in the opt-in HasTimeScale mixin, which now composes through updateSubtree.
ComponentList (the flat sorted array) replaces OrderedSet; the dependency is removed. query<T>()/register<T>() are kept, childrenFactory is replaced by an overridable createComponentList() with an optional comparator for custom orderings such as y-sort.
The update pass runs over the root-owned flattened list; CustomTraversal components are barriers that drive their own subtrees via updateSubtree. A new updatePaused pauses a subtree's updates entirely (and now backs Route.stopTime()).
Allocation hygiene throughout the hot paths: no per-frame decorator or camera closures, lazy render contexts and debug caches, generator-free removal teardown, an early-out for lifecycle processing and for hit tests in games without pointer handlers, and a near-linear insertion sort in the Sweep broadphase.
Golden tests pin lifecycle ordering, hit-test order, and equal-priority ordering across the rewrite. Migration notes are in #3960 and in the migration guide.
Benchmarks
Container candidates, measured with each backing as a drop-in children container (JIT, ms per run, lower is better; the old OrderedSet on then-main as baseline):
Benchmark
OrderedSet
Flat sorted array
Linked list
Priority buckets
Update wide tree (10k x 1)
8.09
5.84
3.93
6.49
Render wide tree (10k x 1)
12.81
8.78
10.94
9.37
Update nested tree (1k x 10)
16.96
7.41
6.11
7.30
Render nested tree (1k x 10)
23.05
10.15
11.21
11.49
Update deep tree (100 levels)
18.14
6.33
5.65
7.31
Lifecycle churn (100/tick, 10k pop)
12.56
7.57
6.81
8.07
Mass add/remove (1k per cycle)
3.43
2.41
2.38
2.47
Priority change (1 child per parent)
74.46
5.99
15.75
9.65
Priority change (y-sort, all children)
22.61
5.95
8.99
31.10
Game-like update pass
185.29
111.24
122.71
115.43
Final state of #3960 (container plus flattened traversal plus allocation hygiene) versus main:
Numbers are flutter test JIT on one desktop machine, for relative comparison; AOT device numbers are pending and should show smaller but same-ranked multiples.
Motivation
An investigation into the performance of the Flame Component System (update/render traversal, lifecycle processing, priority changes, hit testing) found that most of the framework overhead came from the children container and from per-frame allocation churn, not from game logic. The parent/children tree structure and the developer experience were to be preserved (this was not an ECS rewrite), but breaking changes were acceptable.
Implementations considered
Children container. Three full replacements for the
SplayTreeMap<num, Set<Component>>-backedOrderedSetwere implemented, each passing the entire test suite, and benchmarked head-to-head (full comparison table):(priority, insertion order), with the container and slot index stored intrusively on the component,nulltombstones for removals (compacted once per tick), and a stable sort on priority changes that is skipped when nothing actually reordered. Best or within noise on nearly every benchmark, and no pathological case.bench/linked-list):_prevSibling/_nextSiblingpointers, O(1) link/unlink. Fastest pure update walk, but pointer-chasing loses on rendering (contiguous pointer arrays prefetch, dependent loads serialize), priority changes cost about 2.7x more than the array, and any structural mutation must invalidate live iterators.bench/priority-buckets): one dense array per distinct priority, so a priority change is a bucket move instead of a sort. Middling everywhere and pathological under y-sort, where 1000 distinct priorities degenerate into 1000 single-element buckets and it lands slower than the oldOrderedSet.Update traversal. Keeping the recursive
updateTreewas compared against a root-owned flattened pre-order list. The flat list won once two problems were solved: overridden traversals being silently skipped (solved by makingupdateTreenon-virtual behind aCustomTraversalmarker +updateSubtree), and rebuild cost under churn (solved by structure versioning with lazy rebuilds that are fused into the same tick's update pass).Considered and rejected:
renderTreehas many legitimate overriders; the render pass stays recursive and virtual. Render-order work continues as theRenderLayerproposal in Add a RenderLayer system for global render ordering #3967.componentsAtLocationis an overridden extension point (CameraComponent,Route, ecosystem), so a parallel path would silently skip those overrides. Root-level counters of mounted pointer-event handlers attack the same per-event cost safely instead.timeScaleonComponent: rarely used; it stays in the opt-inHasTimeScalemixin, which now composes throughupdateSubtree.Decision
Implemented in #3960:
ComponentList(the flat sorted array) replacesOrderedSet; the dependency is removed.query<T>()/register<T>()are kept,childrenFactoryis replaced by an overridablecreateComponentList()with an optional comparator for custom orderings such as y-sort.CustomTraversalcomponents are barriers that drive their own subtrees viaupdateSubtree. A newupdatePausedpauses a subtree's updates entirely (and now backsRoute.stopTime()).Sweepbroadphase.Golden tests pin lifecycle ordering, hit-test order, and equal-priority ordering across the rewrite. Migration notes are in #3960 and in the migration guide.
Benchmarks
Container candidates, measured with each backing as a drop-in
childrencontainer (JIT, ms per run, lower is better; the oldOrderedSeton then-main as baseline):Final state of #3960 (container plus flattened traversal plus allocation hygiene) versus main:
Numbers are
flutter testJIT on one desktop machine, for relative comparison; AOT device numbers are pending and should show smaller but same-ranked multiples.Related: #1938 (first item).