Skip to content

Commit 4a55ab6

Browse files
committed
feat(benchmark-tests): enhance hydration benchmarks with batch processing and result tracking
- Increased default test timeout from 5000ms to 10000ms for better stability. - Implemented batch processing in hydration tests to improve performance measurement. - Introduced a results map to collect and store hydration durations for each tag. - Updated test configurations to accommodate new batch size and timeout settings. - Refactored result writing logic into a dedicated module for better organization. - Enhanced the test run logic to handle batches of elements and track hydration performance more effectively.
1 parent fd79c29 commit 4a55ab6

File tree

12 files changed

+133
-104
lines changed

12 files changed

+133
-104
lines changed

packages/components/src/components/toaster/toaster.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class ToasterService {
3838
this.toastContainerElement = undefined;
3939
element.remove();
4040
} else {
41-
Log.warn('Toaster service is already disposed.', { forceLog: true });
41+
Log.warn('Toaster service is already disposed.');
4242
}
4343
}
4444

packages/components/src/schema/props/color.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const handleColorChange = (value: unknown): ColorPair => {
108108
break;
109109
}
110110
case null:
111-
Log.warn(`_color was empty or invalid (${JSON.stringify(value)})`, { forceLog: true });
111+
Log.warn(`_color was empty or invalid (${JSON.stringify(value)})`);
112112
colorContrast = createContrastColorPair({
113113
background: '#000',
114114
foreground: '#000',

packages/components/src/utils/unique-nav-labels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const UNIQUE_LABELS: Set<string> = new Set();
99

1010
export function addNavLabel(ariaLabel: string): void {
1111
if (UNIQUE_LABELS.has(ariaLabel)) {
12-
Log.error(`There already is a nav element with the label "${ariaLabel}"`, { forceLog: true });
12+
Log.warn(`There already is a nav element with the label "${ariaLabel}"`);
1313
} else {
1414
UNIQUE_LABELS.add(ariaLabel);
1515
}

packages/tools/benchmark-tests/playwright.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { defineConfig, devices } from '@playwright/test';
22

3-
const TEST_TIMEOUT = parseInt(process.env.TEST_TIMEOUT || '5000', 10);
3+
const TEST_TIMEOUT = parseInt(process.env.TEST_TIMEOUT || '10000', 10);
44

55
export default defineConfig({
66
testDir: 'tests',
77
testMatch: '*.playwright.ts',
8-
timeout: TEST_TIMEOUT,
8+
timeout: 2 * TEST_TIMEOUT,
99
use: {
1010
headless: true,
1111
viewport: { width: 800, height: 600 },
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { test } from '@playwright/test';
2-
import { TAGS, TEST_URL } from './lib/config';
3-
import { runBenchmark, writeResultFile } from './lib/test';
2+
import { createResultsMap, TAGS, TEST_URL } from './lib/config';
43
import type { Params } from './lib/types';
4+
import { runBenchmark } from './lib/browser';
5+
import { writeResultFile } from './lib/after';
6+
7+
const RESULTS = createResultsMap();
58

69
test.describe('Hydration Benchmark', () => {
710
test.beforeEach(async ({ page }) => {
811
await page.goto(TEST_URL);
912
});
1013

1114
for (const tag of TAGS) {
12-
test(`${tag}`, async ({ page }) => await runBenchmark(tag, (fn: any, params: Params) => page.evaluate(fn, params)));
15+
test(`${tag}`, async ({ page }) => await runBenchmark(tag, (fn: any, params: Params) => page.evaluate(fn, params), RESULTS));
1316
}
1417

15-
test.afterAll(writeResultFile);
18+
test.afterAll(() => {
19+
writeResultFile(RESULTS);
20+
});
1621
});
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { TAGS, TEST_URL } from './lib/config';
2-
import { runBenchmark, writeResultFile } from './lib/test';
1+
import { writeResultFile } from './lib/after';
2+
import { runBenchmark } from './lib/browser';
3+
import { createResultsMap, TAGS, TEST_URL } from './lib/config';
34
import type { Params } from './lib/types';
45

6+
const RESULTS = createResultsMap();
7+
58
describe('Hydration Benchmark', () => {
69
before(async () => {
710
await browser.url(TEST_URL);
811
});
912

1013
for (const tag of TAGS) {
11-
it(`${tag}`, async () => await runBenchmark(tag, (fn: any, params: Params) => browser.execute(fn, params)));
14+
it(`${tag}`, async () => await runBenchmark(tag, (fn: any, params: Params) => browser.execute(fn, params), RESULTS));
1215
}
1316

14-
after(writeResultFile);
17+
after(() => {
18+
writeResultFile(RESULTS);
19+
});
1520
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { writeFileSync } from 'fs';
2+
import type { TagType } from './types';
3+
4+
export function writeResultFile(results: Map<TagType, number[]>) {
5+
function percentile(sorted: number[], p: number): number {
6+
const i = Math.floor(sorted.length * p);
7+
return sorted[i] ?? sorted[sorted.length - 1];
8+
}
9+
10+
function stddev(arr: number[]) {
11+
const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
12+
const squared = arr.map((x) => (x - mean) ** 2);
13+
return Math.sqrt(squared.reduce((a, b) => a + b, 0) / arr.length);
14+
}
15+
16+
const finalResults = Array.from(results.entries())
17+
.filter(([_, values]) => values.length > 0)
18+
.map(([tag, values]) => {
19+
values.sort((a, b) => a - b);
20+
const mid = Math.floor(values.length / 2);
21+
return {
22+
name: tag,
23+
unit: 'ms',
24+
value: values[mid],
25+
p95: percentile(values, 0.95),
26+
p99: percentile(values, 0.99),
27+
min: values[0],
28+
max: values[values.length - 1],
29+
stddev: stddev(values),
30+
};
31+
});
32+
33+
writeFileSync('benchmark-result.json', JSON.stringify(finalResults, null, 2));
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { TEST_BATCH_SIZE, TEST_ITERATIONS, TEST_TIMEOUT } from './config';
2+
import { testRun } from './test';
3+
import type { TagType } from './types';
4+
5+
export async function runBenchmark(tag: TagType, execFn: any, results: Map<TagType, number[]>) {
6+
const durations: number[] = await execFn(testRun, {
7+
batchSize: TEST_BATCH_SIZE,
8+
iterations: TEST_ITERATIONS,
9+
tag,
10+
timeout: TEST_TIMEOUT,
11+
});
12+
13+
console.log(`Hydration durations for ${tag}:`, durations);
14+
/**
15+
* Cut warmup iterations from the results.
16+
*/
17+
results.set(tag, durations.splice(1, durations.length - 1));
18+
}

packages/tools/benchmark-tests/tests/lib/config.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
export const TEST_ITERATIONS = parseInt(process.env.TEST_ITERATIONS || '25', 10);
2-
export const TEST_TIMEOUT = parseInt(process.env.TEST_TIMEOUT || '5000', 10);
3-
// export const TEST_BATCH_SIZE = parseInt(process.env.TEST_BATCH_SIZE || '10', 10);
1+
import type { TagType } from './types';
2+
3+
export const TEST_ITERATIONS = parseInt(process.env.TEST_ITERATIONS || '50', 10);
4+
export const TEST_BATCH_SIZE = parseInt(process.env.TEST_BATCH_SIZE || '100', 10);
5+
export const TEST_TIMEOUT = parseInt(process.env.TEST_TIMEOUT || '10000', 10);
46
export const TEST_URL = 'http://localhost:3000/test-page.html';
57

68
export const TAGS = [
@@ -51,3 +53,7 @@ export const TAGS = [
5153
'kol-tree-item',
5254
'kol-version',
5355
] as const;
56+
57+
export const createResultsMap = (): Map<TagType, number[]> => {
58+
return new Map<TagType, number[]>(TAGS.map((tag) => [tag, []]));
59+
};
Lines changed: 46 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
1-
import { writeFileSync } from 'fs';
2-
import { TEST_ITERATIONS, TEST_TIMEOUT } from './config';
3-
import type { Measure, Params, TagType } from './types';
1+
import type { Measure, Params } from './types';
42

5-
async function testRun({ iterations, tag, timeout }: Params): Promise<number[]> {
3+
export async function testRun({ batchSize, iterations, tag, timeout }: Params): Promise<number[]> {
64
return new Promise(async (resolve) => {
7-
const testResults = new Map<HTMLElement, Measure>();
8-
const webComponents = new Set<HTMLElement>();
9-
const batches = [];
5+
const testResults: Map<Set<HTMLElement>, Measure> = new Map();
6+
const batches: Set<Set<HTMLElement>> = new Set();
7+
let currentBatch: Set<HTMLElement> = new Set();
8+
let batchCounter = 0;
109

1110
window.gc?.();
1211
await customElements.whenDefined(tag);
1312

13+
// Fallback for when no elements are hydrated
14+
setTimeout(() => {
15+
console.warn('⚠️ Timeout: hydration did not finish in time – returning partial or empty results.');
16+
returnDurations();
17+
}, timeout);
18+
1419
function startNextHydration() {
15-
if (webComponents.size > 0) {
16-
const el: HTMLElement = webComponents.values()?.next()?.value!;
17-
performance.mark(`mark-append-${el.getAttribute('data-iteration')}`);
18-
testResults.set(el, {
20+
if (batches.size > 0) {
21+
batchCounter++;
22+
currentBatch = batches.values()?.next()?.value!;
23+
performance.mark(`mark-append-${batchCounter}`);
24+
testResults.set(currentBatch, {
1925
hydrated: null,
2026
themed: null,
2127
});
22-
document.body.appendChild(el);
28+
for (const el of currentBatch) {
29+
document.body.appendChild(el);
30+
}
2331
} else {
2432
returnDurations();
2533
}
@@ -36,8 +44,12 @@ async function testRun({ iterations, tag, timeout }: Params): Promise<number[]>
3644
resolve(durations);
3745
}
3846

47+
function removeBatch(batch: Set<HTMLElement>) {
48+
batches.delete(batch);
49+
}
50+
3951
function removeElement(el: HTMLElement) {
40-
webComponents.delete(el);
52+
currentBatch.delete(el);
4153
try {
4254
el.remove();
4355
} catch {}
@@ -46,89 +58,37 @@ async function testRun({ iterations, tag, timeout }: Params): Promise<number[]>
4658
const observer = new MutationObserver((mutations) => {
4759
for (const mutation of mutations) {
4860
const el = mutation.target as HTMLElement;
49-
if (!webComponents.has(el) && !el.isConnected) continue;
50-
51-
const measure = testResults.get(el);
52-
if (!measure) continue;
61+
if (!currentBatch.has(el) && !el.isConnected) continue;
5362

54-
const id = el.getAttribute('data-iteration');
63+
if (el.classList.contains('hydrated')) {
64+
removeElement(el);
65+
if (currentBatch.size === 0) {
66+
performance.mark(`mark-hydrated-${batchCounter}`);
67+
performance.measure(`hydrated-${batchCounter}`, `mark-append-${batchCounter}`, `mark-hydrated-${batchCounter}`);
5568

56-
if (!measure.hydrated && el.classList.contains('hydrated')) {
57-
performance.mark(`mark-hydrated-${id}`);
58-
performance.measure(`hydrated-${id}`, `mark-append-${id}`, `mark-hydrated-${id}`);
59-
measure.hydrated = performance.getEntriesByName(`hydrated-${id}`).pop()?.duration!;
60-
// } else if (measure.hydrated && el.hasAttribute('data-themed')) {
61-
// performance.mark(`mark-themed-${id}`);
62-
// performance.measure(`themed-${id}`, `mark-append-${id}`, `mark-themed-${id}`);
63-
// measure.themed = performance.getEntriesByName(`themed-${id}`).pop()?.duration!;
69+
const measure = testResults.get(currentBatch)!;
70+
measure.hydrated = performance.getEntriesByName(`hydrated-${batchCounter}`).pop()?.duration!;
6471

65-
removeElement(el);
66-
startNextHydration();
72+
removeBatch(currentBatch);
73+
setTimeout(startNextHydration);
74+
}
6775
}
6876
}
6977
});
7078

7179
for (let i = 0; i <= iterations; i++) {
72-
const el = document.createElement(tag);
73-
el.setAttribute('data-iteration', i.toString());
74-
webComponents.add(el);
75-
observer.observe(el, {
76-
attributes: true,
77-
attributeFilter: ['class', 'data-themed'],
78-
});
80+
const batch = new Set<HTMLElement>();
81+
for (let j = 0; j < batchSize; j++) {
82+
const el = document.createElement(tag);
83+
batch.add(el);
84+
observer.observe(el, {
85+
attributes: true,
86+
attributeFilter: ['class', 'data-themed'],
87+
});
88+
}
89+
batches.add(batch);
7990
}
8091

81-
// Fallback for when no elements are hydrated
82-
setTimeout(returnDurations, timeout);
83-
8492
startNextHydration();
8593
});
8694
}
87-
88-
const results = new Map<TagType, number[]>();
89-
export function writeResultFile() {
90-
function percentile(sorted, p) {
91-
const i = Math.floor(sorted.length * p);
92-
return sorted[i] ?? sorted[sorted.length - 1];
93-
}
94-
95-
function stddev(arr) {
96-
const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
97-
const squared = arr.map((x) => (x - mean) ** 2);
98-
return Math.sqrt(squared.reduce((a, b) => a + b, 0) / arr.length);
99-
}
100-
101-
const finalResults = Array.from(results.entries())
102-
.filter(([_, values]) => values.length > 0)
103-
.map(([tag, values]) => {
104-
values.sort((a, b) => a - b);
105-
const mid = Math.floor(values.length / 2);
106-
return {
107-
name: tag,
108-
unit: 'ms',
109-
value: values[mid],
110-
p95: percentile(values, 0.95),
111-
p99: percentile(values, 0.99),
112-
min: values[0],
113-
max: values[values.length - 1],
114-
stddev: stddev(values),
115-
};
116-
});
117-
118-
writeFileSync('benchmark-result.json', JSON.stringify(finalResults, null, 2));
119-
}
120-
121-
export async function runBenchmark(tag: TagType, execFn: any) {
122-
const durations: number[] = await execFn(testRun, {
123-
iterations: TEST_ITERATIONS,
124-
tag,
125-
timeout: TEST_TIMEOUT,
126-
});
127-
128-
console.log(`Hydration durations for ${tag}:`, durations);
129-
/**
130-
* The network request durations are removed from the test results
131-
* to focus on the hydration performance of the web components itself.
132-
*/
133-
results.set(tag, durations.splice(1, durations.length - 1));
134-
}

0 commit comments

Comments
 (0)