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