Skip to content
Merged
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
20 changes: 20 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,23 @@ tasks:
cmds:
- cp packages/benchmarks/results/latest.json packages/benchmarks/results/baseline.json
- echo "Baseline saved from latest.json"

bench:competitive:
desc: Run competitive benchmarks (vs MUI)
deps: [bench:setup]
cmds:
- pnpm bench:competitive {{.CLI_ARGS}}

bench:competitive:json:
desc: Run competitive benchmarks with JSON output
deps: [bench:setup]
cmds:
- pnpm bench:competitive:json {{.CLI_ARGS}}

bench:competitive:report:
desc: Generate competitive comparison report
preconditions:
- sh: test -f packages/benchmarks/results/competitive/latest.json
msg: "No results/competitive/latest.json found. Run 'task bench:competitive:json' first."
cmds:
- node packages/benchmarks/scripts/generate-competitive-report.js
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"bench:browser": "pnpm --filter @omniview/benchmarks exec vitest bench --config vitest.config.browser.ts --run",
"bench:trace": "TRACE=true pnpm bench:browser",
"bench:deterministic": "DETERMINISTIC=true pnpm bench",
"bench:json": "mkdir -p packages/benchmarks/results && pnpm --filter @omniview/benchmarks exec vitest bench --run --outputJson=results/latest.json"
"bench:json": "mkdir -p packages/benchmarks/results && pnpm --filter @omniview/benchmarks exec vitest bench --run --outputJson=results/latest.json",
"bench:competitive": "pnpm --filter @omniview/benchmarks exec vitest bench --config vitest.config.competitive.ts --run",
"bench:competitive:json": "mkdir -p packages/benchmarks/results/competitive && pnpm --filter @omniview/benchmarks exec vitest bench --config vitest.config.competitive.ts --run --outputJson=results/competitive/latest.json"
},
"devDependencies": {
"@eslint/js": "^9.35.0",
Expand Down
13 changes: 13 additions & 0 deletions packages/benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ For AI agents running optimization sprints:
4. `task bench:json` → read `results/comparison.json`
5. `task bench:report` → generate PR summary

## Competitive Benchmarks

Compare @omniview/base-ui against MUI and raw HTML:

```bash
task bench:competitive # Run all competitive benchmarks
task bench:competitive:json # JSON output
task bench:competitive:report # Generate comparison report
pnpm bench:competitive -- --filter "Button" # Single component
```

Results are written to `results/competitive/report.md` with per-component overhead multipliers vs raw HTML.

## CI

CodSpeed runs automatically on PRs that touch component or benchmark code. It comments on PRs with performance diffs and flags regressions > 5%.
5 changes: 5 additions & 0 deletions packages/benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
"scripts": {
"bench": "vitest bench --run",
"bench:browser": "vitest bench --config vitest.config.browser.ts --run",
"bench:competitive": "vitest bench --config vitest.config.competitive.ts --run",
"perf": "rm -f .reassure/current.perf && vitest --run --config vitest.config.perf.ts",
"perf:baseline": "rm -f .reassure/baseline.perf && REASSURE_OUTPUT_FILE=.reassure/baseline.perf vitest --run --config vitest.config.perf.ts",
"perf:compare": "node scripts/perf-compare.mjs"
},
"devDependencies": {
"@callstack/reassure-compare": "^1.4.1",
"@codspeed/vitest-plugin": "^5.2.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.3.0",
"@mui/material": "^7.3.0",
"@omniview/base-ui": "workspace:*",
"@omniview/editors": "workspace:*",
"@tanstack/react-table": "^8.21.3",
Expand Down
135 changes: 135 additions & 0 deletions packages/benchmarks/scripts/generate-competitive-report.js
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`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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)
PY

Repository: omniviewdev/ui

Length of output: 145


🏁 Script executed:

cat -n packages/benchmarks/scripts/generate-competitive-report.js | head -80

Repository: 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 -20

Repository: 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 -10

Repository: 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.ts

Repository: 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 -10

Repository: omniviewdev/ui

Length of output: 230


🏁 Script executed:

# Check the competitive vitest config
cat packages/benchmarks/vitest.config.competitive.ts

Repository: omniviewdev/ui

Length of output: 1310


🏁 Script executed:

# Look for competitive benchmark files
find packages/benchmarks/src/competitive -name "*.competitive.*" 2>/dev/null | head -5

Repository: 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 -60

Repository: 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 f

Repository: omniviewdev/ui

Length of output: 104


🏁 Script executed:

cat packages/benchmarks/src/utils/bench-compare.ts

Repository: 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 N/A cells, malformed benchmark names are silently dropped, and an empty report (if no benchmarks match) still exits 0. Add explicit validation to throw on malformed benchmark names, unknown implementation labels, and empty extraction results.

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
Verify each finding against the current code and only fix it if needed.

In `@packages/benchmarks/scripts/generate-competitive-report.js` around lines 43 -
53, The current loop in generate-competitive-report.js silently skips malformed
benchmark names and accepts any impl label; update the parsing in the for (const
bench of group.benchmarks ?? []) block to fail fast: after const match =
bench.name.match(/^(.+?)\s+\[(.+)]$/) throw an Error if !match (include
bench.name in the message) instead of continuing, validate match[2] (the impl)
against an explicit allowedImpls set you add (e.g., const ALLOWED_IMPLS = new
Set([...])) and throw if the impl is unknown, and after collecting results (the
results array) throw an Error if results.length === 0 so the script exits
non-zero for empty extraction; reference the variables match, bench.name,
match[1]/match[2], results and suite.replace when implementing these checks.

}
}
}
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');
23 changes: 23 additions & 0 deletions packages/benchmarks/src/competitive/Button.competitive.tsx
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,
});
});
13 changes: 13 additions & 0 deletions packages/benchmarks/src/competitive/Checkbox.competitive.tsx
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 />),
});
});
31 changes: 31 additions & 0 deletions packages/benchmarks/src/competitive/Popover.competitive.tsx
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>
</>,
),
});
});
48 changes: 48 additions & 0 deletions packages/benchmarks/src/competitive/Select.competitive.tsx
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>,
),
});
});
13 changes: 13 additions & 0 deletions packages/benchmarks/src/competitive/Switch.competitive.tsx
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 />),
});
});
41 changes: 41 additions & 0 deletions packages/benchmarks/src/competitive/TextField.competitive.tsx
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,
});
});
Loading
Loading