Skip to content

Commit d3eaee9

Browse files
marvinhagemeisterJoviDeCroock
authored andcommitted
Add very basic benchmark setup
This is just the server for now, without any runner
1 parent 862d9d6 commit d3eaee9

14 files changed

+823
-4
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<meta name="color-scheme" content="dark light" />
8+
<title>{%TITLE%}</title>
9+
</head>
10+
<body>
11+
<h1>{%NAME%}</h1>
12+
<script type="module" src="./index.js"></script>
13+
</body>
14+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { signal, computed } from "@preact/signals-core";
2+
import * as bench from "../measure";
3+
4+
const count = signal(0);
5+
const double = computed(() => count.value * 2);
6+
7+
bench.start();
8+
9+
for (let i = 0; i < 20000000; i++) {
10+
count.value++;
11+
double.value;
12+
}
13+
14+
bench.stop();

benches/cases/measure.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
let startTime = 0;
2+
3+
export function start() {
4+
startTime = performance.now();
5+
}
6+
7+
export function stop() {
8+
const end = performance.now();
9+
const duration = end - startTime;
10+
11+
const url = new URL(window.location.href);
12+
const test = url.pathname;
13+
14+
let memory = 0;
15+
if ("gc" in window && "memory" in window) {
16+
window.gc();
17+
memory = performance.memory.usedJSHeapSize / 1e6;
18+
}
19+
20+
// eslint-disable-next-line no-console
21+
console.log(
22+
`Time: %c${duration.toFixed(2)}ms ${
23+
memory > 0 ? `${memory}MB` : ""
24+
}%c- done`,
25+
"color:peachpuff",
26+
"color:inherit"
27+
);
28+
29+
return fetch("/results", {
30+
method: "POST",
31+
headers: {
32+
"Content-Type": "application/json",
33+
},
34+
body: JSON.stringify({ test, duration, memory }),
35+
});
36+
}

benches/index.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<meta name="color-scheme" content="dark light" />
8+
<title>Benchmarks</title>
9+
<link rel="stylesheet" href="./style.css" />
10+
</head>
11+
<body>
12+
<div class="page">
13+
<h1>Benchmarks</h1>
14+
<p>
15+
This is a list of benchmarks we use to measure the performance of
16+
singals with.
17+
</p>
18+
<p>View results on the <a href="/results">results page</a>.</p>
19+
<h2>Cases</h2>
20+
<ul>
21+
{%LIST%}
22+
</ul>
23+
</div>
24+
</body>
25+
</html>

benches/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "demo",
3+
"private": true,
4+
"scripts": {
5+
"start": "vite",
6+
"build": "vite build",
7+
"preview": "vite preview"
8+
},
9+
"dependencies": {
10+
"preact": "10.9.0",
11+
"@preact/signals-core": "workspace:../packages/core",
12+
"@preact/signals": "workspace:../packages/preact"
13+
},
14+
"devDependencies": {
15+
"@types/connect": "^3.4.35",
16+
"@types/express": "^4.17.14",
17+
"express": "^4.18.1",
18+
"tiny-glob": "^0.2.9",
19+
"vite": "^3.0.7"
20+
}
21+
}

benches/public/favicon.ico

15 KB
Binary file not shown.

benches/results.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<meta name="color-scheme" content="dark light" />
8+
<title>Results - Benchmarks</title>
9+
<link rel="stylesheet" href="./style.css" />
10+
</head>
11+
<body>
12+
<div class="page">
13+
<h1>Results</h1>
14+
<p>
15+
The numbers will be updated whenever you run a benchmark and refresh
16+
this page.
17+
</p>
18+
<table>
19+
<thead>
20+
<tr>
21+
<th>Benchmark Name</th>
22+
<th>Time Range</th>
23+
<th>Memory</th>
24+
</tr>
25+
</thead>
26+
<tbody>
27+
{%ITEMS%}
28+
</tbody>
29+
</table>
30+
</div>
31+
</body>
32+
</html>

benches/style.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
body {
2+
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
3+
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
4+
}
5+
6+
:root {
7+
--primary: #673ab8;
8+
--even: #f3f3f3;
9+
}
10+
11+
@media (prefers-color-scheme: dark) {
12+
:root {
13+
--even: #242424;
14+
}
15+
}
16+
17+
.page {
18+
margin: 0 auto;
19+
max-width: 40rem;
20+
padding: 2rem;
21+
}
22+
23+
table {
24+
border-collapse: collapse;
25+
margin: 25px 0;
26+
font-size: 0.9em;
27+
font-family: sans-serif;
28+
min-width: 400px;
29+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
30+
}
31+
32+
table thead tr {
33+
background-color: var(--primary);
34+
color: #ffffff;
35+
text-align: left;
36+
}
37+
38+
table th,
39+
table td {
40+
padding: 12px 15px;
41+
}
42+
43+
table tbody tr {
44+
border-bottom: 1px solid #dddddd;
45+
}
46+
47+
table tbody tr:nth-of-type(even) {
48+
background-color: var(--even);
49+
}
50+
51+
table tbody tr:last-of-type {
52+
border-bottom: 2px solid var(--primary);
53+
}
54+
55+
table tbody tr.active-row {
56+
font-weight: bold;
57+
color: var(--primary);
58+
}

benches/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"jsx": "react-jsx",
5+
"jsxImportSource": "preact",
6+
"module": "esnext"
7+
}
8+
}

benches/vite.config.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { defineConfig, Plugin } from "vite";
2+
import { resolve, posix } from "path";
3+
import fs from "fs";
4+
import { NextHandleFunction } from "connect";
5+
import * as express from "express";
6+
7+
// Automatically set up aliases for monorepo packages.
8+
// Uses built packages in prod, "source" field in dev.
9+
function packages(prod: boolean) {
10+
const alias: Record<string, string> = {};
11+
const root = resolve(__dirname, "../packages");
12+
for (let name of fs.readdirSync(root)) {
13+
if (name[0] === ".") continue;
14+
const p = resolve(root, name, "package.json");
15+
const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
16+
if (pkg.private) continue;
17+
const entry = prod ? "." : pkg.source;
18+
alias[pkg.name] = resolve(root, name, entry);
19+
}
20+
return alias;
21+
}
22+
23+
export default defineConfig(env => ({
24+
plugins: [
25+
indexPlugin(),
26+
multiSpa(["index.html", "results.html", "cases/**/*.html"]),
27+
],
28+
build: {
29+
polyfillModulePreload: false,
30+
cssCodeSplit: false,
31+
},
32+
resolve: {
33+
extensions: [".ts", ".tsx", ".js", ".jsx", ".d.ts"],
34+
alias: env.mode === "production" ? {} : packages(false),
35+
},
36+
}));
37+
38+
export interface BenchResult {
39+
url: string;
40+
time: number;
41+
memory: number;
42+
}
43+
44+
function escapeHtml(unsafe: string) {
45+
return unsafe
46+
.replace(/&/g, "&amp;")
47+
.replace(/</g, "&lt;")
48+
.replace(/>/g, "&gt;")
49+
.replace(/"/g, "&quot;")
50+
.replace(/'/g, "&#039;");
51+
}
52+
53+
function indexPlugin(): Plugin {
54+
const results = new Map<string, BenchResult>();
55+
56+
return {
57+
name: "index-plugin",
58+
configureServer(server) {
59+
server.middlewares.use(express.json());
60+
server.middlewares.use(async (req, res, next) => {
61+
if (req.url === "/results") {
62+
if (req.method === "GET") {
63+
const cases = await getBenchCases("cases/**/*.html");
64+
cases.htmlUrls.forEach(url => {
65+
if (!results.has(url)) {
66+
results.set(url, { url, time: 0, memory: 0 });
67+
}
68+
});
69+
70+
const items = Array.from(results.entries())
71+
.sort((a, b) => a[0].localeCompare(b[0]))
72+
.map(entry => {
73+
return `<tr>
74+
<td><a href="${encodeURI(entry[0])}">${escapeHtml(entry[0])}</a></td>
75+
<td>${entry[1].time.toFixed(2)}ms</td>
76+
<td>${entry[1].memory}MB</td>
77+
</tr>`;
78+
})
79+
.join("\n");
80+
81+
const html = fs
82+
.readFileSync(resolve(__dirname, "results.html"), "utf-8")
83+
.replace("{%ITEMS%}", items);
84+
res.end(html);
85+
return;
86+
} else if (req.method === "POST") {
87+
// @ts-ignore
88+
const { test, duration, memory } = req.body;
89+
if (
90+
typeof test !== "string" ||
91+
typeof duration !== "number" ||
92+
typeof memory !== "number"
93+
) {
94+
throw new Error("Invalid data");
95+
}
96+
results.set(test, { url: test, time: duration, memory });
97+
res.end();
98+
return;
99+
}
100+
}
101+
102+
next();
103+
});
104+
},
105+
async transformIndexHtml(html, data) {
106+
if (data.path === "/index.html") {
107+
const cases = await getBenchCases("cases/**/*.html");
108+
return html.replace(
109+
"{%LIST%}",
110+
cases.htmlEntries.length > 0
111+
? cases.htmlUrls
112+
.map(
113+
url =>
114+
`<li><a href="${encodeURI(url)}">${escapeHtml(
115+
url
116+
)}</a></li>`
117+
)
118+
.join("\n")
119+
: ""
120+
);
121+
}
122+
123+
const name = posix.basename(posix.dirname(data.path));
124+
return html.replace("{%TITLE%}", name).replace("{%NAME%}", name);
125+
},
126+
};
127+
}
128+
129+
// Vite plugin to serve and build multiple SPA roots (index.html dirs)
130+
import glob from "tiny-glob";
131+
132+
async function getBenchCases(entries: string | string[]) {
133+
let e = await Promise.all([entries].flat().map(x => glob(x)));
134+
const htmlEntries = Array.from(new Set(e.flat()));
135+
// sort by length, longest to shortest:
136+
const htmlUrls = htmlEntries
137+
.map(x => "/" + x)
138+
.sort((a, b) => b.length - a.length);
139+
return { htmlEntries, htmlUrls };
140+
}
141+
142+
function multiSpa(entries: string | string[]): Plugin {
143+
let htmlEntries: string[];
144+
let htmlUrls: string[];
145+
146+
const middleware: NextHandleFunction = (req, res, next) => {
147+
const url = req.url!;
148+
// ignore /@x and file extension URLs:
149+
if (/(^\/@|\.[a-z]+(?:\?.*)?$)/i.test(url)) return next();
150+
// match the longest index.html parent path:
151+
for (let html of htmlUrls) {
152+
if (!html.endsWith("/index.html")) continue;
153+
if (!url.startsWith(html.slice(0, -10))) continue;
154+
req.url = html;
155+
break;
156+
}
157+
next();
158+
};
159+
160+
return {
161+
name: "multi-spa",
162+
async config() {
163+
const cases = await getBenchCases(entries);
164+
htmlEntries = cases.htmlEntries;
165+
htmlUrls = cases.htmlUrls;
166+
},
167+
buildStart(options) {
168+
options.input = htmlEntries;
169+
},
170+
configurePreviewServer(server) {
171+
server.middlewares.use(middleware);
172+
},
173+
configureServer(server) {
174+
server.middlewares.use(middleware);
175+
},
176+
};
177+
}

0 commit comments

Comments
 (0)