-
Notifications
You must be signed in to change notification settings - Fork 0
feat(benchmarks): competitive benchmarks — @omniview/base-ui vs MUI vs raw HTML #30
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
Changes from all commits
b3624fa
9c00cfd
990d091
e4145eb
f75b93e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; | ||
| import { join, dirname } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const resultsDir = join(__dirname, '..', 'results', 'competitive'); | ||
|
|
||
| function loadJSON(filepath) { | ||
| if (!existsSync(filepath)) return null; | ||
| try { | ||
| return JSON.parse(readFileSync(filepath, 'utf-8')); | ||
| } catch (err) { | ||
| throw new Error(`Failed to parse JSON in ${filepath}: ${err.message}`); | ||
| } | ||
| } | ||
|
|
||
| function formatHz(hz) { | ||
| if (!Number.isFinite(hz)) return 'N/A'; | ||
| if (hz >= 1000) return `${(hz / 1000).toFixed(1)}k`; | ||
| return hz.toFixed(0); | ||
| } | ||
|
|
||
| function formatOverhead(raw, lib) { | ||
| if (!Number.isFinite(raw) || !Number.isFinite(lib) || raw <= 0 || lib <= 0) return 'N/A'; | ||
| return `${(raw / lib).toFixed(1)}x`; | ||
| } | ||
|
|
||
| /** | ||
| * Extract benchmarks from Vitest 4.x JSON, parsing the [label] suffix | ||
| * to separate implementation name from benchmark name. | ||
| * | ||
| * Bench names look like: "mount [raw]", "mount [@omniview/base-ui]", "mount [@mui/material]" | ||
| */ | ||
| function extractCompetitiveBenchmarks(data) { | ||
| const results = []; | ||
| for (const file of data.files ?? []) { | ||
| for (const group of file.groups ?? []) { | ||
| const parts = (group.fullName ?? '').split(' > '); | ||
| const suite = parts[parts.length - 1] || 'unknown'; | ||
|
|
||
| for (const bench of group.benchmarks ?? []) { | ||
| // Parse "mount [raw]" → benchName="mount", impl="raw" | ||
| const match = bench.name.match(/^(.+?)\s+\[(.+)]$/); | ||
| if (!match) continue; | ||
|
|
||
| results.push({ | ||
| suite: suite.replace(' competitive', ''), | ||
| benchName: match[1], | ||
| impl: match[2], | ||
| hz: bench.hz, | ||
| }); | ||
|
Comment on lines
+43
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
python - <<'PY'
import json, pathlib, re, sys
path = pathlib.Path("packages/benchmarks/results/competitive/latest.json")
if not path.exists():
sys.exit("Generate packages/benchmarks/results/competitive/latest.json before running this check.")
data = json.loads(path.read_text())
pattern = re.compile(r'^(.+?)\s+\[(.+)\]$')
expected = {"raw", "@omniview/base-ui", "@mui/material"}
total = 0
bad_names = []
unknown_impls = []
for file in data.get("files", []):
for group in file.get("groups", []):
for bench in group.get("benchmarks", []):
total += 1
name = bench.get("name", "")
match = pattern.match(name)
if not match:
bad_names.append((group.get("fullName"), name))
continue
if match.group(2) not in expected:
unknown_impls.append((group.get("fullName"), name, match.group(2)))
print({"total_benchmarks": total, "bad_names": len(bad_names), "unknown_impls": len(unknown_impls)})
for row in bad_names[:20]:
print("bad_name:", row)
for row in unknown_impls[:20]:
print("unknown_impl:", row)
PYRepository: omniviewdev/ui Length of output: 145 🏁 Script executed: cat -n packages/benchmarks/scripts/generate-competitive-report.js | head -80Repository: omniviewdev/ui Length of output: 3158 🏁 Script executed: cat -n packages/benchmarks/scripts/generate-competitive-report.js | sed -n '80,120p'Repository: omniviewdev/ui Length of output: 1757 🏁 Script executed: cat -n packages/benchmarks/scripts/generate-competitive-report.js | sed -n '120,160p'Repository: omniviewdev/ui Length of output: 602 🏁 Script executed: # Check if there are any tests for this script
find packages/benchmarks -type f -name "*.test.*" -o -name "*.spec.*" | head -20Repository: omniviewdev/ui Length of output: 108 🏁 Script executed: # Look for example or actual benchmark results
find packages/benchmarks/results -type f -name "*.json" 2>/dev/null | head -10Repository: omniviewdev/ui Length of output: 40 🏁 Script executed: # Check the test file to see if there are validation tests
cat packages/benchmarks/src/utils/bench-render.test.tsRepository: omniviewdev/ui Length of output: 1890 🏁 Script executed: # Look for vitest configuration and benchmark files
find packages/benchmarks -name "*.config.*" -o -name "vitest.config.*" | head -10Repository: omniviewdev/ui Length of output: 230 🏁 Script executed: # Check the competitive vitest config
cat packages/benchmarks/vitest.config.competitive.tsRepository: omniviewdev/ui Length of output: 1310 🏁 Script executed: # Look for competitive benchmark files
find packages/benchmarks/src/competitive -name "*.competitive.*" 2>/dev/null | head -5Repository: omniviewdev/ui Length of output: 355 🏁 Script executed: # Examine one benchmark file to see the naming pattern
cat packages/benchmarks/src/competitive/Button.competitive.tsx | head -60Repository: omniviewdev/ui Length of output: 954 🏁 Script executed: # Check the bench-compare utilities to see how benchmark names are generated
find packages/benchmarks -name "*bench-compare*" -type fRepository: omniviewdev/ui Length of output: 104 🏁 Script executed: cat packages/benchmarks/src/utils/bench-compare.tsRepository: omniviewdev/ui Length of output: 2192 Add validation to fail fast on unexpected benchmark names and implementation labels. The parser silently skips any benchmarks that don't match the naming pattern, accepts any implementation label without validation, and can produce an empty report without signaling an error. This creates multiple degradation paths: typos in implementation labels result in Proposed fix for (const bench of group.benchmarks ?? []) {
// Parse "mount [raw]" → benchName="mount", impl="raw"
const match = bench.name.match(/^(.+?)\s+\[(.+)]$/);
- if (!match) continue;
+ if (!match) {
+ throw new Error(
+ `Unsupported benchmark name "${bench.name}" in "${group.fullName ?? 'unknown'}". Expected "<metric> [<implementation>]".`,
+ );
+ }
+ if (!['raw', '@omniview/base-ui', '@mui/material'].includes(match[2])) {
+ throw new Error(
+ `Unsupported implementation label "${match[2]}" in "${bench.name}".`,
+ );
+ }
results.push({
suite: suite.replace(' competitive', ''),
benchName: match[1],
impl: match[2],
hz: bench.hz,
});
}
@@
function generateReport(data) {
const benchmarks = extractCompetitiveBenchmarks(data);
+ if (benchmarks.length === 0) {
+ throw new Error(
+ 'No competitive benchmarks were extracted from latest.json. Verify the Vitest JSON schema and benchmark naming convention.',
+ );
+ }🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
|
|
||
| function generateReport(data) { | ||
| const benchmarks = extractCompetitiveBenchmarks(data); | ||
|
|
||
| // Group by suite + benchName | ||
| const groups = new Map(); | ||
| for (const b of benchmarks) { | ||
| const key = `${b.suite}::${b.benchName}`; | ||
| if (!groups.has(key)) groups.set(key, { suite: b.suite, benchName: b.benchName, impls: {} }); | ||
| groups.get(key).impls[b.impl] = b.hz; | ||
| } | ||
|
|
||
| const lines = [ | ||
| '# Competitive Benchmark Report\n', | ||
| `**Date:** ${new Date().toISOString()}`, | ||
| `**Environment:** JSDOM, Vitest ${data.version || 'unknown'}\n`, | ||
| '| Component | Metric | Raw (ops/s) | @omniview | overhead | @mui | overhead |', | ||
| '|-----------|--------|-------------|-----------|----------|------|----------|', | ||
| ]; | ||
|
|
||
| const overheadValues = { ov: [], mui: [] }; | ||
|
|
||
| const sortedGroups = [...groups.values()].sort((a, b) => | ||
| `${a.suite}|${a.benchName}`.localeCompare(`${b.suite}|${b.benchName}`), | ||
| ); | ||
|
|
||
| for (const g of sortedGroups) { | ||
| const rawHz = g.impls['raw'] ?? NaN; | ||
| const ovHz = g.impls['@omniview/base-ui'] ?? NaN; | ||
| const muiHz = g.impls['@mui/material'] ?? NaN; | ||
|
|
||
| const ovOverhead = rawHz / ovHz; | ||
| const muiOverhead = rawHz / muiHz; | ||
|
|
||
| if (Number.isFinite(ovOverhead) && ovOverhead > 0) overheadValues.ov.push(ovOverhead); | ||
| if (Number.isFinite(muiOverhead) && muiOverhead > 0) overheadValues.mui.push(muiOverhead); | ||
|
|
||
| lines.push( | ||
| `| ${g.suite} | ${g.benchName} | ${formatHz(rawHz)} | ${formatHz(ovHz)} | ${formatOverhead(rawHz, ovHz)} | ${formatHz(muiHz)} | ${formatOverhead(rawHz, muiHz)} |`, | ||
| ); | ||
| } | ||
|
|
||
| // Geometric mean | ||
| function geoMean(values) { | ||
| if (values.length === 0) return NaN; | ||
| const logSum = values.reduce((sum, v) => sum + Math.log(v), 0); | ||
| return Math.exp(logSum / values.length); | ||
| } | ||
|
|
||
| const ovGeo = geoMean(overheadValues.ov); | ||
| const muiGeo = geoMean(overheadValues.mui); | ||
|
|
||
| lines.push( | ||
| `| **Geo. mean** | | | | **${Number.isFinite(ovGeo) ? ovGeo.toFixed(1) + 'x' : 'N/A'}** | | **${Number.isFinite(muiGeo) ? muiGeo.toFixed(1) + 'x' : 'N/A'}** |`, | ||
| ); | ||
|
|
||
| lines.push(''); | ||
| lines.push('> Overhead = raw ops/s / library ops/s. Lower is better (1.0x = no overhead).'); | ||
|
|
||
| return lines.join('\n'); | ||
| } | ||
|
|
||
| // Main | ||
| if (!existsSync(resultsDir)) { | ||
| mkdirSync(resultsDir, { recursive: true }); | ||
| } | ||
|
|
||
| const latest = loadJSON(join(resultsDir, 'latest.json')); | ||
|
|
||
| if (!latest) { | ||
| console.error('No results/competitive/latest.json found. Run `pnpm bench:competitive:json` first.'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const report = generateReport(latest); | ||
| writeFileSync(join(resultsDir, 'report.md'), report); | ||
| console.log('Wrote results/competitive/report.md'); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { describe } from 'vitest'; | ||
| import { Button as OvButton } from '@omniview/base-ui'; | ||
| import MuiButton from '@mui/material/Button'; | ||
| import { benchCompare, benchCompareMany } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| describe('Button competitive', () => { | ||
| benchCompare('mount', { | ||
| 'raw': () => wrapRaw(<button type="button">Click</button>), | ||
| '@omniview/base-ui': () => wrapOv(<OvButton>Click</OvButton>), | ||
| '@mui/material': () => wrapMui(<MuiButton>Click</MuiButton>), | ||
| }); | ||
|
|
||
| benchCompareMany('mount 100', 100, { | ||
| 'raw': (i) => <button key={i} type="button">Item {i}</button>, | ||
| '@omniview/base-ui': (i) => <OvButton key={i}>Item {i}</OvButton>, | ||
| '@mui/material': (i) => <MuiButton key={i}>Item {i}</MuiButton>, | ||
| }, { | ||
| 'raw': wrapRaw, | ||
| '@omniview/base-ui': wrapOv, | ||
| '@mui/material': wrapMui, | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { describe } from 'vitest'; | ||
| import { Checkbox as OvCheckbox } from '@omniview/base-ui'; | ||
| import MuiCheckbox from '@mui/material/Checkbox'; | ||
| import { benchCompare } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| describe('Checkbox competitive', () => { | ||
| benchCompare('mount', { | ||
| 'raw': () => wrapRaw(<input type="checkbox" />), | ||
| '@omniview/base-ui': () => wrapOv(<OvCheckbox />), | ||
| '@mui/material': () => wrapMui(<MuiCheckbox />), | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { describe } from 'vitest'; | ||
| import { Popover as OvPopover } from '@omniview/base-ui'; | ||
| import MuiPopover from '@mui/material/Popover'; | ||
| import { benchCompare } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| describe('Popover competitive', () => { | ||
| // Measures overhead of popover machinery when closed. | ||
| // Raw baseline is just the trigger element. | ||
| benchCompare('mount (closed)', { | ||
| 'raw': () => wrapRaw(<button type="button">Open</button>), | ||
| '@omniview/base-ui': () => wrapOv( | ||
| <OvPopover.Root> | ||
| <OvPopover.Trigger>Open</OvPopover.Trigger> | ||
| <OvPopover.Portal> | ||
| <OvPopover.Positioner> | ||
| <OvPopover.Popup>Popover content</OvPopover.Popup> | ||
| </OvPopover.Positioner> | ||
| </OvPopover.Portal> | ||
| </OvPopover.Root>, | ||
| ), | ||
| '@mui/material': () => wrapMui( | ||
| <> | ||
| <button type="button">Open</button> | ||
| <MuiPopover open={false} anchorReference="none"> | ||
| <div>Popover content</div> | ||
| </MuiPopover> | ||
| </>, | ||
| ), | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { describe } from 'vitest'; | ||
| import { Select as OvSelect } from '@omniview/base-ui'; | ||
| import MuiSelect from '@mui/material/Select'; | ||
| import MuiMenuItem from '@mui/material/MenuItem'; | ||
| import { benchCompare } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| const options = Array.from({ length: 5 }, (_, i) => ({ | ||
| value: `option-${i}`, | ||
| label: `Option ${i}`, | ||
| })); | ||
|
|
||
| describe('Select competitive', () => { | ||
| benchCompare('mount (closed)', { | ||
| 'raw': () => wrapRaw( | ||
| <select defaultValue="option-0"> | ||
| {options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)} | ||
| </select>, | ||
| ), | ||
| '@omniview/base-ui': () => wrapOv( | ||
| <OvSelect.Root defaultValue="option-0"> | ||
| <OvSelect.Trigger> | ||
| <OvSelect.Value placeholder="Choose..." /> | ||
| </OvSelect.Trigger> | ||
| <OvSelect.Portal> | ||
| <OvSelect.Positioner> | ||
| <OvSelect.Popup> | ||
| <OvSelect.List> | ||
| {options.map((o) => ( | ||
| <OvSelect.Item key={o.value} value={o.value}> | ||
| <OvSelect.ItemText>{o.label}</OvSelect.ItemText> | ||
| </OvSelect.Item> | ||
| ))} | ||
| </OvSelect.List> | ||
| </OvSelect.Popup> | ||
| </OvSelect.Positioner> | ||
| </OvSelect.Portal> | ||
| </OvSelect.Root>, | ||
| ), | ||
| '@mui/material': () => wrapMui( | ||
| <MuiSelect defaultValue="option-0" size="small"> | ||
| {options.map((o) => ( | ||
| <MuiMenuItem key={o.value} value={o.value}>{o.label}</MuiMenuItem> | ||
| ))} | ||
| </MuiSelect>, | ||
| ), | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { describe } from 'vitest'; | ||
| import { Switch as OvSwitch } from '@omniview/base-ui'; | ||
| import MuiSwitch from '@mui/material/Switch'; | ||
| import { benchCompare } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| describe('Switch competitive', () => { | ||
| benchCompare('mount', { | ||
| 'raw': () => wrapRaw(<input type="checkbox" role="switch" aria-checked="false" />), | ||
| '@omniview/base-ui': () => wrapOv(<OvSwitch />), | ||
| '@mui/material': () => wrapMui(<MuiSwitch />), | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { describe } from 'vitest'; | ||
| import { TextField as OvTextField } from '@omniview/base-ui'; | ||
| import MuiTextField from '@mui/material/TextField'; | ||
| import { benchCompare, benchCompareMany } from '../utils/bench-compare'; | ||
| import { wrapOv, wrapMui, wrapRaw } from './implementations/wrappers'; | ||
|
|
||
| describe('TextField competitive', () => { | ||
| benchCompare('mount', { | ||
| 'raw': () => wrapRaw( | ||
| <div> | ||
| <label htmlFor="raw-input">Name</label> | ||
| <input id="raw-input" type="text" placeholder="Enter name" /> | ||
| </div>, | ||
| ), | ||
| '@omniview/base-ui': () => wrapOv( | ||
| <OvTextField.Root> | ||
| <OvTextField.Label>Name</OvTextField.Label> | ||
| <OvTextField.Control placeholder="Enter name" /> | ||
| </OvTextField.Root>, | ||
| ), | ||
| '@mui/material': () => wrapMui( | ||
| <MuiTextField label="Name" placeholder="Enter name" size="small" />, | ||
| ), | ||
| }); | ||
|
|
||
| benchCompareMany('mount 100', 100, { | ||
| 'raw': (i) => <input key={i} type="text" placeholder={`Field ${i}`} />, | ||
| '@omniview/base-ui': (i) => ( | ||
| <OvTextField.Root key={i}> | ||
| <OvTextField.Control placeholder={`Field ${i}`} /> | ||
| </OvTextField.Root> | ||
| ), | ||
| '@mui/material': (i) => ( | ||
| <MuiTextField key={i} placeholder={`Field ${i}`} size="small" /> | ||
| ), | ||
| }, { | ||
| 'raw': wrapRaw, | ||
| '@omniview/base-ui': wrapOv, | ||
| '@mui/material': wrapMui, | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.