perf: add cross-engines benchmark

This commit is contained in:
harttle
2021-06-20 13:25:08 +08:00
committed by Jun Yang
parent cc9b28307c
commit cdceb25d00
28 changed files with 32097 additions and 1585 deletions
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
require: "ts-node/register/transpile-only",
reporter: "spec"
}
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env node
const Benchmark = require('benchmark')
const data = require('./data/todolist.json')
const path = require('path')
const engines = {
liquid: require('./engines/liquid'),
handlebars: require('./engines/handlebars'),
react: require('./engines/react'),
swig: require('./engines/swig')
}
function crossEngines () {
console.log(' cross engines')
console.log('------------------------')
return new Promise(resolve => {
const suit = new Benchmark.Suite('cross engines')
for (const [name, { load, render }] of Object.entries(engines)) {
const tpl = load(path.resolve(__dirname, `templates/todolist`))
suit.add(name, () => render(tpl, data))
}
suit.on('cycle', event => console.log(String(event.target)))
suit.on('complete', resolve)
suit.run({ async: true })
})
}
module.exports = { crossEngines }
+29
View File
@@ -0,0 +1,29 @@
{
"categories": [{
"color": "red",
"id": "CAT1",
"title": "work"
}, {
"color": "red",
"id": "CAT2",
"title": "emergency"
}, {
"color": "green",
"id": "CAT3",
"title": "sport"
}],
"todos": [{
"title": "fork and clone",
"id": "TODO1",
"category": "work"
}, {
"title": "make it better",
"id": "TODO2",
"category": "sport"
}, {
"title": "make a pull request",
"id": "TODO3",
"category": "work"
}]
}
Regular → Executable
+6 -18
View File
@@ -1,8 +1,10 @@
const Benchmark = require('benchmark')
const { Liquid } = require('..')
const ctx = require('./data/todolist.json')
const { resolve } = require('path')
const engine = new Liquid({
root: __dirname,
root: resolve(__dirname, 'templates'),
extname: '.liquid'
})
@@ -17,28 +19,14 @@ engine.registerTag('header', {
}
})
const ctx = {
todos: ['fork and clone', 'make it better', 'make a pull request'],
title: 'Welcome to liquidjs!'
}
const template = `
{%header content: "welcome to liquid" | capitalize%}
<ul>
{% for todo in todos %}
<li>{{forloop.index}} - {{todo}}</li>
{% endfor %}
</ul>
`
function demo () {
console.log('--- demo ---')
console.log(' demo')
console.log('------------------------')
return new Promise(resolve => {
new Benchmark.Suite('demo')
.add('demo', {
defer: true,
fn: d => engine.parseAndRender(template, ctx).then(x => d.resolve(x))
fn: d => engine.renderFile('todolist', ctx).then(x => d.resolve(x))
})
.on('cycle', event => console.log(String(event.target)))
.on('complete', resolve)
+33
View File
@@ -0,0 +1,33 @@
const handlebars = require('handlebars')
const { readFileSync } = require('fs')
const { join } = require('path')
handlebars.registerPartial(
'todo-icon',
readFileSync(join(__dirname, '../templates/todo-icon.hbs'), 'utf8')
)
handlebars.registerHelper('concat', function (...args) {
return args.filter(x => typeof x === 'string').join('')
})
handlebars.registerHelper('url', function (path) {
return `http://example.com${path}`
})
handlebars.registerHelper('inc', function (num) {
return Number(num) + 1
})
handlebars.registerHelper('capitalize', function (str) {
return str[0].toUpperCase() + str.slice(1).toLowerCase()
})
handlebars.registerHelper('upcase', function (str) {
return str.toUpperCase()
})
module.exports = {
load: path => handlebars.compile(readFileSync(path + '.hbs', 'utf8')),
render: (tpl, data) => tpl(data)
}
+15
View File
@@ -0,0 +1,15 @@
const { Liquid } = require('../..')
const { readFileSync } = require('fs')
const { join } = require('path')
const liquid = new Liquid({
root: join(__dirname, '../templates'),
extname: '.liquid'
})
liquid.registerFilter('url', path => `http://example.com${path}`)
module.exports = {
load: path => liquid.parse(readFileSync(path + '.liquid', 'utf8')),
render: (tpl, data) => liquid.renderSync(tpl, data)
}
+20
View File
@@ -0,0 +1,20 @@
const { readFileSync } = require('fs')
const React = require('react')
const ReactDOMServer = require('react-dom/server')
const babel = require('@babel/core')
const requireFromString = require('require-from-string')
const babelConfig = {
presets: ['@babel/preset-react', '@babel/preset-env']
}
module.exports = {
load: path => {
const src = readFileSync(path + '.jsx', 'utf8')
const transformed = babel.transform(src, babelConfig)
return requireFromString(transformed.code)
},
render: (Component, data) => {
return ReactDOMServer.renderToString(React.createElement(Component, data))
}
}
+30
View File
@@ -0,0 +1,30 @@
const swig = require('swig')
swig.setFilter('url', function (path) {
return `http://example.com${path}`
})
swig.setFilter('prepend', function (input, arg) {
return arg + input
})
swig.setFilter('append', function (input, arg) {
return input + arg
})
swig.setFilter('inc', function (num) {
return Number(num) + 1
})
swig.setFilter('capitalize', function (str) {
return str[0].toUpperCase() + str.slice(1).toLowerCase()
})
swig.setFilter('upcase', function (str) {
return str.toUpperCase()
})
module.exports = {
load: path => swig.compileFile(path + '.swig'),
render: (tpl, data) => tpl(data)
}
Regular → Executable
+2
View File
@@ -3,6 +3,7 @@ const { tag } = require('./tag')
const { demo } = require('./demo')
const { layout } = require('./layout')
const { memory } = require('./memory')
const { crossEngines } = require('./cross-engines')
async function main () {
await output()
@@ -10,6 +11,7 @@ async function main () {
await demo()
await layout()
await memory()
await crossEngines()
}
main()
Regular → Executable
+2 -1
View File
@@ -18,7 +18,8 @@ const template = `
`
function layout () {
console.log('--- layout ---')
console.log(' layout')
console.log('------------------------')
return new Promise(resolve => {
new Benchmark.Suite('layout')
.add('cache=false', {
Regular → Executable
+7 -6
View File
@@ -13,7 +13,8 @@ const engine = new Liquid(engineOptions)
const SAMPLE_COUNT = 1024
function memory () {
console.log('--- memory ---')
console.log(' memory')
console.log('------------------------')
html()
todolist()
}
@@ -29,11 +30,11 @@ function html () {
templates.push(engine.parse(str))
}
const diff1 = (getHeapStatistics().used_heap_size - base) / SAMPLE_COUNT
console.log(`${h(str.length)} lorem-html before GC x ${h(diff1)}/tpl (${SAMPLE_COUNT} instances sampled)`)
console.log(`[lorem-html ${h(str.length)}][before GC] ${h(diff1)}/tpl (${SAMPLE_COUNT} runs sampled)`)
global.gc()
const diff2 = (getHeapStatistics().used_heap_size - base) / SAMPLE_COUNT
console.log(`${h(str.length)} lorem-html after GC x ${h(diff2)}/tpl (${SAMPLE_COUNT} instances sampled)`)
console.log(`[lorem-html ${h(str.length)}][after GC] ${h(diff2)}/tpl (${SAMPLE_COUNT} runs sampled)`)
}
function todolist () {
@@ -47,15 +48,15 @@ function todolist () {
templates.push(engine.parse(str))
}
const diff1 = (getHeapStatistics().used_heap_size - base) / SAMPLE_COUNT
console.log(`${h(str.length)} todolist before GC x ${h(diff1)}/tpl (${SAMPLE_COUNT} instances sampled)`)
console.log(`[todolist ${h(str.length)}][before GC] ${h(diff1)}/tpl (${SAMPLE_COUNT} runs sampled)`)
global.gc()
const diff2 = (getHeapStatistics().used_heap_size - base) / SAMPLE_COUNT
console.log(`${h(str.length)} todolist after GC x ${h(diff2)}/tpl (${SAMPLE_COUNT} instances sampled)`)
console.log(`[todolist ${h(str.length)}][after GC] ${h(diff2)}/tpl (${SAMPLE_COUNT} runs sampled)`)
}
function h (size) {
return (size / 1024).toFixed(3) + ' kB'
return (size / 1024).toFixed(3) + ' KB'
}
module.exports = { memory }
Regular → Executable
+2 -1
View File
@@ -4,7 +4,8 @@ const { Liquid } = require('..')
const liquid = new Liquid()
function output () {
console.log('--- output ---')
console.log(' output')
console.log('------------------------')
return new Promise(resolve => {
new Benchmark.Suite('output')
.add('literal', test('{{false}}{{"foo"}}{{32.322}}'))
+3380
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "benchmark",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node --expose-gc index",
"engines": "node -e 'require(\"./cross-engines\").crossEngines()'"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.13.14",
"@babel/preset-env": "^7.13.12",
"@babel/preset-react": "^7.13.13",
"handlebars": "^4.7.7",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"require-from-string": "^2.0.2",
"swig": "^1.4.2"
}
}
Regular → Executable
+2 -1
View File
@@ -4,7 +4,8 @@ const { Liquid } = require('..')
const liquid = new Liquid()
function tag () {
console.log('--- tag ---')
console.log(' tag')
console.log('------------------------')
return new Promise(resolve => {
new Benchmark.Suite('tag')
.add('if', test('{% if "foobar" %}foo{% endif %}'))
+1
View File
@@ -0,0 +1 @@
<img title="risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" src="http://images.example.com/{{id}}.png">
+1
View File
@@ -0,0 +1 @@
<img title="risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" src="http://images.example.com/{{id}}.png">
+1
View File
@@ -0,0 +1 @@
<img title="risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" src="http://images.example.com/{{todo.id}}.png">
+44
View File
@@ -0,0 +1,44 @@
<section class="todo-list">
<h1>
posuere cubilia Curae; Vestibulum hendrerit malesuada odio. Fusce ut elit
ut augue sollicitudin blandit. Phasellus volutpat lorem. Duis non pede et
neque luctus tincidunt. Duis interdum tempus elit.
<small>Aenean metus. Vestibulum ac lacus. Vivamus porttitor, massa ut.</small>
</h1>
<a href={{url "/add"}} class="todo-add"><i class="fa fa-plus-square"></i>Add Todo</a>
<ul class="filter-category">
{{#each categories }}
<li style="background: {{ color }}">
<a href="/todos/category/{{ id }}">{{ title }}</a>
</li>
{{/each}}
</ul>
{{#each todos }}
<div>
<h2>
Todo {{inc @index}}:
<span class="todo-item content" data-todo-index="{{@index}}">
{{ upcase (concat 'TODO: ' title) }}
</span>
</h2>
{{> todo-icon id=id }}
<p class="description">
Purus eu mi. Proin commodo, massa commodo dapibus elementum,
libero lacus pulvinar eros, ut tincidunt nisl elit ut velit. Cras rutrum
porta purus. Vivamus lorem. Sed turpis enim, faucibus quis, pharetra in,
sagittis sed, magna. Curabitur ultricies felis ut libero. Nullam tincidunt
enim eu nibh. Nunc eget ipsum in sem facilisis convallis. Proin fermentum
</p>
<div class="todo-meta">
{{# if category }}
<span>{{ capitalize category }}</span>
{{/if}}
</div>
<a class="btn btn-primary" href={{ url (concat "/edit/" id) }}><i class="fa fa-pencil"></i> Edit</a>
<a class="btn btn-default"><i class="fa fa-check"></i> Check</a>
<a class="btn ben-danger" href={{ url (concat "/delete/" id) }}><i class="fa fa-trash-o"</i> Trash</a>
</div>
{{/each}}
</section>
+57
View File
@@ -0,0 +1,57 @@
// eslint-disable-next-line
const React = require('react')
function capitalize (str) {
return str[0].toUpperCase() + str.slice(1).toLowerCase()
}
function url (path) {
return `http://example.com${path}`
}
function renderTodoIcon (id) {
return <img title="risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" src="http://images.example.com/{id}.png"/>
}
module.exports = ({ categories, todos }) => {
return <section className="todo-list">
<h1>
posuere cubilia Curae; Vestibulum hendrerit malesuada odio. Fusce ut elit
ut augue sollicitudin blandit. Phasellus volutpat lorem. Duis non pede et
neque luctus tincidunt. Duis interdum tempus elit.
<small>Aenean metus. Vestibulum ac lacus. Vivamus porttitor, massa ut.</small>
</h1>
<a href={url('/add')} className="todo-add"><i className="fa fa-plus-square"></i>Add Todo</a>
<ul className="filter-category">
{categories.map((item, i) => {
return <li key={i} style={{ background: item.color }}>
<a href={'/todos/category/' + item.id }>{ item.title }</a>
</li>
})}
</ul>
{todos.map((todo, index0) => {
return <div key={index0}>
<h2>
Todo { index0 + 1 }:
<span className="todo-item content" data-todo-index="{{ forloop.index0 }}">
{ ('TODO: ' + todo.title).toUpperCase() }
</span>
</h2>
{ renderTodoIcon(todo.id) }
<p className="description">
Purus eu mi. Proin commodo, massa commodo dapibus elementum,
libero lacus pulvinar eros, ut tincidunt nisl elit ut velit. Cras rutrum
porta purus. Vivamus lorem. Sed turpis enim, faucibus quis, pharetra in,
sagittis sed, magna. Curabitur ultricies felis ut libero. Nullam tincidunt
enim eu nibh. Nunc eget ipsum in sem facilisis convallis. Proin fermentum
</p>
<div className="todo-meta">
{ todo.category ? <span>{capitalize(todo.category)}</span> : '' }
</div>
<a className="btn btn-primary" href={url('/edit/' + todo.id)}><i className="fa fa-pencil"></i> Edit</a>
<a className="btn btn-default"><i className="fa fa-check"></i> Check</a>
<a className="btn ben-danger" href={url('/delete/' + todo.id)}><i className="fa fa-trash-o"></i> Trash</a>
</div>
})}
</section>
}
+5 -10
View File
@@ -7,8 +7,7 @@
</h1>
<a href={{"/add" | url}} class="todo-add"><i class="fa fa-plus-square"></i>Add Todo</a>
<ul class="filter-category">
{% for i in (0..categories.length) %}
{% assign item=categories[i] %}
{% for item in categories %}
<li style="background: {{ item.color }}">
<a href="/todos/category/{{ item.id }}">{{ item.title }}</a>
</li>
@@ -22,11 +21,7 @@
{{ todo.title | prepend: 'TODO: ' | upcase }}
</span>
</h2>
<img
title="risus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices"
src="{% include "todo-icon", id=todo.id }} %}"
>
{% render "todo-icon", id:todo.id %}
<p class="description">
Purus eu mi. Proin commodo, massa commodo dapibus elementum,
@@ -38,12 +33,12 @@
<div class="todo-meta">
{% if todo.category %}
<span>{{ todo.category.title | capitalize }}</span>
<span>{{ todo.category | capitalize }}</span>
{% endif %}
</div>
<a class="btn btn-primary" href={{"/edit/" | append todo.id | url }}><i class="fa fa-pencil"></i> Edit</a>
<a class="btn btn-primary" href={{"/edit/" | append: todo.id | url }}><i class="fa fa-pencil"></i> Edit</a>
<a class="btn btn-default"><i class="fa fa-check"></i> Check</a>
<a class="btn ben-danger" href={{"/delete/" | append todo.id | url }}><i class="fa fa-trash-o"</i> Trash</a>
<a class="btn ben-danger" href={{"/delete/" | append: todo.id | url }}><i class="fa fa-trash-o"</i> Trash</a>
</div>
{% endfor %}
</section>
+44
View File
@@ -0,0 +1,44 @@
<section class="todo-list">
<h1>
posuere cubilia Curae; Vestibulum hendrerit malesuada odio. Fusce ut elit
ut augue sollicitudin blandit. Phasellus volutpat lorem. Duis non pede et
neque luctus tincidunt. Duis interdum tempus elit.
<small>Aenean metus. Vestibulum ac lacus. Vivamus porttitor, massa ut.</small>
</h1>
<a href={{"/add" | url}} class="todo-add"><i class="fa fa-plus-square"></i>Add Todo</a>
<ul class="filter-category">
{% for item in categories %}
<li style="background: {{ item.color }}">
<a href="/todos/category/{{ item.id }}">{{ item.title }}</a>
</li>
{% endfor %}
</ul>
{% for todo in todos %}
<div>
<h2>
Todo {{ loop.index }}:
<span class="todo-item content" data-todo-index="{{ loop.index0 }}">
{{ todo.title | prepend('TODO: ') | upcase }}
</span>
</h2>
{% include "todo-icon.swig" %}
<p class="description">
Purus eu mi. Proin commodo, massa commodo dapibus elementum,
libero lacus pulvinar eros, ut tincidunt nisl elit ut velit. Cras rutrum
porta purus. Vivamus lorem. Sed turpis enim, faucibus quis, pharetra in,
sagittis sed, magna. Curabitur ultricies felis ut libero. Nullam tincidunt
enim eu nibh. Nunc eget ipsum in sem facilisis convallis. Proin fermentum
</p>
<div class="todo-meta">
{% if todo.category %}
<span>{{ todo.category | capitalize }}</span>
{% endif %}
</div>
<a class="btn btn-primary" href={{"/edit/" | append(todo.id) | url }}><i class="fa fa-pencil"></i> Edit</a>
<a class="btn btn-default"><i class="fa fa-check"></i> Check</a>
<a class="btn ben-danger" href={{"/delete/" | append(todo.id) | url }}><i class="fa fa-trash-o"</i> Trash</a>
</div>
{% endfor %}
</section>
+28343 -1529
View File
File diff suppressed because it is too large Load Diff
+12 -10
View File
@@ -15,14 +15,15 @@
"check": "npm test && npm run lint",
"unit": "mocha \"test/unit/**/*.ts\"",
"integration": "mocha \"test/integration/**/*.ts\"",
"e2e": "npm run build && mocha \"test/e2e/**/*.ts\"",
"e2e": "mocha \"test/e2e/**/*.ts\"",
"test": "cross-env BUNDLES=cjs,umd npm run build && mocha \"test/**/*.ts\"",
"benchmark": "node --expose-gc benchmark/index",
"benchmark:prepare": "cd benchmark && npm ci",
"benchmark": "cd benchmark && npm start",
"benchmark:engines": "cd benchmark && npm run engines",
"coverage": "nyc --reporter=html mocha \"test/{unit,integration}/**/*.ts\"",
"coverage-coveralls": "nyc mocha \"test/{unit,integration}/**/*.ts\" && nyc report --reporter=text-lcov | coveralls",
"build": "rm -rf dist && rollup -c rollup.config.ts && ls -lh dist",
"build-docs": "bin/build-docs.sh",
"watch": "tsc --watch"
"build-docs": "bin/build-docs.sh"
},
"bin": {
"liquidjs": "./bin/liquid.js",
@@ -59,7 +60,7 @@
},
"homepage": "https://github.com/harttle/liquidjs#readme",
"devDependencies": {
"@commitlint/cli": "^8.2.0",
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^8.2.0",
"@semantic-release/changelog": "^3.0.2",
"@semantic-release/commit-analyzer": "^6.1.0",
@@ -93,13 +94,13 @@
"express": "^4.16.4",
"husky": "^4.2.5",
"jsdom": "^13.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"mocha": "^9.0.1",
"nyc": "^15.1.0",
"regenerator-runtime": "^0.12.1",
"rollup": "^1.1.2",
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-typescript2": "^0.21.1",
"rollup-plugin-uglify": "^6.0.2",
"rollup-plugin-uglify": "^5.0.2",
"semantic-release": "^17.2.3",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
@@ -141,6 +142,7 @@
"pre-commit": "npm run check",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {}
}
}
+2 -2
View File
@@ -7,12 +7,12 @@ import { stringify } from '../../util/underscore'
import { assert } from '../../util/assert'
export function append (v: string, arg: string) {
assert(arg !== undefined, () => 'append expect 2 arguments')
assert(arguments.length === 2, () => 'append expect 2 arguments')
return stringify(v) + stringify(arg)
}
export function prepend (v: string, arg: string) {
assert(arg !== undefined, () => 'prepend expect 2 arguments')
assert(arguments.length === 2, () => 'prepend expect 2 arguments')
return stringify(arg) + stringify(v)
}
+1
View File
@@ -24,6 +24,7 @@ export default {
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, hash, withVar, file } = this
const { renderer } = liquid
// TODO try move all liquid.parse calls into parse() section
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
@@ -9,9 +9,6 @@ describe('filters/string', function () {
it('should return "-3abc" for -3, "abc"',
() => test('{{ -3 | append: "abc" }}', '-3abc'))
it('should return "abar" for "a", foo', () => test('{{ "a" | append: foo }}', { foo: 'bar' }, 'abar'))
it('should throw if second argument undefined', () => {
return expect(test('{{ "abc" | append: undefinedVar }}', 'abc')).to.be.rejectedWith(/2 arguments/)
})
it('should throw if second argument not set', () => {
return expect(test('{{ "abc" | append }}', 'abc')).to.be.rejectedWith(/2 arguments/)
})
@@ -21,9 +18,6 @@ describe('filters/string', function () {
it('should return "-3abc" for -3, "abc"',
() => test('{{ -3 | prepend: "abc" }}', 'abc-3'))
it('should return "abar" for "a", foo', () => test('{{ "a" | prepend: foo }}', { foo: 'bar' }, 'bara'))
it('should throw if second argument undefined', () => {
return expect(test('{{ "abc" | prepend: undefinedVar }}', 'abc')).to.be.rejectedWith(/2 arguments/)
})
it('should throw if second argument not set', () => {
return expect(test('{{ "abc" | prepend }}', 'abc')).to.be.rejectedWith(/2 arguments/)
})
-1
View File
@@ -1 +0,0 @@
--require ts-node/register/transpile-only --strict false