perf(app): only hit the hub on change, fetch boards on view, back off on failure - #3
Conversation
… on failure The menu bar app submitted and fetched every board every ~5 minutes and, on failure, retried every ~60 seconds. On Cloudflare's free tier the binding KV limits are writes and list operations (1,000/day each), and that cadence blew past them with only a couple of always-on machines. Now a background refresh pushes a submit only when today's or yesterday's UTC totals changed (SubmitSignature) and at most once every 10 minutes, so an idle machine makes no hub writes. Boards are fetched only on a forced refresh (Pits tab, create/join/rename, manual refresh, launch), since the menu bar shows only local numbers. After a hub failure the app backs off (2 min, doubling to 30) instead of retrying every minute. The decisions live in BrrrnCore as pure value types (SubmitSignature, PitSync) and are unit tested there; AppModel.syncPits is thin glue. All new state is in memory; nothing new is written to disk.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
brrrn | a6c1481 | Jul 17 2026, 08:53 PM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8c5bd21092
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| let signature = report?.daily.map { SubmitSignature.of(daily: $0, now: now) } | ||
| let doSubmit = forcePit || PitSync.submitDue(pitSync, signature: signature, now: now) | ||
| let doBoards = forcePit |
There was a problem hiding this comment.
Separate startup submission from board fetching
On every launch, start() calls refresh(forcePit: true) at AppModel.swift:101, so setting doBoards from forcePit fetches every board even when the persisted tab is Me and the user never opens Pits. This defeats the new on-view fetching policy and consumes hub and Durable Object reads for idle launches; the initial submission should not automatically imply a board pull.
Useful? React with 👍 / 👎.
| .filter { window.contains($0.date) } | ||
| .sorted { $0.date < $1.date } | ||
| .map { "\($0.date):\($0.tokens):\(money($0.costUSD))" } | ||
| .joined(separator: "|") |
There was a problem hiding this comment.
Fingerprint the full submit payload
When a day's model attribution changes without changing its total tokens or six-decimal total cost, such as a correction between identically priced models, this signature remains unchanged even though build_submit_days also sends per-model input tokens, output tokens, and costs in src/social.rs:318-367. Background sync then skips the updated record, leaving models_week and top_model stale for other members until this user forces a refresh or produces another aggregate change.
Useful? React with 👍 / 👎.
| let signature = report?.daily.map { SubmitSignature.of(daily: $0, now: now) } | ||
| let doSubmit = forcePit || PitSync.submitDue(pitSync, signature: signature, now: now) |
There was a problem hiding this comment.
Preserve forced syncs while another refresh is running
If createPit or joinPit finishes while another refresh holds isRefreshing, its refresh(forcePit: true) is discarded by the guard at AppModel.swift:125. Because subsequent background decisions depend only on the unchanged burn signature here, they do not notice the newly added pit, so it can remain unbackfilled and absent from boards indefinitely until another explicit refresh; queue the forced request or invalidate the sync state when pit configuration changes.
Useful? React with 👍 / 👎.
Two issues from the Codex audit of the cadence change: - The submit signature is built from daily totals, so a change confined to the model breakdown or the Claude/Codex split (same total) was invisible and could age out unsent. Add a periodic safety flush (re-push the current window at least every 6h) so such a change is bounded to hours, never lost. - A forced refresh that arrived while a background scan was running was dropped by the isRefreshing guard, which now matters because the background loop no longer fetches boards. Coalesce it: remember the request and run it when the current pass finishes, so opening the Pits tab never leaves a stale board.
Why
v0.1.3 fixed the hub read amplification. The other half of the KV pressure is
the client: the menu bar app submitted and fetched every board every ~5
minutes regardless of whether anything changed, and on failure it retried
every ~60 seconds. On the free tier the binding limits are writes and list
operations (1,000/day each), and that cadence blew past them with only a
couple of always-on users.
What changed (app only)
brrrn submitto the hub only when today's or yesterday's UTC totals differfrom what was last pushed (
SubmitSignature), and at most once every 10minutes. An idle machine makes zero hub writes.
tab, create/join/rename, or hit refresh, not on a background timer. The menu
bar's own number is local and updates every 60s as before.
up to 30, instead of retrying every minute, so an outage or a hit rate limit
no longer becomes a request storm. A forced (user-initiated) refresh ignores
the backoff.
All new state (
PitSyncState) is in memory onAppModel; nothing new iswritten to disk.
Design
The decisions live in
BrrrnCoreas pure value types (SubmitSignature,PitSync) so they are unit tested without the app, the engine, or the network,matching the repo's "view logic in BrrrnCore" rule.
AppModel.syncPitsis thinglue over them.
Effect on free-tier headroom
Writes were the wall. Before, an always-on machine wrote ~576/day (2 records
every 5 minutes); with this change an idle machine writes ~0 and an actively
burning one writes ~96/day (2 records at most every 10 minutes). Board list
operations drop from a fixed ~288/day/machine to a handful per day, driven by
how often someone actually opens the leaderboard. Together with v0.1.3 that
moves a small friend group from "over the free limits with 1-2 people" to
comfortably in range.
Tests
13 new unit tests in
BrrrnCorecover the signature (window, changedetection, out-of-window edits, day rollover) and the sync policy (submit
interval, first submit, exponential backoff and cap, success/failure
transitions). Full suite green: Rust, hub, Swift, app bundle build, and the
2 MB size budget. Audited with headless Codex.