@ -49,6 +49,253 @@ export function safe(name) {
return name . replace ( /[^a-z0-9._-]+/gi , '_' ) ;
}
/ * *
* @ param { unknown } value
* /
function format _markdown _value ( value ) {
if ( value === null || value === undefined ) return '' ;
if ( Array . isArray ( value ) ) return value . map ( ( item ) => String ( item ) ) . join ( ', ' ) ;
if ( typeof value === 'object' ) return JSON . stringify ( value ) ;
return String ( value ) ;
}
/ * *
* @ param { string } text
* /
function escape _markdown _cell ( text ) {
return text . replace ( /\\/g , '\\\\' ) . replace ( /\|/g , '\\|' ) . replace ( /\r?\n/g , ' ' ) ;
}
/ * *
* @ param { string } value
* /
function normalize _profile _url ( value ) {
if ( ! value ) return '' ;
if ( value . startsWith ( 'file://' ) ) {
try {
const pathname = decodeURIComponent ( new URL ( value ) . pathname ) ;
const relative = path . relative ( process . cwd ( ) , pathname ) ;
if ( relative && ! relative . startsWith ( '..' ) && ! path . isAbsolute ( relative ) ) return relative ;
return pathname ;
} catch {
return value ;
}
}
if ( path . isAbsolute ( value ) ) {
const relative = path . relative ( process . cwd ( ) , value ) ;
if ( relative && ! relative . startsWith ( '..' ) && ! path . isAbsolute ( relative ) ) return relative ;
}
return value ;
}
/ * *
* @ param { string } function _name
* /
function is _special _runtime _node ( function _name ) {
return function _name === '(idle)' || function _name === '(garbage collector)' ;
}
/ * *
* @ param { string } normalized _url
* /
function is _svelte _source _url ( normalized _url ) {
return normalized _url . startsWith ( 'packages/svelte/' ) ;
}
/ * *
* @ param { Record < string , unknown > } profile
* /
function profile _to _markdown ( profile ) {
/** @type {string[]} */
const lines = [ '# CPU profile' ] ;
const metadata = Object . entries ( profile ) . filter (
( [ key ] ) => key !== 'nodes' && key !== 'samples' && key !== 'timeDeltas'
) ;
if ( metadata . length > 0 ) {
lines . push ( '' , '## Metadata' , '| Field | Value |' , '| --- | --- |' ) ;
for ( const [ key , value ] of metadata ) {
lines . push (
` | ${ escape _markdown _cell ( key ) } | ${ escape _markdown _cell ( format _markdown _value ( value ) ) } | `
) ;
}
}
const nodes = Array . isArray ( profile . nodes ) ? profile . nodes : [ ] ;
const samples = Array . isArray ( profile . samples ) ? profile . samples : [ ] ;
const timeDeltas = Array . isArray ( profile . timeDeltas ) ? profile . timeDeltas : [ ] ;
/** @type {Set<number>} */
const included _node _ids = new Set ( ) ;
if ( nodes . length > 0 ) {
/** @type {Map<number, Record<string, unknown>>} */
const nodes _by _id = new Map ( ) ;
/** @type {Map<number, number>} */
const parent _by _id = new Map ( ) ;
for ( const node of nodes ) {
if ( ! node || typeof node !== 'object' ) continue ;
if ( typeof node . id !== 'number' ) continue ;
nodes _by _id . set ( node . id , node ) ;
const children = Array . isArray ( node . children ) ? node . children : [ ] ;
for ( const child of children ) {
if ( typeof child === 'number' ) {
parent _by _id . set ( child , node . id ) ;
}
}
const callFrame =
node . callFrame && typeof node . callFrame === 'object'
? /** @type {Record<string, unknown>} */ ( node . callFrame )
: /** @type {Record<string, unknown>} */ ( { } ) ;
const functionName =
typeof callFrame . functionName === 'string' ? callFrame . functionName : '(anonymous)' ;
const normalizedUrl =
typeof callFrame . url === 'string' ? normalize _profile _url ( callFrame . url ) : '' ;
if ( is _special _runtime _node ( functionName ) || is _svelte _source _url ( normalizedUrl ) ) {
included _node _ids . add ( node . id ) ;
}
}
/** @type {Map<number, number>} */
const self _sample _count = new Map ( ) ;
for ( const sample of samples ) {
if ( typeof sample !== 'number' ) continue ;
if ( ! included _node _ids . has ( sample ) ) continue ;
self _sample _count . set ( sample , ( self _sample _count . get ( sample ) ? ? 0 ) + 1 ) ;
}
/** @type {Map<number, number>} */
const inclusive _sample _count = new Map ( ) ;
/** @type {Set<number>} */
const stack = new Set ( ) ;
/** @param {number} node_id */
const get _inclusive _count = ( node _id ) => {
const cached = inclusive _sample _count . get ( node _id ) ;
if ( cached !== undefined ) return cached ;
if ( stack . has ( node _id ) ) return self _sample _count . get ( node _id ) ? ? 0 ;
stack . add ( node _id ) ;
const node = nodes _by _id . get ( node _id ) ;
const children = node && Array . isArray ( node . children ) ? node . children : [ ] ;
let total = self _sample _count . get ( node _id ) ? ? 0 ;
for ( const child of children ) {
if ( typeof child !== 'number' ) continue ;
total += get _inclusive _count ( child ) ;
}
stack . delete ( node _id ) ;
inclusive _sample _count . set ( node _id , total ) ;
return total ;
} ;
for ( const node _id of included _node _ids ) {
get _inclusive _count ( node _id ) ;
}
const total _samples = [ ... self _sample _count . values ( ) ] . reduce ( ( sum , count ) => sum + count , 0 ) ;
if ( total _samples > 0 ) {
const hotspot _rows = [ ... included _node _ids ]
. map ( ( id ) => nodes _by _id . get ( id ) )
. filter ( ( node ) => ! ! node )
. map ( ( node ) => {
const id = /** @type {number} */ ( node . id ) ;
const callFrame =
node . callFrame && typeof node . callFrame === 'object'
? /** @type {Record<string, unknown>} */ ( node . callFrame )
: /** @type {Record<string, unknown>} */ ( { } ) ;
const functionName =
typeof callFrame . functionName === 'string' && callFrame . functionName . length > 0
? callFrame . functionName
: '(anonymous)' ;
const selfCount = self _sample _count . get ( id ) ? ? 0 ;
const inclusiveCount = inclusive _sample _count . get ( id ) ? ? selfCount ;
return { id , functionName , selfCount , inclusiveCount } ;
} )
. filter ( ( row ) => row . selfCount > 0 || row . inclusiveCount > 0 )
. sort (
( a , b ) =>
b . inclusiveCount - a . inclusiveCount ||
b . selfCount - a . selfCount ||
String ( a . id ) . localeCompare ( String ( b . id ) )
)
. slice ( 0 , 25 ) ;
if ( hotspot _rows . length > 0 ) {
lines . push (
'' ,
'## Top hotspots' ,
'| Rank | Node ID | Function | Self samples | Self % | Inclusive samples | Inclusive % |' ,
'| --- | --- | --- | --- | --- | --- | --- |'
) ;
for ( let i = 0 ; i < hotspot _rows . length ; i += 1 ) {
const row = hotspot _rows [ i ] ;
const selfPct = ( ( row . selfCount / total _samples ) * 100 ) . toFixed ( 2 ) ;
const inclusivePct = ( ( row . inclusiveCount / total _samples ) * 100 ) . toFixed ( 2 ) ;
lines . push (
` | ${ i + 1 } | ${ row . id } | ${ escape _markdown _cell ( row . functionName ) } | ${ row . selfCount } | ${ selfPct } % | ${ row . inclusiveCount } | ${ inclusivePct } % | `
) ;
}
}
}
lines . push (
'' ,
'## Nodes' ,
'| ID | Parent ID | Function | URL | Line | Column | Hit count | Children | Deopt reason |' ,
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |'
) ;
for ( const node of nodes ) {
if ( ! node || typeof node !== 'object' ) continue ;
if ( typeof node . id !== 'number' ) continue ;
if ( ! included _node _ids . has ( node . id ) ) continue ;
const callFrame =
node . callFrame && typeof node . callFrame === 'object'
? /** @type {Record<string, unknown>} */ ( node . callFrame )
: /** @type {Record<string, unknown>} */ ( { } ) ;
const id = typeof node . id === 'number' ? node . id : '' ;
const parentId =
typeof id === 'number' && included _node _ids . has ( parent _by _id . get ( id ) ? ? NaN )
? parent _by _id . get ( id ) ? ? ''
: '' ;
const functionName =
typeof callFrame . functionName === 'string' && callFrame . functionName . length > 0
? callFrame . functionName
: '(anonymous)' ;
const url = typeof callFrame . url === 'string' ? normalize _profile _url ( callFrame . url ) : '' ;
const lineNumber =
typeof callFrame . lineNumber === 'number' ? String ( callFrame . lineNumber + 1 ) : '' ;
const columnNumber =
typeof callFrame . columnNumber === 'number' ? String ( callFrame . columnNumber + 1 ) : '' ;
const hitCount = typeof node . hitCount === 'number' ? node . hitCount : '' ;
const children = Array . isArray ( node . children )
? node . children
. filter ( ( child ) => typeof child === 'number' && included _node _ids . has ( child ) )
. join ( ', ' )
: '' ;
const deoptReason = typeof node . deoptReason === 'string' ? node . deoptReason : '' ;
lines . push (
` | ${ escape _markdown _cell ( String ( id ) ) } | ${ escape _markdown _cell ( String ( parentId ) ) } | ${ escape _markdown _cell ( functionName ) } | ${ escape _markdown _cell ( url ) } | ${ escape _markdown _cell ( lineNumber ) } | ${ escape _markdown _cell ( columnNumber ) } | ${ escape _markdown _cell ( String ( hitCount ) ) } | ${ escape _markdown _cell ( children ) } | ${ escape _markdown _cell ( deoptReason ) } | `
) ;
}
}
return ` ${ lines . join ( '\n' ) } \n ` ;
}
/ * *
* @ template T
* @ param { string | null } profile _dir
@ -73,8 +320,14 @@ export async function with_cpu_profile(profile_dir, profile_name, fn) {
return await fn ( ) ;
} finally {
const { profile } = /** @type {{ profile: object }} */ ( await session . post ( 'Profiler.stop' ) ) ;
const file = path . join ( profile _dir , ` ${ safe ( profile _name ) } .cpuprofile ` ) ;
fs . writeFileSync ( file , JSON . stringify ( profile ) ) ;
const safe _profile _name = safe ( profile _name ) ;
const profile _file = path . join ( profile _dir , ` ${ safe _profile _name } .cpuprofile ` ) ;
const markdown _file = path . join ( profile _dir , ` ${ safe _profile _name } .md ` ) ;
fs . writeFileSync ( profile _file , JSON . stringify ( profile ) ) ;
fs . writeFileSync (
markdown _file ,
profile _to _markdown ( /** @type {Record<string, unknown>} */ ( profile ) )
) ;
session . disconnect ( ) ;
}
}