rnxsim 0.1.313 → 0.1.314
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/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
- 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.cjs +20 -0
- 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 +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- 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/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- 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.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +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 +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// `sootsim get memory` / `sootsim debug memory` — per-worker live object
|
|
2
|
+
// counts (nodes registered, pictures, raster images, paragraphs, yoga wasm
|
|
3
|
+
// nodes), image-loader cache, and JS heaps. one-shot by default; --watch
|
|
4
|
+
// samples repeatedly and reports per-counter slope, which is the leak-RCA
|
|
5
|
+
// workflow: run the repro, watch which counter's slope tracks RSS.
|
|
6
|
+
|
|
7
|
+
import { inspectMemory, type MemoryReport, type WorkerMemorySample } from './core'
|
|
8
|
+
import { printJson, wantsJson } from './shared'
|
|
9
|
+
import type { WsBridge } from '../../ws-bridge'
|
|
10
|
+
|
|
11
|
+
function formatBytes(n: number): string {
|
|
12
|
+
if (n < 1024) return `${n}B`
|
|
13
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`
|
|
14
|
+
return `${(n / 1024 / 1024).toFixed(1)}MB`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pct(num: number, denom: number): string {
|
|
18
|
+
if (denom <= 0) return '?'
|
|
19
|
+
return `${((num / denom) * 100).toFixed(0)}%`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// flatten one report into named numeric counters so watch mode can diff and
|
|
23
|
+
// slope every field uniformly.
|
|
24
|
+
function flattenCounters(report: MemoryReport): Record<string, number> {
|
|
25
|
+
const out: Record<string, number> = {}
|
|
26
|
+
const worker = (prefix: string, w: WorkerMemorySample | null) => {
|
|
27
|
+
if (!w) return
|
|
28
|
+
if (w.nodesRegistered != null) out[`${prefix}.nodes`] = w.nodesRegistered
|
|
29
|
+
if (w.nodesDetached != null) out[`${prefix}.detached`] = w.nodesDetached
|
|
30
|
+
if (w.objects) {
|
|
31
|
+
out[`${prefix}.paragraphs`] = w.objects.paragraphs
|
|
32
|
+
out[`${prefix}.yogaNodes`] = w.objects.yogaNodes
|
|
33
|
+
out[`${prefix}.pictures`] = w.objects.pictures
|
|
34
|
+
out[`${prefix}.rasterImages`] = w.objects.rasterImages
|
|
35
|
+
}
|
|
36
|
+
if (w.imageLoader) {
|
|
37
|
+
out[`${prefix}.imageCacheEntries`] = w.imageLoader.cacheEntries
|
|
38
|
+
out[`${prefix}.imagePixelBytes`] = w.imageLoader.cachePixelBytes
|
|
39
|
+
}
|
|
40
|
+
if (w.workerHeap) out[`${prefix}.heapBytes`] = w.workerHeap.usedJSHeapSize
|
|
41
|
+
if (w.wasmHeapBytes != null) out[`${prefix}.wasmHeapBytes`] = w.wasmHeapBytes
|
|
42
|
+
}
|
|
43
|
+
worker('tenant', report.tenant)
|
|
44
|
+
worker('shell', report.shell)
|
|
45
|
+
worker('compositor', report.compositor)
|
|
46
|
+
if (report.hostHeap) out['host.heapBytes'] = report.hostHeap.usedJSHeapSize
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatCounter(key: string, value: number): string {
|
|
51
|
+
return key.endsWith('Bytes') ? formatBytes(value) : String(value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printWorkerBlock(name: string, w: WorkerMemorySample | null): void {
|
|
55
|
+
if (!w) {
|
|
56
|
+
console.log(` ${name}: not available`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const objects = w.objects
|
|
60
|
+
console.log(` ${name}`)
|
|
61
|
+
if (w.nodesRegistered != null) {
|
|
62
|
+
console.log(` nodes registered: ${w.nodesRegistered}`)
|
|
63
|
+
}
|
|
64
|
+
if (w.nodesDetached != null) {
|
|
65
|
+
const types = Object.entries(w.detachedTypes ?? {})
|
|
66
|
+
.map(([type, count]) => `${type}:${count}`)
|
|
67
|
+
.join(' ')
|
|
68
|
+
console.log(
|
|
69
|
+
` nodes detached: ${w.nodesDetached}${types ? ` (${types})` : ''}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (objects) {
|
|
73
|
+
console.log(` paragraphs: ${objects.paragraphs}`)
|
|
74
|
+
console.log(` yoga nodes: ${objects.yogaNodes}`)
|
|
75
|
+
console.log(` pictures: ${objects.pictures}`)
|
|
76
|
+
console.log(` raster images: ${objects.rasterImages}`)
|
|
77
|
+
}
|
|
78
|
+
if (w.activeNativeAnimations != null) {
|
|
79
|
+
console.log(` native anims: ${w.activeNativeAnimations}`)
|
|
80
|
+
}
|
|
81
|
+
if (w.imageLoader) {
|
|
82
|
+
const im = w.imageLoader
|
|
83
|
+
console.log(
|
|
84
|
+
` image cache: ${im.cacheEntries}/${im.cacheMaxEntries} entries, ${formatBytes(im.cachePixelBytes)}/${formatBytes(im.cachePixelBudget)} (${pct(im.cachePixelBytes, im.cachePixelBudget)})`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
if (w.workerHeap) {
|
|
88
|
+
console.log(
|
|
89
|
+
` js heap: ${formatBytes(w.workerHeap.usedJSHeapSize)} used / ${formatBytes(w.workerHeap.totalJSHeapSize)} total`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
if (w.wasmHeapBytes != null) {
|
|
93
|
+
console.log(` canvaskit wasm: ${formatBytes(w.wasmHeapBytes)}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function printReport(report: MemoryReport): void {
|
|
98
|
+
console.log(' memory:')
|
|
99
|
+
printWorkerBlock('tenant worker', report.tenant)
|
|
100
|
+
printWorkerBlock('shell worker', report.shell)
|
|
101
|
+
printWorkerBlock('compositor worker', report.compositor)
|
|
102
|
+
if (report.hostHeap) {
|
|
103
|
+
console.log(' host')
|
|
104
|
+
console.log(
|
|
105
|
+
` js heap: ${formatBytes(report.hostHeap.usedJSHeapSize)} used / ${formatBytes(report.hostHeap.totalJSHeapSize)} total`,
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
console.log(' host js heap: not available (chrome only)')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readNumberFlag(args: string[], flag: string, fallback: number): number {
|
|
113
|
+
const idx = args.indexOf(flag)
|
|
114
|
+
if (idx === -1) return fallback
|
|
115
|
+
const value = Number(args[idx + 1])
|
|
116
|
+
return Number.isFinite(value) && value > 0 ? value : fallback
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runMemorySubcommand(
|
|
120
|
+
bridge: WsBridge,
|
|
121
|
+
opts: { args: string[] } = { args: [] },
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const json = wantsJson(opts.args)
|
|
124
|
+
const watchIdx = opts.args.indexOf('--watch')
|
|
125
|
+
|
|
126
|
+
if (watchIdx === -1) {
|
|
127
|
+
const report = await inspectMemory(bridge)
|
|
128
|
+
if (json) {
|
|
129
|
+
printJson(report)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
printReport(report)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --watch [seconds] — sample every --interval seconds (default 5) for the
|
|
137
|
+
// given duration (default 60s), then report per-counter slope.
|
|
138
|
+
const durationValue = Number(opts.args[watchIdx + 1])
|
|
139
|
+
const durationSec =
|
|
140
|
+
Number.isFinite(durationValue) && durationValue > 0 ? durationValue : 60
|
|
141
|
+
const intervalSec = readNumberFlag(opts.args, '--interval', 5)
|
|
142
|
+
|
|
143
|
+
const samples: Array<{ tSec: number; counters: Record<string, number> }> = []
|
|
144
|
+
const start = Date.now()
|
|
145
|
+
let prev: Record<string, number> | null = null
|
|
146
|
+
|
|
147
|
+
if (!json) {
|
|
148
|
+
console.log(` sampling every ${intervalSec}s for ${durationSec}s — deltas per tick:`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
while (true) {
|
|
152
|
+
const report = await inspectMemory(bridge)
|
|
153
|
+
const tSec = (Date.now() - start) / 1000
|
|
154
|
+
const counters = flattenCounters(report)
|
|
155
|
+
samples.push({ tSec, counters })
|
|
156
|
+
|
|
157
|
+
if (!json) {
|
|
158
|
+
const parts: string[] = []
|
|
159
|
+
for (const [key, value] of Object.entries(counters)) {
|
|
160
|
+
const delta = prev && key in prev ? value - prev[key] : null
|
|
161
|
+
if (delta === null || delta === 0) continue
|
|
162
|
+
const sign = delta > 0 ? '+' : '-'
|
|
163
|
+
const deltaStr = key.endsWith('Bytes')
|
|
164
|
+
? `${sign}${formatBytes(Math.abs(delta))}`
|
|
165
|
+
: `${sign}${Math.abs(delta)}`
|
|
166
|
+
parts.push(`${key} ${formatCounter(key, value)} (${deltaStr})`)
|
|
167
|
+
}
|
|
168
|
+
console.log(
|
|
169
|
+
` t+${tSec.toFixed(0)}s ${
|
|
170
|
+
parts.length
|
|
171
|
+
? parts.join(' ')
|
|
172
|
+
: prev
|
|
173
|
+
? 'no change'
|
|
174
|
+
: Object.entries(counters)
|
|
175
|
+
.map(([k, v]) => `${k} ${formatCounter(k, v)}`)
|
|
176
|
+
.join(' ')
|
|
177
|
+
}`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
prev = counters
|
|
181
|
+
|
|
182
|
+
if (tSec >= durationSec) break
|
|
183
|
+
await new Promise((resolve) => setTimeout(resolve, intervalSec * 1000))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// per-counter slope over the whole window
|
|
187
|
+
const first = samples[0]
|
|
188
|
+
const last = samples[samples.length - 1]
|
|
189
|
+
const elapsed = Math.max(last.tSec - first.tSec, 0.001)
|
|
190
|
+
const perSecond: Record<string, number> = {}
|
|
191
|
+
for (const key of Object.keys(last.counters)) {
|
|
192
|
+
if (!(key in first.counters)) continue
|
|
193
|
+
perSecond[key] = (last.counters[key] - first.counters[key]) / elapsed
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (json) {
|
|
197
|
+
printJson({ samples, perSecond, elapsedSec: elapsed })
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`\n slope over ${elapsed.toFixed(0)}s:`)
|
|
202
|
+
const moving = Object.entries(perSecond)
|
|
203
|
+
.filter(([, v]) => v !== 0)
|
|
204
|
+
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
|
205
|
+
if (moving.length === 0) {
|
|
206
|
+
console.log(' all counters flat')
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
for (const [key, rate] of moving) {
|
|
210
|
+
const label = key.endsWith('Bytes')
|
|
211
|
+
? `${formatBytes(Math.abs(rate))}/s${rate < 0 ? ' (shrinking)' : ''}`
|
|
212
|
+
: `${rate > 0 ? '+' : ''}${rate.toFixed(2)}/s`
|
|
213
|
+
console.log(` ${key.padEnd(28)}${label}`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const SECURE_TEXT_REDACTION = '<redacted secure text>'
|
|
2
|
+
|
|
3
|
+
type KeyboardSpecLike = {
|
|
4
|
+
secureTextEntry?: boolean | null
|
|
5
|
+
keyboardType?: string | null
|
|
6
|
+
} | null
|
|
7
|
+
|
|
8
|
+
type KeyboardStateLike = {
|
|
9
|
+
layout?: { spec?: KeyboardSpecLike } | null
|
|
10
|
+
spec?: KeyboardSpecLike
|
|
11
|
+
focusedInput?: Record<string, unknown> | null
|
|
12
|
+
} | null
|
|
13
|
+
|
|
14
|
+
export function isSecureKeyboardState(state: KeyboardStateLike): boolean {
|
|
15
|
+
const spec = state?.layout?.spec ?? state?.spec ?? null
|
|
16
|
+
return !!spec?.secureTextEntry && spec.keyboardType !== 'visible-password'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function redactTextForKeyboardState(
|
|
20
|
+
text: string,
|
|
21
|
+
state: KeyboardStateLike,
|
|
22
|
+
): string {
|
|
23
|
+
return isSecureKeyboardState(state) ? SECURE_TEXT_REDACTION : text
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redactKeyboardStateForOutput<T extends KeyboardStateLike>(state: T): T {
|
|
27
|
+
if (!state || !isSecureKeyboardState(state)) return state
|
|
28
|
+
const focusedInput = state.focusedInput
|
|
29
|
+
if (!focusedInput || typeof focusedInput !== 'object') return state
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
focusedInput: {
|
|
33
|
+
...focusedInput,
|
|
34
|
+
secureTextEntry: true,
|
|
35
|
+
text:
|
|
36
|
+
typeof focusedInput.text === 'string' ? SECURE_TEXT_REDACTION : focusedInput.text,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { WsBridge } from '../../ws-bridge'
|
|
2
|
+
|
|
3
|
+
// read a --testid / --text flag from args. returns null when neither is
|
|
4
|
+
// present so coord-based do verbs can fall through to their positional path.
|
|
5
|
+
//
|
|
6
|
+
// pass `positional` from a verb whose positionals are x/y coordinates (tap,
|
|
7
|
+
// double-tap, long-press) and a bare non-numeric first positional reads as a
|
|
8
|
+
// testID: `sootsim do tap my-button` works like `do tap-id my-button` and like
|
|
9
|
+
// `do scroll <id>`, instead of failing with a usage line for the one verb an
|
|
10
|
+
// agent is most likely to reach for. a coordinate is always numeric, so there
|
|
11
|
+
// is nothing to disambiguate.
|
|
12
|
+
export function readTargetFlag(
|
|
13
|
+
args: string[],
|
|
14
|
+
positional?: string[],
|
|
15
|
+
): { mode: 'testid' | 'text'; value: string } | null {
|
|
16
|
+
const tid = args.indexOf('--testid')
|
|
17
|
+
if (tid >= 0 && args[tid + 1]) return { mode: 'testid', value: args[tid + 1] }
|
|
18
|
+
const alt = args.indexOf('--test-id')
|
|
19
|
+
if (alt >= 0 && args[alt + 1]) return { mode: 'testid', value: args[alt + 1] }
|
|
20
|
+
const text = args.indexOf('--text')
|
|
21
|
+
if (text >= 0 && args[text + 1]) return { mode: 'text', value: args[text + 1] }
|
|
22
|
+
const bare = positional?.[1]
|
|
23
|
+
if (bare && !bare.startsWith('-') && !Number.isFinite(Number(bare))) {
|
|
24
|
+
return { mode: 'testid', value: bare }
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function resolveTargetCoords(
|
|
30
|
+
bridge: WsBridge,
|
|
31
|
+
target: { mode: 'testid' | 'text'; value: string },
|
|
32
|
+
): Promise<{
|
|
33
|
+
x: number
|
|
34
|
+
y: number
|
|
35
|
+
id?: string | null
|
|
36
|
+
testID?: string | null
|
|
37
|
+
text?: string | null
|
|
38
|
+
type?: string | null
|
|
39
|
+
} | null> {
|
|
40
|
+
const arg = JSON.stringify(target.value)
|
|
41
|
+
const findExpr =
|
|
42
|
+
target.mode === 'testid'
|
|
43
|
+
? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))`
|
|
44
|
+
: `await t.findByText(${arg})`
|
|
45
|
+
const result = await bridge.send({
|
|
46
|
+
type: 'evaluate',
|
|
47
|
+
code: `(async () => {
|
|
48
|
+
const t = window.__sootsimTest
|
|
49
|
+
if (!t) return null
|
|
50
|
+
const n = ${findExpr}
|
|
51
|
+
if (!n || !n.absolutePosition || !n.layout) return null
|
|
52
|
+
const resolved =
|
|
53
|
+
typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
|
|
54
|
+
? await t.resolveTapTarget(n.nodeId)
|
|
55
|
+
: null
|
|
56
|
+
const cx =
|
|
57
|
+
typeof resolved?.cx === 'number'
|
|
58
|
+
? resolved.cx
|
|
59
|
+
: n.absolutePosition.x + (n.layout.width || 0) / 2
|
|
60
|
+
const cy =
|
|
61
|
+
typeof resolved?.cy === 'number'
|
|
62
|
+
? resolved.cy
|
|
63
|
+
: n.absolutePosition.y + (n.layout.height || 0) / 2
|
|
64
|
+
return {
|
|
65
|
+
x: cx,
|
|
66
|
+
y: cy,
|
|
67
|
+
id: n.id ?? null,
|
|
68
|
+
testID: n.testID ?? null,
|
|
69
|
+
text: ${JSON.stringify(target.mode === 'text')} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
|
|
70
|
+
type: n.type ?? null,
|
|
71
|
+
}
|
|
72
|
+
})()`,
|
|
73
|
+
})
|
|
74
|
+
return (result ?? null) as {
|
|
75
|
+
x: number
|
|
76
|
+
y: number
|
|
77
|
+
id?: string | null
|
|
78
|
+
testID?: string | null
|
|
79
|
+
text?: string | null
|
|
80
|
+
type?: string | null
|
|
81
|
+
} | null
|
|
82
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `sootsim get screens` — print the current navigation stack + shell state +
|
|
2
|
+
// any presented modal + keyboard. answers "what screen am I on?" without
|
|
3
|
+
// having to dump the whole tree or scrape screen-specific testIDs.
|
|
4
|
+
//
|
|
5
|
+
// sources:
|
|
6
|
+
// - navigation-runtime snapshot (test bridge) for screen stack + phase
|
|
7
|
+
// - mainShell.getState() for active app + switcher / launch progress
|
|
8
|
+
// - __sootsimKeyboard.getLayout() for keyboard visibility + spec
|
|
9
|
+
|
|
10
|
+
import { inspectScreens } from './core'
|
|
11
|
+
import type { WsBridge } from '../../ws-bridge'
|
|
12
|
+
|
|
13
|
+
export async function runScreensSubcommand(
|
|
14
|
+
bridge: WsBridge,
|
|
15
|
+
opts: { json?: boolean } = {},
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const { nav, route, keyboard, shell } = await inspectScreens(bridge)
|
|
18
|
+
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(JSON.stringify({ shell: shell ?? null, nav, route, keyboard }, null, 2))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines: string[] = []
|
|
25
|
+
|
|
26
|
+
// shell — what app, what switcher state
|
|
27
|
+
if (shell) {
|
|
28
|
+
const activeApp = shell.activeApp ?? shell.state ?? '<none>'
|
|
29
|
+
const switcher = shell.showSwitcher ? ' (app switcher open)' : ''
|
|
30
|
+
const launchProgress =
|
|
31
|
+
typeof shell.launchProgress === 'number' && shell.launchProgress < 0.98
|
|
32
|
+
? ` launching (${Math.round(shell.launchProgress * 100)}%)`
|
|
33
|
+
: ''
|
|
34
|
+
lines.push(`shell: ${activeApp}${switcher}${launchProgress}`)
|
|
35
|
+
} else {
|
|
36
|
+
lines.push(`shell: <unavailable>`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// navigation stack
|
|
40
|
+
if (nav) {
|
|
41
|
+
const phase =
|
|
42
|
+
nav.transitionPhase !== 'idle'
|
|
43
|
+
? ` (${nav.transitionPhase}, ${nav.activeTransitionCount} active)`
|
|
44
|
+
: ''
|
|
45
|
+
lines.push(`nav: phase=${nav.transitionPhase}${phase}`)
|
|
46
|
+
if (nav.screens.length === 0) {
|
|
47
|
+
lines.push(` <no registered screens — app may not use react-native-screens>`)
|
|
48
|
+
} else {
|
|
49
|
+
for (const s of nav.screens) {
|
|
50
|
+
const marker = s.isActive ? '▶' : ' '
|
|
51
|
+
const route = s.routeName ? ` ${s.routeName}` : ''
|
|
52
|
+
const header = s.headerHeight > 0 ? ` header=${s.headerHeight}` : ''
|
|
53
|
+
const lt =
|
|
54
|
+
s.largeTitleState && s.largeTitleState !== 'expanded'
|
|
55
|
+
? ` large-title=${s.largeTitleState}`
|
|
56
|
+
: ''
|
|
57
|
+
lines.push(` ${marker} #${s.id}${route}${header}${lt}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(`nav: <runtime not available>`)
|
|
62
|
+
}
|
|
63
|
+
lines.push(`route: ${route?.currentPath ?? '<unavailable>'}`)
|
|
64
|
+
if (route?.coveringPath) {
|
|
65
|
+
lines.push(`covered: a presented screen (${route.coveringPath}) is above this route`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// keyboard
|
|
69
|
+
if (keyboard && keyboard.visible) {
|
|
70
|
+
const type = keyboard.spec?.keyboardType ?? 'default'
|
|
71
|
+
const ret = keyboard.spec?.returnKeyType ?? 'default'
|
|
72
|
+
lines.push(`keyboard: visible (${type}, return=${ret}, mode=${keyboard.mode ?? '?'})`)
|
|
73
|
+
} else {
|
|
74
|
+
lines.push(`keyboard: hidden`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(lines.join('\n'))
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { waitForSootsimIdle } from './settling'
|
|
2
|
+
import type { WsBridge } from '../../ws-bridge'
|
|
3
|
+
|
|
4
|
+
// default: layout hash stable for 3 consecutive polls. --strict additionally
|
|
5
|
+
// requires worker queryStats animation flags to be false — use when the
|
|
6
|
+
// target app has no perpetual background animation.
|
|
7
|
+
export async function runSettleSubcommand(opts: {
|
|
8
|
+
bridge: WsBridge
|
|
9
|
+
args: string[]
|
|
10
|
+
positional: string[]
|
|
11
|
+
}): Promise<void> {
|
|
12
|
+
const { bridge, args, positional } = opts
|
|
13
|
+
const maxMs = positional[1] ? Number(positional[1]) * 1000 : 3000
|
|
14
|
+
const strict = args.includes('--strict')
|
|
15
|
+
const { elapsed, settled } = await waitForSootsimIdle({ bridge, maxMs, strict })
|
|
16
|
+
|
|
17
|
+
if (settled) {
|
|
18
|
+
console.log(` settled in ${elapsed}ms`)
|
|
19
|
+
} else {
|
|
20
|
+
console.log(` timed out after ${elapsed}ms (may still be animating)`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { InspectBridge } from './core'
|
|
2
|
+
|
|
3
|
+
export type WaitForSootsimIdleOptions = {
|
|
4
|
+
bridge: InspectBridge
|
|
5
|
+
simId?: string
|
|
6
|
+
maxMs: number
|
|
7
|
+
pollMs?: number
|
|
8
|
+
stablePolls?: number
|
|
9
|
+
strict?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WaitForSootsimIdleResult = {
|
|
13
|
+
elapsed: number
|
|
14
|
+
settled: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function waitForSootsimIdle({
|
|
18
|
+
bridge,
|
|
19
|
+
simId,
|
|
20
|
+
maxMs,
|
|
21
|
+
pollMs = 50,
|
|
22
|
+
stablePolls = 3,
|
|
23
|
+
strict = false,
|
|
24
|
+
}: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
|
|
25
|
+
const result = await bridge.send(
|
|
26
|
+
{
|
|
27
|
+
type: 'evaluate',
|
|
28
|
+
simId,
|
|
29
|
+
code: `(async () => {
|
|
30
|
+
const start = Date.now()
|
|
31
|
+
const deadline = start + ${Math.max(0, Math.round(maxMs))}
|
|
32
|
+
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
33
|
+
const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
|
|
34
|
+
const strict = ${strict ? 'true' : 'false'}
|
|
35
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
36
|
+
|
|
37
|
+
// route-aware settle: a tap that pushes/pops a screen is async — the
|
|
38
|
+
// old screen still renders for a moment, then the new one mounts and
|
|
39
|
+
// its content loads. a pure layout-hash check reports "idle" on the
|
|
40
|
+
// outgoing screen or on the incoming skeleton, so a driver re-taps
|
|
41
|
+
// and stacks duplicate navigations. drain any in-flight screen
|
|
42
|
+
// transition FIRST, bounded by the overall budget. when no
|
|
43
|
+
// transition is happening this returns in ~80ms (startWindowMs), so
|
|
44
|
+
// a plain button tap barely pays for it.
|
|
45
|
+
try {
|
|
46
|
+
const wfst = window.__sootsimTest?.waitForScreenTransitions
|
|
47
|
+
if (typeof wfst === 'function') {
|
|
48
|
+
const remaining = deadline - Date.now()
|
|
49
|
+
if (remaining > 120) {
|
|
50
|
+
await wfst({
|
|
51
|
+
timeoutMs: Math.min(remaining - 80, 4000),
|
|
52
|
+
settleMs: 64,
|
|
53
|
+
startWindowMs: 80,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
|
|
59
|
+
const readSnapshot = async () => {
|
|
60
|
+
let animating = false
|
|
61
|
+
let pendingFetches = 0
|
|
62
|
+
let requestTotal = 0
|
|
63
|
+
let requestInFlight = 0
|
|
64
|
+
try {
|
|
65
|
+
const stats = await window.__sootsimRenderHost?.queryStats?.()
|
|
66
|
+
if (stats) {
|
|
67
|
+
animating =
|
|
68
|
+
stats.hasActiveAnims === true ||
|
|
69
|
+
stats.hasActiveNativeAnimations === true ||
|
|
70
|
+
stats.hasPendingAnimationFrames === true
|
|
71
|
+
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
72
|
+
// but not actually settled — its data/images are still in
|
|
73
|
+
// flight. treat bounded image-loader fetches as not-idle so
|
|
74
|
+
// settle waits for real content, capped by the budget.
|
|
75
|
+
const pf = stats.memory && stats.memory.imageLoader
|
|
76
|
+
? stats.memory.imageLoader.pendingFetches
|
|
77
|
+
: 0
|
|
78
|
+
pendingFetches = typeof pf === 'number' ? pf : 0
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
try {
|
|
82
|
+
const counts = await window.__sootsimTest?.getRequestCounts?.()
|
|
83
|
+
requestTotal = typeof counts?.total === 'number' ? counts.total : 0
|
|
84
|
+
requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
|
|
85
|
+
} catch {}
|
|
86
|
+
const root = window.__sootsimRoot
|
|
87
|
+
const nodes = []
|
|
88
|
+
if (root) {
|
|
89
|
+
const walk = (n) => {
|
|
90
|
+
if (n.layout && n.layout.width > 0) {
|
|
91
|
+
nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
|
|
92
|
+
}
|
|
93
|
+
for (const c of n.children || []) walk(c)
|
|
94
|
+
}
|
|
95
|
+
walk(root)
|
|
96
|
+
}
|
|
97
|
+
return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// how long generic background network is allowed to keep us waiting AFTER
|
|
101
|
+
// the screen is otherwise visually settled. apps with continuous traffic
|
|
102
|
+
// (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
|
|
103
|
+
// requestInFlight===0 / a stable requestTotal, so requiring full network
|
|
104
|
+
// quiet burns the ENTIRE budget on every command — the leading idle that
|
|
105
|
+
// failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
|
|
106
|
+
// layout-stability + the image-loader already prove visible content
|
|
107
|
+
// loaded (a data load changes layout; an image load shows in the loader),
|
|
108
|
+
// so the generic request counters are a courtesy, not a gate: honor them
|
|
109
|
+
// only within this grace once the screen is visually still.
|
|
110
|
+
const networkQuietGraceMs = 1200
|
|
111
|
+
let lastLayout = ''
|
|
112
|
+
let lastRequestTotal = -1
|
|
113
|
+
let stable = 0
|
|
114
|
+
let visuallyStillSince = 0
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
const snapshot = await readSnapshot()
|
|
117
|
+
const strictOk = !strict || !snapshot.animating
|
|
118
|
+
// visually settled: no animations, no image content loading, layout
|
|
119
|
+
// unchanged since the last poll.
|
|
120
|
+
const visuallyStill =
|
|
121
|
+
strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
|
|
122
|
+
// generic network quiet: nothing in flight and no new requests issued.
|
|
123
|
+
const networkQuiet =
|
|
124
|
+
snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
|
|
125
|
+
if (visuallyStill) {
|
|
126
|
+
stable++
|
|
127
|
+
if (visuallyStillSince === 0) visuallyStillSince = Date.now()
|
|
128
|
+
// settle once the screen has held still long enough AND either the
|
|
129
|
+
// network is genuinely quiet OR it has refused to quiet within the
|
|
130
|
+
// grace (continuous background traffic must not block forever).
|
|
131
|
+
if (
|
|
132
|
+
stable >= requiredStablePolls &&
|
|
133
|
+
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
134
|
+
) {
|
|
135
|
+
return { settled: true, elapsed: Date.now() - start }
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
stable = 0
|
|
139
|
+
visuallyStillSince = 0
|
|
140
|
+
}
|
|
141
|
+
lastLayout = snapshot.layout
|
|
142
|
+
lastRequestTotal = snapshot.requestTotal
|
|
143
|
+
await sleep(pollMs)
|
|
144
|
+
}
|
|
145
|
+
return { settled: false, elapsed: Date.now() - start }
|
|
146
|
+
})()`,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
timeoutMs: maxMs + 1_000,
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const { elapsed, settled } = (result ?? {}) as Partial<WaitForSootsimIdleResult>
|
|
154
|
+
return {
|
|
155
|
+
elapsed: typeof elapsed === 'number' ? elapsed : maxMs,
|
|
156
|
+
settled: settled === true,
|
|
157
|
+
}
|
|
158
|
+
}
|