import { execSync } from 'node:child_process'; import { Bench } from 'tinybench'; import Handlebars from '../../lib/index.js'; import { templates as allTemplates } from './templates.js'; import { printResults, printSectionHeader, saveMarkdownReport, } from './report.js'; // ─── Configuration ─────────────────────────────────────────────────────────── // Compilation happens once at app startup — low warmup reflects real-world conditions. // Execution happens thousands of times — higher warmup lets V8 optimize hot paths. const COMPILE_BENCH_CONFIG = { warmupIterations: 10, iterations: 1000, time: 3000, }; const EXEC_BENCH_CONFIG = { warmupIterations: 500, iterations: 5000, time: 3000, }; // ─── CLI Args ──────────────────────────────────────────────────────────────── function getGitBranch() { try { return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', }).trim(); } catch { return null; } } const args = process.argv.slice(2); const labelIdx = args.indexOf('--label'); const label = labelIdx !== -1 ? args[labelIdx + 1] : getGitBranch(); const grepIdx = args.indexOf('--grep'); let grepPattern = null; if (grepIdx !== -1 && args[grepIdx + 1]) { try { grepPattern = new RegExp(args[grepIdx + 1], 'i'); } catch (e) { console.error(`Invalid --grep pattern: ${e.message}`); process.exit(1); } } const sectionIdx = args.indexOf('--section'); let sectionPattern = null; if (sectionIdx !== -1 && args[sectionIdx + 1]) { try { sectionPattern = new RegExp(args[sectionIdx + 1], 'i'); } catch (e) { console.error(`Invalid --section pattern: ${e.message}`); process.exit(1); } } // ─── Filter templates ─────────────────────────────────────────────────────── const templates = Object.fromEntries( Object.entries(allTemplates).filter( ([name]) => !grepPattern || grepPattern.test(name) ) ); if (Object.keys(templates).length === 0) { console.error( `No templates match --grep ${grepPattern}. Available: ${Object.keys(allTemplates).join(', ')}` ); process.exit(1); } if (grepPattern) { console.log( `Filtering templates: ${Object.keys(templates).length}/${Object.keys(allTemplates).length} match /${grepPattern.source}/i` ); console.log(); } // ─── Bench helpers ─────────────────────────────────────────────────────────── function newBench(config) { return new Bench(config); } function createEnv(def) { const hb = Handlebars.create(); if (def.helpers) { hb.registerHelper(def.helpers); } if (def.partials) { for (const [name, tpl] of Object.entries(def.partials)) { hb.registerPartial(name, tpl); } } return hb; } function shouldRunSection(title) { if (!sectionPattern) return true; return sectionPattern.test(title); } async function runSection(title, config, setup) { if (!shouldRunSection(title)) return null; printSectionHeader(title); const bench = newBench(config); setup(bench); await bench.run(); printResults(bench); console.log(); return { title, bench }; } // ─── Main ──────────────────────────────────────────────────────────────────── async function run() { const now = new Date(); const headerLabel = label ? ` [${label}]` : ''; console.log(`Handlebars Performance Benchmark${headerLabel}`); console.log('================================'); console.log( `tinybench | compile: warmup ${COMPILE_BENCH_CONFIG.warmupIterations}, min ${COMPILE_BENCH_CONFIG.iterations} | exec: warmup ${EXEC_BENCH_CONFIG.warmupIterations}, min ${EXEC_BENCH_CONFIG.iterations} | time: ${EXEC_BENCH_CONFIG.time}ms per bench` ); console.log(`Node ${process.version} | ${process.platform} ${process.arch}`); console.log(); const allSections = []; if (sectionPattern) { console.log(`Filtering sections: /${sectionPattern.source}/i`); console.log(); } // ─── COMPILATION ──────────────────────────────────────────────────────────── allSections.push( await runSection( 'COMPILATION (Handlebars.compile + first render)', COMPILE_BENCH_CONFIG, (bench) => { for (const [name, def] of Object.entries(templates)) { const hb = createEnv(def); // Handlebars.compile() uses lazy evaluation — actual compilation // (parsing, codegen, new Function()) only happens on first invocation. bench.add(`compile: ${name}`, () => { hb.compile(def.template)({}); }); } } ) ); // ─── EXECUTION + output verification ──────────────────────────────────────── const expectedOutputs = {}; const execResult = await runSection( 'EXECUTION (template rendering)', EXEC_BENCH_CONFIG, (bench) => { for (const [name, def] of Object.entries(templates)) { const compiled = createEnv(def).compile(def.template); expectedOutputs[name] = compiled(def.context); bench.add(`exec: ${name}`, () => { compiled(def.context); }); } } ); allSections.push(execResult); // Verify outputs haven't changed during benchmarking if (execResult) { let verifyFails = 0; for (const [name, def] of Object.entries(templates)) { const compiled = createEnv(def).compile(def.template); const actual = compiled(def.context); if (actual !== expectedOutputs[name]) { console.warn( ` WARNING: output mismatch for "${name}"\n expected: ${JSON.stringify(expectedOutputs[name])}\n actual: ${JSON.stringify(actual)}` ); verifyFails++; } } if (verifyFails === 0) { console.log('Output verification: all templates OK'); } else { console.warn(`Output verification: ${verifyFails} mismatch(es)`); } console.log(); } // ─── PRECOMPILATION ───────────────────────────────────────────────────────── allSections.push( await runSection( 'PRECOMPILATION (Handlebars.precompile)', COMPILE_BENCH_CONFIG, (bench) => { for (const [name, def] of Object.entries(templates)) { bench.add(`precompile: ${name}`, () => { Handlebars.precompile(def.template); }); } } ) ); // ─── END-TO-END ───────────────────────────────────────────────────────────── allSections.push( await runSection( 'END-TO-END (compile + render)', COMPILE_BENCH_CONFIG, (bench) => { for (const [name, def] of Object.entries(templates)) { const hb = createEnv(def); bench.add(`e2e: ${name}`, () => { const fn = hb.compile(def.template); fn(def.context); }); } } ) ); // ─── COMPILE OPTIONS ─────────────────────────────────────────────────────── allSections.push( await runSection( 'COMPILE OPTIONS COMPARISON', EXEC_BENCH_CONFIG, (bench) => { const src = allTemplates['complex (if/each/helpers)'].template; const ctx = allTemplates['complex (if/each/helpers)'].context; const defaultFn = Handlebars.compile(src); bench.add('exec: default options', () => defaultFn(ctx)); const noEscapeFn = Handlebars.compile(src, { noEscape: true }); bench.add('exec: noEscape=true', () => noEscapeFn(ctx)); const strictFn = Handlebars.compile(src, { strict: true, assumeObjects: true, }); bench.add('exec: strict + assumeObjects', () => strictFn(ctx)); const knownFn = Handlebars.compile(src, { knownHelpers: { if: true, each: true }, knownHelpersOnly: false, }); bench.add('exec: knownHelpers', () => knownFn(ctx)); const compatFn = Handlebars.compile(src, { compat: true }); bench.add('exec: compat=true', () => compatFn(ctx)); const noDataFn = Handlebars.compile(src, { data: false }); bench.add('exec: data=false', () => noDataFn(ctx)); } ) ); const filepath = saveMarkdownReport(allSections.filter(Boolean), { label, compileConfig: COMPILE_BENCH_CONFIG, execConfig: EXEC_BENCH_CONFIG, date: now, }); console.log(`Results saved to: ${filepath}`); } run().catch((err) => { console.error(err); process.exit(1); });