Commit Graph

324 Commits

Author SHA1 Message Date
Julien Calixte
7378d30450 fix(sd_bench): remove the scratch tree without trusting dirent types
std's remove_dir_all reads directories as char devices through the
prebuilt libc's generic-unix DT table, unlinks them, and FatFS refuses
with FR_DENIED — the 6b fan-out subdirs were the first to trigger it.
Recurse with the same both-tables decoding as the palette walk.
2026-07-14 09:44:36 +02:00
Julien Calixte
cdfeabc152 feat(editor): rename :sync to :gp
Pairs with :gl as the push half of the git pair. Same behavior
(fmt → save → commit → push); notices, logs, and current READMEs
follow, historical docs keep :sync as a record of their time.
2026-07-14 09:42:30 +02:00
Julien Calixte
e161b1eae7 docs(v0.7): close the version — run-3 fast-forward pull verified
Pulled shape passed on device: apply_tree_diff 1271ms for 1 file, no
crash, interning confirmed (0 KB internal on re-walk). Records run-3
timings, the reconcile 30s→5s session-resumption win, and the
1-byte-load/0-byte-save trailing-newline watch item.
2026-07-14 09:35:35 +02:00
Julien Calixte
761fd3a750 feat(editor): smartcase + accent-folded search
Supersedes the plain case-insensitive matcher from earlier today (user
decision): an all-lowercase pattern searches case-insensitively, one
capital makes it exact (vim smartcase), and diacritics always fold both
ways — /ete finds été, /été finds ete. fold() strips the Latin-1
accent set (ligatures œ/æ excluded: they fold to two chars); n/N
recompute the smartcase verdict from the remembered pattern so repeats
behave like the original search.
2026-07-14 09:29:31 +02:00
Julien Calixte
11545efb0a docs(v0.7): record run 2, the checkout rebuild, and the closing gate 2026-07-14 09:23:04 +02:00
Julien Calixte
44c65e9ea5 perf(sync): reuse the TLS session and skip the pull's needless fetch
Two halves of the connection-cost lever (first on-device pull: 9.7s
fetch to learn 'up to date'; a rejected-push :sync runs three full
handshakes):

- esp_mbedtls_stream.c caches the mbedTLS session at stream close and
  offers it on the next connect to the same host — the server then
  skips the certificate exchange and most of the key exchange. Second
  vendor delta, single git thread, best-effort (any refusal falls back
  to a full handshake).

- pull_once now ls-refs first: the ref advertisement alone answers
  'anything new?', so up-to-date and local-ahead return without ever
  entering pack negotiation, and a needed download rides the SAME open
  connection instead of a second handshake. Tracking-ref updates share
  one update_tracking helper with the reconcile fetch.
2026-07-14 09:21:58 +02:00
Julien Calixte
75166f74cf perf(palette): intern the file list into one PSRAM blob
1099 paths as individual Strings measured 182 KB of internal DRAM
resident (each alloc is under the 16 KB SPIRAM-malloc threshold), a
main contributor to the DMA-pool exhaustion behind the :gl crash. The
walk now appends into one newline-joined String seeded at 64 KB so it
and its growth land in PSRAM; the editor stores the blob plus sorted
byte spans (set_file_list_joined) and the palette reads slices. :enew
appends to the blob; :delete drops only the span. Also log the :gl
dirty-journal gate, which looked like a silent no-op on the serial log.
2026-07-14 09:17:36 +02:00
Julien Calixte
329995ab58 fix(sync): apply the pull fast-forward as a tree diff, not checkout_tree
First on-device ff attempt (2026-07-14) crashed: libgit2's SAFE
checkout iterates the whole working directory (readdir over SPI on all
~1100 files) to find dirty files — the same O(tree) wall the splice
commit avoids — and with fetch memory still resident it exhausted
internal DRAM; esp-idf's spi_master then NULL-derefs on its own
failed-DMA-alloc path instead of erroring.

apply_tree_diff mirrors the splice: diff_tree_to_tree skips identical
subtree OIDs in memory, then writes/deletes exactly the changed paths
(unlink+tmp+rename — FAT rename won't overwrite). Belt kept O(changed):
any to-be-clobbered file whose content no longer hashes to the old blob
aborts the pull before the first write. Half-applied + unmoved ref
self-heals on the next :gl (idempotent re-apply).

Also raise the internal DMA reserve 32→64 KB so the SPI driver never
sees a failed alloc (its error path is the crash), and log the fetch
completion so the fetch/apply phases are separable in the serial log.
2026-07-14 09:17:36 +02:00
Julien Calixte
0734c1f67e feat(editor): make / search case-insensitive
Unicode lowercase-fold comparison at char boundaries (find_ci/rfind_ci)
instead of str::find — no lowercased buffer copy, whose byte lengths
could shift the returned offsets. Prose search shouldn't care about
capitalization; deliberately not smartcase (a capital silently flipping
to exact-match is a surprise here, not a feature).
2026-07-14 09:09:01 +02:00
Julien Calixte
3009dc4d9c feat(boot): async splash refresh + background palette file walk
The splash's ~2.2s full-refresh waveform now runs while the SD mounts
and the note/prefs load (Epd::display_frame_async + wait_ready guard on
every public display call), and the 4.3s palette walk moves to a 16KB
background thread whose result lands in the idle branch — the first
editor frame no longer waits on non-mandatory work. The post-pull
re-walk rides the same channel, removing a 4.3s UI stall after :gl.

Also: rustfmt pass on sd_bench, drop the spike-9 log line in splash.
2026-07-14 01:34:28 +02:00
Julien Calixte
8c4866df1f feat(bench): attribute the loose-write residual — dir-scaling + splice I/O counters
sd_bench grows a directory-entry-scaling section (stat hit/miss and
the loose-object composite at 8/64/256 siblings): FAT resolves every
path component by linear scan, so cost climbing with N convicts
directory scans as the ~360ms residual; flat cost exonerates them.
stage_and_commit brackets the splice with the p_mmap counters and logs
'N mmaps / M KB read' per commit, splitting the residual into pack-read
I/O vs everything else on the next real :sync.
2026-07-14 01:12:35 +02:00
Julien Calixte
a941ae39b3 feat(sync): :gl pull — fetch + fast-forward only on the git thread
The git channel now carries GitRequest::{Publish,Pull} with a shared
ensure_online preamble (Wi-Fi/clock/TLS once per session). pull_once
fetches origin — refreshing the tracking ref so the radio-free
up-to-date check stays honest — and maps four shapes: up to date,
LocalAhead (stranded commit, :sync's job), clean fast-forward,
Diverged (refused; no merge on the device). The fast-forward is a SAFE
checkout then ref move: it refuses to overwrite content that differs
from HEAD, the belt under the UI gate that refuses :gl while the dirty
journal is non-empty. A RAM-dirty buffer doesn't gate — its edits win
(last-writer-wins, like the reconcile).

After a pull the UI drops clean parked buffers, re-reads the clean
active buffer in place (Editor::refresh_active), and re-walks the
palette list. Firmware 0.6.0 -> 0.7.0. On-device verification pending.
2026-07-14 01:10:00 +02:00
Julien Calixte
8d22f81804 feat(editor): / forward search with n/N repeat (v0.7)
Literal case-sensitive substring search sharing the : command line's
editing (cmd_prompt discriminates the prompt; Enter dispatches). Jumps
on Enter only — incremental caret-chasing loses to the e-ink refresh
cost, same call as the snippet no-popup decision. Wraps with a notice,
keeps the caret on a miss, bare / repeats the editor-global last
pattern. n/N live in move_by so counts and Visual extension come free;
they are not operator targets.
2026-07-13 22:15:19 +02:00
Julien Calixte
4f0d745e84 docs: sweep sync follow-ups into their owning version docs
Kaizen/postmortem next-steps kept accumulating with no owner. Each item
now lives in one version doc (v0.7 sync perf, v0.9 robustness, v1.0
boot) with the source docs linking there; the editor-freeze safety net
is marked hardware-verified and the v0.5 walk re-measure closed at 4.3s.
2026-07-13 22:07:34 +02:00
Julien Calixte
5d623151a8 chore(justfile): repack multi-pack source clones in _load-repo
Single-pack card prep is now enforced, not remembered: each pack's
whole .idx (plus any midx) is mmap'd outside the device's window
budget, and a multi-pack marking walk lost the HTTP keep-alive (run 8:
31s vs 3.5s repacked). Skipped when the clone is already one pack.
2026-07-13 22:07:26 +02:00
Julien Calixte
8d5bd1903d refactor(sync): retire the diagnostic push heartbeat
It served its purpose (localized the mmap plateau, runs 7-9); its 8KB
internal stack was live during the TLS send, where internal min-ever
hit 2099 bytes. The time-gated pack/transfer callbacks stay.
2026-07-13 22:07:16 +02:00
Julien Calixte
028ebc8f5f docs(kaizen): close the real-repo sync loop — first :sync at 24.1s
Step 6 evaluated: infinity (never completes) to 24.1s measured
snackbar-to-snackbar, with the measured after-bar charted in step 6 (the
step-1 chart stays potential-only, per the format). Documents the second
localization loop the verification runs forced on the push half: UI death
by shared heap fate, odb cache exonerated, telemetry axis bug, mmaps named
as the eater, the impossible fixed-maps budget, the keep-alive race, and
pack layout as a device-performance variable.
2026-07-13 21:45:47 +02:00
Julien Calixte
9e99c1767a fix(sync): budget mmaps at 64KB/1.5MB, trace the push heap live
Runs 7-9 on the real repo (2026-07-13): esp_map live-bytes wired into
log_push_heap plus a 5s sibling heartbeat thread proved the push OOM was
libgit2's mmaps — ~3.4MB of whole-file .idx/multi-pack-index maps living
OUTSIDE the mwindow budget, plus windows at the (working) 4MB soft limit,
an impossible budget on the 8MB pool. Shrinking to 64KB windows / 1.5MB
mapped limit left 4.5MB min-ever free and cut marking reads 4x; with the
card repacked to a single pack, the first-ever real-repo push landed
(6713ea6e, :sync 24.1s end-to-end).

The heartbeat stays for now (the marking phase fires no libgit2 callbacks,
so it is the only heap trace through it); its 8KB internal stack is part
of the min-ever-2099B-internal tightness flagged for follow-up.
2026-07-13 21:45:47 +02:00
Julien Calixte
f79d13453a fix(sync): survive push heap exhaustion and instrument the push path
Run 4 (2026-07-13): remote.push() consumed ~6 MB of PSRAM over 66 s and
the UI thread aborted on Frame::new_white's per-draw vec![0xFF; 26928].
Run 5 confirmed the UI now survives, but the push still exhausts both
pools (a ~7 KB inflateInit fails inside the pack build) and the heap
telemetry never fired — hence the telemetry rework.

- display/editor: Editor::draw_into() renders into a caller-owned
  Frame; main.rs keeps two boot-time frames (shown/back) and mem::swaps
  on success, so steady-state repaints never allocate.
- git_sync: odb cache capped at 1 MB via raw libgit2-sys opts (git2
  0.20 doesn't wrap the total cap); log_push_heap at pre-push,
  post-push and push-failure with largest-PSRAM-block and per-pool
  min-evers; pack_progress logging is now time-gated (2 s) — the
  count gate (+256 objects) never fired because AddingObjects reports
  total=0 and a small push inserts only dozens of objects.
- editor: empty/whitespace snippets file parses as 0 snippets instead
  of a JSON error at boot.
- build.rs: refuse a git-feature build with unset TW_* publish vars —
  a bare `cargo build --features git` baked empty creds and produced a
  firmware whose :sync could never work.
2026-07-13 21:04:18 +02:00
Julien Calixte
05706478a2 feat: wrap palette selection; fix push classing, file walk, card load
- Palette Ctrl-N/P (and half-page keys) wrap around the result list
  instead of clamping at the ends; tests updated to the new semantics.
- try_push maps libgit2's negotiation-time NotFastForward error to
  PushFailure::Rejected so the reconcile path runs instead of failing
  as a transport error.
- walk_files decodes esp-idf d_type values (DT_REG=1/DT_DIR=2) that
  libc 0.2.178's generic unix table misreads as fifo/chardev — the
  palette walk was silently dropping every file on the card.
- `just load` refuses to mirror over unpublished device edits listed
  in the card's .typoena-dirty journal (backup+discard via
  TW_DISCARD_UNPUBLISHED=1), gains --force for a from-scratch copy,
  and a flash-only recipe flashes the already-built ELF.
2026-07-13 20:15:14 +02:00
Julien Calixte
5f50cf1b45 chore: remove bar 2026-07-13 11:35:08 +02:00
Julien Calixte
5c97e019ec docs(kaizen): drop the after-kaizen bar until step 6 measures it
Step 1 is baseline + target only; the target line stays, the bar would
fake a measurement that doesn't exist yet.
2026-07-13 11:07:25 +02:00
Julien Calixte
7712de6153 docs(kaizen): size the xychart so its labels stop clipping
Explicit 900x500 canvas, tighter x-axis labels, unit-only y-axis title
(the long vertical title collided with the tick numbers in Remanso).
2026-07-13 11:06:44 +02:00
Julien Calixte
1df25f75d6 docs(kaizen): chart the improvement potential as a mermaid xychart
Before/after bars with the target as a line series; the caption keeps
the nuance the chart can't draw (true before value is ∞, not 611 s).
2026-07-13 11:03:43 +02:00
Julien Calixte
acbafa3d6b docs(kaizen): retell the real-repo sync fix as a six-step kaizen
The tradeoff-curves doc stays the chronological data layer; the kaizen
is the story layer — value model, 8-factor hypothesis table, ideas
ranked by verified cause, and an evaluation left PENDING until the
end-to-end on-device :sync lands.
2026-07-13 10:56:05 +02:00
Julien Calixte
e801d2d480 docs(palette): note the two on-device boot-log measurements pending
Walk time after the d_type fix, and the file-list internal-DRAM cost
with the decision rule for interning paths into one PSRAM buffer.
2026-07-13 10:26:25 +02:00
Julien Calixte
a771c3f6de chore(diag): log internal DRAM next to PSRAM-dominated heap totals
"free heap 5.8MB" in the crash log was PSRAM and masked the actual
internal-RAM exhaustion. git_sync's publish/commit/push lines now
carry the MALLOC_CAP_INTERNAL reading, and boot brackets the palette
file-list build with it — small Strings are forced internal by the
16 KB SPIRAM malloc threshold, so the 1098-path list is a suspected
~60-70 KB DRAM consumer competing with Wi-Fi/TLS; the log decides
whether interning into one PSRAM buffer is worth it.
2026-07-13 10:25:25 +02:00
Julien Calixte
4c92b0dddc fix(sync): move mbedTLS allocations to PSRAM
esp-idf's default is internal-RAM-only, and a TLS connection needs
~33 KB of it (two ~17 KB I/O buffers + contexts) at the exact moment
:sync pushes — with Wi-Fi, USB host, the editor and libgit2 resident,
mbedtls_ssl_setup failed there on the first real-repo push (and its
failure path then hit the stream double-free fixed in the previous
commit). TLS buffers are CPU-only data, so PSRAM is safe; the
handshake is network-bound.
2026-07-13 10:25:25 +02:00
Julien Calixte
af5b41a104 fix(sync): patch the mbedtls stream double-free that reset the chip
Upstream libgit2 v1.9.4 bug: mbedtls_stream_wrap's ssl_setup error
path closes and frees the caller's socket stream, then
git_mbedtls_stream_new frees it again — and wrap's other error paths
don't free it at all, so callers can't compensate either way. When
ssl_setup failed on the device (internal-RAM exhaustion during the
first real-repo push), the double git__free tripped tlsf ("block
already marked as free") and reset the chip mid-:sync.

Fixed the way esp_map.c replaces map.c: the vendored streams/mbedtls.c
is excluded from the component and esp_mbedtls_stream.c (verbatim copy
+ one-hunk fix: out_err leaves st->io to the caller, and frees the
leaked ssl struct) takes its place. Keep the copy in lockstep on
submodule bumps; worth reporting upstream.
2026-07-13 10:25:12 +02:00
Julien Calixte
2660a3e9dd perf(palette): trust dirent d_type instead of a per-entry stat
The per-entry metadata() call makes FatFS re-walk the directory by
path every time — ~32ms/file on the card, 35s for a 1098-file tree.
esp-idf's FAT VFS always fills d_type (DT_DIR/DT_REG from the FILINFO
readdir already holds, never unknown) and Rust std maps file_type()
onto it stat-free, so the walk is now one readdir pass per directory.
2026-07-13 01:05:10 +02:00
Julien Calixte
79fad4689c docs(palette): amend v0.5 with the recursive walk and search threshold 2026-07-13 00:55:34 +02:00
Julien Calixte
98fc817b3f feat(palette): show recents only until the query reaches two chars
With the file list now a recursive walk of the whole card, an unranked
short query pages through too much. Below PALETTE_MIN_QUERY (2) the
result list is the MRU only, keeping Cmd-P + Enter quick-switch one
keystroke away; two chars reveal the full fuzzy-ranked list. Commands
(>) and snippets ($) are short curated lists and keep matching from
the first char.
2026-07-13 00:55:25 +02:00
Julien Calixte
d306caacf7 feat(palette): walk the card recursively for the file list
A nested repo (jcalixte/notes) showed only its top-level files in the
palette. Dot entries are skipped at every level so .git is never
descended; each directory is read fully before recursing so a single
FatFS dir handle is open at a time; depth capped at 8. The boot walk
logs its file count and duration to keep the FAT dir-IO cost visible.
2026-07-13 00:55:16 +02:00
Julien Calixte
2166b932b6 docs(sync): update the wiring section to how-it-landed
The merged plan section now records the shipped design: single path
set (working tree as truth) instead of {changed, deleted}, the dirty
journal, stranded-commit recovery, radio-free up-to-date, and the
soft-reset reconcile with its carried-files side win.
2026-07-13 00:53:50 +02:00
Julien Calixte
02b7ed88b6 fix(provisioning): rewrite the card origin to HTTPS at load
The device's libgit2 has no SSH transport (HTTPS+PAT over mbedTLS
only), but _load-repo copied the desktop clone's origin verbatim — an
SSH-shaped URL fails every on-device push/fetch with "unsupported URL
protocol" (first real-repo :sync, 2026-07-13). Derive the HTTPS
equivalent for the card copy (git@host:path and ssh://git@host/path);
the source clone keeps its own URL. Warns on non-github hosts, since
the embedded trust store carries GitHub roots only.
2026-07-13 00:53:50 +02:00
Julien Calixte
a5edaed810 feat(sync): commit by splicing journaled dirty paths onto HEAD
The index pipeline (add_all → index.write → write_tree) is O(N_tree)
and cannot commit the real 1179-file / 570 MB-pack clone (index.write
measured up to 611 s on FAT's racy-clean re-hash). stage_and_commit is
now an O(depth) TreeBuilder splice of exactly the paths the editor
saved or deleted — ~2-2.8 s on the real clone — with the working tree
as source of truth (existing file → insert, missing → remove).

Storage records those repo-relative paths on every save/delete and
journals them to /sd/.typoena-dirty (atomic, only on growth), so a
power pull can't strand a saved-but-unpublished note now that nothing
walks the tree. take_dirty → publish_succeeded/publish_failed settles
each publish's snapshot from the UI outcome handler.

Also required by / discovered with the splice:
- mwindow opts at git-service start (32-bit defaults would OOM PSRAM
  on the first real-pack access; bench-proven 256 KB / 4 MB).
- 16-FD mount for git builds (libgit2 holds pack+idx descriptors open;
  the editor's 4-FD budget overruns).
- reconcile is a soft reset (no index to reset anymore); side win: a
  remote-only added file is carried by the replay instead of dropped.
- stranded-commit recovery: tree-unchanged now pushes anyway when
  origin/<branch> lacks HEAD (a commit whose push failed used to be
  silently never retried).
- radio-free up-to-date: empty dirty set + origin at HEAD answers
  without bringing Wi-Fi up.
- try_push splits ref rejection (reconcilable) from transport failure
  (surfaced directly) — the first on-device run burned a doomed
  reconcile on "unsupported URL protocol" and hid the cause.

Deliberate behavior change: files changed on the card outside the
editor are never committed anymore (also retires the macOS-cruft
filter). Trail: docs/tradeoff-curves/sync-commit-staging.md.
2026-07-13 00:53:39 +02:00
Julien Calixte
e86a3b8254 docs(sync): merge the commit handoff into the staging tradeoff curve
The bench phase the handoff was written for is closed (splice benched,
fast-seek landed, cache removed, decision made), so the note's live half —
the firmware plumbing plan — moves into sync-commit-staging.md as "The fix —
wiring the O(depth) splice into the firmware", reconciled to the run-5 state.
The duplicated TL;DR/measurement summaries are dropped in favor of the
curve's own trail; inbound references (notes index, git_bench comments)
now point at the curve.
2026-07-13 00:29:07 +02:00
Julien Calixte
9309f3f239 docs(sync): record the esp_map v2 verdict and cache removal
Run 4: memory discipline verified (1833 KB flat, no OOM) but 0 hits
with the small maps demonstrably retained — window-repetition theory
refuted, sub-second bar failed (splice 2.83 s cold / ~2 s steady).
Decision: wire the splice in anyway (~9-10 s cold real-repo :sync vs
611 s/OOM for every alternative). Run 5: cache removal confirmed
free; the ~1.85 MB "resident" was mwindow's live window set, which
makes the mwindow opts in shipping git_sync.rs load-bearing.
2026-07-13 00:20:12 +02:00
Julien Calixte
1a5e1736f5 refactor(sync): remove the never-hit mmap window cache
Four instrumented real-repo bench runs scored 0 cache hits: libgit2's
mwindow layer reuses its open windows, so only genuinely new
(offset, len) ranges ever reach p_mmap — there is no repetition to
cache. The 7.4 MB OOM the v2 discipline fixed was caused by the cache
itself holding buffers past p_munmap; plain free-at-munmap is honest
with MWINDOW_MAPPED_LIMIT by construction. Run 5 confirmed removal is
I/O-neutral on device (byte-identical read pattern, warm splice 1953
vs 1949 ms) and even 15 reads better — v2's low-water eviction was
fighting mwindow. Stats counters kept to spot any future workload
that does repeat ranges.
2026-07-13 00:20:02 +02:00
Julien Calixte
ce3204a350 docs(sync): record fast-seek root cause and splice bench trail
Splice bench (6.5 s, O(depth) shape confirmed), FatFS cluster-chain
root cause with elm-chan/ESP-IDF references, on-device fast-seek
verification, second localization round (strict-creation refuted,
read_header 470 ms), esp_map v2 design, and the two firmware plumbing
gaps (mwindow opts + 16-FD mount).
2026-07-13 00:02:55 +02:00
Julien Calixte
dd3d70cbec perf(sync): cache small pack windows and evict mmap cache on munmap
Admission keyed on file size (>= 1 MB) instead of map length: the hot
set is the small repeated maps (pack trailer, idx fanout, delta bases)
that the 64 KB floor excluded — 0 hits across three real-repo runs.
Small mutable working-tree files stay uncacheable. p_munmap now evicts
unreferenced entries to a 2 MB low-water mark so released windows are
actually returned to git__malloc (fixes the 7.4 MB resident / 508 KB
heap zlib OOM and keeps MWINDOW_MAPPED_LIMIT honest).
2026-07-13 00:02:54 +02:00
Julien Calixte
2c1c590c9e perf(sync): enable FatFS fast seek for pack reads
Without CONFIG_FATFS_USE_FASTSEEK every long/backward lseek in the
263 MB pack walks the FAT cluster chain over SPI (~190 ms), and libgit2
pays ~8 such seeks per loose-object write. The CLMT makes lseek O(1)
for read-mode files; verified on device: far seek 198.7 -> 20.4 ms,
splice commit 6.5 -> 2.8 s.
2026-07-13 00:02:42 +02:00
Julien Calixte
2c24ece3a5 feat(bench): add packfile long-seek op to sd_bench
Reads 4 KB at the start vs the end of the repo's largest pack (skipping
macOS ._ sidecars). Proved the ~1.5 s/loose-object cost was FatFS
walking the FAT cluster chain on every long/backward lseek: 5.8 ms at
offset 0 vs 198.7 ms at the end of the 263 MB pack.
2026-07-13 00:02:42 +02:00
Julien Calixte
da8ca7d8d2 feat(bench): bench the O(depth) TreeBuilder splice and odb probes
Adds the splice prototype (patch one path onto HEAD's tree, O(depth))
as git_bench's headline op, run first so its cold number survives the
index ops' OOM, plus odb.read_header/odb.exists probes and strict-off
re-benches that localized the residual cost and refuted the
strict-object-creation theory.
2026-07-13 00:02:29 +02:00
Julien Calixte
dade5e6bb3 docs(sync): record real-repo commit bench and TreeBuilder handoff
The real jcalixte/notes clone (570 MB pack, 1179 files) proved both index
strategies are O(N_tree) and unshippable: index.write re-hashes the tree
(611 s), and the index-free read_tree is 77 s cold and OOMs the mmap cache
(zlib crash). Record the run in the tradeoff curve, revise the verdict to an
O(depth) TreeBuilder walk, and add a handoff note with the design, firmware
call sites, and bench steps for the next session.
2026-07-12 15:04:59 +02:00
Julien Calixte
a6f52df8b6 perf(sync): bench the index-free commit path and mmap cache
Replace the index.write()/write_tree ops (which re-hash the whole working
tree on a fresh FAT clone -- up to 611 s on the real repo) with the
index-free candidate: Index::new + read_tree(HEAD) + blob + write_tree_to.
Add per-op mmap-cache + free-heap logging (via esp_map_stats), announce each
op before it runs so a hang reveals the culprit, mount_for_git for the FD
budget, tune the mwindow limits, and drop N 10 -> 3 for the slow real clone.
2026-07-12 15:04:52 +02:00
Julien Calixte
6c5e666f4b feat(persistence): add mount_for_git with a larger open-file budget
libgit2 keeps the pack, .idx and commit-graph descriptors open for the
repo's lifetime and opens loose objects on top, so a read_tree walk overruns
the editor's tight 4-FD budget with "no free file descriptors". Add
mount_for_git (16 FDs, matching the flash-FAT git binaries) alongside the
editor's mount; both route through a shared mount_with_max_files.
2026-07-12 15:04:44 +02:00
Julien Calixte
c5e119f6be perf(git): cache emulated pack mmap to avoid re-reading the pack
p_mmap emulated a mapping by malloc+read()ing the range from SD on every
call. libgit2 re-hits the pack idx/windows on every object write (via
git_odb__freshen -> git_odb_refresh), so a commit re-read pack bytes over
SPI repeatedly -- ~500ms-1.1s/op on a small repo, far worse on the real
570 MB pack.

Cache read-only mappings >= 64 KB (pack idx/windows, commit-graph, midx,
packed-refs -- all immutable on this device) in a refcounted, PSRAM-backed
slot table keyed by (dev, ino, size, mtime, offset, len), LRU-evicted under
a soft cap. Small mutable working-tree maps (diff_file.c) fall below the
floor and are never served stale.
2026-07-12 15:04:37 +02:00
Julien Calixte
456c4c43e7 perf(sync): instrument and benchmark commit-staging latency
Break the stage+commit window into sub-phases (FAT working-tree walk vs
object writes) via `commit split —` log lines, and add two micro-benchmarks
(sd_bench for SD/FAT primitive ops, git_bench for libgit2 object overhead)
with justfile recipes. Documents the walk-vs-writes cost model in
tradeoff-curves/sync-commit-staging.md to decide whether explicit-path
staging over the editor's dirty set is worth replacing add_all(["*"]).
2026-07-12 12:24:50 +02:00
Julien Calixte
beb11eda5e chore(firmware): skip legacy-I2C conflict check
esp-idf-hal always compiles its i2c.rs, which binds the old driver/i2c.h
API, so ESP-IDF links the legacy driver's constructor into every image and
prints its deprecation warning at boot. Typoena uses no I2C (EPD/SD are SPI,
keyboard is USB), so the warning is pure noise; this flag compiles the
constructor (and its conflict-abort check) out.
2026-07-12 12:23:10 +02:00