@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.
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +1 -5
- package/build/back-handler/index.js +0 -6
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/index.js +2 -2
- package/build/fabric-props.js +74 -179
- package/build/fabric.d.ts +0 -13
- package/build/fabric.js +18 -38
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +0 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.js +10 -23
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +0 -11
- package/build/image-source-write.js +14 -34
- package/build/imperative.d.ts +0 -28
- package/build/imperative.js +42 -92
- package/build/index.d.ts +1 -0
- package/build/index.js +24 -37
- package/build/mutation-buffer.d.ts +0 -177
- package/build/mutation-buffer.js +137 -308
- package/build/native-engine.d.ts +0 -102
- package/build/native-engine.js +38 -98
- package/build/native-events.js +9 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +14 -31
- package/build/node.d.ts +0 -212
- package/build/node.js +338 -774
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +0 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- 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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
//
|
|
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
|
-
|
|
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
|
|
94
|
-
//
|
|
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
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
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
|
-
//
|
|
117
|
-
//
|
|
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
|
|
125
|
-
// existed all drain here
|
|
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
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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
|
|
156
|
-
// named. Registered from here rather than imported there
|
|
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
|
-
//
|
|
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:
|
|
22
|
-
//
|
|
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;
|
package/build/touch-history.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
// Per-touch position/time tracking, ported from RN's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
package/build/tree-host.d.ts
CHANGED
|
@@ -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[];
|