Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add very basic benchmark setup #142

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions benches/cases/create-computed/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>{%TITLE%}</title>
</head>
<body>
<h1>{%NAME%}</h1>
<script type="module" src="./index.js"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions benches/cases/create-computed/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { signal, computed } from "@preact/signals-core";
import * as bench from "../measure";

const count = signal(0);
const double = computed(() => count.value * 2);

bench.start();

for (let i = 0; i < 20000000; i++) {
count.value++;
double.value;
}

bench.stop();
36 changes: 36 additions & 0 deletions benches/cases/measure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
let startTime = 0;

export function start() {
startTime = performance.now();
}

export function stop() {
const end = performance.now();
const duration = end - startTime;

const url = new URL(window.location.href);
const test = url.pathname;

let memory = 0;
if ("gc" in window && "memory" in window) {
window.gc();
memory = performance.memory.usedJSHeapSize / 1e6;
}

// eslint-disable-next-line no-console
console.log(
`Time: %c${duration.toFixed(2)}ms ${
memory > 0 ? `${memory}MB` : ""
}%c- done`,
"color:peachpuff",
"color:inherit"
);

return fetch("/results", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ test, duration, memory }),
});
}
25 changes: 25 additions & 0 deletions benches/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Benchmarks</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="page">
<h1>Benchmarks</h1>
<p>
This is a list of benchmarks we use to measure the performance of
singals with.
</p>
<p>View results on the <a href="/results">results page</a>.</p>
<h2>Cases</h2>
<ul>
{%LIST%}
</ul>
</div>
</body>
</html>
21 changes: 21 additions & 0 deletions benches/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "demo",
"private": true,
"scripts": {
"start": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"preact": "10.9.0",
"@preact/signals-core": "workspace:../packages/core",
"@preact/signals": "workspace:../packages/preact"
},
"devDependencies": {
"@types/connect": "^3.4.35",
"@types/express": "^4.17.14",
"express": "^4.18.1",
"tiny-glob": "^0.2.9",
"vite": "^3.0.7"
}
}
Binary file added benches/public/favicon.ico
Binary file not shown.
32 changes: 32 additions & 0 deletions benches/results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Results - Benchmarks</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="page">
<h1>Results</h1>
<p>
The numbers will be updated whenever you run a benchmark and refresh
this page.
</p>
<table>
<thead>
<tr>
<th>Benchmark Name</th>
<th>Time Range</th>
<th>Memory</th>
</tr>
</thead>
<tbody>
{%ITEMS%}
</tbody>
</table>
</div>
</body>
</html>
58 changes: 58 additions & 0 deletions benches/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

:root {
--primary: #673ab8;
--even: #f3f3f3;
}

@media (prefers-color-scheme: dark) {
:root {
--even: #242424;
}
}

.page {
margin: 0 auto;
max-width: 40rem;
padding: 2rem;
}

table {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}

table thead tr {
background-color: var(--primary);
color: #ffffff;
text-align: left;
}

table th,
table td {
padding: 12px 15px;
}

table tbody tr {
border-bottom: 1px solid #dddddd;
}

table tbody tr:nth-of-type(even) {
background-color: var(--even);
}

table tbody tr:last-of-type {
border-bottom: 2px solid var(--primary);
}

table tbody tr.active-row {
font-weight: bold;
color: var(--primary);
}
8 changes: 8 additions & 0 deletions benches/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"module": "esnext"
}
}
177 changes: 177 additions & 0 deletions benches/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { defineConfig, Plugin } from "vite";
import { resolve, posix } from "path";
import fs from "fs";
import { NextHandleFunction } from "connect";
import * as express from "express";

// Automatically set up aliases for monorepo packages.
// Uses built packages in prod, "source" field in dev.
function packages(prod: boolean) {
const alias: Record<string, string> = {};
const root = resolve(__dirname, "../packages");
for (let name of fs.readdirSync(root)) {
if (name[0] === ".") continue;
const p = resolve(root, name, "package.json");
const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
if (pkg.private) continue;
const entry = prod ? "." : pkg.source;
alias[pkg.name] = resolve(root, name, entry);
}
return alias;
}

export default defineConfig(env => ({
plugins: [
indexPlugin(),
multiSpa(["index.html", "results.html", "cases/**/*.html"]),
],
build: {
polyfillModulePreload: false,
cssCodeSplit: false,
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".d.ts"],
alias: env.mode === "production" ? {} : packages(false),
},
}));

export interface BenchResult {
url: string;
time: number;
memory: number;
}

function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

function indexPlugin(): Plugin {
const results = new Map<string, BenchResult>();

return {
name: "index-plugin",
configureServer(server) {
server.middlewares.use(express.json());
server.middlewares.use(async (req, res, next) => {
if (req.url === "/results") {
if (req.method === "GET") {
const cases = await getBenchCases("cases/**/*.html");
cases.htmlUrls.forEach(url => {
if (!results.has(url)) {
results.set(url, { url, time: 0, memory: 0 });
}
});

const items = Array.from(results.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(entry => {
return `<tr>
<td><a href="${encodeURI(entry[0])}">${escapeHtml(entry[0])}</a></td>
<td>${entry[1].time.toFixed(2)}ms</td>
<td>${entry[1].memory}MB</td>
</tr>`;
})
.join("\n");

const html = fs
.readFileSync(resolve(__dirname, "results.html"), "utf-8")
.replace("{%ITEMS%}", items);
res.end(html);
return;
} else if (req.method === "POST") {
// @ts-ignore
const { test, duration, memory } = req.body;
if (
typeof test !== "string" ||
typeof duration !== "number" ||
typeof memory !== "number"
) {
throw new Error("Invalid data");
}
results.set(test, { url: test, time: duration, memory });
res.end();
return;
}
}

next();
});
},
async transformIndexHtml(html, data) {
if (data.path === "/index.html") {
const cases = await getBenchCases("cases/**/*.html");
return html.replace(
"{%LIST%}",
cases.htmlEntries.length > 0
? cases.htmlUrls
.map(
url =>
`<li><a href="${encodeURI(url)}">${escapeHtml(
url
)}</a></li>`
)
.join("\n")
: ""
);
}

const name = posix.basename(posix.dirname(data.path));
return html.replace("{%TITLE%}", name).replace("{%NAME%}", name);
},
};
}

// Vite plugin to serve and build multiple SPA roots (index.html dirs)
import glob from "tiny-glob";

async function getBenchCases(entries: string | string[]) {
let e = await Promise.all([entries].flat().map(x => glob(x)));
const htmlEntries = Array.from(new Set(e.flat()));
// sort by length, longest to shortest:
const htmlUrls = htmlEntries
.map(x => "/" + x)
.sort((a, b) => b.length - a.length);
return { htmlEntries, htmlUrls };
}

function multiSpa(entries: string | string[]): Plugin {
let htmlEntries: string[];
let htmlUrls: string[];

const middleware: NextHandleFunction = (req, res, next) => {
const url = req.url!;
// ignore /@x and file extension URLs:
if (/(^\/@|\.[a-z]+(?:\?.*)?$)/i.test(url)) return next();
// match the longest index.html parent path:
for (let html of htmlUrls) {
if (!html.endsWith("/index.html")) continue;
if (!url.startsWith(html.slice(0, -10))) continue;
req.url = html;
break;
}
next();
};

return {
name: "multi-spa",
async config() {
const cases = await getBenchCases(entries);
htmlEntries = cases.htmlEntries;
htmlUrls = cases.htmlUrls;
},
buildStart(options) {
options.input = htmlEntries;
},
configurePreviewServer(server) {
server.middlewares.use(middleware);
},
configureServer(server) {
server.middlewares.use(middleware);
},
};
}
Loading