summaryrefslogtreecommitdiff
path: root/preact/benches/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-23 16:46:06 -0300
committerSebastian <sebasjm@gmail.com>2021-08-23 16:48:30 -0300
commit38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch)
tree453dbf70000cc5e338b06201af1eaca8343f8f73 /preact/benches/src
parentf26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff)
downloadnode-vendor-master.tar.gz
node-vendor-master.tar.bz2
node-vendor-master.zip
added web vendorsHEADmaster
Diffstat (limited to 'preact/benches/src')
-rw-r--r--preact/benches/src/02_replace1k.html67
-rw-r--r--preact/benches/src/03_update10th1k_x16.html78
-rw-r--r--preact/benches/src/07_create10k.html49
-rw-r--r--preact/benches/src/filter_list.html92
-rw-r--r--preact/benches/src/hydrate1k.html145
-rw-r--r--preact/benches/src/keyed-children/components.js152
-rw-r--r--preact/benches/src/keyed-children/index.js29
-rw-r--r--preact/benches/src/keyed-children/store.js119
-rw-r--r--preact/benches/src/many_updates.html112
-rw-r--r--preact/benches/src/text_update.html34
-rw-r--r--preact/benches/src/util.js98
11 files changed, 975 insertions, 0 deletions
diff --git a/preact/benches/src/02_replace1k.html b/preact/benches/src/02_replace1k.html
new file mode 100644
index 0000000..e6b6270
--- /dev/null
+++ b/preact/benches/src/02_replace1k.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>replace all rows</title>
+ <meta name="description" content="updating all 1,000 rows" />
+ <style>
+ .preloadicon {
+ display: none;
+ }
+ .glyphicon-remove:before {
+ content: '⨯';
+ }
+ </style>
+ </head>
+ <body>
+ <div id="main"></div>
+ <script type="module">
+ import {
+ measureName,
+ measureMemory,
+ testElementText,
+ afterFrame,
+ afterFrameAsync,
+ markRunStart,
+ markRunEnd
+ } from './util.js';
+ import * as framework from 'framework';
+ import { render } from '../src/keyed-children/index.js';
+
+ const { run } = render(framework, document.getElementById('main'));
+
+ async function main() {
+ const elementSelector = 'tr:first-child > td:first-child';
+
+ // MUST BE KEPT IN SYNC WITH WARMUP COUNT IN benches/scripts/config.js
+ const WARMUP_COUNT = 5;
+ for (let i = 0; i < WARMUP_COUNT; i++) {
+ markRunStart(`warmup-${i}`);
+ run();
+ await markRunEnd(`warmup-${i}`);
+
+ await afterFrameAsync();
+ testElementText(elementSelector, (i * 1000 + 1).toFixed());
+ }
+
+ await afterFrameAsync();
+
+ afterFrame(function () {
+ testElementText(elementSelector, WARMUP_COUNT + '001');
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ });
+
+ markRunStart('final');
+ performance.mark('start');
+ run();
+ await markRunEnd('final');
+ }
+
+ afterFrame(main);
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/03_update10th1k_x16.html b/preact/benches/src/03_update10th1k_x16.html
new file mode 100644
index 0000000..9de8fdb
--- /dev/null
+++ b/preact/benches/src/03_update10th1k_x16.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>partial update</title>
+ <meta
+ name="description"
+ content="updating every 10th row for 1,000 rows (3 warmup runs). 16x CPU slowdown."
+ />
+ <style>
+ .preloadicon {
+ display: none;
+ }
+ .glyphicon-remove:before {
+ content: "⨯";
+ }
+ </style>
+ </head>
+ <body>
+ <div id="main"></div>
+ <script type="module">
+ import {
+ measureName,
+ measureMemory,
+ afterFrame,
+ afterFrameAsync,
+ getRowLinkSel,
+ testElement,
+ testElementTextContains
+ } from './util.js';
+ import * as framework from 'framework';
+ import { render } from '../src/keyed-children/index.js';
+
+ const { run: mount, update } = render(
+ framework,
+ document.getElementById('main')
+ );
+
+ function repeat(pattern, repeats) {
+ let result = '';
+ for (let i = 0; i < repeats; i++) {
+ result += pattern;
+ }
+
+ return result;
+ }
+
+ async function init() {
+ mount();
+
+ await afterFrameAsync();
+ testElement(getRowLinkSel(1000));
+
+ for (let i = 0; i < 3; i++) {
+ update();
+
+ await afterFrameAsync();
+ testElementTextContains(getRowLinkSel(991), repeat(' !!!', i + 1));
+ }
+ }
+
+ async function run() {
+ performance.mark('start');
+ update();
+
+ await afterFrameAsync();
+ testElementTextContains(getRowLinkSel(991), repeat(' !!!', 3 + 1));
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ }
+
+ init().then(run);
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/07_create10k.html b/preact/benches/src/07_create10k.html
new file mode 100644
index 0000000..728d472
--- /dev/null
+++ b/preact/benches/src/07_create10k.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>create many rows</title>
+ <meta name="description" content="creating 10,000 rows" />
+ <style>
+ .preloadicon {
+ display: none;
+ }
+ .glyphicon-remove:before {
+ content: '⨯';
+ }
+ </style>
+ </head>
+ <body>
+ <div id="main"></div>
+ <script type="module">
+ import {
+ measureName,
+ measureMemory,
+ testElementText,
+ afterFrame
+ } from './util.js';
+ import * as framework from 'framework';
+ import { render } from '../src/keyed-children/index.js';
+
+ const { runLots } = render(framework, document.getElementById('main'));
+
+ async function main() {
+ const elementSelector = 'tr:last-child > td:first-child';
+
+ performance.mark('start');
+ runLots();
+
+ afterFrame(() => {
+ testElementText(elementSelector, '10000');
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ });
+ }
+
+ main();
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/filter_list.html b/preact/benches/src/filter_list.html
new file mode 100644
index 0000000..d31e1af
--- /dev/null
+++ b/preact/benches/src/filter_list.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Patching HTML</title>
+ <style>
+ .items {
+ margin: 1em 0;
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ }
+
+ .items > * {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 3em;
+ height: 2em;
+ margin: 0;
+ padding: 0;
+ background: #eee;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="app"></div>
+ <script type="module">
+ import { measureName, measureMemory } from './util.js';
+ import { createRoot, createElement as h, Component } from 'framework';
+
+ function Row(props) {
+ return h('article', null, props.children);
+ }
+
+ function App(props) {
+ return h('div', {}, [
+ h(
+ 'div',
+ { class: 'items' },
+ props.items.map(id => h(Row, { key: id }, id))
+ )
+ ]);
+ }
+
+ const count = 1000;
+ const start = 20;
+ const end = 600;
+
+ const newItems = () =>
+ Array(count)
+ .fill(0)
+ .map((item, i) => i);
+ let items = newItems();
+ let currentItems = items;
+
+ const root = createRoot(document.getElementById('app'));
+ root.render(h(App, { items }));
+
+ function runPatch() {
+ items = newItems().filter(id => {
+ const isVisible = currentItems.includes(id);
+ return id >= start && id <= end ? !isVisible : isVisible;
+ });
+ currentItems = items;
+
+ root.render(h(App, { items }));
+ }
+
+ async function warmup() {
+ const count = 25;
+
+ for (let i = 0; i < count; i++) {
+ runPatch();
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ }
+
+ warmup().then(async () => {
+ performance.mark('start');
+ runPatch();
+ await new Promise(r => requestAnimationFrame(r));
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ });
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/hydrate1k.html b/preact/benches/src/hydrate1k.html
new file mode 100644
index 0000000..bf43448
--- /dev/null
+++ b/preact/benches/src/hydrate1k.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>hydrate 1k table rows</title>
+ <meta name="description" content="hydrating 1,000 rows" />
+ <style>
+ .preloadicon {
+ display: none;
+ }
+ .glyphicon-remove:before {
+ content: '⨯';
+ }
+ </style>
+ </head>
+ <body>
+ <template id="template"></template>
+ <script type="module">
+ import {
+ measureName,
+ measureMemory,
+ testElementText,
+ afterFrame,
+ afterFrameAsync
+ } from './util.js';
+ import * as framework from 'framework';
+ import { getComponents } from '../src/keyed-children/components.js';
+ import { Store } from '../src/keyed-children/store.js';
+
+ /** @type {HTMLTemplateElement} */
+ const template = document.getElementById('template');
+ const { Main } = getComponents(framework);
+ const { createRoot, createElement } = framework;
+
+ const firstRowSel = 'tr:first-child > td:first-child';
+ const lastRowSel = 'tr:last-child > td:first-child';
+
+ const baseStore = new Store();
+ baseStore.run();
+
+ /**
+ * Delete the old hydrate root and create a new one with a clone of the
+ * template's content
+ */
+ function setupHydrateRoot() {
+ const hydrateRootId = 'hydrate-root';
+ let hydrateRoot = document.getElementById(hydrateRootId);
+ if (hydrateRoot) {
+ hydrateRoot.remove();
+ }
+
+ hydrateRoot = document.createElement('div');
+ hydrateRoot.id = hydrateRootId;
+ hydrateRoot.appendChild(template.content.cloneNode(true));
+ document.body.appendChild(hydrateRoot);
+ return hydrateRoot;
+ }
+
+ /** Render the app inside the template tag */
+ async function initializeTemplate() {
+ // Initialize template
+ createRoot(template.content).render(
+ createElement(Main, { store: baseStore })
+ );
+ await afterFrameAsync();
+ }
+
+ /**
+ * Click the second row's remove link and ensure the number of rows before
+ * and after the click are as expected
+ */
+ async function clickRemove(root, label, expectedBefore, expectedAfter) {
+ let rowCount = root.querySelectorAll('tr').length;
+ if (rowCount !== expectedBefore) {
+ throw new Error(
+ `${label}: Incorrect number of rows before remove click. Expected ${expectedBefore} but got ${rowCount}`
+ );
+ }
+
+ const removeLink = root.querySelector(
+ 'tr:nth-child(2) td:nth-child(3) a'
+ );
+ removeLink.click();
+ await afterFrameAsync();
+
+ rowCount = root.querySelectorAll('tr').length;
+ if (rowCount !== expectedAfter) {
+ throw new Error(
+ `${label}: Incorrect number of rows after after remove click. Expected ${expectedAfter} but got ${rowCount}`
+ );
+ }
+ }
+
+ async function warmupRun(i) {
+ // Test out hydrate and ensure it works
+ const hydrateRoot = setupHydrateRoot();
+
+ // Verify initial hydrate root isn't already hydrated and is static
+ testElementText(firstRowSel, '1');
+ testElementText(lastRowSel, '1000');
+ await clickRemove(hydrateRoot, `WARMUP ${i} - prehydrate`, 1000, 1000);
+
+ const store = new Store();
+ store.data = baseStore.data.slice();
+ createRoot(hydrateRoot).hydrate(createElement(Main, { store }));
+
+ // Verify hydrate has correct markup and is properly hydrated
+ testElementText(firstRowSel, '1');
+ testElementText(lastRowSel, '1000');
+ await clickRemove(hydrateRoot, `WARMUP ${i} - posthydrate`, 1000, 999);
+ }
+
+ function timedRun() {
+ afterFrame(function () {
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ });
+
+ const hydrateRoot = setupHydrateRoot();
+ const store = new Store();
+ store.data = baseStore.data.slice();
+
+ performance.mark('start');
+ createRoot(hydrateRoot).hydrate(createElement(Main, { store }));
+ }
+
+ async function main() {
+ // const WARMUP_COUNT = 5;
+ const WARMUP_COUNT = 5;
+ for (let i = 0; i < WARMUP_COUNT; i++) {
+ await warmupRun(i);
+ }
+
+ await afterFrameAsync();
+
+ timedRun();
+ }
+
+ initializeTemplate().then(main);
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/keyed-children/components.js b/preact/benches/src/keyed-children/components.js
new file mode 100644
index 0000000..0960b89
--- /dev/null
+++ b/preact/benches/src/keyed-children/components.js
@@ -0,0 +1,152 @@
+import { Store } from './store.js';
+
+/**
+ * @param {import('./index').Framework} framework
+ */
+export function getComponents({ createElement, Component }) {
+ class Row extends Component {
+ constructor(props) {
+ super(props);
+ this.onDelete = this.onDelete.bind(this);
+ this.onClick = this.onClick.bind(this);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ nextProps.data !== this.props.data ||
+ nextProps.styleClass !== this.props.styleClass
+ );
+ }
+
+ onDelete() {
+ this.props.onDelete(this.props.data.id);
+ }
+
+ onClick() {
+ this.props.onClick(this.props.data.id);
+ }
+
+ render() {
+ let { styleClass, onClick, onDelete, data } = this.props;
+ return createElement(
+ 'tr',
+ {
+ className: styleClass
+ },
+ createElement(
+ 'td',
+ {
+ className: 'col-md-1'
+ },
+ data.id
+ ),
+ createElement(
+ 'td',
+ {
+ className: 'col-md-4'
+ },
+ createElement(
+ 'a',
+ {
+ onClick: this.onClick
+ },
+ data.label
+ )
+ ),
+ createElement(
+ 'td',
+ {
+ className: 'col-md-1'
+ },
+ createElement(
+ 'a',
+ {
+ onClick: this.onDelete
+ },
+ createElement('span', {
+ className: 'glyphicon glyphicon-remove',
+ 'aria-hidden': 'true'
+ })
+ )
+ ),
+ createElement('td', {
+ className: 'col-md-6'
+ })
+ );
+ }
+ }
+
+ class Main extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { store: props.store ?? new Store() };
+ this.select = this.select.bind(this);
+ this.delete = this.delete.bind(this);
+
+ // @ts-ignore
+ window.app = this;
+ }
+ run() {
+ this.state.store.run();
+ this.setState({ store: this.state.store });
+ }
+ add() {
+ this.state.store.add();
+ this.setState({ store: this.state.store });
+ }
+ update() {
+ this.state.store.update();
+ this.setState({ store: this.state.store });
+ }
+ select(id) {
+ this.state.store.select(id);
+ this.setState({ store: this.state.store });
+ }
+ delete(id) {
+ this.state.store.delete(id);
+ this.setState({ store: this.state.store });
+ }
+ runLots() {
+ this.state.store.runLots();
+ this.setState({ store: this.state.store });
+ }
+ clear() {
+ this.state.store.clear();
+ this.setState({ store: this.state.store });
+ }
+ swapRows() {
+ this.state.store.swapRows();
+ this.setState({ store: this.state.store });
+ }
+ render() {
+ let rows = this.state.store.data.map((d, i) => {
+ return createElement(Row, {
+ key: d.id,
+ data: d,
+ onClick: this.select,
+ onDelete: this.delete,
+ styleClass: d.id === this.state.store.selected ? 'danger' : ''
+ });
+ });
+ return createElement(
+ 'div',
+ {
+ className: 'container'
+ },
+ createElement(
+ 'table',
+ {
+ className: 'table table-hover table-striped test-data'
+ },
+ createElement('tbody', {}, rows)
+ ),
+ createElement('span', {
+ className: 'preloadicon glyphicon glyphicon-remove',
+ 'aria-hidden': 'true'
+ })
+ );
+ }
+ }
+
+ return { Main, Row };
+}
diff --git a/preact/benches/src/keyed-children/index.js b/preact/benches/src/keyed-children/index.js
new file mode 100644
index 0000000..ece1265
--- /dev/null
+++ b/preact/benches/src/keyed-children/index.js
@@ -0,0 +1,29 @@
+import { getComponents } from './components.js';
+
+/**
+ * @typedef Framework
+ * @property {(type: any, props?: any, ...children: any) => JSX.Element} createElement
+ * @property {(root: HTMLElement) => ({ render(vnode: JSX.Element): void; hydrate(vnode: JSX.Element): void; })} createRoot
+ * @property {any} Component
+ *
+ * @param {Framework} framework
+ * @param {HTMLElement} rootDom
+ */
+export function render(framework, rootDom) {
+ const { Main } = getComponents(framework);
+ framework.createRoot(rootDom).render(framework.createElement(Main));
+
+ /** @type {Main} */
+ // @ts-ignore
+ const app = window.app;
+ return {
+ run: app.run.bind(app),
+ add: app.add.bind(app),
+ update: app.update.bind(app),
+ select: app.select.bind(app),
+ delete: app.delete.bind(app),
+ runLots: app.runLots.bind(app),
+ clear: app.clear.bind(app),
+ swapRows: app.swapRows.bind(app)
+ };
+}
diff --git a/preact/benches/src/keyed-children/store.js b/preact/benches/src/keyed-children/store.js
new file mode 100644
index 0000000..413208d
--- /dev/null
+++ b/preact/benches/src/keyed-children/store.js
@@ -0,0 +1,119 @@
+function _random(max) {
+ return Math.round(Math.random() * 1000) % max;
+}
+
+export class Store {
+ constructor() {
+ this.data = [];
+ this.selected = undefined;
+ this.id = 1;
+ }
+ buildData(count = 1000) {
+ var adjectives = [
+ 'pretty',
+ 'large',
+ 'big',
+ 'small',
+ 'tall',
+ 'short',
+ 'long',
+ 'handsome',
+ 'plain',
+ 'quaint',
+ 'clean',
+ 'elegant',
+ 'easy',
+ 'angry',
+ 'crazy',
+ 'helpful',
+ 'mushy',
+ 'odd',
+ 'unsightly',
+ 'adorable',
+ 'important',
+ 'inexpensive',
+ 'cheap',
+ 'expensive',
+ 'fancy'
+ ];
+ var colours = [
+ 'red',
+ 'yellow',
+ 'blue',
+ 'green',
+ 'pink',
+ 'brown',
+ 'purple',
+ 'brown',
+ 'white',
+ 'black',
+ 'orange'
+ ];
+ var nouns = [
+ 'table',
+ 'chair',
+ 'house',
+ 'bbq',
+ 'desk',
+ 'car',
+ 'pony',
+ 'cookie',
+ 'sandwich',
+ 'burger',
+ 'pizza',
+ 'mouse',
+ 'keyboard'
+ ];
+ var data = [];
+ for (var i = 0; i < count; i++)
+ data.push({
+ id: this.id++,
+ label:
+ adjectives[_random(adjectives.length)] +
+ ' ' +
+ colours[_random(colours.length)] +
+ ' ' +
+ nouns[_random(nouns.length)]
+ });
+ return data;
+ }
+ updateData(mod = 10) {
+ for (let i = 0; i < this.data.length; i += 10) {
+ this.data[i] = Object.assign({}, this.data[i], {
+ label: this.data[i].label + ' !!!'
+ });
+ }
+ }
+ delete(id) {
+ var idx = this.data.findIndex(d => d.id === id);
+ this.data.splice(idx, 1);
+ }
+ run() {
+ this.data = this.buildData();
+ this.selected = undefined;
+ }
+ add() {
+ this.data = this.data.concat(this.buildData(1000));
+ }
+ update() {
+ this.updateData();
+ }
+ select(id) {
+ this.selected = id;
+ }
+ runLots() {
+ this.data = this.buildData(10000);
+ this.selected = undefined;
+ }
+ clear() {
+ this.data = [];
+ this.selected = undefined;
+ }
+ swapRows() {
+ if (this.data.length > 998) {
+ var a = this.data[1];
+ this.data[1] = this.data[998];
+ this.data[998] = a;
+ }
+ }
+}
diff --git a/preact/benches/src/many_updates.html b/preact/benches/src/many_updates.html
new file mode 100644
index 0000000..e59b304
--- /dev/null
+++ b/preact/benches/src/many_updates.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Patching HTML</title>
+ <style>
+ .hello {
+ color: red;
+ }
+
+ .bye {
+ color: blue;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="root"></div>
+ <script type="module">
+ import { measureName, measureMemory } from './util.js';
+ import { createRoot, createElement as h } from 'framework';
+
+ const state = {
+ msg: 'hello',
+ list: new Array(1000).fill(0).map((_, i) => ({
+ i,
+ text: 'foobar' + i
+ }))
+ };
+
+ let counter = 0;
+ function App() {
+ return h(
+ 'div',
+ { id: 'app' },
+ h('p', null, '> ', ++counter, ' <'),
+ h('p', null, state.msg),
+ ...state.list.map((obj, i) =>
+ h(
+ 'div',
+ { key: i, title: state.msg + i },
+ h('span', { className: state.msg }, obj.text),
+ h('span', { className: 'baz' }, 'one'),
+ h('span', { className: 'qux' }, 'two'),
+ h(
+ 'div',
+ null,
+ h('span', { className: 'qux' }, 'three'),
+ h('span', { className: 'qux' }, 'four'),
+ h('span', { className: 'baz' }, 'five'),
+ h(
+ 'div',
+ null,
+ h('span', { className: 'qux' }, 'six'),
+ h('span', { className: 'baz' }, 'seven'),
+ h('span', { className: state.msg }, 'eight')
+ )
+ )
+ )
+ )
+ );
+ }
+
+ const root = createRoot(document.getElementById('root'));
+
+ // const p = performance.now();
+ root.render(h(App));
+ // console.log(`mount: ${(performance.now() - p).toFixed(2)}ms`);
+
+ // const patchResults = [];
+
+ function runPatch() {
+ // const s = performance.now();
+ state.msg = state.msg === 'hello' ? 'bye' : 'hello';
+ state.list[0].text = state.msg;
+ root.render(h(App));
+ // patchResults.push(performance.now() - s);
+ }
+
+ async function warmup() {
+ // const count = 100;
+ const count = 25;
+
+ for (let i = 0; i < count; i++) {
+ runPatch();
+ await new Promise(r => requestAnimationFrame(r));
+ }
+
+ // let fastest = Infinity;
+ // const total = patchResults.reduce((all, cur) => {
+ // if (cur < fastest) {
+ // fastest = cur;
+ // }
+ // return all + cur;
+ // }, 0);
+
+ // console.log(`${count} runs average: ${(total / count).toFixed(2)}ms`);
+ // console.log(`fastest run: ${fastest.toFixed(2)}ms`);
+ }
+
+ warmup().then(async () => {
+ performance.mark('start');
+ runPatch();
+ await new Promise(r => requestAnimationFrame(r));
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+
+ measureMemory();
+ });
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/text_update.html b/preact/benches/src/text_update.html
new file mode 100644
index 0000000..6874558
--- /dev/null
+++ b/preact/benches/src/text_update.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Text Updates</title>
+ </head>
+ <body>
+ <div id="root"></div>
+ <script type="module">
+ import { measureName, measureMemory } from './util.js';
+ import { createRoot, createElement } from 'framework';
+
+ const root = createRoot(document.getElementById('root'));
+
+ function component({ randomValue }) {
+ return createElement('div', {}, [
+ createElement('h2', {}, 'Test ' + randomValue),
+ createElement('h1', {}, `===${randomValue}===`)
+ ]);
+ }
+
+ let result;
+
+ performance.mark('start');
+ for (let i = 0; i < 100; i++) {
+ root.render(createElement(component, { randomValue: i }));
+ }
+ performance.mark('stop');
+ performance.measure(measureName, 'start', 'stop');
+ measureMemory();
+ </script>
+ </body>
+</html>
diff --git a/preact/benches/src/util.js b/preact/benches/src/util.js
new file mode 100644
index 0000000..64c4765
--- /dev/null
+++ b/preact/benches/src/util.js
@@ -0,0 +1,98 @@
+// import afterFrame from "../node_modules/afterframe/dist/afterframe.module.js";
+import afterFrame from 'afterframe';
+
+export { afterFrame };
+
+export const measureName = 'duration';
+
+let promise = null;
+export function afterFrameAsync() {
+ if (promise === null) {
+ promise = new Promise(resolve =>
+ afterFrame(time => {
+ promise = null;
+ resolve(time);
+ })
+ );
+ }
+
+ return promise;
+}
+
+export function measureMemory() {
+ if ('gc' in window && 'memory' in performance) {
+ // Report results in MBs
+ window.gc();
+ window.usedJSHeapSize = performance.memory.usedJSHeapSize / 1e6;
+ } else {
+ window.usedJSHeapSize = 0;
+ }
+}
+
+export function markRunStart(runId) {
+ performance.mark(`run-${runId}-start`);
+}
+
+let staticPromise = Promise.resolve();
+export function markRunEnd(runId) {
+ return staticPromise.then(() => {
+ performance.mark(`run-${runId}-end`);
+ performance.measure(
+ `run-${runId}`,
+ `run-${runId}-start`,
+ `run-${runId}-end`
+ );
+ });
+}
+
+export function getRowIdSel(index) {
+ return `tbody > tr:nth-child(${index}) > td:first-child`;
+}
+
+export function getRowLinkSel(index) {
+ return `tbody > tr:nth-child(${index}) > td:nth-child(2) > a`;
+}
+
+/**
+ * @param {string} selector
+ * @returns {Element}
+ */
+export function getBySelector(selector) {
+ const element = document.querySelector(selector);
+ if (element == null) {
+ throw new Error(`Could not find element matching selector: ${selector}`);
+ }
+
+ return element;
+}
+
+export function testElement(selector) {
+ const testElement = document.querySelector(selector);
+ if (testElement == null) {
+ throw new Error(
+ 'Test failed. Rendering after one paint was not successful'
+ );
+ }
+}
+
+export function testElementText(selector, expectedText) {
+ const elm = document.querySelector(selector);
+ if (elm == null) {
+ throw new Error('Could not find element matching selector: ' + selector);
+ }
+
+ if (elm.textContent != expectedText) {
+ throw new Error(
+ `Element did not have expected text. Expected: '${expectedText}' Actual: '${elm.textContent}'`
+ );
+ }
+}
+
+export function testElementTextContains(selector, expectedText) {
+ const elm = getBySelector(selector);
+ if (!elm.textContent.includes(expectedText)) {
+ throw new Error(
+ `Element did not include expected text. Expected to include: '${expectedText}' Actual: '${elm.textContent}'`
+ );
+ }
+}