Commit Graph

2 Commits

Author SHA1 Message Date
ac1c32e4c0 Add pre-release frame-regression profiling
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).
2026-08-20 14:18:08 -04:00
be48ad8157 perf: default-off in-app profiler + debug scroll jumps; verify scroll perf & clamping
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.
2026-08-16 16:53:28 -04:00