Commit ede9d7f
bodymovin
Nnnnn improve load performance (#14149) 1d51731ef0
* perf(runtime): artboard instances replay their source's dependency order
Artboard::instance() clones m_Objects 1:1 by index and the clone's
dependency graph is structurally identical to the source's, so the
topological order sortDependencies() computes is the same permutation
the source already computed. Every instance was re-walking that graph
from scratch.
The source now records its order once as (objectIndex, helperSlot)
pairs and instances replay it in O(n). helperSlot addresses the two
Components that are real nodes in the graph but never appear in
m_Objects: Shape's by-value PathComposer and TextStyle's lazily created
TextVariationHelper (both report coreType ComponentBase, which is how
they surface). Slot 0 is the object itself.
Building the recipe needs no reverse Component* -> index map:
sortDependencies() has already stamped each component with its position
via m_GraphOrder (single writer), so one walk of m_Objects scatters
every entry into place. That detail is what makes this worth doing --
the editor re-decodes source artboards on every runtime-file
regeneration, so the recipe never amortizes and an unordered_map build
ate most of the saving.
Safety: every slot starts at a sentinel and duplicate claims are
refused, so unless the filled count exactly equals the order length the
recipe is marked unusable and the artboard sorts as before. Instances
whose object count differs from their source, or whose helper slot is
empty, also fall back.
Measured on an editor runtime-file regeneration for a 54 artboard /
72k object file with 44 mounted instances: sortDependencies across
instancing 52.6ms -> 5.6ms, instancing initialize ~140ms -> ~79ms,
mounted-artboard reload 272ms -> ~210ms.
Verified by computing both orders and comparing on every instance
across the full unit-test corpus: 4810 instances, 0 mismatches, 47
fallbacks (all artboards whose source has an empty object list).
* feat(runtime): re-import a single artboard in place
Groundwork for incremental runtime-file updates in the editor, which today
throws away the whole rive::File and every ArtboardInstance on each
regeneration and rebuilds them.
An ArtboardInstance shares its source's animations and state machines by
pointer, so it is tied to that one artboard rather than to the file as a
whole. That means replacing a single source artboard invalidates only its
own instances -- instances of every other artboard stay valid, provided the
File itself survives. This adds the two pieces needed to exploit that:
- File::artboardByteRange(index) reports where an artboard's objects sit in
the stream it was imported from. An artboard is a contiguous run (its
Artboard object, its components, then its animations and state machines),
recorded during read().
- File::replaceArtboard(index, bytes) re-imports one artboard from that run.
The run carries no Backboard, assets or view models of its own, so the
import stack is pre-seeded with a BackboardImporter primed from the file's
existing artboards and assets; the new artboard's asset and nested-artboard
references then resolve against what is already loaded. The artboard keeps
its index, so everything referring to it by index stays correct, and every
ArtboardReferencer pointing at that index is re-pointed at the replacement
rather than left dangling.
Nothing calls replaceArtboard yet; existing behaviour is unchanged.
read() is now a thin wrapper over readObjects(), which both paths share.
readObjects() resolves the import stack itself rather than leaving that to
the caller: TextAssetImporter retains the address of a local (inBandContent)
and dereferences it during resolve(), so the resolve has to happen while that
local is still alive.
Caller contract: release every ArtboardInstance of an artboard before
replacing it. Not yet covered: a replacement introducing a property key
absent from the header's table of contents, and view-model/data-bind state
inside a replaced artboard.
Tests cover contiguity of the ranges across several files, replacing an
artboard with its own bytes (another artboard's pre-existing instance still
works), replacing it with a *different* artboard's bytes (the content really
changes), referencers following the replacement, and bad input leaving the
file untouched.
* perf(editor): re-import only changed artboards on runtime regeneration
Every edit to a component artboard rebuilt the whole runtime file: a full
export, a full native decode, and re-instancing every mounted artboard.
Behind ENABLE_INCREMENTAL_RUNTIME_REGEN, a regeneration that qualifies now
keeps the live rive::File and re-imports just the artboards that changed,
using File::replaceArtboard from 755049bc16.
Measured on a 54 artboard / 72k object file, comparing passes within one
session: ~1183ms -> ~481ms, a 59% reduction. Re-importing one artboard costs
~6ms where decoding the file cost ~620ms.
How it fits together:
- The exporter records each artboard's byte extent while writing and rebases
them at stitch time onto the same coordinate space File::artboardByteRange
reports on import, so a single artboard's run can be handed back to the
runtime. A round-trip test asserts the two sides agree index for index; it
fails on a one-byte discrepancy.
- completeChanges() accumulates the changed artboards instead of collapsing
them to a bool. The per-artboard flags it builds from are cleared every
pass while regeneration is debounced, so the flags alone cannot say what a
pass needs to cover. The set holds every changed artboard, not just the
ones that trigger a regeneration: a full export picks the others up as a
side effect, so anything doing less has to know about them.
- ExportIndexSignature fingerprints the global index namespaces (artboard,
asset, view model, enum, converter counters plus the manifest string and
path tables) and the property keys written. Artboard bytes encode indices
into all of these, so they are only interchangeable between two exports if
none of them moved. Property keys are compared as a subset: a key the
loaded header lacks cannot be decoded, extra keys are harmless.
- The gate refuses -- and falls back to the full path -- on a global change,
an empty changed set, a moved namespace, a missing byte range, or a failed
re-import. It is a necessary condition, not a sufficient one: it says the
namespaces did not move, not that their contents match. Each pass logs its
verdict and the running hit rate, which is ~60% over three sessions.
Ordering matters in three places, each of which would otherwise be a bug:
the gate needs the new export's signature, so tearing down view models and
the export context is deferred until after the verdict; replaceArtboard
requires every instance of that artboard released first, which means both
disposeMountedArtboard and _clearHostedAnimations, since the nested animation
instances hold the last reference to the artboard instance; and the splice
re-mounts as part of itself, because callers of genRuntimeFile that are not
_updateRuntime do not re-mount, and doing it twice would discard the
instances just built.
Deliberately conservative: it still re-mounts every host, so the saving is
the decode rather than the re-instancing. spliceRemount is now ~210ms of the
~294ms splice; narrowing it to the changed artboards plus their transitive
nesters is the next step and needs the NestedInput.inputId and cross-artboard
event index hazards handled.
Verified visually in the editor on nested components.
The FeatureFlags getter is hardcoded to true in this commit rather
than reading the flag, so the splice path is on for every build of
it. That is a mistake, not the intent; the following commit restores
the lookup.
* perf(editor): re-mount only the artboards a splice invalidates
The incremental path re-imported only the changed artboards but still
released and rebuilt every mounted artboard afterwards, which left the
re-instancing cost untouched -- spliceRemount was ~210ms of a ~294ms splice.
Instancing an artboard clones its nested artboards too, so an instance of A
embeds an instance of everything A nests, recursively. Replacing C therefore
invalidates instances of C, of B which nests C, and of A which nests B --
and nothing else. artboardsAffectedByReplacing walks the nesting graph
upward to a fixed point; release and re-mount now cover only hosts that
mount something in that set, and only those script inputs.
The direction is easy to get backwards: nesting does not propagate downward.
Replacing A leaves instances of B alone, because those are built from B's
source, not A's. Rebuilding them would discard exactly the instances this
change exists to keep.
Measured on the same file, comparing a full rebuild against a spliced pass
in one session: 1440ms -> ~185ms. spliceRemount 210ms -> 0.8ms and
spliceRelease 60-70ms -> 0.3ms, with affectedArtboards=2 (the edited
artboard plus the one nesting it) rather than all 44. The native side is now
~7ms of the pass; the export is the remaining 96%.
Tests cover a lone artboard, one level of nesting, transitivity through two
levels, that nesting does not propagate downward, that unrelated artboards
are excluded, and that a nesting cycle terminates -- files do contain cycles,
which is why checkNestedArtboardDependencies detects them.
Still behind ENABLE_INCREMENTAL_RUNTIME_REGEN, dark by default. Verified
visually in the editor on nested components.
* fix(editor): keep spliced artboards pointing at the right assets
An artboard re-imported on its own kept the image its source used to have,
which showed up as an artboard nesting it rendering a stale image -- the
top-level artboard looked right because it draws from the core, while the
nested one renders through the runtime file.
referencedFileAssets collects into a Set, so its order was the order assets
were discovered: those reached through a referencer first, in referencer
order, then the rest in core order. Re-pointing one Image at a different
asset moves assets between those two groups and renumbers the whole list.
Artboard bytes index into that list, so bytes exported against one numbering
and spliced into a file built on another resolve every asset reference to the
wrong asset. Assets are now ordered by their own ids, so the numbering
depends only on which assets are exported, never on how they were found.
Two further holes of the same shape, where the export changes outside any
artboard's bytes and a splice would not pick it up:
- Image, audio and library asset changes now mark a global change as well as
the artboards drawing them. An asset's bytes live in the file-wide asset
stream, so re-exporting only those artboards keeps serving the old asset.
- The splice gate compares the exported asset identities against the live
file's and refuses with asset-order-moved if they differ. The tracked order
only advances on a full decode, since a splice leaves the file's assets
alone. ExportIndexSignature compares counts, which cannot see a reorder or
a swap that keeps the count -- it is a necessary condition, not a
sufficient one.
Also narrows four onGlobalChange sites that were global only because their
types extend Core rather than Component and so have no artboard getter:
TransitionComparator (found through the condition referencing it, as the
comment there proposed), ListenerInputType, and the keyboard and semantic
inputs targeting one. Each falls back to a global change when its chain does
not resolve -- narrowing too far fails silently, as a stale preview rather
than an error.
And skips a trailing regeneration pass when nothing is outstanding. It exists
to catch changes that landed mid-export, but the pass that just finished
cleared the accumulators, so with nothing since it rebuilt the file for
nothing. Only triggers the accumulators model are skippable: a view-model
addition reaches _reloadViewModelInstances without setting either, so
_pendingPassMustRun forces those through.
Tests cover asset order surviving a re-pointed referencer (verified to fail
without the ordering fix), an image asset change forcing a full rebuild, the
four narrowed routes plus both fallbacks, the trailing-pass predicate, and a
replaced artboard still resolving its file assets to the same asset objects
the file already holds.
* chore(editor): make the incremental regen flag a real feature flag
Two problems with how ENABLE_INCREMENTAL_RUNTIME_REGEN was wired.
The FeatureFlags getter returned a hardcoded true with the flag lookup
commented out beneath it -- a leftover from testing that made the splice
path unconditionally on. It reads the flag again.
And the flag itself was declared with environmentVariable, so turning it on
meant a --dart-define and a rebuild, and it could not be enabled per user or
team. It now carries ordinary env settings: on in development and uat, off
in early access, absent for prod.
* feedback and fixes
* feedback1 parent 07fcb0a commit ede9d7f
2 files changed
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
Submodule rive-runtime updated 10 files
- .rive_head+1-1
- include/rive/artboard.hpp+47-4
- include/rive/component.hpp+8-1
- include/rive/file.hpp+46
- include/rive/importers/backboard_importer.hpp+6
- include/rive/text/text_style.hpp+11
- src/artboard.cpp+159-2
- src/file.cpp+203-4
- src/importers/backboard_importer.cpp+5
- tests/unit_tests/runtime/artboard_byte_range_test.cpp+345
0 commit comments