Skip to content

(perf): FTS dirty-flag — skip reindex on tab switch when files unchanged - #74

Merged
JeanBaptisteRenard merged 2 commits into
mainfrom
perf/fts-dirty-flag
Jun 13, 2026
Merged

(perf): FTS dirty-flag — skip reindex on tab switch when files unchanged#74
JeanBaptisteRenard merged 2 commits into
mainfrom
perf/fts-dirty-flag

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Collaborator

Problem

get-memories and get-work-files are called every time the user opens or switches to the Memory/Work Files sidebar tabs. Each call unconditionally ran a full FTS re-index: deleteSearchType wipes all rows, then upsertSearchEntries re-reads every file from disk. With the contentless FTS5 schema (#69), this also rewrites the external-content + trigram index each time — pure churn when nothing changed.

Solution: signature-based dirty-flag

Three pure module-level helpers introduced in main.js:

  • computeIndexSignature(files) — produces a stable filePath:mtimeMs:size|... string sorted by path, order-independent.
  • shouldReindex(type, sig) — compares the signature against the last-indexed one stored in a Map. Returns true (and updates the stored sig) only when changed or never indexed.
  • invalidateFtsSignature(type) — clears the stored signature, forcing a fresh reindex on the next tab open.

Both handlers compute the signature from the already-gathered file list and skip the deleteSearchType + upsertSearchEntries block (including all fs.readFileSync calls) when unchanged.

Correctness invariants

  • Always-return: the result payload (file tree sent to the UI) is built unconditionally, before the shouldReindex guard. Only the FTS side-effect is gated.
  • Mutation invalidation: save-memory calls invalidateFtsSignature('memory') after writing (guards against sub-second mtime precision). delete-work-file calls invalidateFtsSignature('work-file') after deletion (path-set change).
  • The signature catches all cases automatically: added/removed files change the path set; edits change mtime; work-file sizes are also tracked.

Tests

21 unit tests in test/fts-dirty-flag.test.js:

  • computeIndexSignature: empty array, single-file encoding, sort order-independence, mtime/size/path-set change detection
  • shouldReindex: first call forces reindex, same sig skips, changed sig reindexes, types are isolated
  • invalidateFtsSignature: forces reindex even for same sig, scoped to targeted type, re-prime after invalidation
  • Static-analysis assertions: all four handlers wired, always-return ordering verified

Each tab switch to Memory or Work Files called deleteSearchType + upsertSearchEntries
unconditionally, re-reading every file from disk on every open. With the recent
contentless FTS5 (#69) this also rewrites the external-content + trigram index
each time — pure churn when nothing changed.

Add three pure helpers (computeIndexSignature, shouldReindex, invalidateFtsSignature)
stored in module-level Maps keyed by FTS type. Both handlers now compute a
filePath:mtimeMs:size signature over the collected file list and skip the wipe+rebuild
when it matches the last-indexed signature. The result payload (file tree returned to
the UI) is built and returned unconditionally — only the FTS side-effect is gated.

save-memory and delete-work-file call invalidateFtsSignature to force a fresh reindex
on the next tab open even when mtime precision could mask a sub-second write.

21 unit tests cover: signature stability, order-independence, mtime/size/path-set
change detection, type isolation, invalidation, and static wiring assertions for all
four handlers.
save-file-for-panel wrote to disk without calling invalidateFtsSignature,
leaving a sub-second window where a save followed immediately by a search
would return stale FTS body content. Mirrors the explicit invalidation
already present in save-memory and delete-work-file (review MAJOR-1).
Paths under /.work-files/ invalidate 'work-file'; .md paths invalidate
'memory'; both can fire on the same path (e.g. a .work-files/note.md).

computeIndexSignature now uses NUL field separators and newline record
separators instead of ':' and '|', eliminating a theoretical collision
where a filePath containing the literal ':<digits>:<digits>|' string
could produce the same joined string as two separate entries (MINOR-2).

Tests: 2 new static-analysis assertions for save-file-for-panel wiring;
1 new NUL-delimiter collision test; 2 updated format assertions for
the changed separator characters. 24 tests total, all pass.
@JeanBaptisteRenard
JeanBaptisteRenard merged commit 843de89 into main Jun 13, 2026
7 checks passed
@JeanBaptisteRenard
JeanBaptisteRenard deleted the perf/fts-dirty-flag branch June 13, 2026 18:07
JeanBaptisteRenard added a commit that referenced this pull request Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant