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.
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
> Docs index: [`../README.md`](../README.md). Project overview:
|
||||
> [`../../README.md`](../../README.md).
|
||||
|
||||
| Note | What it argues |
|
||||
| --- | --- |
|
||||
| [`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md) | Durability before delivery — surfacing "commit landed" at ~0.2 s makes the 5–10 s `Ctrl-G` push feel instant. |
|
||||
| [`git-sync-images-and-repo-size.md`](git-sync-images-and-repo-size.md) | Why we don't shrink the notes repo — its 153 MB of media is remanso's image CDN, so rewriting history to slim the on-device clone breaks the web app. |
|
||||
| [`boot-time-budget.md`](boot-time-budget.md) | Where the ~4.3 s to cursor goes — and why the ≤ 3 s v1.0 target is hard: one ~1.9 s full refresh is unavoidable at cold boot, so the splash is nearly free. |
|
||||
| [`sync-latency.md`](sync-latency.md) | Where the ~16 s cold `:sync` goes — Wi-Fi + SNTP + one TLS push; why optimistic-retry cut a whole handshake, and why the rest is close to the protocol floor. |
|
||||
| Note | What it argues |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md) | Durability before delivery — surfacing "commit landed" at ~0.2 s makes the 5–10 s `Ctrl-G` push feel instant. |
|
||||
| [`git-sync-images-and-repo-size.md`](git-sync-images-and-repo-size.md) | Why we don't shrink the notes repo — its 153 MB of media is remanso's image CDN, so rewriting history to slim the on-device clone breaks the web app. |
|
||||
| [`boot-time-budget.md`](boot-time-budget.md) | Where the ~4.3 s to cursor goes — and why the ≤ 3 s v1.0 target is hard: one ~1.9 s full refresh is unavoidable at cold boot, so the splash is nearly free. |
|
||||
| [`sync-latency.md`](sync-latency.md) | Where the ~16 s cold `:sync` goes — Wi-Fi + SNTP + one TLS push; why optimistic-retry cut a whole handshake, and why the rest is close to the protocol floor. |
|
||||
|
||||
@@ -16,25 +16,25 @@
|
||||
|
||||
From the boot serial log (power-on → first editor frame + input loop live):
|
||||
|
||||
| Phase | ~ms | Lever |
|
||||
| --- | ---: | --- |
|
||||
| ROM + 2nd-stage bootloader + app image load | ~550 | flash speed (DIO now; QIO / 80 MHz ≈ −200 ms, speculative) |
|
||||
| PSRAM init + **memtest** + heap | ~920 | `CONFIG_SPIRAM_MEMTEST=n` → **−730 ms** (kept **on**: a real HW sanity check on a hand-wired board) |
|
||||
| EPD reset + init | ~130 | fixed panel bring-up |
|
||||
| **Splash full refresh** | ~1850 | e-ink floor — see below |
|
||||
| SD mount + note load | ~70 | quick on the genuine 32 GB SDHC |
|
||||
| USB host install + git thread spawn | ~60 | background |
|
||||
| **First editor render** (full-area partial) | ~680 | already fixed from ~1870 ms (was a *second* full refresh) |
|
||||
| **Total** | **~4260** | |
|
||||
| Phase | ~ms | Lever |
|
||||
| ------------------------------------------- | --------: | --------------------------------------------------------------------------------------------------- |
|
||||
| ROM + 2nd-stage bootloader + app image load | ~550 | flash speed (DIO now; QIO / 80 MHz ≈ −200 ms, speculative) |
|
||||
| PSRAM init + **memtest** + heap | ~920 | `CONFIG_SPIRAM_MEMTEST=n` → **−730 ms** (kept **on**: a real HW sanity check on a hand-wired board) |
|
||||
| EPD reset + init | ~130 | fixed panel bring-up |
|
||||
| **Splash full refresh** | ~1850 | e-ink floor — see below |
|
||||
| SD mount + note load | ~70 | quick on the genuine 32 GB SDHC |
|
||||
| USB host install + git thread spawn | ~60 | background |
|
||||
| **First editor render** (full-area partial) | ~680 | already fixed from ~1870 ms (was a _second_ full refresh) |
|
||||
| **Total** | **~4260** | |
|
||||
|
||||
Two lines carry the weight: the **splash full refresh (~1.85 s)** and the
|
||||
**first editor render (~0.68 s)**. Everything else is ≤ ~0.9 s combined, and the
|
||||
biggest of *those* — the ~0.73 s PSRAM memtest — is a deliberate keep.
|
||||
biggest of _those_ — the ~0.73 s PSRAM memtest — is a deliberate keep.
|
||||
|
||||
## The insight: one full refresh is unavoidable, so the splash is nearly free
|
||||
|
||||
After power-on the panel controller's `0x26` "previous" RAM bank holds garbage. A
|
||||
partial refresh *diffs the new image against that bank*
|
||||
partial refresh _diffs the new image against that bank_
|
||||
([`../tradeoff-curves/epd-refresh-latency.md`](../tradeoff-curves/epd-refresh-latency.md)),
|
||||
so the **first clean paint must be a full refresh** (~1.9 s) to establish a known
|
||||
image. There is no way around this on this panel short of a different waveform.
|
||||
@@ -42,12 +42,12 @@ image. There is no way around this on this panel short of a different waveform.
|
||||
That reframes two things:
|
||||
|
||||
- **The splash costs almost nothing.** Boot needs one full refresh regardless; the
|
||||
splash simply *is* that refresh, turned into a "boot is happening" affordance.
|
||||
splash simply _is_ that refresh, turned into a "boot is happening" affordance.
|
||||
Dropping the splash would **not** save the 1.9 s — the editor's first frame would
|
||||
then have to be the full refresh instead. (This is exactly what the old boot did
|
||||
and why it paid *two* full refreshes.)
|
||||
and why it paid _two_ full refreshes.)
|
||||
- **The v0.1 win was removing the second full refresh, not the first.** Once the
|
||||
splash has seeded a clean baseline, the editor rides in on a full-area *partial*
|
||||
splash has seeded a clean baseline, the editor rides in on a full-area _partial_
|
||||
(~0.63 s) instead of a second full refresh (~1.9 s) — the ~1.25 s saving that
|
||||
took cold boot from ~5.5 s to ~4.26 s. Verified clean on-panel (no splash
|
||||
ghost behind the editor text).
|
||||
@@ -60,7 +60,7 @@ To go from ~4.26 s to ≤ 3 s needs ~1.26 s cut. The honest lever list:
|
||||
reasonable once the board is no longer hand-wired.
|
||||
- **Faster flash boot (QIO / 80 MHz):** ~−0.2 s, speculative, needs a bench check.
|
||||
- **Overlap cheap init under the splash busy-wait:** SD mount + note load + USB
|
||||
install (~0.13 s total) currently run *after* the splash refresh returns, but
|
||||
install (~0.13 s total) currently run _after_ the splash refresh returns, but
|
||||
the refresh is a `wait_while_busy` spin — those could be kicked off before it.
|
||||
Saves ~0.1 s at most.
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ reasons worth writing down before anyone force-pushes a rewritten history.
|
||||
|
||||
## The measured reality
|
||||
|
||||
| Metric (target repo, measured 2026-07-07) | Value |
|
||||
| --- | --- |
|
||||
| Working tree | 3.9 GB (dominated by `node_modules`) |
|
||||
| `.git` (what a clone actually transfers) | **566 MB** |
|
||||
| Commits / objects | 13,852 / 63,252 |
|
||||
| Depth-1 snapshot (HEAD tree + blobs) | **154.7 MB** |
|
||||
| — of which markdown (the notes) | **1.4 MB** |
|
||||
| — of which media (png/jpg/pdf/gif/bmp) | **153 MB** |
|
||||
| Media across *all* history (dedup) | 566 MB (715 PNG objects = 463 MB) |
|
||||
| Metric (target repo, measured 2026-07-07) | Value |
|
||||
| ----------------------------------------- | ------------------------------------ |
|
||||
| Working tree | 3.9 GB (dominated by `node_modules`) |
|
||||
| `.git` (what a clone actually transfers) | **566 MB** |
|
||||
| Commits / objects | 13,852 / 63,252 |
|
||||
| Depth-1 snapshot (HEAD tree + blobs) | **154.7 MB** |
|
||||
| — of which markdown (the notes) | **1.4 MB** |
|
||||
| — of which media (png/jpg/pdf/gif/bmp) | **153 MB** |
|
||||
| Media across _all_ history (dedup) | 566 MB (715 PNG objects = 463 MB) |
|
||||
|
||||
Two early assumptions corrected:
|
||||
|
||||
@@ -38,7 +38,7 @@ Two early assumptions corrected:
|
||||
The clean fix for "device only needs 1.4 MB of text" would be a blobless
|
||||
partial clone (`--filter=blob:none`) — **libgit2 does not support it**. The
|
||||
fallbacks are LFS migration, a filter-repo history purge, or `git rm` of media
|
||||
at HEAD. All three remove the image *blobs* from what a git client sees.
|
||||
at HEAD. All three remove the image _blobs_ from what a git client sees.
|
||||
|
||||
That breaks **remanso**, the web app that reads the same repo. remanso is a
|
||||
frontend with no server-side git; it displays a note image by reading the
|
||||
@@ -50,7 +50,7 @@ image straight out of git as a blob and inlining it as a data URI
|
||||
3. `GET /repos/{owner}/{repo}/git/blobs/{sha}` → **base64 of the git blob**
|
||||
4. `img.src = "data:image/jpeg;base64," + thatContent`
|
||||
|
||||
The image *is* the git blob. GitHub's Git Blobs / Trees / Contents APIs do
|
||||
The image _is_ the git blob. GitHub's Git Blobs / Trees / Contents APIs do
|
||||
**not** resolve LFS (only `media.githubusercontent.com` does, which remanso
|
||||
doesn't use). So after an LFS migration those endpoints return the ~130-byte
|
||||
**pointer text**, remanso wraps it in `data:image/jpeg;base64,…`, and every
|
||||
@@ -62,20 +62,20 @@ the blobs from the tree, so remanso can't find them either. **Any approach
|
||||
that takes the images out of the git repo breaks remanso**, because those
|
||||
150 MB are load-bearing infrastructure for the web app, not offloadable bloat.
|
||||
|
||||
| Consumer | How it reads images | If we shrink the repo |
|
||||
| --- | --- | --- |
|
||||
| Typoena (libgit2) | doesn't render images; needs valid git objects | fine — would get tiny pointers |
|
||||
| remanso (Blobs API → data URI) | reads image bytes straight out of git | **broken** — pointer/missing bytes render as a dead image; uploads bypass LFS |
|
||||
| Consumer | How it reads images | If we shrink the repo |
|
||||
| ------------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Typoena (libgit2) | doesn't render images; needs valid git objects | fine — would get tiny pointers |
|
||||
| remanso (Blobs API → data URI) | reads image bytes straight out of git | **broken** — pointer/missing bytes render as a dead image; uploads bypass LFS |
|
||||
|
||||
## Decision
|
||||
|
||||
**Leave the notes repo untouched. Pre-seed the device SD card with a full
|
||||
`git clone` from a computer.**
|
||||
|
||||
Repo size is only a *device* constraint when the *device* does the cold clone.
|
||||
Repo size is only a _device_ constraint when the _device_ does the cold clone.
|
||||
A laptop clones 566 MB in ~2 minutes onto the SD via a card reader; the SD has
|
||||
GB to spare. The device then only ever takes the `open` + incremental
|
||||
fetch/commit/push path (`open_or_clone` already splits on this). A *full*
|
||||
fetch/commit/push path (`open_or_clone` already splits on this). A _full_
|
||||
pre-seed (not depth-1) also sidesteps the shallow-push sharp edge. remanso
|
||||
keeps working, the device gets everything, and repo size stops being anyone's
|
||||
problem.
|
||||
@@ -87,7 +87,7 @@ done so it goes straight into Typoena:
|
||||
the card's `repo/`, excluding everything the repo's `.gitignore` ignores (so
|
||||
`node_modules` and local secrets like `firmware/.env` never land on the card),
|
||||
then writes `/sd/typoena.conf` — Wi-Fi creds + PAT + git identity — from the
|
||||
TW_* vars already in `firmware/.env` (no re-typing, no prompts).
|
||||
TW\_\* vars already in `firmware/.env` (no re-typing, no prompts).
|
||||
- `just load <repo-path>` — the repo copy on its own (refresh after big upstream
|
||||
changes).
|
||||
- `just provision` — the config on its own (rotate the PAT / switch networks
|
||||
@@ -112,7 +112,7 @@ Once integration lands, the costs of carrying media the device never renders:
|
||||
1. **Bandwidth for unusable bytes.** No partial fetch in libgit2, so a fetch
|
||||
pulls the full new image blobs. 20 MB of pasted screenshots = a 20 MB fetch
|
||||
before a one-line note can publish.
|
||||
2. **~2× SD storage.** Each image lives in `.git` *and* in the working-tree
|
||||
2. **~2× SD storage.** Each image lives in `.git` _and_ in the working-tree
|
||||
checkout that `checkout_head(force)` writes.
|
||||
3. **Memory — the real edge.** libgit2 tends to materialize a whole blob in
|
||||
RAM for checkout. History already has a 38 MB mp3 and 16 MB PNGs. Against
|
||||
|
||||
@@ -18,23 +18,23 @@
|
||||
From the serial log, first `:sync` after a cold boot
|
||||
(`… wifi 3654ms, clock 2108ms, tls 304ms, publish(commit+push) 9944ms, total 16012ms`):
|
||||
|
||||
| Phase | ~ms | One-time? | Lever |
|
||||
| --- | ---: | --- | --- |
|
||||
| Wi-Fi assoc + DHCP | ~3650 | yes (per power cycle) | radio off until first `:sync`; association floor |
|
||||
| SNTP first sync | ~2100 | yes | varies with NTP RTT (4.2 s the prior run); needed before TLS + commit time |
|
||||
| TLS trust store install | ~300 | yes | write ~6 KB CA bundle to SD + set libgit2 option |
|
||||
| **publish** = stage+commit + push | **~9900** | **every sync** | see below |
|
||||
| **Total** | **~16000** | | |
|
||||
| Phase | ~ms | One-time? | Lever |
|
||||
| --------------------------------- | ---------: | --------------------- | -------------------------------------------------------------------------- |
|
||||
| Wi-Fi assoc + DHCP | ~3650 | yes (per power cycle) | radio off until first `:sync`; association floor |
|
||||
| SNTP first sync | ~2100 | yes | varies with NTP RTT (4.2 s the prior run); needed before TLS + commit time |
|
||||
| TLS trust store install | ~300 | yes | write ~6 KB CA bundle to SD + set libgit2 option |
|
||||
| **publish** = stage+commit + push | **~9900** | **every sync** | see below |
|
||||
| **Total** | **~16000** | | |
|
||||
|
||||
The three one-time phases (~6.1 s) only pay on the *first* sync of a power cycle —
|
||||
The three one-time phases (~6.1 s) only pay on the _first_ sync of a power cycle —
|
||||
Wi-Fi, the clock, and the trust store are set up once and reused, so a **warm sync
|
||||
is just the ~10 s publish**. Publish splits as:
|
||||
|
||||
| Sub-phase | ~ms | Note |
|
||||
| --- | ---: | --- |
|
||||
| stage + commit | ~3150 | `add_all(["*"])` walking the SD/FAT working tree, then commit to FAT |
|
||||
| push: TLS handshake | ~2400 | one mbedTLS handshake to github.com |
|
||||
| push: pack negotiate + upload | ~4400 | tiny delta — cost is negotiation/round-trips, not payload |
|
||||
| Sub-phase | ~ms | Note |
|
||||
| ----------------------------- | ----: | -------------------------------------------------------------------- |
|
||||
| stage + commit | ~3150 | `add_all(["*"])` walking the SD/FAT working tree, then commit to FAT |
|
||||
| push: TLS handshake | ~2400 | one mbedTLS handshake to github.com |
|
||||
| push: pack negotiate + upload | ~4400 | tiny delta — cost is negotiation/round-trips, not payload |
|
||||
|
||||
## The win: one TLS handshake, not two
|
||||
|
||||
@@ -45,7 +45,7 @@ sync (remote unchanged), and it did ~6 s of real work the one time it absorbed a
|
||||
maintenance commit.
|
||||
|
||||
The optimistic-retry rewrite (commit `3386969`) drops it: **push onto the current
|
||||
tip first**; only if the remote *rejects* the push non-fast-forward do we fetch,
|
||||
tip first**; only if the remote _rejects_ the push non-fast-forward do we fetch,
|
||||
reconcile, and retry. The happy path — what runs ~99 % of the time — is now a
|
||||
**single** handshake. That took the true normal-cold baseline from ~19 s to
|
||||
**16.0 s** (and the inflated 23.7 s figure will never recur, since it was the
|
||||
@@ -57,8 +57,8 @@ On a rejected push, `reconcile_onto_origin` fetches origin and does a **mixed**
|
||||
reset onto it — moving the branch ref + index but leaving the working tree, so the
|
||||
just-saved note survives — then `stage_and_commit` replays the note on the new tip
|
||||
and retries. For this **single-writer appliance** that resolves last-writer-wins:
|
||||
a concurrent remote *edit* to the same note loses to ours, and a remote-only
|
||||
*added* file the card doesn't have would be dropped by the replay's `add --all`.
|
||||
a concurrent remote _edit_ to the same note loses to ours, and a remote-only
|
||||
_added_ file the card doesn't have would be dropped by the replay's `add --all`.
|
||||
Both need a real merge (increment B) and don't arise from the device's own use.
|
||||
This path is **hardware-verified for the happy case** (`9b635c42` fast-forwarded
|
||||
clean); the reconcile branch itself is compile-verified but not yet exercised on
|
||||
@@ -77,7 +77,7 @@ The big rocks are physics or protocol, not slack:
|
||||
shave.
|
||||
- **stage + commit ~3.1 s** is the one soft spot: staging over the editor's dirty
|
||||
set (`add_path`) instead of `add_all(["*"])` would skip the SD/FAT tree walk
|
||||
(likely → sub-second) *without* losing multi-file — the dirty set is the file
|
||||
(likely → sub-second) _without_ losing multi-file — the dirty set is the file
|
||||
list. Whether the walk actually dominates the ~4 s commit is now being measured
|
||||
by the `commit split —` log line; the cost model and the rule it decides live in
|
||||
[`../tradeoff-curves/sync-commit-staging.md`](../tradeoff-curves/sync-commit-staging.md).
|
||||
@@ -86,5 +86,5 @@ The big rocks are physics or protocol, not slack:
|
||||
one TLS push over Wi-Fi with a fresh clock." It reads as slow only if you wait on
|
||||
it — and by design you don't: `:sync` is a deliberate action with a snackbar, and
|
||||
[`ctrl-g-perceived-latency.md`](ctrl-g-perceived-latency.md) argues the perceived
|
||||
cost is set by *when durability is surfaced*, not by wall-clock. Recorded here so
|
||||
cost is set by _when durability is surfaced_, not by wall-clock. Recorded here so
|
||||
the number is scoped against the protocol, not treated as a regression.
|
||||
|
||||
Reference in New Issue
Block a user