Skip to content

perf(dashboard): aggregate stats in SQL with TTL cache and cost backfill - #70

Merged
jmlago merged 1 commit into
mainfrom
perf/dashboard-stats-sql
Jul 3, 2026
Merged

perf(dashboard): aggregate stats in SQL with TTL cache and cost backfill#70
jmlago merged 1 commit into
mainfrom
perf/dashboard-stats-sql

Conversation

@MuncleUscles

Copy link
Copy Markdown
Member

Problem

The analytics dashboard took 20+ seconds to load at current request volume. Three panels (stats, login connections, provider-keys) each loaded the entire 30-day retention window from calls into Python dicts and aggregated there — no LIMIT, no GROUP BY — with the full set folded ~4× per page (filtered + unfiltered aggregation, daily totals, per-snapshot), the price file re-read and regex-parsed on every fold, and the 15s frontend auto-refresh re-running all of it from scratch.

Fix

  • SQL aggregation: one GROUPING SETS scan (host_store.usage_aggregate) computes totals + by_caller/provider/model_family/route/served_model/status/day; recent activity is a LIMIT 200 page (usage_rows_page); login-connections and provider-keys panels get their own SQL rollups. All range-scan idx_calls_ts.
  • Cost backfill: idempotent, keyset-batched startup job stamps cost_usd (cost_basis='computed') on unpriced rows, so SUM(cost_usd) is authoritative. Provider-reported costs untouched. Historical costs now freeze at backfill-time prices instead of being re-priced per read.
  • 12s TTL cache on snapshots (keyed by full query tuple) — the 15s auto-refresh and concurrent viewers do near-zero DB work.
  • _price_table() memoized on file mtime.

Response shape unchanged — no frontend changes. timeframe=runtime, /v1/usage, and cost-accuracy paths untouched.

Semantics preserved (test-pinned)

error = status >= 400; NULL/'' bucket keys → unknown; tokens_total falls back to in+out; daily buckets are UTC calendar dates; rejects remain runtime-only (never persisted). One documented drift: a just-landed row still present in the runtime deque used to win latency dedup on the provider-keys panel; now the persisted row wins.

Numbers

At ~500k retained rows: cold page load ~18s → ~0.85s of query work; warm (within TTL) loads touch only a ≤200-id probe for unlanded runtime rows.

Tests

New tests/test_dashboard_stats_sql.py: exact parity between the SQL bundle and the old Python aggregation over 300 seeded rows × 6 filter combinations, provider/connections rollup parity, backfill idempotency + NULL scoping, TTL cache hit/miss, empty-DB shapes. Full suite: 470 passed, 2 skipped, 1 pre-existing unrelated failure (test_antseed_node.py Node harness).

GET /dashboard/api/stats was O(all retained rows) per page load: every
non-runtime timeframe loaded the whole calls window three times (stats,
logins, provider-keys), folded it in Python twice, re-priced unstamped
rows per row per read, and re-parsed the price file per call — 20+
seconds at production volume.

- host_store: usage_aggregate (one GROUPING SETS scan over idx_calls_ts
  for totals + by_caller/provider/model_family/route/served_model/
  status/day), usage_rows_page (LIMIT'd recent), usage_count,
  usage_provider_stats, usage_connections, usage_event_ids_present, and
  an idempotent batched backfill_call_costs that stamps
  cost_usd/cost_basis='computed' on unpriced rows (unpriceable rows stay
  NULL and sum as 0, as before).
- auth_proxy: the persistent-timeframe stats snapshot, provider
  credentials and login connections derive from the SQL rollups behind
  a 12s TTL cache keyed by the full query tuple (DASHBOARD_STATS_TTL_S);
  only not-yet-landed runtime rows are folded on top. _price_table() is
  memoized on file mtime. Cost backfill runs once at startup off the
  event loop. The dead double aggregation and _dedup_usage_rows are
  removed; the runtime path, /v1/usage and response shapes are
  unchanged.
- tests: parity suite asserting the SQL path reproduces the reference
  Python aggregation exactly (totals, every group-by, daily buckets,
  recent page, filters, empty shapes), plus TTL-cache and backfill
  idempotency coverage.

~7x faster cold at 500k rows (0.85s vs 5.9s per full-window fold, of
which the old path ran three per page load); repeat loads within the
TTL do zero window scans.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@MuncleUscles, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afe4bfd5-50a7-4735-817a-99927bc9252f

📥 Commits

Reviewing files that changed from the base of the PR and between 95562b6 and bb4f324.

📒 Files selected for processing (3)
  • auth_proxy.py
  • host_store.py
  • tests/test_dashboard_stats_sql.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/dashboard-stats-sql

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jmlago
jmlago merged commit 4e445e9 into main Jul 3, 2026
1 check passed
MuncleUscles added a commit that referenced this pull request Aug 6, 2026
The repo had no pre-merge checks. notify-ci.yml fires on push to main — after
the merge button — and only dispatches to the private CI repo; PR checks were
CodeRabbit alone, and branch protection required a review but zero status
checks. tests/test_antseed_node.py was consequently red for a month across
#63/#68/#69/#70/#71 and every one merged.

Three jobs:
- tests: pytest against a UTF8 Postgres service (SQL_ASCII makes psycopg return
  TEXT as bytes, which silently voids settings overrides), submodules checked
  out so the engine-backed tests are real, node present for the sidecar suite.
- core-tests: the Lua policy core's unit + golden conformance vectors.
- images: build both images, then smoke the artifact.

The image smoke is the one that would have caught #95. A green suite proves the
REPO is consistent and says nothing about what COPY put in the image — which is
exactly how a control.js requiring ./ids.js shipped without it, died at import,
and took every wallet endpoint down with a 502. scripts/check_sidecar_modules.js
resolves (never executes) each shipped module's local imports inside the built
image; verified to reproduce that failure against the old COPY list.
jmlago pushed a commit that referenced this pull request Aug 7, 2026
…#96)

* ci: gate merges on tests, core conformance and a built-image smoke

The repo had no pre-merge checks. notify-ci.yml fires on push to main — after
the merge button — and only dispatches to the private CI repo; PR checks were
CodeRabbit alone, and branch protection required a review but zero status
checks. tests/test_antseed_node.py was consequently red for a month across
#63/#68/#69/#70/#71 and every one merged.

Three jobs:
- tests: pytest against a UTF8 Postgres service (SQL_ASCII makes psycopg return
  TEXT as bytes, which silently voids settings overrides), submodules checked
  out so the engine-backed tests are real, node present for the sidecar suite.
- core-tests: the Lua policy core's unit + golden conformance vectors.
- images: build both images, then smoke the artifact.

The image smoke is the one that would have caught #95. A green suite proves the
REPO is consistent and says nothing about what COPY put in the image — which is
exactly how a control.js requiring ./ids.js shipped without it, died at import,
and took every wallet endpoint down with a 502. scripts/check_sidecar_modules.js
resolves (never executes) each shipped module's local imports inside the built
image; verified to reproduce that failure against the old COPY list.

* ci: give the image smoke a database and fail fast on a dead container

The router opens a host-store pool at startup, so booting it without Postgres
proved only that it can fail to connect. Adds a postgres service to the images
job and runs the container with --network host so it can reach it (a bridged
container cannot see the runner's localhost).

Also drops the '|| true' after docker run: a container that fails to start must
fail the job immediately, not fall through to a curl loop that reports the same
thing sixty seconds later.
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.

2 participants