const fs = require('fs');
const path = require('path');
function loadTimingDataFromFile() {
try {
const criterionPath = 'target/criterion';
if (!fs.existsSync(criterionPath)) {
console.error("No Criterion benchmark data found. Please run benchmarks first.");
throw new Error("No Criterion benchmark data found. Please run benchmarks first.");
}
console.log(`Loading timing data from ${criterionPath}...`);
const implementations = fs.readdirSync(criterionPath)
.filter(name => !name.startsWith('.') && name !== 'report');
const timingData = [];
for (const impl of implementations) {
const implPath = path.join(criterionPath, impl);
const implStats = fs.statSync(implPath);
if (implStats.isDirectory()) {
const operations = fs.readdirSync(implPath)
.filter(name => !name.startsWith('.'));
for (const op of operations) {
const opPath = path.join(implPath, op, 'new', 'estimates.json');
if (fs.existsSync(opPath)) {
try {
const content = fs.readFileSync(opPath, 'utf8');
const data = JSON.parse(content);
const meanNs = data.mean.point_estimate;
const medianNs = data.median.point_estimate;
timingData.push({
event: 'bench',
name: `benchmarks::${impl}::${op}`,
implementation: impl,
operation: op,
mean: meanNs,
median: medianNs
});
} catch (parseError) {
console.warn(`Failed to parse ${opPath}: ${parseError.message}`);
}
}
}
}
}
if (timingData.length > 0) {
console.log(`Parsed ${timingData.length} benchmark results from Criterion data`);
return timingData;
} else {
console.error("No valid benchmark data found in Criterion output");
throw new Error("No valid benchmark data found in Criterion output");
}
} catch (error) {
console.error("Error loading timing data:", error.message);
throw error;
}
}
function parseCriterionOutput(content) {
const timingData = [];
const lines = content.split('\n');
let currentBenchmark = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (trimmedLine.includes('/') && !trimmedLine.includes('time:') && !trimmedLine.includes('change:') && !trimmedLine.includes('slope') && !trimmedLine.includes('mean') && !trimmedLine.includes('median')) {
const match = trimmedLine.match(/^([^/]+)\/(.+)$/);
if (match) {
currentBenchmark = {
name: `benchmarks::${match[1]}::${match[2]}`,
implementation: match[1],
operation: match[2]
};
}
}
if (currentBenchmark && trimmedLine.startsWith('time:')) {
const timeMatch = trimmedLine.match(/time:\s*\[([^\]]+)\]/);
if (timeMatch) {
const timeStr = timeMatch[1];
const timeValues = [];
const parts = timeStr.split(/\s+/).filter(s => s.trim());
for (let j = 0; j < parts.length; j += 2) {
if (j + 1 < parts.length) {
timeValues.push(parts[j] + ' ' + parts[j + 1]);
}
}
if (timeValues.length >= 3) {
const medianTime = timeValues[1];
const ns = parseTimeToNanoseconds(medianTime);
if (ns > 0) {
timingData.push({
event: 'bench',
name: currentBenchmark.name,
implementation: currentBenchmark.implementation,
operation: currentBenchmark.operation,
mean: ns,
median: ns
});
}
}
}
}
if (trimmedLine.includes('/') && trimmedLine.includes('time:')) {
const nameMatch = trimmedLine.match(/^([^/]+\/[^\s]+)\s+time:\s*\[([^\]]+)\]/);
if (nameMatch) {
const nameParts = nameMatch[1].split('/');
const timeStr = nameMatch[2];
currentBenchmark = {
name: `benchmarks::${nameParts[0]}::${nameParts[1]}`,
implementation: nameParts[0],
operation: nameParts[1]
};
const timeValues = [];
const parts = timeStr.split(/\s+/).filter(s => s.trim());
for (let j = 0; j < parts.length; j += 2) {
if (j + 1 < parts.length) {
timeValues.push(parts[j] + ' ' + parts[j + 1]);
}
}
if (timeValues.length >= 3) {
const medianTime = timeValues[1];
const ns = parseTimeToNanoseconds(medianTime);
if (ns > 0) {
timingData.push({
event: 'bench',
name: currentBenchmark.name,
implementation: currentBenchmark.implementation,
operation: currentBenchmark.operation,
mean: ns,
median: ns
});
}
}
}
}
}
return timingData;
}
function parseTimeToNanoseconds(timeStr) {
const trimmed = timeStr.trim();
const match = trimmed.match(/^([0-9.]+)\s*([a-zA-Zµ]+)$/);
if (!match) {
return 0;
}
const numericValue = parseFloat(match[1]);
const unit = match[2];
if (isNaN(numericValue)) {
return 0;
}
if (unit === 'ns') {
return numericValue;
} else if (unit === 'ps') {
return numericValue / 1000;
} else if (unit === 'µs' || unit === 'us') {
return numericValue * 1000;
} else if (unit === 'ms') {
return numericValue * 1000000;
} else if (unit === 's') {
return numericValue * 1000000000;
}
return 0;
}
function parseBenchmarkResults(timingData) {
if (!timingData || timingData.length === 0) {
return [];
}
const results = [];
for (const benchmark of timingData) {
const nameParts = benchmark.name.split('::');
if (nameParts.length >= 3) {
const groupName = nameParts[1]; const operation = nameParts[2];
if (groupName === 'Comparison') {
continue;
}
const meanNs = benchmark.mean || 0;
const medianNs = benchmark.median || 0;
if (meanNs > 0 && medianNs > 0) {
results.push({
implementation: groupName,
operation: operation,
mean: meanNs,
median: medianNs
});
}
}
}
const uniqueResults = [];
const seen = new Set();
results.sort((a, b) => a.mean - b.mean);
for (const result of results) {
const key = `${result.implementation}-${result.operation}`;
if (!seen.has(key)) {
seen.add(key);
uniqueResults.push(result);
}
}
return uniqueResults.sort((a, b) => {
if (a.implementation === b.implementation) {
return a.operation.localeCompare(b.operation);
}
return a.implementation.localeCompare(b.implementation);
});
}
function generatePerformanceComparisonChart(results) {
const meaningfulResults = results.filter(r => r.mean > 0.001);
if (meaningfulResults.length === 0) {
return '```mermaid\nxychart-beta\n title "No benchmark data available"\n x-axis "No data" ["No data"]\n y-axis "Time" 0 --> 1\n BAR [0]\n```';
}
const implementations = [...new Set(meaningfulResults.map(r => r.implementation))];
const operationGroups = new Map();
for (const result of meaningfulResults) {
if (!operationGroups.has(result.operation)) {
operationGroups.set(result.operation, []);
}
operationGroups.get(result.operation).push(result);
}
const validOperations = [];
for (const [operation, results] of operationGroups) {
if (results.length >= 2) {
validOperations.push(operation);
}
}
if (validOperations.length === 0) {
return '```mermaid\nxychart-beta\n title "No benchmark data available"\n x-axis "No data" ["No data"]\n y-axis "Time" 0 --> 1\n BAR [0]\n```';
}
let chart = '```mermaid\nxychart-beta\n';
chart += ' title "Performance Comparison: QuickBool vs LazyLock vs OnceLock"\n';
chart += ' x-axis "Operation" [';
chart += validOperations.map(op => `"${op}"`).join(', ');
chart += ']\n';
const maxValue = Math.max(...meaningfulResults.map(r => r.mean));
const yMax = Math.ceil(maxValue * 1.2);
chart += ` y-axis "Time (nanoseconds)" 0 --> ${yMax}\n`;
for (const impl of implementations) {
const data = validOperations.map(op => {
const result = meaningfulResults.find(r =>
r.implementation === impl && r.operation === op);
if (result && result.mean > 0.001) {
return result.mean.toFixed(3);
}
return null;
});
const validData = data.filter(d => d !== null);
if (validData.length >= validOperations.length * 0.5) { const cleanData = data.map(d => d || '0.001'); chart += ` LINE [${cleanData.join(', ')}] "${impl}"\n`;
}
}
chart += '```';
return chart;
}
function generateFirstAccessChart(results) {
const firstAccess = results.filter(r => r.operation.includes('first') && r.mean > 0.001);
if (firstAccess.length === 0) {
return '```mermaid\nxychart-beta\n title "No first access data available"\n x-axis "No data" ["No data"]\n y-axis "Time" 0 --> 1\n BAR [0]\n```';
}
let chart = '```mermaid\nxychart-beta\n';
chart += ' title "First Access Performance Comparison"\n';
chart += ' x-axis "Implementation" [';
chart += firstAccess.map(r => `"${r.implementation}"`).join(', ');
chart += ']\n';
const maxValue = Math.max(...firstAccess.map(r => r.mean));
const yMax = Math.ceil(maxValue * 1.2);
chart += ` y-axis "Time (nanoseconds)" 0 --> ${yMax}\n`;
const data = firstAccess.map(r => r.mean.toFixed(3));
chart += ` BAR [${data.join(', ')}]\n`;
chart += '```';
return chart;
}
function generateCachedAccessChart(results) {
const cachedAccess = results.filter(r => r.operation.includes('cached') && r.mean > 0.001);
if (cachedAccess.length === 0) {
return '```mermaid\nxychart-beta\n title "No cached access data available"\n x-axis "No data" ["No data"]\n y-axis "Time" 0 --> 1\n BAR [0]\n```';
}
const bestCached = [];
const seen = new Set();
for (const result of cachedAccess) {
if (!seen.has(result.implementation)) {
seen.add(result.implementation);
bestCached.push(result);
}
}
let chart = '```mermaid\nxychart-beta\n';
chart += ' title "Cached Access Performance"\n';
chart += ' x-axis "Implementation" [';
chart += bestCached.map(r => `"${r.implementation}"`).join(', ');
chart += ']\n';
const maxValue = Math.max(...bestCached.map(r => r.mean));
const yMax = Math.ceil(maxValue * 1.2);
chart += ` y-axis "Time (nanoseconds)" 0 --> ${yMax}\n`;
const data = bestCached.map(r => r.mean.toFixed(3));
chart += ` BAR [${data.join(', ')}]\n`;
chart += '```';
return chart;
}
function main() {
console.log("Loading existing benchmark timing data...");
const timingData = loadTimingDataFromFile();
if (!timingData || timingData.length === 0) {
console.error("No benchmark timing data found");
return;
}
console.log(`Found ${timingData.length} benchmark timing records`);
const results = parseBenchmarkResults(timingData);
if (!results || results.length === 0) {
console.error("No benchmark results found");
return;
}
console.log(`Parsed ${results.length} benchmark results`);
const outputDir = "tmp/md";
fs.mkdirSync(outputDir, { recursive: true });
console.log("Generating Mermaid charts...");
const charts = {
firstAccess: generateFirstAccessChart(results),
cachedAccess: generateCachedAccessChart(results)
};
fs.writeFileSync('tmp/md/first_access_chart.md', charts.firstAccess);
fs.writeFileSync('tmp/md/cached_access_chart.md', charts.cachedAccess);
console.log("Mermaid charts generated successfully!");
}
main();