Skip to content

perf: worker-local initial indexing with deterministic merge#221

Merged
justrach merged 2 commits intomainfrom
feature/218-perf-worker-local-initial-indexing-with-determinis
Apr 11, 2026
Merged

perf: worker-local initial indexing with deterministic merge#221
justrach merged 2 commits intomainfrom
feature/218-perf-worker-local-initial-indexing-with-determinis

Conversation

@justrach
Copy link
Copy Markdown
Owner

@justrach justrach commented Apr 8, 2026

Closes #218

Summary

  • split initial indexing into worker-local parse and main-thread commit phases
  • preserve deterministic merge order across sequential and parallel initial scans
  • fix outline ownership so worker-parsed outlines are cloned and released correctly
  • add a regression test covering sequential vs 4-worker parity

Verification

  • zig test src/tests.zig --test-filter "watcher: parallel initial scan matches sequential results"
  • zig build
  • pre-push checks: zero leaks, benchmark gate passed

Perf

Cold openclaw tree, ReleaseFast, fresh HOME each run:

  • CODEDB_SCAN_WORKERS=1: mean 8.039s, median 8.076s
  • CODEDB_SCAN_WORKERS=4: mean 7.032s, median 7.027s
  • improvement: about 12.5%

Warm openclaw MCP search regression check:

  • workers=1: mean 0.104 ms
  • workers=4: mean 0.105 ms
  • effectively unchanged

@justrach
Copy link
Copy Markdown
Owner Author

justrach commented Apr 8, 2026

@codex review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

Benchmark Regression Report

Threshold: 10.00%

Tool Base (ns) Head (ns) Delta Status
codedb_bundle 696741 709002 +1.76% OK
codedb_changes 125151 121422 -2.98% OK
codedb_deps 33426 32549 -2.62% OK
codedb_edit 20815 19775 -5.00% OK
codedb_find 153120 155862 +1.79% OK
codedb_hot 176916 177786 +0.49% OK
codedb_outline 470588 444493 -5.55% OK
codedb_read 185395 195440 +5.42% OK
codedb_search 278857 285086 +2.23% OK
codedb_snapshot 4064238 4155711 +2.25% OK
codedb_status 164621 208940 +26.92% FAIL
codedb_symbol 69699 67690 -2.88% OK
codedb_tree 86210 84320 -2.19% OK
codedb_word 102127 103480 +1.32% OK

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bea5e33556

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/watcher.zig
const count = chunk_size + extra;
const chunk = entries.items[offset .. offset + count];
offset += count;
threads[i] = try std.Thread.spawn(.{}, initialScanWorker, .{ worker, root, chunk });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Join already-started scan threads when spawn fails

If std.Thread.spawn fails for a later worker (for example, due to process thread limits), this try returns immediately, but any previously started workers keep running while deferred cleanup tears down workers. Those active threads still write into results.items, so freeing worker arenas first creates a use-after-free race and can crash initial indexing under resource pressure.

Useful? React with 👍 / 👎.

Comment thread src/watcher.zig

const workers = try allocator.alloc(WorkerParsedResults, n_workers);
defer {
for (workers) |*worker| worker.deinit(allocator);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deinitialize only worker slots that were initialized

The deferred cleanup iterates every element in workers, but initialization happens inside the spawn loop. If the loop exits early on an error, trailing slots remain undefined and worker.deinit will run ArenaAllocator.deinit on garbage state. This can trigger invalid frees or memory corruption on the same spawn-failure path.

Useful? React with 👍 / 👎.

Comment thread src/watcher.zig
{
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const parsed = try parseInitialScanEntry(root, entry, arena.allocator());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep single-worker scan resilient to per-file read errors

In single-worker mode, try parseInitialScanEntry aborts the whole initial scan on a single file-level I/O failure (e.g., a file removed after collectInitialScanEntries but before read). Previously, per-file indexing failures were ignored and scanning continued, so this is a behavior regression that can leave indexing incomplete when CODEDB_SCAN_WORKERS=1 is used.

Useful? React with 👍 / 👎.

Split initial scan into worker-local read/parse and deterministic main-thread merge. Add parity coverage for 1-worker vs 4-worker initial scan and keep warm search behavior unchanged.

Refs #218
@justrach justrach force-pushed the feature/218-perf-worker-local-initial-indexing-with-determinis branch from bea5e33 to c534341 Compare April 11, 2026 08:10
@justrach justrach merged commit 65e5b73 into main Apr 11, 2026
1 check passed
@github-actions
Copy link
Copy Markdown

Benchmark Regression Report

Threshold: 10.00%

Tool Base (ns) Head (ns) Delta Status
codedb_bundle 703086 687680 -2.19% OK
codedb_changes 111421 111907 +0.44% OK
codedb_deps 30564 29462 -3.61% OK
codedb_edit 19991 19092 -4.50% OK
codedb_find 144710 140317 -3.04% OK
codedb_hot 150945 152671 +1.14% OK
codedb_outline 457200 447373 -2.15% OK
codedb_read 174737 178670 +2.25% OK
codedb_search 298128 295090 -1.02% OK
codedb_snapshot 4341612 4276292 -1.50% OK
codedb_status 256769 253953 -1.10% OK
codedb_symbol 64789 63191 -2.47% OK
codedb_tree 98815 95160 -3.70% OK
codedb_word 91287 91780 +0.54% OK

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c534341a7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/explore.zig
Comment on lines +218 to +220
errdefer owned_outline.deinit();
var persistent_outline = try cloneOutline(&owned_outline, self.allocator);
defer owned_outline.deinit();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove duplicate teardown of owned outline on error

commitParsedFileOwnedOutline registers both errdefer owned_outline.deinit() and defer owned_outline.deinit() for the same object; if any later try fails (for example allocation/indexing failure), both handlers run and deinitialize the same outline twice. That can double-free symbol/import buffers and corrupt allocator state on the error path, which makes failure handling unsafe instead of recoverable.

Useful? React with 👍 / 👎.

Comment thread src/watcher.zig
for (entries) |entry| {
const parsed = parseInitialScanEntry(root, entry, arena_alloc) catch null;
if (parsed) |file| {
results.items.append(arena_alloc, file) catch return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate worker append failures instead of dropping chunks

In initialScanWorker, an append allocation failure causes an immediate return, which silently skips all remaining files in that worker’s chunk while the caller still treats the scan as successful. Under memory pressure this produces incomplete indexing without surfacing an error, so users can get partial search/tree results with no indication that files were dropped.

Useful? React with 👍 / 👎.

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.

perf: worker-local initial indexing with deterministic merge

1 participant