Skip to content

feat: add run telemetry probes to diagnostics - #205

Merged
V3RON merged 11 commits into
mainfrom
feat/run-telemetry-probes
Oct 16, 2026
Merged

V3RON merged 11 commits into
mainfrom
feat/run-telemetry-probes

Conversation

@V3RON

@V3RON V3RON commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

What is this?

Harness diagnostics now record resource telemetry alongside the existing timing spans. When diagnostics are enabled, a run samples host load, CPU and memory pressure, the Harness process's memory and event-loop lag, takes process listings at a few run milestones on macOS, and reports the iOS simulator's boot stage and guest process count. Entries are written incrementally, so a cancelled or timed-out CI job keeps its data, and each run ends with a short key-number summary.

This closes the gap that made iOS E2E failures on small CI runners impossible to diagnose: the simulator boot wait there takes minutes instead of seconds, the box runs out of memory and into the compressor, and the only trace was written at the end of the run, which never came when the job hit its timeout.

How does it work?

The existing Diagnostics object stays the single sink and gains a gauge entry kind. Probes live in a separate telemetry module in @react-native-harness/tools, one per file: run context, host resources (vm_stat on macOS, /proc/meminfo on Linux, load and CPU deltas everywhere), Node runtime, process snapshot, and a platform collector. They are started from one composition point in the Jest package and react to lifecycle phases the session emits (session.created, metro.ready, platform.ready, app.ready, run.start, test-file.start, test-file.end, run.end, dispose). Business code only emits those phases; it makes no diagnostics calls. Platforms can expose an optional createTelemetryCollector(); the iOS simulator runner uses it for the guest process count, and the boot stage reported by simctl bootstatus lands as attributes on the existing boot span through the instrumented proxy.

Timer-driven sampling only uses cheap sources and skips overlapping samples. Heavy collection (top, launchctl list inside the simulator) runs only at platform.ready, the first app.ready, and dispose, and is bounded on teardown. When diagnostics are off, no probe, timer, listener, or process is started. Samplers pause between runs in watch mode.

The diagnostics config option accepts an object (enabled, probes: { host, node, snapshots, platform }, sampleIntervalMs) in addition to true, and RN_HARNESS_DIAGNOSTICS keeps working. Entries stream to .harness/diagnostics/<sessionId>.jsonl; the Chrome trace still goes to <runId>.json, with gauges as counter tracks that Perfetto graphs; KPIs (boot wait, time to ready, peak load, peak compressed memory, minimum free memory, swapouts, event-loop lag) are printed, saved to <runId>.summary.json, and appended to the GitHub Actions job summary when available.

Why is this useful?

It turns "the simulator did not start" into numbers: how long the boot waited and at which stage, what the load average and free memory were at the time, whether the Harness process itself was starved, and which processes held the memory. Those numbers survive a cancelled job and can be compared across runs, so decisions about runner size, simulator slimming, or startup ordering can be made from evidence instead of guesses. The data has zero cost for users who leave diagnostics off.

V3RON added 9 commits October 16, 2026 08:40
Adds packages/tools/src/telemetry with probes that translate polled host and
process data into the existing Diagnostics sink: run context, host resources
(load, CPU split, vm_stat memory pressure), Node runtime (memory, event loop
lag), lifecycle driven process snapshots and a platform collector bridge.
Probes are started through one composition point and cost nothing when
diagnostics are disabled.
waitForBoot now returns the last stage and elapsed time that simctl bootstatus
reported. Rather than adding diagnostics calls to the runner, instrumented()
accepts optional resultAttrs/errorAttrs derivers; the simctl module is wrapped
with ones that attach the stage to the platform.ios.simctl.waitForBoot span,
including when the wait times out or is aborted (the case that matters most).

Platform runners can expose an optional createTelemetryCollector(); the
simulator runner implements it to count guest launchd services at
platform.ready and dispose through the (instrumented) simctl module.
diagnostics can now be a boolean or an object with enabled, probes
(host, node, snapshots, platform) and sampleIntervalMs. isDiagnosticsEnabled
keeps its semantics (true, an object that does not opt out, or
RN_HARNESS_DIAGNOSTICS), and resolveDiagnosticsOptions fills in defaults.
The session now starts telemetry through one composition point
(startSessionTelemetry) and only emits lifecycle phases (session.created,
metro.ready, platform.ready, app.ready, test-file.start/end, dispose); probes
translate them into diagnostics entries. End-of-run collection is awaited, with
a bound, before the platform is torn down.

Entries are appended to .harness/diagnostics/<sessionId>.jsonl as they arrive
(batched, flushed on exit and on SIGINT/SIGTERM) so a cancelled CI job keeps
its data. Gauges are written to the Chrome trace as counter events so Perfetto
graphs them. summarizeRunKpis reduces a run to boot wait, time to ready, host
pressure peaks and the result; it is printed, written to <runId>.summary.json
and appended to GITHUB_STEP_SUMMARY when set.
TelemetryCollector.collect now receives an abort signal that fires when the
probe stops or the context aborts, and the iOS guest process listing passes it
to its spawn, so teardown never waits on a measurement nobody will read.
run.dispose() no longer waits for work whose context was already aborted, and
bounds the wait otherwise.

Queued probe jobs are skipped once a probe stops or aborts, so no collect()
runs after collector.dispose() and no AbortError snapshot marks are written.
The process snapshot is now taken at platform.ready, the first app.ready and
dispose only, instead of after every app relaunch.

Samplers pause on the new run.end phase and resume on run.start or dispose, so
a watch-mode session no longer accumulates idle samples while a single run
still samples from session start to teardown. Diagnostics.subscribe gains a
replay option for sinks attached after entries were recorded.

guest_process_count now counts only launchd rows with a PID (running
processes), not loaded jobs.
The appender now writes asynchronously, one chunk at a time, so recording an
entry never blocks on disk; only the exit handler writes synchronously (and
also writes chunks still pending). SIGINT/SIGTERM handlers are removed: the
session owns those signals and leaves through process.exit, and a re-raise
that depends on listener order can fight other handlers. The appender replays
entries recorded before it attached, so session.config.load is in the log.

Entries recorded after a run's report was written (the dispose-phase process
snapshot and guest process count, dispose.total) are appended to that run's
trace when the session is disposed. executeRun emits run.start and run.end so
samplers pause between runs in watch mode. Telemetry is also stopped when
setup fails before the main try block.
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-native-harness Ready Ready Preview Oct 16, 2026 11:31am UTC

Request Review

The runtime package typechecks the tools sources with the DOM lib, where
setInterval and setTimeout return numbers, so calling unref() on the result
and annotating it as NodeJS.Timeout failed CI. Timers now go through a guarded
unrefTimer helper and ReturnType-based annotations that hold under both libs.
@V3RON
V3RON merged commit 9c257af into main Oct 16, 2026
6 checks passed
@V3RON
V3RON deleted the feat/run-telemetry-probes branch October 16, 2026 12:03

This branch was successfully deployed

1 active deployment
Preview — eef52289 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant