mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 21:27:11 +01:00
Some checks failed
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md / TODO*.md so a future session can tell a live task from a finished record without reading the file to find out. TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced by the md-naming pre-commit hook and CI workflow, both delegating to the shared gate in kuhyx/utils so this repo's idea of a valid name cannot drift from every other repo's. Includes the patch bump this repo's bump-patch-version hook applies to app/pubspec.yaml on every commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
99 lines
4.7 KiB
Markdown
99 lines
4.7 KiB
Markdown
# Sync latency: what must never block the user
|
|
|
|
Measured 2026-08-21 against the live remote. Before this work the gate ran a
|
|
*full* sync tick before deciding whether to lock: ~27s, and the lock screen's
|
|
"Fetch from sync" button ran the same tick inline on the Tk thread.
|
|
|
|
| Path | Before | After |
|
|
|---|---|---|
|
|
| `gate --check` (nothing due) | 216ms | ~105ms |
|
|
| "Fetch from sync" button | 18-27s | ~84ms |
|
|
| `diet-guard log ...` | 15.5s | ~2ms |
|
|
| Background full tick | ~27s | ~8.9s |
|
|
|
|
## The narrow pull
|
|
|
|
`_sync_refresh.pull_peer_logs()` answers one question — *"has a peer already
|
|
logged this slot?"* — and only the peer **logs** can answer it. No budget, no
|
|
food banks, no `rebuild_food_bank`, and **no push**.
|
|
|
|
Three state invariants, each with a regression test, because each is silent
|
|
when broken:
|
|
|
|
* **`pushed_rev` survives verbatim.** This pass never pushes; overwriting it
|
|
convinces the next full tick it has already published, and the device goes
|
|
invisible to every peer, permanently.
|
|
* **`peer_revs` is merged, not replaced.** Only changed peers are visited, so a
|
|
wholesale replace drops every untouched entry and re-downloads them next tick.
|
|
* **The log is written before the state.** The other order records a peer as
|
|
merged while its records are absent — silent data loss.
|
|
|
|
Peer enumeration deliberately avoids `list_directory` (~445ms mirrored, because
|
|
the GitHub half alone is ~360ms) and unions the revision map with `peer_revs`
|
|
instead. Membership is tested with `in`, never `.get(...) is not None`:
|
|
`peer_revs` legitimately holds explicit nulls, and treating those as "never
|
|
seen" re-downloads them forever.
|
|
|
|
## Threading
|
|
|
|
The fetch button runs the pull on a **daemon** worker and marshals the result
|
|
back through a `queue.Queue` polled by `root.after`. The worker touches **no
|
|
widget** — Tcl is not thread-safe, and that includes `after`. Polling also
|
|
makes the completion path testable with no thread at all: put a result on the
|
|
queue, call the poll, assert.
|
|
|
|
A second click while a fetch is in flight is ignored. That is the reentrancy
|
|
guard *and* the write-race fix: the pull persists its merge, so two workers
|
|
would race two `write_raw_log` calls and orphan one result.
|
|
|
|
`diet-guard log` publishes through `publish_after_log_detached` (non-daemon, so
|
|
the process waits for the push at exit while the user already has their
|
|
output). **The gate does not**: `diet-guard-gate.service` is `Type=oneshot`, so
|
|
systemd reaps the unit the moment the main thread exits and a background push
|
|
would be killed halfway.
|
|
|
|
## Timeouts
|
|
|
|
`INTERACTIVE_TIMEOUT_SECONDS` (2s) bounds anything a user waits on.
|
|
`SYNC_TIMEOUT_SECONDS` (10s) only ever reached GitHub: `firebase_client_for`
|
|
accepts no timeout in crdt_sync v0.6.0, so Firebase silently used its 15s
|
|
default and one hung session refresh stalled the lock window. The journal
|
|
recorded five such failures in five days, one of them a meal that logged
|
|
locally and never published.
|
|
|
|
`_apply_timeout` reaches for the private `_timeout_seconds` because the pinned
|
|
library exposes no other lever. The clean fix is a `timeout_seconds` kwarg
|
|
upstream; that is a cross-repo change (five consumers, tag bump).
|
|
|
|
## Stale peers
|
|
|
|
Every install mints a uuid and pushes under it, and nothing cleaned them up.
|
|
This remote reached 25 devices. Each dead one cost a round trip in the log pull
|
|
and in each of the three bank syncs — roughly 400ms per peer per tick.
|
|
|
|
They were not phone reinstalls: seventeen were frozen at the *same* instant
|
|
(2026-08-13T16:45:04) holding near-identical ~400-record copies of one log,
|
|
the signature of repeated fresh-install runs. 20 were pruned on 2026-08-21
|
|
after verifying zero records would be lost.
|
|
|
|
`prune-peers` is dry-run by default, refuses `--apply` without `--backup-dir`,
|
|
and never proposes `pc`/`phone`/`desktop` (they carry pre-migration history and
|
|
`SYNC_LEGACY_DEVICE_ID` names one as this device's own former path).
|
|
|
|
**It must delete each device's `revs/` marker along with its data.**
|
|
`_candidate_peers` enumerates peers from that map rather than from
|
|
`list_directory`, so an orphaned marker resurrects a deleted device as a
|
|
candidate and buys a round trip discovering it is gone. Pruning without this
|
|
took the narrow pull from ~90ms to ~15s — slower than before the prune.
|
|
|
|
## Import cost
|
|
|
|
`gate --check` used to import `requests` (~78ms) and tkinter for a window it
|
|
never opens. Both are now resolved through module-level `__getattr__`
|
|
(PEP 562) plus `import_module`, which keeps every existing `patch.object`
|
|
target working and needs no lint suppression. The chains were
|
|
`_cli_args` -> `_cli_sync` -> `crdt_sync` -> `requests`, and
|
|
`_cli_log` -> `_resolve` -> `_estimator_off` -> `requests`.
|
|
|
|
Verify with `python -X importtime -c "import diet_guard._cli_gate" | grep -c requests` (expect 0).
|