Files
typewriter/docs/tradeoff-curves/wifi-auto-sync.md
Julien Calixte 43d80b518f style: run oxfmt repo-wide
First full pass of the pnpm fmt toolchain: markdown table alignment,
TOML array wrapping, CSS/HTML expansion in the enclosure concept page.
No content changes; mermaid blocks untouched.
2026-07-14 12:40:51 +02:00

9.0 KiB
Raw Blame History

Wi-Fi energy vs auto-sync interval

Decision: auto_sync defaults to 10 min, and is an opportunistic, rate-limited push — not a wall-clock timer that wakes the device. See Policy. Backs the .typoena.toml auto_sync key in ../macroplan.md (v0.5). The runtime behaviour was pencilled for v0.7, but v0.7 closed 2026-07-14 as search + manual :gl/:gp without it — it is re-homed to v0.8 (../v0.8-battery-and-sleep.md), alongside the sleep transitions it must respect and the per-sync radio teardown it depends on.

Revised 2026-07-14 with the first real-device sync measurements (v0.7 runs on the real ~560 MB notes repo, see ../v0.7-search-and-git.md): the per-sync constant roughly doubles and its mix shifts from radio to SD/CPU, but the shape, the knee, and the decision all survive — and two v0.7 results make the teardown-between-syncs argument stronger.

Tradeoff-curves index: README.md. Docs index: ../README.md.

The model

For a text commit the git payload is a few KB — negligible. The cost of one sync is (almost) independent of how much you wrote — but it is no longer the single fixed radio burst the first version of this doc assumed. Measured on device (v0.7 run 4, 2026-07-14, warm clean :gp on the real notes repo):

splice commit (SD/CPU, radio not needed)   10.3 s   O(depth) loose writes × ~0.4 s FAT dir scan
push leg (connect + pack + upload)          5.9 s   TLS-resumed connect 2.4 s (4.0 s first-of-session)
                                          ──────
warm `:gp` end-to-end                      19.1 s   root-level file ≈ 1213 s — the splice is depth-bound

Both halves are paid once per dirty sync, so energy per unit time still scales as (fixed cost per sync) × (syncs per hour):

E(T) = K / T          T = interval in minutes,  K = one sync's worth of energy

A hyperbola. Doubling the frequency doubles the cost; the words you actually wrote barely move it. What the measurement changed is K and its composition: the awake window is ~1219 s (not the ~8 s placeholder), and more than half of it is SD/CPU splice work that no radio policy can touch — but the splice re-runs on every sync too, so it rides the same 1/T curve. The splice scales with file depth and dirty-set size, not with words (the FAT-dir-scan story in sync-commit-staging.md, run 6).

Placeholder currents, still pending the v0.8 bench measurement ("measure idle / typing / push current draw"): ~10 s of CPU+SD at ~100 mA plus ~8 s of radio-on at ~150 mA ⇒ ≈ 0.6 mAh per sync, K ≈ 36 mAh·min/hr — about 2× the original estimate. The vertical scale below moves with the real current measurement; the shape and the knee do not.

Two refinements the field runs added:

  • A clean tick is free. Since the dirty-journal plumbing (2026-07-13), :gp with an empty journal answers "up to date" without touching the radio at all. An auto-sync tick only pays when there is actually something to push — idle hours with no edits cost zero regardless of the interval.
  • The worst case is bounded at ~2×. If the mirror moved underneath (a Mac push), an auto-sync hits the rejected-push → reconcile → replay → push cycle: 24.0 s and three connections measured (run 3, with session resumption). Rare, and it doesn't move the knee.

One assumption is baked into the burst: the radio is fully off between syncs, not parked in modem-sleep. Holding the association awake to skip the per-sync handshake costs ~1520 mAh/hr on the WROOM — more than a 1-min interval and ~10× the 10-min default. Two v0.7 results bury keep-alive further:

  • TLS session resumption (the esp_mbedtls_stream.c vendor delta) cut a reconnect to 2.4 s vs 4.0 s for the session's first: the handshake that staying-associated was supposed to amortise is now the cheap part, and the session ticket lives in RAM — it survives radio de-init between syncs.
  • Held connections die anyway. In run 8 the idle keep-alive was closed during a ~31 s quiet gap and libgit2 does not reconnect (SSL Generic error, push lost). Keep-alive is not just expensive; it's fragile.

So each sync legitimately pays a fresh wake → associate → resumed-handshake burst, and "off" everywhere here means radio de-init, not beacon-listening. Tear the connection down immediately after each push, too: with syncs ≥ 2 min apart a keep-alive window saves nothing. (v0.7 added :gl pull, so "Typoena only ever pushes" is no longer literally true — but pull is user-initiated and outbound like everything else; nothing unsolicited ever arrives, so there is still no reason to stay reachable.)

Status (v0.7) — the shipped firmware still does not cycle the radio. Wi-Fi comes up lazily on the first git op and then stays associated for the rest of the session: ensure_online in ../../firmware/src/git_sync.rs owns the wifi handle and the module never stops, disconnects, or drops it (the remote.disconnect() calls in there are git smart-protocol connections, not the radio). So today's device runs the stay-associated strategy this section argues against, at ~1520 mAh/hr after the first git op. Per-sync teardown remains a v0.8 refactor of the modem ownership — and a prerequisite before any sleep mode ships.

The curve

E(T) = K / T with the revised K ≈ 36 mAh·min/hr (≈ 0.6 mAh per dirty sync):

xychart-beta
    title "Wi-Fi energy vs auto-sync interval (placeholder currents)"
    x-axis "interval (min)" ["1", "2", "5", "10", "15", "30", "60"]
    y-axis "mAh/hr" 0 --> 36
    bar [36, 18, 7.2, 3.6, 2.4, 1.2, 0.6]

The knee sits at 510 min: left of it, every extra sync per hour costs a full splice + radio burst for zero payload benefit; right of it, the tail is nearly flat and longer intervals save almost nothing.

interval syncs/hr Wi-Fi mAh/hr vs 5-min per 8 h day
1 min 60 36.0 5.0× 288 mAh
2 min 30 18.0 2.5× 144 mAh
5 min 12 7.2 1.0× 58 mAh
10 min 6 3.6 0.5× 29 mAh
15 min 4 2.4 0.33× 19 mAh
30 min 2 1.2 0.17× 9.6 mAh
60 min 1 0.6 0.08× 4.8 mAh

(Every row assumes the device was dirty at each tick — a writing session in progress. Clean ticks cost nothing, per the journal gate above.)

Two things that move where "best" sits

save_on_idle already prevents data loss — auto-sync is only remote-mirror freshness. The durable local copy is the SD write on the idle pause. A longer sync interval never risks losing work; it only means the GitHub mirror is a few minutes staler. That's a weak cost, and it pushes the optimum toward longer intervals. The doubled K pushes the same direction: each avoided sync now saves twice what the original model claimed.

The real battery risk is the sleep interaction, not the awake case. While you're typing, the CPU/e-ink baseline dwarfs the sync cost — 5 vs 15 min is noise. The damage happens when the device is idle or asleep and a wall-clock timer wakes it just to push: each wake pays the radio burst plus the wake/boot cost and blocks the low-power state. That turns "closed on the desk overnight" from weeks of standby into dead-by-morning.

Policy

Ship auto_sync as an opportunistic, rate-limited push, with the config value read as a max-staleness cap rather than a timer period:

  • Push when already awake + dirty, coalesced into the existing idle-pause, rate-limited to at most once per auto_sync — so a fast typist pausing every 20 s doesn't sync 100×/hr. The dirty journal makes the check free: a tick with nothing to push never spins the radio (this is :gp's shipped radio-free up-to-date path).
  • Push once on the way into sleep (idle → light sleep, and especially lid-close → deep sleep) if dirty. This is the highest-value sync: nearly free (the device is spinning up anyway) and it's the freshness guarantee.
  • Never wake from deep sleep purely to sync. The one behavior that wrecks standby life.

On the single number: 10 min halves the sync energy versus a 5-min default (3.6 vs 7.2 mAh/hr under the revised K) for essentially no real cost, because save_on_idle already owns data safety. Clamp the minimum to ~2 min so a palette command (> auto sync: 10s) can't quietly drain the battery. One new UX input since the first version: a dirty auto-sync now occupies the git thread for ~1219 s (UI stays live — proven by the run-4/5 concurrent-UI work), so at the 2-min clamp a heavy writer's device would be syncing ~15% of the time. The 10-min default keeps that under 3%.