Pad/doc/README.md
Greg Pomerantz 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

82 lines
4.1 KiB
Markdown

# Pad documentation
Three documents, kept at the level of *what, why, and invariants* — not
line-by-line code — so they stay true as the implementation evolves.
| Doc | What it is |
|---|---|
| [`spec.md`](./spec.md) | What the app **actually does**, measured performance, the code layout, and an explicit list of deferred features. |
| [`architecture.md`](./architecture.md) | How it works: single-owner concurrency model, channel topology, Frame handoff contract, ownership rules, editor/browser/render internals. |
| [`development_plan.md`](./development_plan.md) | The active plan: completed phases, remaining work, and the on-device observation loop. |
## Package inventory
| Package | Role |
|---------|------|
| `internal/editor` | Chunked buffer, line index, virtualized viewport, IME ops, autosave, logic goroutine, state, Frame |
| `internal/browser` | Directory browsing, search, sort, pagination, browser state machine |
| `internal/ui` | Element tree, frame layout, renderer (op-based), IME wiring, gestures |
| `internal/perf` | Default-off performance profiler (per-logic-frame cadence + scroll state → CSV) |
| `internal/io/pool` | Worker pool (8 workers, 2 priority lanes) + file/dir tasks |
| `internal/test/e2e` | Harness driving the real `Logic` + `Inspect` |
| `cmd/pad` | `main.go` (entry), `impl_android.go` (base path) |
## Documentation policy
1. **Docs describe invariants and contracts, not code lines.** If a document
has to change on every refactor, it is too detailed — delete it or raise
its level of abstraction. Point-in-time implementation plans are deleted
once implemented (the previous `*_implementation_plan.md`,
`touch.md`, `element_model.md`, `layout_rendering.md`,
`virtual_scroll_render_optimization.md`, and `conflict_resolution.md` were
removed in the 2026-08 doc reorganization for this reason).
2. **Unbuilt behavior is not spec'd.** Requirements that are not in the code
live in `spec.md` §7 (deferred), never as if they worked.
3. **Code wins.** When doc and code disagree, fix the doc.
## Build & install (Android, this workstation)
Prereqs on this box (already installed): JDK 17, Go, Android SDK at
`~/android-sdk` (platform-tools, build-tools 35.0.0, emulator, NDK),
`gogio@v0.10.0` in `~/go/bin`, `apktool` at `/tmp/apktool.jar`, debug
keystore at `~/.android/debug.keystore`. Env vars are set in `~/.bashrc`.
The full recipe is `/tmp/build_pad.sh` (machine-local). Steps:
1. `gogio -target android -targetsdk 35 -arch amd64 -o /tmp/pad-raw.apk .`
(from `cmd/pad/`)
2. `apktool d /tmp/pad-raw.apk -o /tmp/pad_decoded -f`
3. Inject `<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>`
into the decoded `AndroidManifest.xml` (Android 15 needs it for
`/storage/emulated/0`).
4. `apktool b /tmp/pad_decoded -o /tmp/pad-unsigned.apk`
5. `apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android
--key-pass pass:android --ks-key-alias androiddebugkey --out cmd/pad/pad-emu.apk /tmp/pad-unsigned.apk`
6. `adb install -r cmd/pad/pad-emu.apk`
Run the emulator (headless, AVD `pad_avd`, API 35):
```
nohup emulator -avd pad_avd -no-window -no-audio -no-boot-anim \
-gpu swiftshader_indirect >/tmp/emulator.log 2>&1 &
```
App package/activity: `pad.pad / org.gioui.GioActivity`. Default root
directory: `/storage/emulated/0/Notes` (push test files there with
`adb push file /storage/emulated/0/Notes/`).
## On-device observation loop (quick reference)
Gio renders into one GL surface, so the authoritative debug signals are
data, not pixels:
- `adb logcat -s pad.pad` — app log (IME commits, open-file timing, errors).
- `adb shell input tap|swipe|text` — drive the UI (see `development_plan.md`
for the tap coordinates and IME-tap cadence that works).
- Autosave debounce is 1 s: **wait ~1.6 s before reading a file back from
disk** after typing.
- Memory: `adb shell dumpsys meminfo pad.pad` (watch PSS/RSS; the 3.8 GB
emulator OOMs the app above ~2.5 GB RSS).
- Screenshots (`adb exec-out screencap -p > /tmp/x.png`) are an auxiliary
check only — state, logcat, and file diffs are authoritative.