rnxsim 0.1.378 → 0.1.380
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/cli/commands/cpu-profile.ts +556 -50
- package/cli/commands/perf.ts +1 -0
- package/cli/commands/upload.ts +7 -1
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +7 -7
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/skills/rnx-perf/SKILL.md +68 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// rnx perf cpu — capture
|
|
1
|
+
// rnx perf cpu — capture sampled CPU traces from the page and every worker.
|
|
2
2
|
//
|
|
3
3
|
// the JS Self-Profiler API (`new Profiler()`) cannot be constructed in a
|
|
4
4
|
// dedicated-worker scope, and sootsim runs the guest app in the tenant worker,
|
|
5
5
|
// so the old in-worker approach was unsupported (F28). this implementation
|
|
6
|
-
// attaches Chromium's `Profiler` CDP domain to the worker
|
|
7
|
-
// which
|
|
6
|
+
// attaches Chromium's `Profiler` CDP domain to the page and worker targets —
|
|
7
|
+
// which returns the .cpuprofile shape directly for every execution context.
|
|
8
8
|
//
|
|
9
9
|
// dedicated workers are not top-level CDP targets, so we connect to the page
|
|
10
10
|
// target, `Target.setAutoAttach` to discover the worker child sessions, and
|
|
@@ -25,9 +25,12 @@
|
|
|
25
25
|
// also drives the interaction itself lives at
|
|
26
26
|
// scripts/debug/cdp-worker-cpu-profile.ts.
|
|
27
27
|
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { createHash } from 'node:crypto'
|
|
29
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { dirname, resolve } from 'node:path'
|
|
30
31
|
import { WebSocket } from 'ws'
|
|
32
|
+
import { getCliVersion } from '../../src/cli-version'
|
|
33
|
+
import { IS_STANDALONE } from '../standalone'
|
|
31
34
|
|
|
32
35
|
interface ProfileOptions {
|
|
33
36
|
port?: number
|
|
@@ -41,8 +44,15 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
|
41
44
|
class Cdp {
|
|
42
45
|
private ws: WebSocket
|
|
43
46
|
private nextId = 1
|
|
44
|
-
private pending = new Map<
|
|
45
|
-
|
|
47
|
+
private pending = new Map<
|
|
48
|
+
number,
|
|
49
|
+
{
|
|
50
|
+
res: (v: any) => void
|
|
51
|
+
rej: (e: any) => void
|
|
52
|
+
timeout: ReturnType<typeof setTimeout>
|
|
53
|
+
}
|
|
54
|
+
>()
|
|
55
|
+
private listeners = new Set<(method: string, params: any, sessionId?: string) => void>()
|
|
46
56
|
private ready: Promise<void>
|
|
47
57
|
|
|
48
58
|
constructor(wsUrl: string) {
|
|
@@ -54,12 +64,20 @@ class Cdp {
|
|
|
54
64
|
this.ws.on('message', (data) => {
|
|
55
65
|
const msg = JSON.parse(data.toString())
|
|
56
66
|
if (msg.id && this.pending.has(msg.id)) {
|
|
57
|
-
const { res, rej } = this.pending.get(msg.id)!
|
|
67
|
+
const { res, rej, timeout } = this.pending.get(msg.id)!
|
|
58
68
|
this.pending.delete(msg.id)
|
|
69
|
+
clearTimeout(timeout)
|
|
59
70
|
msg.error ? rej(new Error(JSON.stringify(msg.error))) : res(msg.result)
|
|
60
71
|
} else if (msg.method) {
|
|
61
|
-
for (const l of this.listeners) l(msg.method, msg.params)
|
|
72
|
+
for (const l of this.listeners) l(msg.method, msg.params, msg.sessionId)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
this.ws.on('close', () => {
|
|
76
|
+
for (const [id, request] of this.pending) {
|
|
77
|
+
clearTimeout(request.timeout)
|
|
78
|
+
request.rej(new Error(`CDP connection closed with request ${id} pending`))
|
|
62
79
|
}
|
|
80
|
+
this.pending.clear()
|
|
63
81
|
})
|
|
64
82
|
}
|
|
65
83
|
|
|
@@ -67,7 +85,7 @@ class Cdp {
|
|
|
67
85
|
return this.ready
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
on(l: (method: string, params: any) => void) {
|
|
88
|
+
on(l: (method: string, params: any, sessionId?: string) => void) {
|
|
71
89
|
this.listeners.add(l)
|
|
72
90
|
}
|
|
73
91
|
|
|
@@ -78,7 +96,11 @@ class Cdp {
|
|
|
78
96
|
): Promise<any> {
|
|
79
97
|
const id = this.nextId++
|
|
80
98
|
return new Promise((res, rej) => {
|
|
81
|
-
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
this.pending.delete(id)
|
|
101
|
+
rej(new Error(`CDP ${method} timed out after 5s`))
|
|
102
|
+
}, 5_000)
|
|
103
|
+
this.pending.set(id, { res, rej, timeout })
|
|
82
104
|
this.ws.send(
|
|
83
105
|
JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }),
|
|
84
106
|
)
|
|
@@ -94,10 +116,135 @@ class Cdp {
|
|
|
94
116
|
|
|
95
117
|
interface CdpTarget {
|
|
96
118
|
type: string
|
|
119
|
+
title?: string
|
|
97
120
|
url: string
|
|
98
121
|
webSocketDebuggerUrl: string
|
|
99
122
|
}
|
|
100
123
|
|
|
124
|
+
interface AttachedTarget {
|
|
125
|
+
attachedAtMs: number
|
|
126
|
+
detached: boolean
|
|
127
|
+
detachedAtMs?: number
|
|
128
|
+
error?: string
|
|
129
|
+
profile?: any
|
|
130
|
+
profileStartedAtMs?: number
|
|
131
|
+
profileStoppedAtMs?: number
|
|
132
|
+
role: string
|
|
133
|
+
sessionId?: string
|
|
134
|
+
started: boolean
|
|
135
|
+
title: string
|
|
136
|
+
type: string
|
|
137
|
+
url: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CpuProfileInteractionBarrier {
|
|
141
|
+
marker: string
|
|
142
|
+
reachedAtEpochMs: number
|
|
143
|
+
releasedBy: string
|
|
144
|
+
releasedAtEpochMs: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface CpuProfileCoverage {
|
|
148
|
+
coverageMs: number
|
|
149
|
+
coverageRatio: number
|
|
150
|
+
startedOffsetMs: number | null
|
|
151
|
+
stoppedOffsetMs: number | null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function cpuProfileCoverage(
|
|
155
|
+
profileStartedAtMs: number | undefined,
|
|
156
|
+
profileStoppedAtMs: number | undefined,
|
|
157
|
+
windowStartedAtMs: number,
|
|
158
|
+
windowEndedAtMs: number,
|
|
159
|
+
): CpuProfileCoverage {
|
|
160
|
+
const requestedMs = Math.max(0, windowEndedAtMs - windowStartedAtMs)
|
|
161
|
+
if (
|
|
162
|
+
profileStartedAtMs === undefined ||
|
|
163
|
+
profileStoppedAtMs === undefined ||
|
|
164
|
+
requestedMs === 0
|
|
165
|
+
) {
|
|
166
|
+
return {
|
|
167
|
+
coverageMs: 0,
|
|
168
|
+
coverageRatio: 0,
|
|
169
|
+
startedOffsetMs:
|
|
170
|
+
profileStartedAtMs === undefined ? null : profileStartedAtMs - windowStartedAtMs,
|
|
171
|
+
stoppedOffsetMs:
|
|
172
|
+
profileStoppedAtMs === undefined ? null : profileStoppedAtMs - windowStartedAtMs,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const coverageMs = Math.max(
|
|
176
|
+
0,
|
|
177
|
+
Math.min(profileStoppedAtMs, windowEndedAtMs) -
|
|
178
|
+
Math.max(profileStartedAtMs, windowStartedAtMs),
|
|
179
|
+
)
|
|
180
|
+
return {
|
|
181
|
+
coverageMs,
|
|
182
|
+
coverageRatio: coverageMs / requestedMs,
|
|
183
|
+
startedOffsetMs: profileStartedAtMs - windowStartedAtMs,
|
|
184
|
+
stoppedOffsetMs: profileStoppedAtMs - windowStartedAtMs,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function cpuProfileActionsWithinWindow(
|
|
189
|
+
actionEpochMs: readonly number[],
|
|
190
|
+
windowStartedAtEpochMs: number,
|
|
191
|
+
windowEndedAtEpochMs: number,
|
|
192
|
+
): boolean {
|
|
193
|
+
return (
|
|
194
|
+
actionEpochMs.length > 0 &&
|
|
195
|
+
actionEpochMs.every(
|
|
196
|
+
(epochMs) =>
|
|
197
|
+
Number.isFinite(epochMs) &&
|
|
198
|
+
epochMs >= windowStartedAtEpochMs &&
|
|
199
|
+
epochMs <= windowEndedAtEpochMs,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function cpuProfileInteractionBarrierValid(
|
|
205
|
+
barrier: CpuProfileInteractionBarrier | null,
|
|
206
|
+
windowStartedAtEpochMs: number,
|
|
207
|
+
windowEndedAtEpochMs: number,
|
|
208
|
+
): boolean {
|
|
209
|
+
return (
|
|
210
|
+
barrier !== null &&
|
|
211
|
+
barrier.marker.length > 0 &&
|
|
212
|
+
barrier.releasedBy === 'profiler' &&
|
|
213
|
+
Number.isFinite(barrier.reachedAtEpochMs) &&
|
|
214
|
+
Number.isFinite(barrier.releasedAtEpochMs) &&
|
|
215
|
+
barrier.reachedAtEpochMs <= barrier.releasedAtEpochMs &&
|
|
216
|
+
barrier.releasedAtEpochMs >= windowStartedAtEpochMs &&
|
|
217
|
+
barrier.releasedAtEpochMs <= windowEndedAtEpochMs
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readInteractionBarrierState(
|
|
222
|
+
value: unknown,
|
|
223
|
+
): CpuProfileInteractionBarrier | null {
|
|
224
|
+
if (!value || typeof value !== 'object') return null
|
|
225
|
+
const marker = Reflect.get(value, 'marker')
|
|
226
|
+
const reachedAtEpochMs = Reflect.get(value, 'reachedAtEpochMs')
|
|
227
|
+
const releasedBy = Reflect.get(value, 'releasedBy')
|
|
228
|
+
const releasedAtEpochMs = Reflect.get(value, 'releasedAtEpochMs')
|
|
229
|
+
if (
|
|
230
|
+
typeof marker !== 'string' ||
|
|
231
|
+
typeof reachedAtEpochMs !== 'number' ||
|
|
232
|
+
typeof releasedBy !== 'string' ||
|
|
233
|
+
typeof releasedAtEpochMs !== 'number'
|
|
234
|
+
) {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
return { marker, reachedAtEpochMs, releasedBy, releasedAtEpochMs }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readActionEpochs(value: unknown): number[] {
|
|
241
|
+
if (!Array.isArray(value)) return []
|
|
242
|
+
return value.filter(
|
|
243
|
+
(epochMs): epochMs is number =>
|
|
244
|
+
typeof epochMs === 'number' && Number.isFinite(epochMs),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
101
248
|
export async function runCpuProfile(
|
|
102
249
|
args: string[],
|
|
103
250
|
opts: ProfileOptions,
|
|
@@ -108,6 +255,21 @@ export async function runCpuProfile(
|
|
|
108
255
|
return 1
|
|
109
256
|
}
|
|
110
257
|
const sampleInterval = Number(valueOf(args, '--sample-interval') ?? '0.1') // ms
|
|
258
|
+
const interactionBarrierEnabled = args.includes('--interaction-barrier')
|
|
259
|
+
const interactionBarrierTimeoutSeconds = Number(
|
|
260
|
+
valueOf(args, '--interaction-barrier-timeout') ?? '90',
|
|
261
|
+
)
|
|
262
|
+
if (
|
|
263
|
+
!Number.isFinite(interactionBarrierTimeoutSeconds) ||
|
|
264
|
+
interactionBarrierTimeoutSeconds <= 0
|
|
265
|
+
) {
|
|
266
|
+
console.error(' --interaction-barrier-timeout must be positive (seconds)')
|
|
267
|
+
return 1
|
|
268
|
+
}
|
|
269
|
+
const interactionBarrierLeaseMs = Math.min(
|
|
270
|
+
2_147_000_000,
|
|
271
|
+
Math.ceil((interactionBarrierTimeoutSeconds + duration + 5) * 1000),
|
|
272
|
+
)
|
|
111
273
|
const cdpPort = Number(
|
|
112
274
|
valueOf(args, '--cdp-port') ?? process.env.RNX_CDP_PORT ?? '9222',
|
|
113
275
|
)
|
|
@@ -141,65 +303,396 @@ export async function runCpuProfile(
|
|
|
141
303
|
|
|
142
304
|
const cdp = new Cdp(page.webSocketDebuggerUrl)
|
|
143
305
|
await cdp.waitOpen()
|
|
306
|
+
let interactionBarrierInstalled = false
|
|
307
|
+
const evaluatePage = async (expression: string): Promise<unknown> => {
|
|
308
|
+
const response = await cdp.send('Runtime.evaluate', {
|
|
309
|
+
expression,
|
|
310
|
+
returnByValue: true,
|
|
311
|
+
})
|
|
312
|
+
if (response?.exceptionDetails) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`page evaluation failed: ${response.exceptionDetails.text ?? 'unknown error'}`,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
return response?.result?.value
|
|
318
|
+
}
|
|
319
|
+
const releaseInteractionBarrier = (releasedBy: 'cleanup' | 'profiler') =>
|
|
320
|
+
evaluatePage(`(() => {
|
|
321
|
+
const release = Reflect.get(globalThis, '__sootsimReleaseCpuProfileBarrier')
|
|
322
|
+
return typeof release === 'function'
|
|
323
|
+
? Reflect.apply(release, globalThis, [${JSON.stringify(releasedBy)}])
|
|
324
|
+
: null
|
|
325
|
+
})()`)
|
|
144
326
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
327
|
+
if (interactionBarrierEnabled) {
|
|
328
|
+
const installed = await evaluatePage(`(() => {
|
|
329
|
+
const state = {
|
|
330
|
+
marker: null,
|
|
331
|
+
reachedAtEpochMs: null,
|
|
332
|
+
releasedBy: null,
|
|
333
|
+
releasedAtEpochMs: null,
|
|
334
|
+
}
|
|
335
|
+
let releaseGate = () => {}
|
|
336
|
+
const gate = new Promise((resolve) => {
|
|
337
|
+
releaseGate = resolve
|
|
338
|
+
})
|
|
339
|
+
Reflect.set(globalThis, '__sootsimCpuProfileBarrierState', state)
|
|
340
|
+
Reflect.set(globalThis, '__sootsimCpuProfileBarrier', async (marker) => {
|
|
341
|
+
if (state.marker === null) {
|
|
342
|
+
state.marker = String(marker)
|
|
343
|
+
state.reachedAtEpochMs = Date.now()
|
|
344
|
+
}
|
|
345
|
+
await gate
|
|
346
|
+
})
|
|
347
|
+
let lease = null
|
|
348
|
+
const releaseBarrier = (releasedBy) => {
|
|
349
|
+
if (state.releasedAtEpochMs === null) {
|
|
350
|
+
state.releasedAtEpochMs = Date.now()
|
|
351
|
+
state.releasedBy = String(releasedBy)
|
|
352
|
+
if (lease !== null) clearTimeout(lease)
|
|
353
|
+
releaseGate()
|
|
354
|
+
}
|
|
355
|
+
return { ...state }
|
|
356
|
+
}
|
|
357
|
+
lease = setTimeout(
|
|
358
|
+
() => releaseBarrier('lease'),
|
|
359
|
+
${interactionBarrierLeaseMs},
|
|
360
|
+
)
|
|
361
|
+
Reflect.set(globalThis, '__sootsimReleaseCpuProfileBarrier', releaseBarrier)
|
|
362
|
+
return true
|
|
363
|
+
})()`)
|
|
364
|
+
if (installed !== true) {
|
|
365
|
+
console.error(' failed to install the interaction profile barrier')
|
|
366
|
+
return 1
|
|
367
|
+
}
|
|
368
|
+
interactionBarrierInstalled = true
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// discover every child worker. keep detached entries in the manifest: a
|
|
372
|
+
// worker can exit during the capture, and deleting it used to make the
|
|
373
|
+
// command either mislabel the busiest survivor as "tenant" or lose the
|
|
374
|
+
// entire run when Profiler.stop hit the detached session first.
|
|
375
|
+
const workers = new Map<string, AttachedTarget>()
|
|
376
|
+
let captureStarted = false
|
|
377
|
+
let captureWindowClosed = false
|
|
378
|
+
const startPromises: Array<Promise<void>> = []
|
|
379
|
+
const discoveryPromises: Array<Promise<void>> = []
|
|
380
|
+
let attachmentGeneration = 0
|
|
381
|
+
let awaitedDiscoveryCount = 0
|
|
382
|
+
let awaitedStartCount = 0
|
|
383
|
+
const enableRecursiveAutoAttach = (sessionId?: string) =>
|
|
384
|
+
cdp.send(
|
|
385
|
+
'Target.setAutoAttach',
|
|
386
|
+
{
|
|
387
|
+
autoAttach: true,
|
|
388
|
+
waitForDebuggerOnStart: false,
|
|
389
|
+
flatten: true,
|
|
390
|
+
},
|
|
391
|
+
sessionId,
|
|
392
|
+
)
|
|
393
|
+
const startTarget = async (target: AttachedTarget) => {
|
|
394
|
+
try {
|
|
395
|
+
await cdp.send('Profiler.enable', {}, target.sessionId)
|
|
396
|
+
await cdp.send(
|
|
397
|
+
'Profiler.setSamplingInterval',
|
|
398
|
+
{ interval: sampleInterval * 1000 },
|
|
399
|
+
target.sessionId,
|
|
400
|
+
)
|
|
401
|
+
await cdp.send('Profiler.start', {}, target.sessionId)
|
|
402
|
+
target.started = true
|
|
403
|
+
target.profileStartedAtMs = performance.now()
|
|
404
|
+
} catch (error) {
|
|
405
|
+
target.error = error instanceof Error ? error.message : String(error)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const waitForStableTargetSetup = async () => {
|
|
409
|
+
while (true) {
|
|
410
|
+
const observedGeneration = attachmentGeneration
|
|
411
|
+
if (awaitedDiscoveryCount < discoveryPromises.length) {
|
|
412
|
+
const pendingDiscoveries = discoveryPromises.slice(awaitedDiscoveryCount)
|
|
413
|
+
awaitedDiscoveryCount = discoveryPromises.length
|
|
414
|
+
await Promise.all(pendingDiscoveries)
|
|
415
|
+
}
|
|
416
|
+
if (awaitedStartCount < startPromises.length) {
|
|
417
|
+
const pendingStarts = startPromises.slice(awaitedStartCount)
|
|
418
|
+
awaitedStartCount = startPromises.length
|
|
419
|
+
await Promise.all(pendingStarts)
|
|
420
|
+
}
|
|
421
|
+
// recursive auto-attach notifications can arrive just after the CDP
|
|
422
|
+
// command resolves. require a quiet generation before releasing work.
|
|
423
|
+
await sleep(50)
|
|
424
|
+
if (
|
|
425
|
+
observedGeneration === attachmentGeneration &&
|
|
426
|
+
awaitedDiscoveryCount === discoveryPromises.length &&
|
|
427
|
+
awaitedStartCount === startPromises.length
|
|
428
|
+
) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
147
433
|
cdp.on((method, params) => {
|
|
148
434
|
if (method === 'Target.attachedToTarget' && params.targetInfo?.type === 'worker') {
|
|
149
|
-
|
|
435
|
+
if (captureWindowClosed) return
|
|
436
|
+
const sessionId = params.sessionId
|
|
437
|
+
const targetInfo = params.targetInfo
|
|
438
|
+
const target: AttachedTarget = {
|
|
439
|
+
attachedAtMs: performance.now(),
|
|
440
|
+
detached: false,
|
|
441
|
+
role: cpuProfileTargetRole(targetInfo.url, targetInfo.title),
|
|
442
|
+
sessionId,
|
|
443
|
+
started: false,
|
|
444
|
+
title: targetInfo.title ?? '',
|
|
445
|
+
type: targetInfo.type,
|
|
446
|
+
url: targetInfo.url,
|
|
447
|
+
}
|
|
448
|
+
workers.set(sessionId, target)
|
|
449
|
+
attachmentGeneration += 1
|
|
450
|
+
discoveryPromises.push(
|
|
451
|
+
enableRecursiveAutoAttach(sessionId).catch((error: unknown) => {
|
|
452
|
+
target.error = `recursive auto-attach failed: ${error instanceof Error ? error.message : String(error)}`
|
|
453
|
+
}),
|
|
454
|
+
)
|
|
455
|
+
if (captureStarted) startPromises.push(startTarget(target))
|
|
456
|
+
}
|
|
457
|
+
if (method === 'Target.detachedFromTarget') {
|
|
458
|
+
const target = workers.get(params.sessionId)
|
|
459
|
+
if (target) {
|
|
460
|
+
target.detached = true
|
|
461
|
+
target.detachedAtMs = performance.now()
|
|
462
|
+
}
|
|
150
463
|
}
|
|
151
|
-
if (method === 'Target.detachedFromTarget') workers.delete(params.sessionId)
|
|
152
|
-
})
|
|
153
|
-
await cdp.send('Target.setAutoAttach', {
|
|
154
|
-
autoAttach: true,
|
|
155
|
-
waitForDebuggerOnStart: false,
|
|
156
|
-
flatten: true,
|
|
157
464
|
})
|
|
465
|
+
await enableRecursiveAutoAttach()
|
|
158
466
|
// auto-attach events arrive asynchronously after the call resolves.
|
|
159
467
|
await sleep(500)
|
|
160
|
-
|
|
161
|
-
|
|
468
|
+
await waitForStableTargetSetup()
|
|
469
|
+
const pageTarget: AttachedTarget = {
|
|
470
|
+
attachedAtMs: performance.now(),
|
|
471
|
+
detached: false,
|
|
472
|
+
role: 'page',
|
|
473
|
+
started: false,
|
|
474
|
+
title: page.title ?? '',
|
|
475
|
+
type: 'page',
|
|
476
|
+
url: page.url,
|
|
477
|
+
}
|
|
478
|
+
if (interactionBarrierEnabled) {
|
|
479
|
+
console.log(' waiting for the workload interaction barrier…')
|
|
480
|
+
const deadline = performance.now() + interactionBarrierTimeoutSeconds * 1000
|
|
481
|
+
let reached = false
|
|
482
|
+
while (performance.now() < deadline) {
|
|
483
|
+
const value = await evaluatePage(`(() => {
|
|
484
|
+
const state = Reflect.get(globalThis, '__sootsimCpuProfileBarrierState')
|
|
485
|
+
if (!state || typeof state !== 'object') return null
|
|
486
|
+
return {
|
|
487
|
+
marker: Reflect.get(state, 'marker'),
|
|
488
|
+
reachedAtEpochMs: Reflect.get(state, 'reachedAtEpochMs'),
|
|
489
|
+
}
|
|
490
|
+
})()`)
|
|
491
|
+
if (
|
|
492
|
+
value &&
|
|
493
|
+
typeof value === 'object' &&
|
|
494
|
+
typeof Reflect.get(value, 'marker') === 'string' &&
|
|
495
|
+
typeof Reflect.get(value, 'reachedAtEpochMs') === 'number'
|
|
496
|
+
) {
|
|
497
|
+
reached = true
|
|
498
|
+
break
|
|
499
|
+
}
|
|
500
|
+
await sleep(25)
|
|
501
|
+
}
|
|
502
|
+
if (!reached) {
|
|
503
|
+
console.error(
|
|
504
|
+
` workload interaction barrier was not reached within ${interactionBarrierTimeoutSeconds}s`,
|
|
505
|
+
)
|
|
506
|
+
return 1
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
captureStarted = true
|
|
510
|
+
startPromises.push(startTarget(pageTarget))
|
|
511
|
+
for (const target of workers.values()) startPromises.push(startTarget(target))
|
|
512
|
+
await waitForStableTargetSetup()
|
|
513
|
+
const startedCount = [pageTarget, ...workers.values()].filter(
|
|
514
|
+
(target) => target.started,
|
|
515
|
+
).length
|
|
516
|
+
if (startedCount === 0) {
|
|
517
|
+
console.error(' no CDP targets accepted Profiler.start')
|
|
162
518
|
return 1
|
|
163
519
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
520
|
+
const captureWindowStartedAtMs = performance.now()
|
|
521
|
+
const captureWindowStartedAtEpochMs = Date.now()
|
|
522
|
+
const captureWindowEndedAtMs = captureWindowStartedAtMs + duration * 1000
|
|
523
|
+
const captureWindowEndedAtEpochMs = captureWindowStartedAtEpochMs + duration * 1000
|
|
524
|
+
const interactionBarrier = interactionBarrierEnabled
|
|
525
|
+
? readInteractionBarrierState(await releaseInteractionBarrier('profiler'))
|
|
526
|
+
: null
|
|
527
|
+
if (opts.verbose) {
|
|
528
|
+
for (const target of [pageTarget, ...workers.values()]) {
|
|
529
|
+
console.log(
|
|
530
|
+
` target ${target.role} ${target.type} ${target.title || '(untitled)'} ${target.url}`,
|
|
531
|
+
)
|
|
532
|
+
}
|
|
173
533
|
}
|
|
174
534
|
console.log(
|
|
175
|
-
` recording ${duration}s across ${
|
|
535
|
+
` recording ${duration}s across ${startedCount} target(s) — interact now…`,
|
|
176
536
|
)
|
|
177
|
-
await sleep(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
537
|
+
await sleep(Math.max(0, captureWindowEndedAtMs - performance.now()))
|
|
538
|
+
captureStarted = false
|
|
539
|
+
captureWindowClosed = true
|
|
540
|
+
await Promise.allSettled(startPromises)
|
|
541
|
+
const results = [pageTarget, ...workers.values()]
|
|
542
|
+
await Promise.all(
|
|
543
|
+
results.map(async (target) => {
|
|
544
|
+
if (!target.started) return
|
|
545
|
+
try {
|
|
546
|
+
const { profile } = await cdp.send('Profiler.stop', {}, target.sessionId)
|
|
547
|
+
target.profile = profile
|
|
548
|
+
target.profileStoppedAtMs = performance.now()
|
|
549
|
+
} catch (error) {
|
|
550
|
+
target.error = error instanceof Error ? error.message : String(error)
|
|
551
|
+
target.profileStoppedAtMs = target.detachedAtMs
|
|
552
|
+
}
|
|
553
|
+
await cdp.send('Profiler.disable', {}, target.sessionId).catch(() => {})
|
|
554
|
+
}),
|
|
555
|
+
)
|
|
556
|
+
// querying the page while five high-frequency profilers are still active
|
|
557
|
+
// can itself time out and lengthen the requested sampling interval. stop
|
|
558
|
+
// first, then read the already-recorded action timestamps.
|
|
559
|
+
const workloadActionEpochMs = interactionBarrierEnabled
|
|
560
|
+
? readActionEpochs(
|
|
561
|
+
await evaluatePage(`(() => {
|
|
562
|
+
const partial = Reflect.get(globalThis, '__sootsimInteractionPartialResult')
|
|
563
|
+
const captures = partial && typeof partial === 'object'
|
|
564
|
+
? Reflect.get(partial, 'captures')
|
|
565
|
+
: null
|
|
566
|
+
if (!Array.isArray(captures)) return []
|
|
567
|
+
return captures.map((capture) => {
|
|
568
|
+
if (!capture || typeof capture !== 'object') return null
|
|
569
|
+
const note = Reflect.get(capture, 'note')
|
|
570
|
+
return note && typeof note === 'object'
|
|
571
|
+
? Reflect.get(note, 'actionEpochMs')
|
|
572
|
+
: null
|
|
573
|
+
})
|
|
574
|
+
})()`),
|
|
575
|
+
)
|
|
576
|
+
: []
|
|
185
577
|
|
|
186
578
|
mkdirSync(dirname(outputPath), { recursive: true })
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
579
|
+
const roleCounts = new Map<string, number>()
|
|
580
|
+
const manifest = results.map((target) => {
|
|
581
|
+
const coverage = cpuProfileCoverage(
|
|
582
|
+
target.profileStartedAtMs,
|
|
583
|
+
target.profileStoppedAtMs,
|
|
584
|
+
captureWindowStartedAtMs,
|
|
585
|
+
captureWindowEndedAtMs,
|
|
586
|
+
)
|
|
587
|
+
const priorCount = roleCounts.get(target.role) ?? 0
|
|
588
|
+
roleCounts.set(target.role, priorCount + 1)
|
|
589
|
+
const role = priorCount === 0 ? target.role : `${target.role}-${priorCount + 1}`
|
|
590
|
+
const profilePath = target.profile
|
|
591
|
+
? outputPath.replace(/(\.[^.]+)?$/, `.${role}$1`)
|
|
592
|
+
: null
|
|
593
|
+
if (profilePath) {
|
|
594
|
+
writeFileSync(profilePath, JSON.stringify(target.profile))
|
|
595
|
+
console.log(
|
|
596
|
+
` ${role} ${shortUrl(target.url)}: ${target.profile.samples.length} samples → ${profilePath}`,
|
|
597
|
+
)
|
|
598
|
+
} else {
|
|
599
|
+
console.error(
|
|
600
|
+
` ${role} ${shortUrl(target.url)}: no profile${target.error ? ` — ${target.error}` : ''}`,
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
if (opts.verbose && target.profile?.samples.length > 0) {
|
|
604
|
+
for (const fn of topSelfTime(target.profile, 12)) {
|
|
196
605
|
console.log(` ${fn.pct.toFixed(1).padStart(5)}% ${fn.name} ${fn.url}`)
|
|
197
606
|
}
|
|
198
607
|
}
|
|
608
|
+
return {
|
|
609
|
+
role,
|
|
610
|
+
type: target.type,
|
|
611
|
+
title: target.title,
|
|
612
|
+
url: target.url,
|
|
613
|
+
detached: target.detached,
|
|
614
|
+
attachedOffsetMs: target.attachedAtMs - captureWindowStartedAtMs,
|
|
615
|
+
...coverage,
|
|
616
|
+
samples: target.profile?.samples.length ?? 0,
|
|
617
|
+
profilePath,
|
|
618
|
+
error: target.error ?? null,
|
|
619
|
+
}
|
|
199
620
|
})
|
|
621
|
+
const requiredRoles = ['page', 'shell', 'compositor', 'tenant']
|
|
622
|
+
const capturedRoles = new Set(
|
|
623
|
+
manifest
|
|
624
|
+
.filter((target) => target.profilePath !== null)
|
|
625
|
+
.map((target) => target.role),
|
|
626
|
+
)
|
|
627
|
+
const missingRoles = requiredRoles.filter((role) => !capturedRoles.has(role))
|
|
628
|
+
const interactionBarrierComplete =
|
|
629
|
+
!interactionBarrierEnabled ||
|
|
630
|
+
(cpuProfileInteractionBarrierValid(
|
|
631
|
+
interactionBarrier,
|
|
632
|
+
captureWindowStartedAtEpochMs,
|
|
633
|
+
captureWindowEndedAtEpochMs,
|
|
634
|
+
) &&
|
|
635
|
+
cpuProfileActionsWithinWindow(
|
|
636
|
+
workloadActionEpochMs,
|
|
637
|
+
captureWindowStartedAtEpochMs,
|
|
638
|
+
captureWindowEndedAtEpochMs,
|
|
639
|
+
))
|
|
640
|
+
const complete =
|
|
641
|
+
missingRoles.length === 0 &&
|
|
642
|
+
interactionBarrierComplete &&
|
|
643
|
+
manifest.length > 0 &&
|
|
644
|
+
manifest.every(
|
|
645
|
+
(target) =>
|
|
646
|
+
target.profilePath !== null &&
|
|
647
|
+
target.error === null &&
|
|
648
|
+
target.coverageRatio >= 0.99,
|
|
649
|
+
)
|
|
650
|
+
const manifestPath = outputPath.replace(/(\.[^.]+)?$/, '.manifest.json')
|
|
651
|
+
const executableSha256 = IS_STANDALONE
|
|
652
|
+
? createHash('sha256').update(readFileSync(process.execPath)).digest('hex')
|
|
653
|
+
: null
|
|
654
|
+
writeFileSync(
|
|
655
|
+
manifestPath,
|
|
656
|
+
JSON.stringify(
|
|
657
|
+
{
|
|
658
|
+
cli: {
|
|
659
|
+
version: getCliVersion(),
|
|
660
|
+
standalone: IS_STANDALONE,
|
|
661
|
+
executablePath: process.execPath,
|
|
662
|
+
executableSha256,
|
|
663
|
+
argv: process.argv.slice(1),
|
|
664
|
+
},
|
|
665
|
+
page: page.url,
|
|
666
|
+
complete,
|
|
667
|
+
missingRoles,
|
|
668
|
+
requestedDurationMs: duration * 1000,
|
|
669
|
+
captureWindowStartedAt: new Date(captureWindowStartedAtEpochMs).toISOString(),
|
|
670
|
+
captureWindowEndedAt: new Date(captureWindowEndedAtEpochMs).toISOString(),
|
|
671
|
+
interactionBarrier,
|
|
672
|
+
workloadActionEpochMs,
|
|
673
|
+
manifest,
|
|
674
|
+
},
|
|
675
|
+
null,
|
|
676
|
+
2,
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
console.log(` manifest → ${manifestPath}`)
|
|
680
|
+
if (!complete) {
|
|
681
|
+
console.error(
|
|
682
|
+
` incomplete attribution capture${missingRoles.length ? ` — missing ${missingRoles.join(', ')}` : ''}`,
|
|
683
|
+
)
|
|
684
|
+
if (!interactionBarrierComplete) {
|
|
685
|
+
console.error(
|
|
686
|
+
' workload actions did not fall inside the interaction profile window',
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
200
690
|
console.log(' open in chrome devtools → Performance → Load profile to inspect.')
|
|
201
|
-
return 0
|
|
691
|
+
return complete ? 0 : 1
|
|
202
692
|
} finally {
|
|
693
|
+
if (interactionBarrierInstalled) {
|
|
694
|
+
await releaseInteractionBarrier('cleanup').catch(() => {})
|
|
695
|
+
}
|
|
203
696
|
cdp.close()
|
|
204
697
|
}
|
|
205
698
|
}
|
|
@@ -242,6 +735,19 @@ function shortUrl(url: string): string {
|
|
|
242
735
|
.slice(-50)
|
|
243
736
|
}
|
|
244
737
|
|
|
738
|
+
export function cpuProfileTargetRole(url: string, title = ''): string {
|
|
739
|
+
const identity = `${title} ${url}`.toLowerCase()
|
|
740
|
+
if (identity.includes('compositor-worker') || identity.includes('sootsim-compositor')) {
|
|
741
|
+
return 'compositor'
|
|
742
|
+
}
|
|
743
|
+
if (identity.includes('shell-worker')) return 'shell'
|
|
744
|
+
if (identity.includes('named-worklet')) return 'worklet'
|
|
745
|
+
if (identity.includes('render-worker') || identity.includes('tenant-worker')) {
|
|
746
|
+
return 'tenant'
|
|
747
|
+
}
|
|
748
|
+
return 'worker'
|
|
749
|
+
}
|
|
750
|
+
|
|
245
751
|
function topSelfTime(profile: any, topN: number) {
|
|
246
752
|
const total = profile.samples.length || 1
|
|
247
753
|
const byFn = new Map<string, { self: number; name: string; url: string }>()
|
package/cli/commands/perf.ts
CHANGED
package/cli/commands/upload.ts
CHANGED
|
@@ -528,7 +528,11 @@ export async function runUpload(
|
|
|
528
528
|
const [mainManifest, workerManifest] = await Promise.all([
|
|
529
529
|
evalInBridge<RecordedEntry[]>(
|
|
530
530
|
bridge,
|
|
531
|
-
`(
|
|
531
|
+
`(async () => {
|
|
532
|
+
const rec = window.__sootsimPreviewRecorder
|
|
533
|
+
await rec?.flush?.()
|
|
534
|
+
return rec?.list?.(${JSON.stringify(bundleOrigin)}) || []
|
|
535
|
+
})()`,
|
|
532
536
|
),
|
|
533
537
|
evalInBridge<RecordedEntry[]>(
|
|
534
538
|
bridge,
|
|
@@ -560,6 +564,7 @@ export async function runUpload(
|
|
|
560
564
|
`(async () => {
|
|
561
565
|
const rec = window.__sootsimPreviewRecorder
|
|
562
566
|
const workerList = window.__sootsimListWorkerFetchRecorder
|
|
567
|
+
await rec?.flush?.()
|
|
563
568
|
return {
|
|
564
569
|
main: rec?.list ? rec.list() : [],
|
|
565
570
|
worker: typeof workerList === 'function' ? await workerList() : [],
|
|
@@ -604,6 +609,7 @@ export async function runUpload(
|
|
|
604
609
|
const rec = window.__sootsimPreviewRecorder
|
|
605
610
|
const workerDump = window.__sootsimDumpWorkerFetchRecorder
|
|
606
611
|
const bundleUrl = ${JSON.stringify(snapshot.bundleUrl)}
|
|
612
|
+
await rec?.flush?.()
|
|
607
613
|
const keep = (r) => {
|
|
608
614
|
try {
|
|
609
615
|
const u = new URL(r.url)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/config.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/detox/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/home-paths.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
|
|
3
3
|
// src/host/fetch-proxy-overrides.ts
|
|
4
4
|
var FETCH_PROXY_BROWSER_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/menu.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/menu.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/metro.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/dist-lib/profiles.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/render-mode.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/dist-lib/sdk.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/sdk.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
4
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
package/dist-lib/skills.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1787,8 +1787,8 @@ var init_registry = __esm({
|
|
|
1787
1787
|
coverageSource: "estimated",
|
|
1788
1788
|
capabilityCoverage: CAPABILITY_INVENTORY_PACKAGE_COVERAGE["react-native"],
|
|
1789
1789
|
note: "React Native 0.86 root surface with useful core rendering, input, scrolling, animation, list, platform, and native-module behavior",
|
|
1790
|
-
working: "all 99 React Native 0.86.2 public runtime root export names; flex and absolute layout, text shaping and wrapping, image loading and resize modes, touch and responder routing, controlled text input and keyboard presentation, vertical and horizontal scrolling with paging/snapping/sticky and maintained content, windowed/inverted/horizontal lists, common Animated graphs and composition, alerts and action sheets, live platform/dimension/appearance state, and explicit native-module lookup",
|
|
1791
|
-
missing: "
|
|
1790
|
+
working: "all 99 React Native 0.86.2 public runtime root export names; flex and absolute layout, text shaping and wrapping, image loading and resize modes, touch and responder routing, controlled text input and keyboard presentation, vertical and horizontal scrolling with paging/snapping/sticky and maintained content, windowed/inverted/horizontal lists, common Animated graphs and composition, core LayoutAnimation create/update/delete geometry, alerts and action sheets, live platform/dimension/appearance state, and explicit native-module lookup",
|
|
1791
|
+
missing: "arbitrary app-specific NativeModules and complete core module methods, exact automatic scroll insets and nested-scroll edge behavior, advanced TextInput selection/autofill, uncommon platform lifecycle and accessibility events, complete UIManager/system/dev-tool semantics, LogBox and Systrace behavior, and native virtual-collection visibility modes"
|
|
1792
1792
|
}
|
|
1793
1793
|
]
|
|
1794
1794
|
},
|
|
@@ -1843,8 +1843,8 @@ var init_registry = __esm({
|
|
|
1843
1843
|
coverageSource: "estimated",
|
|
1844
1844
|
capabilityCoverage: CAPABILITY_INVENTORY_PACKAGE_COVERAGE["react-native-screens"],
|
|
1845
1845
|
note: "screen container, stack, header config, search bar, navigation props",
|
|
1846
|
-
working: "Screen, ScreenContainer, ScreenStack, enableFreeze/freezeEnabled with delayed React freeze, Android modal/pageSheet push fallback, Android statusBarHidden/statusBarStyle/navigationBarHidden traits, push/pop slide, zoom/fade/fade_from_bottom transitions, edge swipe-back, parallax behind-screen, large title collapse, header (back button, inline/large title, custom left/center/right/search bar subviews, native header bar buttons/menus/badges, crossfade), formSheet custom initial detents/corner radius/grabber/undimmed range and stacked sheets, useHeaderHeight, useTransitionProgress, FullWindowOverlay, SearchBar, Tabs.Host/Screen, lifecycle callbacks (onAppear/onDisappear/onWillAppear/onWillDisappear), onDismissed",
|
|
1847
|
-
missing: "
|
|
1846
|
+
working: "Screen, ScreenContainer, ScreenStack, enableFreeze/freezeEnabled with delayed React freeze, Android modal/pageSheet push fallback, Android statusBarHidden/statusBarStyle/navigationBarHidden traits, push/pop slide, replaceAnimation push/pop direction, zoom/fade/fade_from_bottom transitions, edge swipe-back, parallax behind-screen, large title collapse, header (back button, inline/large title, custom left/center/right/search bar subviews, native header bar buttons/menus/badges, crossfade), formSheet custom initial detents/corner radius/grabber/undimmed range and stacked sheets, useHeaderHeight, useTransitionProgress, FullWindowOverlay, SearchBar, Tabs.Host/Screen, lifecycle callbacks (onAppear/onDisappear/onWillAppear/onWillDisappear), onDismissed",
|
|
1847
|
+
missing: "interactive drag between formSheet detents, integrated search bar placements"
|
|
1848
1848
|
}
|
|
1849
1849
|
]
|
|
1850
1850
|
},
|
|
@@ -1990,8 +1990,8 @@ var init_registry = __esm({
|
|
|
1990
1990
|
coverageSource: "estimated",
|
|
1991
1991
|
capabilityCoverage: CAPABILITY_INVENTORY_PACKAGE_COVERAGE["react-native-keyboard-controller"],
|
|
1992
1992
|
note: "native bindings stubbed (KeyboardControllerNative, KeyboardEvents, FocusedInputEvents, WindowDimensionsEvents, KeyboardControllerView + sibling native views); upstream pure-JS components, hooks, animated module, and KeyboardAvoidingView resolve from node_modules unchanged",
|
|
1993
|
-
working: "every upstream JS export
|
|
1994
|
-
missing: "preload is a noop; KeyboardGestureArea
|
|
1993
|
+
working: "every upstream JS export runs through reanimated worklets; Android KeyboardGestureArea drives the existing keyboard owner through drag progress, velocity/distance settling, cancellation, focus retention, and cleanup; supported exports include KeyboardProvider, KeyboardAvoidingView, KeyboardStickyView, KeyboardAwareScrollView, KeyboardToolbar, KeyboardGestureArea, useReanimatedKeyboardAnimation, useKeyboardAnimation, useKeyboardHandler, useGenericKeyboardHandler, useKeyboardController, useKeyboardState, useKeyboardContext, useReanimatedFocusedInput, useFocusedInputHandler, useResizeMode, useWindowDimensions, KeyboardEvents, FocusedInputEvents, WindowDimensionsEvents resize events, KeyboardController (dismiss/setFocusTo/isVisible/state/preload/setInputMode/setDefaultMode), KeyboardControllerView, OverKeyboardView, KeyboardBackgroundView, KeyboardExtender, ClippingScrollView, KeyboardToolbarGroupView, and AndroidSoftInputModes",
|
|
1994
|
+
missing: "preload is a noop; KeyboardGestureArea lacks its iOS textInputNativeID/offset effect; OverKeyboardView, KeyboardBackgroundView, and KeyboardExtender are passthrough views without their native platform effects"
|
|
1995
1995
|
}
|
|
1996
1996
|
]
|
|
1997
1997
|
},
|
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.380 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
package/package.json
CHANGED
package/skills/rnx-perf/SKILL.md
CHANGED
|
@@ -170,6 +170,43 @@ the report and how to read it:
|
|
|
170
170
|
three runs minimum; single-run p95 is noise. a fresh `perf start` between
|
|
171
171
|
runs clears the buffer.
|
|
172
172
|
|
|
173
|
+
### browser-site interaction matrices
|
|
174
|
+
|
|
175
|
+
for a browser-level regression, use three fresh documents per browser engine.
|
|
176
|
+
within each document, capture cold then warm versions of the same interaction;
|
|
177
|
+
do not call two independent fresh documents "cold versus warm." wait for the
|
|
178
|
+
home surface's first compositor paint before starting—the compositor-ready flag
|
|
179
|
+
only proves worker bootstrap and can precede the first visible layout.
|
|
180
|
+
|
|
181
|
+
during a measured transition, use a fixed observation window. do not poll the
|
|
182
|
+
shell state, test tree, or perf bridge while the animation is running; verify
|
|
183
|
+
the positive control once after profiling stops. query a targeted id or text on
|
|
184
|
+
large virtualized apps, never the full tree, because enumeration can dominate
|
|
185
|
+
or terminate the browser process. for a remotely populated list, require the
|
|
186
|
+
same scroll-container identity and content extent for a quiet window before the
|
|
187
|
+
gesture; "a row exists" can still be true while the app replaces the tree.
|
|
188
|
+
keep pointer down, moves, release, and the fixed settle window inside one
|
|
189
|
+
uninterrupted profile. stopping or querying between drag and release changes
|
|
190
|
+
velocity, cache state, and worker scheduling.
|
|
191
|
+
|
|
192
|
+
the minimum browser matrix is home drag plus snapback, a cold/warm built-in app
|
|
193
|
+
open, and a cold/warm app-switcher open. add one quiet heavy-app screen, one
|
|
194
|
+
navigation transition, and one long-list scroll when those surfaces are in
|
|
195
|
+
scope. use GPU-backed Chrome for Chromium claims. use installed WebKit in a
|
|
196
|
+
visible nonpersistent WKWebView for Safari-engine claims; Playwright WebKit is
|
|
197
|
+
not the installed Safari engine.
|
|
198
|
+
|
|
199
|
+
every receipt must carry the exact page and frame URLs, browser user agent,
|
|
200
|
+
harness-source identity, capture time, engine build identity and generation,
|
|
201
|
+
runtime bundle receipt, system memory, runtime errors, and any partial failure.
|
|
202
|
+
partial artifacts remain inadmissible until every requested run number for that
|
|
203
|
+
scenario exists with one harness, engine, seed, and viewport identity. browser
|
|
204
|
+
comparisons use the same css viewport and backing scale; finite but different
|
|
205
|
+
device pixel ratios are not comparable.
|
|
206
|
+
the host-clock `displayFrames`/`timerFrames` and shell-link
|
|
207
|
+
`displayPumps`/`timerPumps` counters prove whether the visible-rAF fallback
|
|
208
|
+
engaged. preserve failed receipts instead of retrying them out of the data set.
|
|
209
|
+
|
|
173
210
|
### confirm which build you measured, every time
|
|
174
211
|
|
|
175
212
|
Against a dev-source sim, the FIRST reload after an engine edit serves the
|
|
@@ -196,11 +233,31 @@ rnx open 8089 --new --driver playwright --cdp-port 9222 # cpu-profile a driveabl
|
|
|
196
233
|
rnx debug enable layout,render && rnx debug recent layout 40
|
|
197
234
|
```
|
|
198
235
|
|
|
236
|
+
`rnx perf cpu` writes one profile for the page and one for every attached
|
|
237
|
+
worker, plus a manifest that labels shell, compositor, tenant, worklet, and
|
|
238
|
+
unknown workers. exit zero requires page, shell, compositor, and tenant plus
|
|
239
|
+
every other discovered target, with each covering at least 99% of the requested
|
|
240
|
+
window. inspect start/stop offsets as well as sample counts; a late-attached
|
|
241
|
+
near-empty profile is incomplete. a profile of only the tenant—or a helper
|
|
242
|
+
that relabels the busiest worker as tenant—cannot distinguish CPU saturation
|
|
243
|
+
from sparse browser scheduling.
|
|
244
|
+
|
|
245
|
+
for an automated interaction, pass `--interaction-barrier` and start the cpu
|
|
246
|
+
command during the runner's pre-workload attachment delay, and give the runner
|
|
247
|
+
a post-workload delay long enough to remain open through profiler stop. the
|
|
248
|
+
pre-workload delay alone is not synchronization: it can end the profile while
|
|
249
|
+
the workload is still preparing. barrier mode pauses the workload before its
|
|
250
|
+
first measured capture until every discovered target is profiling, then rejects
|
|
251
|
+
the manifest unless a recorded action timestamp falls inside the capture
|
|
252
|
+
interval. the barrier also has a bounded page-side lease so an interrupted
|
|
253
|
+
profiler cannot strand the workload; lease expiry releases the runner but
|
|
254
|
+
invalidates the profile.
|
|
255
|
+
|
|
199
256
|
### the cadence block: frames that never happened
|
|
200
257
|
|
|
201
258
|
work-per-painted-frame cannot see a frame that was never delivered — host
|
|
202
|
-
rAF starved, shell worker descheduled,
|
|
203
|
-
|
|
259
|
+
rAF starved, shell worker descheduled, or the compositor's shared-frame wait
|
|
260
|
+
resumed late. the report shows low avg and zero jank while the user sees stutter.
|
|
204
261
|
the `cadence` section of `perf shell stop` measures the delivery side and
|
|
205
262
|
attributes a missing frame to its hop:
|
|
206
263
|
|
|
@@ -214,6 +271,14 @@ attributes a missing frame to its hop:
|
|
|
214
271
|
- `idle breaks` — gaps >250ms, excluded from all three: demand-gated
|
|
215
272
|
quiesce is legitimate, not starvation.
|
|
216
273
|
|
|
274
|
+
the compositor `compositorHostFrameClock` separates display/timer ticks published into
|
|
275
|
+
its shared latest-frame cell from ticks consumed during the acknowledged worker
|
|
276
|
+
profile window. published timer ticks prove the renderer fallback won; sparse consumed ticks
|
|
277
|
+
with low production time identifies worker scheduling starvation. many consumed
|
|
278
|
+
ticks with sparse paints can be legitimate dirty-gating; correlate the frame
|
|
279
|
+
series before treating it as a regression. shell-owned captures expose the
|
|
280
|
+
corresponding `note.frameLink.sharedFrameClock` counters.
|
|
281
|
+
|
|
217
282
|
compare baseline vs contended captures of the same scripted window; also
|
|
218
283
|
compare **painted-frame counts**, the crudest and most robust signal.
|
|
219
284
|
|
|
@@ -276,6 +341,7 @@ which tier *should* absorb your cost and prove whether it does.
|
|
|
276
341
|
| damage rects (`damage-rect.ts`) | repaint clips to changed region (opaque-backdrop gated) | worst-frame render bounded during small updates |
|
|
277
342
|
| raster tier | stable rows become GPU textures; scroll skips recording | compositor block: promotions > 0 once warm, blits ≈ one per visible row (uniswap token list: ~13.7/frame) |
|
|
278
343
|
| flood guard, pointer coalescing, vsync hysteresis | message-storm and pump-thrash protection | msgs/s sane during gestures and at rest |
|
|
344
|
+
| resilient renderer-main frame clock | visible rAF holes do not stall pointer coalescing or worker vsync | compositor `published` / `consumed` display and timer frames; shell `displayPumps` / `timerPumps` |
|
|
279
345
|
|
|
280
346
|
there is no whole-screen scroll blit. one existed for a few hours in June
|
|
281
347
|
2026 and was reverted (`6706f3a014`): it shifted a surface RECTANGLE, so a
|