8.0 KiB
8.0 KiB
Performance Baseline — Phase 02 Quickwins (Task #36)
This document tracks the expected and observed effect of the 8 quickwin
optimizations shipped under task #36
(docs/improvement-plan/02-observability/performance-quickwins.md).
Status
- Code-level changes: shipped to
main2026-05-17 (8 atomic commits + this baseline doc). /metricsendpoint status: code shipped for API and worker runtimes. The API exposes token-protectedGET /metricswhenMETRICS_ENABLED=true; worker admin servers expose token-protected/metricswhen enabled and keep health probes on a separate port.- Live
/metricsbaseline: operator-owned. The endpoint code exists, but the live before/after numbers still require an environment withMETRICS_ENABLED=true,METRICS_TOKEN, scrape access and a controlled load run.
Per-item expected gains (code-level reasoning)
| Item | File | Mechanism | Expected gain |
|---|---|---|---|
| 1. N+1 in admin users list | apps/web/app/routes/admin/accounts/users.tsx + apps/web/app/utils/db/logs.db.server.ts |
Replace N findFirst calls with 1 groupBy + 1 findMany |
DB queries on the admin users-list loader: O(N) → O(1) (101→2 for a 100-user page). Largest practical win on this route. |
| 2. Duplicate tenant fetch | apps/web/app/utils/session.server.ts + apps/web/app/routes/admin/accounts/users.tsx + apps/web/app/routes/oauth/github/callback.tsx |
setLoggedUser now returns defaultTenant; callers reuse it instead of fetching again |
One fewer getTenant() round-trip per sign-in / impersonation start (≈ 1 DB query/login). |
| 3. S3 client + signer caching | apps/api/src/storage.ts |
Memoize S3 storage adapter, signed-URL adapter and per-day SigV4 signing key by SHA256-of-config | Eliminates per-call adapter construction + HMAC chain (4 HMAC-SHA256 ops) on every signed URL. Per-request CPU win in hot upload/download paths. |
| 4. clearSubscriptionsCache thrash | apps/web/app/utils/services/.server/subscriptionService.ts + apps/web/app/utils/db/subscriptions/tenantSubscriptionProducts.db.server.ts |
New clearTenantSubscriptionCache(tenantId) replaces a global wipe in cancelTenantSubscriptionProduct |
Subscription-change webhooks no longer evict every tenant's cached subscription. Tenant N=1000 → only 1 eviction per event instead of 1000. |
| 5. CSRF header-first check | apps/web/app/utils/session.server.ts |
Already-resolved in commit 83a57f1c; regression test added |
No additional perf change in #36; documents the historical fix and now-enforced contract: 0 body-clones when the header carries the token. |
| 6. Recovery-code rejection sampling | apps/web/app/utils/auth/mfa/recovery-codes.server.ts |
Drop while(set.size < n) loop; generate batch + max 2 retries |
Generates 10 codes in one batch (≥99.999% of calls); throws on systemic failure instead of looping forever. Bounds wall-clock generation cost. |
| 7. UUID collision loop | apps/api/src/auth/session/session-store.ts |
while (sessions.has(sessionId)) → single insert; throw on collision |
1 ID generation per createSession instead of unbounded retry. UUID-v4 has 122 bits of entropy, so a collision is a systemic error to surface, not mask. |
| 8. CSRF token size | apps/web/app/utils/session.server.ts |
randomBytes(100).toString("base64") → randomBytes(32).toString("hex") |
Token shrinks ~136 chars → 64 chars. Saves ~70 bytes/cookie and ~70 bytes/response per CSRF-bearing endpoint. Aggregate response-body reduction across the cluster. |
Test coverage added
apps/web/app/utils/db/logs.db.server.test.ts—getLastUserLogsByUserIdsuses exactly 1groupBy+ 1findMany.apps/web/app/utils/session.server.test.ts—setLoggedUserreturnsdefaultTenant;validateCSRFTokennever clones the request body when the header carries the token;generateCSRFTokenemits 64 hex chars.apps/api/src/storage.test.ts— repeated config hits the adapter cache; bucket change misses; secret rotation misses.apps/web/app/utils/services/.server/subscriptionService.test.ts—clearTenantSubscriptionCachedoes not enumerate other tenants;clearAllSubscriptionsCacheinvalidates all tenant entries.apps/web/app/utils/auth/mfa/recovery-codes.server.test.ts— generation completes under a strict wall-clock bound (regression guard against unbounded rejection sampling).apps/api/src/auth/session/session-store.test.ts— UUID collision now throws instead of silently retrying.
Live baseline (operator-owned)
Populate the table below by running oha -n 5000 -c 50 against staging
twice for a comparable before/after window and scraping API/worker
/metrics snapshots with Authorization: Bearer <METRICS_TOKEN>.
The code path is shipped; the live baseline values are operational
evidence and should be recorded only from the target environment.
| Metric | Before | After | Delta |
|---|---|---|---|
p50 latency, GET /admin/users |
TBD | TBD | TBD |
p95 latency, GET /admin/users |
TBD | TBD | TBD |
| Prisma query count / request (admin users list) | TBD | TBD | TBD |
| p95 latency, signed-URL endpoint | TBD | TBD | TBD |
| p95 latency, login flow | TBD | TBD | TBD |
| Cache evictions per Stripe webhook | TBD | TBD | TBD |
| CSRF cookie size (bytes) | ~136 hex equiv. | 64 | -53% |
Notes
- All eight items shipped as atomic commits; any single item can be reverted in isolation.
- Item 5 was already in tree from
83a57f1c fix: enforce csrf on settings mutations. It is recorded here for completeness and now carries an explicit no-body-read assertion. - Path corrections were necessary on items 2 and 6 — see the per-item
Progress Log entries in
docs/improvement-plan/02-observability/performance-quickwins.md.
Footer
- Date: 2026-05-17
- Owner: Phase-02 quickwins agent
- Live-test hardware: operator-owned (fill with the target environment used for the controlled load run)