From e97fe1099a83f14c62fb60ae0f65e795c6e5a2c7 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 16 Aug 2026 17:42:52 +0200 Subject: [PATCH 1/2] perf: Drive descendants() with an explicit stack instead of a sync* generator --- .../lib/src/components/core/component.dart | 157 ++++++++++++++++-- .../flame/test/components/component_test.dart | 78 +++++++++ 2 files changed, 220 insertions(+), 15 deletions(-) diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index d4616bbaa25..30ae05b7907 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -428,22 +428,21 @@ class Component { /// traverse the component tree when required. This allows efficient chaining /// of various iterable methods, such as filtering, early stopping, folding, /// and so on -- see the documentation of the [Iterable] class for details. + /// The traversal is driven by a single explicit stack of children iterators, + /// so its cost does not grow with the depth of the tree beyond that stack. + /// + /// Modifying the tree while iterating may throw a + /// [ConcurrentModificationError], exactly as iterating over [children] does. + /// Call `toList()` first to iterate over a snapshot instead. Iterable descendants({ bool includeSelf = false, bool reversed = false, - }) sync* { - if (includeSelf && !reversed) { - yield this; - } - if (hasChildren) { - final childrenIterable = reversed ? children.reversed : children; - for (final child in childrenIterable) { - yield* child.descendants(includeSelf: true, reversed: reversed); - } - } - if (includeSelf && reversed) { - yield this; - } + }) { + return _DescendantsIterable( + this, + includeSelf: includeSelf, + reversed: reversed, + ); } /// This method first calls the passed handler on the leaves in the tree, @@ -1395,8 +1394,7 @@ class Component { /// order that `descendants(reversed: true, includeSelf: true)` would /// produce: leaves first, siblings in reverse order, ancestors after their /// subtrees. The snapshot allows [_remove] to run user callbacks that - /// mutate the tree while it walks the subtree, without allocating generator - /// frames per tree level the way [descendants] does. + /// mutate the tree while it walks the subtree. /// /// The [out] parameter is only used by the recursive calls, so that the /// whole subtree is collected into a single list. @@ -1513,6 +1511,135 @@ class Component { enum ChildrenChangeType { added, removed } +/// The lazy [Iterable] returned by [Component.descendants]. +class _DescendantsIterable extends Iterable { + _DescendantsIterable( + this._root, { + required this.includeSelf, + required this.reversed, + }); + + final Component _root; + final bool includeSelf; + final bool reversed; + + @override + Iterator get iterator => reversed + ? _ReversedDescendantsIterator(_root, includeSelf: includeSelf) + : _DescendantsIterator(_root, includeSelf: includeSelf); +} + +/// Walks a subtree depth-first in preorder: every component is emitted before +/// its children, and siblings are visited in the order of their container. +/// +/// The walk keeps one children iterator per level on an explicit stack, so the +/// only allocations are the stack itself and the container iterators, no +/// matter how deep the tree is. +class _DescendantsIterator implements Iterator { + _DescendantsIterator(this._root, {required bool includeSelf}) + : _pendingRoot = includeSelf { + _push(_root); + } + + final Component _root; + final List> _stack = []; + bool _pendingRoot; + Component? _current; + + @override + Component get current => _current!; + + void _push(Component component) { + final children = component._children; + if (children != null && children.isNotEmpty) { + _stack.add(children.iterator); + } + } + + @override + bool moveNext() { + if (_pendingRoot) { + _pendingRoot = false; + _current = _root; + return true; + } + final stack = _stack; + while (stack.isNotEmpty) { + final iterator = stack.last; + if (iterator.moveNext()) { + final component = iterator.current; + _current = component; + _push(component); + return true; + } + stack.removeLast(); + } + _current = null; + return false; + } +} + +/// Walks a subtree in the exact reverse of [_DescendantsIterator]: siblings +/// are visited in reverse container order, and every component is emitted +/// after its children. +/// +/// A parent is emitted when its children iterator is exhausted, so the walk +/// keeps the parent of every open iterator on a parallel stack. +class _ReversedDescendantsIterator implements Iterator { + _ReversedDescendantsIterator(this._root, {required bool includeSelf}) + : _pendingRoot = includeSelf { + _push(_root); + } + + final Component _root; + final List _parents = []; + final List> _stack = []; + bool _pendingRoot; + Component? _current; + + @override + Component get current => _current!; + + bool _push(Component component) { + final children = component._children; + if (children != null && children.isNotEmpty) { + _parents.add(component); + _stack.add(children.reversed.iterator); + return true; + } + return false; + } + + @override + bool moveNext() { + final stack = _stack; + while (stack.isNotEmpty) { + final iterator = stack.last; + if (iterator.moveNext()) { + final component = iterator.current; + if (!_push(component)) { + _current = component; + return true; + } + } else { + stack.removeLast(); + final parent = _parents.removeLast(); + if (!identical(parent, _root)) { + _current = parent; + return true; + } + } + } + if (_pendingRoot) { + _pendingRoot = false; + _current = _root; + return true; + } + _current = null; + return false; + } +} + /// Lifecycle futures for a group of components, mirroring the ones available on /// a single [Component]. extension ComponentIterableExtension on Iterable { diff --git a/packages/flame/test/components/component_test.dart b/packages/flame/test/components/component_test.dart index a6a12ea0ffd..36e002782aa 100644 --- a/packages/flame/test/components/component_test.dart +++ b/packages/flame/test/components/component_test.dart @@ -1679,6 +1679,84 @@ void main() { expect(componentE.visited, false); }); + testWithFlameGame( + 'descendants() of a component without children', + (game) async { + final leaf = Component(); + await game.world.ensureAdd(leaf); + + expect(leaf.descendants().toList(), isEmpty); + expect(leaf.descendants(reversed: true).toList(), isEmpty); + expect(leaf.descendants(includeSelf: true).toList(), [leaf]); + expect( + leaf.descendants(includeSelf: true, reversed: true).toList(), + [leaf], + ); + }, + ); + + testWithFlameGame( + 'descendants() order matches on a deep chain with side branches', + (game) async { + final world = game.world; + final expectedOrder = []; + Component parent = world; + for (var level = 0; level < 50; level++) { + final sideBranch = Component()..addToParent(parent); + final next = Component()..addToParent(parent); + expectedOrder + ..add(sideBranch) + ..add(next); + parent = next; + } + await game.ready(); + + expect(world.descendants().toList(), expectedOrder); + expect( + world.descendants(reversed: true).toList(), + expectedOrder.reversed.toList(), + ); + }, + ); + + testWithFlameGame( + 'descendants() iterator can be resumed after exhaustion', + (game) async { + final componentA = Component()..addToParent(game.world); + final componentB = Component()..addToParent(componentA); + await game.ready(); + + final iterator = game.world.descendants().iterator; + expect(iterator.moveNext(), isTrue); + expect(iterator.current, componentA); + expect(iterator.moveNext(), isTrue); + expect(iterator.current, componentB); + expect(iterator.moveNext(), isFalse); + expect(iterator.moveNext(), isFalse); + }, + ); + + testWithFlameGame( + 'descendants() throws when children are reordered during iteration', + (game) async { + final parent = Component(); + await game.world.ensureAdd(parent); + await parent.ensureAddAll([Component(), Component()]); + + expect( + () { + for (final component in game.world.descendants()) { + if (component == parent) { + parent.add(Component(priority: -1)); + game.update(0); + } + } + }, + throwsConcurrentModificationError, + ); + }, + ); + testWithFlameGame( 'firstChild returns the first child on the matching type', (game) async { From 39220108652c19e22b71c95fccc692dfa44141ce Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 28 Sep 2026 15:20:24 +0200 Subject: [PATCH 2/2] perf: Defer creating each children iterator until the caller has processed its parent --- .../lib/src/components/core/component.dart | 58 ++++++++++++------- .../flame/test/components/component_test.dart | 48 ++++++++++++++- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index 30ae05b7907..e2bf3a3fa14 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -1534,42 +1534,51 @@ class _DescendantsIterable extends Iterable { /// /// The walk keeps one children iterator per level on an explicit stack, so the /// only allocations are the stack itself and the container iterators, no -/// matter how deep the tree is. +/// matter how deep the tree is. The children iterator of an emitted component +/// is only created on the following [moveNext], after the caller has processed +/// the component itself, so a caller may still reorder the children of the +/// component it just received. class _DescendantsIterator implements Iterator { - _DescendantsIterator(this._root, {required bool includeSelf}) - : _pendingRoot = includeSelf { - _push(_root); - } + _DescendantsIterator(Component root, {required bool includeSelf}) + : _pendingRoot = includeSelf ? root : null, + _pendingPush = includeSelf ? null : root; + + /// The root, while it still has to be emitted. + Component? _pendingRoot; + + /// The component whose children iterator has not been pushed yet. + Component? _pendingPush; - final Component _root; final List> _stack = []; - bool _pendingRoot; Component? _current; @override Component get current => _current!; - void _push(Component component) { - final children = component._children; - if (children != null && children.isNotEmpty) { - _stack.add(children.iterator); - } - } - @override bool moveNext() { - if (_pendingRoot) { - _pendingRoot = false; - _current = _root; + final pendingRoot = _pendingRoot; + if (pendingRoot != null) { + _pendingRoot = null; + _pendingPush = pendingRoot; + _current = pendingRoot; return true; } + final pendingPush = _pendingPush; + if (pendingPush != null) { + _pendingPush = null; + final children = pendingPush._children; + if (children != null && children.isNotEmpty) { + _stack.add(children.iterator); + } + } final stack = _stack; while (stack.isNotEmpty) { final iterator = stack.last; if (iterator.moveNext()) { final component = iterator.current; _current = component; - _push(component); + _pendingPush = component; return true; } stack.removeLast(); @@ -1584,17 +1593,18 @@ class _DescendantsIterator implements Iterator { /// after its children. /// /// A parent is emitted when its children iterator is exhausted, so the walk -/// keeps the parent of every open iterator on a parallel stack. +/// keeps the parent of every open iterator on a parallel stack. The root's +/// children iterator is only created on the first [moveNext], so the tree is +/// not touched before iteration starts. class _ReversedDescendantsIterator implements Iterator { _ReversedDescendantsIterator(this._root, {required bool includeSelf}) - : _pendingRoot = includeSelf { - _push(_root); - } + : _pendingRoot = includeSelf; final Component _root; final List _parents = []; final List> _stack = []; bool _pendingRoot; + bool _started = false; Component? _current; @override @@ -1612,6 +1622,10 @@ class _ReversedDescendantsIterator implements Iterator { @override bool moveNext() { + if (!_started) { + _started = true; + _push(_root); + } final stack = _stack; while (stack.isNotEmpty) { final iterator = stack.last; diff --git a/packages/flame/test/components/component_test.dart b/packages/flame/test/components/component_test.dart index 36e002782aa..d9e1deb2989 100644 --- a/packages/flame/test/components/component_test.dart +++ b/packages/flame/test/components/component_test.dart @@ -1737,16 +1737,17 @@ void main() { ); testWithFlameGame( - 'descendants() throws when children are reordered during iteration', + 'descendants() throws when the container being walked is reordered', (game) async { final parent = Component(); await game.world.ensureAdd(parent); - await parent.ensureAddAll([Component(), Component()]); + final firstChild = Component(); + await parent.ensureAddAll([firstChild, Component()]); expect( () { for (final component in game.world.descendants()) { - if (component == parent) { + if (component == firstChild) { parent.add(Component(priority: -1)); game.update(0); } @@ -1757,6 +1758,47 @@ void main() { }, ); + testWithFlameGame( + 'descendants() allows reordering the children of the component that ' + 'was just emitted', + (game) async { + final parent = Component(); + await game.world.ensureAdd(parent); + await parent.ensureAddAll([Component(), Component()]); + final inserted = Component(priority: -1); + + final visited = []; + for (final component in game.world.descendants()) { + visited.add(component); + if (component == parent) { + parent.add(inserted); + game.update(0); + } + } + expect(visited, [parent, inserted, ...parent.children.skip(1)]); + }, + ); + + testWithFlameGame( + 'descendants() does not touch the tree before iteration starts', + (game) async { + final parent = Component(); + await game.world.ensureAdd(parent); + await parent.ensureAddAll([Component(), Component()]); + + for (final reversed in [false, true]) { + final iterator = parent.descendants(reversed: reversed).iterator; + parent.add(Component(priority: -1)); + game.update(0); + final visited = []; + while (iterator.moveNext()) { + visited.add(iterator.current); + } + expect(visited.length, parent.children.length); + } + }, + ); + testWithFlameGame( 'firstChild returns the first child on the matching type', (game) async {