Repository navigation
Speed up llm logs filters, prompt -c and logging on large^H^H^Hnormal after few months :) databases - #1657
Speed up llm logs filters, prompt -c and logging on large^H^H^Hnormal after few months :) databases#1657rdslw wants to merge 1 commit into
Conversation
ac77f6a to
07a9250
Compare
…ry conversation id for 'prompt -c' EXPLAIN QUERY PLAN over the queries llm logs issues showed five scans that grow linearly with the store, and one more on every logged turn. m028_log_lookup_indexes: - parts(tool_name, message_hash) where type = 'tool_result': the --tools/-T filters resolve "messages carrying a tool_result part (of this name)". That was a full scan of parts - the fastest-growing table - plus a transient automatic index SQLite built on every query. The trailing message_hash makes the index covering for exactly that subquery. The index is partial on purpose: without sqlite_stat1, a full index that leads with type gets picked for every "type = ?" lookup, so _part_ids() and the message_tree view would read all parts of a type instead of one message's parts. Only queries that spell out type = 'tool_result' can use it. - tool_calls/tool_results/tool_responses(response_id): the legacy extras and legacy --tools filter read these only through correlated subqueries on response_id - one full scan per listed response, and tool_results rows carry the tool outputs. tool_responses' primary key starts with tool_id, so it could not serve the probe either. - responses(conversation_id): --cid on the legacy side was a full responses scan; turns got idx_turns_thread_id in m023 but the legacy column never got the equivalent. load_conversation(): the latest-conversation lookup for 'prompt -c' used a distinct union of threads and conversations, which materializes every id from both tables before sorting for one row. 'union all' lets SQLite stop at the first row of each primary key; the outer limit 1 already discards any dual-write duplicate. TURN_SEARCH_INSERT_SQL: the turn_search refresh that LogStore.log runs for every logged turn joined output_messages to messages and parts with plain joins. SQLite cannot tell the recursive chain is short, so it put parts in the outer loop and scanned the whole table per turn. Cross joins keep output_messages outermost; the derived rows are the same. tests/test_migrate.py asserts migrate() leaves all five indexes in place, the parts one partial.
07a9250 to
a08d649
Compare
|
@simonw I re-ran the numbers for this PR on top of 0.37.1 (rebased, now on b9786a2). They showed two regressions in the Setup. A copy of my logs.db (442 legacy responses, 407 turns, 709 messages, 1,422 parts) and the same rows replicated 10x and 50x (50x: 22,100 responses, 20,350 turns, 35,450 messages, 71,100 parts; keys rewritten consistently, What was wrong with
The branch now uses the partial index from the description: Write path (in 0.37.1, not caused by this PR). The In-process medians in ms, 0.37.1 → this PR:
End to end at 50x with hyperfine (
The five indexes add 2.1 MiB at 50x (0.24% of the file). m028 takes 9 ms on my database and 204 ms at 50x. p.s. created with help of Opus-5.5. Reviewed and used by me. |
I updated PR after llm-0.37 release. please look at the comment below for accurate perfoemnce table measurements.
TL;DR: this PR offers substantial performance improvemnts via simple change:
EXPLAIN QUERY PLANover the queriesllm logsruns shows five lookups that scan a whole table. Fine at hundreds of rows, painful as the store grows. This adds one migration with five indexes and a one-word query change:parts(type, tool_name, message_hash)- the--tools/-Tfilters resolve "messages with a tool_result part" by scanningparts, the fastest-growing table, and building a transient automatic index on every query.tool_calls,tool_results,tool_responsesonresponse_id- the legacy extras and the legacy--toolsfilter probe these per listed response through correlated subqueries.responses(conversation_id)- legacy--cidfiltering;turns.thread_idgot its index in m023, this column never did.load_conversation()forprompt -cused a distinctunionofthreadsandconversations, which materializes every id before taking one row.union alllets SQLite stop at the first row of each primary key; the outerlimit 1already discards any dual-write duplicate.Measured on a copy of my logs.db (442 legacy responses, 359 turns, 1202 parts) and the same database replicated 50x (22k responses, 18k turns, 60k parts). Query timings, median of 15 runs:
--toolsfilter--cidfilter--toolsfilter (parts)-T namefilter (parts)prompt -c(12k threads)End to end at 50x, hyperfine mean of 3 runs (about 2 s of each is interpreter and plugin startup):
llm logs list --tools -n 5llm logs list -T Brave_context -n 5llm logs list --cid ID -n 5The five indexes add about 0.6% to the file size. If you prefer a smaller footprint on
parts, a partial indexon parts (tool_name, message_hash) where type = 'tool_result'serves the same query; happy to switch.Created with help of Fable-5.1 during migration of llm-openai-codex from llm-0.32 to llm-0.33.