Skip to content

perf(render): deserialize a scene's children once instead of once per frame - #419

Merged
LeadcodeDev merged 1 commit into
mainfrom
perf/deserialize-scene-children-once
Sep 29, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
perf/deserialize-scene-children-once

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #159.

Measured first, because the issue asked for that

prepare_scene ran on the per-frame path with no cache, although its result
depends only on the scene. Every frame cloned the whole serde_json subtree of
every child and deserialized it into ChildComponent — an untagged enum with 57
variants, so serde re-parses the same object once per failed variant before it
lands.

Timing the call for every frame of a render, release build:

Scenario before after
examples/mega-showcase.json — 1236 frames, 9 scenes, 31 children 144 ms 46 ms
50 nodes in one scene, 1200 frames 388 ms 3.2 ms

The 46 ms that remain on mega-showcase are the nine first-time deserializations —
the work that actually has to happen.

End to end this is within noise on both (9.0 s and 8.5 s): the redundancy was
1.6 % and 4.5 % of a render, spread across workers. That is the order of magnitude
the issue asked for before deciding how much to spend, and it is why this is the
contained fix rather than the restructuring the issue sketched. It grows with node
count, which is where it will matter.

Shape

The memo is a OnceLock on the Scene, not a keyed cache. So it has no
identity problem and no invalidation to get wrong: it dies with the scenario.

rustmotion-core cannot name ChildComponent, which lives a crate above it, so
the slot holds Arc<dyn Any + Send + Sync> and prepare_scene is the typed
accessor — and its only writer, which is what makes the downcast total.

Checked before relying on it: the studio rebuilds a whole ResolvedScenario on
every optimistic edit and swaps the Arc, so its scenes are new objects with
fresh slots. include.rs's &mut Scene runs at load, before any render. No path
mutates scene.children in place afterwards.

The warning the issue mentions

deserialize_children emits a stderr line per unreadable child. That fired once
per frame, riding on the same repetition. It is now once per scene, pinned by
a_child_that_cannot_be_read_is_reported_once_rather_than_once_per_frame.

Tests

Four, on Arc::ptr_eq — which is the direct statement of "deserialized once".
Reverting to an uncached Arc::new(deserialize_children(scene)) fails two of them.

Gate

cargo fmt --all --check clean · cargo clippy --workspace --all-targets --features rustmotion/studio -D warnings clean · cargo test --workspace 1802 passed.

… frame

prepare_scene ran on the per-frame path and had no cache, although its
result depends only on the scene. Every frame cloned the whole
serde_json subtree of every child and deserialized it into
ChildComponent -- an untagged enum with 57 variants, so serde re-parses
the same object once per failed variant before it lands.

Measured on two scenarios, timing the call for every frame of a render:

  mega-showcase, 1236 frames, 9 scenes    144ms -> 46ms
  50 nodes in one scene, 1200 frames      388ms -> 3.2ms

The 46ms that remain on mega-showcase are the nine first-time
deserializations, which is the work that actually has to happen.

End-to-end this is within noise on both (9.0s and 8.5s): the redundancy
was 1.6% and 4.5% of a render, spread across workers. The issue asked
for the order of magnitude before deciding how much to spend, and this
is it -- so the fix is the contained one, not the restructuring.

The memo is a OnceLock on the Scene rather than a keyed cache, so it has
no identity problem and no invalidation to get wrong: it dies with the
scenario. rustmotion-core cannot name ChildComponent, which lives a
crate above it, so the slot holds Arc<dyn Any + Send + Sync> and
prepare_scene is the typed accessor -- and its only writer, which is
what makes the downcast total.

The studio rebuilds a whole ResolvedScenario on every optimistic edit
and swaps the Arc, so its scenes are new objects with fresh slots; there
is no path that mutates scene.children in place after load.

deserialize_children's stderr warning for an unreadable child rode on
the repetition. It is now emitted once per scene.
@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Sep 29, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 29, 2026
@LeadcodeDev
LeadcodeDev merged commit 1a0dac3 into main Sep 29, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the perf/deserialize-scene-children-once branch September 29, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The scene's JSON tree is re-deserialized on every frame (no cache between frames)

1 participant