@symbiote-native/engine 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/build/accessibility-props.d.ts +0 -11
  2. package/build/accessibility-props.js +30 -68
  3. package/build/animated/graph.js +1 -1
  4. package/build/animated/leaf-lifecycle.js +2 -2
  5. package/build/asset-source-resolver.d.ts +2 -0
  6. package/build/asset-source-resolver.js +13 -0
  7. package/build/back-handler/index.d.ts +1 -5
  8. package/build/back-handler/index.js +0 -6
  9. package/build/debug.js +8 -22
  10. package/build/dispatch.js +3 -9
  11. package/build/events/index.js +2 -2
  12. package/build/fabric-props.js +74 -179
  13. package/build/fabric.d.ts +0 -13
  14. package/build/fabric.js +18 -38
  15. package/build/host-access.d.ts +1 -128
  16. package/build/host-access.js +96 -205
  17. package/build/host-behavior.d.ts +0 -100
  18. package/build/host-behavior.js +125 -311
  19. package/build/image-loader.js +10 -23
  20. package/build/image-source-resolver.js +3 -7
  21. package/build/image-source-write.d.ts +0 -11
  22. package/build/image-source-write.js +14 -34
  23. package/build/imperative.d.ts +0 -28
  24. package/build/imperative.js +42 -92
  25. package/build/index.d.ts +1 -0
  26. package/build/index.js +24 -37
  27. package/build/mutation-buffer.d.ts +0 -177
  28. package/build/mutation-buffer.js +137 -308
  29. package/build/native-engine.d.ts +0 -102
  30. package/build/native-engine.js +38 -98
  31. package/build/native-events.js +9 -18
  32. package/build/native-tree-host.d.ts +0 -21
  33. package/build/native-tree-host.js +14 -31
  34. package/build/node.d.ts +0 -212
  35. package/build/node.js +338 -774
  36. package/build/post-commit.js +3 -8
  37. package/build/process-aspect-ratio.js +3 -7
  38. package/build/process-background-longhands.js +10 -19
  39. package/build/process-filter.js +11 -19
  40. package/build/process-font-variant.js +3 -7
  41. package/build/registry.d.ts +0 -33
  42. package/build/registry.js +22 -57
  43. package/build/report-error.js +4 -18
  44. package/build/structured-style.d.ts +0 -9
  45. package/build/structured-style.js +16 -31
  46. package/build/styles.js +3 -6
  47. package/build/surface.d.ts +0 -26
  48. package/build/surface.js +29 -76
  49. package/build/text-input-state.js +4 -8
  50. package/build/touch-history.js +5 -11
  51. package/build/tree-host.d.ts +0 -270
  52. package/build/tree-host.js +63 -153
  53. package/build/view-config.js +17 -37
  54. package/package.json +2 -2
package/build/surface.js CHANGED
@@ -1,15 +1,5 @@
1
1
  // A surface is one mounted root: it owns the rootTag handed down by the native Fabric host and the
2
2
  // top-level nodes under it. Adapters mutate it and ask it to commit.
3
- //
4
- // A SURFACE IS ONE ORDINARY NODE, and that is the whole implementation rather than a trick. It holds
5
- // a real handle, its child ops are the ordinary append / insertBefore / removeChild, and `OP_COMMIT`
6
- // names that one handle — so the host needs no `rootTag -> node` map, which is what keeps it
7
- // stateless.
8
- //
9
- // The node is RN's AppContainer (`createSurfaceRoot` in `node.ts` — `flex: 1`, `box-none`), so what
10
- // reaches the root child set is one view with the app under it. An ANCHOR in the same position
11
- // hoists its children instead, and the host takes both through the same call, so nothing here is a
12
- // special case.
13
3
  import { dlog } from './debug.js';
14
4
  import { installEventHandler } from './events/index.js';
15
5
  import { detachAnimatedProps } from './animated/host-binding.js';
@@ -33,17 +23,10 @@ export class SymbioteSurface {
33
23
  this.rootTag = rootTag;
34
24
  this.node = createSurfaceRoot();
35
25
  }
36
- /**
37
- * Every OTHER live surface, so one commit names every root — see `commitSurfaceOps`.
38
- *
39
- * Static because it reads a sibling instance's private handle, which only a member of this class
40
- * may do. One surface — the universal case — allocates nothing.
41
- *
42
- * `self` may already be OUT of the registry: a teardown unregisters and then commits, and that
43
- * commit is the one carrying the removals. So the fast path cannot be a size check — with one
44
- * live surface left, `size === 1` means either "only me" or "only the other one", and taking the
45
- * shortcut on the second reading is how a surface loses the very batch that empties it.
46
- */
26
+ // Every other live surface, so one commit names every root — see commitSurfaceOps. Static
27
+ // because it reads a sibling instance's private handle, which only a member of this class may do.
28
+ // self may already be out of the registry: a teardown unregisters and then commits, carrying the
29
+ // removals — so the fast path can't be a size check, since size===1 could mean either surface.
47
30
  static others(self) {
48
31
  let out;
49
32
  for (const other of surfaces.values()) {
@@ -54,13 +37,8 @@ export class SymbioteSurface {
54
37
  }
55
38
  return out ?? NO_CO_COMMITTERS;
56
39
  }
57
- /**
58
- * The top-level nodes, asked of the host.
59
- *
60
- * A getter rather than an array this class maintains: a second copy of a child list is the thing
61
- * this design removes, and `nextSiblingOf` (host-access.ts) needs the same answer the host gives
62
- * for a parented node.
63
- */
40
+ // The top-level nodes, asked of the host. A getter rather than an array this class maintains: a
41
+ // second copy of a child list is the thing this design removes.
64
42
  get children() {
65
43
  return childrenOf(this.node);
66
44
  }
@@ -70,10 +48,8 @@ export class SymbioteSurface {
70
48
  insertBefore(child, beforeChild) {
71
49
  insertBefore(this.node, child, beforeChild);
72
50
  }
73
- // Nomination for teardown rides on `node.ts`'s `removeChild`, and only NOMINATES for the reason
74
- // stated there: a framework may spell a move as remove-then-reinsert, so the commit sweep
75
- // decides. The surface is one ordinary node, so it is not a second removal path that could miss
76
- // the sweep and leave a behavior — its timers included — outliving the surface.
51
+ // Nomination for teardown rides on node.ts's removeChild, and only nominates: a framework may
52
+ // spell a move as remove-then-reinsert, so the commit sweep decides.
77
53
  removeChild(child) {
78
54
  removeChild(this.node, child);
79
55
  }
@@ -81,63 +57,41 @@ export class SymbioteSurface {
81
57
  for (const child of this.children)
82
58
  removeChild(this.node, child);
83
59
  }
84
- /**
85
- * Release every host behavior still standing under this surface, at unmount.
86
- *
87
- * The sweep above cannot answer this: it only sees nodes a `removeChild` NOMINATED, and an
88
- * unmount removes nothing — the adapter drops the whole surface. Without it every node keeps its
89
- * `afterCommit` registration and its timers, and a restarted surface's commits drain the dead
90
- * one's hooks forever.
91
- */
60
+ // Release every host behavior still standing under this surface, at unmount. The sweep above
61
+ // can't answer this: it only sees nodes removeChild nominated, and an unmount removes nothing —
62
+ // the adapter drops the whole surface.
92
63
  teardown() {
93
- // The nominations first: an adapter that empties the surface and disposes it without a commit
94
- // in between (React's `clearContainer`) never reaches the commit sweep, and the walk below
95
- // cannot see those nodes either — they already left the tree.
64
+ // The nominations first: an adapter that empties and disposes the surface without a commit in
65
+ // between never reaches the commit sweep, and the walk below can't see those nodes either.
96
66
  sweepDetachedBehaviors(this.children, detachAnimatedProps);
97
67
  teardownSubtree(this.node, detachAnimatedProps);
98
68
  }
99
69
  // Synchronous commit: used by React's resetAfterCommit, which already batches per logical update.
100
70
  commit() {
101
- // A SUPERSEDED surface still flushes its ops and still names every other root — its teardown is
102
- // what carries the removals — but it must not complete a root another surface now owns. Fast
103
- // Refresh and the focus lifecycle re-mount the same rootTag, so the old surface's teardown
104
- // commit lands AFTER the new one's mount commit and would hand Fabric the emptied tree.
105
- // An UNREGISTERED surface is not superseded — nobody took the root, so its final emptied tree
106
- // is still the truth for it. Only a live OTHER owner suppresses the op.
107
- // The teardown half of the behavior lifecycle. It runs AFTER the ops are applied — it decides
108
- // what really left by asking the host for a parent, and the host does not know about a removal
109
- // it has not been handed — but BEFORE the root is completed, so a behavior's parting writes
110
- // (ScrollView taking its forced `scrollEventThrottle` back) ride this commit instead of owing
111
- // another one. `flushOps` is that split: apply, do not publish.
112
- //
113
- // `removeChild` only NOMINATES — a framework spells a move as remove-then-reinsert, so tearing
114
- // down at the call would kill a machine that comes back in the same batch.
71
+ // A superseded surface still flushes its ops and names every other root (its teardown carries
72
+ // the removals), but must not complete a root another surface now owns — Fast Refresh and the
73
+ // focus lifecycle can re-mount the same rootTag while an old teardown commit is still pending.
74
+ // The behavior-teardown half runs after ops are applied (asking the host for a parent needs the
75
+ // removal already handed over) but before the root completes, so a behavior's parting writes
76
+ // ride this commit instead of owing another one. flushOps is that split: apply, don't publish.
115
77
  flushOps();
116
- // GUARDED AT THE CALL SITE, not inside the sweep — `this.children` is a host read that builds
117
- // the whole top-level list, and it would run on every commit for a sweep that had nothing to do.
78
+ // Guarded at the call site, not inside the sweep — this.children is a host read that builds the
79
+ // whole top-level list, and it would run on every commit for a sweep with nothing to do.
118
80
  if (hasDetachCandidates()) {
119
81
  sweepDetachedBehaviors(this.children, detachAnimatedProps);
120
82
  }
121
83
  const owner = surfaces.get(this.rootTag);
122
84
  const superseded = owner !== undefined && owner !== this;
123
85
  commitSurfaceOps(superseded ? undefined : this.rootTag, this.node, SymbioteSurface.others(this));
124
- // Fresh Fabric handles are now assigned, so the three things that could not run before one
125
- // existed all drain here — this is the moment the old `commitChildren` drained them too.
126
- //
127
- // `notifyCommitted` releases the imperative waiters (`whenCommitted`); `runPostCommitHooks` the
128
- // consumers that needed a committed TAG and ran too early, which is the Animated native driver
129
- // binding a props node under an async-batched commit; `runDeferredAttaches` the half of a host
130
- // behavior whose setup needs a tag (a view command, an event attach). The predicate is passed in
131
- // rather than imported by `host-behavior.ts`, keeping that dependency one-directional — a cycle
132
- // there is a live hazard under Metro's `inlineRequires`.
86
+ // Fresh Fabric handles are now assigned, so the three things that couldn't run before one
87
+ // existed all drain here: notifyCommitted releases the imperative waiters, runPostCommitHooks
88
+ // the consumers that needed a committed tag, runDeferredAttaches the same for a host behavior.
133
89
  notifyCommitted();
134
90
  runPostCommitHooks();
135
91
  runDeferredAttaches(isNodeCommitted);
136
- // After the setup half, never before it: a node carrying both hooks has `attachAfterCommit`
137
- // seed the mirrors `afterCommit` then compares against. Unlike the two above it asks only
138
- // "props were published", so it is NOT gated on the commit having made native calls — a fold
139
- // that strips a prop makes its own commit byte-identical, and the hook that must react to the
140
- // flip would be the one the flip cannot wake.
92
+ // After the setup half, never before: a node carrying both hooks has attachAfterCommit seed the
93
+ // mirrors afterCommit then compares against. Unlike the two above, this asks only "props were
94
+ // published", so it isn't gated on the commit having made native calls.
141
95
  runCommittedHooks(isNodeCommitted);
142
96
  }
143
97
  // Coalesced commit: for reactive frameworks that emit many mutations per tick. Collapses to a
@@ -152,9 +106,8 @@ export class SymbioteSurface {
152
106
  });
153
107
  }
154
108
  }
155
- // Every live surface, so the microtask flush in `imperative.ts` can commit the one a queued write
156
- // named. Registered from here rather than imported there: a surface owns its own commit, and
157
- // reaching into it from the imperative half would put back the cycle that split exists to remove.
109
+ // Every live surface, so the microtask flush in imperative.ts can commit the one a queued write
110
+ // named. Registered from here rather than imported there, keeping that dependency one-directional.
158
111
  const surfaces = new Map();
159
112
  registerSurfaceCommit(rootTag => {
160
113
  surfaces.get(rootTag)?.commit();
@@ -1,7 +1,5 @@
1
- // Mirrors RN's TextInput/TextInputState: the single currently-focused input, tracked
2
- // JS-side because native exposes no focus getter. TextInput reports focus/blur here so
3
- // Keyboard.dismiss can blur whatever holds focus without a ref, exactly how RN's
4
- // dismissKeyboard() works (blurTextInput(currentlyFocusedInput())).
1
+ // Mirrors RN's TextInput/TextInputState: the single currently-focused input, tracked JS-side since
2
+ // native exposes no focus getter. Lets Keyboard.dismiss blur whatever holds focus without a ref.
5
3
  import { dispatchViewCommand, propOf } from './imperative.js';
6
4
  import { dlog } from './debug.js';
7
5
  let currentlyFocused = null;
@@ -18,10 +16,8 @@ export function setInputBlurred(node) {
18
16
  if (currentlyFocused === node)
19
17
  currentlyFocused = null;
20
18
  }
21
- // Imperative blur: drive the native `blur` view command and drop the tracked focus.
22
- // Used by TextInput.blur() and Keyboard.dismiss(). A no-op if this node isn't the
23
- // currently-focused one — mirrors RN's TextInputState.blurTextInput, which guards the
24
- // same way so blurring an already-unfocused input never reaches native.
19
+ // Imperative blur: drives the native `blur` command and drops tracked focus. A no-op if this
20
+ // node isn't the currently-focused one, mirroring RN's TextInputState.blurTextInput guard.
25
21
  export function blurTextInput(node) {
26
22
  if (node === null || currentlyFocused !== node)
27
23
  return;
@@ -1,14 +1,8 @@
1
- // Per-touch position/time tracking, ported from RN's
2
- // react-native-renderer/.../legacy-events/ResponderTouchHistoryStore.js. PanResponder's
3
- // multitouch dx/vx math needs each touch's own previous->current delta (RN counts only
4
- // touches that moved since `_accountsForMovesUpTo`), which a grant-relative centroid of
5
- // ALL live touches cannot reconstruct. We maintain the bank as touches flow and ATTACH
6
- // `touchHistory` onto the nativeEvent reaching responder handlers, exactly how
7
- // ResponderEventPlugin.js sets `*.touchHistory`.
8
- //
9
- // events/index.ts consumes only this file's public surface (recordTouchTrack,
10
- // attachTouchHistory, resetTouchHistory, touchHistory); everything else here is a
11
- // private implementation detail of the bank.
1
+ // Per-touch position/time tracking, ported from RN's ResponderTouchHistoryStore. PanResponder's
2
+ // multitouch dx/vx math needs each touch's own previous->current delta, which a grant-relative
3
+ // centroid of all live touches can't reconstruct; the bank attaches to responder events.
4
+ // events/index.ts consumes only this file's public surface (recordTouchTrack, attachTouchHistory,
5
+ // resetTouchHistory, touchHistory); everything else here is a private implementation detail.
12
6
  import { isRecord } from './type-guards.js';
13
7
  // RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
14
8
  // events may carry larger or absent ids), we just skip anything out of a sane range.
@@ -1,13 +1,6 @@
1
1
  import type { IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, IMeasureOnSuccess, IRootTag } from './fabric';
2
2
  import { type IMutationBatch, type IMutationHandle } from './mutation-buffer';
3
- /** What Fabric currently holds for one node — the three fields every imperative call is aimed at. */
4
3
  export type ICommittedRecord = {
5
- /**
6
- * OPAQUE, and typed that way because it is: each host puts its own thing here — the native host a
7
- * `ShadowNode`, a headless one whatever it committed — and the field's only contract is identity.
8
- * It used to say `IFabricNode`, which promised a Fabric node from every host and was true of one.
9
- * `IFabricNode` is a brand with no members, so nothing a caller could do with it is lost.
10
- */
11
4
  handle: object;
12
5
  tag: number;
13
6
  rootTag: IRootTag;
@@ -16,88 +9,24 @@ export interface ITreeCensus {
16
9
  nodes: number;
17
10
  anchors: number;
18
11
  emptyRawTexts: number;
19
- /** Nodes that actually become a Fabric view: `nodes` minus everything the commit skips. */
20
12
  renderable: number;
21
- /** children.length of every parent holding at least one skipped child, widest first. */
22
13
  flattenWidths: number[];
23
14
  }
24
- /**
25
- * The census of a tree nobody counted — what a host with no walk of its own answers, and what
26
- * `censusRetainedTree` answers when no host is installed at all.
27
- *
28
- * Not exported from the package barrel: it is a HOST-author constant, and an app reading a census
29
- * asserts against a mounted tree, where a zero from here cannot be mistaken for a zero from a real
30
- * empty tree.
31
- */
32
15
  export declare const EMPTY_CENSUS: ITreeCensus;
33
- /**
34
- * Everything JS asks of the tree it no longer owns.
35
- *
36
- * No read here is on a commit path — they run at GESTURE or lifecycle rate (a host behavior seeing
37
- * the props it reacts to, an app measuring a ref, a framework seam navigating what it just built).
38
- * This comment used to conclude from that that the crossing cost was irrelevant, at "~10 reads per
39
- * touch". Counted, it was 18 per EVENT and 19 per drag FRAME — 60 times a second for as long as a
40
- * finger is down — because a gesture is not one touch and a walk that asks per level pays per
41
- * level. Both are 1 now.
42
- *
43
- * `parentsOf`, `subtreesOf` and `ancestorsOf` are what that cost: each answers exactly what its
44
- * singular twin answers, in ONE crossing, for the three walks whose size is the TREE's rather than
45
- * a node's — teardown down, dispatch and responder negotiation up.
46
- */
47
16
  export type ITreeHost = {
48
17
  applyOps: (batch: IMutationBatch) => void;
49
18
  propOf: (handle: object, key: string) => unknown;
50
19
  propsOf: (handle: object) => Readonly<Record<string, unknown>>;
51
20
  markPropsDirty: (handle: object) => void;
52
21
  committedRecordOf: (handle: object) => ICommittedRecord | undefined;
53
- /**
54
- * A TEST read: the PAYLOAD the last commit handed Fabric for this node, `undefined` before one.
55
- *
56
- * It exists because the alternative reads are both blind. `Props::getDebugProps()` is a
57
- * hand-written selection per component — `RCTSinglelineTextInputView` reports `testID` and nothing
58
- * else — and RN's complete `Props::rawProps` needs `RN_SERIALIZABLE_STATE`, which pulls fbjni into
59
- * `State` and does not compile on a host build. So the rules in `SymbioteFabricProps.cpp` were
60
- * verifiable only through their TypeScript twins, which is the drift this read closes.
61
- *
62
- * Not on any commit path, and it adds no bookkeeping: the bag is already retained per node as the
63
- * next commit's diff baseline.
64
- *
65
- * It answers what we SENT, not what Fabric parsed — a key no ViewConfig declares is still in here.
66
- */
67
22
  committedPayloadOf: (handle: object) => Readonly<Record<string, unknown>> | undefined;
68
23
  parentOf: (handle: object) => object | undefined;
69
24
  childrenOf: (handle: object) => readonly object[];
70
25
  firstChildOf: (handle: object) => object | undefined;
71
26
  nextSiblingOf: (handle: object) => object | undefined;
72
27
  parentsOf: (handles: readonly object[]) => readonly (object | undefined)[];
73
- /** Each root and every descendant, PRE-ORDER, concatenated in root order. */
74
28
  subtreesOf: (roots: readonly object[]) => readonly object[];
75
- /**
76
- * The same walk, narrowed to the nodes a TEARDOWN has work for — each root, every node carrying
77
- * an intrinsic tag, and every node between the two.
78
- *
79
- * It exists because the sweep's whole cost is how WIDE this walk is: a thousand-row clear crossed
80
- * ten thousand handles to release a thousand machines, and eight in ten of those nodes were plain
81
- * views the sweep marked and did nothing else with. An ancestor of a tagged node has to come back
82
- * too, or a framework that returns an interior node on its own would never re-arm what hangs
83
- * beneath it (`host-behavior.test.ts` guards exactly that shape).
84
- *
85
- * Not a replacement for `subtreesOf`: an animated binding is per node and carries no tag, so
86
- * `host-access.ts` asks for the full walk whenever one exists.
87
- */
88
29
  teardownSubtreesOf: (roots: readonly object[]) => readonly object[];
89
- /**
90
- * The node itself and every ancestor above it, DEEPEST FIRST.
91
- *
92
- * The upward twin of `subtreesOf`, and it exists for the same reason: a walk that asks per LEVEL
93
- * pays a crossing per level. Event dispatch needs this chain for every event — capture reads it
94
- * reversed, bubble reads it forward — and the responder negotiation needs it again on every frame
95
- * of every drag. Measured before it existed: 18 crossings per event on a depth-8 chain, then 9
96
- * once the two phases shared one walk, against the 1 an answer from here costs.
97
- *
98
- * A SURFACE is included, exactly as `parentOf`'s answer is — stopping at one is
99
- * `host-access.ts`'s job, and it does it by reading the answer's `component`.
100
- */
101
30
  ancestorsOf: (handle: object) => readonly object[];
102
31
  census: (roots: readonly object[]) => ITreeCensus;
103
32
  dispatchCommand: (handle: object, commandName: string, args: readonly unknown[]) => void;
@@ -107,137 +36,32 @@ export type ITreeHost = {
107
36
  measureLayout: (handle: object, relativeTo: object, onFail: () => void, onSuccess: IMeasureLayoutOnSuccess) => void;
108
37
  setIsJSResponder: (handle: object, isResponder: boolean, blockNativeResponder: boolean) => void;
109
38
  };
110
- /**
111
- * Install the tree host. `installFabric()` (test-utils) calls this with the TypeScript applier; the
112
- * native module installs itself the same way once its bindings carry these reads.
113
- *
114
- * Passing `undefined` uninstalls, so a fixture can prove a path degrades rather than throws.
115
- */
116
39
  export declare function setTreeHost(next: ITreeHost | undefined): void;
117
- /** The installed host, or `undefined` on a runtime that has none. */
118
40
  export declare function treeHost(): ITreeHost | undefined;
119
41
  export declare function registerBeforeFlush(listener: () => void): () => void;
120
- /**
121
- * Collect what the listeners are holding, WITHOUT draining.
122
- *
123
- * Separate from `flushOps` because a read may legitimately decide it needs no drain — `parentOf`
124
- * skips one for a node whose placement is not pending — and skipping the drain must not also skip
125
- * asking. A held write is still a write, and a reader that cannot see it is reading a stale tree.
126
- */
127
42
  export declare function settleBeforeFlush(): void;
128
43
  export declare function flushOps(): void;
129
- /**
130
- * Record a surface's commit and drain the buffer into the host.
131
- *
132
- * A SURFACE IS AN ANCHOR: an anchor is a node whose children belong to its parent's list, and a
133
- * surface is a node whose children belong to the root child set. Same shape, which is why `OP_COMMIT`
134
- * names one node and the host needs no `rootTag -> map`.
135
- *
136
- * `others` is every OTHER live surface, and it exists because the buffer is GLOBAL while a commit
137
- * names ONE root. A framework can mutate a tree that belongs to a different surface than the one
138
- * whose renderer it is holding — a portal, a tunnel, any cross-surface move — and every adapter
139
- * then asks its OWN surface to commit. The ops reach the host either way, so the other surface is
140
- * left correctly updated in the tree and never handed to `completeRoot`: a stale Fabric root that
141
- * nothing will refresh until something unrelated dirties it. Naming every root here is what makes a
142
- * commit mean "flush what changed" rather than "flush the surface I happen to be bound to".
143
- *
144
- * A single-surface app — every example, and the overwhelming case — passes an empty list and the
145
- * behaviour is byte-identical to naming only itself.
146
- */
147
44
  export declare function commitSurfaceOps(rootTag: IRootTag | undefined, surface: IMutationHandle, others?: readonly (readonly [IRootTag, IMutationHandle])[]): void;
148
45
  export interface ICommitProfile {
149
46
  commits: number;
150
47
  propWrites: number;
151
- /**
152
- * Fresh Fabric families minted in this window — the SAME quantity a stock React Native app counts
153
- * by wrapping `global.nativeFabricUIManager.createNode`, and the only like-for-like census left
154
- * between the two stacks: our creates are issued from C++ (`SymbioteTree.cpp`,
155
- * `uiManager.createNode`) and never touch that global, so a JS wrapper over it reads zero here by
156
- * construction. Two arms whose node counts differ are not one workload, whatever their
157
- * milliseconds say, so this belongs next to the writes rather than behind a surface id.
158
- */
159
48
  nodesCreated: number;
160
- /**
161
- * How many times `applyOps` was ENTERED for this window — the number of JSI crossings the buffer
162
- * actually cost, as against the one the architecture promises.
163
- *
164
- * A whole create should read 2. It reads more when something READS the tree while the tree is
165
- * being built, because a read is a batch boundary: the buffer has to drain before the answer can
166
- * be given, so a framework navigating what it is inserting enters `applyOps` once per mutation.
167
- * `small-batch-crossing-cost.itest.ts` prices an empty prologue at 1.5-4.4 us, so ten thousand
168
- * boundaries is tens of milliseconds that no node count and no write count can see — which is
169
- * exactly the shape of a cost that shows up on a device and not in a fixture.
170
- */
171
49
  applyCalls: number;
172
- /**
173
- * `applyOps` end to end, and the part of it that reads the buffer out of JS.
174
- *
175
- * THE ONE QUESTION A DEVICE HAS TO ANSWER and a fixture cannot. Our crossing is a single call
176
- * carrying a 12 000-entry array that C++ walks element by element through JSI; stock's is ten
177
- * thousand calls carrying scalars. On the harness's JavaScriptCore that walk is ~4 ms of a ~48 ms
178
- * `applyOps`. Hermes is a different JSI implementation with different array-read costs and nothing
179
- * headless can price it, so the number has to be read on a phone — near 4 ms and the buffer is
180
- * innocent, tens of milliseconds and it is most of the gap the device reports.
181
- */
182
50
  applyMs: number;
183
51
  decodeMs: number;
184
52
  }
185
- /**
186
- * NOTE: reading this now DRAINS the surface telemetry too — `nodesCreated` is folded in from
187
- * `readSurfaceTelemetry`, which zeroes on read in C++. A sampler polling this on an interval
188
- * therefore empties what a later `readSurfaceTelemetry` call would have reported.
189
- */
190
53
  export declare function readCommitProfile(): ICommitProfile;
191
54
  export type ISurfaceTelemetry = {
192
55
  layoutMs: number;
193
56
  textMs: number;
194
- /**
195
- * `ShadowTree::commit`'s own window — and **NOT** `materialize`'s.
196
- *
197
- * This field's doc used to claim it was the clone-on-write walk, and F-80/F-81/F-82 each read it
198
- * that way and concluded the native pipeline was too small to matter. `materialize` runs inside
199
- * `kOpCommit` BEFORE `completeSurface` is called, so it is outside both this window and layout's.
200
- * Pricing our own walk means timing `applyOps` from JS and subtracting these two.
201
- */
202
57
  commitMs: number;
203
58
  layoutNodes: number;
204
59
  textMeasures: number;
205
- /**
206
- * How many parents took the targeted-replace path since the last read, zeroed on read.
207
- *
208
- * OURS, not React Native's. It is a LIVENESS signal, not a performance one: every test in this
209
- * repository stays green when `canReplaceInPlace` is off, which is how it spent eighteen months
210
- * disabled. Assert it is non-zero wherever the fast path is the point of the test.
211
- */
212
60
  targetedReplaces: number;
213
- /**
214
- * `materialize`'s own walk, and the fields below break it down. OURS, zeroed on read.
215
- *
216
- * The doc above says the walk falls outside every window React Native times, which left it
217
- * priceable only by subtraction — and a subtraction gives a budget, not an address. Measured
218
- * 2026-09-17: ~200 ms of a 327 ms headless create sat here with nothing inside it named.
219
- *
220
- * `walkMs` is the single entry point in `kOpCommit`; `propsMs` / `rawPropsMs` / `createNodeMs` /
221
- * `appendChildMs` / `diffPropsMs` are per-node sums inside it and do NOT add up to it — what is
222
- * left over is the walk's own bookkeeping.
223
- */
224
61
  walkMs: number;
225
- /** `fabricProps` alone, on both the create and the clone path. The fold LOOKUP is billed apart. */
226
62
  propsMs: number;
227
- /**
228
- * Asking a node whether it carries a `payloadFold`: a JSI property read, and on a hit a
229
- * `jsi::Function` allocation. The fold's own CALL is inside `propsMs`, where `fabricProps` makes
230
- * it. Separated because the two answer different questions — how big the payload is, against how
231
- * much the seam to JS costs to reach.
232
- */
233
63
  foldLookupMs: number;
234
- /** How many nodes the lookup found one on. Zero makes `foldLookupMs` pure probe cost. */
235
64
  foldsFound: number;
236
- /**
237
- * Inside a fold that runs, split three ways because a fold's CONTRACT is bag in, bag out: both
238
- * conversions walk every key of the node whatever the fold actually reads. If the conversions
239
- * dominate, the fix is a narrower contract; if `foldCallMs` does, the fix is not having a fold.
240
- */
241
65
  foldToJsMs: number;
242
66
  foldCallMs: number;
243
67
  foldFromJsMs: number;
@@ -246,124 +70,30 @@ export type ISurfaceTelemetry = {
246
70
  createNodeMs: number;
247
71
  appendChildMs: number;
248
72
  diffPropsMs: number;
249
- /** Nodes that minted a fresh Fabric family, were cloned, or were returned untouched. */
250
73
  nodesCreated: number;
251
74
  nodesCloned: number;
252
75
  nodesReused: number;
253
- /**
254
- * `applyOps`' own decode, per created element, and its three biggest parts.
255
- *
256
- * A different question from the walk's. The walk asks what Fabric charges; this asks what it costs
257
- * US to turn one op into one node — and the buffer architecture only pays for itself if that is
258
- * well under the per-node JSI call it replaces. On `build-release` it was not, which is why these
259
- * exist. `publishMs` contains `nativeStateMs`; neither contains `instanceHandleMs`.
260
- */
261
76
  decodeMs: number;
262
77
  instanceHandleMs: number;
263
78
  publishMs: number;
264
79
  nativeStateMs: number;
265
80
  nodesDecoded: number;
266
- /**
267
- * `kOpSetProp` and, inside it, the JS value -> `folly::dynamic` conversion.
268
- *
269
- * `setPropMs` skips the two early exits (deleting an absent key, and a value equal to the one
270
- * standing), so it under-counts exactly the cheap paths; `propConvertMs` has no such hole.
271
- */
272
81
  setPropMs: number;
273
82
  propConvertMs: number;
274
83
  setProps: number;
275
- /**
276
- * The two `setProp` ops that changed nothing, counted apart because they are not the same waste.
277
- *
278
- * `deletesOfAbsent` leaves before the value conversion and costs a hash lookup. `writesOfUnchanged`
279
- * leaves AFTER it, so the adapter has already paid the JSI -> `folly::dynamic` crossing for a value
280
- * that changes nothing — that is the expensive one, and it is what the device benchmark's
281
- * `WRITES n/m` second figure reports.
282
- */
283
84
  deletesOfAbsent: number;
284
85
  writesOfUnchanged: number;
285
- /**
286
- * How well the buffer's value interning worked: entries in the batch's value table, and how many
287
- * of them an op actually reached and converted.
288
- *
289
- * `setProps / valueEntries` is the dedup achieved. A ratio near 1 means the values are unique —
290
- * which is a fact about what the caller HANDS the buffer, not about the interning: a style slot
291
- * rebuilt per node arrives as a fresh reference and cannot be folded with anything.
292
- */
293
86
  valueEntries: number;
294
87
  valueConversions: number;
295
- /**
296
- * `applyOps` end to end, plus the two parts of it that are neither a create nor a prop write.
297
- *
298
- * `applyMs` is the whole native call, so `applyMs` minus `decodeMs` / `setPropMs` /
299
- * `stringDecodeMs` / `structureMs` is what the op loop itself costs — the books close here.
300
- */
301
88
  applyMs: number;
302
- /**
303
- * How many nodes the C++ tree is holding right now — a LEVEL, not a total, and the one counter
304
- * here that is not drained on read.
305
- *
306
- * What it is for: JS ownership anchors the C++ side (a node lives while a parent holds it or while
307
- * JS names it through `NativeState`), so a heap reading proves the JS half was released and only
308
- * infers the other. This is the other half as a reading.
309
- */
310
89
  liveNodes: number;
311
90
  stringDecodeMs: number;
312
- /** Every append / insert / remove op together. */
313
91
  structureMs: number;
314
- /** Inside `structureMs`: promoting a node's weak handle reference to a strong one. */
315
92
  holdHandleMs: number;
316
- /**
317
- * `subtreesOf` — the batched host read the teardown sweep makes, and how many handles it returned.
318
- *
319
- * Not on a commit path and timed anyway: it hands JS a handle for every node in every removed
320
- * subtree, which on a 1 000-row clear is ten thousand. Whether that time is the crossing or the JS
321
- * loop above it decides whether the torn-down mark is worth moving into C++.
322
- */
323
93
  hostReadMs: number;
324
94
  hostReadHandles: number;
325
- /**
326
- * How many times `applyOps` was entered since the last read.
327
- *
328
- * The string and value tables are interned PER BATCH, so a driver that flushes in many small
329
- * batches cannot fold a repeated value across them. This is what distinguishes "this adapter sends
330
- * more values" from "this adapter sends the same values in more batches" — two very different
331
- * findings that look identical in `valueEntries` alone.
332
- */
333
95
  applyCalls: number;
334
96
  };
335
- /**
336
- * RN's own commit telemetry for a surface THIS HOST NEED NOT HAVE DRIVEN, or `undefined` when the
337
- * runtime cannot answer.
338
- *
339
- * It exists for one comparison and should not be reached for anything else: `readCommitProfile`
340
- * accumulates inside our own commit, so it can only ever describe a tree we built, and the standing
341
- * open question is whether a tree-wide text re-measure is something we cause or something a Fabric
342
- * commit costs whoever drives it. Point this at a surface React Native's OWN renderer committed and
343
- * the two numbers are directly comparable — same device, same RN, same tree.
344
- *
345
- * `undefined` rather than zeroes on a runtime without the binding, deliberately: zeroes would read
346
- * as "the other renderer measures no text", which is precisely the claim under test.
347
- */
348
97
  export declare function readSurfaceTelemetry(surfaceId: number): ISurfaceTelemetry | undefined;
349
- /**
350
- * Arm the C++ half's diagnostics (`SymbioteDebug.h`), which `installBindings` already did from
351
- * `DEBUG=1` / `globalThis.__SYMBIOTE_DEBUG__` at install.
352
- *
353
- * This is for the LATER toggle — the runtime escape hatch `debug.ts` documents for hosts where the
354
- * env is not reachable. Without it a `globalThis.__SYMBIOTE_DEBUG__ = true` typed into a running app
355
- * would flip the JS half and silently leave the engine's own half dark, which is the surprise worth
356
- * the six lines.
357
- */
358
98
  export declare function setNativeDebug(enabled: boolean): void;
359
- /**
360
- * Drain what the C++ half has logged since the last call.
361
- *
362
- * The reason those lines are retained at all rather than only written to stderr: a diagnostic nobody
363
- * can assert on is one that rots. This is what lets a test say "the engine warned about that" —
364
- * see `core/engine/cpp/tests/js/native-debug-log.itest.ts`.
365
- *
366
- * An empty array on a runtime without the binding, not `undefined`: the question "what was logged"
367
- * has an honest empty answer, unlike the telemetry read above, where a zero would be a false claim.
368
- */
369
99
  export declare function takeNativeDebugLog(): readonly string[];