Skip to content

perf!: Rebuild the FCS core: ComponentList children, allocation hygiene, and flattened update traversal - #3960

Open
spydon wants to merge 26 commits into
mainfrom
perf/component-set-backing
Open

perf!: Rebuild the FCS core: ComponentList children, allocation hygiene, and flattened update traversal#3960
spydon wants to merge 26 commits into
mainfrom
perf/component-set-backing

Conversation

@spydon

@spydon spydon commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

Rebuilds the core of the Flame Component System for performance (implements #3957).

Children live in a Flame-owned ComponentList

The ordered_set dependency 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:

  • remove and contains are O(1), with no hashing or tree walks; add is an O(1) append in the common case.
  • Removals leave null tombstones that are compacted once per parent per tick, so removing k children among n costs O(k + n) instead of O(k*n).
  • A priority change triggers at most one stable sort per parent per tick, skipped entirely when nothing actually reordered.
  • The register<T>()/query<T>() cache surface is kept, and results are now always in priority order.
  • Component.childrenFactory is replaced by an overridable createComponentList(), which accepts an optional Comparator<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.updateTree is now @nonVirtual. Components that manage their own subtree traversal implement the CustomTraversal marker interface and override Component.updateSubtree; traversal mixins carry the marker via implements, so plain with HasTimeScale keeps working and implementations compose through super.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). CustomTraversal components act as barriers that drive their own subtrees. The render pass intentionally stays recursive and virtual, since renderTree has 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.

Per-frame allocations and other hot-path work removed

  • processLifecycleEvents returns immediately on an empty queue and allocates nothing per tick.
  • Decorator render closures (previously one super.renderTree tear-off per PositionComponent per frame), the camera's renderWorld closure, render-context stacks, and the debug caches are cached or lazily created.
  • The removal teardown collects into a reusable buffer instead of allocating generator frames per tree level.
  • The root counts mounted pointer-event handler components, so containsEventHandlerAt answers without a tree walk for games without handlers.
  • The Sweep broadphase re-sorts its nearly-sorted items with an insertion sort and prunes its active list with a swap-remove.

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)

Benchmark main this PR speedup
Update wide tree (10k x 1) 8.09 3.44 2.4x
Update nested tree (1k x 10) 16.96 3.97 4.3x
Update deep tree (100 levels) 18.14 3.36 5.4x
Update barrier tree (10% time-scaled) 20.45 4.47 4.6x
Render wide tree (10k x 1) 12.81 4.64 2.8x
Render nested tree (1k x 10) 23.05 8.25 2.8x
Render deep tree (100 levels) 27.44 6.89 4.0x
Lifecycle churn (100/tick, 1k pop) 5.10 2.54 2.0x
Lifecycle churn (100/tick, 10k pop) 12.56 8.63 1.5x
Mass add/remove (1k per cycle) 3.43 1.31 2.6x
Priority change (1 child per parent) 74.46 6.92 10.8x
Priority change (y-sort, all children) 22.61 5.82 3.9x
Type-query churn (2 registered queries) 6.95 5.16 1.3x
Game-like update pass 185.29 95.31 1.9x
Hit test + delivery (cached) 90.93 39.06 2.3x
Flat collision detection 9.24 3.35 2.8x
Nested collision detection 15.41 3.52 4.4x

AOT device numbers are still pending; expect smaller (but same-ranked) multiples under AOT.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Migration instructions

  • children is now a ComponentList instead of an OrderedSet. The iterable surface, query<T>(), register<T>(), and reversed() are unchanged, so most code compiles as is. The ordered_set dependency is gone.
  • Component.childrenFactory is removed. Override createComponentList() on the component instead; it accepts an optional Comparator<Component> for custom orderings (for example y-sort).
  • Component.updateTree is non-virtual. If you overrode it, add implements CustomTraversal and override Component.updateSubtree instead; call super.updateSubtree(dt) for the standard traversal. HasTimeScale usage is unchanged (with HasTimeScale still works; the mixin carries the marker itself).
  • Route.stopTime() now sets updatePaused instead of zeroing timeScale: while stopped, timeScale keeps its previous value (a slow-motion factor survives a stop/resume cycle), and assigning a new timeScale no longer resumes a stopped route; use resumeTime() or updatePaused = false. Pending lifecycle events on a stopped route now still complete.
  • Mutating children while iterating it now tolerates removals and tail appends; only position-shifting operations (mid-list insertion, reorder, compaction) throw ConcurrentModificationError.

Related Issues

Closes #3957

@spydon
spydon marked this pull request as ready for review July 22, 2026 12:02
Comment thread packages/flame/lib/src/components/core/component_list.dart Outdated
spydon added 24 commits July 22, 2026 15:47
Comment thread doc/flame/migration.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the Flame Component System core more efficient

2 participants