Skip to content

feat(core): native update scheduler and commit hook (phase 0) - #11414

Draft
edusperoni wants to merge 6 commits into
mainfrom
feat/native-updates-phase0
Draft

edusperoni wants to merge 6 commits into
mainfrom
feat/native-updates-phase0

Conversation

@edusperoni

@edusperoni edusperoni commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR Checklist

What is the current behavior?

A property setter is the native call. Each Property/CssProperty/CssAnimationProperty/InheritedCssProperty setter carries its own copy of the "suspended → record in _suspendedUpdates, else [setNative](value)" block (six copies). The first native pass happens at onLoaded in 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 (_batchUpdate is 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 (_suspendNativeUpdatesCount as the holds, _suspendedUpdates as the dirty set), so every existing code path produces the same native calls, in the same order, with the same <name>Change timing. Six commits, each green on its own; review commit by commit:

  1. test(core): add native-update oracle specs — golden [setNative]/<name>Change sequences 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.
  2. refactor(core): unify native property routing — the six copies collapse into one routing function; applyPendingNativeSetters/applyAllNativeSetters share the default-capture helper.
  3. feat(core): add commitNativeUpdates hook and NativeUpdateBatchViewBase.commitNativeUpdates(batch) is the ordering hook (override, apply what the class cares about, super applies the rest). onResumeNativeUpdatesinitNativeView builds the batch and calls it. NativeUpdateBatch: entries/has/apply/skip/previous/isMount/children (an entry is a property or an aggregate Invalidation; 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 declaring invalidates), so default nodes allocate nothing extra in a batch or across unload/reload.
  4. feat(core): add NativeUpdates scheduler and flushNativeUpdatesNativeUpdates.batch()/begin()/end()/flush() coalesce writes per node across nodes (implemented as _batchUpdate semantics per touched node); mode is 'sync' only. ViewBase.flushNativeUpdates({ subtree, force }); force pushes to an existing native view while the node is not loaded and leaves the holds in place.
  5. feat(core): add invalidates option to propertiesPropertyOptions.invalidates?: Invalidation[] on every property kind; a node handles an aggregate with [invalidation.apply](batch). No core property declares one yet.
  6. 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 a NativeUpdateBatch, 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 to view-base/suspend-type.ts, still internal), _batchUpdate, _suspendNativeUpdates, _resumeNativeUpdates, onResumeNativeUpdates, initNativeView, applyPendingNativeSetters, applyAllNativeSetters, affectsLayout timing.

Points for reviewers

  • Cost, measured jitless (VITEST_NO_OPT=1, interpreter-only like iOS) with a no-op setNative, 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:

    scenario base c5 c6
    load: construct + 8 props + _setupUI + callLoaded 7.8 10.2 9.4
    reload: unload + 4 props + load 4.3 5.7 5.0
    _batchUpdate, 8 props, live view 3.44 5.20 4.12

    The 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 in native-updates-commit-cost.spec.ts; both print on every run.

  • invalidates is 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 setNative handler runs _batchUpdate on the same node mid-sweep.

  • Property.set with 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.batch equals _batchUpdate counts, flush inside a batch, forced flush while unloaded) are added but were not run here. The existing lifecycle suite there asserts exact setNative counts 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 run in packages/core), no pre-existing failures. Typecheck: npx tsc -p packages/core/tsconfig.lib.json --noEmit clean.

Related: #11412 (origin on PropertyChangeData, independent), #11413 (InheritedProperty reset cascade fix, found on the way).

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.
@nx-cloud

nx-cloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 6aad60a

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 4m 7s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-09-05 17:34:32 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11414
npm i https://pkg.pr.new/@nativescript/vite@11414
npm i https://pkg.pr.new/@nativescript/webpack@11414

commit: 6aad60a

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread packages/core/ui/core/native-updates/batch.ts
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
edusperoni force-pushed the feat/native-updates-phase0 branch from 71cdb2f to 6aad60a Compare September 5, 2026 17:27
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.

2 participants