feat: perf testing (#1789)
This commit is contained in:
parent
034ef4d318
commit
9a55a290cc
5 changed files with 368 additions and 5 deletions
|
|
@ -18,8 +18,10 @@ install:
|
||||||
# Bundle before running tests so the bundle is always up-to-date
|
# Bundle before running tests so the bundle is always up-to-date
|
||||||
before_script: npm run build
|
before_script: npm run build
|
||||||
|
|
||||||
# This is the default, but leaving so it is obvious
|
# Run tests, lint, and then check for perf regressions
|
||||||
# script: npm test
|
script:
|
||||||
|
- npm test
|
||||||
|
- npm run perf
|
||||||
|
|
||||||
# After a successful build commit changes back to repo
|
# After a successful build commit changes back to repo
|
||||||
after_success:
|
after_success:
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@
|
||||||
"build-min": "node bundler/cli browser.js -o mithril.min.js -m",
|
"build-min": "node bundler/cli browser.js -o mithril.min.js -m",
|
||||||
"lintdocs": "node docs/lint",
|
"lintdocs": "node docs/lint",
|
||||||
"gendocs": "node docs/generate",
|
"gendocs": "node docs/generate",
|
||||||
"lint": "eslint .",
|
"lint": "eslint . || true",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
|
"perf": "node performance/test-perf.js",
|
||||||
"test": "node ospec/bin/ospec",
|
"test": "node ospec/bin/ospec",
|
||||||
"posttest": "npm run lint || true",
|
"posttest": "npm run lint",
|
||||||
"cover": "istanbul cover --print both ospec/bin/ospec",
|
"cover": "istanbul cover --print both ospec/bin/ospec",
|
||||||
"release": "npm version -m 'v%s'",
|
"release": "npm version -m 'v%s'",
|
||||||
"preversion": "npm run test",
|
"preversion": "npm run test",
|
||||||
|
|
@ -24,6 +25,7 @@
|
||||||
"postversion": "git push --follow-tags"
|
"postversion": "git push --follow-tags"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
"eslint": "^3.16.1",
|
"eslint": "^3.16.1",
|
||||||
"istanbul": "^0.4.3",
|
"istanbul": "^0.4.3",
|
||||||
"marked": "^0.3.6"
|
"marked": "^0.3.6"
|
||||||
|
|
|
||||||
23
performance/index.html
Normal file
23
performance/index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../module/module.js"></script>
|
||||||
|
<script src="../ospec/ospec.js"></script>
|
||||||
|
<script src="../querystring/parse.js"></script>
|
||||||
|
<script src="../test-utils/parseURL.js"></script>
|
||||||
|
<script src="../test-utils/callAsync.js"></script>
|
||||||
|
<script src="../test-utils/domMock.js"></script>
|
||||||
|
<script src="../test-utils/pushStateMock.js"></script>
|
||||||
|
<script src="../test-utils/xhrMock.js"></script>
|
||||||
|
<script src="../test-utils/browserMock.js"></script>
|
||||||
|
<script src="../mithril.js"></script>
|
||||||
|
<script src="../node_modules/lodash/lodash.js"></script>
|
||||||
|
<script src="../node_modules/benchmark/benchmark.js"></script>
|
||||||
|
<script src="test-perf.js"></script>
|
||||||
|
|
||||||
|
<!-- test-perf runs itself for CLI usage -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
336
performance/test-perf.js
Normal file
336
performance/test-perf.js
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
/* global Benchmark */
|
||||||
|
"use strict"
|
||||||
|
|
||||||
|
/* Based off of preact's perf tests, so including their MIT license */
|
||||||
|
/*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Jason Miller
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var browserMock = require("../test-utils/browserMock")
|
||||||
|
|
||||||
|
// Do this silly dance so browser testing works
|
||||||
|
var B = typeof Benchmark === "undefined" ? require("benchmark") : Benchmark
|
||||||
|
|
||||||
|
var m, scratch;
|
||||||
|
|
||||||
|
// set up browser env on before running tests
|
||||||
|
var doc = typeof document !== "undefined" ? document : null
|
||||||
|
|
||||||
|
if(!doc) {
|
||||||
|
var mock = browserMock()
|
||||||
|
if (typeof global !== "undefined") { global.window = mock }
|
||||||
|
|
||||||
|
doc = mock.document
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have to include mithril AFTER browser polyfill is set up
|
||||||
|
m = require("../mithril") // eslint-disable-line global-require
|
||||||
|
|
||||||
|
scratch = doc.createElement("div");
|
||||||
|
|
||||||
|
(doc.body || doc.documentElement).appendChild(scratch)
|
||||||
|
|
||||||
|
// Initialize benchmark suite
|
||||||
|
var suite = new B.Suite("mithril perf")
|
||||||
|
|
||||||
|
suite.on("start", function() {
|
||||||
|
this.start = Date.now();
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.on("cycle", function(e) {
|
||||||
|
console.log(e.target.toString())
|
||||||
|
|
||||||
|
scratch.innerHTML = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.on("complete", function() {
|
||||||
|
console.log("Completed perf tests in " + (Date.now() - this.start) + "ms")
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.on("error", console.error.bind(console))
|
||||||
|
|
||||||
|
suite.add({
|
||||||
|
name : "rerender without changes",
|
||||||
|
onStart : function() {
|
||||||
|
this.vdom = m("div", {class: "foo bar", "data-foo": "bar", p: 2},
|
||||||
|
m("header",
|
||||||
|
m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"),
|
||||||
|
m("nav",
|
||||||
|
m("a", {href: "/foo"}, "Foo"),
|
||||||
|
m("a", {href: "/bar"}, "Bar")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m("main",
|
||||||
|
m("form", {onSubmit: function onSubmit() {}},
|
||||||
|
m("input", {type: "checkbox", checked: true}),
|
||||||
|
m("input", {type: "checkbox", checked: false}),
|
||||||
|
m("fieldset",
|
||||||
|
m("label",
|
||||||
|
m("input", {type: "radio", checked: true})
|
||||||
|
),
|
||||||
|
m("label",
|
||||||
|
m("input", {type: "radio"})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m("button-bar",
|
||||||
|
m("button",
|
||||||
|
{style: "width:10px; height:10px; border:1px solid #FFF;"},
|
||||||
|
"Normal CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: "top:0 ; right: 20"},
|
||||||
|
"Poor CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true},
|
||||||
|
"Poorer CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: {margin: 0, padding: "10px", overflow: "visible"}},
|
||||||
|
"Object CSS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fn : function() {
|
||||||
|
m.render(scratch, this.vdom)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.add({
|
||||||
|
name : "construct large VDOM tree",
|
||||||
|
|
||||||
|
onStart : function() {
|
||||||
|
var fields = []
|
||||||
|
|
||||||
|
for(var i=100; i--;) {
|
||||||
|
fields.push((i * 999).toString(36))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fields = fields;
|
||||||
|
},
|
||||||
|
|
||||||
|
fn : function () {
|
||||||
|
m("div", {class: "foo bar", "data-foo": "bar", p: 2},
|
||||||
|
m("header",
|
||||||
|
m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"),
|
||||||
|
m("nav",
|
||||||
|
m("a", {href: "/foo"}, "Foo"),
|
||||||
|
m("a", {href: "/bar"}, "Bar")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m("main",
|
||||||
|
m("form",
|
||||||
|
{onSubmit: function onSubmit() {}},
|
||||||
|
m("input", {type: "checkbox", checked: true}),
|
||||||
|
m("input", {type: "checkbox"}),
|
||||||
|
m("fieldset",
|
||||||
|
this.fields.map(function (field) {
|
||||||
|
return m("label",
|
||||||
|
field,
|
||||||
|
":",
|
||||||
|
m("input", {placeholder: field})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
m("button-bar",
|
||||||
|
m("button",
|
||||||
|
{style: "width:10px; height:10px; border:1px solid #FFF;"},
|
||||||
|
"Normal CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: "top:0 ; right: 20"},
|
||||||
|
"Poor CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true},
|
||||||
|
"Poorer CSS"
|
||||||
|
),
|
||||||
|
m("button",
|
||||||
|
{style: {margin: 0, padding: "10px", overflow: "visible"}},
|
||||||
|
"Object CSS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.add({
|
||||||
|
name : "mutate styles/properties",
|
||||||
|
|
||||||
|
onStart : function () {
|
||||||
|
var counter = 0
|
||||||
|
var keyLooper = function (n) { return function (c) { return c % n ? (c + "px") : c } }
|
||||||
|
var get = function (obj, i) { return obj[i%obj.length] }
|
||||||
|
var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga"]
|
||||||
|
var styles = []
|
||||||
|
var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"]
|
||||||
|
var stylekeys = [
|
||||||
|
["left", keyLooper(3)],
|
||||||
|
["top", keyLooper(2)],
|
||||||
|
["margin", function (c) { return get(multivalue, c).replace("1px", c+"px") }],
|
||||||
|
["padding", function (c) { return get(multivalue, c) }],
|
||||||
|
["position", function (c) { return c%5 ? c%2 ? "absolute" : "relative" : null }],
|
||||||
|
["display", function (c) { return c%10 ? c%2 ? "block" : "inline" : "none" }],
|
||||||
|
["color", function (c) { return ("rgba(" + (c%255) + ", " + (255 - c%255) + ", " + (50+c%150) + ", " + (c%50/50) + ")") }],
|
||||||
|
["border", function (c) { return c%5 ? ((c%10) + "px " + (c%2?"solid":"dotted") + " " + (stylekeys[6][1](c))) : "" }]
|
||||||
|
]
|
||||||
|
var i, j, style, conf
|
||||||
|
|
||||||
|
for (i=0; i<1000; i++) {
|
||||||
|
style = {}
|
||||||
|
for (j=0; j<i%10; j++) {
|
||||||
|
conf = get(stylekeys, ++counter)
|
||||||
|
style[conf[0]] = conf[1](counter)
|
||||||
|
}
|
||||||
|
styles[i] = style
|
||||||
|
}
|
||||||
|
|
||||||
|
this.count = 0
|
||||||
|
this.app = function (index) {
|
||||||
|
return m("div",
|
||||||
|
{
|
||||||
|
class: get(classes, index),
|
||||||
|
"data-index": index,
|
||||||
|
title: index.toString(36)
|
||||||
|
},
|
||||||
|
m("input", {type: "checkbox", checked: index % 3 == 0}),
|
||||||
|
m("input", {value: "test " + (Math.floor(index / 4)), disabled: index % 10 ? null : true}),
|
||||||
|
m("div", {class: get(classes, index * 11)},
|
||||||
|
m("p", {style: get(styles, index)}, "p1"),
|
||||||
|
m("p", {style: get(styles, index + 1)}, "p2"),
|
||||||
|
m("p", {style: get(styles, index * 2)}, "p3"),
|
||||||
|
m("p", {style: get(styles, index * 3 + 1)}, "p4")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fn : function () {
|
||||||
|
m.render(scratch, this.app(++this.count))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shared components for node recyling benchmarks
|
||||||
|
var Header = {
|
||||||
|
view : function () {
|
||||||
|
return m("header",
|
||||||
|
m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"),
|
||||||
|
m("nav",
|
||||||
|
m("a", {href: "/foo"}, "Foo"),
|
||||||
|
m("a", {href: "/bar"}, "Bar")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Form = {
|
||||||
|
view : function () {
|
||||||
|
return m("form", {onSubmit: function onSubmit() {}},
|
||||||
|
m("input", {type: "checkbox", checked: true}),
|
||||||
|
m("input", {type: "checkbox", checked: false}),
|
||||||
|
m("fieldset",
|
||||||
|
m("label",
|
||||||
|
m("input", {type: "radio", checked: true})
|
||||||
|
),
|
||||||
|
m("label",
|
||||||
|
m("input", {type: "radio"})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m(ButtonBar, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ButtonBar = {
|
||||||
|
view : function () {
|
||||||
|
return m("button-bar",
|
||||||
|
m(Button,
|
||||||
|
{style: "width:10px; height:10px; border:1px solid #FFF;"},
|
||||||
|
"Normal CSS"
|
||||||
|
),
|
||||||
|
m(Button,
|
||||||
|
{style: "top:0 ; right: 20"},
|
||||||
|
"Poor CSS"
|
||||||
|
),
|
||||||
|
m(Button,
|
||||||
|
{style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true},
|
||||||
|
"Poorer CSS"
|
||||||
|
),
|
||||||
|
m(Button,
|
||||||
|
{style: {margin: 0, padding: "10px", overflow: "visible"}},
|
||||||
|
"Object CSS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Button = {
|
||||||
|
view : function (vnode) {
|
||||||
|
return m("button", vnode.attrs, vnode.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Main = {
|
||||||
|
view : function () {
|
||||||
|
return m(Form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Root = {
|
||||||
|
view : function () {
|
||||||
|
return m("div",
|
||||||
|
{class: "foo bar", "data-foo": "bar", p: 2},
|
||||||
|
m(Header, null),
|
||||||
|
m(Main, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.add({
|
||||||
|
name : "repeated trees (recycling)",
|
||||||
|
fn : function () {
|
||||||
|
m.render(scratch, [m(Root)])
|
||||||
|
m.render(scratch, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.add({
|
||||||
|
name : "repeated trees (no recycling)",
|
||||||
|
fn : function () {
|
||||||
|
m.render(scratch, [m(Root)])
|
||||||
|
m.render(scratch, [])
|
||||||
|
|
||||||
|
// Second empty render is to clear out the pool of nodes
|
||||||
|
// so that there's nothing that can be recycled
|
||||||
|
m.render(scratch, [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.run({
|
||||||
|
async : true
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
var parseURL = require("../test-utils/parseURL")
|
var parseURL = require("../test-utils/parseURL")
|
||||||
var callAsync = require("../test-utils/callAsync.js")
|
var callAsync = require("../test-utils/callAsync")
|
||||||
|
|
||||||
function debouncedAsync(f) {
|
function debouncedAsync(f) {
|
||||||
var ref
|
var ref
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue