Skip to content

perf: faster first frames, playback and loading, with less memory - #1

Merged
dXnSix merged 53 commits into
mainfrom
refactor/perofrmance-improvements
Oct 28, 2026
Merged

dXnSix merged 53 commits into
mainfrom
refactor/perofrmance-improvements

Conversation

@dXnSix

@dXnSix dXnSix commented Oct 27, 2026

Copy link
Copy Markdown
Owner

A 50-iteration improvement loop on playback performance, memory and allocation cost, and time to first frame: 41 changes kept, 9 tried and reverted. Each experiment is recorded under .agentx/experiments/2026-10-27-performance-improvements/.

Results

vector-animation-check bench artifacts-temp/corpus (the 53 corpus files, release build, median of 3):

Measure Before After Change
Time to first frame (sum) 253.5 ms 93.8 ms −63%
— read 73.9 ms 62.6 ms −15%
— compile 179.6 ms 31.2 ms −83%
Allocations at load 2.87 M 0.77 M −73%
Bytes allocated at load 1,168 MB 382 MB −67%
Held after load 99.7 MB 67.4 MB −32%
frame_at a frame (sum) 354 µs 201 µs −43%
Allocations a frame 7,789 2,908 −63%
Engine-thread work a frame 833 µs 476 µs −43%
Graph breakpoints 115,936 53,982 −53%
Graph nodes 5,026 4,972 −1%
Canvas ops (save/restore aside) 45,331 44,936 −1%

An app's imported compiled scene (Source::Json) reads 30% faster: it is no longer written back to text and parsed again.

What changed

  • Player load: the plan is compiled once on load, for the view, and the first drawing takes it (it was compiled twice). The loaded scene is shared with what plays it instead of cloned.
  • Plan compile: moving layers' matrices are multiplied directly instead of sampling probe frames. Outline switches are bisected on the easing alone. Plan images use the fast PNG encoder. Sampled parts are joined without copying them, which was quadratic. A still scene is recognized without sampling its frames. Graph nodes use a fast hash.
  • Graph size: a repeating track (looping CSS animations over their common timeline) is one repetition read modulo its period. Sampled values keep only the samples a line does not stand for; a gradient's reach is sampled within 0.1%. Single-valued tracks are constants.
  • Canvases: saves that keep nothing are dropped, and consecutive still translations are combined.
  • Core frame_at: fill and stroke take the shape's path rather than copies, there is no merged copy of a single outline, and rectangles, ellipses and stars allocate their outlines once.
  • Renderer: box blurs run over pixel chunks and blur columns a row at a time; blurs are written back in a flat pass. Layers composite only where they draw, and normal-blend layers use a direct source-over loop that computes exactly what tiny-skia's pipeline computes.
  • Importers:
    • SVG: keyframe values are parsed once, CSS rules are indexed by id, class or tag, class matching allocates nothing, and cascaded declarations are shared.
    • Lottie: pointer keys are escaped only when needed, baked paths are moved instead of cloned, merge buffers are presized, and a JSON value is copied only where expressions are baked into it.
    • Compiled scenes: parsed in one pass. New APIs compiled::from_value and dxn_vector_animation_lottie::read_value read JSON values directly.
  • Measurement: new vector-animation-check bench <dir> [--runs n] [--files] [--values] with a counting allocator.

For review

  • Output unchanged:
    • The reference renderer's 4,629 frames of the corpus (4 fps, 160 px) are byte-identical to main.
    • Each corpus file's compiled scene JSON and warnings are byte-identical to main.
    • The corpus plan-versus-reference test (DXN_CORPUS) gives the same per-file results.
    • The direct compositing loop has a unit test against tiny-skia's draw_pixmap over all 8.4 million (channel, alpha, destination) combinations, at four opacities.
  • Behaviour changes:
    • The player's choice between the native graph and sampling the whole scene now comes from the compile for the view (as measured, or at the scene's own size) at the display's pixel ratio. It used to come from a separate compile at the scene's size and 1 pixel a point.
    • JSON-value sources are not checked against Limits::max_bytes, since the value is already in memory. Node, keyframe, depth and point limits still apply through Scene::check and the converters.
  • Tests: new test groups cover each new mapping: repeating_tracks, sampled_values, constant_tracks, canvas_saves, still_translations, still_parts and json_values in the player, plus new tests in css_selectors and compiled_checks.
  • Validation: all of AGENTS.md's commands pass: fmt, clippy for the core and the player, cargo test --all-features, the player's tests, and the wasm check.
  • Not measured: NSH's own evaluation of the graph on platform UI threads. Fewer breakpoints and ops should help there.

dXnSix added 30 commits October 27, 2026 21:17
dXnSix added 23 commits October 27, 2026 22:39
A part rastered frame by frame is made less often where its rasters take
long, each held a few frames; the playback's end then showed a frame held
from before it. The end is now drawn at its own frame (the held-rasters
test failed on slow CI machines).
A shadow cast from past the view (seen through see-through content, or
whose offset moves) is cast by its content moved past the view's left
edge, on a canvas reaching over it. The canvas reached over the casters
only, so their blur was cut where it ended: a straight edge in the
shadow's halo. It now reaches as far as the blur does.
Groups moved by the groups they are in became layers only where the
playback was short enough (10 s) for their matrix to be sampled at every
frame; they now are up to 20 s. Moving canvases are sized to where they
draw wherever their bounds (sampled 15 times a second) can be sampled
throughout, not only within 10 s. Over the corpus, the operations drawn
again each frame fall from 23,835 to 20,328 (welcome-star's 3,883 to 430).
@dXnSix
dXnSix merged commit 6f1e587 into main Oct 28, 2026
6 checks passed
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.

1 participant