Skip to content

Commit c9fec36

Browse files
authored
Merge pull request #43 from bombshell-dev/codspeed-wizard-1779643506234
Adds performance benchmarks with tinybench, benchmark CI via CodSpeed
2 parents 819da8e + e5ae33e commit c9fec36

9 files changed

Lines changed: 632 additions & 22 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Benchmark
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
# `workflow_dispatch` allows CodSpeed to trigger backtest
9+
# performance analysis in order to generate initial data.
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
jobs:
17+
benchmarks:
18+
name: Run benchmarks
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
with:
25+
submodules: true
26+
27+
- name: Setup Deno
28+
uses: denoland/setup-deno@v2
29+
with:
30+
deno-version: v2.x
31+
32+
- name: Setup Node
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: 22
36+
37+
- name: Build WASM
38+
run: make
39+
40+
- name: Install dependencies
41+
run: deno install
42+
43+
- name: Run benchmarks
44+
uses: CodSpeedHQ/action@v4
45+
with:
46+
mode: simulation
47+
# IMPORTANT! deno task bench fails in CI due to incompatible V8 bindings
48+
run: node bench/mod.ts

README.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,25 +149,28 @@ Pass pointer state to `render()` to have clayterm do hit detection and return
149149
pointer events in addition to the byte sequence.
150150

151151
```typescript
152-
let { output, events } = term.render([
153-
open("root", {
154-
layout: { width: grow(), height: grow(), direction: "ltr" },
155-
}),
156-
open("sidebar", {
157-
layout: { width: fixed(20), height: grow() },
158-
bg: rgba(30, 30, 40),
159-
}),
160-
text("Sidebar"),
161-
close(),
162-
open("main", {
163-
layout: { width: grow(), height: grow() },
164-
}),
165-
text("Main content"),
166-
close(),
167-
close(),
168-
], {
169-
pointer: { x: mouseX, y: mouseY, down: mouseDown },
170-
});
152+
let { output, events } = term.render(
153+
[
154+
open("root", {
155+
layout: { width: grow(), height: grow(), direction: "ltr" },
156+
}),
157+
open("sidebar", {
158+
layout: { width: fixed(20), height: grow() },
159+
bg: rgba(30, 30, 40),
160+
}),
161+
text("Sidebar"),
162+
close(),
163+
open("main", {
164+
layout: { width: grow(), height: grow() },
165+
}),
166+
text("Main content"),
167+
close(),
168+
close(),
169+
],
170+
{
171+
pointer: { x: mouseX, y: mouseY, down: mouseDown },
172+
},
173+
);
171174

172175
for (let event of events) {
173176
// { type: "pointerenter", id: "sidebar" }

bench/input.bench.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Bench } from "tinybench";
2+
import { withCodSpeed } from "@codspeed/tinybench-plugin";
3+
import { createInput } from "../input.ts";
4+
5+
function bytes(...values: number[]): Uint8Array {
6+
return new Uint8Array(values);
7+
}
8+
9+
function str(s: string): Uint8Array {
10+
return new TextEncoder().encode(s);
11+
}
12+
13+
let input = await createInput({ escLatency: 25 });
14+
15+
let longBurst = new Uint8Array(200);
16+
for (let i = 0; i < 200; i++) {
17+
longBurst[i] = 0x61 + (i % 26);
18+
}
19+
20+
let bench = withCodSpeed(new Bench());
21+
22+
bench
23+
.add("printable ASCII (single char)", () => {
24+
input.scan(bytes(0x61));
25+
})
26+
.add("printable ASCII (short string)", () => {
27+
input.scan(str("hello world"));
28+
})
29+
.add("arrow key (CSI sequence)", () => {
30+
input.scan(bytes(0x1b, 0x5b, 0x41));
31+
})
32+
.add("modifier combo (Ctrl+Shift+Arrow)", () => {
33+
input.scan(bytes(0x1b, 0x5b, 0x31, 0x3b, 0x38, 0x41));
34+
})
35+
.add("SGR mouse press", () => {
36+
input.scan(str("\x1b[<0;35;12M"));
37+
})
38+
.add("multi-event burst (arrows + text)", () => {
39+
input.scan(bytes(0x1b, 0x5b, 0x41, 0x1b, 0x5b, 0x42, 0x68, 0x69));
40+
})
41+
.add("UTF-8 3-byte character", () => {
42+
input.scan(bytes(0xe4, 0xb8, 0xad));
43+
})
44+
.add("UTF-8 4-byte emoji", () => {
45+
input.scan(bytes(0xf0, 0x9f, 0x8e, 0x89));
46+
})
47+
.add("Kitty protocol (CSI u with modifiers)", () => {
48+
input.scan(str("\x1b[97;3u"));
49+
})
50+
.add("long input burst (200 bytes)", () => {
51+
input.scan(longBurst);
52+
});
53+
54+
await bench.run();
55+
console.table(bench.table());

bench/mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import "./input.bench.ts";
2+
import "./render.bench.ts";
3+
import "./ops.bench.ts";

bench/ops.bench.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Bench } from "tinybench";
2+
import { withCodSpeed } from "@codspeed/tinybench-plugin";
3+
import { close, fixed, grow, open, pack, rgba, text } from "../ops.ts";
4+
import type { Op } from "../ops.ts";
5+
6+
function makeBuf(size: number): ArrayBuffer {
7+
return new ArrayBuffer(size);
8+
}
9+
10+
let simpleOps: Op[] = [
11+
open("root", {
12+
layout: { width: grow(), height: grow(), direction: "ttb" },
13+
}),
14+
text("Hello, World!"),
15+
close(),
16+
];
17+
18+
let complexOps: Op[] = [
19+
open("root", {
20+
layout: { width: grow(), height: grow(), direction: "ttb" },
21+
}),
22+
open("header", {
23+
layout: {
24+
width: grow(),
25+
height: fixed(3),
26+
padding: { left: 1, right: 1 },
27+
direction: "ltr",
28+
},
29+
bg: rgba(30, 30, 40),
30+
border: {
31+
color: rgba(100, 100, 120),
32+
bottom: 1,
33+
},
34+
}),
35+
text("Title", { color: rgba(255, 255, 255), fontSize: 1 }),
36+
close(),
37+
open("body", {
38+
layout: {
39+
width: grow(),
40+
height: grow(),
41+
direction: "ltr",
42+
gap: 1,
43+
},
44+
}),
45+
open("sidebar", {
46+
layout: {
47+
width: fixed(20),
48+
height: grow(),
49+
direction: "ttb",
50+
padding: { left: 1, right: 1, top: 1 },
51+
},
52+
bg: rgba(25, 25, 35),
53+
border: {
54+
color: rgba(60, 60, 80),
55+
right: 1,
56+
},
57+
}),
58+
text("Menu Item 1"),
59+
text("Menu Item 2"),
60+
text("Menu Item 3"),
61+
close(),
62+
open("main", {
63+
layout: {
64+
width: grow(),
65+
height: grow(),
66+
direction: "ttb",
67+
padding: { left: 2, top: 1 },
68+
},
69+
}),
70+
text("Main content area with longer text to exercise the encoder"),
71+
close(),
72+
close(),
73+
open("footer", {
74+
layout: {
75+
width: grow(),
76+
height: fixed(1),
77+
padding: { left: 1 },
78+
direction: "ltr",
79+
},
80+
bg: rgba(30, 30, 40),
81+
}),
82+
text("Status: OK"),
83+
close(),
84+
close(),
85+
];
86+
87+
let listOps: Op[] = [
88+
open("root", {
89+
layout: { width: grow(), height: grow(), direction: "ttb" },
90+
}),
91+
...Array.from({ length: 50 }, (_, i) => [
92+
open(`item-${i}`, {
93+
layout: {
94+
width: grow(),
95+
height: fixed(1),
96+
padding: { left: 2 },
97+
direction: "ltr",
98+
},
99+
bg: i % 2 === 0 ? rgba(30, 30, 40) : rgba(35, 35, 45),
100+
}),
101+
text(`List item ${i}: some description text`),
102+
close(),
103+
]).flat(),
104+
close(),
105+
];
106+
107+
let bench = withCodSpeed(new Bench());
108+
109+
bench
110+
.add("simple tree (root + text)", () => {
111+
let buf = makeBuf(4096);
112+
pack(simpleOps, buf, 0);
113+
})
114+
.add("complex layout (header + sidebar + main + footer)", () => {
115+
let buf = makeBuf(8192);
116+
pack(complexOps, buf, 0);
117+
})
118+
.add("large list (50 items)", () => {
119+
let buf = makeBuf(32768);
120+
pack(listOps, buf, 0);
121+
});
122+
123+
await bench.run();
124+
console.table(bench.table());

0 commit comments

Comments
 (0)