Skip to content

Commit cf9dfcf

Browse files
committed
improve attest trace, bump versions
1 parent 2948b72 commit cf9dfcf

File tree

6 files changed

+212
-97
lines changed

6 files changed

+212
-97
lines changed

ark/attest/cli/trace.ts

Lines changed: 207 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface TraceEntry {
3535
interface 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

5454
interface 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+
7683
export 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
}

ark/attest/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ark/attest",
3-
"version": "0.43.1",
3+
"version": "0.43.2",
44
"license": "MIT",
55
"author": {
66
"name": "David Blass",

0 commit comments

Comments
 (0)