@@ -14,5 +14,6 @@ node_modules
|
||||
# Generated files
|
||||
/coverage/
|
||||
/dist/
|
||||
/tests/bench/results/
|
||||
/tests/integration/*/dist/
|
||||
/spec/tmp/*
|
||||
|
||||
+2
-3
@@ -116,10 +116,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/bench/**/*.js"],
|
||||
"files": ["tests/bench/**/*.mjs"],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-var": "off"
|
||||
"no-console": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -104,6 +104,36 @@ does have some big performance advantages. Justin Marney, a.k.a.
|
||||
rewritten Handlebars (current version) is faster than the old version,
|
||||
with many performance tests being 5 to 7 times faster than the Mustache equivalent.
|
||||
|
||||
### Benchmarks
|
||||
|
||||
The project includes a comprehensive benchmark suite (powered by [tinybench](https://github.com/tinylibs/tinybench)) that measures compilation, execution, precompilation, and end-to-end performance across templates of varying size and complexity.
|
||||
|
||||
```bash
|
||||
# Run benchmarks (auto-labels with current git branch)
|
||||
npm run bench
|
||||
|
||||
# Run with a custom label
|
||||
npm run bench -- --label my-optimization
|
||||
|
||||
# Compare results
|
||||
npm run bench:compare
|
||||
|
||||
# Or specify files explicitly
|
||||
npm run bench:compare -- bench/results/bench-*-main.md bench/results/bench-*-feat.md
|
||||
```
|
||||
|
||||
Results are saved as timestamped Markdown files in `bench/results/`. Each report includes ops/sec, avg latency, p50/p75/p99 percentiles, and sample counts.
|
||||
|
||||
Typical workflow for comparing branches:
|
||||
|
||||
```bash
|
||||
git checkout main && npm run bench
|
||||
git checkout my-feature && npm run bench
|
||||
npm run bench:compare
|
||||
```
|
||||
|
||||
When run without arguments, `bench:compare` auto-selects two result files: if a file labelled "main" exists it is always used as the baseline, otherwise the older file is the baseline. The comparison uses p75 latency for the diff to filter outliers, and marks changes with `!` (>2%) and `!!` (>5%).
|
||||
|
||||
## Upgrading
|
||||
|
||||
See [release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) for upgrade notes.
|
||||
|
||||
@@ -173,6 +173,9 @@ JavaScriptCompiler.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
// Release AST/compiler references only needed during compilation for dedup
|
||||
this.context.environments.length = 0;
|
||||
|
||||
if (this.environment.usePartial) {
|
||||
ret.usePartial = true;
|
||||
}
|
||||
|
||||
Generated
+24
-3522
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -51,6 +51,9 @@
|
||||
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
|
||||
"test:serve": "npx serve -l 9999 .",
|
||||
"test:integration": "npm run build && ./tests/integration/run-integration-tests.sh",
|
||||
"bench": "node tests/bench/perf.mjs",
|
||||
"bench:compare": "node tests/bench/compare.mjs",
|
||||
"bench:size": "node tests/bench/size.mjs",
|
||||
"--- combined tasks ---": "",
|
||||
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
|
||||
},
|
||||
@@ -70,22 +73,19 @@
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"aws-sdk": "^2.1.49",
|
||||
"benchmark": "~1.0",
|
||||
"concurrently": "^5.0.0",
|
||||
"dustjs-linkedin": "^2.0.2",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-plugin-compat": "^7.0.1",
|
||||
"fs-extra": "^8.1.0",
|
||||
"husky": "^3.1.0",
|
||||
"lint-staged": "^16.3.2",
|
||||
"mock-stdin": "^0.3.0",
|
||||
"mustache": "^2.1.3",
|
||||
"oxfmt": "^0.36.0",
|
||||
"oxlint": "^1.51.0",
|
||||
"semver": "^5.0.1",
|
||||
"tinybench": "^6.0.0",
|
||||
"typescript": "^3.4.3",
|
||||
"uglify-js": "^3.19.3",
|
||||
"underscore": "^1.5.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const metrics = require('../tests/bench');
|
||||
const { createRegisterAsyncTaskFn } = require('./util/async-grunt-task');
|
||||
|
||||
module.exports = function (grunt) {
|
||||
const registerAsyncTask = createRegisterAsyncTaskFn(grunt);
|
||||
|
||||
registerAsyncTask('metrics', function () {
|
||||
const onlyExecuteName = grunt.option('name');
|
||||
const events = {};
|
||||
|
||||
const promises = Object.keys(metrics).map(async (name) => {
|
||||
if (name.startsWith('_')) {
|
||||
return;
|
||||
}
|
||||
if (onlyExecuteName != null && name !== onlyExecuteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
metrics[name](grunt, function (data) {
|
||||
events[name] = data;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
};
|
||||
@@ -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