@@ -4,8 +4,7 @@ import { walk } from 'estree-walker'
44import type { Node } from 'estree-walker'
55import MagicString from 'magic-string'
66import tsBlankSpace from 'ts-blank-space'
7-
8- import { generateFinallyCode , generateInitCode , leading , trailing } from './timings-babel.mjs'
7+ import { fileURLToPath } from 'node:url'
98
109declare global{
1110
@@ -56,3 +55,123 @@ export function AnnotateFunctionTimingsPlugin (): Plugin {
5655} ,
5756} satisfies Plugin
5857}
58+
59+ const metricsPath = fileURLToPath ( new URL ( '../../debug-timings.json' , import . meta. url ) )
60+
61+ // inlined from https://github.com/danielroe/errx
62+ function captureStackTrace ( ) {
63+ const IS_ABSOLUTE_RE = / ^ [ / \\ ] (? ! [ / \\ ] ) | ^ [ / \\ ] { 2 } (? ! \. ) | ^ [ a - z ] : [ / \\ ] / i
64+ const LINE_RE = / ^ \s + a t (?: (?< function > [ ^ ) ] + ) \( ) ? (?< source > [ ^ ) ] + ) \) ? $ / u
65+ const SOURCE_RE = / ^ (?< source > .+ ) : (?< line > \d + ) : (?< column > \d + ) $ / u
66+
67+ if ( ! Error . captureStackTrace ) {
68+ return [ ]
69+ }
70+ // eslint-disable-next-line unicorn/error-message
71+ const stack = new Error ( )
72+ Error . captureStackTrace ( stack )
73+ const trace = [ ]
74+ for ( const line of stack . stack ?. split ( '\n' ) || [ ] ) {
75+ const parsed = LINE_RE . exec ( line ) ?. groups
76+ if ( ! parsed ) {
77+ continue
78+ }
79+ if ( ! parsed . source ) {
80+ continue
81+ }
82+ const parsedSource = SOURCE_RE . exec ( parsed . source ) ?. groups
83+ if ( parsedSource ) {
84+ Object . assign ( parsed , parsedSource )
85+ }
86+ if ( IS_ABSOLUTE_RE . test ( parsed . source ) ) {
87+ parsed . source = `file://${ parsed . source } `
88+ }
89+ if ( parsed . source === import . meta. url ) {
90+ continue
91+ }
92+ for ( const key of [ 'line' , 'column' ] ) {
93+ //@ts -expect-error changing type from string to number
94+ parsed [ key ] &&= Number ( parsed [ key ] )
95+ }
96+ trace . push ( parsed )
97+ }
98+ return trace
99+ }
100+
101+ export const leading = `
102+ import { writeFileSync as ____writeFileSync } from 'node:fs'
103+ const ___captureStackTrace =${ captureStackTrace . toString ( ) } ;
104+ globalThis.___calls ||= {};
105+ globalThis.___timings ||= {};
106+ globalThis.___callers ||= {};`
107+
108+ function onExit ( ) {
109+ if ( globalThis . ___logged ) { return }
110+ globalThis . ___logged = true
111+
112+ ____writeFileSync ( metricsPath , JSON . stringify ( Object . fromEntries ( Object . entries ( globalThis . ___timings ) . map ( ( [ name , time ] ) => [
113+ name ,
114+ {
115+ time :Number ( Number ( time ) . toFixed ( 2 ) ) ,
116+ calls :globalThis . ___calls [ name ] ,
117+ callers :globalThis . ___callers [ name ] ?Object . fromEntries ( Object . entries ( globalThis . ___callers [ name ] ) . map ( ( [ name , count ] ) => [ name . trim ( ) , count ] ) . sort ( ( a , b ) => typeof b [ 0 ] === 'string' && typeof a [ 0 ] === 'string' ?a [ 0 ] . localeCompare ( b [ 0 ] ) :0 ) ) :undefined ,
118+ } ,
119+ ] ) . sort ( ( a , b ) => typeof b [ 0 ] === 'string' && typeof a [ 0 ] === 'string' ?a [ 0 ] . localeCompare ( b [ 0 ] ) :0 ) ) , null , 2 ) )
120+
121+ // worst by total time
122+ const timings = Object . entries ( globalThis . ___timings )
123+
124+ const topFunctionsTotalTime = timings
125+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
126+ . slice ( 0 , 20 )
127+ . map ( ( [ name , time ] ) => ( {
128+ name,
129+ time :Number ( Number ( time ) . toFixed ( 2 ) ) ,
130+ calls :globalThis . ___calls [ name ] ,
131+ callers :globalThis . ___callers [ name ] && Object . entries ( globalThis . ___callers [ name ] ) . map ( ( [ name , count ] ) => `${ name . trim ( ) } (${ count } )` ) . join ( ', ' ) ,
132+ } ) )
133+
134+ // eslint-disable-next-line no-console
135+ console . log ( 'Top 20 functions by total time:' )
136+ // eslint-disable-next-line no-console
137+ console . table ( topFunctionsTotalTime )
138+
139+ // worst by average time (excluding single calls)
140+ const topFunctionsAverageTime = timings
141+ . filter ( ( [ name ] ) => ( globalThis . ___calls [ name ] || 0 ) > 1 )
142+ . map ( ( [ name , time ] ) => [ name , time / ( globalThis . ___calls [ name ] || 1 ) ] as const )
143+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
144+ . slice ( 0 , 20 )
145+ . map ( ( [ name , time ] ) => ( {
146+ name,
147+ time :Number ( Number ( time ) . toFixed ( 2 ) ) ,
148+ calls :name && globalThis . ___calls [ name ] ,
149+ callers :name && globalThis . ___callers [ name ] && Object . entries ( globalThis . ___callers [ name ] ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) . map ( ( [ name , count ] ) => `${ name . trim ( ) } (${ count } )` ) . join ( ', ' ) ,
150+ } ) )
151+
152+ // eslint-disable-next-line no-console
153+ console . log ( 'Top 20 functions by average time:' )
154+ // eslint-disable-next-line no-console
155+ console . table ( topFunctionsAverageTime )
156+ }
157+
158+ export const trailing = `process.on("exit",${ onExit . toString ( ) . replace ( 'metricsPath' , JSON . stringify ( metricsPath ) ) } )`
159+
160+ export function generateInitCode ( functionName :string ) {
161+ return `
162+ ___calls[${ JSON . stringify ( functionName ) } ] = (___calls[${ JSON . stringify ( functionName ) } ] || 0) + 1;
163+ ___timings[${ JSON . stringify ( functionName ) } ] ||= 0;
164+ const ___now = Date.now();`
165+ }
166+
167+ export function generateFinallyCode ( functionName :string ) {
168+ return `
169+ ___timings[${ JSON . stringify ( functionName ) } ] += Date.now() - ___now;
170+ try {
171+ const ___callee = ___captureStackTrace()[1]?.function;
172+ if (___callee) {
173+ ___callers[${ JSON . stringify ( functionName ) } ] ||= {};
174+ ___callers[${ JSON . stringify ( functionName ) } ][' ' + ___callee] = (___callers[${ JSON . stringify ( functionName ) } ][' ' + ___callee] || 0) + 1;
175+ }
176+ } catch {}`
177+ }