perf(state): replace search_sessions full-table aggregate with correlated subquery - #76161
perf(state): replace search_sessions full-table aggregate with correlated subquery#76161spfcraze wants to merge 1 commit into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating a real session-list query cost. Current main still builds the global derived aggregate in search_sessions (hermes_state.py:6966-6972), while the proposed correlated lookup follows the established list_sessions_rich pattern (hermes_state.py:5381-5384).
Problems
tests/test_hermes_state.py:763-789captures internal SQL and assertsEXPLAIN QUERY PLAN/literalGROUP BYandMATERIALIZEdetails. That freezes this exact implementation rather than the returned-row contract;AGENTS.md:80-83requires behavior contracts over snapshots.
Suggested changes
- Drop the plan-pin test and keep the behavioral ordering/fallback test (
tests/test_hermes_state.py:731-742) plus result-parity coverage (:744-761).
Automated hermes-sweeper review.
| ) | ||
| assert "GROUP BY" not in issued[0], ( | ||
| "search_sessions still aggregates the whole messages table") | ||
| assert "MATERIALIZE" not in plan, f"full-table aggregate in plan:\n{plan}" |
There was a problem hiding this comment.
Please remove this planner/SQL-shape assertion. It couples the test to one SQLite implementation rather than the observable MRU ordering and started_at fallback contract already covered above.
…ated subquery search_sessions() computed last_active via a LEFT JOIN over a derived table aggregating MAX(timestamp) GROUP BY session_id across EVERY message in the DB — O(all messages) on every call, reached on each 'hermes -c'/'--resume' launch and ACP session-list. The correlated scalar subquery resolves MAX(timestamp) per session row via the messages(session_id, ...) index — the same pattern list_sessions_rich already uses for its last_active. Measured (real schema, 2k sessions / 120k messages, median of 15): 10.03 ms -> 1.85 ms per LIMIT-20 call (5.4x), with the OLD plan materializing a SCAN of all messages and the NEW plan doing indexed per-row lookups. Results byte-identical (page and full-table). Tests: MRU ordering + COALESCE fallback behavior, byte-exact parity against the pre-fix join form (unfiltered/filtered/paginated), and a plan pin asserting the issued query never materializes a GROUP BY over the messages table.
|
Replaced the plan-pin in 6ea5a58 (amended, force-pushed). You're right it froze the implementation — EXPLAIN text and a literal In its place, the perf contract is now asserted behaviorally: Re-verified after the amend: 143/143 in the state suite, sabotage revert-verified (the work-bound test fails on pre-fix main, everything passes with the fix), zero branch-only failures vs baseline. |
547a31a to
6ea5a58
Compare
What does this PR do?
search_sessions() computed last_active via a LEFT JOIN over a derived table aggregating MAX(timestamp) GROUP BY session_id across EVERY message in the DB — O(all messages) per call, reached on every 'hermes -c'/'--resume' launch and ACP session-list. Replace with a correlated scalar subquery that resolves MAX(timestamp) per session row via the messages(session_id, ...) index — the exact pattern list_sessions_rich already uses for its last_active.
Related Issue
No direct issue — discovered via code review and reproduced live (see below).
Related PRs reviewed during the duplicate check (none covers this change):
Changes Made
fix/search-sessions-subquery— 2 file(s) changed vs base:hermes_state.pytests/test_hermes_state.pyhermes_state.py: one query rewrite in search_sessions() (derived-table LEFT JOIN -> correlated scalar subquery), same COALESCE fallback and ORDER BY. tests/test_hermes_state.py: +4 tests — MRU ordering with COALESCE-to-started_at fallback for message-less sessions, byte-exact parity vs the pre-fix join form across unfiltered/filtered/paginated calls, and a plan pin that drives the real method and asserts the issued query never materializes a GROUP BY over the messages table.
How to Test
Measured on a scratch DB with the real schema and indexes (2k sessions / 120k messages, median of 15 runs, repo venv): OLD 10.03 ms vs NEW 1.85 ms per LIMIT-20 call (5.4x). EXPLAIN QUERY PLAN: OLD materializes a SCAN of all 120k messages plus an AUTOMATIC COVERING INDEX for the join; NEW does per-row indexed lookups (idx_messages_session). Results byte-identical for both the page query and the full unbounded query. Gap grows with DB size — the aggregate cost is O(total messages), the correlated form is O(sessions).
Validation completed (recorded by prp):
tests/test_hermes_state.py.tests/test_hermes_state.py: branch 143 passed / 0 failed vs baseline 140 passed / 0 failed — zero branch-only failures.Logs
Sabotage verification output: