@@ -0,0 +1,250 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
writeFileSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
import { formatOps } from './report.mjs';
|
||||
|
||||
// ─── Resolve files ───────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let baselinePath, currentPath;
|
||||
|
||||
if (args.length >= 2) {
|
||||
[baselinePath, currentPath] = args;
|
||||
} else {
|
||||
const resultsDir = join(__dirname, 'results');
|
||||
let files;
|
||||
try {
|
||||
files = readdirSync(resultsDir)
|
||||
.filter((f) => f.endsWith('.md') && f.startsWith('bench-'))
|
||||
.map((f) => join(resultsDir, f));
|
||||
} catch {
|
||||
files = [];
|
||||
}
|
||||
|
||||
if (files.length < 2) {
|
||||
console.error(
|
||||
files.length === 0
|
||||
? 'No benchmark results found in bench/results/.'
|
||||
: 'Only one benchmark result found. Run benchmarks on another branch first.'
|
||||
);
|
||||
console.error('');
|
||||
console.error('Usage: node bench/compare.mjs [<baseline.md> <current.md>]');
|
||||
console.error('');
|
||||
console.error(
|
||||
'With no arguments, auto-selects the two most recent results.'
|
||||
);
|
||||
console.error('A result labelled "main" is always used as the baseline.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find the "main" labelled file if any
|
||||
const mainFile = files.find((f) => {
|
||||
const content = readFileSync(f, 'utf8');
|
||||
const match = content.match(/^- \*\*Label:\*\* (.+)$/m);
|
||||
return match && match[1] === 'main';
|
||||
});
|
||||
|
||||
if (mainFile) {
|
||||
// Baseline is main, current is the most recent non-main file
|
||||
baselinePath = mainFile;
|
||||
const others = files
|
||||
.filter((f) => f !== mainFile)
|
||||
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
||||
currentPath = others[0];
|
||||
} else {
|
||||
// Sort by mtime, older = baseline, newer = current
|
||||
files.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
|
||||
baselinePath = files[files.length - 2];
|
||||
currentPath = files[files.length - 1];
|
||||
}
|
||||
|
||||
console.log(`Auto-selected files:`);
|
||||
console.log(` baseline: ${basename(baselinePath)}`);
|
||||
console.log(` current: ${basename(currentPath)}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ─── Parse ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function parseOps(str) {
|
||||
str = str.trim();
|
||||
if (str.endsWith('M')) return parseFloat(str) * 1_000_000;
|
||||
if (str.endsWith('K')) return parseFloat(str) * 1_000;
|
||||
return parseFloat(str);
|
||||
}
|
||||
|
||||
function parseNs(str) {
|
||||
str = str.trim();
|
||||
if (str.endsWith('ms')) return parseFloat(str) * 1_000_000;
|
||||
if (str.endsWith('µs')) return parseFloat(str) * 1_000;
|
||||
if (str.endsWith('ns')) return parseFloat(str);
|
||||
return parseFloat(str);
|
||||
}
|
||||
|
||||
function parseReport(filepath) {
|
||||
const content = readFileSync(filepath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let label = null;
|
||||
const labelMatch = content.match(/^- \*\*Label:\*\* (.+)$/m);
|
||||
if (labelMatch) label = labelMatch[1];
|
||||
|
||||
const sections = {};
|
||||
let currentSection = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('## ')) {
|
||||
currentSection = line.slice(3).trim();
|
||||
sections[currentSection] = {};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!currentSection ||
|
||||
!line.startsWith('|') ||
|
||||
line.startsWith('|---') ||
|
||||
line.startsWith('| Benchmark')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cells = line
|
||||
.split('|')
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean);
|
||||
if (cells.length < 8) continue;
|
||||
|
||||
const [name, opsSec, avg, p50, p75, p99, rme, samples] = cells;
|
||||
sections[currentSection][name] = {
|
||||
opsSec: parseOps(opsSec),
|
||||
avg: parseNs(avg),
|
||||
p50: parseNs(p50),
|
||||
p75: parseNs(p75),
|
||||
p99: parseNs(p99),
|
||||
rme: parseFloat(rme),
|
||||
samples: parseInt(samples, 10),
|
||||
};
|
||||
}
|
||||
|
||||
return { label, sections };
|
||||
}
|
||||
|
||||
// ─── Compare ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function pctChange(baseline, current) {
|
||||
if (baseline === 0) return 0;
|
||||
return ((current - baseline) / baseline) * 100;
|
||||
}
|
||||
|
||||
function formatPct(pct) {
|
||||
const sign = pct > 0 ? '+' : '';
|
||||
return `${sign}${pct.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function indicator(pct, higherIsBetter) {
|
||||
const effective = higherIsBetter ? pct : -pct;
|
||||
if (effective > 5) return ' !!';
|
||||
if (effective > 2) return ' !';
|
||||
if (effective < -5) return ' !!';
|
||||
if (effective < -2) return ' !';
|
||||
return '';
|
||||
}
|
||||
|
||||
// ─── Output ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const baseline = parseReport(baselinePath);
|
||||
const current = parseReport(currentPath);
|
||||
|
||||
const baseLabel = baseline.label || 'baseline';
|
||||
const curLabel = current.label || 'current';
|
||||
|
||||
console.log(`Benchmark Comparison: ${baseLabel} vs ${curLabel}`);
|
||||
console.log('='.repeat(50));
|
||||
console.log(`Baseline: ${baselinePath}`);
|
||||
console.log(`Current: ${currentPath}`);
|
||||
console.log();
|
||||
console.log('Legend: ! = >2% change, !! = >5% change');
|
||||
console.log();
|
||||
|
||||
const mdLines = [];
|
||||
mdLines.push(`# Benchmark Comparison: ${baseLabel} vs ${curLabel}`);
|
||||
mdLines.push('');
|
||||
mdLines.push(`- **Baseline:** ${baseLabel} (${baselinePath})`);
|
||||
mdLines.push(`- **Current:** ${curLabel} (${currentPath})`);
|
||||
mdLines.push(`- **Legend:** ! = >2% change, !! = >5% change`);
|
||||
mdLines.push('');
|
||||
|
||||
const allSections = [
|
||||
...new Set([
|
||||
...Object.keys(baseline.sections),
|
||||
...Object.keys(current.sections),
|
||||
]),
|
||||
].sort();
|
||||
|
||||
for (const section of allSections) {
|
||||
const baseBenches = baseline.sections[section] || {};
|
||||
const curBenches = current.sections[section] || {};
|
||||
const allNames = [
|
||||
...new Set([...Object.keys(baseBenches), ...Object.keys(curBenches)]),
|
||||
].sort();
|
||||
|
||||
if (allNames.size === 0) continue;
|
||||
|
||||
console.log(`## ${section}`);
|
||||
console.log();
|
||||
|
||||
const header = `| Benchmark | ${baseLabel} ops/sec | ${curLabel} ops/sec | ops/sec | p75 latency |`;
|
||||
const sep = '|---|---|---|---|---|';
|
||||
|
||||
console.log(header);
|
||||
console.log(sep);
|
||||
|
||||
mdLines.push(`## ${section}`);
|
||||
mdLines.push('');
|
||||
mdLines.push(header);
|
||||
mdLines.push(sep);
|
||||
|
||||
for (const name of allNames) {
|
||||
const b = baseBenches[name];
|
||||
const c = curBenches[name];
|
||||
|
||||
if (!b || !c) {
|
||||
const row = `| ${name} | ${b ? formatOps(b.opsSec) : 'n/a'} | ${c ? formatOps(c.opsSec) : 'n/a'} | - | - |`;
|
||||
console.log(row);
|
||||
mdLines.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const opsPct = pctChange(b.opsSec, c.opsSec);
|
||||
const p75Pct = pctChange(b.p75, c.p75);
|
||||
|
||||
const opsStr = `${formatPct(opsPct)}${indicator(opsPct, true)}`;
|
||||
const p75Str = `${formatPct(p75Pct)}${indicator(p75Pct, false)}`;
|
||||
|
||||
const row = `| ${name} | ${formatOps(b.opsSec)} | ${formatOps(c.opsSec)} | ${opsStr} | ${p75Str} |`;
|
||||
console.log(row);
|
||||
mdLines.push(row);
|
||||
}
|
||||
|
||||
console.log();
|
||||
mdLines.push('');
|
||||
}
|
||||
|
||||
const resultsDir = join(__dirname, 'results');
|
||||
mkdirSync(resultsDir, { recursive: true });
|
||||
const curLabelSlug = curLabel.replace(/[^a-zA-Z0-9-_]/g, '_');
|
||||
const baseLabelSlug = baseLabel.replace(/[^a-zA-Z0-9-_]/g, '_');
|
||||
const outPath = join(
|
||||
resultsDir,
|
||||
`compare-${baseLabelSlug}-vs-${curLabelSlug}.md`
|
||||
);
|
||||
writeFileSync(outPath, mdLines.join('\n'));
|
||||
console.log(`Comparison saved to: ${outPath}`);
|
||||
@@ -1,43 +0,0 @@
|
||||
var async = require('neo-async'),
|
||||
fs = require('fs'),
|
||||
zlib = require('zlib');
|
||||
|
||||
module.exports = function (grunt, callback) {
|
||||
var distFiles = fs.readdirSync('dist'),
|
||||
distSizes = {};
|
||||
|
||||
async.each(
|
||||
distFiles,
|
||||
function (file, callback) {
|
||||
var content;
|
||||
try {
|
||||
content = fs.readFileSync('dist/' + file);
|
||||
} catch (err) {
|
||||
if (err.code === 'EISDIR') {
|
||||
callback();
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
file = file.replace(/\.js/, '').replace(/\./g, '_');
|
||||
distSizes[file] = content.length;
|
||||
|
||||
zlib.gzip(content, function (err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
distSizes[file + '_gz'] = data.length;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function () {
|
||||
grunt.log.writeln(
|
||||
'Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2)
|
||||
);
|
||||
callback([distSizes]);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
var fs = require('fs');
|
||||
|
||||
var metrics = fs.readdirSync(__dirname);
|
||||
metrics.forEach(function (metric) {
|
||||
if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var name = RegExp.$1;
|
||||
metric = require('./' + name);
|
||||
if (metric instanceof Function) {
|
||||
module.exports[name] = metric;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Bench } from 'tinybench';
|
||||
import Handlebars from '../../lib/index.js';
|
||||
import { templates as allTemplates } from './templates.mjs';
|
||||
import {
|
||||
printResults,
|
||||
printSectionHeader,
|
||||
saveMarkdownReport,
|
||||
} from './report.mjs';
|
||||
|
||||
// ─── 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);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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;
|
||||
}
|
||||
|
||||
async function runSection(title, config, setup) {
|
||||
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 = [];
|
||||
|
||||
// ─── 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 = {};
|
||||
|
||||
allSections.push(
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Verify outputs haven't changed during benchmarking
|
||||
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, {
|
||||
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);
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
var _ = require('underscore'),
|
||||
templates = require('./templates');
|
||||
|
||||
module.exports = function (grunt, callback) {
|
||||
// Deferring to here in case we have a build for parser, etc as part of this grunt exec
|
||||
var Handlebars = require('../../lib');
|
||||
|
||||
var templateSizes = {};
|
||||
_.each(templates, function (info, template) {
|
||||
var src = info.handlebars,
|
||||
compiled = Handlebars.precompile(src, {}),
|
||||
knownHelpers = Handlebars.precompile(src, {
|
||||
knownHelpersOnly: true,
|
||||
knownHelpers: info.helpers,
|
||||
});
|
||||
|
||||
templateSizes[template] = compiled.length;
|
||||
templateSizes['knownOnly_' + template] = knownHelpers.length;
|
||||
});
|
||||
grunt.log.writeln(
|
||||
'Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2)
|
||||
);
|
||||
callback([templateSizes]);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// ─── Formatting ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function formatNs(ns) {
|
||||
if (ns < 1000) return `${ns.toFixed(0)}ns`;
|
||||
if (ns < 1_000_000) return `${(ns / 1000).toFixed(2)}µs`;
|
||||
return `${(ns / 1_000_000).toFixed(2)}ms`;
|
||||
}
|
||||
|
||||
export function formatOps(ops) {
|
||||
if (ops >= 1_000_000) return `${(ops / 1_000_000).toFixed(2)}M`;
|
||||
if (ops >= 1000) return `${(ops / 1000).toFixed(2)}K`;
|
||||
return ops.toFixed(0);
|
||||
}
|
||||
|
||||
function taskToRow(task) {
|
||||
const { latency, throughput } = task.result;
|
||||
const toNs = (ms) => ms * 1_000_000;
|
||||
return {
|
||||
name: task.name,
|
||||
opsSec: formatOps(throughput.mean),
|
||||
avg: formatNs(toNs(latency.mean)),
|
||||
p50: formatNs(toNs(latency.p50)),
|
||||
p75: formatNs(toNs(latency.p75)),
|
||||
p99: formatNs(toNs(latency.p99)),
|
||||
rme: latency.rme.toFixed(2),
|
||||
samples: latency.samplesCount,
|
||||
};
|
||||
}
|
||||
|
||||
function completedTasks(bench) {
|
||||
return bench.tasks.filter((t) => t.result?.state === 'completed');
|
||||
}
|
||||
|
||||
// ─── Console output ──────────────────────────────────────────────────────────
|
||||
|
||||
export function printResults(bench) {
|
||||
for (const task of bench.tasks) {
|
||||
if (!task.result || task.result.state !== 'completed') {
|
||||
console.error(
|
||||
` FAILED: ${task.name}`,
|
||||
task.result?.error || 'no result'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const results = completedTasks(bench).map((task) => {
|
||||
const row = taskToRow(task);
|
||||
return {
|
||||
Name: row.name,
|
||||
'ops/sec': row.opsSec,
|
||||
avg: row.avg,
|
||||
p50: row.p50,
|
||||
p75: row.p75,
|
||||
p99: row.p99,
|
||||
'±%': row.rme,
|
||||
samples: row.samples,
|
||||
};
|
||||
});
|
||||
|
||||
console.table(results);
|
||||
}
|
||||
|
||||
export function printSectionHeader(title) {
|
||||
const padded = ` ${title} `;
|
||||
const width = Math.max(padded.length + 4, 59);
|
||||
const inner = padded.padEnd(width - 2);
|
||||
console.log(`┌${'─'.repeat(width - 2)}┐`);
|
||||
console.log(`│${inner}│`);
|
||||
console.log(`└${'─'.repeat(width - 2)}┘`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ─── Markdown report ─────────────────────────────────────────────────────────
|
||||
|
||||
export function saveMarkdownReport(
|
||||
sections,
|
||||
{ label, compileConfig, execConfig, date }
|
||||
) {
|
||||
const resultsDir = join(__dirname, 'results');
|
||||
mkdirSync(resultsDir, { recursive: true });
|
||||
|
||||
const timestamp = date.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const labelSlug = label ? `-${label.replace(/[^a-zA-Z0-9-_]/g, '_')}` : '';
|
||||
const filename = `bench-${timestamp}${labelSlug}.md`;
|
||||
const filepath = join(resultsDir, filename);
|
||||
|
||||
const lines = [];
|
||||
const labelStr = label ? ` [${label}]` : '';
|
||||
lines.push(`# Handlebars Benchmark Results${labelStr}`);
|
||||
lines.push('');
|
||||
lines.push(`- **Date:** ${date.toISOString()}`);
|
||||
if (label) lines.push(`- **Label:** ${label}`);
|
||||
lines.push(`- **Node:** ${process.version}`);
|
||||
lines.push(`- **Platform:** ${process.platform} ${process.arch}`);
|
||||
lines.push(
|
||||
`- **Compile config:** warmup=${compileConfig.warmupIterations}, minIterations=${compileConfig.iterations}, time=${compileConfig.time}ms`
|
||||
);
|
||||
lines.push(
|
||||
`- **Exec config:** warmup=${execConfig.warmupIterations}, minIterations=${execConfig.iterations}, time=${execConfig.time}ms`
|
||||
);
|
||||
lines.push('');
|
||||
|
||||
for (const { title, bench } of sections) {
|
||||
lines.push(`## ${title}`);
|
||||
lines.push('');
|
||||
lines.push(
|
||||
'| Benchmark | ops/sec | avg | p50 | p75 | p99 | ±% | samples |'
|
||||
);
|
||||
lines.push('|---|---|---|---|---|---|---|---|');
|
||||
for (const row of completedTasks(bench).map(taskToRow)) {
|
||||
lines.push(
|
||||
`| ${row.name} | ${row.opsSec} | ${row.avg} | ${row.p50} | ${row.p75} | ${row.p99} | ${row.rme} | ${row.samples} |`
|
||||
);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
writeFileSync(filepath, lines.join('\n'));
|
||||
return filepath;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
import { gzip } from 'node:zlib';
|
||||
import { promisify } from 'node:util';
|
||||
import Handlebars from '../../lib/index.js';
|
||||
import { templates } from './templates.mjs';
|
||||
|
||||
const gzipAsync = promisify(gzip);
|
||||
|
||||
// ─── Dist sizes ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function measureDistSizes() {
|
||||
const distDir = join(__dirname, '..', 'dist');
|
||||
let files;
|
||||
try {
|
||||
files = readdirSync(distDir).filter((f) => {
|
||||
try {
|
||||
return statSync(join(distDir, f)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.warn('dist/ directory not found — run `npm run build` first.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const file of files) {
|
||||
const content = readFileSync(join(distDir, file));
|
||||
const gzipped = await gzipAsync(content);
|
||||
results.push({
|
||||
File: file,
|
||||
'Raw (bytes)': content.length,
|
||||
'Gzip (bytes)': gzipped.length,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Precompile sizes ────────────────────────────────────────────────────────
|
||||
|
||||
function measurePrecompileSizes() {
|
||||
const results = [];
|
||||
for (const [name, def] of Object.entries(templates)) {
|
||||
const compiled = Handlebars.precompile(def.template, {});
|
||||
|
||||
const helpers = {};
|
||||
if (def.helpers) {
|
||||
for (const k of Object.keys(def.helpers)) {
|
||||
helpers[k] = true;
|
||||
}
|
||||
}
|
||||
const knownOnly = Handlebars.precompile(def.template, {
|
||||
knownHelpersOnly: true,
|
||||
knownHelpers: helpers,
|
||||
});
|
||||
|
||||
results.push({
|
||||
Template: name,
|
||||
'Default (chars)': compiled.length,
|
||||
'knownHelpersOnly (chars)': knownOnly.length,
|
||||
'Saved (chars)': compiled.length - knownOnly.length,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function run() {
|
||||
console.log('Handlebars Size Report');
|
||||
console.log('=====================');
|
||||
console.log();
|
||||
|
||||
const distSizes = await measureDistSizes();
|
||||
if (distSizes.length > 0) {
|
||||
console.log('## Distribution files');
|
||||
console.table(distSizes);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('## Precompiled template sizes');
|
||||
const precompileSizes = measurePrecompileSizes();
|
||||
console.table(precompileSizes);
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
export const templates = {
|
||||
// --- Small templates ---
|
||||
'static string (no expressions)': {
|
||||
template: 'Hello world',
|
||||
context: {},
|
||||
},
|
||||
'simple variables': {
|
||||
template: 'Hello {{name}}! You have {{count}} new messages.',
|
||||
context: { name: 'Mick', count: 30 },
|
||||
},
|
||||
'dot paths': {
|
||||
template:
|
||||
'{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
|
||||
context: { person: { name: { bar: { baz: 'Larry' } }, age: 45 } },
|
||||
},
|
||||
'with helper': {
|
||||
template: '{{#with person}}{{name}}{{age}}{{/with}}',
|
||||
context: { person: { name: 'Larry', age: 45 } },
|
||||
},
|
||||
|
||||
// --- Arguments & mustache-style ---
|
||||
'arguments (positional + hash)': {
|
||||
template:
|
||||
'{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}',
|
||||
context: { bar: true },
|
||||
helpers: { foo: () => '' },
|
||||
},
|
||||
'depth-1 (../)': {
|
||||
template: '{{#each names}}{{../foo}}{{/each}}',
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
'mustache-style section (array)': {
|
||||
template: '{{#names}}{{name}}{{/names}}',
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
},
|
||||
'mustache-style section (object)': {
|
||||
template: '{{#person}}{{name}}{{age}}{{/person}}',
|
||||
context: { person: { name: 'Larry', age: 45 } },
|
||||
},
|
||||
|
||||
// --- Medium templates ---
|
||||
'each (small array, 4 items)': {
|
||||
template: '{{#each names}}{{name}}{{/each}}',
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
},
|
||||
'each with @index/@key': {
|
||||
template: '{{#each names}}{{@index}}:{{name}} {{/each}}',
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
},
|
||||
'if/else conditional': {
|
||||
template:
|
||||
'{{#if active}}<span class="active">{{name}}</span>{{else}}<span>{{name}}</span>{{/if}}',
|
||||
context: { active: true, name: 'Widget' },
|
||||
},
|
||||
'partial (each + partial)': {
|
||||
template: '{{#each peeps}}{{>variables}}{{/each}}',
|
||||
context: {
|
||||
peeps: [
|
||||
{ name: 'Moe', count: 15 },
|
||||
{ name: 'Larry', count: 5 },
|
||||
{ name: 'Curly', count: 1 },
|
||||
],
|
||||
},
|
||||
partials: {
|
||||
variables: 'Hello {{name}}! You have {{count}} new messages.',
|
||||
},
|
||||
},
|
||||
'nested depth (../../)': {
|
||||
template:
|
||||
'{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
|
||||
context: {
|
||||
names: [
|
||||
{ bat: 'foo', name: ['Moe'] },
|
||||
{ bat: 'foo', name: ['Larry'] },
|
||||
{ bat: 'foo', name: ['Curly'] },
|
||||
{ bat: 'foo', name: ['Shemp'] },
|
||||
],
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
subexpressions: {
|
||||
template: '{{echo (header)}}',
|
||||
context: { echo: () => {}, header: () => {} },
|
||||
helpers: {
|
||||
echo: (value) => 'foo ' + value,
|
||||
header: () => 'Colors',
|
||||
},
|
||||
},
|
||||
|
||||
// --- Complex template ---
|
||||
'complex (if/each/helpers)': {
|
||||
template: `<h1>{{header}}</h1>
|
||||
{{#if items}}
|
||||
<ul>
|
||||
{{#each items}}
|
||||
{{#if current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{^}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{^}}
|
||||
<p>The list is empty.</p>
|
||||
{{/if}}`,
|
||||
context: {
|
||||
header() {
|
||||
return 'Colors';
|
||||
},
|
||||
hasItems: true,
|
||||
items: [
|
||||
{ name: 'red', current: true, url: '#Red' },
|
||||
{ name: 'green', current: false, url: '#Green' },
|
||||
{ name: 'blue', current: false, url: '#Blue' },
|
||||
],
|
||||
},
|
||||
},
|
||||
'recursive partials': {
|
||||
template: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
|
||||
context: {
|
||||
name: '1',
|
||||
kids: [{ name: '1.1', kids: [{ name: '1.1.1', kids: [] }] }],
|
||||
},
|
||||
partials: {
|
||||
recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
|
||||
},
|
||||
},
|
||||
|
||||
// --- Large/stress templates ---
|
||||
'each (large array, 100 items)': {
|
||||
template:
|
||||
'{{#each items}}<div class="item"><h3>{{title}}</h3><p>{{description}}</p><span>{{price}}</span></div>{{/each}}',
|
||||
context: {
|
||||
items: Array.from({ length: 100 }, (_, i) => ({
|
||||
title: `Item ${i}`,
|
||||
description: `Description for item ${i} with some longer text to simulate real content`,
|
||||
price: `$${(i * 9.99).toFixed(2)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
'each (large array, 1000 items)': {
|
||||
template: '{{#each items}}{{name}} {{/each}}',
|
||||
context: {
|
||||
items: Array.from({ length: 1000 }, (_, i) => ({ name: `item-${i}` })),
|
||||
},
|
||||
},
|
||||
'deeply nested context (4 levels)': {
|
||||
template: `{{#with level1}}
|
||||
{{#with level2}}
|
||||
{{#each items}}
|
||||
{{#if active}}
|
||||
{{../../title}}: {{name}} ({{../label}})
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/with}}
|
||||
{{/with}}`,
|
||||
context: {
|
||||
level1: {
|
||||
title: 'Root',
|
||||
level2: {
|
||||
label: 'Section',
|
||||
items: Array.from({ length: 20 }, (_, i) => ({
|
||||
name: `item-${i}`,
|
||||
active: i % 2 === 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'many partials (10 partials)': {
|
||||
template: Array.from({ length: 10 }, (_, i) => `{{>partial${i}}}`).join(
|
||||
'\n'
|
||||
),
|
||||
context: { name: 'World', count: 42 },
|
||||
partials: Object.fromEntries(
|
||||
Array.from({ length: 10 }, (_, i) => [
|
||||
`partial${i}`,
|
||||
`<section><h2>Section {{name}} #${i}</h2><p>Count: {{count}}</p></section>`,
|
||||
])
|
||||
),
|
||||
},
|
||||
'page template (mixed features)': {
|
||||
template: `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>{{title}}</title></head>
|
||||
<body>
|
||||
<header>{{>header}}</header>
|
||||
<nav>
|
||||
<ul>
|
||||
{{#each nav}}
|
||||
<li{{#if active}} class="active"{{/if}}><a href="{{url}}">{{label}}</a></li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
{{#if showBanner}}<div class="banner">{{bannerText}}</div>{{/if}}
|
||||
{{#each sections}}
|
||||
<section>
|
||||
<h2>{{title}}</h2>
|
||||
{{#each items}}
|
||||
<div class="card">
|
||||
<h3>{{name}}</h3>
|
||||
<p>{{description}}</p>
|
||||
{{#if featured}}<span class="badge">Featured</span>{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</section>
|
||||
{{/each}}
|
||||
</main>
|
||||
<footer>{{>footer}}</footer>
|
||||
</body>
|
||||
</html>`,
|
||||
context: {
|
||||
title: 'My Page',
|
||||
showBanner: true,
|
||||
bannerText: 'Welcome!',
|
||||
nav: [
|
||||
{ label: 'Home', url: '/', active: true },
|
||||
{ label: 'About', url: '/about', active: false },
|
||||
{ label: 'Contact', url: '/contact', active: false },
|
||||
],
|
||||
sections: Array.from({ length: 5 }, (_, si) => ({
|
||||
title: `Section ${si + 1}`,
|
||||
items: Array.from({ length: 8 }, (_, ii) => ({
|
||||
name: `Card ${si * 8 + ii + 1}`,
|
||||
description: `Description for card ${si * 8 + ii + 1}`,
|
||||
featured: ii === 0,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
partials: {
|
||||
header: '<h1>{{title}}</h1>',
|
||||
footer: '<p>© 2026 {{title}}</p>',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
helpers: {
|
||||
foo: function () {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
context: {
|
||||
bar: true,
|
||||
},
|
||||
|
||||
handlebars:
|
||||
'{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}',
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
handlebars: '{{#each names}}{{name}}{{/each}}',
|
||||
dust: '{#names}{name}{/names}',
|
||||
mustache: '{{#names}}{{name}}{{/names}}',
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
handlebars: '{{#names}}{{name}}{{/names}}',
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
<h1>{header}</h1>
|
||||
{?items}
|
||||
<ul>
|
||||
{#items}
|
||||
{#current}
|
||||
<li><strong>{name}</strong></li>
|
||||
{:else}
|
||||
<li><a href="{url}">{name}</a></li>
|
||||
{/current}
|
||||
{/items}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>The list is empty.</p>
|
||||
{/items}
|
||||
@@ -1,14 +0,0 @@
|
||||
<h1>{{header}}</h1>
|
||||
{{#if items}}
|
||||
<ul>
|
||||
{{#each items}}
|
||||
{{#if current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{^}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{^}}
|
||||
<p>The list is empty.</p>
|
||||
{{/if}}
|
||||
@@ -1,19 +0,0 @@
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
context: {
|
||||
header: function () {
|
||||
return 'Colors';
|
||||
},
|
||||
hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays
|
||||
items: [
|
||||
{ name: 'red', current: true, url: '#Red' },
|
||||
{ name: 'green', current: false, url: '#Green' },
|
||||
{ name: 'blue', current: false, url: '#Blue' },
|
||||
],
|
||||
},
|
||||
|
||||
handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(),
|
||||
dust: fs.readFileSync(__dirname + '/complex.dust').toString(),
|
||||
mustache: fs.readFileSync(__dirname + '/complex.mustache').toString(),
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
<h1>{{header}}</h1>
|
||||
{{#hasItems}}
|
||||
<ul>
|
||||
{{#items}}
|
||||
{{#current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{/current}}
|
||||
{{^current}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/current}}
|
||||
{{/items}}
|
||||
</ul>
|
||||
{{/hasItems}}
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
},
|
||||
handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}',
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
names: [
|
||||
{ name: 'Moe' },
|
||||
{ name: 'Larry' },
|
||||
{ name: 'Curly' },
|
||||
{ name: 'Shemp' },
|
||||
],
|
||||
foo: 'bar',
|
||||
},
|
||||
handlebars: '{{#each names}}{{../foo}}{{/each}}',
|
||||
mustache: '{{#names}}{{foo}}{{/names}}',
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
names: [
|
||||
{ bat: 'foo', name: ['Moe'] },
|
||||
{ bat: 'foo', name: ['Larry'] },
|
||||
{ bat: 'foo', name: ['Curly'] },
|
||||
{ bat: 'foo', name: ['Shemp'] },
|
||||
],
|
||||
foo: 'bar',
|
||||
},
|
||||
handlebars:
|
||||
'{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
|
||||
mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}',
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
var fs = require('fs');
|
||||
|
||||
var templates = fs.readdirSync(__dirname);
|
||||
templates.forEach(function (template) {
|
||||
if (template === 'index.js' || !/(.*)\.js$/.test(template)) {
|
||||
return;
|
||||
}
|
||||
module.exports[RegExp.$1] = require('./' + RegExp.$1);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
context: { person: { name: 'Larry', age: 45 } },
|
||||
handlebars: '{{#person}}{{name}}{{age}}{{/person}}',
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
context: { person: { name: 'Larry', age: 45 } },
|
||||
handlebars: '{{#with person}}{{name}}{{age}}{{/with}}',
|
||||
dust: '{#person}{name}{age}{/person}',
|
||||
mustache: '{{#person}}{{name}}{{age}}{{/person}}',
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
name: '1',
|
||||
kids: [{ name: '1.1', kids: [{ name: '1.1.1', kids: [] }] }],
|
||||
},
|
||||
partials: {
|
||||
mustache: { recursion: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' },
|
||||
handlebars: { recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}' },
|
||||
},
|
||||
handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
|
||||
dust: '{name}{#kids}{>recursion:./}{/kids}',
|
||||
mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}',
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
context: {
|
||||
peeps: [
|
||||
{ name: 'Moe', count: 15 },
|
||||
{ name: 'Larry', count: 5 },
|
||||
{ name: 'Curly', count: 1 },
|
||||
],
|
||||
},
|
||||
partials: {
|
||||
mustache: { variables: 'Hello {{name}}! You have {{count}} new messages.' },
|
||||
handlebars: {
|
||||
variables: 'Hello {{name}}! You have {{count}} new messages.',
|
||||
},
|
||||
},
|
||||
|
||||
handlebars: '{{#each peeps}}{{>variables}}{{/each}}',
|
||||
dust: '{#peeps}{>variables/}{/peeps}',
|
||||
mustache: '{{#peeps}}{{>variables}}{{/peeps}}',
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
context: { person: { name: { bar: { baz: 'Larry' } }, age: 45 } },
|
||||
handlebars:
|
||||
'{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
|
||||
dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}',
|
||||
mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
context: {},
|
||||
handlebars: 'Hello world',
|
||||
dust: 'Hello world',
|
||||
mustache: 'Hello world',
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
helpers: {
|
||||
echo: function (value) {
|
||||
return 'foo ' + value;
|
||||
},
|
||||
header: function () {
|
||||
return 'Colors';
|
||||
},
|
||||
},
|
||||
handlebars: '{{echo (header)}}',
|
||||
};
|
||||
|
||||
module.exports.context = module.exports.helpers;
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
context: { name: 'Mick', count: 30 },
|
||||
handlebars: 'Hello {{name}}! You have {{count}} new messages.',
|
||||
dust: 'Hello {name}! You have {count} new messages.',
|
||||
mustache: 'Hello {{name}}! You have {{count}} new messages.',
|
||||
};
|
||||
@@ -1,131 +0,0 @@
|
||||
var _ = require('underscore'),
|
||||
runner = require('./util/template-runner'),
|
||||
dust,
|
||||
Handlebars,
|
||||
Mustache;
|
||||
|
||||
try {
|
||||
dust = require('dustjs-linkedin');
|
||||
} catch {
|
||||
/* NOP */
|
||||
}
|
||||
|
||||
try {
|
||||
Mustache = require('mustache');
|
||||
} catch {
|
||||
/* NOP */
|
||||
}
|
||||
|
||||
function error() {
|
||||
throw new Error('EWOT');
|
||||
}
|
||||
|
||||
function makeSuite(bench, name, template, handlebarsOnly) {
|
||||
// Create aliases to minimize any impact from having to walk up the closure tree.
|
||||
var templateName = name,
|
||||
context = template.context,
|
||||
partials = template.partials,
|
||||
handlebarsOut,
|
||||
compatOut,
|
||||
dustOut,
|
||||
mustacheOut;
|
||||
|
||||
var handlebar = Handlebars.compile(template.handlebars, { data: false }),
|
||||
compat = Handlebars.compile(template.handlebars, {
|
||||
data: false,
|
||||
compat: true,
|
||||
}),
|
||||
options = { helpers: template.helpers };
|
||||
_.each(
|
||||
template.partials && template.partials.handlebars,
|
||||
function (partial, partialName) {
|
||||
Handlebars.registerPartial(
|
||||
partialName,
|
||||
Handlebars.compile(partial, { data: false })
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
handlebarsOut = handlebar(context, options);
|
||||
bench('handlebars', function () {
|
||||
handlebar(context, options);
|
||||
});
|
||||
|
||||
compatOut = compat(context, options);
|
||||
bench('compat', function () {
|
||||
compat(context, options);
|
||||
});
|
||||
|
||||
if (handlebarsOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dust) {
|
||||
if (template.dust) {
|
||||
dustOut = false;
|
||||
dust.loadSource(dust.compile(template.dust, templateName));
|
||||
|
||||
dust.render(templateName, context, function (err, out) {
|
||||
dustOut = out;
|
||||
});
|
||||
|
||||
bench('dust', function () {
|
||||
dust.render(templateName, context, function () {});
|
||||
});
|
||||
} else {
|
||||
bench('dust', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (Mustache) {
|
||||
var mustacheSource = template.mustache,
|
||||
mustachePartials = partials && partials.mustache;
|
||||
|
||||
if (mustacheSource) {
|
||||
mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials);
|
||||
|
||||
bench('mustache', function () {
|
||||
Mustache.to_html(mustacheSource, context, mustachePartials);
|
||||
});
|
||||
} else {
|
||||
bench('mustache', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack around whitespace until we have whitespace control
|
||||
handlebarsOut = handlebarsOut.replace(/\s/g, '');
|
||||
function compare(b, lang) {
|
||||
if (b == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
b = b.replace(/\s/g, '');
|
||||
|
||||
if (handlebarsOut !== b) {
|
||||
throw new Error(
|
||||
'Template output mismatch: ' +
|
||||
name +
|
||||
'\n\nHandlebars: ' +
|
||||
handlebarsOut +
|
||||
'\n\n' +
|
||||
lang +
|
||||
': ' +
|
||||
b
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
compare(compatOut, 'compat');
|
||||
compare(dustOut, 'dust');
|
||||
compare(mustacheOut, 'mustache');
|
||||
}
|
||||
|
||||
module.exports = function (grunt, callback) {
|
||||
// Deferring load in case we are being run inline with the grunt build
|
||||
Handlebars = require('../../lib');
|
||||
|
||||
console.log('Execution Throughput');
|
||||
runner(grunt, makeSuite, function (times, scaled) {
|
||||
callback(scaled);
|
||||
});
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
var _ = require('underscore'),
|
||||
Benchmark = require('benchmark');
|
||||
|
||||
function BenchWarmer() {
|
||||
this.benchmarks = [];
|
||||
this.currentBenches = [];
|
||||
this.names = [];
|
||||
this.times = {};
|
||||
this.minimum = Infinity;
|
||||
this.maximum = -Infinity;
|
||||
this.errors = {};
|
||||
}
|
||||
|
||||
BenchWarmer.prototype = {
|
||||
winners: function (benches) {
|
||||
return Benchmark.filter(benches, 'fastest');
|
||||
},
|
||||
suite: function (suite, fn) {
|
||||
this.suiteName = suite;
|
||||
this.times[suite] = {};
|
||||
this.first = true;
|
||||
|
||||
var self = this;
|
||||
|
||||
fn(function (name, benchFn) {
|
||||
self.push(name, benchFn);
|
||||
});
|
||||
},
|
||||
push: function (name, fn) {
|
||||
if (this.names.indexOf(name) === -1) {
|
||||
this.names.push(name);
|
||||
}
|
||||
|
||||
var first = this.first,
|
||||
suiteName = this.suiteName,
|
||||
self = this;
|
||||
this.first = false;
|
||||
|
||||
var bench = new Benchmark(fn, {
|
||||
name: this.suiteName + ': ' + name,
|
||||
onComplete: function () {
|
||||
if (first) {
|
||||
self.startLine(suiteName);
|
||||
}
|
||||
self.writeBench(bench);
|
||||
self.currentBenches.push(bench);
|
||||
},
|
||||
onError: function () {
|
||||
self.errors[this.name] = this;
|
||||
},
|
||||
});
|
||||
bench.suiteName = this.suiteName;
|
||||
bench.benchName = name;
|
||||
|
||||
this.benchmarks.push(bench);
|
||||
},
|
||||
|
||||
bench: function (callback) {
|
||||
var self = this;
|
||||
|
||||
this.printHeader('ops/msec', true);
|
||||
|
||||
Benchmark.invoke(this.benchmarks, {
|
||||
name: 'run',
|
||||
onComplete: function () {
|
||||
self.scaleTimes();
|
||||
|
||||
self.startLine('');
|
||||
|
||||
console.log('\n');
|
||||
self.printHeader('scaled');
|
||||
_.each(self.scaled, function (value, name) {
|
||||
self.startLine(name);
|
||||
|
||||
_.each(self.names, function (lang) {
|
||||
self.writeValue(value[lang] || '');
|
||||
});
|
||||
});
|
||||
console.log('\n');
|
||||
|
||||
var errors = false,
|
||||
prop,
|
||||
bench;
|
||||
for (prop in self.errors) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(self, prop) &&
|
||||
self.errors[prop].error.message !== 'EWOT'
|
||||
) {
|
||||
errors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
console.log('\n\nErrors:\n');
|
||||
Object.keys(self.errors).forEach(function (prop) {
|
||||
if (self.errors[prop].error.message !== 'EWOT') {
|
||||
bench = self.errors[prop];
|
||||
console.log('\n' + bench.name + ':\n');
|
||||
console.log(bench.error.message);
|
||||
if (bench.error.stack) {
|
||||
console.log(bench.error.stack.join('\n'));
|
||||
}
|
||||
console.log('\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
console.log('\n');
|
||||
},
|
||||
|
||||
scaleTimes: function () {
|
||||
var scaled = (this.scaled = {});
|
||||
_.each(
|
||||
this.times,
|
||||
function (times, name) {
|
||||
var output = (scaled[name] = {});
|
||||
|
||||
_.each(
|
||||
times,
|
||||
function (time, lang) {
|
||||
output[lang] = (
|
||||
((time - this.minimum) / (this.maximum - this.minimum)) *
|
||||
100
|
||||
).toFixed(2);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
|
||||
printHeader: function (title, winners) {
|
||||
var benchSize = 0,
|
||||
names = this.names,
|
||||
i,
|
||||
l;
|
||||
|
||||
for (i = 0, l = names.length; i < l; i++) {
|
||||
var name = names[i];
|
||||
|
||||
if (benchSize < name.length) {
|
||||
benchSize = name.length;
|
||||
}
|
||||
}
|
||||
|
||||
this.nameSize = benchSize + 2;
|
||||
this.benchSize = 20;
|
||||
var horSize = 0;
|
||||
|
||||
this.startLine(title);
|
||||
horSize = horSize + this.benchSize;
|
||||
for (i = 0, l = names.length; i < l; i++) {
|
||||
this.writeValue(names[i]);
|
||||
horSize = horSize + this.benchSize;
|
||||
}
|
||||
|
||||
if (winners) {
|
||||
console.log('WINNER(S)');
|
||||
horSize = horSize + 'WINNER(S)'.length;
|
||||
}
|
||||
|
||||
console.log('\n' + '-'.repeat(horSize));
|
||||
},
|
||||
|
||||
startLine: function (name) {
|
||||
var winners = Benchmark.map(
|
||||
this.winners(this.currentBenches),
|
||||
function (bench) {
|
||||
return bench.name.split(': ')[1];
|
||||
}
|
||||
);
|
||||
|
||||
this.currentBenches = [];
|
||||
|
||||
console.log(winners.join(', '));
|
||||
console.log('\n');
|
||||
|
||||
if (name) {
|
||||
this.writeValue(name);
|
||||
}
|
||||
},
|
||||
writeBench: function (bench) {
|
||||
var out;
|
||||
|
||||
if (!bench.error) {
|
||||
var count = bench.hz,
|
||||
moe = (count * bench.stats.rme) / 100,
|
||||
minimum,
|
||||
maximum;
|
||||
|
||||
count = Math.round(count / 1000);
|
||||
moe = Math.round(moe / 1000);
|
||||
minimum = count - moe;
|
||||
maximum = count + moe;
|
||||
|
||||
out = count + ' ±' + moe + ' (' + bench.cycles + ')';
|
||||
|
||||
this.times[bench.suiteName][bench.benchName] = count;
|
||||
this.minimum = Math.min(this.minimum, minimum);
|
||||
this.maximum = Math.max(this.maximum, maximum);
|
||||
} else if (bench.error.message === 'EWOT') {
|
||||
out = 'NA';
|
||||
} else {
|
||||
out = 'E';
|
||||
}
|
||||
this.writeValue(out);
|
||||
},
|
||||
|
||||
writeValue: function (out) {
|
||||
var padding = this.benchSize - out.length + 1;
|
||||
out = out + ' '.repeat(Math.max(0, padding - 1));
|
||||
console.log(out);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = BenchWarmer;
|
||||
@@ -1,29 +0,0 @@
|
||||
var _ = require('underscore'),
|
||||
BenchWarmer = require('./benchwarmer'),
|
||||
templates = require('../templates');
|
||||
|
||||
module.exports = function (grunt, makeSuite, callback) {
|
||||
var warmer = new BenchWarmer();
|
||||
|
||||
var handlebarsOnly = grunt.option('handlebars-only'),
|
||||
grep = grunt.option('grep');
|
||||
if (grep) {
|
||||
grep = new RegExp(grep);
|
||||
}
|
||||
|
||||
_.each(templates, function (template, name) {
|
||||
if (!template.handlebars || (grep && !grep.test(name))) {
|
||||
return;
|
||||
}
|
||||
|
||||
warmer.suite(name, function (bench) {
|
||||
makeSuite(bench, name, template, handlebarsOnly);
|
||||
});
|
||||
});
|
||||
|
||||
warmer.bench(function () {
|
||||
if (callback) {
|
||||
callback(warmer.times, warmer.scaled);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user