Skip to content

perf: Cache the rotation and skip redundant work in Transform2D - #4024

Open
spydon wants to merge 3 commits into
mainfrom
perf/transform2d-cached-rotation
Open

perf: Cache the rotation and skip redundant work in Transform2D#4024
spydon wants to merge 3 commits into
mainfrom
perf/transform2d-cached-rotation

Conversation

@spydon

@spydon spydon commented Aug 28, 2026

Copy link
Copy Markdown
Member

Description

Makes Transform2D cheaper on its hot paths without changing its API or behavior:

  • The sine and cosine of the angle are cached and only recomputed when the angle actually changes. Previously every matrix recalculation called cos and sin, even when only the position, scale or offset had changed, which is by far the most common case (a component that moves every frame).
  • The transformMatrix setter no longer throws away the matrix it was just given. It set _recalculate = false before updating the scale and position, whose listeners flipped it back to true, so the next read recomposed the matrix from the decomposed values. It now keeps the assigned matrix, and it updates the scale and position with one setValues call each instead of two separate x/y assignments, halving the number of notifications going out to their listeners.
  • setFrom copies the fields directly (including the cached rotation) instead of going through the public setters, and Transform2D.copy is built on top of it.
  • localToGlobal and globalToLocal read each matrix cell and point coordinate once instead of repeatedly, and hasReflection no longer goes through .sign.

A transform2d_benchmark.dart suite is added to packages/flame/benchmark so that these paths can be tracked going forward. Every case runs 1000 iterations per run.

Benchmarks

Times are per run (1000 transforms or point pairs), median of three rounds.

AOT (flutter run --release on an arm64 Android emulator, API 34, Apple Silicon host):

Case Before After Change
position update + matrix read 250 µs 182 µs -27%
angle update + matrix read 153 µs 158 µs ±0 (noise)
localToGlobal + globalToLocal 65 µs 68 µs ±0 (noise)
transformMatrix setter + matrix read 715 µs 504 µs -30%
setFrom + matrix read 540 µs 398 µs -26%

JIT (flutter test benchmark/transform2d_benchmark.dart, macOS, Apple Silicon):

Case Before After Change
position update + matrix read 353 µs 300 µs -15%
angle update + matrix read 201 µs 204 µs ±0 (noise)
localToGlobal + globalToLocal 49 µs 48 µs ±0 (noise)
transformMatrix setter + matrix read 896 µs 563 µs -37%
setFrom + matrix read 662 µs 593 µs -10%

The angle-update case is expected to stay flat since changing the angle still has to recompute the trigonometry; the point is that all other property changes no longer pay for it. The remaining cost of the position-update case is dominated by the ChangeNotifier round trip from NotifyingVector2 into Transform2D, which is out of scope here since the Vector2 implementation is about to change.

One thing worth noting for future work on this class: an earlier iteration of this PR inlined the recalculation check directly into localToGlobal/globalToLocal via a private _recalculateMatrix() method, and that made the point-conversion case about 50% slower under the JIT (76 µs vs 49 µs) while being neutral in AOT, presumably from the cold call site being inlined and bloating the hot methods. Going through the transformMatrix getter, as the code did before, keeps both compilers happy.

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.

https://claude.ai/code/session_01H6f6E3vr3DM6iAWskqXpQT

spydon added 3 commits August 28, 2026 16:47
Cache the sine and cosine of the angle so that matrix recalculation
after position, scale or offset changes no longer recomputes them,
keep the assigned matrix instead of recomputing it after the
transformMatrix setter, batch the scale and position notifications in
that setter, and avoid repeated reads in the point conversion methods.
Adds a Transform2D micro-benchmark.

Claude-Session: https://claude.ai/code/session_01H6f6E3vr3DM6iAWskqXpQT
Keeps the transform matrix bit-identical to the previous implementation,
which the snapshot golden depends on.

Claude-Session: https://claude.ai/code/session_01H6f6E3vr3DM6iAWskqXpQT
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.

2 participants