Skip to content

Commit 38d57fd

Browse files
committed
chore: wip
chore: wip chore: wip
1 parent 1e00cce commit 38d57fd

9 files changed

Lines changed: 1123 additions & 1102 deletions

File tree

benchmark/binary-vs-api.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Crosswind: Compiled Binary vs JS/TS API Benchmark
4+
*
5+
* Compares two execution modes:
6+
* 1. JS/TS API — import and call CSSGenerator directly from TypeScript
7+
* 2. Compiled Binary — run the `crosswind build` CLI binary (bun --compile)
8+
*
9+
* This measures real-world end-to-end performance including:
10+
* - File I/O (reading HTML, writing CSS)
11+
* - Class extraction from HTML content
12+
* - CSS generation
13+
* - CSS output/serialization
14+
*/
15+
16+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs'
17+
import { execSync } from 'node:child_process'
18+
import { CSSGenerator } from '../packages/crosswind/src/generator'
19+
import { defaultConfig } from '../packages/crosswind/src/config'
20+
import { extractClasses } from '../packages/crosswind/src/parser'
21+
22+
const BINARY = new URL('../packages/crosswind/bin/crosswind', import.meta.url).pathname
23+
const FIXTURES_DIR = new URL('./fixtures', import.meta.url).pathname
24+
25+
const fixtures = [
26+
{ name: 'Small page (4 lines, ~20 classes)', file: 'small.html' },
27+
{ name: 'Medium page (100 lines, ~200 classes)', file: 'medium.html' },
28+
{ name: 'Large page (500 lines, ~1000 classes)', file: 'large.html' },
29+
]
30+
31+
function cleanup(path: string) {
32+
if (existsSync(path)) unlinkSync(path)
33+
}
34+
35+
console.log('='.repeat(70))
36+
console.log('Crosswind: Compiled Binary vs JS/TS API Benchmark')
37+
console.log('='.repeat(70))
38+
console.log(`Binary: ${BINARY}`)
39+
console.log(`Runtime: Bun ${Bun.version}`)
40+
console.log('')
41+
42+
// ============================================================================
43+
// BENCHMARK 1: JS/TS API (in-process)
44+
// ============================================================================
45+
46+
console.log('--- JS/TS API (in-process, no file I/O for generation) ---')
47+
console.log('')
48+
49+
for (const fixture of fixtures) {
50+
const html = readFileSync(`${FIXTURES_DIR}/${fixture.file}`, 'utf8')
51+
const classes = extractClasses(html)
52+
const classArray = Array.from(classes)
53+
54+
// Warm up
55+
const warmGen = new CSSGenerator(defaultConfig)
56+
warmGen.generateBatch(classArray)
57+
warmGen.toCSS(false)
58+
59+
// Benchmark: fresh generator per run (cold)
60+
const coldRuns = 100
61+
const coldStart = performance.now()
62+
for (let i = 0; i < coldRuns; i++) {
63+
const gen = new CSSGenerator(defaultConfig)
64+
gen.generateBatch(classArray)
65+
gen.toCSS(false)
66+
}
67+
const coldAvg = (performance.now() - coldStart) / coldRuns
68+
69+
// Benchmark: reuse generator (warm)
70+
const warmRuns = 1000
71+
const gen = new CSSGenerator(defaultConfig)
72+
const warmStart = performance.now()
73+
for (let i = 0; i < warmRuns; i++) {
74+
gen.reset()
75+
gen.generateBatch(classArray)
76+
gen.toCSS(false)
77+
}
78+
const warmAvg = (performance.now() - warmStart) / warmRuns
79+
80+
console.log(` ${fixture.name}`)
81+
console.log(` Classes: ${classArray.length} unique`)
82+
console.log(` Cold (new CSSGenerator each): ${coldAvg.toFixed(3)} ms`)
83+
console.log(` Warm (reset + generate): ${warmAvg.toFixed(3)} ms`)
84+
console.log('')
85+
}
86+
87+
// ============================================================================
88+
// BENCHMARK 2: Compiled Binary (subprocess)
89+
// ============================================================================
90+
91+
console.log('--- Compiled Binary (subprocess, full E2E with file I/O) ---')
92+
console.log('')
93+
94+
for (const fixture of fixtures) {
95+
const inputPath = `${FIXTURES_DIR}/${fixture.file}`
96+
const outputPath = `${FIXTURES_DIR}/_bench_output.css`
97+
cleanup(outputPath)
98+
99+
// Write a temp crosswind config
100+
const configPath = `${FIXTURES_DIR}/_bench_config.ts`
101+
writeFileSync(configPath, `
102+
export default {
103+
content: ['${inputPath}'],
104+
output: '${outputPath}',
105+
minify: false,
106+
}
107+
`)
108+
109+
// Warm up the binary (first run may be slower due to OS caching)
110+
try {
111+
execSync(`${BINARY} build --config ${configPath} --no-preflight 2>/dev/null`, { stdio: 'pipe' })
112+
} catch { /* ignore */ }
113+
cleanup(outputPath)
114+
115+
// Benchmark binary
116+
const runs = 10
117+
const times: number[] = []
118+
for (let i = 0; i < runs; i++) {
119+
cleanup(outputPath)
120+
const start = performance.now()
121+
try {
122+
execSync(`${BINARY} build --config ${configPath} --no-preflight 2>/dev/null`, { stdio: 'pipe' })
123+
} catch { /* ignore errors */ }
124+
times.push(performance.now() - start)
125+
}
126+
127+
const avg = times.reduce((a, b) => a + b, 0) / times.length
128+
const min = Math.min(...times)
129+
const max = Math.max(...times)
130+
131+
// Check output
132+
const outputExists = existsSync(outputPath)
133+
const outputSize = outputExists ? readFileSync(outputPath).length : 0
134+
135+
console.log(` ${fixture.name}`)
136+
console.log(` Avg: ${avg.toFixed(1)} ms (min: ${min.toFixed(1)} ms, max: ${max.toFixed(1)} ms)`)
137+
console.log(` Output: ${outputExists ? `${outputSize} bytes` : 'no output (build may have failed)'}`)
138+
console.log('')
139+
140+
cleanup(outputPath)
141+
cleanup(configPath)
142+
}
143+
144+
// ============================================================================
145+
// BENCHMARK 3: JS/TS API Full E2E (including file read + class extraction)
146+
// ============================================================================
147+
148+
console.log('--- JS/TS API Full E2E (file read + extract + generate + output) ---')
149+
console.log('')
150+
151+
for (const fixture of fixtures) {
152+
const inputPath = `${FIXTURES_DIR}/${fixture.file}`
153+
154+
const runs = 100
155+
const start = performance.now()
156+
let sink = 0
157+
for (let i = 0; i < runs; i++) {
158+
const html = readFileSync(inputPath, 'utf8')
159+
const classes = extractClasses(html)
160+
const gen = new CSSGenerator(defaultConfig)
161+
gen.generateBatch(Array.from(classes))
162+
const css = gen.toCSS(false)
163+
sink += css.length
164+
}
165+
const avg = (performance.now() - start) / runs
166+
167+
console.log(` ${fixture.name}`)
168+
console.log(` Full E2E (read + extract + generate + toCSS): ${avg.toFixed(3)} ms`)
169+
console.log('')
170+
}
171+
172+
console.log('='.repeat(70))
173+
console.log('Summary')
174+
console.log('='.repeat(70))
175+
console.log('')
176+
console.log('JS/TS API: Best for embedded use (dev servers, plugins, SSR)')
177+
console.log(' Near-zero startup, sub-millisecond generation')
178+
console.log('')
179+
console.log('Compiled Binary: Best for CI/CD builds and CLI usage')
180+
console.log(' Includes process startup + file I/O overhead')
181+
console.log('')

0 commit comments

Comments
 (0)