Skip to content
Merged
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
55 changes: 55 additions & 0 deletions .github/scripts/perf-benchmark-history/bench-run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const dir = process.argv[2];
const output = join(tmpdir(), 'repomix-bench-output.txt');
const runs = Number(process.env.BENCH_RUNS) || 20;
const bin = join(dir, 'bin', 'repomix.cjs');

// Warmup runs to stabilize OS page cache and JIT
for (let i = 0; i < 2; i++) {
try {
execFileSync(process.execPath, [bin, dir, '--output', output], { stdio: 'ignore' });
} catch {}
}

// Measurement runs
const times = [];
for (let i = 0; i < runs; i++) {
try {
const start = Date.now();
execFileSync(process.execPath, [bin, dir, '--output', output], { stdio: 'ignore' });
times.push(Date.now() - start);
} catch (e) {
console.error(`Run ${i + 1}/${runs} failed: ${e.message}`);
}
}

if (times.length === 0) {
console.error('All benchmark runs failed');
process.exit(1);
}

times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
const q1 = times[Math.floor(times.length * 0.25)];
const q3 = times[Math.floor(times.length * 0.75)];
const iqr = q3 - q1;
Comment thread
yamadashy marked this conversation as resolved.
Comment thread
yamadashy marked this conversation as resolved.

const osName = process.env.RUNNER_OS;

// Output in customSmallerIsBetter format for github-action-benchmark
const result = [
{
name: `Repomix Pack (${osName})`,
unit: 'ms',
value: median,
range: `±${iqr}`,
extra: `Median of ${times.length} runs\nQ1: ${q1}ms, Q3: ${q3}ms\nAll times: ${times.join(', ')}ms`,
},
];

writeFileSync(join(process.env.RUNNER_TEMP, 'bench-result.json'), JSON.stringify(result));
console.log(`${osName}: median=${median}ms (±${iqr}ms)`);
69 changes: 69 additions & 0 deletions .github/scripts/perf-benchmark/bench-comment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { esc, extractHistory, renderHistory } from './bench-utils.mjs';

const shortSha = process.env.COMMIT_SHA.slice(0, 7);
Comment thread
yamadashy marked this conversation as resolved.
const commitMsg = process.env.COMMIT_MSG;
const runUrl = process.env.WORKFLOW_RUN_URL;
const oldBody = readFileSync(`${process.env.RUNNER_TEMP}/old-comment.txt`, 'utf8');

const history = extractHistory(oldBody);

// Read benchmark results from artifacts
function readResult(os) {
const file = join('results', `bench-result-${os}`, 'bench-result.json');
try {
return JSON.parse(readFileSync(file, 'utf8'));
} catch {
return null;
}
Comment thread
yamadashy marked this conversation as resolved.
}

function formatResult(data) {
if (!data) return '-';
const prSec = (data.pr / 1000).toFixed(2);
const mainSec = (data.main / 1000).toFixed(2);
const prIqr = (data.prIqr / 1000).toFixed(2);
const mainIqr = (data.mainIqr / 1000).toFixed(2);
const diff = data.pr - data.main;
const diffSec = `${diff >= 0 ? '+' : ''}${(diff / 1000).toFixed(2)}`;
const diffPct = data.main > 0 ? `${diff >= 0 ? '+' : ''}${((diff / data.main) * 100).toFixed(1)}` : 'N/A';
return `${mainSec}s (\u00b1${mainIqr}s) \u2192 ${prSec}s (\u00b1${prIqr}s) \u00b7 ${diffSec}s (${diffPct}%)`;
}

const ubuntuStr = formatResult(readResult('ubuntu-latest'));
const macosStr = formatResult(readResult('macos-latest'));
const windowsStr = formatResult(readResult('windows-latest'));

const jsonComment = `<!-- bench-history-json-start ${JSON.stringify(history)} bench-history-json-end -->`;
let body = `<!-- repomix-perf-benchmark -->\n${jsonComment}\n`;
body += '## \u26a1 Performance Benchmark\n\n';
body += `<table><tr><td><strong>Latest commit:</strong></td><td><code>${shortSha}</code> ${esc(commitMsg)}</td></tr>\n`;
body += `<tr><td><strong>Status:</strong></td><td>\u2705 Benchmark complete!</td></tr>\n`;
body += `<tr><td><strong>Ubuntu:</strong></td><td>${ubuntuStr}</td></tr>\n`;
body += `<tr><td><strong>macOS:</strong></td><td>${macosStr}</td></tr>\n`;
body += `<tr><td><strong>Windows:</strong></td><td>${windowsStr}</td></tr>\n`;
body += '</table>\n\n';
body += '<details>\n<summary>Details</summary>\n\n';
body += '- Packing the repomix repository with `node bin/repomix.cjs`\n';
body += '- Warmup: 2 runs (discarded), interleaved execution\n';
body += '- Measurement: 20 runs / 30 on macOS (median \u00b1 IQR)\n';
body += `- [Workflow run](${runUrl})\n\n`;
body += '</details>';

const historyHtml = renderHistory(history);
if (historyHtml) {
body += `\n\n<details>\n<summary>History</summary>\n\n${historyHtml}\n\n</details>`;
}

// Write to step summary (without HTML comments)
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
if (summaryFile) {
const summaryBody = body
.split('\n')
.filter((l) => !l.startsWith('<!-- '))
.join('\n');
appendFileSync(summaryFile, `${summaryBody}\n`);
}

writeFileSync(`${process.env.RUNNER_TEMP}/new-comment.md`, body);
45 changes: 45 additions & 0 deletions .github/scripts/perf-benchmark/bench-pending.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { esc, extractHistory, renderHistory } from './bench-utils.mjs';

const shortSha = process.env.COMMIT_SHA.slice(0, 7);
Comment thread
yamadashy marked this conversation as resolved.
const commitMsg = process.env.COMMIT_MSG;
const runUrl = process.env.WORKFLOW_RUN_URL;
const oldBody = readFileSync(`${process.env.RUNNER_TEMP}/old-comment.txt`, 'utf8');

let history = extractHistory(oldBody);

// If previous comment had completed results, archive them into history
if (oldBody.includes('\u2705 Benchmark complete!')) {
// Extract only the main result table (before any <details> block)
const mainSection = oldBody.split('<details>')[0] || '';
const commitMatch = mainSection.match(/Latest commit:<\/strong><\/td><td><code>([a-f0-9]+)<\/code>\s*(.*?)<\/td>/);
const prevSha = commitMatch ? commitMatch[1] : '';
const prevMsg = commitMatch ? commitMatch[2] : '';
if (prevSha) {
const rowRe = /<tr><td><strong>(Ubuntu|macOS|Windows):<\/strong><\/td><td>(.*?)<\/td><\/tr>/g;
const entry = { sha: prevSha, msg: prevMsg };
for (let m = rowRe.exec(mainSection); m !== null; m = rowRe.exec(mainSection)) {
entry[m[1].toLowerCase()] = m[2];
}
if (prevSha !== shortSha && !history.some((h) => h.sha === prevSha)) {
history.unshift(entry);
}
}
}

// Keep max 50 entries
history = history.slice(0, 50);

const jsonComment = `<!-- bench-history-json-start ${JSON.stringify(history)} bench-history-json-end -->`;
let body = `<!-- repomix-perf-benchmark -->\n${jsonComment}\n`;
body += '## \u26a1 Performance Benchmark\n\n';
body += `<table><tr><td><strong>Latest commit:</strong></td><td><code>${shortSha}</code> ${esc(commitMsg)}</td></tr>\n`;
body += '<tr><td><strong>Status:</strong></td><td>\u26a1 Benchmark in progress...</td></tr></table>\n\n';
body += `[Workflow run](${runUrl})`;

const historyHtml = renderHistory(history);
if (historyHtml) {
body += `\n\n<details>\n<summary>History</summary>\n\n${historyHtml}\n\n</details>`;
}

writeFileSync(`${process.env.RUNNER_TEMP}/new-comment.md`, body);
82 changes: 82 additions & 0 deletions .github/scripts/perf-benchmark/bench-run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const [prDir, mainDir] = process.argv.slice(2);
Comment thread
yamadashy marked this conversation as resolved.
const output = join(tmpdir(), 'repomix-bench-output.txt');
const runs = Number(process.env.BENCH_RUNS) || 20;

const prBin = join(prDir, 'bin', 'repomix.cjs');
const mainBin = join(mainDir, 'bin', 'repomix.cjs');

function run(bin, dir) {
const start = Date.now();
execFileSync(process.execPath, [bin, dir, '--output', output], { stdio: 'ignore' });
return Date.now() - start;
Comment thread
yamadashy marked this conversation as resolved.
}

// Warmup both branches to stabilize OS page cache and JIT
console.error('Warming up...');
for (let i = 0; i < 2; i++) {
try {
run(prBin, prDir);
} catch {}
try {
run(mainBin, mainDir);
} catch {}
}

// Interleaved measurement: alternate PR and main each iteration
// so both branches experience similar runner load conditions.
// Even/odd alternation neutralizes ordering bias from CPU/filesystem cache warming.
const prTimes = [];
const mainTimes = [];
for (let i = 0; i < runs; i++) {
console.error(`Run ${i + 1}/${runs}`);
if (i % 2 === 0) {
try {
prTimes.push(run(prBin, prDir));
} catch (e) {
console.error(`PR run ${i + 1} failed: ${e.message}`);
}
try {
mainTimes.push(run(mainBin, mainDir));
} catch (e) {
console.error(`main run ${i + 1} failed: ${e.message}`);
}
} else {
try {
mainTimes.push(run(mainBin, mainDir));
} catch (e) {
console.error(`main run ${i + 1} failed: ${e.message}`);
}
try {
prTimes.push(run(prBin, prDir));
} catch (e) {
console.error(`PR run ${i + 1} failed: ${e.message}`);
}
}
}

if (prTimes.length === 0 || mainTimes.length === 0) {
console.error('All benchmark runs failed');
process.exit(1);
}

function stats(times) {
times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
const q1 = times[Math.floor(times.length * 0.25)];
const q3 = times[Math.floor(times.length * 0.75)];
return { median, iqr: q3 - q1 };
Comment thread
yamadashy marked this conversation as resolved.
}
Comment thread
yamadashy marked this conversation as resolved.

const pr = stats(prTimes);
const main = stats(mainTimes);

console.error(`PR median: ${pr.median}ms (±${pr.iqr}ms)`);
console.error(`main median: ${main.median}ms (±${main.iqr}ms)`);

const result = { pr: pr.median, prIqr: pr.iqr, main: main.median, mainIqr: main.iqr };
writeFileSync(join(process.env.RUNNER_TEMP, 'bench-result.json'), JSON.stringify(result));
38 changes: 38 additions & 0 deletions .github/scripts/perf-benchmark/bench-utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Escape HTML special characters for safe embedding in comments.
*/
export const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

/**
* Extract benchmark history JSON embedded in an HTML comment.
*/
export function extractHistory(body) {
const jsonMatch = body.match(/<!-- bench-history-json-start ([\s\S]*?) bench-history-json-end -->/);
if (!jsonMatch) return [];
try {
return JSON.parse(jsonMatch[1]);
} catch (e) {
console.error('Failed to parse benchmark history:', e);
return [];
}
}

/**
* Render history entries as collapsible HTML.
*/
export function renderHistory(hist) {
if (hist.length === 0) return '';
return hist
.map((h) => {
const label = `<code>${h.sha}</code>${h.msg ? ` ${h.msg}` : ''}`;
const osRows = ['ubuntu', 'macos', 'windows']
.filter((os) => h[os] && h[os] !== '-')
.map((os) => {
const osLabel = os === 'ubuntu' ? 'Ubuntu' : os === 'macos' ? 'macOS' : 'Windows';
return `<tr><td><strong>${osLabel}:</strong></td><td>${h[os]}</td></tr>`;
})
.join('\n');
return `${label}\n<table>\n${osRows}\n</table>`;
})
.join('\n\n');
}
56 changes: 4 additions & 52 deletions .github/workflows/perf-benchmark-history.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
runs: 10
- os: macos-latest
runs: 20
- os: macos-latest
runs: 30
- os: windows-latest
runs: 10
runs: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand All @@ -47,55 +47,7 @@ jobs:
shell: bash
env:
BENCH_RUNS: ${{ matrix.runs }}
run: |
cat > "$RUNNER_TEMP/benchmark.mjs" << 'BENCHSCRIPT'
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';

const dir = process.argv[2];
const output = join(tmpdir(), 'repomix-bench-output.txt');
const runs = Number(process.env.BENCH_RUNS) || 10;
const bin = join(dir, 'bin', 'repomix.cjs');

// Warmup runs to stabilize OS page cache and JIT
for (let i = 0; i < 2; i++) {
try {
execFileSync(process.execPath, [bin, dir, '--output', output], { stdio: 'ignore' });
} catch {}
}

// Measurement runs
const times = [];
for (let i = 0; i < runs; i++) {
const start = Date.now();
execFileSync(process.execPath, [bin, dir, '--output', output], { stdio: 'ignore' });
times.push(Date.now() - start);
}

times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
const q1 = times[Math.floor(times.length * 0.25)];
const q3 = times[Math.floor(times.length * 0.75)];
const iqr = q3 - q1;

const osName = process.env.RUNNER_OS;

// Output in customSmallerIsBetter format for github-action-benchmark
const result = [{
name: `Repomix Pack (${osName})`,
unit: 'ms',
value: median,
range: '±' + iqr,
extra: `Median of ${runs} runs\nQ1: ${q1}ms, Q3: ${q3}ms\nAll times: ${times.join(', ')}ms`
}];

writeFileSync(join(process.env.RUNNER_TEMP, 'bench-result.json'), JSON.stringify(result));
console.log(`${osName}: median=${median}ms (±${iqr}ms)`);
BENCHSCRIPT

node "$RUNNER_TEMP/benchmark.mjs" "$GITHUB_WORKSPACE"
run: node .github/scripts/perf-benchmark-history/bench-run.mjs "$GITHUB_WORKSPACE"

- name: Upload benchmark result
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
Expand Down
Loading
Loading