Skip to content

Commit 14920b7

Browse files
authored
perf: speed up bench script for CI (#15)
* perf: speed up bench script for CI * fix: omit empty feature comparisons from report in quick mode * fix: include key benchmarks table in markdown report * refactor: move AB-only definitions inside quick mode guard * ci: retrigger
1 parent 0748898 commit 14920b7

File tree

1 file changed

+129
-148
lines changed

1 file changed

+129
-148
lines changed

tests/bench/run.ts

Lines changed: 129 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
measureWithContentDetection,
2121
} from "../../src/utils/measure";
2222
import { createNormalizedLogo } from "../../src/utils/normalize";
23-
import { fmtNs, fmtP, type TTestResult, welchTTest } from "./welch";
23+
import { fmtNs, fmtP, welchTTest } from "./welch";
2424

2525
const origCreateElement = document.createElement.bind(document);
2626
document.createElement = ((tag: string, options?: ElementCreationOptions) => {
@@ -64,14 +64,21 @@ const svgFiles = readdirSync(LOGOS_DIR)
6464
.filter((f) => f.endsWith(".svg"))
6565
.sort();
6666

67-
console.log(`Loading ${svgFiles.length} real SVGs from static/logos/…`);
67+
const QUICK = !!(process.env.BENCH_QUICK || process.env.CI);
6868

69-
const allLogos: RenderedLogo[] = [];
70-
for (const file of svgFiles) {
71-
const buf = Buffer.from(await Bun.file(join(LOGOS_DIR, file)).arrayBuffer());
72-
const { img, width, height } = await loadSvgAtWidth(buf, 400);
73-
allLogos.push({ name: file.replace(/\.svg$/, ""), img, width, height });
74-
}
69+
console.log(
70+
`Loading ${svgFiles.length} real SVGs from static/logos/…${QUICK ? " (quick mode)" : ""}`,
71+
);
72+
73+
const allLogos: RenderedLogo[] = await Promise.all(
74+
svgFiles.map(async (file) => {
75+
const buf = Buffer.from(
76+
await Bun.file(join(LOGOS_DIR, file)).arrayBuffer(),
77+
);
78+
const { img, width, height } = await loadSvgAtWidth(buf, 400);
79+
return { name: file.replace(/\.svg$/, ""), img, width, height };
80+
}),
81+
);
7582

7683
const byPixels = [...allLogos].sort(
7784
(a, b) => a.width * a.height - b.width * b.height,
@@ -104,10 +111,10 @@ const normalized20 = Array.from({ length: 20 }, (_, i) => {
104111

105112
const logos20 = allLogos.slice(0, 20);
106113

107-
const TIME_BUDGET_MS = 2_000;
108-
const MIN_SAMPLES = 30;
114+
const TIME_BUDGET_MS = QUICK ? 800 : 2_000;
115+
const MIN_SAMPLES = QUICK ? 20 : 30;
109116
const MAX_SAMPLES = 2_000;
110-
const WARMUP_MS = 200;
117+
const WARMUP_MS = QUICK ? 80 : 200;
111118

112119
function collectSamples(fn: () => void): number[] {
113120
const warmupEnd = Bun.nanoseconds() + WARMUP_MS * 1e6;
@@ -188,92 +195,13 @@ const benchMount20Baseline = () => {
188195
}
189196
};
190197

191-
const benchReNormalize20 = () => {
192-
for (let i = 0; i < 20; i++) {
193-
const idx = i % allLogos.length;
194-
const logo = createNormalizedLogo(
195-
sources[idx]!,
196-
realMeasurements[idx]!,
197-
DEFAULT_BASE_SIZE,
198-
DEFAULT_SCALE_FACTOR,
199-
DEFAULT_DENSITY_FACTOR,
200-
);
201-
blackhole(getVisualCenterTransform(logo, DEFAULT_ALIGN_BY));
202-
}
203-
};
204-
205198
const keyBenchmarks: Record<string, () => void> = {
206199
"content detection (1 logo)": benchMeasure,
207200
"render pass (20 logos)": benchGetVCT20,
208201
"mount 20 logos (no detection)": benchMount20Baseline,
209202
"mount 20 logos (defaults)": benchMount20,
210203
};
211204

212-
interface ABComparison {
213-
name: string;
214-
a: { label: string; fn: () => void };
215-
b: { label: string; fn: () => void };
216-
}
217-
218-
const medianMeasurement = realMeasurements[allLogos.indexOf(medianLogo)]!;
219-
220-
const abComparisons: ABComparison[] = [
221-
{
222-
name: "densityAware: true vs false",
223-
a: {
224-
label: "true",
225-
fn: benchMeasure,
226-
},
227-
b: {
228-
label: "false",
229-
fn: () =>
230-
measureWithContentDetection(
231-
medianLogo.img,
232-
DEFAULT_CONTRAST_THRESHOLD,
233-
false,
234-
),
235-
},
236-
},
237-
{
238-
name: "alignBy: visual-center-y vs bounds",
239-
a: { label: "visual-center-y", fn: benchGetVCT20 },
240-
b: {
241-
label: "bounds",
242-
fn: () => {
243-
for (let i = 0; i < 20; i++)
244-
getVisualCenterTransform(normalized20[i]!, "bounds");
245-
},
246-
},
247-
},
248-
{
249-
name: "cropToContent: true vs false",
250-
a: {
251-
label: "true",
252-
fn: () => {
253-
if (medianMeasurement.contentBox)
254-
blackhole(
255-
cropToDataUrl(medianLogo.img, medianMeasurement.contentBox),
256-
);
257-
},
258-
},
259-
b: {
260-
label: "false (noop)",
261-
fn: () => {},
262-
},
263-
},
264-
{
265-
name: "layout update: full mount vs cached",
266-
a: {
267-
label: "full mount",
268-
fn: benchMount20,
269-
},
270-
b: {
271-
label: "cached re-normalize",
272-
fn: benchReNormalize20,
273-
},
274-
},
275-
];
276-
277205
console.log();
278206
console.log("─".repeat(72));
279207
console.log(" KEY BENCHMARKS");
@@ -293,77 +221,130 @@ for (const [name, fn] of Object.entries(keyBenchmarks)) {
293221
);
294222
}
295223

296-
console.log();
297-
console.log("─".repeat(72));
298-
console.log(" FEATURE COMPARISONS (Welch's t-test, two-tailed)");
299-
console.log(" Legend: * p<0.05 ** p<0.01 *** p<0.001");
300-
console.log("─".repeat(72));
301-
302-
interface ABResult {
303-
name: string;
304-
aLabel: string;
305-
bLabel: string;
306-
aMean: string;
307-
bMean: string;
308-
pctChange: number;
309-
result: TTestResult;
310-
}
311-
312-
const abResults: ABResult[] = [];
313-
314-
for (const comp of abComparisons) {
315-
const samplesA = collectSamples(comp.a.fn);
316-
const samplesB = collectSamples(comp.b.fn);
317-
const result = welchTTest(samplesA, samplesB);
318-
const sA = stats(samplesA);
319-
const sB = stats(samplesB);
320-
const pctChange = sB.mean !== 0 ? ((sA.mean - sB.mean) / sB.mean) * 100 : 0;
321-
322-
const sig = result.significant ? `YES ${result.marker}` : "NO";
323-
const sign = pctChange > 0 ? "+" : "";
324-
console.log(` > ${comp.name}`);
325-
console.log(
326-
` ${comp.a.label}: ${fmtNs(sA.mean)} vs ${comp.b.label}: ${fmtNs(sB.mean)} (${sign}${pctChange.toFixed(1)}%)`,
327-
);
328-
console.log(` p=${fmtP(result.p)} significant=${sig}`);
329-
console.log();
330-
331-
abResults.push({
332-
name: comp.name,
333-
aLabel: comp.a.label,
334-
bLabel: comp.b.label,
335-
aMean: fmtNs(sA.mean),
336-
bMean: fmtNs(sB.mean),
337-
pctChange,
338-
result,
339-
});
340-
}
341-
342224
const md: string[] = [
343225
"## react-logo-soup Benchmark Report",
344226
"",
345227
`Test fixtures: ${allLogos.length} real SVGs from static/logos/. ` +
346228
`${TIME_BUDGET_MS}ms budget per bench, ${MIN_SAMPLES}-${MAX_SAMPLES} samples.`,
347229
"",
348-
"### Feature Comparisons (Welch's t-test)",
349-
"",
350-
"| Test | A | B | Delta | Sig |",
351-
"|:-----|--:|--:|------:|:----|",
230+
"| Benchmark | Mean | ± Stddev | Samples |",
231+
"|:----------|-----:|---------:|--------:|",
352232
];
353233

354-
for (const r of abResults) {
355-
const sig = r.result.significant ? `YES ${r.result.marker}` : "NO";
356-
const sign = r.pctChange > 0 ? "+" : "";
234+
for (const [name, samples] of Object.entries(benchSamples)) {
235+
const s = stats(samples);
357236
md.push(
358-
`| ${r.name} | ${r.aMean} | ${r.bMean} | ${sign}${r.pctChange.toFixed(1)}% | ${fmtP(r.result.p)} ${sig} |`,
237+
`| ${name} | ${fmtNs(s.mean)} | ${fmtNs(s.stddev)} | ${samples.length} |`,
359238
);
360239
}
361240

362241
md.push("");
363-
md.push(
364-
"A/B columns match the order in the test name. Sig: `*` p<0.05, `**` p<0.01, `***` p<0.001.",
365-
);
366-
md.push("");
242+
243+
if (!QUICK) {
244+
const medianMeasurement = realMeasurements[allLogos.indexOf(medianLogo)]!;
245+
246+
const benchReNormalize20 = () => {
247+
for (let i = 0; i < 20; i++) {
248+
const idx = i % allLogos.length;
249+
const logo = createNormalizedLogo(
250+
sources[idx]!,
251+
realMeasurements[idx]!,
252+
DEFAULT_BASE_SIZE,
253+
DEFAULT_SCALE_FACTOR,
254+
DEFAULT_DENSITY_FACTOR,
255+
);
256+
blackhole(getVisualCenterTransform(logo, DEFAULT_ALIGN_BY));
257+
}
258+
};
259+
260+
const abComparisons = [
261+
{
262+
name: "densityAware: true vs false",
263+
a: { label: "true", fn: benchMeasure },
264+
b: {
265+
label: "false",
266+
fn: () =>
267+
measureWithContentDetection(
268+
medianLogo.img,
269+
DEFAULT_CONTRAST_THRESHOLD,
270+
false,
271+
),
272+
},
273+
},
274+
{
275+
name: "alignBy: visual-center-y vs bounds",
276+
a: { label: "visual-center-y", fn: benchGetVCT20 },
277+
b: {
278+
label: "bounds",
279+
fn: () => {
280+
for (let i = 0; i < 20; i++)
281+
getVisualCenterTransform(normalized20[i]!, "bounds");
282+
},
283+
},
284+
},
285+
{
286+
name: "cropToContent: true vs false",
287+
a: {
288+
label: "true",
289+
fn: () => {
290+
if (medianMeasurement.contentBox)
291+
blackhole(
292+
cropToDataUrl(medianLogo.img, medianMeasurement.contentBox),
293+
);
294+
},
295+
},
296+
b: { label: "false (noop)", fn: () => {} },
297+
},
298+
{
299+
name: "layout update: full mount vs cached",
300+
a: { label: "full mount", fn: benchMount20 },
301+
b: { label: "cached re-normalize", fn: benchReNormalize20 },
302+
},
303+
];
304+
305+
console.log();
306+
console.log("─".repeat(72));
307+
console.log(" FEATURE COMPARISONS (Welch's t-test, two-tailed)");
308+
console.log(" Legend: * p<0.05 ** p<0.01 *** p<0.001");
309+
console.log("─".repeat(72));
310+
311+
md.push(
312+
"### Feature Comparisons (Welch's t-test)",
313+
"",
314+
"| Test | A | B | Delta | Sig |",
315+
"|:-----|--:|--:|------:|:----|",
316+
);
317+
318+
for (const comp of abComparisons) {
319+
const samplesA = collectSamples(comp.a.fn);
320+
const samplesB = collectSamples(comp.b.fn);
321+
const result = welchTTest(samplesA, samplesB);
322+
const sA = stats(samplesA);
323+
const sB = stats(samplesB);
324+
const pctChange = sB.mean !== 0 ? ((sA.mean - sB.mean) / sB.mean) * 100 : 0;
325+
326+
const sig = result.significant ? `YES ${result.marker}` : "NO";
327+
const sign = pctChange > 0 ? "+" : "";
328+
console.log(` > ${comp.name}`);
329+
console.log(
330+
` ${comp.a.label}: ${fmtNs(sA.mean)} vs ${comp.b.label}: ${fmtNs(sB.mean)} (${sign}${pctChange.toFixed(1)}%)`,
331+
);
332+
console.log(` p=${fmtP(result.p)} significant=${sig}`);
333+
console.log();
334+
335+
md.push(
336+
`| ${comp.name} | ${fmtNs(sA.mean)} | ${fmtNs(sB.mean)} | ${sign}${pctChange.toFixed(1)}% | ${fmtP(result.p)} ${sig} |`,
337+
);
338+
}
339+
340+
md.push(
341+
"",
342+
"A/B columns match the order in the test name. Sig: `*` p<0.05, `**` p<0.01, `***` p<0.001.",
343+
"",
344+
);
345+
} else {
346+
console.log("\n Skipping AB comparisons in quick mode.");
347+
}
367348

368349
const outDir = process.env.BENCH_OUT_DIR ?? "tmp";
369350
mkdirSync(outDir, { recursive: true });

0 commit comments

Comments
 (0)