feat(core): native update scheduler and commit hook (phase 0) - #11414
Draft
edusperoni wants to merge 6 commits into
Draft
edusperoni wants to merge 6 commits into
edusperoni wants to merge 6 commits into
Conversation
Pin the native call sequences, their order and the <name>Change timing that the native-update rework has to keep, plus a hot-path timing probe for I6.
The six copies of the suspend-or-apply block now share one routing helper, and the native default capture exists in one place instead of eight.
|
View your CI Pipeline Execution ↗ for commit 6aad60a
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
commit: |
farfromrefug
reviewed
Sep 4, 2026
| export type NativeUpdateProperty = Property<any, any> | CssProperty<any, any> | CssAnimationProperty<any, any>; | ||
|
|
||
| /** What a commit applies: a property, through its `[setNative]`, or an aggregate invalidation. */ | ||
| export type NativeUpdateTarget = NativeUpdateProperty | Invalidation; |
Collaborator
There was a problem hiding this comment.
i have doubt about the name. Target often refer to an View in Nativescript. at first i could not find where the "property" array was in NativeUpdateBatch. Could we rename it to something more obvious?
A resume now builds a batch of everything dirty on the node and hands it to commitNativeUpdates, which a class can override to order its own native writes.
batch()/begin()/end() coalesce the writes made to each node into one commit, and flushNativeUpdates pushes what is pending, optionally past the loaded hold.
A property can name aggregate invalidations it raises; the node's commit runs each one's handler once, after the properties, however many of them raised it.
A node using the default commit hook with no aggregate invalidations pending cannot reach the batch, so the commit sweeps the dirty set directly instead of building one.
edusperoni
force-pushed
the
feat/native-updates-phase0
branch
from
September 5, 2026 17:27
71cdb2f to
6aad60a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
What is the current behavior?
A property setter is the native call. Each
Property/CssProperty/CssAnimationProperty/InheritedCssPropertysetter carries its own copy of the "suspended → record in_suspendedUpdates, else[setNative](value)" block (six copies). The first native pass happens atonLoadedin the order properties were first set on that instance; afterwards every set is one synchronous native call. There is no way for a widget to see "these properties changed together", no previous committed value, no public batching primitive (_batchUpdateis private and per node), and no way for a flavor to coalesce writes across nodes.What is the new behavior?
Phase 0 of the native-updates redesign: additive plumbing with no behavior change. The new API is implemented on top of the existing storage (
_suspendNativeUpdatesCountas the holds,_suspendedUpdatesas the dirty set), so every existing code path produces the same native calls, in the same order, with the same<name>Changetiming. Six commits, each green on its own; review commit by commit:test(core): add native-update oracle specs— golden[setNative]/<name>Changesequences for every setter kind, load/unload/reload, batch, reset, shorthand, inheritance, keyframes. These specs are byte-identical from commit 1 to HEAD and pass at every commit.refactor(core): unify native property routing— the six copies collapse into one routing function;applyPendingNativeSetters/applyAllNativeSettersshare the default-capture helper.feat(core): add commitNativeUpdates hook and NativeUpdateBatch—ViewBase.commitNativeUpdates(batch)is the ordering hook (override, apply what the class cares about,superapplies the rest).onResumeNativeUpdates→initNativeViewbuilds the batch and calls it.NativeUpdateBatch:entries/has/apply/skip/previous/isMount/children(an entry is a property or an aggregateInvalidation; the name avoids "target", which means a view elsewhere in core). Previous values are recorded only for nodes that can read them back (a class overriding the hook, or a property declaringinvalidates), so default nodes allocate nothing extra in a batch or across unload/reload.feat(core): add NativeUpdates scheduler and flushNativeUpdates—NativeUpdates.batch()/begin()/end()/flush()coalesce writes per node across nodes (implemented as_batchUpdatesemantics per touched node);modeis'sync'only.ViewBase.flushNativeUpdates({ subtree, force });forcepushes to an existing native view while the node is not loaded and leaves the holds in place.feat(core): add invalidates option to properties—PropertyOptions.invalidates?: Invalidation[]on every property kind; a node handles an aggregate with[invalidation.apply](batch). No core property declares one yet.perf(core): commit without a batch for nodes that cannot observe one— a node whose class uses the default hook and has no pending aggregate invalidation cannot observe aNativeUpdateBatch, so its commit runs the existing sweeps directly (same order) instead of building one.Fast path. A live set on a class that does not override
commitNativeUpdates, with no aggregate invalidations and no open batch, still calls[setNative]straight from the setter with no allocation.Legacy surface kept (names, types, exports, semantics):
_suspendNativeUpdatesCount,_suspendedUpdates,SuspendType(moved toview-base/suspend-type.ts, still internal),_batchUpdate,_suspendNativeUpdates,_resumeNativeUpdates,onResumeNativeUpdates,initNativeView,applyPendingNativeSetters,applyAllNativeSetters,affectsLayouttiming.Points for reviewers
Cost, measured jitless (
VITEST_NO_OPT=1, interpreter-only like iOS) with a no-opsetNative, medians. Live setter: roughly +40–60 ns per set from collapsing six inlined copies into one routing call (JIT: within noise); a real native write is microseconds, so the relative impact in apps is far smaller. Commit path (per view, µs), base → without commit 6 → with commit 6:_setupUI+callLoaded_batchUpdate, 8 props, live viewThe residual over base is two helper calls per property in the sweep; inlining the default-capture into those helpers would recover most of it at the cost of duplicating it, deliberately not done in Phase 0. The setter numbers live in
native-updates-hot-path.spec.ts, the commit-path ones innative-updates-commit-cost.spec.ts; both print on every run.invalidatesis additive: a property that declares it still runs its own[setNative], then the aggregates. Whether aggregates should replace the property's own setter is a Phase 1 decision.Dirty state is cleared before the commit runs, so a write made from a handler queues against a fresh set (previously the set was replaced after the sweep). Only observable if a
setNativehandler runs_batchUpdateon the same node mid-sweep.Property.setwith an override present routes through the commit and uses pending semantics (value re-read at commit). Only affects classes that opt in.Device tests (
apps/automated/src/ui/lifecycle/lifecycle-tests.ts:NativeUpdates.batchequals_batchUpdatecounts, flush inside a batch, forced flush while unloaded) are added but were not run here. The existing lifecycle suite there asserts exactsetNativecounts through XML inflation,_batchUpdate, navigation and CSS class changes and is the production check for "same native calls". To run on iOS and Android before undrafting.Tests: 485 → 557 passing (
npx vitest runinpackages/core), no pre-existing failures. Typecheck:npx tsc -p packages/core/tsconfig.lib.json --noEmitclean.Related: #11412 (
originonPropertyChangeData, independent), #11413 (InheritedPropertyreset cascade fix, found on the way).