Files
OmniRoute/perf-audit-report.md
Paijo 3238df3204 perf: lazy provider init, P2C quota cache, structuredClone elimination, getSettings→getCachedSettings (batch 2) (#7893)
* perf: startup parallelization, stream TextEncoder lift, auth middleware bottlenecks

Startup (~100-300ms faster cold start):
- Parallelize 4 early imports via Promise.all() in registerNodejs()
- Parallelize 10 independent background services via Promise.allSettled()
- Each service has independent try/catch — no failure domino effect

Streaming pipeline (8 fewer TextEncoder GC allocations per SSE event):
- Lift new TextEncoder() from per-chunk inside buildClaudeStreamingResponse
  to function scope alongside existing decoder singleton

Auth middleware bottlenecks (from PerfBottleneckAnalysis):
- Backoff decay loop: replace updateProviderConnection (full CRUD:
  SELECT+encrypt+cache-invalidate+backup) with resetConnectionBackoff
  (targeted UPDATE of backoff/error columns only)
- Dual .filter() for quota: replace two passes calling
  isQuotaExhaustedForRequest per connection with a single for loop
  partitioning into withQuota/exhaustedQuota
- Debug-log filter recomputation: capture connectionFilterStatus Map
  during the filter pass; debug loop reads 6 string comparisons instead
  of 6 function calls per connection

Supporting:
- Add resetConnectionBackoff to src/lib/db/providers.ts (patterned after
  clearConnectionErrorIfUnchanged, no CAS check)
- Re-export resetConnectionBackoff from src/lib/localDb.ts
- Update integration-wiring.test.ts regex for parallelized dynamic import

* perf: P2C quota cache, lazy provider init, structuredClone elimination, getSettings→getCachedSettings

- **auth.ts: P2C quota re-evaluation cache** — quotaResults Map threaded
  through selectPoolSubset → compareP2CConnections → getP2CConnectionScore.
  Populated during filter + partition passes, eliminating redundant
  evaluateQuotaLimitPolicy / isQuotaExhaustedForRequest calls when the
  P2C comparator re-evaluates previously-scored connections.
- **constants.ts: lazy PROVIDERS via Proxy** — replaces eager
  generateLegacyProviders() + loadProviderCredentials() at module load
  with Proxy delegating to deferred init on first property access.
- **providerModels.ts: lazy PROVIDER_MODELS + PROVIDER_ID_TO_ALIAS** —
  same Proxy pattern for both exports; generateModels()/generateAliasMap()
  deferred until first read.
- **stream.ts: structuredClone → minimal object spread** — replaces
  O(n) deep clone of SSE response chunks with targeted reconstruction
  of only mutated fields (usage, delta.content, finish_reason).
- **progressTracker.ts: TextDecoder lift** — module-level decoder
  instead of per-chunk new TextDecoder().
- **Route files: getSettings() → getCachedSettings()** — 13 API route
  files converted from uncached per-request DB reads to TTL-cached
  wrapper (5s default), eliminating redundant queries on every request.
- **settings.ts: re-export getCachedSettings** from readCache for
  non-localDb consumers.
- **Remove settingsCache.ts** — dead file, no imports reference it.

TS compile: 0 errors. Auth tests: 225/225 pass. Services: 269/269 pass.

* perf: Phase 1 tangible wins — egressCache eviction, mmap_size PRAGMA, composite indexes, proxyFallback lazy import

- egressCache: lazy TTL eviction on getCachedEgressIp access (bounds memory
  leak to distinct proxy URLs, typically <100)
- mmap_size: apply stored PRAGMA from key_value table (256MiB default) after
  applyStoredDatabaseOptimizationSettings — setting was stored but never applied
- schemaColumns: add idx_uh_provider_model_timestamp (covers getModelLatencyStats)
  and idx_pc_provider_auth_type (covers 6+ provider_connections queries)
- proxyFallback: convert static import to dynamic import() inside error handler
  (defers 210ms module load from startup to first proxy-retry scenario)

* perf: add dedup expression index, unref() sweep timers

- Add COALESCE expression index idx_uh_dedup on usage_history
  matching the exact dedup query pattern. Eliminates FULL TABLE
  SCAN on every saveRequestUsage insert.
- Add composite idx_uh_provider_model_timestamp on usage_history.
- Add composite idx_pc_provider_auth_type on provider_connections.
- Add .unref() to setInterval in batchProcessor.ts (polling loop).
- Add .unref() to setInterval in runtimeHeartbeat.ts (heartbeat).

* perf: bump SQLite cache_size default from 16MB to 64MB

New installs now start with 64MB page cache (was 16MB). Existing
users' stored settings are unchanged. Reduces disk reads for the
typical ~250MB database by keeping ~25% of pages in memory.

Also resolved pre-existing merge conflict in webhooks.ts.

* docs: add Redis production config guide and proxy port clash investigation report

- docs/redis-production-config.md: comprehensive Redis tuning guide
  covering client options, server config, Docker settings, scaling,
  and monitoring for all three Redis workloads (rate limiting,
  auth cache, quota store)
- docs/proxy-port-clash-report.md: investigation confirming proxy
  subsystem has no port binding issues; real EADDRINUSE history
  traced to process supervisor crash-loop restart race (#4425) and
  live-dashboard port clash (#6324), both already fixed

* fix: address PR #7893 review — add Proxy traps, extract migrations to reduce providers.ts size

- Add set trap to PROVIDER_ID_TO_ALIAS Proxy (providerModels.ts)
- Add deleteProperty traps to all three lazy Proxies (PROVIDERS,
  PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS)
- Extract autoMigrateLegacyEncryptedConnections and getGheCopilotHosts
  from providers.ts (1129→1036 lines, -93) into providers/migrations.ts
- Both functions re-exported via providers.ts for backward compat

File-size ratchet resolved: src/lib/db/providers.ts now 1036 lines.

* fix: resolve merge conflict markers in 3 route/test files

- model-combo-mappings/route.ts: kept upstream version (Zod pagination
  via validateBody + isValidationFailure), restored missing return
  statement for GET handler
- playground/presets/route.ts: kept stashed version details (satisfies
  type-narrowing + inlined Response) — functionally identical
- error-sanitization.test.ts: matches upstream exactly (no diff)

Test verification: same 7 pre-existing failures confirmed on upstream
baseline (c1bdd91e7). Zero regressions from conflict resolution.

Closes remaining uncommitted work from PR #7046 rebase.

* test(db): add resetConnectionBackoff coverage + fix file-size ratchet regression

- Add tests/unit/reset-connection-backoff.test.ts: covers the new
  resetConnectionBackoff lightweight-UPDATE helper (clears backoff/error
  columns and re-activates a connection, unconditional-write behavior on
  terminal statuses, no-op for empty/unknown ids). Zero prior coverage
  per pre-merge review of PR #7893 (Hard Rule #8).
- open-sse/services/batchProcessor.ts: fold the new unref() call into the
  existing setInterval(...).unref() chain instead of a separate statement,
  keeping the file at the frozen 915-line ratchet (Timeout.unref() already
  returns `this`, so no type cast is needed).
- src/lib/localDb.ts: drop one blank separator line so the new
  resetConnectionBackoff re-export stays within the frozen 808-line ratchet.

Pre-merge fix for PR #7893 (perf/startup-stream-auth-optimizations) per
/green-prs plan-file _tasks/pipeline/prs/1-analyzed/7893-*.plan.md.

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
2026-07-21 11:50:14 -03:00

90 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OmniRoute Performance Audit — Phase 3 Report
## Measured data
| Metric | Value |
|--------|-------|
| Cold-start open-sse module load | **2,317ms** (first import) |
| proxyFallback.ts import cost | **210ms** (SQLite init + undici re-import) |
| proxyDispatcher.ts import cost | **69ms** |
| Handlers/streaming code | **686ms** |
| Services (token refresh, etc.) | **172ms** |
| Provider registry (211 files, 1.7MB) | **<5ms** (per-file lazy) |
| Provider constants lazy Proxy | **0.24ms** (first access) |
| Provider models lazy Proxy | **0.17ms** (first access) |
| Static provider imports (eager) | **~201 files** (module eval, ~200500ms I/O) |
| Executor singletons at module level | **42** |
| Module-level `setInterval` timers | **24** (many NOT `unref()`-ed) |
| Polyfill/global-patch operations | **5+** |
| DB size | 1.4GB+, usage_history 250K+ rows |
| SQLite cache_size | 16MB (conservative) |
| mmap_size in settings | 256MB (never applied as PRAGMA — **now fixed**) |
| Per-chunk transform layers | 25 `pipeThrough()` calls |
| Chunk transform GC pressure | Moderate (structuredClone removed, TextDecoder lifted) |
| Upstream HTTP | undici 3tier dispatcher (wellpooled) |
| Sync DB writes post-streaming | #1 bottleneck: saveRequestUsage + saveCallLog block event loop |
## Ranked findings (effort × impact)
### Implemented in this PR
| # | Finding | Impact | Effort | Fix |
|---|---------|--------|--------|-----|
| 1 | 🔴 **Proxy fallback loaded eagerly at startup** | **210ms** on first import | Low | Dynamic `import()` in proxyFetch.ts error handler |
| 2 | 🔴 **egressCache memory leak** (never evicts) | HIGH — unbounded growth | Very Low | Lazy TTL cleanup on `getCachedEgressIp` |
| 3 | 🔴 **Missing composite index: usage_history(provider, model, timestamp)** | HIGH — full scan on `getModelLatencyStats` | Very Low | `CREATE INDEX IF NOT EXISTS …` in schemaColumns.ts |
| 4 | 🔴 **Missing composite index: provider_connections(provider, auth_type)** | HIGH — full scan on 6+ queries | Very Low | `CREATE INDEX IF NOT EXISTS …` in schemaColumns.ts |
| 5 | 🔴 **mmap_size PRAGMA never applied** | HIGH — 256MB setting stored but unused | Very Low | PRAGMA applied after `applyStoredDatabaseOptimizationSettings` |
### Already in PR #7893 (pre-Phase 1)
| # | Finding | Impact | Effort |
|---|---------|--------|--------|
| 6 | 🔴 **Startup serialization** | 500+ms serial blocking (early imports + background services) | Low → wrapped in Promise.all / Promise.allSettled |
| 7 | 🟡 **Per-chunk structuredClone in createSSEStream** | GC pressure on every chunk | Low → replaced with minimal object spread |
| 8 | 🟡 **Per-chunk `new TextDecoder()` in progressTracker** | Minor GC churn | Very Low → module-level const |
| 9 | 🟡 **P2C quota re-evaluated per comparison (exponential blowup)** | N² work on each pool filter | Medium → Map cache threaded through pipeline |
| 10 | 🟡 **Dual `.filter()` passes in selectPoolSubset** | Double iteration on active set | Very Low → single `for` loop |
| 11 | 🟢 **Debug-loop re-filters 6 function calls** | No-op in production | Very Low → Map-based string comparisons |
| 12 | 🟢 **Backoff decay loop uses full CRUD update** | SELECT+encrypt+invalidate per unused connection | Low → targeted `resetConnectionBackoff` |
| 13 | 🟢 **Lazy PROVIDERS/PROVIDER_MODELS** | Startup saving per lazy Proxy 0.2ms | Low → Proxy on constants.ts + providerModels.ts |
| 14 | 🟢 **TextEncoder lift (claude-web.ts)** | Eliminates per-chunk instances | Low → module-level encoder |
| 15 | 🟢 **13 route files `getSettings()` → `getCachedSettings()`** | Avoids redundant decrypts | Low → import swap |
| 16 | 🟢 **settingsCache.ts dead file deletion** | Cleanup | Very Low → removed |
### Future opportunities (not yet implemented)
| # | Finding | Impact | Effort | Priority |
|---|---------|--------|--------|----------|
| 17 | 🔴 **`saveRequestUsage` dedup guard uses COALESCE on indexed columns** | FULL TABLE SCAN on every request completion | Medium | **NEXT** |
| 18 | 🔴 **24 module-level `setInterval` timers (many NOT `unref()`-ed)** | Prevent process exit + 2μs/call overhead | Low | Soon |
| 19 | 🔴 **`providerFallback.ts` (2nd path via proxyAutoSelector→transport→validation)** | 210ms but already lazy (route handlers only) | Low | Bonded |
| 20 | 🟡 **Sync DB writes block event loop after every stream** | saveRequestUsage + saveCallLog serialize through single-writer lock | High | Candidate for worker_thread |
| 21 | 🟡 **DB cache_size conservate (16MB)** | For 1.4GB DB, increases page reads | Very Low | PRAGMA change |
| 22 | 🟡 **Enable Redis for auth cache + quota store** | Offloads SQLite read/write pressure | Low | Config change + doc |
| 23 | 🟡 **DashboardLayout is `"use client"` with 7+ heavy children** | Entire dashboard forced to client render | High | Structural layout split |
| 24 | 🟢 **mermaid (84MB unused in src/) in dependencies** | Install bloat, not server-side cost | Very Low | Move to devDeps |
| 25 | 🟢 **3 duplicated deps in root + open-sse** | Redundant install | Very Low | Deduplicate |
| 26 | 🟡 **`SELECT *` unbounded in `getUsageHistory` (admin API)** | Risks scan of 250K+ rows | Low | Add LIMIT |
| 27 | 🟢 **Sync `readFileSync` at module eval in config loading** | Blocks event-loop-startup once | Very Low | Could defer |
| 28 | 🟡 **SetInterval timers: confirm all `unref()`-ed for remaining** | ~12 without `unref()` prevent clean exit | Low | Audit + fix |
## Status summary
| Category | Status |
|----------|--------|
| PR #7893 (original 16 optimizations) | **OPEN** — all core changes verified |
| Phase 1 tangible wins (5 items) | **Implemented** — uncommitted |
| Phase 2 EventLoopHealth | **Completed** — hot path is clean, timers need `unref()` |
| Phase 2 RequestTrace | **Not completed** (agent lost on session boundary) |
| Phase 2 TransitiveDeps | **Not completed** (agent lost on session boundary) |
| Phase 3 Report | **This document** |
## Recommended next actions
1. **Commit Phase 1 wins** (egressCache, mmap_size, indexes, proxyFallback lazy) → push to PR #7893
2. **Complete #17** — fix `COALESCE` defeating index in `saveRequestUsage` dedup guard
3. **Complete #18** — add `unref()` to all 24 module-level `setInterval` timers
4. **Complete #21** — bump `cache_size` PRAGMA to 64-128MB
5. **Document Redis configuration** for auth cache + quota store offload