@@ -35,7 +35,7 @@ interface TraceEntry {
3535interface CallRange {
3636 id : string
3737 typeId : string
38- typeName : string
38+ functionName : string
3939 callSite : string
4040 startTime : number
4141 endTime : number
@@ -53,8 +53,7 @@ interface CallSiteDetail {
5353
5454interface FunctionStats {
5555 typeId : string
56- typeName : string
57- callSites : Set < string >
56+ functionName : string
5857 totalTime : number
5958 selfTime : number
6059 count : number
@@ -73,6 +72,14 @@ interface AnalysisContext {
7372 allFunctions : FunctionStats [ ]
7473}
7574
75+ interface UngroupedCallStats {
76+ typeId : string
77+ functionName : string
78+ callSite : string
79+ duration : number
80+ selfTime : number
81+ }
82+
7683export const trace = async ( args : string [ ] ) : Promise < void > => {
7784 const packageDir = resolve ( args [ 0 ] ?? process . cwd ( ) )
7885 const config = getConfig ( )
@@ -232,14 +239,13 @@ const processCallExpression = (
232239 // Use the function name for both ID and display name
233240 // This will ensure all calls to the same function are grouped together
234241 const typeId = `function-${ functionName } `
235- const typeName = functionName
236242
237243 const callSite = `${ entry . args . path } :${ pos } -${ end } `
238244
239245 ctx . callRanges . push ( {
240246 id : `${ entry . ts } -${ pos } -${ end } ` ,
241247 typeId,
242- typeName ,
248+ functionName ,
243249 callSite,
244250 startTime : entry . ts ,
245251 endTime : entry . ts + ( entry . dur || 0 ) ,
@@ -268,7 +274,7 @@ const processNonCallNode = (
268274 ctx . callRanges . push ( {
269275 id : `${ entry . ts } -${ pos } -${ end } ` ,
270276 typeId,
271- typeName,
277+ functionName : typeName ,
272278 callSite,
273279 startTime : entry . ts ,
274280 endTime : entry . ts + ( entry . dur || 0 ) ,
@@ -341,8 +347,7 @@ const collectFunctionStats = (call: CallRange, ctx: AnalysisContext): void => {
341347 if ( ! ctx . functionStats [ call . typeId ] ) {
342348 ctx . functionStats [ call . typeId ] = {
343349 typeId : call . typeId ,
344- typeName : call . typeName ,
345- callSites : new Set ( ) ,
350+ functionName : call . functionName ,
346351 totalTime : 0 ,
347352 selfTime : 0 ,
348353 count : 0 ,
@@ -357,7 +362,6 @@ const collectFunctionStats = (call: CallRange, ctx: AnalysisContext): void => {
357362 addCallSiteToStats ( call , stats )
358363
359364 // Update aggregate metrics
360- stats . callSites . add ( call . callSite )
361365 stats . totalTime += call . duration
362366 stats . selfTime += call . selfTime
363367 stats . count ++
@@ -390,81 +394,193 @@ const sortAndRankFunctions = (ctx: AnalysisContext): void => {
390394 )
391395}
392396
393- const displaySummary = ( ctx : AnalysisContext ) : void => {
394- const top20Functions = ctx . allFunctions . slice ( 0 , 20 )
397+ const analyzeTypeInstantiations = ( traceDir : string ) : void => {
398+ // Initialize the analysis context
399+ const ctx = initializeAnalysisContext ( traceDir )
395400
396- console . log ( "\nTop Functions by Self Type-Checking Time:\n" )
397- console . log (
398- "Rank | Type Name | Self Time (ms) | Calls | Unique Sites | Top Usage (ms)"
399- )
400- console . log (
401- "-----|-----------------------------------------------|----------------|-------|--------------|-------------------------------------------"
402- )
401+ // Filter entries with duration information
402+ filterDurationEntries ( ctx )
403403
404- top20Functions . forEach ( ( stats , index ) => printFunctionSummary ( stats , index ) )
405- }
404+ // Process each duration entry to extract call ranges
405+ ctx . durationEntries . forEach ( entry => processDurationEntry ( entry , ctx ) )
406406
407- const printFunctionSummary = ( stats : FunctionStats , index : number ) : void => {
408- const typeNameFormatted = formatTypeName ( stats . typeName , 45 )
409- const selfTimeMs = ( stats . selfTime / 1000 ) . toFixed ( 2 ) . padStart ( 14 )
410- const calls = stats . count . toString ( ) . padStart ( 5 )
411- const sites = stats . callSites . size . toString ( ) . padStart ( 12 )
407+ // Build call tree from ranges
408+ buildCallTree ( ctx )
412409
413- // Get details about top usage site
414- const topUsage = stats . detailedCallSites [ 0 ] || {
415- path : "unknown" ,
416- pos : 0 ,
417- end : 0 ,
418- selfTime : 0
419- }
420- const topUsageTime = ( topUsage . selfTime / 1000 ) . toFixed ( 2 ) + "ms"
421- const topUsageLocation = formatLocation (
422- `${ topUsage . path } :${ topUsage . pos } -${ topUsage . end } `
410+ // Calculate self-time for each call
411+ ctx . rootCalls . forEach ( calculateSelfTimes )
412+
413+ // Collect statistics by view type
414+ const ungroupedStats = collectUngroupedStats ( ctx )
415+ ctx . rootCalls . forEach ( call => collectFunctionStats ( call , ctx ) )
416+
417+ // Sort and rank functions
418+ sortAndRankFunctions ( ctx )
419+
420+ // Display summaries to console
421+ console . log ( "\n📊 Performance Analysis - Top Individual Calls:\n" )
422+ displayIndividualSummary ( ungroupedStats )
423+
424+ console . log ( "\n📊 Performance Analysis - Functions by Self Time:\n" )
425+ displayGroupedSummary ( ctx . allFunctions )
426+
427+ // Write CSV reports
428+ const rangesCsvPath = join ( traceDir , "ranges.csv" )
429+ const namesCsvPath = join ( traceDir , "names.csv" )
430+
431+ // Export individual ranges to CSV
432+ writeToCsv (
433+ rangesCsvPath ,
434+ [ "Function Name" , "Self (ms)" , "Duration (ms)" , "Location" ] ,
435+ ungroupedStats . map ( stat => [
436+ stat . functionName ,
437+ ( stat . selfTime / 1000 ) . toFixed ( 3 ) ,
438+ ( stat . duration / 1000 ) . toFixed ( 3 ) ,
439+ formatLocation ( stat . callSite )
440+ ] )
441+ )
442+
443+ // Export grouped functions to CSV
444+ writeToCsv (
445+ namesCsvPath ,
446+ [
447+ "Function Name" ,
448+ "Total Self (ms)" ,
449+ "Avg Self (ms)" ,
450+ "Call Count" ,
451+ "Top Location" ,
452+ "Top Self (ms)"
453+ ] ,
454+ ctx . allFunctions . map ( stats => {
455+ const topUsage = stats . detailedCallSites [ 0 ] || {
456+ path : "unknown" ,
457+ pos : 0 ,
458+ end : 0 ,
459+ selfTime : 0
460+ }
461+ return [
462+ stats . functionName ,
463+ ( stats . selfTime / 1000 ) . toFixed ( 3 ) ,
464+ ( stats . selfTime / stats . count / 1000 ) . toFixed ( 3 ) ,
465+ stats . count . toString ( ) ,
466+ formatLocation ( `${ topUsage . path } :${ topUsage . pos } -${ topUsage . end } ` ) ,
467+ ( topUsage . selfTime / 1000 ) . toFixed ( 3 )
468+ ]
469+ } )
423470 )
424471
425472 console . log (
426- `${ ( index + 1 ) . toString ( ) . padStart ( 4 ) } | ${ typeNameFormatted } | ${ selfTimeMs } | ${ calls } | ${ sites } | ${ topUsageLocation } (${ topUsageTime } )`
473+ `\n✅ Analysis complete! Results exported to:\n` +
474+ ` - ${ rangesCsvPath } (individual calls)\n` +
475+ ` - ${ namesCsvPath } (grouped by function name)`
427476 )
428477}
429478
430- const writeDetailedReport = ( ctx : AnalysisContext ) : void => {
431- const outputPath = join ( ctx . traceDir , "analysis.txt" )
432- let outputContent = createReportHeader ( )
479+ /**
480+ * Collect statistics for individual (ungrouped) calls
481+ */
482+ const collectUngroupedStats = ( ctx : AnalysisContext ) : UngroupedCallStats [ ] => {
483+ const ungroupedStats : UngroupedCallStats [ ] = [ ]
433484
434- // Add detailed information for each function
435- ctx . allFunctions . forEach ( ( stats , index ) => {
436- outputContent += createFunctionReport ( stats , index )
437- } )
485+ // Flatten the call tree into individual entries
486+ const flattenCallTree = ( call : CallRange ) : void => {
487+ ungroupedStats . push ( {
488+ typeId : call . typeId ,
489+ functionName : call . functionName ,
490+ callSite : call . callSite ,
491+ duration : call . duration ,
492+ selfTime : call . selfTime
493+ } )
438494
439- // Write to file
440- writeFile ( outputPath , outputContent )
441- console . log (
442- `\n✅ Analysis complete! A more detailed breakdown is available at ${ outputPath } `
443- )
495+ call . children . forEach ( flattenCallTree )
496+ }
497+
498+ ctx . rootCalls . forEach ( flattenCallTree )
499+
500+ // Sort by self time in descending order
501+ return ungroupedStats . sort ( ( a , b ) => b . selfTime - a . selfTime )
444502}
445503
446- const createReportHeader = ( ) : string =>
447- "TypeScript Type-Checking Performance Analysis\n" +
448- "===========================================\n\n" +
449- "Functions sorted by total self type-checking time\n\n"
504+ /**
505+ * Display a summary of individual calls
506+ */
507+ const displayIndividualSummary = ( stats : UngroupedCallStats [ ] ) : void => {
508+ displayTableHeader ( [ "Rank" , "Function Name" , "Self (ms)" , "Location" ] )
509+
510+ const top20 = stats . slice ( 0 , 20 )
450511
451- const createFunctionReport = ( stats : FunctionStats , index : number ) : string => {
452- let report = `${ index + 1 } . ${ stats . typeName } \n`
453- report += ` Self time: ${ ( stats . selfTime / 1000 ) . toFixed ( 2 ) } ms\n`
454- report += ` Total time: ${ ( stats . totalTime / 1000 ) . toFixed ( 2 ) } ms\n`
455- report += ` Call count: ${ stats . count } \n`
456- report += ` Unique call sites: ${ stats . callSites . size } \n`
512+ top20 . forEach ( ( stat , index ) => {
513+ const typeNameFormatted = formatTypeName ( stat . functionName , 20 )
514+ const selfTimeMs = ( stat . selfTime / 1000 ) . toFixed ( 2 ) . padStart ( 15 )
515+ const location = formatLocation ( stat . callSite )
457516
458- if ( stats . detailedCallSites . length > 0 ) {
459- report += ` Top call sites by self time:\n`
460- stats . detailedCallSites . forEach ( ( site , i ) => {
461- report += ` ${ i + 1 } . ${ site . path } :${ site . pos } -${ site . end } \n`
462- report += ` Self time: ${ ( site . selfTime / 1000 ) . toFixed ( 2 ) } ms\n`
463- } )
464- }
517+ console . log (
518+ `${ ( index + 1 ) . toString ( ) . padStart ( 4 ) } | ${ typeNameFormatted } | ${ selfTimeMs } | ${ location } `
519+ )
520+ } )
521+ }
522+
523+ /**
524+ * Display a summary table header with aligned columns
525+ */
526+ const displayTableHeader = ( columns : string [ ] ) : void => {
527+ // Create header row with standard column widths
528+ const headerRow = [
529+ columns [ 0 ] . padEnd ( 4 ) ,
530+ columns [ 1 ] . padEnd ( 20 ) ,
531+ columns [ 2 ] . padEnd ( 15 ) ,
532+ ...columns . slice ( 3 )
533+ ] . join ( " | " )
534+
535+ // Create separator line matching the header column widths
536+ const separatorRow = [
537+ "----" ,
538+ "--------------------" ,
539+ "---------------" ,
540+ ...columns . slice ( 3 ) . map ( col => "-" . repeat ( col . length ) )
541+ ] . join ( "-|-" )
542+
543+ console . log ( headerRow )
544+ console . log ( separatorRow )
545+ }
546+
547+ /**
548+ * Display a combined summary of grouped functions showing both total and average times
549+ */
550+ const displayGroupedSummary = ( functions : FunctionStats [ ] ) : void => {
551+ displayTableHeader ( [
552+ "Rank" ,
553+ "Function Name" ,
554+ "Total Self (ms)" ,
555+ "Avg Self (ms)" ,
556+ "Calls" ,
557+ "Top Usage"
558+ ] )
559+
560+ functions . slice ( 0 , 20 ) . forEach ( ( stats , index ) => {
561+ const typeNameFormatted = formatTypeName ( stats . functionName , 20 )
562+ const totalTimeMs = ( stats . selfTime / 1000 ) . toFixed ( 2 ) . padStart ( 15 )
563+ const avgTimeMs = ( stats . selfTime / stats . count / 1000 )
564+ . toFixed ( 2 )
565+ . padStart ( 13 )
566+ const calls = stats . count . toString ( ) . padStart ( 5 )
567+
568+ // Get details about top usage site
569+ const topUsage = stats . detailedCallSites [ 0 ] || {
570+ path : "unknown" ,
571+ pos : 0 ,
572+ end : 0 ,
573+ selfTime : 0
574+ }
575+ const topUsageTime = ( topUsage . selfTime / 1000 ) . toFixed ( 2 ) + "ms"
576+ const topUsageLocation = formatLocation (
577+ `${ topUsage . path } :${ topUsage . pos } -${ topUsage . end } `
578+ )
465579
466- report += "\n"
467- return report
580+ console . log (
581+ `${ ( index + 1 ) . toString ( ) . padStart ( 4 ) } | ${ typeNameFormatted } | ${ totalTimeMs } | ${ avgTimeMs } | ${ calls } | ${ topUsageLocation } (${ topUsageTime } )`
582+ )
583+ } )
468584}
469585
470586/**
@@ -584,31 +700,30 @@ const findNodeByPreference = (nodes: ts.Node[]): ts.Node | undefined => {
584700 return nodes [ 0 ]
585701}
586702
587- const analyzeTypeInstantiations = ( traceDir : string ) : void => {
588- // Initialize the analysis context
589- const ctx = initializeAnalysisContext ( traceDir )
590-
591- // Filter entries with duration information
592- filterDurationEntries ( ctx )
593-
594- // Process each duration entry to extract call ranges
595- ctx . durationEntries . forEach ( entry => processDurationEntry ( entry , ctx ) )
596-
597- // Build call tree from ranges
598- buildCallTree ( ctx )
599-
600- // Calculate self-time for each call
601- ctx . rootCalls . forEach ( calculateSelfTimes )
602-
603- // Collect statistics about function types
604- ctx . rootCalls . forEach ( call => collectFunctionStats ( call , ctx ) )
605-
606- // Sort and rank functions by self-time
607- sortAndRankFunctions ( ctx )
703+ /**
704+ * Write data to a CSV file
705+ */
706+ const writeToCsv = (
707+ filePath : string ,
708+ headers : string [ ] ,
709+ rows : string [ ] [ ]
710+ ) : void => {
711+ const content = [
712+ headers . join ( "," ) ,
713+ ...rows . map ( row => row . map ( escapeForCsv ) . join ( "," ) )
714+ ] . join ( "\n" )
608715
609- // Display summary to console
610- displaySummary ( ctx )
716+ writeFile ( filePath , content )
717+ }
611718
612- // Write detailed report to file
613- writeDetailedReport ( ctx )
719+ /**
720+ * Escape string for CSV format
721+ */
722+ const escapeForCsv = ( value : string ) : string => {
723+ if ( value . includes ( "," ) || value . includes ( '"' ) || value . includes ( "\n" ) ) {
724+ // If the value contains commas, double quotes, or newlines, wrap it in quotes
725+ // and escape any double quotes by doubling them
726+ return `"${ value . replace ( / " / g, '""' ) } "`
727+ }
728+ return value
614729}
0 commit comments