@@ -6,44 +6,60 @@ import path from 'path';
66
77const DEFAULT_OPTIONS = {
88 frames : 100 ,
9- timing : 'fixed' ,
109 skip : [ ] ,
1110 profile : false ,
12- suffix : '' ,
11+ prefix : '' ,
1312 headless : false ,
1413 screenshot : false ,
1514 viewport : '616x800' ,
15+ fixInject : null ,
16+ fixInitial : null ,
17+ fixUpdate : null ,
18+ throttling : 8 ,
1619} ;
1720
1821const HELP = `
1922Usage: node performance.js [options]
2023
2124Options:
22- --frames=<number> Number of animation frames to run (default: 100)
23- Note: Use 200+ frames for reliable optimization measurements.
24- Lower values (5-20) are useful for quick sanity checks.
25- --timing=<mode> Timing mode: 'raf' or 'fixed' (default: 'fixed')
26- --skip=<group> Skip test group: 'inject', 'initial', or 'update' (can be used multiple times)
27- --profile=true Enable performance profiling with Chrome DevTools
28- --suffix=<string> File suffix for output files (default: none)
29- Examples: --suffix=before, --suffix=optimized
30- --headless=true Run in headless mode (default: false, shows browser)
31- --screenshot=true Save screenshot after tests complete (default: false)
32- --viewport=WxH Set browser viewport size (default: 616x800)
33- Examples: --viewport=1920x1080, --viewport=800x600
34- --help Show this help message
25+ --frames=<number> Number of animation frames to run (default: 100)
26+ Note: Use 200+ frames for reliable optimization measurements.
27+ Lower values (5-20) are useful for quick sanity checks.
28+ --skip=<item> Skip test group ('inject', 'initial', 'update')
29+ Skip library ('default', 'lit-html', 'uhtml', 'react')
30+ Can be used multiple times.
31+ Examples: --skip=react, --skip=inject
32+ --profile=true Enable performance profiling with Chrome DevTools
33+ --prefix=<string> File prefix for output files (default: none)
34+ Examples: --prefix=baseline, --prefix=optimized
35+ --headless=true Run in headless mode (default: false, shows browser)
36+ --screenshot=true Save screenshot after tests complete (default: false)
37+ --viewport=WxH Set browser viewport size (default: 616x800)
38+ Examples: --viewport=1920x1080, --viewport=800x600
39+ --fix-inject=min-max Fix inject visualization range in microseconds
40+ Example: --fix-inject=55-65
41+ --fix-initial=min-max Fix initial visualization range in microseconds
42+ Example: --fix-initial=5-7
43+ --fix-update=min-max Fix update visualization range in microseconds
44+ Example: --fix-update=0.6-0.8
45+ --throttling=<number> CPU throttling rate (default: 8, high throttling)
46+ Example: --throttling=1 (no throttling)
47+ --help Show this help message
3548
3649Examples:
37- node performance.js --frames=5 # Quick sanity check
38- node performance.js --frames=200 # Reliable optimization testing
39- node performance.js --frames=50 --timing=raf
50+ node performance.js --frames=5 # Quick sanity check
51+ node performance.js --frames=200 # Reliable optimization testing
4052 node performance.js --skip=inject --skip=initial
53+ node performance.js --skip=react --skip=lit-html # Skip specific libraries
4154 node performance.js --profile=true
42- node performance.js --suffix=before # Saves to performance-before.json
43- node performance.js --suffix=optimized # Saves to performance-optimized.json
44- node performance.js --headless=true # Run without visible browser
45- node performance.js --screenshot=true # Save screenshot for record keeping
46- node performance.js --viewport=1920x1080 # High resolution viewport and screenshot
55+ node performance.js --prefix=baseline # Saves to baseline-performance.json
56+ node performance.js --prefix=optimized # Saves to optimized-performance.json
57+ node performance.js --headless=true # Run without visible browser
58+ node performance.js --screenshot=true # Save screenshot for record keeping
59+ node performance.js --viewport=1920x1080 # High resolution viewport and screenshot
60+ node performance.js --fix-inject=55-65 # Fixed visualization range for inject
61+ node performance.js --fix-initial=5-7 # Fixed visualization range for initial
62+ node performance.js --fix-update=0.6-0.8 # Fixed visualization range for update
4763
4864Note: Server must be running on localhost:8080 (use 'npm start')
4965` ;
@@ -62,12 +78,10 @@ function parseArgs() {
6278 }
6379 if ( arg . startsWith ( '--frames=' ) ) {
6480 options . frames = parseInt ( arg . split ( '=' ) [ 1 ] ) ;
65- } else if ( arg . startsWith ( '--timing=' ) ) {
66- options . timing = arg . split ( '=' ) [ 1 ] ;
6781 } else if ( arg . startsWith ( '--skip=' ) ) {
6882 options . skip . push ( arg . split ( '=' ) [ 1 ] ) ;
69- } else if ( arg . startsWith ( '--suffix =' ) ) {
70- options . suffix = arg . split ( '=' ) [ 1 ] ;
83+ } else if ( arg . startsWith ( '--prefix =' ) ) {
84+ options . prefix = arg . split ( '=' ) [ 1 ] ;
7185 } else if ( arg . startsWith ( '--profile=' ) ) {
7286 const value = arg . split ( '=' ) [ 1 ] ;
7387 if ( value === 'true' ) {
@@ -105,6 +119,34 @@ function parseArgs() {
105119 process . exit ( 1 ) ;
106120 }
107121 options . viewport = viewport ;
122+ } else if ( arg . startsWith ( '--fix-inject=' ) ) {
123+ const range = arg . split ( '=' ) [ 1 ] ;
124+ if ( ! / ^ \d + ( \. \d + ) ? - \d + ( \. \d + ) ? $ / . test ( range ) ) {
125+ console . error ( `Invalid fix-inject format: ${ range } . Must be min-max (e.g., 55-65).` ) ; // eslint-disable-line no-console
126+ process . exit ( 1 ) ;
127+ }
128+ options . fixInject = range ;
129+ } else if ( arg . startsWith ( '--fix-initial=' ) ) {
130+ const range = arg . split ( '=' ) [ 1 ] ;
131+ if ( ! / ^ \d + ( \. \d + ) ? - \d + ( \. \d + ) ? $ / . test ( range ) ) {
132+ console . error ( `Invalid fix-initial format: ${ range } . Must be min-max (e.g., 5-7).` ) ; // eslint-disable-line no-console
133+ process . exit ( 1 ) ;
134+ }
135+ options . fixInitial = range ;
136+ } else if ( arg . startsWith ( '--fix-update=' ) ) {
137+ const range = arg . split ( '=' ) [ 1 ] ;
138+ if ( ! / ^ \d + ( \. \d + ) ? - \d + ( \. \d + ) ? $ / . test ( range ) ) {
139+ console . error ( `Invalid fix-update format: ${ range } . Must be min-max (e.g., 0.6-0.8).` ) ; // eslint-disable-line no-console
140+ process . exit ( 1 ) ;
141+ }
142+ options . fixUpdate = range ;
143+ } else if ( arg . startsWith ( '--throttling=' ) ) {
144+ const rate = parseFloat ( arg . split ( '=' ) [ 1 ] ) ;
145+ if ( isNaN ( rate ) || rate < 1 ) {
146+ console . error ( `Invalid throttling: ${ arg . split ( '=' ) [ 1 ] } . Must be a number >= 1.` ) ; // eslint-disable-line no-console
147+ process . exit ( 1 ) ;
148+ }
149+ options . throttling = rate ;
108150 }
109151 }
110152 return options ;
@@ -116,16 +158,25 @@ function parseArgs() {
116158 */
117159async function runPerformanceTests ( options ) {
118160 // Clean up any existing result files to ensure fresh results
119- const performanceDir = 'performance' ;
120- const suffix = options . suffix ? `-${ options . suffix } ` : '' ;
121- const resultsFile = path . join ( performanceDir , `performance${ suffix } .json` ) ;
122- const profileFile = path . join ( performanceDir , `performance-profile${ suffix } .json` ) ;
123- if ( fs . existsSync ( resultsFile ) ) {
124- fs . unlinkSync ( resultsFile ) ;
125- }
126- if ( fs . existsSync ( profileFile ) ) {
127- fs . unlinkSync ( profileFile ) ;
161+ const resultsDir = path . join ( 'performance' , 'results' ) ;
162+ const prefix = options . prefix ? `${ options . prefix } -` : '' ;
163+ const resultsFile = path . join ( resultsDir , `${ prefix } performance.json` ) ;
164+ const oldProfileFile = path . join ( resultsDir , `${ prefix } performance-profile.json` ) ;
165+ const cpuProfileFile = path . join ( resultsDir , `${ prefix } performance-profile.cpuprofile` ) ;
166+ const profileSummaryFile = path . join ( resultsDir , `${ prefix } performance-profile-summary.txt` ) ;
167+
168+ // Ensure results directory exists
169+ if ( ! fs . existsSync ( resultsDir ) ) {
170+ fs . mkdirSync ( resultsDir , { recursive : true } ) ;
128171 }
172+
173+ // Clean up any existing files that will be regenerated
174+ const filesToCleanup = [ resultsFile , oldProfileFile , cpuProfileFile , profileSummaryFile ] ;
175+ filesToCleanup . forEach ( file => {
176+ if ( fs . existsSync ( file ) ) {
177+ fs . unlinkSync ( file ) ;
178+ }
179+ } ) ;
129180
130181 const browser = await puppeteer . launch ( {
131182 headless : options . headless ,
@@ -140,6 +191,7 @@ async function runPerformanceTests(options) {
140191 '--disable-features=TranslateUI' , // Disable translate features
141192 '--disable-dev-shm-usage' , // Use /tmp instead of /dev/shm
142193 '--memory-pressure-off' , // Disable memory pressure signals
194+ '--run-all-compositor-stages-before-draw' , // Force full render pipeline each frame
143195 '--js-flags=--max-old-space-size=8192 --expose-gc' , // Control heap and expose window.gc()
144196 ] ,
145197 } ) ;
@@ -148,7 +200,7 @@ async function runPerformanceTests(options) {
148200
149201 // Set viewport size
150202 const [ width , height ] = options . viewport . split ( 'x' ) . map ( Number ) ;
151- await page . setViewport ( { width, height } ) ;
203+ await page . setViewport ( { width, height, deviceScaleFactor : 1 } ) ;
152204
153205 // Configure page for stable performance testing
154206 await page . setCacheEnabled ( false ) ;
@@ -157,7 +209,6 @@ async function runPerformanceTests(options) {
157209 // Build URL with query parameters
158210 const params = new URLSearchParams ( ) ;
159211 params . set ( 'frames' , options . frames . toString ( ) ) ;
160- params . set ( 'timing' , options . timing ) ;
161212 for ( const skip of options . skip ) {
162213 params . append ( 'skip' , skip ) ;
163214 }
@@ -167,20 +218,29 @@ async function runPerformanceTests(options) {
167218 params . set ( 'profile' , 'true' ) ;
168219 }
169220
221+ // Add fixed range parameters when provided
222+ if ( options . fixInject ) {
223+ params . set ( 'fixInject' , options . fixInject ) ;
224+ }
225+ if ( options . fixInitial ) {
226+ params . set ( 'fixInitial' , options . fixInitial ) ;
227+ }
228+ if ( options . fixUpdate ) {
229+ params . set ( 'fixUpdate' , options . fixUpdate ) ;
230+ }
231+
170232 const url = `http://localhost:8080/performance/?${ params . toString ( ) } ` ;
171233
172- // Enable profiling if requested
234+ const cdpSession = await page . createCDPSession ( ) ;
235+
236+ // Fixed CPU throttle if requested.
237+ await cdpSession . send ( 'Emulation.setCPUThrottlingRate' , { rate : options . throttling } ) ;
238+
239+ // Enable CPU profiling if requested.
173240 if ( options . profile ) {
174- // Start tracing with comprehensive categories
175- await page . tracing . start ( {
176- path : `performance/performance-profile${ suffix } .json` ,
177- categories : [
178- 'devtools.timeline' ,
179- 'v8.execute' ,
180- 'disabled-by-default-v8.cpu_profiler' ,
181- 'disabled-by-default-v8.cpu_profiler.hires' ,
182- ] ,
183- } ) ;
241+ // Connect to Chrome DevTools Protocol.
242+ await cdpSession . send ( 'Profiler.enable' ) ;
243+ await cdpSession . send ( 'Profiler.start' ) ;
184244 }
185245
186246 // Capture performance results and wait for completion
@@ -215,25 +275,55 @@ async function runPerformanceTests(options) {
215275 // Wait for tests to complete or timeout
216276 await Promise . race ( [ completionPromise , timeoutPromise ] ) ;
217277
218- // Stop profiling if enabled
278+ // Stop CPU profiling if enabled and save profile
219279 if ( options . profile ) {
220- await page . tracing . stop ( ) ;
280+ const profile = await cdpSession . send ( 'Profiler.stop' ) ;
281+
282+ // Save raw CPU profile
283+ fs . writeFileSync ( cpuProfileFile , JSON . stringify ( profile . profile ) ) ;
284+
285+ // Generate human-readable summary
286+ const hotspots = profile . profile . nodes . filter ( node => node . hitCount > 0 ) ;
287+ hotspots . sort ( ( a , b ) => b . hitCount - a . hitCount ) ;
288+
289+ let summary = '=== CPU PROFILE HOTSPOTS ===\n' ;
290+ summary += 'HitCount | Function | File:Line\n' ;
291+ summary += '---------|----------|----------\n' ;
292+
293+ hotspots . slice ( 0 , 20 ) . forEach ( node => {
294+ const frame = node . callFrame ;
295+ const fileName = frame . url ? frame . url . split ( '/' ) . pop ( ) : 'native' ;
296+ const location = frame . url ? `${ fileName } :${ frame . lineNumber } ` : 'native' ;
297+ const functionName = frame . functionName || '(anonymous)' ;
298+ summary += `${ node . hitCount . toString ( ) . padStart ( 8 ) } | ${ functionName . padEnd ( 30 ) } | ${ location } \n` ;
299+ } ) ;
300+
301+ summary += '\n=== X-ELEMENT SPECIFIC HOTSPOTS ===\n' ;
302+ const xElementHotspots = hotspots . filter ( node =>
303+ node . callFrame . url && (
304+ node . callFrame . url . includes ( 'x-parser.js' ) ||
305+ node . callFrame . url . includes ( 'x-template.js' )
306+ )
307+ ) ;
308+
309+ xElementHotspots . forEach ( node => {
310+ const frame = node . callFrame ;
311+ const fileName = frame . url . split ( '/' ) . pop ( ) ;
312+ const functionName = frame . functionName || '(anonymous)' ;
313+ summary += `${ node . hitCount . toString ( ) . padStart ( 8 ) } | ${ functionName . padEnd ( 30 ) } | ${ fileName } :${ frame . lineNumber } \n` ;
314+ } ) ;
315+
316+ fs . writeFileSync ( profileSummaryFile , summary ) ;
221317 }
222318
223319 // Save performance results to file
224320 if ( performanceResults ) {
225- if ( ! fs . existsSync ( performanceDir ) ) {
226- fs . mkdirSync ( performanceDir , { recursive : true } ) ;
227- }
228321 fs . writeFileSync ( resultsFile , JSON . stringify ( performanceResults , null , 2 ) ) ;
229322 }
230323
231324 // Take screenshot if requested
232325 if ( options . screenshot ) {
233- if ( ! fs . existsSync ( performanceDir ) ) {
234- fs . mkdirSync ( performanceDir , { recursive : true } ) ;
235- }
236- const screenshotFile = path . join ( performanceDir , `performance-screenshot${ suffix } .png` ) ;
326+ const screenshotFile = path . join ( resultsDir , `${ prefix } performance-screenshot.png` ) ;
237327 await page . screenshot ( { path : screenshotFile , fullPage : true , type : 'png' } ) ;
238328 }
239329 } finally {
0 commit comments