Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:

jobs:
claude-review:
if: github.event.pull_request.head.repo.full_name == github.repository
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
Expand Down Expand Up @@ -41,4 +42,3 @@ jobs:
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
cache: npm

- name: Install dependencies
run: npm ci
run: npm ci --force

- name: Run node test suite
run: npm test
197 changes: 197 additions & 0 deletions scripts/benchmark-file-search-delete-batch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env node

import { createRequire } from 'node:module';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import vm from 'node:vm';

const require = createRequire(import.meta.url);
const ts = require('typescript');

function loadFileSearchIndexInternals() {
const filePath = path.resolve('src/main/file-search-index.ts');
const source = `${fs.readFileSync(filePath, 'utf8')}

exports.__test = {
indexEntry,
searchIndexedFiles,
tombstoneDeletedPaths,
makeSnapshot() {
return {
entries: [],
prefixToEntryIds: new Map(),
pathToEntryId: new Map(),
builtAt: Date.now(),
};
},
setActiveIndex(snapshot, homeDir) {
activeIndex = snapshot;
configuredHomeDir = homeDir;
includeRoots = [homeDir];
includeProtectedHomeRoots = true;
lastBuildStartedAt = 0;
lastIndexError = null;
},
};
`;

const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
esModuleInterop: true,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
},
fileName: filePath,
});

const module = { exports: {} };
const sandbox = {
module,
exports: module.exports,
require,
console,
Date,
Map,
Promise,
Set,
clearInterval,
clearTimeout,
process,
setInterval,
setTimeout,
};
vm.runInNewContext(transpiled.outputText, sandbox, { filename: filePath });
return module.exports.__test;
}

function addEntry(api, snapshot, filePath, isDirectory = false) {
api.indexEntry(snapshot, {
path: filePath,
name: path.basename(filePath),
parentPath: path.dirname(filePath),
isDirectory,
});
}

function buildDirectoryScenario(api) {
const homeDir = '/tmp/supercmd-file-index-bench';
const snapshot = api.makeSnapshot();
const deletedRoot = path.join(homeDir, 'deleted-root');
const survivorRoot = path.join(homeDir, 'survivors');
const deletePaths = [deletedRoot];

addEntry(api, snapshot, deletedRoot, true);
for (let group = 0; group < 40; group += 1) {
const groupDir = path.join(deletedRoot, `group-${group}`);
addEntry(api, snapshot, groupDir, true);
deletePaths.push(groupDir);
for (let item = 0; item < 100; item += 1) {
const nestedDir = path.join(groupDir, `nested-${item}`);
addEntry(api, snapshot, nestedDir, true);
addEntry(api, snapshot, path.join(nestedDir, `deleted-${group}-${item}.txt`));
deletePaths.push(nestedDir);
}
}

addEntry(api, snapshot, survivorRoot, true);
for (let dir = 0; dir < 120; dir += 1) {
const dirPath = path.join(survivorRoot, `dir-${dir}`);
addEntry(api, snapshot, dirPath, true);
for (let file = 0; file < 500; file += 1) {
addEntry(api, snapshot, path.join(dirPath, `survivor-${dir}-${file}.txt`));
}
}

return {
deletePaths,
deletedRoot,
expectedDeletedCount: 1 + (40 * 100) + 40 + (40 * 100),
homeDir,
name: 'directory-tree-batch',
postDeleteQuery: 'deleted 39 99',
postDeleteQueryBucket: 'deleted',
snapshot,
survivorPath: path.join(survivorRoot, 'dir-119', 'survivor-119-499.txt'),
};
}

function buildDirectFileScenario(api) {
const homeDir = '/tmp/supercmd-file-index-bench-direct';
const snapshot = api.makeSnapshot();
const rootDir = path.join(homeDir, 'projects');
const deletePaths = [];

addEntry(api, snapshot, rootDir, true);
for (let dir = 0; dir < 160; dir += 1) {
const dirPath = path.join(rootDir, `dir-${dir}`);
addEntry(api, snapshot, dirPath, true);
for (let file = 0; file < 500; file += 1) {
const filePath = path.join(dirPath, `file-${dir}-${file}.txt`);
addEntry(api, snapshot, filePath);
if (deletePaths.length < 500 && file % 8 === 0) {
deletePaths.push(filePath);
}
}
}

return {
deletePaths,
expectedDeletedCount: deletePaths.length,
homeDir,
name: 'direct-file-batch',
postDeleteQuery: path.basename(deletePaths[0] || ''),
postDeleteQueryBucket: 'file',
snapshot,
survivorPath: path.join(rootDir, 'dir-159', 'file-159-499.txt'),
};
}

async function measureScenario(api, scenario) {
const bucketBefore = scenario.snapshot.prefixToEntryIds.get(scenario.postDeleteQueryBucket)?.length || 0;
const started = performance.now();
api.tombstoneDeletedPaths(scenario.snapshot, scenario.deletePaths);
const elapsedMs = performance.now() - started;
const bucketAfter = scenario.snapshot.prefixToEntryIds.get(scenario.postDeleteQueryBucket)?.length || 0;

let deletedCount = 0;
for (const entry of scenario.snapshot.entries) {
if (entry.deleted) deletedCount += 1;
}

const survivorId = scenario.snapshot.pathToEntryId.get(scenario.survivorPath);
assert.equal(deletedCount, scenario.expectedDeletedCount);
assert.notEqual(survivorId, undefined);
assert.equal(scenario.snapshot.entries[survivorId].deleted, undefined);

api.setActiveIndex(scenario.snapshot, scenario.homeDir);
const queryStarted = performance.now();
const postDeleteResults = await api.searchIndexedFiles(scenario.postDeleteQuery, { limit: 20 });
const postDeleteQueryMs = performance.now() - queryStarted;
assert.equal(postDeleteResults.some((result) => result.path && result.path.includes('deleted-root')), false);

return {
name: scenario.name,
entries: scenario.snapshot.entries.length,
deletePaths: scenario.deletePaths.length,
deletedCount,
elapsedMs: Number(elapsedMs.toFixed(3)),
postDeleteQueryMs: Number(postDeleteQueryMs.toFixed(3)),
postDeleteResultCount: postDeleteResults.length,
touchedBucketSizeBefore: bucketBefore,
touchedBucketSizeAfter: bucketAfter,
};
}

const api = loadFileSearchIndexInternals();
const scenarios = [
await measureScenario(api, buildDirectFileScenario(api)),
await measureScenario(api, buildDirectoryScenario(api)),
];

console.log(JSON.stringify({
benchmark: 'file-search-delete-batch',
scenarios,
}, null, 2));
Loading
Loading