Ensures the app never generates frames without a cause (frame emission
is event-driven; a spinner would burn CPU/battery for the app's entire
idle life). Verified on the emulator and the phone: healthy runs show
ZERO frames across all idle windows (PERF-PRESENT fps < 1).
- perf: rows whose previous frame is >= 100ms away are idle gaps, not
slow frames — flagged in the CSV (new gap column) and kept out of the
logcat summary's latency percentiles (reported as gaps=N maxGap=...).
A frame >= 2s after the previous one flushes the CSV, so a burst's
rows are persisted the moment the next burst starts (an idle tail or
force-stop no longer loses the last burst).
- editor: debug 'open <path>' command (cmd-file poller, perf mode):
opens a file from any page via the same OpenFile path as a browser
tap — deterministic, no pixel tapping.
- e2e: TestNoFramesWhileIdle (browser + editor after load/scroll/find)
asserts the logic emits ZERO frames across an idle window once
settled — the headless contract, in the regular go-test suite.
- scripts/profile_emulator.sh: drives launch, 8s idle, open (seeded
4000-line file), 16s idle, 3 scrolls on a device/emulator; groups the
CSV into phases at idle gaps >= 2s and FAILs on a phase over its frame
budget (1/s spinner exceeds a 16s idle budget; faster ones balloon a
phase or show in PERF-PRESENT). Validated both ways: PASS on a healthy
build (emulator + phone), FAIL on an injected 500ms frame spinner.
- doc: architecture.md §11 updated (gap handling, burst flush, debug
open, frame-regression guard, 2026-08 measurements).
internal/perf: single-goroutine profiler (no locks). When enabled by the
/storage/emulated/0/PadPerf/enable marker it records one CSV row per logic
frame (seq, ms, frame delta, page, scroll Dp, max-scroll Dp, total lines,
visible byte range), logs a rolling ~1/s summary, and on Stop reports
nearest-rank p50/p90/p99/max. Flushes per row-batch but never fsyncs per
flush (avoids periodic hitches in the logic path).
editor: PerfRecord package-level hook (nil when off) + ProbeRecord;
Logic.emitFrame() now centralizes every frame emission so the profiler sees
each logic frame exactly once, on the owner goroutine. State gains
VisibleStart/VisibleEnd so the probe can confirm shaping stays
viewport-bounded.
logic: optional debug cmd poller (off by default) watches <dir>/cmd as a
one-shot file (top/bottom/frac <0..1>/dp <int>) and the owner applies a
clamped [0,MaxScroll] jump + frame. Enables deterministic large-offset scroll
tests without pixel taps.
main: wires the profiler when the marker file exists; logs present-fps
every 2s; stops the profiler on Destroy.
docs: README package inventory (add internal/perf); architecture §11
profiler facility; spec §5 invariant 7 (scroll always clamped to
[0,maxScroll]) + §6 measured rows; development_plan v5 + Phase 6 results.
Verified: go build/vet + full -race green. On-device 10 MB file:
logic-frame cadence flat across offsets 0.02->1.0 (no large-offset
degradation), visible byte range <=4.3 KB at every offset, clamping exact
across 2->130,955-line files, PSS plateaus ~250 MB (bounded, no leak);
profiler overhead negligible.