Skip to content

feat: add React Native UIKit runtime primitives - #46

Open
DjDeveloperr wants to merge 33 commits into
refactorfrom
codex/rn-module-fabric-turbomodule-worklets
Open

feat: add React Native UIKit runtime primitives#46
DjDeveloperr wants to merge 33 commits into
refactorfrom
codex/rn-module-fabric-turbomodule-worklets

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

React Native UIKit runtime primitives

Adds the runtime primitives that let React Native drive real UIKit views and view controllers from NativeScript via direct FFI — no bridge/serialization round-trip.

What's in it

  • Direct-FFI interop core — thread-safe per-runtime object expandos, associated-object/primitive-alias marshalling, class-return handling, pointer guards, and a lazily-indexed RN backend config (no aggregate globals).
  • JS-subclass identity & dispatch — stable identity for JS subclasses of native classes, with correct super/$base dispatch to the class an override was registered against.
  • UIAppearance proxy primitives — read/write native appearance state through the same interop path.
  • Interop profiling — call counters for the FFI boundary.
  • RN module — the TurboModule (NativeScriptNativeApiModule) wiring these primitives into React Native's lifecycle; Fabric NativeScriptUIViewComponentView hosting an adopted UIKit view/view-controller subtree as a Fabric component, with size feedback so Auto-Layout-driven sizing flows back into Fabric's layout; classic-view-manager entry point; worklet-thread callback dispatch.

Tests

  • Runtime Jasmine suite (TestRunner, on-simulator): 713 specs, 0 failures.
  • Unit pins (packages/react-native/test/*.test.js): re-pinned to the simplified surface.

Stacked on refactor (#43).

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e744549-f53d-4ec8-8fd2-ed4fc7aa1808

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/rn-module-fabric-turbomodule-worklets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DjDeveloperr
DjDeveloperr marked this pull request as ready for review August 5, 2026 20:11
@DjDeveloperr
DjDeveloperr force-pushed the codex/rn-module-fabric-turbomodule-worklets branch 2 times, most recently from 5ae0568 to 2374fb5 Compare August 13, 2026 01:29
@DjDeveloperr
DjDeveloperr force-pushed the codex/rn-module-fabric-turbomodule-worklets branch 2 times, most recently from 4151972 to d225c70 Compare August 14, 2026 10:38
DjDeveloperr and others added 22 commits August 14, 2026 07:24
Multi-candidate Resources resolution for resolveMainPath() (bundle
resourcePath, executable-relative Contents/Resources, argv[0], _NSGetExecutablePath,
cwd) so the CLI/test-runner processes that don't run from a standard .app
bundle can still find app/index.js or a package.json "main", gated behind
NS_BUNDLE_LOADER_DEBUG logging.

NativeScript.mm: runMainApplication now tries resolveMainPath() before
falling back to "./app/index.js". Switch runtime_ from unique_ptr to a raw
pointer with an explicit resetRuntime() teardown point: at process exit,
static-destruction order relative to the ObjC runtime is unspecified, so an
implicit unique_ptr destructor can run after dependencies it needs are
already gone; restartWithConfig: also needs the old runtime to outlive the
new one's Init().

ThreadSafeFunction.mm: turn the global cleanup-hook mutex/condvar/map into
leaked-singleton accessors (heap-allocated, never destructed) for the same
static-destruction-order reason.

ci.yml: enable IOS_TEST_VERBOSE_SPECS for per-spec start/done logging.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Object expandos (setObjectExpando/findObjectExpando/forgetObjectExpandos)
gain a per-runtime key: a worklet spins up an additional Runtime on its own
thread against the same shared bridge, so a Value created in one Runtime
must never leak into another. Storage becomes native-pointer -> property ->
owning-runtime, all under one objectExpandosMutex_ (also now guarding the
existing objectExpandoOwnerCounts_ refcounts, since a host-object dtor can
release its owner count from either thread relative to a get/set).

runtimeObjectExpandoKey() derives the per-runtime identity: the JSI-facing
engines (V8/JSC/QuickJS) key on runtime.state().get(), Hermes keys on the
Runtime& address directly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
… no aggregate globals

NativeApiBackendConfig gains callbackInvocationAllowed (teardown-safety
gate for RN) and indexRuntimePointers (default true; RN sets false).
NativeApiBridge::addSymbol() only eagerly resolves objc_lookUpClass /
protocol pointers when indexRuntimePointers_ is set — RN launch cost:
don't realize every class/protocol at symbol-index time when RN never
touches most of them at startup.

Callbacks.mm invoke() now checks bridge_->callbackInvocationAllowed()
before running the callback and zero-returns instead when the host is
tearing down or reloading.

NativeApiJsiReactNative.h: RN config sets installGlobalSymbols=false
(unchanged behavior) and now also indexRuntimePointers=false. Install.mm's
else-branch drops the InstallAggregateGlobals call for RN — unused, and
building it eagerly cost launch time.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ointer guards

interop.setAssociatedObject/getAssociatedObject: the sanctioned way to
persist state on a native UIKit-backed object across engine calls. JS
expandos on a host-object wrapper do not round-trip (a fresh wrapper can be
handed back for the same native receiver on the next call); a real
objc_setAssociatedObject does, because it lives on the native object
itself. Target accepts a live wrapped object/pointer or the decimal text of
a raw address.

convertNativeReturnValue: an id-typed return that is actually a Class now
resolves through the class-symbol path (by runtime pointer, then runtime
class, then bare class_getName) instead of falling into makeNativeObjectValue.

nativeObjectPointerMayBeObject (`raw > 0x1000`) guards every id-typed return
path (nativeObjectIsStringLike, findCachedNativeObjectReturn,
convertNativeReturnValue) against dereferencing a misread register value —
without it, a non-object primitive read back as `id` can crash on
object_getClass/isKindOfClass:.

Primitive type-alias table: long/ulong/NSInteger/NSUInteger (mdTypeSLong/
mdTypeULong), BOOL/CGFloat (platform width)/NSTimeInterval/CFTimeInterval,
so signatures can use the platform typedef names instead of only the
fixed-width primitives.

packages/objc-node-api/index.d.ts: types for the associated-object API.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The ClassBuilder ("extend()"/native-subclass) surface's identity and
dispatch primitives:

- Object.mm: NativeApiObjectHostObject gains a superDispatchClass_ (set at
  construction or via setSuperDispatchClass), used to answer `this.super`
  correctly after a wrapper has been re-seated (see below) instead of always
  recomputing the receiver's immediate runtime superclass.
  detachObjectPreservingBridgeState() disowns a wrapper WITHOUT forgetting
  its round-trip value or dropping its expandos — used when an initializer
  returns the same receiver a second, divergent wrapper had already claimed.
  get()'s engine-extended branch now falls through, for inherited METHODS
  only (accessors stay deferred to avoid re-entrant shadowing), to metadata
  method resolution via the nearest metadata ancestor, so a first access to
  an inherited (non-overridden) selector on a JS subclass resolves instead of
  hard-returning undefined. set() hoists the JS-accessor-setter attempt
  above the metadata/runtime setter paths (an accessor override must win)
  and, in the no-JS-setter fallback, stores the expando unconditionally
  (dropped enginePrototypeHasSetter — reaching that branch already proves no
  JS setter fired, so re-probing for one was redundant).
- classPrototypeForObject gains a symbol-name fallback (classes only known
  by symbol, not yet indexed by runtime pointer with indexRuntimePointers
  off).
- Class.mm: makeNativeObjectValue takes an optional superDispatchClass,
  threaded onto both the fresh-wrapper and cached-wrapper paths.
- Callbacks.mm: a per-callback NativeApiMethodCallbackPolicy (trimmed to the
  subset with a live consumer: callSuperBeforeCallback +
  skipCallbackIfAssociatedObjectTruthy, read off a JS function's
  `__nativeScriptMethodPolicy` expando via NativeScriptRuntime.nativeMethodPolicy).
  invokeMethodSuper() calls the ObjC super implementation via
  objc_msgSendSuper before the JS override runs when the policy asks for it.
  shouldSkipConstructingMethodCallback suppresses a non-init method callback
  reaching a receiver still marked under construction. bindThis_ callbacks'
  `this` now carries the override's superDispatchClass too.
- ClassBuilder.mm: preservedNativeApiInitializerSelfReturn detects an
  initializer returning the same receiver a wrapper was already created for
  and keeps that one wrapper live (detaching the divergent duplicate) rather
  than letting two wrappers fight over the same native receiver's bridge
  state. callNativeApiBaseObjectSelector wraps $base/super dispatch with
  this handling. nativeAccessorCallbackPolicy auto-applies a re-entrancy
  guard key to every native accessor (getter/setter) override.
- HostObject.mm: __setObjectConstructionState / __setObjectAccessorCallbackState
  native entry points backing the above (associated objects, not JS
  expandos — expandos don't round-trip across proxy instances for the same
  native receiver).
- Install.mm (JS bootstrap): alloc/init construction marks/unmarks
  construction state around JS-subclass instantiation;
  installInstanceClassIdentity gives extended prototypes a `class`/
  `superclass` identity that resolves to the actual (possibly further
  subclassed) constructor; indexed-collection method aliases
  (objectAtIndexedSubscript/setObjectAtIndexedSubscript/Symbol.iterator) for
  extend()ed NSFastEnumeration-like classes, with accessor callback-state
  wrapping folded into the same helper.
- V8HostObjects.mm: the masking (kNone) host-object interceptor's get/set
  now check the real V8 prototype chain first (findPrototypeDescriptor/
  tryResolvePrototypeGet/tryInvokePrototypeSetter) so a JS-defined prototype
  accessor is honored ahead of the interceptor.
- Per-engine (hermes/jsc/quickjs/v8) selector-group call sites: after a
  prepared instance-initializer selector call, apply
  preservedNativeApiInitializerSelfReturn to the result.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
[SomeView appearance] (and appearanceWhenContainedIn: etc.) hands back an
opaque _UIAppearance proxy: UIKit forwards recognized selectors to an
internal invocation-recording store instead of actually running them, and
there's no public way to ask "what class are you a proxy for" besides
parsing `-description`'s `<Customizable class: ClassName>` format. New
host_objects/Appearance.mm holds the primitives built on that: parse the
description once, tag the recovered class onto the proxy as an expando,
then read/write a class-keyed (not proxy-instance-keyed — UIAppearance
state is effectively global per class/containment chain) property cache so
get() sees what a prior set() wrote instead of round-tripping through
UIKit's opaque recording. Setters cache too, since an appearance proxy
setter doesn't reliably support read-your-write.

Wired in everywhere a UIAppearance proxy's properties can be read or
written:
- host_objects/Object.mm get()/set(): consult/populate the appearance cache
  before falling through to metadata/runtime property resolution.
  tagStaticAppearanceSelectorResult (needs the complete
  NativeApiObjectHostObject type) stays here and tags+installs accessors on
  the result of any `[SomeClass appearance...]`-family call.
- host_objects/Class.mm: intercepts the `appearance` static method itself so
  its result gets tagged/accessor-installed rather than staying a plain
  callable selector-group function.
- host_objects/Protocol.mm: the same cache read/write for protocol-declared
  properties.
- Invocation.mm: callPreparedObjCSelector/callObjCSelector tag every
  fast-path and generic-tail result, and cache every property-setter call
  (NativeApiPreparedObjCInvocation gains propertySetterName so a successful
  setter call can cache without re-deriving the property name).
  callObjCSelector also allows a forwarded property selector through when
  the receiver is a tagged appearance proxy (class_getInstanceMethod/
  respondsToSelector: can both say no for a selector UIKit will still
  forward).
- SelectorGroupCall.h: the shared resolveNativeApiSelectorGroupCall()
  short-circuits a property-getter call through the appearance cache before
  ever touching ObjC, and gains a gsdAllowed field so appearance static
  selectors are excluded from every engine's raw-GSD fast path (which
  bypasses proxy tagging).
- Per-engine (hermes/jsc/quickjs/v8) GSD/fast-path tails: cache a successful
  setter call's value and tag/re-tag the result, mirroring the generic path.

Also brings in the runtimeReadablePropertyGetter cache (simplified to a
single mutex-guarded (Class, property) -> selector map, no thread-local
front cache) and objectGetPathCanReadRuntimeProperty, both prerequisites for
the appearance-adjacent set() success-path expando write (fixes a
set-then-get asymmetry for write-only/asymmetrically-named runtime
properties) and reused by get()'s inherited-method resolution added in the
previous commit. classPrototypeForObject's symbol-name fallback (needed
when a class isn't yet runtime-pointer-indexed).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
InteropProfiler.h: global atomic counters for every ffi_call dispatch
(count + cumulative duration). gCallsAlways is a single always-on relaxed
increment (no getenv branch, no clock read) so pop-perf gating has a
trustworthy, non-self-perturbing interop-call count in every build; gCalls/
gNs only accumulate when NS_NS_HOST_PROFILE is set (that flag also enables
verbose logging elsewhere that would otherwise perturb the volume being
measured). Declared as C++17 inline variables in their own header included
at file scope by each engine TU, since the engine TUs include the shared
bridge sources inside an anonymous namespace.

NativeScriptInteropCallTimer (Invocation.mm) wraps every ffi_call site
(CFunction, prepared CFunction, callPreparedObjCSelector's and
callObjCSelector's objc_msgSend/objc_msgSendSuper dispatch) plus hermes's
GSD invoker calls (the only engine with its own inline fast path bypassing
Invocation.mm's callPreparedObjCSelector).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…edback, worklets)

The @nativescript/react-native package: TurboModule (NativeScriptNativeApiModule)
wiring the runtime primitives from the previous commits (callbackInvocationAllowed
gate, indexRuntimePointers, interop profiler counters) into RN's lifecycle;
Fabric NativeScriptUIViewComponentView hosting a UIKit view/view-controller
subtree as a Fabric-managed component (adoptHostViewAsController); a new
NativeScriptUIViewSizeFeedback so an adopted UIKit subtree's intrinsic/
Auto-Layout-driven size can flow back into Fabric's layout instead of being
one-way; NativeScriptUIView/NativeScriptUIViewManager for the classic-view-
manager entry point; NativeScriptUIKitHost as the public adoption surface.

src/index.ts: the TurboModule spec + JS surface (Fabric host lifecycle,
worklet-thread callback dispatch, associated-object helpers, gesture/tab
support the fork's demo app exercises) — src/index.d.ts (a stale hand-
maintained duplicate of these types) is deleted in favor of the generated
declarations from this file. NativeScriptMethodCallbackPolicy is trimmed to
its two live fields (callSuperBeforeCallback, skipCallbackIfAssociatedObjectTruthy)
— the fuller DSL (argument-index targets, associated-object condition/
comparison trees, keyPath assignments, typed skip-return values) had no
caller anywhere in the fork or its own pin tests advertising it, so it's cut
along with the matching runtime surface (see the previous two commits).

NativeScriptNativeApi.podspec: an after-compile script phase prunes the
shipped metadata bundle down to just the current build's platform/arch
metadata.*.nsmd (keeps refactor's native-api/ffi/objc/... source globs).

scripts/run-tests-ios.js: simctl log/process-snapshot diagnostic collection
no longer silently swallows a failed simctl invocation — reports a warning
string instead of returning empty, so an inactivity-timeout diagnostic dump
says why simulator state couldn't be collected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The package's text-pin unit tests (packages/react-native/test/*.test.js),
applied then re-pinned onto the split/simplified sources:

- Path moves: ffi/shared/... -> ffi/objc/shared/..., ffi/{hermes,jsc,
  quickjs,v8}/... -> ffi/objc/{...}/..., and every HostObjects.mm reference
  re-anchored to the specific split file the pinned content now lives in
  (host_objects/{Object,Class,Protocol,Appearance}.mm). Where a pin's
  substring/ordering assertions span what used to be one file (the
  appearance cluster; the get()/set() JS-subclass dispatch path), the test
  now concatenates the split host_objects/*.mm files back into one logical
  blob in the same order the residual HostObjects.mm #includes them, so the
  original cross-reference assertions still hold without rewriting their
  logic.
- Trimmed-DSL pins: runtime-callback-policy.test.js and
  runtime-instance-selector-base-dispatch.test.js asserted on the full
  method-callback-policy DSL (skipCallbackIfAllAssociatedObjectConditions,
  setAssociatedObjectsBeforeSkip/setKeyPathValuesBeforeSkip,
  returnValueIfSkipped, objectForMethodPolicyTarget/TargetKind::Argument,
  associatedObjectsAreEqual, applyMethodPolicyAssignments,
  storePrimitivePolicyReturnValue, class-level ObjCMethodPolicies/
  methodPolicies plumbing) and enginePrototypeHasSetter — all dropped in
  this simplification (no caller anywhere used them). These pins now assert
  the trimmed shape directly (callSuperBeforeCallback +
  skipCallbackIfAssociatedObjectTruthy only) and assert the dropped surface
  is ABSENT, rather than asserting on code that no longer exists.
  runtime-js-subclass-expando.test.js and runtime-member-cache.test.js
  received the same treatment for the simplified set()-fallback and the
  single mutex-guarded property-getter cache (no thread-local front cache).
- packages/react-native/native-api and packages/react-native/types are
  gitignored build artifacts (`npm run build-rn-turbomodule`) that don't
  exist in a fresh checkout; pins that read them now skip gracefully
  (existsSync) instead of throwing ENOENT, while still asserting against
  them when a maintainer has generated them.

Pre-existing, unrelated failure (confirmed identical on origin/refactor,
not touched by this diff): packages/react-native/test/babel-plugin.test.js
fails with MODULE_NOT_FOUND for @babel/core — `npm install` was never run
in this checkout (no root node_modules at all). Environment gap, not a pin
issue.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…e crash fix

build_react_native_turbomodule.sh's copy list was missing files the single-TU
NativeApiJsi.mm build (via HostObjects.mm's #include chain) needs to compile:
- InteropProfiler.h (added by 18dddf43, never added to the copy list)
- SelectorGroupState.h / SelectorGroupCall.h and the host_objects/*.mm split
  (host_objects/{Interop,Struct,Appearance,Object,Class,Protocol}.mm), both
  predating this stack (introduced by refactor's own c589999) but likewise
  never copied

Running `npm run build-rn-turbomodule` without this produced a hard
"file not found" at CompileC for NativeApiJsi.mm's own #includes -- discovered
while regenerating packages/react-native/native-api to get the Callbacks.mm
super-dispatch fix into the itest demo's actual compiled pod.

Also fixes NativeScriptNativeApiModule.mm's #include of InteropProfiler.h,
which still pointed at the pre-split "native-api/ffi/shared/bridge/..." path
instead of "native-api/ffi/objc/shared/bridge/...".

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…atch

runtime-callback-policy.test.js asserted callbackSource.includes(
"class_getSuperclass(methodBaseClass_)") -- exactly the buggy line the
58563def fixup (ffi(subclass): JS-subclass identity & dispatch) removed.
methodBaseClass_ IS already the override's base class (threaded from
ClassBuilder's addEngineOverrideMethod); further-superclassing it skips past
it, making any member declared exactly on that class unreachable via
this.super/$base. Re-pinned to assert the fixed "Class superDispatchClass =
methodBaseClass_;" line instead.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…ssing

A bound selector-group method function (e.g. `view.viewWithTag`) is cached
as a native-object expando keyed by the underlying ObjC pointer
(Object.mm's `bridge_->setObjectExpando(..., methodFunction)`), so the
cache survives independently of the `NativeApiObjectHostObject` wrapper it
was bound to. Once that original wrapper is torn down (its owning JS proxy
collected) and the SAME native pointer is later re-wrapped by a fresh
`NativeApiObjectHostObject` on another crossing, the stale cached function
still resolves its receiver via the dead wrapper's weak/lifetime state --
`data.boundReceiverState->object()` (SelectorGroupCall.h) and
`state.boundReceiver.lock()` (NativeApiJsi.mm) both silently return nil --
so every call through it threw "Objective-C selector requires a native
receiver" even though the method is being invoked on a live object.

Reproduced 100% of the time on cold launch of every itest scenario
(including plain `nav-stack`, previously 12/12 clean), isolated away from
the react-native-screens adapter and the simulator via: (1) fresh
never-booted simulator device still crashed, (2) causally disabling the
adapter's only recent change did not stop it, (3) an attached lldb session
showed `state.boundReceiver` / `data.boundReceiverState` resolving a dead
weak_ptr (strong=0) at the exact throw site.

Fix: when the bound receiver has died, fall back to resolving from the
call's actual `thisValue` (the live receiver `.method(...)` was invoked
on) instead of throwing -- exactly what the unbound path already does.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
Cold-launch profiling (NS_NS_HOST_PROFILE/NS_NS_HOST_PROFILE_INNER) traced
the port's ~800-1100ms main-thread stall (vs upstream's ~115ms) to a burst
of small, separate Fabric mounting transactions hitting the same ~6 UIKit
hosts right after first content, each paying a full native->worklet
round trip via runUIKitHostLifecycleFromNative regardless of whether the
work was useful.

Two safe, ordering-preserving cuts:

- index.ts runUIKitHostLifecycleFromNative: stop re-passing
  nativeMountInfoJson into createRegisteredUIKitHostFromNative -- it was
  already parsed and synced by syncUIKitNativeMountInfo a few lines above,
  so every crossing re-parsed the identical JSON and re-resolved every
  native handle in it a second time, unconditionally.
- Same function, "transactionCommitted" phase: skip
  parseUIKitFabricTransactionJson + commitUIKitHostFabricTransaction when
  the resolved host defines neither mountingTransactionDidMount nor
  transactionCommitted. commitUIKitHostFabricTransaction already no-ops in
  that case; several hosts (the nav stack/screen controllers, the badge)
  have no such callback and were paying to parse a full mounted-children
  snapshot (with a native handle resolved per child) for a result that was
  discarded immediately. Hosts that DO define one (e.g. the tabs host) are
  unaffected.

Measured effect (NS_NS_HOST_PROFILE_INNER jsMs sum across the cold-launch
crossing burst, same simulator/build): ~549ms -> ~352ms, about a 36% cut.

A third change -- deferring the synchronous "update" phase crossing
(NativeScriptUIView.mm setUpdateRevision) via a dispatch_async + token
coalescing pattern mirroring scheduleUIKitHostPropsTransactionCommitIfNeeded
-- was tried and reverted. It collapsed the redundant "update" bursts too,
but itest's pop-slide content-discipline gate caught a real regression:
deferring the crossing that runs the adapter's stack reconcile by even one
runloop turn let a synchronous pop-transition step elsewhere run before the
reconcile it depended on, dropping the content slide on roughly half of a
10-pop cycle (POP_DID_NOT_SLIDE, slides=5/10). Reverted to the original
synchronous call; a comment at the call site documents why and the residual
redundant-crossing cost this leaves unresolved.

Verified via test/screens-itest: --suite slide (pop-slide, edge-swipe-slide)
2/2 clean on the reverted+kept fix. --suite core showed pre-existing/
session-length-related flakiness in this run that a fresh-simulator retest
did not cleanly resolve (agent-device's accessibility bridge itself
degraded mid-investigation, confirmed via a HOST_TIMEOUT run) -- flagged in
the task writeup as needing a clean-session re-verification, not folded
into this commit's claims.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ignature

uikit-host-fabric-mount-info-api.test.js and uikit-host-native-props-api.test.js
still asserted the OLD 4-arg createRegisteredUIKitHostFromNative(hostId,
undefined, false, nativeMountInfoJson) call that 6738e4e intentionally
replaced with the 3-arg form (the mount info is already synced by
syncUIKitNativeMountInfo just above, so re-passing it re-parsed the identical
JSON and re-resolved every native handle a second time on every crossing).
These two node-run structural tests aren't wired into CI, so the drift went
unnoticed; found while sanity-checking a follow-on change to this exact path.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
taggedAppearanceProxyClass's untagged fallback (appearanceProxyCustomizable-
ClassFromExactDescription) sends a real Objective-C `-description` message to
ANY object on its first property read, as a heuristic to detect UIAppearance
proxies. That runs unconditionally inside NativeApiObjectHostObject::get(),
so it fires for every property access on every native object, including
objects handed to JS reentrantly as callback arguments while native code's
own machinery is still on the stack.

Root-caused via os_log breadcrumbs bracketing the sheet-detents-custom spike
end to end (both in-app and inside the interop bridge itself): the
customDetentWithIdentifierResolver resolver block was invoked correctly, and
returning a bare CGFloat constant from it always worked -- the block's own
return-value marshalling was never broken. The hang was reading
ctx.maximumDetentValue: the first property access on the live, UIKit-owned
UISheetPresentationControllerDetentResolutionContext object triggers this
generic appearance-proxy check, which calls -description on it -- and that
deadlocks inside UIKit's own detent-resolution machinery, which is still
running on the same call stack.

Fix: skip the -description fallback when gNativeCallerThreadEngineCallback-
Depth > 0 (already used elsewhere in this file's own call chain to detect
exactly this situation). A real UIAppearance proxy is only ever obtained by
JS calling an `+appearance`-family method itself -- an outbound call this
engine makes, never something delivered inbound as a callback argument -- so
the guard never regresses genuine appearance-proxy detection; it only
disables an unsafe heuristic for objects that were never appearance proxies
to begin with.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…ounter

iteration-5 Stage 0 (dev-notes/perf/iteration-5-batching-design.md, in the
nativescript-react-native-screens repo): callPreparedObjCSelector and
callObjCSelector's prepared-invoker fast paths dispatch through
prepared.preparedInvoker(...) without ever constructing a
NativeScriptInteropCallTimer, so they were counted by neither gCalls nor
gCallsAlways -- the always-on counter __nsInteropCallCount() exposes to JS
undercounted every crossing that takes this path. Add the same unconditional
gCallsAlways increment the timer's constructor does elsewhere, so the
JS-visible crossing count is complete. Metrology only: no timing added, no
control flow changed, gCalls (the NS_NS_HOST_PROFILE-gated profiling counter)
is untouched.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…ubtree)

iteration-5 Stage 2 (dev-notes/perf/iteration-5-batching-design.md §B.6, the
`kind 4` fillSubviews op). New shared TU BatchOps.mm mirrors the adapter's
isNativeScrollView / shouldFillHostedSubview / layoutHostedSubviewChain /
enableHostedInteraction / layoutHostedReactSubviews fill body in plain ObjC
-- same depth caps (8 fill / 12 interaction), same UIScrollView stop, same
autoresizingMask (18) -- and does the whole recursive walk in ONE crossing
instead of ~4-8 per hosted view. Exposed as __nsFillHostedSubtree from
NativeApiHostObject::get (HostObject.mm, next to __fastEnumeration);
textually included by NativeApiJsi.mm before HostObject.mm's own include.

Scope narrowing (disclosed in BatchOps.mm's header comment): implements only
this one op as its own host function rather than standing up the design's
full generic `__applyOps(targets, ops)` executor (kinds 0/1/2/3/5 are
Stage 3+, out of scope this iteration). Never calls -description (the hazard
fixed in 33b583a); wraps the walk in @try/@catch so a native exception
becomes a non-empty error string, never a JS exception.

Stage-0 attribution (this iteration) measured the walk this replaces at
533 crossings (mounted phase) / 756 crossings (update phase), 100%
reproducible across 3 reps, and jsMs-dominant (~90% of each phase's total
wall time) -- so this is the single highest-value cut in the batching
design. Measured after: screen mounted 616->95 crossings (jsMs 59-92ms ->
below the 2ms profiling threshold), stack mounted 593->87 crossings, same
magnitude for stack/screen update. The corresponding JS-side integration
(nativeFillHostedSubtree, fail-open to the original JS walk on any native
failure) lives in the adapter, propagated to nativescript-react-native-screens.

Gates (ship-config itest build, DEVELOPER_DIR=Xcode-old, same sim): slide
2/2 (pop-slide strands=0/rehost+0/bypass+0, edge-swipe-slide strands=0),
core 12/12 clean zero retries, parity 16/17 (searchbar -- one of the
task's documented borderline-under-load flakes -- failed 3/3 in-suite
attempts on MAIN_THREAD_STALL_STEADY with its real assertion
(type-query=changes:7,text:nsrocks,focus:1) passing every time, then
PASSED on a fresh-reset isolated re-run), reveal 12/12, npm test 238/238,
npm run test:rnav 19/19.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
check_ffi_boundaries.sh greps shared/ FFI sources for the literal string
"NativeApiJsi" (a proxy for JSI-Hermes coupling) -- it does not distinguish
comments from code. BatchOps.mm's header comment named that file directly,
tripping the check even though the file contains no facebook::jsi:: or
<jsi/ reference (it uses the same unqualified Runtime/Value/Array/
PropNameID names as every other shared/bridge TU, via the using-declarations
already in scope where it is textually included -- same pattern as
HostObject.mm, which the checker does not flag because it never spells out
the filename). No code change, no behavior change.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…AttachedContentPresence)

Iteration 6 JOB 2 -- the tabs host is the largest remaining crossing pool
(2,272 crossings, untouched by iteration 5a's stack/screen walker). Adds
a second read-only native walker mirroring the tabs adapter's
attachedDescendantContentPresence (react-native-screens-nativescript-tabs-
snapshot repo): same depth cap 16, same early-exit-when-both-true, same
per-node hidden/alpha/window/frame/interactive checks, in ONE crossing
instead of ~5-7 per node. Exposed as __nsScanAttachedContentPresence next
to the existing __nsFillHostedSubtree walker. Never mutates, never calls
-description (33b583a).
CI (build job, "Build NativeScript" step) has been failing since iteration
5a's Stage 2 landed (2d44ebf): HostObject.mm references
nativescript::NsFillHostedSubtreeHostFunction (and, as of this iteration,
NsScanAttachedContentPresenceHostFunction), both defined in BatchOps.mm.
The Hermes TU (NativeApiJsi.mm) already includes BatchOps.mm immediately
before its own HostObject.mm include; the V8 TU (NativeApiV8.mm) never
got the matching include, so HostObject.mm's reference to those symbols
is undefined there -- "no member named 'NsFillHostedSubtreeHostFunction'
in namespace 'nativescript'". Invisible to every device-level gate because
the demo apps are Hermes-only; only surfaced in CI's V8-backend build.
Mirrors the Hermes ordering exactly (after ClassBuilder.mm, before
HostObject.mm) -- BatchOps.mm has no Hermes-specific dependencies (pure
jsi::Runtime/Value/Array + ObjC), so this is a mechanical, safe fix.
…PHONE

CI's V8/napi-cli "Build NativeScript" step failed again after the
previous fix (77e93b2) let it reach BatchOps.mm: "unknown type name
'UIView'" / "use of undeclared identifier 'UIScrollView'". Root cause:
BatchOps.mm is pure UIKit code with no explicit UIKit import -- it only
ever compiled in the Hermes path because that target's CocoaPods/Xcode
project supplies an implicit UIKit import via its prefix header. The
standalone napi-ios CLI build (V8 backend) has no such prefix header and
also targets non-iOS platforms (macOS/tvOS/visionOS) where UIKit does
not exist at all.

Guards both walkers' bodies in BatchOps.mm's `namespace nativescript`
block, and the matching __nsFillHostedSubtree/__nsScanAttachedContentPresence
registrations in HostObject.mm, with `#if TARGET_OS_IPHONE` -- the same
convention already used by this codebase's other UIKit-only code
(host_objects/Appearance.mm's appearanceProxyCustomizableClassFromExactDescription).
On a non-iOS build these two ops are simply absent from the api object;
every JS call site already fails open to its original walk when
`typeof api.__nsFillHostedSubtree !== 'function'`, so this changes
nothing observable on any platform that already worked.

Verified: local Hermes/iOS build (demo app, arm64 simulator) still
succeeds and both NsFillHostedSubtreeHostFunction and
NsScanAttachedContentPresenceHostFunction symbols remain present in the
compiled binary -- the guard is a no-op on TARGET_OS_IPHONE=1 targets.
…BatchOps.mm

93aa4a0's `#if TARGET_OS_IPHONE` guard around BatchOps.mm's walker bodies
did not fix CI's V8/napi-cli "Build NativeScript" step: that build targets
iOS SIMULATOR (TARGET_OS_IPHONE is true there too, same as device), so the
guarded UIKit code was still reached, just without UIKit declared -- the
standalone V8 CLI build has no CocoaPods/Xcode prefix header to import it
implicitly the way the Hermes demo-app build does. host_objects/Appearance.mm
was not a working precedent for this: its own TARGET_OS_IPHONE-guarded block
only touches Foundation types (id/NSString/NSRange/Class), never a UIKit
type, so it never needed an import.

Tried adding `#import <UIKit/UIKit.h>` inside BatchOps.mm's own guard first
-- that reproduces the exact same "declarations may only appear in global
scope" error, because NativeApiV8.mm textually #includes BatchOps.mm AFTER
opening `namespace nativescript { namespace { ... } }`, and Objective-C
@interface/@protocol declarations (UIKit.h is full of them) are illegal
inside a C++ namespace. BatchOps.mm is restored byte-identical to 93aa4a0;
the import is added instead to NativeApiV8.mm at its true global scope,
before either namespace opens, guarded identically (a no-op include on
macOS, the only TARGET_OS_IPHONE=0 target this codebase builds).

Verified locally: `npm run build:ios-sim` (DEVELOPER_DIR=Xcode-old, default
TARGET_ENGINE=v8, matching CI's failing target) now succeeds end-to-end
(BUILD SUCCEEDED, xcframework written). BatchOps.mm being unchanged means
the Hermes/CocoaPods demo build (already gate-verified this iteration) is
unaffected.
@DjDeveloperr
DjDeveloperr force-pushed the codex/rn-module-fabric-turbomodule-worklets branch from d225c70 to 431a33e Compare August 14, 2026 11:27
DjDeveloperr and others added 11 commits August 14, 2026 09:01
…lver

The tabs host's mountChild crossing storm (663 crossings/screen, iteration
4/6/7's explicitly-deferred top lever) is a BFS descendant walk
(embeddedNavigationControllerRecordInMountedTabsChildSubviewTree, tabs
snapshot repo) with a 2-tier associated-object lookup + nextResponder-chain
fallback at every visited node -- queue cap 64, subview fan-out cap 16.

Adds __nsResolveEmbeddedNavigationController(candidates) to BatchOps.mm,
mirroring that walk byte-for-byte natively: same association-key derivation
TypeConv.mm's interop.set/getAssociatedObject already use
(objc_getAssociatedObject + sel_registerName), same handle-then-value tier
order and short-circuit semantics, same nextResponder fallback, same depth
caps. Runs the scan across every mount-child candidate (the same array the
JS already builds from 6 direct values + up to 10 resolved handles) in ONE
crossing, returning [err, ok, containerView, navigationController,
navigationView]. err!='' means the op failed and the JS caller must fall
back to its original per-candidate loop (fail-open, same convention as the
existing __nsFillHostedSubtree/__nsScanAttachedContentPresence walkers);
err=='' with ok==0 is a TRUSTED negative the caller must NOT re-run the JS
walk for, or the crossing-count win is lost on the common (non-stack-
embedded) case.

Also fixes scripts/build_react_native_turbomodule.sh, which has never
copied BatchOps.mm into the build-time mirror
(packages/react-native/native-api/ffi/objc/**) since that file was added --
the exact staleness trap iterations 5a/6 hit and worked around manually
each time, now fixed at the source.

Measured (SIMCTL_CHILD_NS_NS_HOST_PROFILE=1, reproducible 3/3): tabs
mountChild interopCalls 663+87 -> 22+64 (-88.5%); tabs pool total
2272 (iter-4 baseline) -> 1069 (-52.9%). ms payoff is small (~10-15ms) per
iteration 7's own phase attribution, since mountChild's own JS-crossing
cost was already a small fraction of the ~347ms port cold-launch budget --
the crossing count is the load-independent, reproducible signal here.

dev-notes/perf/iteration-8-*.md (nativescript-react-native-screens repo)
has the full design + measurement writeup.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
C-fix-1 (iteration 9 architecture scoping). Every NativeScriptUIViewComponentView
observer built the whole 109-entry mutation-record array (one NSDictionary alloc
per mutation) BEFORE checking whether it had anything to deliver, so every
registered observer paid the allocation for every Fabric transaction in the app --
including hosts with no modification in that transaction. Move the build below the
early-return, and additionally gate it on fabricLifecycleCallbacks (the same flag
mountChild/unmountChild already gate their child-level events on; no consumer reads
the delivered `mutations` field on a host that doesn't opt in, and
-fabricTransactionJsonWithModifiedChildren:...mutations: already turns `nil` into
`@[]`, so payload is byte-identical for hosts that do consume it).

Est. 2-5ms at cold launch, larger steady-state relief (every unrelated Fabric
commit anywhere in the app was paying 6x this on main). Risk ~0 -- pure refactor,
no ordering change, no crossing-count change.

Gate (rn-primitives, itest suite, EXPO_PUBLIC_ITEST_HARNESS=1 build): core 12/12,
parity 17/17, reveal 12/12, slide 2/2 (pop-slide 10/10 strands=0 rehost+0 bypass+0;
edge-swipe-slide 5/5 strands=0), tab-switch-storm worstWarm=2ms, jest 238/238,
test:rnav 19/19 -- all clean, gated together with C-fix-2 (next commit) per the
design's own Step-1 staging.
C-fix-2 (iteration 9 architecture scoping). notifyFabricTransactionCommitted...
built the children snapshot (fabricMountedChildrenSnapshot) TWICE per commit:
once to feed its own debug-log summary (NativeScriptFabricDebugLog -- compiled
in but env-gated off by default, yet its args, including the snapshot, were
always evaluated), and again inside fabricTransactionJsonWithModifiedChildren:...
to build the actual JSON payload. Compute it once and thread it through via a
new childrenSnapshot: parameter (nil falls back to the old self-computing
behavior for the other, colder call sites).

Within that snapshot, fabricChildEventForComponentView:childContainerView:index:
recomputed the OWNER's uikitHostHandles (5 %p-formatted handle strings) on every
call, even though the owner (self) never changes across the per-child loop in
fabricMountedChildrenSnapshot. Compute it once per snapshot and pass it in; the
single-child convenience wrapper (used by notifyFabricChildMounted/Unmounted,
which only ever call it once) keeps computing its own.

Same output dictionaries, same delivered JSON -- pure refactor. Est. 3-8ms
across cold-launch deliveries. Risk ~0.

Gated together with C-fix-1 (previous commit) per the design's Step-1 staging:
core 12/12, parity 17/17, reveal 12/12, slide 2/2 (pop-slide 10/10 strands=0
rehost+0 bypass+0; edge-swipe-slide 5/5 strands=0), tab-switch-storm
worstWarm=2ms, jest 238/238, test:rnav 19/19 -- all clean.
…ommits

C-fix-3 (iteration 9 architecture scoping) -- the big one, ~15-30ms estimated.
mountChildComponentView:index:'s and updateProps:oldProps:'s own
-refreshContainerViewFrameAndHost tail calls, and the NativeScriptUIView
insertSubview:atIndex: funnel's own per-insert layout/display/sentinel/
hostReady suite (each doing double depth-3 topology-string snapshot keys, a
depth-10 full-subtree setNeedsDisplay walk on topology change, and a
depth-32 descendant-count walk), all fired PER MUTATION EVENT inside the
mount phase -- 5-8 times per wrapper for transaction #1 at cold launch.
mountingTransactionDidMount already runs a full, unconditional
-refreshContainerViewFrameAndHost before it delivers (immediately if
immediateTransactionCommit, else one dispatch_async hop later) whenever a
host has any modification -- a precondition for reaching any of the
per-event call sites. Coalesce: skip the per-event tail work and let
didMount's own call converge it once, same runloop turn / one queue hop
later, same as RNS's own container-update-deferred-to-didMount precedent
(RNSScreenStack.mm) this port already partially mirrors elsewhere.

Scoped STRICTLY to insert-only transactions via a new, transaction-wide (not
host-scoped) Remove/Delete scan, computed once per observer in
mountingTransactionWillMount and exposed as
-currentTransactionHasRemovalMutation (NativeScriptUIViewComponentView.h/.mm)
-- reused by a small NativeScriptShouldCoalesceInTransactionRefreshTail()
helper in NativeScriptUIView.mm (mirrors the existing
replayFabricTransactionAfterHostCreationIfNeeded pattern for reaching the
owning ComponentView's isApplyingMountingTransaction). unmountChildComponentView
is untouched (a transaction containing an unmount always has a Remove/Delete,
so the guard is always false there) -- the pop/content-discipline path is
byte-identical. The synchronous "update" crossing (setUpdateRevision's
runUIKitHostLifecycle:@"update") and props delivery are completely untouched;
only the refresh TAIL is deferred to didMount's existing convergence point.

Includes TEMPORARY [FABRIC7] instrumentation (a direct read of Fabric's own
facebook::react::TransactionTelemetry mount-phase timing, env-gated on
NS_NS_FABRIC7, for this iteration's measurement pass) -- will be stripped in
a follow-up commit before the final push, per the iteration's own
"remove all instrumentation, rebuild clean" close-out step.

Gate (itest, EXPO_PUBLIC_ITEST_HARNESS=1): core 12/12, parity 17/17
(incl. modal-chain, modal-chain-rapid), reveal 12/12 (all 6 cold-first-*
families, reveal-sequence gateBypassed+0/gateDeferred+0/revealRehost+0),
slide 2/2 (pop-slide 10/10 slides=10/10 strands=0 rehost+0 bypass+0
armedFail=0; edge-swipe-slide 5/5 strands=0 armedFail=0 rehost=0),
tab-switch-storm worstWarm=2ms, jest 238/238, test:rnav 19/19 -- all clean,
zero regressions.
…tion

Iteration 9's measurement pass is done (see dev-notes). Strips the
NS_NS_FABRIC7-gated TransactionTelemetry read added on top of C-fix-3 for
that measurement; no functional change to the landed C-fix-1/2/3 code.
…rumentation (iteration 10, Stage 0)

Iteration 9's terminal verdict left unresolved whether the
MountingTransactionObserving didMount dispatch runs before or after
Fabric's own telemetry.didMount() stamp. Verified from RN source
(TelemetryController.cpp:19-51): it runs AFTER -- the entire observer
phase (where C-fix-1/2/3 and this iteration's stages live) is
structurally invisible to the FABRIC7 bracket, explaining iteration 9's
null measurement.

Restores FABRIC7 (env NS_NS_FABRIC7, reverse of 828bc2b) alongside a
new FABRIC10 bracket (env NS_NS_FABRIC10) around
mountingTransactionDidMount:'s own body -- entry to return for the
immediate (synchronous) commit path, entry to return of the
dispatch_async tail for the deferred path -- with a per-transaction
cumulative timer so the last-logged line for a transaction number
approximates the whole multi-host observer-phase span.

Temporary measurement instrumentation only, env-gated (no functional
change; both flags default off). Gate: --suite core 12/12, --suite
parity 17/17, --suite reveal 12/12, --suite slide pop-slide 10/10 +
edge-swipe-slide 5/5 (strands=0 rehost+0 bypass+0), tab-switch-storm
worstWarm=2ms, npm test 238/238, npm run test:rnav 19/19. Zero
regressions.

See dev-notes/perf/iteration-10-native-hosting-architecture.md §0.
…lt-off

Adds a fourth staged descriptor bit from the iteration-10 native-hosting
design (dev-notes/perf/iteration-10-native-hosting-architecture.md §2.3
Stage 1): `nativeCommitObservations` (NativeScriptUIViewNativeComponent.ts,
NativeScriptUIView.h/.mm, ComponentView dict+typed prop paths, index.ts's
UIKitHostDefinition + both render funnels). Default off, fail-open,
composes with the existing flags exactly like immediateTransactionCommit.

When set on a host whose adopted `_viewController` (already tracked
natively via the controllerHandle prop, NativeScriptUIView.mm) is a
UITabBarController, the committed transaction payload
(`fabricTransactionJsonWithModifiedChildren:...childrenSnapshot:`) gains
an `observations` object: `selectedControllerHandle` +
`viewControllerHandles`, computed from the live controller with zero FFI
crossings (native already holds the reference). Versioned (`"v": 1`) per
the design's payload-evolution mandate.

Scoped down from the design's full observations shape (selectedViewFrame/
contentPresence/firstSubviewHandle deferred -- those serve a different,
lower-priority consumer, selectedTabPreparedFastReconcileKey) to keep this
landing minimal and reviewable: the highest-value target measured in
Stage 0 (tabControllerViewControllersMatch's O(n) native-array read +
per-index nativeObjectsEqual, the dominant contributor to the 579+404
FFI-read count iteration 6 measured).

Gate (rn-primitives): --suite core 12/12, parity 17/17, reveal 12/12
(one cancel-swipe-wedge flake on the first pass, confirmed unrelated --
3/3 clean in isolation, 12/12 clean on re-run), slide 10/10+5/5
strands=0 rehost+0 bypass+0, tab-switch-storm worstWarm=2ms, npm test
238/238, npm run test:rnav 19/19. Zero regressions.

See dev-notes/perf/iteration-10-native-hosting-architecture.md §2.3.
…rumentation (iteration 10)

Stage 0's measurement pass is done (dev-notes/perf/iteration-10-stage0-*.md).
Strips both env-gated telemetry brackets added on top of Stage 1's landed
nativeCommitObservations bit; no functional change to that bit or any
other landed code. File is now byte-identical to 828bc2b (the
iteration-9 FABRIC7-removal baseline) plus only the Stage 1 diff.
…(iteration 11, Stage 0)

Temporary, env-gated (NS_NS_ITER11_TXN), default off. One line per host per
Fabric mounting transaction (transaction index, mutation count, modified
children/props flags) so the settle-cascade transactions #2-#7
nativeHandleAuthority (Stage 1+) predicts will disappear can be counted
directly. Zero functional delta: full gate matrix clean (core 12/12, parity
17/17, reveal 12/12, slide 10/10+5/5, tab-switch-storm worstWarm=4ms, jest
238/238, rnav 19/19). Removed before this iteration lands for real.

See dev-notes/perf/iteration-11-stage0-metrology.md (nativescript-react-native-screens repo).

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…onsumers

Adds UIKitHostViewProps.nativeHandleAuthority (JS-only, per-host,
default-off descriptor prop) and gates applyHostHandles' body plus both
setNativeHostRevision call sites inside defineUIKitHost. No native/codegen
diff. No consumer sets the bit yet -- behavior is byte-identical to
06003c7 for every existing host.

The audit (dev-notes/perf/iteration-11-handle-authority-audit.md) established
native is already the handle authority on every path that matters: every
lifecycle crossing applies uikitHostHandles(host) to the container natively,
the create-miss race heals via refreshContainerViewFrameAndHost ->
mountUIKitHostIfNeeded, and the React-delivered handle props are inert behind
the Bug-B empty-string guards. The settle setStates this bit will gate (once
adapters opt in, Stage 2+) are the spawners of the ~245ms post-content Fabric
transaction cascade (#2-#7) quantified in iteration 10 and re-confirmed in
this iteration's Stage 0.

Gate: jest 238/238, --suite core 12/12, --suite slide pop-slide 10/10
(strands=0 rehost+0 bypass+0) + edge-swipe-slide 5/5. Crossing-count spot
check (3 reps, bit compiled but unused): final txn index 6-7, inside the
Stage 0 baseline's own run-to-run noise band (6-8) -- no systematic shift.

See dev-notes/perf/iteration-11-stage1-the-bit.md (nativescript-react-native-screens repo).

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
…on 11)

Nets Stage 0's instrumentation commit to zero -- added then fully removed
within this iteration, matching this series' established protocol. The
demo-side NsFabric10Watchdog (outside git) was removed the same way.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E6M4WHJVVjazd1RQhi9aSb
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.

1 participant