Skip to content

Commit

Permalink
perf: Switch from forEach to regular for-loops for about 30% improvem…
Browse files Browse the repository at this point in the history
…ent in raw update performance (#3472)

Replaces uses of `.forEach()` with regular `for` loops. This has
significant impact on performance in hot paths such as
`Component.updateTree()` and `Component.renderTree()`.

![Updating
Components](https://github.com/user-attachments/assets/e183ce4c-5b37-45d9-ad81-a0a35719e0dd)

In the graph above, you see 50 runs of
`benchmark/update_components_benchmark.dart`. The forEach results are
blue, the for-loop results are green. I could see this effect after just
replacing the `forEach` calls in `component.dart`. Data
[here](https://docs.google.com/spreadsheets/d/e/2PACX-1vRk_yGmLN6o0oqSUWDBh7ODx7B8EIToeahZcZBS3VKHX8AbEnmrgmEqDt98cZLoBjIKQX3MlOc0XwsP/pubhtml).

> Aside for posterity: `for i in {1..50}; do flutter test
benchmark/main.dart --no-pub -r silent 2>/dev/null >>
benchmarks_for_loop.txt; done`, then get the data from the text file.

I went ahead and replaced additional `forEach` calls elsewhere in the
engine codebase, but there was no additional effect on the benchmark.
Still, I kept those changes in. I only replaced `forEach` in places that
seemed relatively hot (e.g. `ComponentSet.reorder()`). There are more
`forEach` calls in the codebase but those seem fine to me as they aren't
likely to be called too often.

It should be noted that I needed to update the benchmark to add children
to the components. Every `_BenchmarkComponent` now has 10 children. This
feels a bit more realistic use of the framework than having a flat array
of components with no children. By changing the benchmark code in this
way, I made it a bit slower, so I'm not sure if the effect will be seen
in the CI/CD.

I also tried whether the change will have effect on my game's benchmark
(which is a lot more involved and uses `flutter driver` to test the
whole game in AOT mode). For the game, the effect is negligible but that
was kind of expected since my game spends a significant amount of its
CPU time on AI, raycasting, smoke simulation and drawVertices, none of
which really depend on the speed of the engine `update()` mechanism.

---------

Co-authored-by: Erick <[email protected]>
  • Loading branch information
filiph and erickzanardo authored Feb 5, 2025
1 parent c3f9925 commit 9891f4e
Show file tree
Hide file tree
Showing 17 changed files with 91 additions and 37 deletions.
8 changes: 8 additions & 0 deletions packages/flame/benchmark/update_components_benchmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flame/game.dart';
const _amountComponents = 1000;
const _amountTicks = 2000;
const _amountInputs = 500;
const _amountChildren = 10;

class UpdateComponentsBenchmark extends AsyncBenchmarkBase {
final Random random;
Expand Down Expand Up @@ -65,6 +66,13 @@ class _BenchmarkComponent extends PositionComponent {

_BenchmarkComponent(this.id);

@override
Future<void> onLoad() async {
for (var i = 0; i < _amountChildren; i++) {
await add(PositionComponent(position: Vector2(i * 2, 0)));
}
}

void input({
required int xDirection,
required bool doJump,
Expand Down
4 changes: 3 additions & 1 deletion packages/flame/lib/src/camera/viewport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ abstract class Viewport extends Component
}
onViewportResize();
if (hasChildren) {
children.forEach((child) => child.onParentResize(_size));
for (final child in children) {
child.onParentResize(_size);
}
}
}

Expand Down
10 changes: 6 additions & 4 deletions packages/flame/lib/src/collisions/hitboxes/shape_hitbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ mixin ShapeHitbox on ShapeComponent implements Hitbox<ShapeHitbox> {
_validAabb = false;
onAabbChanged?.call();
};
ancestors(includeSelf: true).whereType<PositionComponent>().forEach((c) {
_transformAncestors.add(c.transform);
c.transform.addListener(_transformListener);
});
final positionComponents =
ancestors(includeSelf: true).whereType<PositionComponent>();
for (final ancestor in positionComponents) {
_transformAncestors.add(ancestor.transform);
ancestor.transform.addListener(_transformListener);
}

if (shouldFillParent) {
_parentSizeListener = () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
List<ShapeHitbox>? ignoreHitboxes,
List<RaycastResult<ShapeHitbox>>? out,
}) sync* {
out?.forEach((e) => e.reset());
if (out != null) {
for (final result in out) {
result.reset();
}
}
var currentRay = ray;
for (var i = 0; i < maxDepth; i++) {
final hasResultObject = (out?.length ?? 0) > i;
Expand Down
25 changes: 19 additions & 6 deletions packages/flame/lib/src/components/core/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,12 @@ class Component {
/// priority of the direct siblings, not the children or the ancestors.
void updateTree(double dt) {
update(dt);
_children?.forEach((c) => c.updateTree(dt));
final children = _children;
if (children != null) {
for (final child in children) {
child.updateTree(dt);
}
}
}

/// This method will be invoked from lifecycle if [child] has been added
Expand All @@ -535,7 +540,12 @@ class Component {

void renderTree(Canvas canvas) {
render(canvas);
_children?.forEach((c) => c.renderTree(canvas));
final children = _children;
if (children != null) {
for (final child in children) {
child.renderTree(canvas);
}
}

// Any debug rendering should be rendered on top of everything
if (debugMode) {
Expand Down Expand Up @@ -868,11 +878,14 @@ class Component {
@mustCallSuper
@internal
void handleResize(Vector2 size) {
_children?.forEach((child) {
if (child.isLoading || child.isLoaded) {
child.onGameResize(size);
final children = _children;
if (children != null) {
for (final child in children) {
if (child.isLoading || child.isLoaded) {
child.onGameResize(size);
}
}
});
}
}

FutureOr<void> _startLoading() {
Expand Down
4 changes: 3 additions & 1 deletion packages/flame/lib/src/components/core/component_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class ComponentSet extends QueryableOrderedSet<Component> {
final elements = toList();
// bypass the wrapper because the components are already added
super.clear();
elements.forEach(super.add);
for (final element in elements) {
super.add(element);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ class ComponentTreeRoot extends Component {
@internal
void handleResize(Vector2 size) {
super.handleResize(size);
_queue.forEach((event) {
for (final event in _queue) {
if ((event.kind == _LifecycleEventKind.add) &&
(event.child!.isLoading || event.child!.isLoaded)) {
event.child!.onGameResize(size);
}
});
}
}

@mustCallSuper
Expand Down
9 changes: 6 additions & 3 deletions packages/flame/lib/src/components/mixins/has_paint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ class _MultiPaintOpacityProvider<T extends Object> implements OpacityProvider {
maxOpacity = max(target.getOpacity(paintId: paintId), maxOpacity);
}
if (includeLayers) {
target.paintLayersInternal?.forEach(
(paint) => maxOpacity = max(paint.color.a, maxOpacity),
);
final targetLayers = target.paintLayersInternal;
if (targetLayers != null) {
for (final paint in targetLayers) {
maxOpacity = max(paint.color.a, maxOpacity);
}
}
}

return maxOpacity;
Expand Down
5 changes: 3 additions & 2 deletions packages/flame/lib/src/components/position_component.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:math' as math;

import 'dart:ui' hide Offset;

import 'package:collection/collection.dart';
Expand Down Expand Up @@ -195,7 +194,9 @@ class PositionComponent extends Component
set size(Vector2 size) {
_size.setFrom(size);
if (hasChildren) {
children.forEach((child) => child.onParentResize(_size));
for (final child in children) {
child.onParentResize(_size);
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/flame/lib/src/components/text_box_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
lines.clear();
var lineHeight = 0.0;
final maxBoxWidth = _fixedSize ? width : _boxConfig.maxWidth;
text.split(' ').forEach((word) {
for (final word in text.split(' ')) {
final wordLines = word.split('\n');
final possibleLine =
lines.isEmpty ? wordLines[0] : '${lines.last} ${wordLines[0]}';
Expand All @@ -192,7 +192,7 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
} else {
lines.addAll(wordLines);
}
});
}
_totalLines = lines.length;
_lineHeight = lineHeight;
size = _recomputeSize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ class DoubleTapDispatcher extends Component with HasGameReference<FlameGame> {
}

void _onDoubleTapUp(DoubleTapEvent event) {
_components.forEach((component) => component.onDoubleTapUp(event));
for (final component in _components) {
component.onDoubleTapUp(event);
}
_components.clear();
}

void _onDoubleTapCancel(DoubleTapCancelEvent event) {
_components.forEach((component) => component.onDoubleTapCancel(event));
for (final component in _components) {
component.onDoubleTapCancel(event);
}
_components.clear();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Polygon extends Shape {
var nInteriorAngles = 0;
var nExteriorAngles = 0;
var previousEdge = _edges.last;
_edges.forEach((edge) {
for (final edge in _edges) {
final crossProduct = edge.cross(previousEdge);
previousEdge = edge;
// A straight angle counts as both internal and external
Expand All @@ -75,7 +75,7 @@ class Polygon extends Shape {
if (crossProduct <= 0) {
nExteriorAngles++;
}
});
}
if (nInteriorAngles < nExteriorAngles) {
_reverseVertices();
_initializeEdges();
Expand Down Expand Up @@ -116,7 +116,9 @@ class Polygon extends Shape {
Aabb2? _aabb;
Aabb2 _calculateAabb() {
final aabb = Aabb2.minMax(_vertices.first, _vertices.first);
_vertices.forEach(aabb.hullPoint);
for (final vertex in _vertices) {
aabb.hullPoint(vertex);
}
return aabb;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/flame/lib/src/game/flame_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ class FlameGame<W extends World> extends ComponentTreeRoot
// there is no way to explicitly call the [Component]'s implementation,
// we propagate the event to [FlameGame]'s children manually.
handleResize(size);
children.forEach((child) => child.onParentResize(size));
for (final child in children) {
child.onParentResize(size);
}
}

/// Ensure that all pending tree operations finish.
Expand Down
4 changes: 3 additions & 1 deletion packages/flame/lib/src/geometry/line_segment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ class LineSegment {
};
if (overlaps.isNotEmpty) {
final sum = Vector2.zero();
overlaps.forEach(sum.add);
for (final overlap in overlaps) {
sum.add(overlap);
}
return [sum..scale(1 / overlaps.length)];
}
}
Expand Down
13 changes: 9 additions & 4 deletions packages/flame/lib/src/layers/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ abstract class Layer {

@mustCallSuper
void render(Canvas canvas, {double x = 0.0, double y = 0.0}) {
if (_picture == null) {
final picture = _picture;
if (picture == null) {
return;
}

canvas.save();
canvas.translate(x, y);

preProcessors.forEach((p) => p.process(_picture!, canvas));
canvas.drawPicture(_picture!);
postProcessors.forEach((p) => p.process(_picture!, canvas));
for (final p in preProcessors) {
p.process(picture, canvas);
}
canvas.drawPicture(picture);
for (final p in postProcessors) {
p.process(picture, canvas);
}
canvas.restore();
}

Expand Down
4 changes: 2 additions & 2 deletions packages/flame/lib/src/parallax.dart
Original file line number Diff line number Diff line change
Expand Up @@ -468,15 +468,15 @@ class Parallax {
final _delta = Vector2.zero();

void update(double dt) {
layers.forEach((layer) {
for (final layer in layers) {
layer.update(
_delta
..setFrom(baseVelocity)
..multiply(layer.velocityMultiplier)
..scale(dt),
dt,
);
});
}
}

/// Note that this method only should be used if all of your layers should
Expand Down
8 changes: 6 additions & 2 deletions packages/flame/lib/src/text/elements/group_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ class GroupElement extends BlockElement {

@override
void translate(double dx, double dy) {
children.forEach((child) => child.translate(dx, dy));
for (final child in children) {
child.translate(dx, dy);
}
}

@override
void draw(Canvas canvas) {
children.forEach((child) => child.draw(canvas));
for (final child in children) {
child.draw(canvas);
}
}

@override
Expand Down

0 comments on commit 9891f4e

Please sign in to comment.