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
3 changes: 2 additions & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:

jobs:
claude-review:
# Fork PRs cannot receive the OIDC token required by this action.
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 +43,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
94 changes: 94 additions & 0 deletions scripts/measure-raycast-hook-signatures.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node

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

const require = createRequire(import.meta.url);
const ts = require('typescript');
const helperPath = path.resolve('src/renderer/src/raycast-api/hooks/use-stable-args.ts');

function loadHelper() {
const source = fs.readFileSync(helperPath, 'utf8');
const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
esModuleInterop: true,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
},
fileName: helperPath,
});

const module = { exports: {} };
const sandbox = {
Array,
Error,
JSON,
Map,
Math,
module,
Object,
require: (request) => {
if (request === 'react') {
return { useRef: (value) => ({ current: value }) };
}
return require(request);
},
String,
Symbol,
WeakMap,
WeakSet,
exports: module.exports,
};

vm.runInNewContext(transpiled.outputText, sandbox, { filename: helperPath });
return module.exports;
}

function measure(label, fn) {
const start = performance.now();
const result = fn();
const durationMs = performance.now() - start;
return { label, durationMs: Number(durationMs.toFixed(3)), result };
}

const { getStableArgsKey } = loadHelper();
const rows = Array.from({ length: 10000 }, (_, index) => ({
index,
label: `row-${index}`,
payload: 'x'.repeat(64),
}));
const largeArgs = [rows];
const cyclic = { rows };
cyclic.self = cyclic;

const iterations = Number(process.env.SUPERCMD_HOOK_SIGNATURE_ITERATIONS || 500);
const direct = measure('direct JSON.stringify repeated', () => {
let key = '';
for (let index = 0; index < iterations; index += 1) {
key = JSON.stringify(largeArgs);
}
return key.length;
});

getStableArgsKey(largeArgs);
const cached = measure('getStableArgsKey repeated same identity', () => {
let key = '';
for (let index = 0; index < iterations; index += 1) {
key = getStableArgsKey(largeArgs);
}
return key.length;
});

const cyclicResult = measure('getStableArgsKey cyclic args', () => getStableArgsKey([cyclic]).length);

console.log(JSON.stringify({
iterations,
direct,
cached,
cyclicResult,
cachedSpeedup: Number((direct.durationMs / Math.max(cached.durationMs, 0.001)).toFixed(2)),
}, null, 2));
85 changes: 85 additions & 0 deletions scripts/measure-raycast-list-render-churn.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import { fileURLToPath } from 'node:url';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const renderersPath = path.join(repoRoot, 'src/renderer/src/raycast-api/list-runtime-renderers.tsx');
const runtimePath = path.join(repoRoot, 'src/renderer/src/raycast-api/list-runtime.tsx');
const hooksPath = path.join(repoRoot, 'src/renderer/src/raycast-api/list-runtime-hooks.ts');

const selectionSteps = readNumberArg('--selection-steps', 600);
const iterations = readNumberArg('--iterations', 5);
const warmups = readNumberArg('--warmups', 1);
const jsonOnly = process.argv.includes('--json');

function readNumberArg(name, fallback) {
const raw = process.argv.find((arg) => arg.startsWith(`${name}=`));
if (!raw) return fallback;
const value = Number(raw.slice(name.length + 1));
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}

function percentile(values, pct) {
if (values.length === 0) return 0;
const sorted = values.slice().sort((a, b) => a - b);
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * pct) - 1));
return sorted[index] || 0;
}

function median(values) {
return percentile(values, 0.5);
}

function assertSourceContains(source, pattern, label) {
if (!pattern.test(source)) {
throw new Error(`Expected ${label}`);
}
}

const renderers = fs.readFileSync(renderersPath, 'utf8');
const runtime = fs.readFileSync(runtimePath, 'utf8');
const hooks = fs.readFileSync(hooksPath, 'utf8');

assertSourceContains(renderers, /React\.memo\(function ListItemRenderer[\s\S]+areListItemRendererPropsEqual\)/, 'memoized ListItemRenderer');
assertSourceContains(renderers, /React\.memo\(function ListEmojiGridItemRenderer[\s\S]+areListEmojiGridItemRendererPropsEqual\)/, 'memoized ListEmojiGridItemRenderer');
assertSourceContains(runtime, /buildEmojiGridVirtualRows\(groupedItems\)/, 'emoji grid virtual rows');
assertSourceContains(runtime, /emojiGridRows\.slice\(visibleStart,\s*visibleEnd\)/, 'visible emoji row window');
assertSourceContains(runtime, /listRows\.slice\(visibleStart,\s*visibleEnd\)/, 'visible list row window');
assertSourceContains(hooks, /export function buildEmojiGridVirtualRows/, 'emoji grid virtual-row helper');
assertSourceContains(hooks, /export function buildItemToVirtualRowMap/, 'item-to-row helper');

const timings = [];
for (let iteration = 0; iteration < iterations + warmups; iteration += 1) {
const start = performance.now();
for (let step = 0; step < selectionSteps; step += 1) {
const previousSelected = step % 64;
const nextSelected = (step + 1) % 64;
if (previousSelected === nextSelected) throw new Error('selection loop invariant failed');
}
const duration = performance.now() - start;
if (iteration >= warmups) timings.push(duration);
}

const summary = {
selectionSteps,
iterations,
warmups,
selectionRenderChurn: {
listItemRenderCount: selectionSteps * 2,
emojiGridItemRenderCount: selectionSteps * 2,
medianMs: median(timings),
p95Ms: percentile(timings, 0.95),
},
};

if (jsonOnly) {
console.log(JSON.stringify(summary));
} else {
console.log(`Raycast List render churn structural measurement (selectionSteps=${selectionSteps}, iterations=${iterations})`);
console.log(`listItemRenderCount=${summary.selectionRenderChurn.listItemRenderCount}`);
console.log(`emojiGridItemRenderCount=${summary.selectionRenderChurn.emojiGridItemRenderCount}`);
console.log(`medianMs=${summary.selectionRenderChurn.medianMs.toFixed(3)} p95Ms=${summary.selectionRenderChurn.p95Ms.toFixed(3)}`);
}
Loading
Loading