feat(perf): performance test harness, Server-Timing middleware, and baseline evidence - #753
feat(perf): performance test harness, Server-Timing middleware, and baseline evidence#753PaoloC68 wants to merge 29 commits into
Conversation
…ware, and EXPLAIN capture
…+ report generator
Review: feat(perf): performance test harness, Server-Timing middleware, baseline evidenceOverall a high-quality piece of infrastructure work. Carefully scoped path filter, ContextVar isolation, explicit isolation guards on the perf DB, and reasonable test coverage for new modules. The notes below are roughly in priority order. Higher-impact
Smaller correctness notes
Test coverage / nits
Documentation
Generated by Claude (claude-opus-4-7) on PR #753. |
Claude review — perf-infra (PR #753)Solid infrastructure PR — Server-Timing, deterministic seeding, cursor pagination, fragment endpoints, perf-test tier. Test coverage is good (cursor tampering, concurrent context isolation, path filtering). A few things worth addressing before flipping out of draft. Security1. Cursor HMAC key default is a hardcoded dev secret — ConfigFieldMeta(
"cursor_hmac_key", "CURSOR_HMAC_KEY", str, "luthien-perf-cursor-key-dev",
...
)The default 2. Likely XSS in <div class="session-card"
onclick="window.location='/conversation/live/{{ session.session_id }}'">Jinja autoescape converts 3. Inconsistent search semantics across backends — User-supplied Correctness / bugs4. Double serialization in result = await fetch_session_list(...)
with time_phase("serialize"):
result.model_dump() # discarded
return result # FastAPI serializes againYou're measuring the right thing but doing the work twice, which inflates 5. Misleading middleware-ordering comment — # Add ServerTimingMiddleware as the last (innermost) middleware
# so it captures actual handler latency
app.add_middleware(ServerTimingMiddleware)
6. UUID cast in cursor_id_param: str | _UUID = _UUID(cursor_event_id) if not db_pool.is_sqlite else cursor_event_idThis will 7. Unindexed full-text scan in the "Claude Code" quick filter AND session_id IN (SELECT DISTINCT session_id FROM conversation_events WHERE payload::text ILIKE '%claude-code%')At the 10k-session scale you're benchmarking this is going to be the slowest query in the entire UI. If the goal is "show sessions that used Claude Code", filter on 8. Alpine state hack in const newSessions = Array.from(container.querySelectorAll('.session-card')).length - this.sessions.length;
for(let i=0; i<newSessions; i++) this.sessions.push({});The comment already calls it a hack. The bigger issue is that the canonical session data now lives in DOM rather than reactive state — meaning Alpine reactivity around 9. Feature regression vs. previous list page Minor
Test coverageGood: cursor roundtrip / tampering / short-token, concurrent ContextVar isolation, Server-Timing path filter (positive + negative), fragment pagination + filter combinations. Gaps worth filling:
Happy to dig deeper on any of these if useful. |
- Eliminate double serialization in history and debug routes: return JSONResponse(content=result.model_dump(mode='json')) inside the time_phase block so serialize time is measured accurately and FastAPI does not re-serialize the model a second time - Fix misleading ServerTimingMiddleware comment in main.py: add_middleware stacks outermost-last, so the last call is the outermost layer (not innermost as the old comment claimed) - Add intent comment to seeding.py PRAGMA synchronous=OFF to prevent well-meaning future removal of an intentionally unsafe setting - Replace hardcoded /Users/test/ path in test_db.py with tmp_path fixture for portability across CI environments
Code Review — PR #753Solid foundation overall: isolated perf DB, deterministic seeding, contextvar‑based timing middleware with good concurrency tests, and broad coverage across page-load / transcript / throttled / SSE / API-contract / EXPLAIN scenarios. The middleware design and isolation gates are particularly well thought out. Below are the issues worth addressing — grouped by severity. Blocking / High-priority
Notable issues
Performance considerations
Security
Test coverage
Small nits
Overall this is a high‑quality scaffold. The seeding payload‑size discrepancy (#2) and the bundled |
Blocking: - Remove changelog.d/perf-fix.md — describes cursor pagination and UI rewrites that belong to the follow-up PR, not this perf-infra slice - Fix seeding payload sizes: _REQ_PAD and _RESP_PAD were 50/100 bytes, producing 484/378-byte payloads instead of the documented ~5KB/~20KB. Recalculated pad sizes (2368/20202) to hit exactly 5120/20480 bytes. Add test_payload_sizes to catch future regressions. - Add test_seeded_db_has_same_indexes_as_migrated_db: asserts that the drop-and-recreate index dance in _seed_sqlite leaves the same index set as a freshly-migrated DB, catching drift when migrations add new indexes. Notable: - Fix _discover_html_routes: DatabasePool is lazy (no connection opened at __init__), add comment to prevent future misreading as a leak - Fix cleanup_loop wrong-loop: replace cleanup_loop.run_until_complete() with asyncio.run() so db_pool.close() runs on a fresh loop, not one that may conflict with the uvicorn thread's loop - Promote _apply_sqlite_migrations to apply_sqlite_migrations (public); update all call sites across src/ and tests/ - Add TODO to Postgres dead code in drop_perf_db (untested path) Minor: - Fix sqlite:// -> sqlite:/// in run_perf.sh example commands - Note in time_phase docstring that phases are recorded even on exception - Use /api/debug/calls (real route) in test_path_filter_includes_debug
Review: perf-infra (PR #753)Reviewed core code changes (skipping evidence/log artifacts). Overall this is a well-structured infrastructure PR: isolation enforcement, deterministic seeding, and clear separation between baseline-only measurement and SLO assertion. Comments below in rough order of importance. Bugs / behavioral concerns
Performance / correctness on the gateway-side changes
Security / safety
Tests
Nits
SummarySolid baseline infrastructure. The biggest concerns are the relative |
Bugs:
- Fix relative EVIDENCE_DIR in all four perf test modules: anchor to
Path(__file__).resolve().parents[3] (repo root) so evidence files land
in the right place regardless of pytest invocation directory
- Restore response_model= on /api/debug/calls and /api/debug/calls/{id}:
FastAPI uses response_model for OpenAPI schema generation even when the
handler returns a JSONResponse directly, so the schema is preserved
without re-serialization. Also restore response_model= on history routes
for the same reason.
- Fix changelog.d/perf-baseline.md: pr: 752 -> 753
Notable:
- Rename test_first_turn_painted_500_turns ->
test_first_turn_painted_largest_sami_session (the session has 442 msgs,
not 500; the old name would drift further as fixtures evolve)
- Add 'Public API' note to apply_sqlite_migrations docstring explaining
the underscore removal was intentional, not accidental
- Parameterize hardcoded 'sqlite' in perf_report.py: add backend param to
generate_report() and _section_hardware(), and --backend CLI flag
Minor:
- Clarify N_RUNS=3 comment in test_throttled_network.py: CDP throttle
adds ~300 ms/run so 3 runs is enough for a stable median
Review: perf-baselineOverall this is a well-scoped infrastructure PR with strong isolation guards (dev-DB refusal), good determinism (deterministic seeding, snapshot tests), and thorough unit coverage. A few items worth attention before merge — one of them is a potential hot-path regression. Significant concerns1. In if not should_time:
return await call_next(request)does not bypass
Recommendation: implement as a pure ASGI middleware (a callable 2. History/debug routes now bypass FastAPI's In
3. Smaller issues
Nits
Test coverageCoverage looks good for the new code ( What I like
Happy to dig deeper on the middleware concern if useful — I think that one warrants either a measurement or a switch to a pure-ASGI implementation before this lands, given |
Significant: - Replace BaseHTTPMiddleware with pure ASGI middleware in ServerTimingMiddleware: wrap send instead of using call_next, which avoids Starlette's pipe-buffering wrapper that can materialize streaming responses and break ContextVar propagation. The /v1/messages SSE path now has zero per-request overhead from this middleware. - Switch model_dump(mode='json') to model_dump_json() in history and debug routes: avoids the intermediate Python dict and uses Pydantic's own JSON serializer (orjson-backed when available). Return Response(content=..., media_type='application/json') to preserve the fast path. - Add test_time_phase_outside_request_context_does_not_raise to cover the silent-discard branch (time_phase called with no ContextVar set). Notable: - Move _discover_html_routes() call from module-level to pytest_generate_tests hook so it runs at parametrize time rather than on every collection import - Replace drop_perf_db postgres asyncio.run() with NotImplementedError to prevent foot-gun when called from async contexts (postgres path untested) - Add async-context constraint note to migrate_perf_db docstring - Add dedicated-session note to perf_gateway_url os.environ mutation - Add comment to _REQ_PAD/_RESP_PAD pointing at test_payload_sizes invariant Minor: - Add tier-10000 disk warning in run_perf.sh (~7-8 GB footprint)
Review (Claude)Solid foundation for the perf workstream — good test coverage, ContextVar-based isolation in the middleware, and strong perf-DB isolation guards. Below are concrete things to address or consider. Inconsistency:
|
Blocking (merge criteria):
- Align get_call_diff with the other debug routes: was an oversight —
add time_phase('serialize') and model_dump_json() to /api/debug/calls/{id}/diff
for consistent timing and serialization performance across all 3 debug endpoints
- Add response_model+Response tradeoff explanation to module docstrings of
debug/routes.py and history/routes.py: FastAPI skips validation when the
handler returns a pre-built Response; response_model= is kept for OpenAPI
schema docs only; contract coverage via test_api_contract.py snapshots
Notable:
- Add pytest_configure xdist guard in perf conftest: raises UsageError if
pytest-xdist is loaded, making the 'dedicated session' requirement explicit
rather than relying on run_perf.sh enforcement alone
Minor:
- Add 128 MB cache_size note to tier-10000 disk warning in run_perf.sh
Review: perf-infra (PR #753)Overall this is solid foundational work — the isolation, ContextVar-based timing, and harness design are well thought out. A few comments below, mostly minor. Strengths
Issues / suggestions
Nits
Test coverageGood — new unit tests cover the middleware (including concurrency), seeding (row counts, idempotency, payload sizes, isolation refusal), the perf DB (isolation + migration), and the report generator (deterministic output). The SSE regression test in the |
- Fix asyncio.run() two-loop anti-pattern in test_server_timing.py:
drop the asyncio.run(_setup()) call — DatabasePool.__init__ is lazy
(no connection opened until first get_pool()), so the pre-warm is
unnecessary. One asyncio.run() remains for teardown only.
- Add positive Server-Timing integration test: new
test_server_timing_header_present_on_timed_path uses a minimal FastAPI
app with the middleware to assert header is present on /api/history/*
without requiring DB tables (avoids the unmigrated in-memory DB issue).
- Relabel time_phase('db') in _fetch_session_list_pg/_fetch_session_list_sqlite:
both blocks wrap multiple queries plus Python postprocessing (string
construction, dict building, list comprehensions). Renamed to 'db+python'
so Server-Timing headers and perf reports are not misleading.
fetch_session_detail keeps 'db' — that block wraps a single conn.fetch().
- Add timed_json_response() helper to timing_middleware.py: dedupes the
with time_phase('serialize'): body = model.model_dump_json(); return
Response(content=body, media_type='application/json') pattern across all
5 timed route handlers. Update both route modules to use it.
- Add note to _extract_shape docstring about list[0]-only inspection limit
- Note: --backend postgres --clean in run_perf.sh uses its own psycopg2
implementation and is unaffected by drop_perf_db(NotImplementedError)
Review: PR #753 — perf test harness, Server-Timing middleware, baseline evidenceStrong, well-scoped PR. Architecture is clean, tests are comprehensive, and isolation enforcement is taken seriously. Below: a few real issues and several smaller nits. Strengths
Issues1. AGENTS.md describes fixtures that don't exist
2.
|
- Fix AGENTS.md fixture names: browser/page → playwright_browser/playwright_page,
perf_admin_api_key → admin_headers, measure_time() → measure_page_load(),
perf_db_path → perf_db_url. Remove Scroll Performance SLO section which had
no corresponding test.
- Delete measure_scroll_fps dead code: 55-line FPS helper with no callers; also
delete ScrollFPSMetrics dataclass and the unit test that tested the dataclass.
- Rename time_phase('db+python') → 'db': reviewer correctly identified that the
block exits before row hydration (list comprehensions run outside the block).
All three time_phase('db') blocks in history/service.py now wrap only
conn.fetch()/fetchval() calls, matching the label.
- Add Python-side disk warning in seed_sessions(tier>=10000): raises warnings.warn
with disk/RAM estimate so safety net works when called directly (not just via
run_perf.sh CLI).
- Fix body: str|bytes annotation in timed_json_response: model_dump_json() returns
str in Pydantic v2; remove the spurious '| bytes' widening.
- Fix double ABOUTME comment in run_perf.sh
- Fix re-import of Path inside fixture body in test_api_contract.py
- Note 2 missing contract test endpoints in test_api_contract.py module docstring:
calls/{id}/events and calls/{id}/diff lack snapshots; gap documented explicitly
Code Review — perf infrastructure + Server-TimingSubstantial, well-documented PR. The isolation story is strong, the middleware design avoids the classic 🔴 Bugs1. 2. 3. Disk-warning math in 🟡 Smaller issues4. 5. JSON built via string concatenation in 6. 7. 8. Duplicate seeding pattern across test files. 9. 10. ✅ Things to keep
Test coverageGood coverage of the new code: unit tests for 🤖 Generated with Claude Code |
…s + smaller issues
Bugs:
- Fix perf_report.py to match actual test output format: the section
functions used _find_result(results, 'page_timings') but tests write
{'fixture': ..., 'scenarios': [...]} with no 'type' field. Added
_page_timing_records(), _throttled_records(), _sse_memory_records()
helpers that detect actual file shapes, and rewrote all section functions
to read the formats tests actually produce. Baseline report will now
render real data instead of 7x 'NO DATA YET'.
- Fix --assert-slo in run_perf.sh: flag only set PERF_ASSERT_SLO=1 but
tests read PERF_THROTTLE_BASELINE and PERF_ASSERT_MEMORY. Now exports
all three so --assert-slo actually gates all SLO assertions.
- Fix disk estimate math: tier * 25 // 1000 gave 250 GB for tier=10000.
Correct formula: tier * 25 * 25 // 1_000_000 ≈ 6 GB (events × avg_payload_KB).
Smaller issues:
- Fix _extract_shape empty list: was returning 'list[unknown]' (str) for
empty lists vs [type] (list) for non-empty — inconsistent container shape
would break snapshot comparison. Now returns [] for empty lists.
- _make_send_with_timing: strip any existing server-timing header before
appending to prevent duplicate headers if inner middleware also emits it.
- Add SSE exclusion comment to _TIMED_PREFIXES explaining why long-lived
connections are excluded.
- Update _BASE_TS from datetime(2025, 1, 1) to datetime.now() - 60 days:
seeded sessions were 17 months old, making recency filters show empty
results against the perf DB.
- Add _BATCH_SIZE memory note: 5000 × ~25 KB ≈ 125 MB per batch.
Review —
|
Bugs:
- Tighten ensure_perf_isolation: SQLite check now parses the URL path and
compares filename == 'local.db' (was substring anywhere in URL); Postgres
check now requires 'options=-csearch_path=perf_test' exactly (was
substring 'perf_test' anywhere in URL).
- Remove time_phase('render') from perf_report.py: phase was listed in the
report's Server-Timing section but never emitted by any handler.
- Add contract snapshot tests for /api/debug/calls/{id} and
/api/debug/calls/{id}/diff — closes the gap called out in the module
docstring; add snapshots call_events.json and call_diff.json.
- Add explicit BEGIN EXCLUSIVE / COMMIT in _seed_sqlite: DDL (DROP INDEX)
was auto-committing before inserts, leaving DB index-less on crash.
Now uses isolation_level=None for manual transaction control, with ROLLBACK
on exception.
- Fix _BASE_TS: was datetime.now()-60d (non-deterministic between runs).
Use datetime(2026, 1, 1) — fixed epoch, deterministic, and recent enough
for most recency-filter use cases.
Performance / production:
- Add BaseHTTPMiddleware/StaticCacheMiddleware ContextVar warning to
ServerTimingMiddleware docstring (issue 6): time_phase from streaming
handlers may silently lose spans.
- Add phases-after-http.response.start note (issue 7): any time_phase block
running during body streaming won't appear in Server-Timing header.
- Rename time_phase('db') -> time_phase('db_block') in _fetch_session_list_pg
and _fetch_session_list_sqlite: blocks span multiple queries + Python
postprocessing. fetch_session_detail keeps 'db' (single conn.fetch call).
Minor:
- total_rows = 3 * n_calls_total (was n_calls_total + 2 * n_calls_total)
- Disk estimate: tier * 25 * 45 // 1_000_000 (~10 GB for 10k) + note on
SQLite overhead; old formula (tier * 25 * 25) under-reported by ~40%
- Align NotImplementedError wording between drop_perf_db and migrate_perf_db
Code Review — PR #753TL;DR: Well-engineered PR with strong isolation guards, deterministic seeding, and a thoughtful pure-ASGI timing middleware. The eye-catching Major1. Middleware ordering contradicts the middleware's own documented hazard. app.add_middleware(StaticCacheMiddleware) # BaseHTTPMiddleware
# … add_middleware stacks outside-in, so ServerTimingMiddleware ends up outermost:
app.add_middleware(ServerTimingMiddleware)The docstring on Fix: swap the order so 2. The shell-level guard is a substring check for conn = psycopg2.connect(url)
cur.execute("DROP SCHEMA IF EXISTS perf_test CASCADE")…on whatever Suggested fix: from luthien_proxy.perf.db import ensure_perf_isolation, drop_perf_schema_postgres
ensure_perf_isolation(os.environ["DATABASE_URL"])
drop_perf_schema_postgres()3.
So every Minor4. 5. 6. 7. 8. Nit9. Magic seed value. 10. Duplicate isolation check. 11. Cost estimate magic formula. Test coverageTest coverage is strong: unit tests for One small gap: I didn't find a test that exercises the middleware ordering end-to-end on a streaming response. Given finding (1), adding a tiny Style / conventions
Overall this is a careful, well-tested infra PR. Address findings #1, #2, and #3 before merge; the rest are polish. |
Major (blocking):
- Convert StaticCacheMiddleware from BaseHTTPMiddleware to pure ASGI:
replaces dispatch() with __call__(scope, receive, send) that wraps send
to inject Cache-Control headers on http.response.start. This eliminates
the BaseHTTPMiddleware between ServerTimingMiddleware and the routes,
fixing ContextVar propagation for streaming responses on timed paths.
Also removes the now-unused BaseHTTPMiddleware import.
Extract _INDEX_STMTS tuple for use in both the seed loop and the finally.
- Add ensure_perf_isolation to run_perf.sh postgres --clean path: the shell
script was bypassing the Python isolation gate (only checked for local.db
substring, not the perf_test schema requirement). Now calls
ensure_perf_isolation(DATABASE_URL) via uv run python before psycopg2.
- Rename time_phase('db_block') -> 'db' everywhere: 'db_block' was correct
but invisible to perf_report.py which only aggregates ('db', 'serialize').
The reviewer recommends 'db' everywhere — a block of queries is still
'db time'. perf_report.py already handles this correctly.
Minor:
- Recreate indexes in _seed_sqlite finally block: failed seed previously
left the DB index-less. Now uses _INDEX_STMTS (extracted module-level
constant) in both the main seed path and finally, so crash-recovery
always leaves a usable (if slower) DB.
- Add \r\n check to time_phase(name): raises ValueError if name contains
CR or LF to prevent header injection by future callers with user data.
- Propagate asyncio.run() constraint to seed_sessions docstring so callers
don't need to chase it through migrate_perf_db.
- Simplify apply_sqlite_migrations docstring note to one line.
- Extract _DETERMINISTIC_RNG_SEED = 0xABCDEF module-level constant.
- Remove duplicate ensure_perf_isolation call in perf_explain.py:
ensure_no_dev_db_in_env() (line 205) already gates on DATABASE_URL;
the second call on get_perf_db_url() output was redundant.
- Add 'events/session × KB/event ÷ 1e6' comment to disk estimate formula.
Review — feat(perf): performance test harness, Server-Timing, baseline evidenceSolid foundation. The perf-DB isolation gate, ContextVar-based timing, and snapshot-based contract tests are the right shapes. Most comments below are nits and one regression risk in the Bugs / regressions
Code quality
Performance
Security
Test coverageStrong. Notable additions: ContextVar concurrent-isolation test, time_phase-outside-context safety test, isolation-refusal test for the seeder, and shape-snapshot contract tests for the six endpoints affected by the Gaps worth a follow-up (not blocking):
Smaller stuff
Nice work overall — the |
Bugs/regressions: - Fix StaticCacheMiddleware: filter existing cache-control before appending — was appending unconditionally (regression from BaseHTTPMiddleware refactor); now symmetric with ServerTimingMiddleware's filter-then-append pattern. - Fix activity stream test timing: replace asyncio.sleep(0.3) with asyncio.Event (sse_ready) set after response headers are confirmed; send_synthetic_requests now awaits sse_ready.wait() instead of a fixed delay — eliminates the race on slow CI runners. - Drop redundant COMMIT in _seed_sqlite finally block: with isolation_level=None the connection is in autocommit mode after ROLLBACK, so CREATE INDEX persists immediately; the trailing COMMIT was dead code that silently failed (the except Exception: pass was hiding its own error). Code quality: - Add else: raise to ensure_perf_isolation for unrecognized URL schemes: closes the silent-pass loophole for schemes other than sqlite:// and postgresql://; function now always explicitly accepts or rejects. - Soften GB estimate: rename gb_estimate -> gb_rough, add 'rough:' prefix to formula comment, expand warning message to say 'roughly ... estimate'. - Fix timed_json_response type: parameter changed from object to BaseModel, removes the # type: ignore[attr-defined] — pyright now checks model_dump_json. - Add sole-writer note to apply_sqlite_migrations docstring.
Code ReviewThanks for the thorough infrastructure — the isolation enforcement, evidence capture, and snapshot coverage are well thought through. Posting feedback grouped by severity. Important1. 2. 3. 4. 5. Worth Noting6. 7. Middleware integration is correct 8. Perf isolation gate is solid Nits
Test Coverage Gaps for New Production Code
|
Important: - seed_sessions now raises if rows with the current prefix already exist: adds _assert_no_existing_rows() called before seeding; callers must call drop_perf_db() first. Docstring updated to document the requirement. - _seed_sqlite finally block now logs on index-recreation failure instead of silently swallowing — caller knows the DB may be index-less. - Add migrate_perf_db_async(): async variant of migrate_perf_db, backed by _migrate_sqlite_async() which is the refactored async core; sync version becomes a thin asyncio.run() wrapper. Safe for async fixtures/handlers. - Tighten time_phase name validation to RFC 8941 token grammar ([A-Za-z0-9_-]+) via re.fullmatch — rejects ;=, commas, spaces, CR/LF. Code quality: - Rename SeedingReport.tier: int|str -> label: str — callers always use str(tier) or 'sami'; typed union was misleading. Update _seed_sqlite parameter and all constructors. - Fix perf_explain.py: exit 1 when Postgres backend is unavailable instead of exit 0 so CI matrices detect the gap. Tests: - test_ensure_perf_isolation_rejects_unrecognized_scheme (mysql://) - test_seed_sqlite_recreates_indexes_after_rollback: exercises _INDEX_STMTS idempotency — drop then recreate leaves full index set - test_seed_sessions_raises_if_rows_already_exist: seeds tier=10, verifies second call raises with 'already exist' - test_static_cache_middleware_replaces_not_appends: creates a route that sets Cache-Control, verifies only one header in the response
PR Review —
|
High priority (blocking): - Remove transient agent artifacts from git: add .gitignore patterns for .sisyphus/evidence/*.log, *.txt, task-*.json, and .sisyphus/plans/; git rm --cached the 6 already-committed transient files (21k lines removed). Canonical baseline reports (.md, query plans) remain tracked. - Precompile time_phase regex at module level: _PHASE_NAME_RE = re.compile(...) eliminates per-call re-import and regex recompilation; validators now do a single _PHASE_NAME_RE.fullmatch(name) lookup. Medium priority: - Remove dead code in _seed_sqlite finally: with isolation_level=None, DROP INDEX inside the BEGIN EXCLUSIVE transaction is rolled back on ROLLBACK (indexes still exist), and on COMMIT the indexes are already created. The finally's CREATE INDEX IF NOT EXISTS was a no-op in both paths. Simplified to just conn.close(). - Upgrade WARNING in routes module docstrings: add 'Do NOT copy this pattern to new routes without adding a contract snapshot test' to both debug/routes.py and history/routes.py so the FastAPI validation-disabled footgun is prominent. Low priority: - Add test_time_phase_records_elapsed_even_when_block_raises: validates the docstring claim that 'phases are recorded even when the block raises'. - Add seeding side-effect comment to perf_explain.py with --seed-if-empty alternative so the surprising implicit seeding is visible. - Add TODO for Postgres options= concatenation issue in get_perf_db_url.
Review: feat(perf) — performance test harness, Server-Timing middleware, and baseline evidenceSolid, well-documented infrastructure PR. The ContextVar-based middleware is a thoughtful design (the Bugs / correctness
Performance considerations
Security
Test coverage
Style / minor
VerdictShip-worthy infrastructure. The two items I'd address before merge are (1) the Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com |
- seed_sami_like: add _assert_no_existing_rows guard for sqlite backend, matching seed_sessions behaviour; re-running without drop_perf_db now raises RuntimeError instead of sqlite3.IntegrityError - migration_check.py: replace assert isinstance(conn, SqliteConnection) with an explicit TypeError so the check survives python -O - perf/db.py: raise RuntimeError immediately when DATABASE_URL already contains options=, preventing a silent duplicate-parameter collision on Postgres - run_perf.sh: tighten local.db isolation check from substring match to basename comparison so paths like mylocal.db.perf no longer trigger a false positive
Adds test_monitored_path_with_zero_phases_omits_header: a handler on a monitored path (/api/history/sessions) that records no time_phase blocks. Verifies the 'if phases' guard in _make_send_with_timing correctly omits the Server-Timing header when the phase list is empty.
Review — PR #753 (perf-infra)Comprehensive infrastructure PR — perf test harness, Server-Timing middleware, deterministic seeding, EXPLAIN capture, and contract snapshots. Overall the design choices are sound and the safety story is much better than I expected for "perf tooling." Comments below. Strengths
Issues & Suggestions1. Doc/test name drift — 2. Inconsistent disk-size warning for tier-10000 3.
Not blocking, but the next person to ctrl-C a 10k seed will appreciate it. 4. 5. 6. Hardcoded line-number callouts in
7. 8. Server-Timing duplicate phase names Test CoverageCoverage looks solid:
One gap worth considering: there's no test that Nits
Nothing here is blocking. Approving the design; the items above are mostly cleanup or follow-ups. 🤖 Generated with Claude Code |
|
Claude-generated merge-queue triage of all open Luthien PRs, requested by Scott (Jul 7, 2026). Advisory only; Scott has not yet acted on these recommendations. Recommendation: close. The slowness this harness was built to measure is being fixed directly (see #795 and the related history work), and the code it instruments has been heavily rewritten since May, which is why the branch now conflicts and sits roughly 600 commits behind. Landing it would mean re-validating 5,400 lines of measurement infrastructure against a codebase that moved past the captured baseline. A performance-test tier can be reintroduced later; the isolation and middleware design here got consistently positive review and remain a good reference. |
Summary
Adds the performance testing infrastructure for the admin UI:
tests/luthien_proxy/perf_tests/): Playwright harness with per-page timing, throttled-network, transcript-open, and SSE memory scenariossrc/luthien_proxy/perf/timing_middleware.py): Instrumentsdb,serialize,renderphases on history/debug endpointssrc/luthien_proxy/perf/seeding.py): 100/1k/10k tier seeding with isolated perf DBscripts/perf_explain.py): SQLite EXPLAIN QUERY PLAN output at 10k scale.sisyphus/evidence/perf-report-baseline.md): SQLite baseline capturedChangelog:
changelog.d/perf-baseline.mdDepends on: nothing (standalone infrastructure)
Followed by:
perf-baselinePR (cursor pagination + UI rewrites)