appblocks
Version:
A lightweight javascript library for building micro apps for the front-end.
151 lines (128 loc) • 5.14 kB
JavaScript
const fs = require('fs');
const path = require('path');
const BASE_DIR = path.resolve(process.cwd(), '.benchmarks');
const BASELINE_FILE = path.join(BASE_DIR, 'baseline.json');
function ensureDir() {
if (!fs.existsSync(BASE_DIR)) {
fs.mkdirSync(BASE_DIR, { recursive: true });
}
}
function initBaseline(baseline = { mean: null, samples: [] }) {
ensureDir();
fs.writeFileSync(BASELINE_FILE, JSON.stringify(baseline, null, 2), 'utf8');
return BASELINE_FILE;
}
/**
* runBenchmark(scenarioFn, runs = 10)
* scenarioFn: async function that performs a single render and returns timing in milliseconds
* returns: { samples: number[], mean: number, baseline: object|null, comparison: { abs, pct }|null }
*/
async function runBenchmark(scenarioFn, runs = 10) {
if (typeof scenarioFn !== 'function') throw new Error('scenarioFn must be a function');
const samples = [];
for (let i = 0; i < runs; i++) {
const start = Date.now();
// allow scenarioFn to be sync or return a promise
await Promise.resolve().then(() => scenarioFn(i));
const end = Date.now();
samples.push(end - start);
}
const mean = samples.reduce((a, b) => a + b, 0) / samples.length;
let baseline = null;
if (fs.existsSync(BASELINE_FILE)) {
try {
baseline = JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
} catch (err) {
baseline = null;
}
}
// Compute median for outlier detection
const sorted = samples.slice().sort((a,b) => a-b);
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
// Detect outliers: samples > 200% of median
const outliers = samples.filter(s => median > 0 ? s > (2 * median) : false);
let comparison = null;
if (baseline && typeof baseline.mean === 'number' && baseline.mean > 0) {
const abs = mean - baseline.mean;
const pct = (abs / baseline.mean) * 100;
comparison = { abs, pct };
}
// If no baseline exists, initialize one with current measurements
if (!baseline) {
try {
initBaseline({ mean, samples, date: new Date().toISOString() });
baseline = { mean, samples, date: new Date().toISOString() };
} catch (err) {
// ignore write errors
}
}
// Build human-readable report
let report = `Mean ${mean.toFixed(2)}ms`;
if (comparison) {
const sign = comparison.abs >= 0 ? '+' : '-';
report += ` (${sign}${Math.abs(comparison.pct).toFixed(1)}% vs baseline ${baseline.mean}ms)`;
} else {
report += ' (baseline created)';
}
if (outliers.length > 0) {
report += `; outliers: ${outliers.length}/${samples.length}`;
}
// Soft regression warning (>10% mean increase)
if (comparison && comparison.pct > 10) {
const warnMsg = `Benchmark regression: mean increased by ${comparison.pct.toFixed(1)}% vs baseline`;
try { console.warn(warnMsg); } catch (err) {}
report += `; WARNING: ${warnMsg}`;
}
return { samples, mean, median, outliers, baseline, comparison, report };
}
module.exports = { ensureDir, initBaseline, runBenchmark };
// Build a simple scenario template string used by the benchmark. Exported for tests.
function buildScenarioTemplate() {
// Includes: two placeholders, one c-if, one c-for, and a filter marker `|` for filtered placeholders
return `
<div>
<div c-if="data.show">{data.value|upper}</div>
<ul>
<li c-for="item in items">{item|trim}</li>
</ul>
<div>{data.other}</div>
</div>`;
}
module.exports.buildScenarioTemplate = buildScenarioTemplate;
// Build a scenario template with 20 conditional directives for performance testing
function buildConditionalScenarioTemplate() {
let html = '<div>';
for (let i = 0; i < 20; i++) {
html += `<div c-if="data.show${i}">{data.value${i}}</div>`;
}
html += '</div>';
return html;
}
module.exports.buildConditionalScenarioTemplate = buildConditionalScenarioTemplate;
// Build a scenario template with method calls in placeholders (nested, filtered, whitespace)
function buildMethodCallScenarioTemplate() {
// Realistic method call pattern: nested method call with filters and whitespace
// This tests: nested argument parsing, filter chaining, whitespace tolerance
return `
<div>
<div>{transformMethod( rangeMethod( 1, 5 ) )|upperCase}</div>
<div>{calculateSum( data.x, data.y )|minusOne}</div>
<ul>
<li c-for="item in rangeMethod(data.start, data.end)">{item|trim}</li>
</ul>
</div>`;
}
module.exports.buildMethodCallScenarioTemplate = buildMethodCallScenarioTemplate;
// Build a scenario template with deeply nested method calls (≥5 levels)
// Tests recursive argument parsing and call stacking
function buildDeepNestingScenarioTemplate() {
// Pattern: methodA(methodB(methodC(methodD(methodE(data.prop)))))
// Tests nested parenthesis parsing and call stack depth
return `
<div>
<div>{methodA(methodB(methodC(methodD(methodE(data.value)))))}</div>
<div>{methodA(methodB(methodC(methodD(methodE(data.value)))))}</div>
</div>`;
}
module.exports.buildDeepNestingScenarioTemplate = buildDeepNestingScenarioTemplate;