perf(core): hoist ingest_priority lookup out of FilesystemPath insert#76
Draft
andykais-claude wants to merge 1 commit into
Draft
perf(core): hoist ingest_priority lookup out of FilesystemPath insert#76andykais-claude wants to merge 1 commit into
andykais-claude wants to merge 1 commit into
Conversation
filesystem.discover() degraded quadratically as the filesystem_path table grew: every INSERT computed `(SELECT MAX(ingest_priority) FROM filesystem_path WHERE directory = 0)` inline via a CASE/subquery. SQLite's planner picked the non-partial `filesystem_path_directory` index, forcing a full scan of every directory=0 row on each insert. This change: * removes the priority_instruction CASE/subquery from FilesystemPath.create — the model now accepts ingest_priority directly * adds select_max_priority() / select_min_priority() helpers that pin the partial unique index `filesystem_path_priority` via INDEXED BY, turning the lookup into an O(log n) covering-index probe * moves the 'first'/'none' resolution into FileSystemActions.discover, which now resolves MAX(ingest_priority) once per pass and increments next_file_priority in memory per new row Benchmark (linux, in-memory wal, single discover loop): rows before after ---- ------ ----- 1k 0.84 ms 1.24 ms / insert 5-10k 1.18 ms 1.10 ms / insert 10-20k 2.93 ms 0.59 ms / insert 20-40k 10.26 ms 1.19 ms / insert 40-80k n/a 1.67 ms / insert select_max_priority() itself runs at ~0.004 ms via SEARCH filesystem_path USING COVERING INDEX filesystem_path_priority. Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
forager.filesystem.discover()degraded quadratically as thefilesystem_pathtable grew. EachFilesystemPath.create()ran a CASE/subquery to compute the nextingest_priority:The schema does have a partial unique index that should make this O(log n):
But SQLite's planner picks
filesystem_path_directory(the plain index ondirectory) and does a full scan of everydirectory=0row to find the MAX.EXPLAIN QUERY PLANconfirmed it:So every insert paid an O(n) probe, making bulk discovery O(n²).
Fix
Two layered changes:
Model (
packages/core/src/models/filesystem_path.ts)#createnow bindsingest_prioritydirectly as a parameter.select_max_priority()/select_min_priority()helpers that pin the partial unique index viaINDEXED BY filesystem_path_priority. With the hint, SQLite uses a covering-index lookup (SEARCH ... USING COVERING INDEX filesystem_path_priority), turning the probe into O(log n).PRIORITY_SPACERas a static so callers can replicate the existing "+ 1000 per row" stride.Actions (
packages/core/src/actions/filesystem_actions.ts)MAX(ingest_priority)exactly once at the start ofdiscover(), then hand out monotonically increasing priorities by simple in-memory increment per newly-created row. Within a singlediscover()pass we are the only writer, so the partial unique constraint is still preserved.#add_filepathnow returns whether it inserted (so the loop only advances the counter on actual creations).Benchmark
Single discover loop, fresh DB, WAL on, Linux.
Before: linear per-insert growth → O(n²) total. After: flat per-insert cost → O(n) total. At 40k rows the per-row cost dropped ~8.5x; the gap widens further with table size (the original 40k → 80k batch would have taken ~30+ minutes; the new path finishes in ~67s).
Standalone
select_max_priority()benchmark on a populated table runs in ~0.004 ms thanks to:Testing
deno check packages/core/src/mod.tsdeno task --cwd packages/core lintdeno test ... --filter 'ingest actions'(exercises the new discover flow end-to-end)deno testcore suite: 26 tests pass; the 4 pre-existing failures are all duration/ffprobe floating-point precision issues called out inAGENTS.md("Audio duration: 6.96 vs 6.92", keypoint timestamp precision) and reproduce onmainunchanged.Compatibility notes
FilesystemPath.create({ priority_instruction: ... })is gone. Callers passingest_priority: number | nulldirectly. The two in-tree callers (#add_filepathfor files,#add_directoriesfor directories) are updated.