Skip to content

Commit ede9d7f

Browse files
author
bodymovin
committed
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 * feedback
1 parent 07fcb0a commit ede9d7f

2 files changed

Lines changed: 2 additions & 2 deletions

File tree

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1d567b30ab397b0d033b5e74bb5211a33472b805
1+
1d51731ef0d3db92dd3b7d9972725ba0738cbda7

0 commit comments

Comments
 (0)