perf!: Replace the children OrderedSet with ComponentList and flatten the update traversal - #3960
Open
spydon wants to merge 20 commits into
Open
perf!: Replace the children OrderedSet with ComponentList and flatten the update traversal#3960spydon wants to merge 20 commits into
spydon wants to merge 20 commits into
Conversation
spydon
marked this pull request as ready for review
July 22, 2026 12:02
erickzanardo
approved these changes
Jul 22, 2026
spydon
requested review from
luanpotter,
renancaraujo,
ufrshubham and
wolfenrain
July 22, 2026 12:33
spydon
force-pushed
the
perf/component-set-backing
branch
from
July 22, 2026 13:50
3c6251e to
b083b5f
Compare
spydon
commented
Jul 23, 2026
This was referenced Aug 5, 2026
Merged
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 5, 2026 16:38
e187790 to
a55e3fb
Compare
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 5, 2026 20:18
a55e3fb to
d009510
Compare
5 tasks
spydon
added a commit
that referenced
this pull request
Aug 16, 2026
# Description <!-- End of exclude from commit message --> `processLifecycleEvents` now returns immediately when the queue is empty instead of allocating a set and a closure on every tick, the reorder-parents set is only allocated when a priority change is actually queued, and the blocked-set hash lookups are skipped while the set is empty (the common single-pass case). Extracted from #3960 so the data-structure change there stands alone (as requested in [this comment](#3957 (comment))). Behavior is unchanged; this only removes per-tick allocations and lookups from the game loop.
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 16, 2026 13:51
d009510 to
8684bd0
Compare
spydon
added a commit
that referenced
this pull request
Aug 16, 2026
# Description <!-- End of exclude from commit message --> Every `Component` eagerly allocated a `QueueList` for render contexts plus two debug-paint `ValueCache`s. The context stack is now a lazily created plain list (most components never provide or receive a render context) and the debug caches are `late final`, so plain components allocate none of them. Extracted from #3960 so the data-structure change there stands alone (as requested in [this comment](#3957 (comment))). Stacked on #3979.
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 16, 2026 15:08
8684bd0 to
f28286c
Compare
spydon
added a commit
that referenced
this pull request
Aug 16, 2026
…xplicit collection pass (#3981) # Description <!-- End of exclude from commit message --> The removal teardown and `propagateToChildren` walked the subtree through the recursive `descendants` sync* generator, allocating generator frames per tree level on every traversal. The teardown now collects the subtree into a local buffer (same leaves-first order) via `_collectDescendants` and iterates that snapshot, since `onRemove` callbacks may mutate the tree mid-walk. `propagateToChildren` instead walks the tree with direct recursion and unwinds as soon as a handler stops propagation, so it neither allocates a buffer nor visits more components than the lazy generator did. Event delivery (`deliverToComponents`) is routed through `propagateToChildren`, so tap, drag, and keyboard propagation benefit as well. The public `descendants()` method keeps its documented lazy semantics, since user code relies on early stopping and live iteration there. Extracted from #3960 so the data-structure change there stands alone (as requested in [this comment](#3957 (comment))).
spydon
added a commit
that referenced
this pull request
Aug 16, 2026
…list (#3983) # Description <!-- End of exclude from commit message --> `Sweep.update` re-sorted its items with a full `List.sort` and a closure comparator every tick, although the list is nearly sorted between ticks (hitboxes only move a little per frame); an insertion sort makes that near-linear. `Sweep.query` also pruned its active list with a searching `List.remove`; since active-list order does not matter, that is now an O(1) swap-remove. Extracted from #3960 so the data-structure change there stands alone (as requested in [this comment](#3957 (comment))). Stacked on #3982.
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 16, 2026 16:12
ad27f0a to
3276a7e
Compare
…ory with per-component comparator support
…ual-priority ordering
…y the CustomTraversal marker
spydon
force-pushed
the
perf/component-set-backing
branch
from
August 17, 2026 07:46
3276a7e to
cc84561
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Rebuilds the core of the Flame Component System for performance (implements #3957, together with the stacked PRs listed below).
Children live in a Flame-owned
ComponentListThe
ordered_setdependency is gone. Children are stored in a single flat array sorted by(priority, insertion order), and each component intrusively stores its container and slot index:removeandcontainsare O(1), with no hashing or tree walks;addis an O(1) append in the common case.nulltombstones that are compacted once per parent per tick, so removing k children among n costs O(k + n) instead of O(k*n).register<T>()/query<T>()cache surface is kept, and results are now always in priority order.Component.childrenFactoryis replaced by an overridablecreateComponentList(), which accepts an optionalComparator<Component>for custom orderings such as y-sort.Three backing designs were implemented and benchmarked before settling on this one; see the comparison in the issue.
The update pass runs over a flattened traversal list
Component.updateTreeis now@nonVirtual. Components that manage their own subtree traversal implement theCustomTraversalmarker interface and overrideComponent.updateSubtree; traversal mixins carry the marker viaimplements, so plainwith HasTimeScalekeeps working and implementations compose throughsuper.updateSubtree.The root updates everything through a flattened pre-order list that is rebuilt lazily, only on ticks where the tree structure changed (and then fused into that tick's update pass, so the rebuild costs no extra traversal).
CustomTraversalcomponents act as barriers that drive their own subtrees. The render pass intentionally stays recursive and virtual, sincerenderTreehas many legitimate overriders (decorators, visibility, snapshot, cameras).New
Component.updatePaused: pauses updates for a component and its whole subtree while rendering, event handling, and lifecycle processing continue; paused subtrees cost nothing per tick.Route.stopTime()is built on it.Relation to the allocation-hygiene PRs
The tangential hot-path improvements were extracted from this PR and reviewed independently, per the review feedback. Merged: #3978 (lifecycle early-out), #3979 (camera render closures; the decorator tear-off caching was deferred during its review), #3980 (lazy render contexts), #3981 (tree teardown and propagation), #3983 (sweep broadphase). #3982 (pointer-handler counters) was closed in favor of an upcoming events rework. This PR is rebased on main with all of that included and contains only the data-structure and traversal work. Golden tests pin lifecycle-event ordering, hit-test order, and equal-priority ordering across the rewrite.
Benchmarks (JIT, same machine, ms per run, lower is better)
Measured against current main, which already includes the merged hygiene PRs, so this table isolates this PR's own effect:
AOT device numbers are still pending; expect smaller (but same-ranked) multiples under AOT.
Checklist
docsand added dartdoc comments with///.examplesordocs.Breaking Change?
Migration instructions
childrenis now aComponentListinstead of anOrderedSet. The iterable surface,query<T>(),register<T>(), andreversed()are unchanged, so most code compiles as is. Theordered_setdependency is gone.Component.childrenFactoryis removed. OverridecreateComponentList()on the component instead; it accepts an optionalComparator<Component>for custom orderings (for example y-sort).Component.updateTreeis non-virtual. If you overrode it, addimplements CustomTraversaland overrideComponent.updateSubtreeinstead; callsuper.updateSubtree(dt)for the standard traversal.HasTimeScaleusage is unchanged (with HasTimeScalestill works; the mixin carries the marker itself).Route.stopTime()now setsupdatePausedinstead of zeroingtimeScale: while stopped,timeScalekeeps its previous value (a slow-motion factor survives a stop/resume cycle), and assigning a newtimeScaleno longer resumes a stopped route; useresumeTime()orupdatePaused = false. Pending lifecycle events on a stopped route now still complete.childrenwhile iterating it now tolerates removals and tail appends; only position-shifting operations (mid-list insertion, reorder, compaction) throwConcurrentModificationError.Related Issues
Closes #3957