Skip to content

perf(app): only hit the hub on change, fetch boards on view, back off on failure - #3

Merged
kevinsslin merged 2 commits into
mainfrom
perf/app-hub-cadence
Jul 17, 2026
Merged

perf(app): only hit the hub on change, fetch boards on view, back off on failure#3
kevinsslin merged 2 commits into
mainfrom
perf/app-hub-cadence

Conversation

@kevinsslin

Copy link
Copy Markdown
Owner

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)

  • Submit only when something changed. A background refresh pushes a
    brrrn submit to the hub only when today's or yesterday's UTC totals differ
    from what was last pushed (SubmitSignature), and at most once every 10
    minutes. An idle machine makes zero hub writes.
  • Fetch boards only when looking. Boards are pulled when you open the Pits
    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.
  • Back off on failure. After a hub error the app waits 2 minutes, doubling
    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 on AppModel; nothing new is
written to disk.

Design

The decisions live in BrrrnCore as 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.syncPits is thin
glue 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 BrrrnCore cover the signature (window, change
detection, 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.

… 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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
brrrn a6c1481 Jul 17 2026, 08:53 PM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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: "|")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +159 to +160
let signature = report?.daily.map { SubmitSignature.of(daily: $0, now: now) }
let doSubmit = forcePit || PitSync.submitDue(pitSync, signature: signature, now: now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@kevinsslin
kevinsslin merged commit 9c82fd4 into main Jul 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant