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
133 changes: 133 additions & 0 deletions scripts/bench-script-command-discovery.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env node

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { loadScriptCommandRunner } from './lib/script-command-runner-harness.mjs';

const fileCount = Number(process.env.SUPERCMD_BENCH_SCRIPT_COUNT || 40);
const bodyBytesPerFile = Number(process.env.SUPERCMD_BENCH_SCRIPT_BODY_BYTES || 1024 * 1024);
const iconBytesPerFile = Number(process.env.SUPERCMD_BENCH_SCRIPT_ICON_BYTES || 0);

function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
}

function makeLargeScript(title, bodyBytes, icon = '⚡') {
const header = `#!/bin/bash
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title ${title}
# @raycast.mode fullOutput

# Optional parameters:
# @raycast.packageName Benchmark
# @raycast.icon ${icon}
# @raycast.description Large script command fixture
# @raycast.needsConfirmation false

exit 0
`;
const line = `# ${'x'.repeat(253)}\n`;
const repeatCount = Math.max(0, Math.ceil(bodyBytes / Buffer.byteLength(line)));
return header + line.repeat(repeatCount);
}

function snapshot(metrics) {
return {
readFileSyncCalls: metrics.readFileSyncCalls,
readFileSyncBytes: metrics.readFileSyncBytes,
readSyncCalls: metrics.readSyncCalls,
readSyncBytes: metrics.readSyncBytes,
openSyncCalls: metrics.openSyncCalls,
totalBytes: metrics.readFileSyncBytes + metrics.readSyncBytes,
};
}

function printMetric(label, durationMs, metricSnapshot) {
console.log(`${label}: ${durationMs.toFixed(1)}ms`);
console.log(` readFileSync: ${metricSnapshot.readFileSyncCalls} calls, ${formatBytes(metricSnapshot.readFileSyncBytes)}`);
console.log(` readSync: ${metricSnapshot.readSyncCalls} calls, ${formatBytes(metricSnapshot.readSyncBytes)}`);
console.log(` total runner bytes read: ${formatBytes(metricSnapshot.totalBytes)}`);
}

const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'supercmd-script-bench-'));
const scriptsDir = path.join(tempRoot, 'script-commands');
const userDataDir = path.join(tempRoot, 'user-data');
fs.mkdirSync(scriptsDir, { recursive: true });
fs.mkdirSync(userDataDir, { recursive: true });

try {
const iconsDir = path.join(scriptsDir, '.icons');
if (iconBytesPerFile > 0) {
fs.mkdirSync(iconsDir, { recursive: true });
}

for (let i = 0; i < fileCount; i += 1) {
const scriptPath = path.join(scriptsDir, `large-command-${String(i + 1).padStart(3, '0')}.sh`);
let icon = '⚡';
if (iconBytesPerFile > 0) {
icon = `.icons/icon-${String(i + 1).padStart(3, '0')}.png`;
fs.writeFileSync(path.join(scriptsDir, icon), Buffer.alloc(iconBytesPerFile, i % 251));
}
fs.writeFileSync(scriptPath, makeLargeScript(`Large Command ${i + 1}`, bodyBytesPerFile, icon), {
mode: 0o755,
});
}

process.env.SUPERCMD_SCRIPT_COMMAND_PATHS = scriptsDir;
const { module: runner, metrics, resetMetrics } = await loadScriptCommandRunner({
userDataDir,
scriptCommandFolders: [],
instrumentFs: true,
});

resetMetrics();
const discoveryStart = performance.now();
const commands = runner.discoverScriptCommands();
const discoveryMs = performance.now() - discoveryStart;
const discoveryMetrics = snapshot(metrics);

if (commands.length !== fileCount) {
throw new Error(`Expected ${fileCount} commands, discovered ${commands.length}`);
}

resetMetrics();
const executeStart = performance.now();
await runner.executeScriptCommand(commands[0].id);
const executeMs = performance.now() - executeStart;
const executeMetrics = snapshot(metrics);

resetMetrics();
runner.invalidateScriptCommandsCache();
const invalidatedDiscoveryStart = performance.now();
const rediscoveredCommands = runner.discoverScriptCommands();
const invalidatedDiscoveryMs = performance.now() - invalidatedDiscoveryStart;
const invalidatedDiscoveryMetrics = snapshot(metrics);

if (rediscoveredCommands.length !== fileCount) {
throw new Error(`Expected ${fileCount} commands after invalidation, discovered ${rediscoveredCommands.length}`);
}

resetMetrics();
runner.invalidateScriptCommandsCache();
const invalidatedExecuteStart = performance.now();
await runner.executeScriptCommand(commands[0].id);
const invalidatedExecuteMs = performance.now() - invalidatedExecuteStart;
const invalidatedExecuteMetrics = snapshot(metrics);

console.log('Script command discovery benchmark');
console.log(`files: ${fileCount}`);
console.log(`body bytes per file: ${formatBytes(bodyBytesPerFile)}`);
console.log(`icon bytes per file: ${iconBytesPerFile > 0 ? formatBytes(iconBytesPerFile) : 'emoji'}`);
printMetric('discovery', discoveryMs, discoveryMetrics);
printMetric('cached execution shebang lookup', executeMs, executeMetrics);
printMetric('invalidated discovery', invalidatedDiscoveryMs, invalidatedDiscoveryMetrics);
printMetric('invalidated execution lookup', invalidatedExecuteMs, invalidatedExecuteMetrics);
} finally {
delete process.env.SUPERCMD_SCRIPT_COMMAND_PATHS;
fs.rmSync(tempRoot, { recursive: true, force: true });
}
170 changes: 170 additions & 0 deletions scripts/benchmark-file-search-delete-batch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/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,
tombstoneDeletedPaths,
makeSnapshot() {
return {
entries: [],
prefixToEntryIds: new Map(),
pathToEntryId: new Map(),
builtAt: Date.now(),
};
},
};
`;

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),
name: 'directory-tree-batch',
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,
name: 'direct-file-batch',
snapshot,
survivorPath: path.join(rootDir, 'dir-159', 'file-159-499.txt'),
};
}

function measureScenario(api, scenario) {
const started = performance.now();
api.tombstoneDeletedPaths(scenario.snapshot, scenario.deletePaths);
const elapsedMs = performance.now() - started;

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);

return {
name: scenario.name,
entries: scenario.snapshot.entries.length,
deletePaths: scenario.deletePaths.length,
deletedCount,
elapsedMs: Number(elapsedMs.toFixed(3)),
};
}

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

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