bsprof-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/analyzer/Aggregator.d.ts +10 -0
- package/dist/analyzer/Aggregator.js +120 -0
- package/dist/analyzer/CpuAnalyzer.d.ts +3 -0
- package/dist/analyzer/CpuAnalyzer.js +59 -0
- package/dist/analyzer/DiffAnalyzer.d.ts +3 -0
- package/dist/analyzer/DiffAnalyzer.js +114 -0
- package/dist/analyzer/MemoryAnalyzer.d.ts +3 -0
- package/dist/analyzer/MemoryAnalyzer.js +47 -0
- package/dist/analyzer/common.d.ts +4 -0
- package/dist/analyzer/common.js +31 -0
- package/dist/formatter/ChromeTraceExporter.d.ts +13 -0
- package/dist/formatter/ChromeTraceExporter.js +106 -0
- package/dist/formatter/CsvExporter.d.ts +5 -0
- package/dist/formatter/CsvExporter.js +32 -0
- package/dist/formatter/JsonFormatter.d.ts +10 -0
- package/dist/formatter/JsonFormatter.js +21 -0
- package/dist/formatter/MarkdownFormatter.d.ts +15 -0
- package/dist/formatter/MarkdownFormatter.js +174 -0
- package/dist/formatter/TextFormatter.d.ts +16 -0
- package/dist/formatter/TextFormatter.js +193 -0
- package/dist/formatter/helpers.d.ts +8 -0
- package/dist/formatter/helpers.js +44 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +220 -0
- package/dist/lib.d.ts +27 -0
- package/dist/lib.js +30 -0
- package/dist/parser/BsprofParser.d.ts +27 -0
- package/dist/parser/BsprofParser.js +251 -0
- package/dist/parser/types.d.ts +202 -0
- package/dist/parser/types.js +2 -0
- package/dist/parser/varint.d.ts +21 -0
- package/dist/parser/varint.js +60 -0
- package/package.json +59 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { BsprofParser } from '../parser/BsprofParser.js';
|
|
2
|
+
/**
|
|
3
|
+
* Export profiling data to Chrome DevTools Trace Event format.
|
|
4
|
+
* Viewable in chrome://tracing or https://ui.perfetto.dev
|
|
5
|
+
*
|
|
6
|
+
* Each CPU measurement becomes a Complete ("X") event with duration.
|
|
7
|
+
* Thread/module structure is preserved via pid/tid mapping.
|
|
8
|
+
*/
|
|
9
|
+
export class ChromeTraceExporter {
|
|
10
|
+
export(profile) {
|
|
11
|
+
const events = [];
|
|
12
|
+
const { cpuByPE } = profile.parseResult;
|
|
13
|
+
// Map module names to numeric pids, thread IDs
|
|
14
|
+
const moduleIds = new Map();
|
|
15
|
+
let nextPid = 1;
|
|
16
|
+
// Metadata events for process/thread names
|
|
17
|
+
const metaEvents = [];
|
|
18
|
+
let tsAccumulator = 0;
|
|
19
|
+
// Sort by cpuSelf descending for consistent layout
|
|
20
|
+
const sortedEntries = [...cpuByPE.entries()].sort((a, b) => b[1].cpuSelf - a[1].cpuSelf);
|
|
21
|
+
for (const [peId, cpu] of sortedEntries) {
|
|
22
|
+
if (cpu.cpuSelf <= 0 && cpu.wallSelf <= 0)
|
|
23
|
+
continue;
|
|
24
|
+
const mod = BsprofParser.resolveModuleName(profile, peId);
|
|
25
|
+
const func = BsprofParser.resolveFuncName(profile, peId);
|
|
26
|
+
const file = BsprofParser.resolveFileName(profile, peId);
|
|
27
|
+
let pid = moduleIds.get(mod);
|
|
28
|
+
if (pid === undefined) {
|
|
29
|
+
pid = nextPid++;
|
|
30
|
+
moduleIds.set(mod, pid);
|
|
31
|
+
metaEvents.push({
|
|
32
|
+
name: 'process_name',
|
|
33
|
+
cat: '__metadata',
|
|
34
|
+
ph: 'M',
|
|
35
|
+
ts: 0,
|
|
36
|
+
pid,
|
|
37
|
+
tid: 0,
|
|
38
|
+
args: { name: mod },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Use wall time for duration (in microseconds)
|
|
42
|
+
const dur = cpu.wallSelf > 0 ? cpu.wallSelf : cpu.cpuSelf;
|
|
43
|
+
events.push({
|
|
44
|
+
name: func,
|
|
45
|
+
cat: file,
|
|
46
|
+
ph: 'X',
|
|
47
|
+
ts: tsAccumulator,
|
|
48
|
+
dur,
|
|
49
|
+
pid,
|
|
50
|
+
tid: 1,
|
|
51
|
+
args: {
|
|
52
|
+
file,
|
|
53
|
+
cpuSelf: cpu.cpuSelf,
|
|
54
|
+
wallSelf: cpu.wallSelf,
|
|
55
|
+
callCount: cpu.callCount || 0,
|
|
56
|
+
module: mod,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
tsAccumulator += dur;
|
|
60
|
+
}
|
|
61
|
+
// Build call-stack-based flame chart for top entries
|
|
62
|
+
this.addCallStackEvents(profile, events, moduleIds);
|
|
63
|
+
const allEvents = [...metaEvents, ...events];
|
|
64
|
+
return JSON.stringify({ traceEvents: allEvents }, null, 2);
|
|
65
|
+
}
|
|
66
|
+
addCallStackEvents(profile, events, moduleIds) {
|
|
67
|
+
const { cpuByPE } = profile.parseResult;
|
|
68
|
+
// For entries with significant CPU, reconstruct nested call stacks
|
|
69
|
+
const topEntries = [...cpuByPE.entries()]
|
|
70
|
+
.filter(([, cpu]) => cpu.cpuSelf > 0)
|
|
71
|
+
.sort((a, b) => b[1].cpuSelf - a[1].cpuSelf)
|
|
72
|
+
.slice(0, 100);
|
|
73
|
+
let stackTs = events.length > 0 ? (events[events.length - 1].ts ?? 0) + (events[events.length - 1].dur ?? 0) + 1000 : 0;
|
|
74
|
+
for (const [peId, cpu] of topEntries) {
|
|
75
|
+
const stack = BsprofParser.resolveCallStack(profile, peId);
|
|
76
|
+
if (stack.length <= 1)
|
|
77
|
+
continue;
|
|
78
|
+
const mod = BsprofParser.resolveModuleName(profile, peId);
|
|
79
|
+
const pid = moduleIds.get(mod) ?? 1;
|
|
80
|
+
const totalDur = cpu.wallSelf > 0 ? cpu.wallSelf : cpu.cpuSelf;
|
|
81
|
+
// Emit nested "B"/"E" events for the call stack (deepest first in stack)
|
|
82
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
83
|
+
events.push({
|
|
84
|
+
name: stack[i],
|
|
85
|
+
cat: 'callstack',
|
|
86
|
+
ph: 'B',
|
|
87
|
+
ts: stackTs,
|
|
88
|
+
pid,
|
|
89
|
+
tid: 2,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0; i < stack.length; i++) {
|
|
93
|
+
events.push({
|
|
94
|
+
name: stack[i],
|
|
95
|
+
cat: 'callstack',
|
|
96
|
+
ph: 'E',
|
|
97
|
+
ts: stackTs + totalDur,
|
|
98
|
+
pid,
|
|
99
|
+
tid: 2,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
stackTs += totalDur + 100;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=ChromeTraceExporter.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { aggregate } from '../analyzer/Aggregator.js';
|
|
2
|
+
const CSV_HEADER = 'function,file,module,allocBytes,freeBytes,retainedBytes,allocCount,freeCount,cpuSelf,wallSelf,callCount';
|
|
3
|
+
export class CsvExporter {
|
|
4
|
+
export(profile, options = {}) {
|
|
5
|
+
const { byFunction } = aggregate(profile, options);
|
|
6
|
+
const lines = [CSV_HEADER];
|
|
7
|
+
const sorted = [...byFunction.values()].sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
8
|
+
for (const f of sorted) {
|
|
9
|
+
lines.push([
|
|
10
|
+
csvEscape(f.function),
|
|
11
|
+
csvEscape(f.file),
|
|
12
|
+
csvEscape(f.module),
|
|
13
|
+
f.allocBytes,
|
|
14
|
+
f.freeBytes,
|
|
15
|
+
f.retainedBytes,
|
|
16
|
+
f.allocCount,
|
|
17
|
+
f.freeCount,
|
|
18
|
+
f.cpuSelf,
|
|
19
|
+
f.wallSelf,
|
|
20
|
+
f.callCount,
|
|
21
|
+
].join(','));
|
|
22
|
+
}
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function csvEscape(value) {
|
|
27
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
28
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=CsvExporter.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MemoryReport, CpuReport, FullReport, SummaryReport, DiffReport, HeaderInfo } from '../parser/types.js';
|
|
2
|
+
export declare class JsonFormatter {
|
|
3
|
+
formatMemoryReport(report: MemoryReport): string;
|
|
4
|
+
formatCpuReport(report: CpuReport): string;
|
|
5
|
+
formatFullReport(report: FullReport): string;
|
|
6
|
+
formatSummaryReport(report: SummaryReport): string;
|
|
7
|
+
formatDiffReport(report: DiffReport): string;
|
|
8
|
+
formatInfo(header: HeaderInfo, fileSize: number): string;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=JsonFormatter.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class JsonFormatter {
|
|
2
|
+
formatMemoryReport(report) {
|
|
3
|
+
return JSON.stringify(report, null, 2);
|
|
4
|
+
}
|
|
5
|
+
formatCpuReport(report) {
|
|
6
|
+
return JSON.stringify(report, null, 2);
|
|
7
|
+
}
|
|
8
|
+
formatFullReport(report) {
|
|
9
|
+
return JSON.stringify(report, null, 2);
|
|
10
|
+
}
|
|
11
|
+
formatSummaryReport(report) {
|
|
12
|
+
return JSON.stringify(report, null, 2);
|
|
13
|
+
}
|
|
14
|
+
formatDiffReport(report) {
|
|
15
|
+
return JSON.stringify(report, null, 2);
|
|
16
|
+
}
|
|
17
|
+
formatInfo(header, fileSize) {
|
|
18
|
+
return JSON.stringify({ ...header, fileSize }, null, 2);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=JsonFormatter.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MemoryReport, CpuReport, FullReport, SummaryReport, DiffReport, HeaderInfo } from '../parser/types.js';
|
|
2
|
+
export declare class MarkdownFormatter {
|
|
3
|
+
formatMemoryReport(report: MemoryReport): string;
|
|
4
|
+
formatCpuReport(report: CpuReport): string;
|
|
5
|
+
formatFullReport(report: FullReport): string;
|
|
6
|
+
formatSummaryReport(report: SummaryReport): string;
|
|
7
|
+
formatDiffReport(report: DiffReport): string;
|
|
8
|
+
formatInfo(header: HeaderInfo, fileSize: number): string;
|
|
9
|
+
private headerBlock;
|
|
10
|
+
private moduleTable;
|
|
11
|
+
private functionTable;
|
|
12
|
+
private cpuTable;
|
|
13
|
+
private deltaTable;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=MarkdownFormatter.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { fmtBytes, fmtTime, fmtDuration, fmtNum } from './helpers.js';
|
|
2
|
+
export class MarkdownFormatter {
|
|
3
|
+
formatMemoryReport(report) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
lines.push(this.headerBlock(report.header));
|
|
6
|
+
lines.push('');
|
|
7
|
+
lines.push('## Memory Summary');
|
|
8
|
+
lines.push('');
|
|
9
|
+
lines.push(`| Metric | Value |`);
|
|
10
|
+
lines.push(`|--------|-------|`);
|
|
11
|
+
lines.push(`| Total Allocated | ${fmtBytes(report.summary.totalAllocBytes)} |`);
|
|
12
|
+
lines.push(`| Total Freed | ${fmtBytes(report.summary.totalFreeBytes)} |`);
|
|
13
|
+
lines.push(`| **Total Retained** | **${fmtBytes(report.summary.totalRetainedBytes)}** |`);
|
|
14
|
+
lines.push(`| Alloc Count | ${fmtNum(report.summary.totalAllocCount)} |`);
|
|
15
|
+
lines.push(`| Free Count | ${fmtNum(report.summary.totalFreeCount)} |`);
|
|
16
|
+
lines.push('');
|
|
17
|
+
lines.push('## Memory by Module');
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push(this.moduleTable(report.byModule));
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push(`## Top ${report.byFunction.length} Functions by Retained Bytes`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push(this.functionTable(report.byFunction));
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
formatCpuReport(report) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push(this.headerBlock(report.header));
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push('## CPU Summary');
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push(`| Metric | Value |`);
|
|
33
|
+
lines.push(`|--------|-------|`);
|
|
34
|
+
lines.push(`| Total CPU Time | ${fmtTime(report.summary.totalCpuTime)} |`);
|
|
35
|
+
lines.push(`| Total Wall Time | ${fmtTime(report.summary.totalWallTime)} |`);
|
|
36
|
+
lines.push(`| Total Calls | ${fmtNum(report.summary.totalCallCount)} |`);
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push(`## Top ${report.byFunction.length} Functions by CPU Self Time`);
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push(this.cpuTable(report.byFunction));
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
formatFullReport(report) {
|
|
44
|
+
return this.formatMemoryReport(report.memory) + '\n\n---\n\n' + this.formatCpuReport(report.cpu);
|
|
45
|
+
}
|
|
46
|
+
formatSummaryReport(report) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(this.headerBlock(report.header));
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('## Parse Stats');
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(`| Metric | Value |`);
|
|
53
|
+
lines.push(`|--------|-------|`);
|
|
54
|
+
lines.push(`| Entries | ${fmtNum(report.parseStats.entries)} |`);
|
|
55
|
+
lines.push(`| Strings | ${fmtNum(report.parseStats.strings)} |`);
|
|
56
|
+
lines.push(`| Modules | ${fmtNum(report.parseStats.modules)} |`);
|
|
57
|
+
lines.push(`| Path Elements | ${fmtNum(report.parseStats.pathElements)} |`);
|
|
58
|
+
lines.push(`| Errors | ${report.parseStats.errors} |`);
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('## Module Overview');
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(this.moduleTable(report.moduleOverview));
|
|
63
|
+
if (report.topMemoryLeaks.length > 0) {
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('## Top Memory Leaks');
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(this.functionTable(report.topMemoryLeaks));
|
|
68
|
+
}
|
|
69
|
+
if (report.topCpuConsumers.length > 0) {
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('## Top CPU Consumers');
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(this.cpuTable(report.topCpuConsumers));
|
|
74
|
+
}
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
formatDiffReport(report) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push('# Profile Comparison');
|
|
80
|
+
lines.push('');
|
|
81
|
+
lines.push(`- **Before:** ${report.before.file} (${report.before.targetVersion})`);
|
|
82
|
+
lines.push(`- **After:** ${report.after.file} (${report.after.targetVersion})`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
const md = report.memoryDelta;
|
|
85
|
+
lines.push('## Memory Delta');
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`| Metric | Value |`);
|
|
88
|
+
lines.push(`|--------|-------|`);
|
|
89
|
+
lines.push(`| Retained Before | ${fmtBytes(md.totalRetainedBefore)} |`);
|
|
90
|
+
lines.push(`| Retained After | ${fmtBytes(md.totalRetainedAfter)} |`);
|
|
91
|
+
lines.push(`| **Delta** | **${fmtBytes(md.totalRetainedDelta)}** |`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
if (md.regressions.length > 0) {
|
|
94
|
+
lines.push('### Regressions');
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push(this.deltaTable(md.regressions));
|
|
97
|
+
lines.push('');
|
|
98
|
+
}
|
|
99
|
+
if (md.improvements.length > 0) {
|
|
100
|
+
lines.push('### Improvements');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push(this.deltaTable(md.improvements));
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
if (md.newLeaks.length > 0) {
|
|
106
|
+
lines.push(`**New Leaks:** ${md.newLeaks.join(', ')}`);
|
|
107
|
+
lines.push('');
|
|
108
|
+
}
|
|
109
|
+
if (md.resolvedLeaks.length > 0) {
|
|
110
|
+
lines.push(`**Resolved Leaks:** ${md.resolvedLeaks.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
formatInfo(header, fileSize) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(`# ${header.targetName} ${header.targetVersion}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(`| Field | Value |`);
|
|
119
|
+
lines.push(`|-------|-------|`);
|
|
120
|
+
lines.push(`| Device | ${header.device} |`);
|
|
121
|
+
lines.push(`| Firmware | ${header.firmware} |`);
|
|
122
|
+
lines.push(`| Format | ${header.formatVersion} |`);
|
|
123
|
+
if (header.duration > 0) {
|
|
124
|
+
lines.push(`| Duration | ${fmtDuration(header.duration)} |`);
|
|
125
|
+
}
|
|
126
|
+
lines.push(`| Size | ${fmtBytes(fileSize)} |`);
|
|
127
|
+
lines.push(`| Features | ${header.features.join(', ') || 'none'} |`);
|
|
128
|
+
return lines.join('\n');
|
|
129
|
+
}
|
|
130
|
+
headerBlock(header) {
|
|
131
|
+
return `# ${header.targetName} ${header.targetVersion}\n\n` +
|
|
132
|
+
`> Device: ${header.device} (${header.firmware}) | Format: ${header.formatVersion} | Size: ${fmtBytes(header.fileSize)}`;
|
|
133
|
+
}
|
|
134
|
+
moduleTable(modules) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push('| Module | Alloc | Free | Retained | Allocs# | Frees# |');
|
|
137
|
+
lines.push('|--------|-------|------|----------|---------|--------|');
|
|
138
|
+
for (const m of modules) {
|
|
139
|
+
lines.push(`| ${m.module} | ${fmtBytes(m.allocBytes)} | ${fmtBytes(m.freeBytes)} | ${fmtBytes(m.retainedBytes)} | ${fmtNum(m.allocCount)} | ${fmtNum(m.freeCount)} |`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
functionTable(funcs) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push('| # | Retained | Alloc | Free | Ops | Calls | CPU Self | Wall Self | Function | File |');
|
|
146
|
+
lines.push('|---|----------|-------|------|-----|-------|----------|-----------|----------|------|');
|
|
147
|
+
for (let i = 0; i < funcs.length; i++) {
|
|
148
|
+
const f = funcs[i];
|
|
149
|
+
lines.push(`| ${i + 1} | ${fmtBytes(f.retainedBytes)} | ${fmtBytes(f.allocBytes)} | ${fmtBytes(f.freeBytes)} | ${fmtNum(f.allocCount)} | ${f.callCount ? fmtNum(f.callCount) : ''} | ${fmtTime(f.cpuSelf)} | ${fmtTime(f.wallSelf)} | ${f.function} | ${f.file} |`);
|
|
150
|
+
}
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
cpuTable(funcs) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push('| # | CPU Self | Wall Self | Calls | Function | File |');
|
|
156
|
+
lines.push('|---|----------|-----------|-------|----------|------|');
|
|
157
|
+
for (let i = 0; i < funcs.length; i++) {
|
|
158
|
+
const f = funcs[i];
|
|
159
|
+
lines.push(`| ${i + 1} | ${fmtTime(f.cpuSelf)} | ${fmtTime(f.wallSelf)} | ${f.callCount ? fmtNum(f.callCount) : ''} | ${f.function} | ${f.file} |`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
deltaTable(deltas) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
lines.push('| Function | File | Before | After | Delta |');
|
|
166
|
+
lines.push('|----------|------|--------|-------|-------|');
|
|
167
|
+
for (const d of deltas) {
|
|
168
|
+
const sign = d.delta > 0 ? '+' : '';
|
|
169
|
+
lines.push(`| ${d.function} | ${d.file} | ${fmtBytes(d.retainedBefore)} | ${fmtBytes(d.retainedAfter)} | ${sign}${fmtBytes(d.delta)} |`);
|
|
170
|
+
}
|
|
171
|
+
return lines.join('\n');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=MarkdownFormatter.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MemoryReport, CpuReport, FullReport, SummaryReport, DiffReport, HeaderInfo } from '../parser/types.js';
|
|
2
|
+
export declare class TextFormatter {
|
|
3
|
+
formatMemoryReport(report: MemoryReport): string;
|
|
4
|
+
formatCpuReport(report: CpuReport): string;
|
|
5
|
+
formatFullReport(report: FullReport): string;
|
|
6
|
+
formatSummaryReport(report: SummaryReport): string;
|
|
7
|
+
formatDiffReport(report: DiffReport): string;
|
|
8
|
+
formatInfo(header: HeaderInfo, fileSize: number): string;
|
|
9
|
+
private formatHeader;
|
|
10
|
+
private moduleTable;
|
|
11
|
+
private fileTable;
|
|
12
|
+
private functionTable;
|
|
13
|
+
private cpuFunctionTable;
|
|
14
|
+
private deltaTable;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=TextFormatter.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { fmtBytes, fmtTime, fmtDuration, fmtNum, pad, padEnd } from './helpers.js';
|
|
3
|
+
export class TextFormatter {
|
|
4
|
+
formatMemoryReport(report) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(this.formatHeader(report.header));
|
|
7
|
+
lines.push('');
|
|
8
|
+
lines.push(chalk.bold('=== MEMORY SUMMARY ==='));
|
|
9
|
+
lines.push(` Total Allocated: ${chalk.cyan(fmtBytes(report.summary.totalAllocBytes))}`);
|
|
10
|
+
lines.push(` Total Freed: ${chalk.green(fmtBytes(report.summary.totalFreeBytes))}`);
|
|
11
|
+
lines.push(` Total Retained: ${chalk.red(fmtBytes(report.summary.totalRetainedBytes))}`);
|
|
12
|
+
lines.push(` Alloc Count: ${fmtNum(report.summary.totalAllocCount)}`);
|
|
13
|
+
lines.push(` Free Count: ${fmtNum(report.summary.totalFreeCount)}`);
|
|
14
|
+
lines.push(` Parse Stats: ${fmtNum(report.summary.parseStats.entries)} entries, ${report.summary.parseStats.errors} errors`);
|
|
15
|
+
lines.push('');
|
|
16
|
+
lines.push(chalk.bold('=== MEMORY BY MODULE/THREAD ==='));
|
|
17
|
+
lines.push(this.moduleTable(report.byModule));
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push(chalk.bold(`=== TOP ${report.byFile.length} MEMORY BY FILE (retained bytes) ===`));
|
|
20
|
+
lines.push(this.fileTable(report.byFile));
|
|
21
|
+
lines.push('');
|
|
22
|
+
lines.push(chalk.bold(`=== TOP ${report.byFunction.length} MEMORY BY FUNCTION (retained bytes) ===`));
|
|
23
|
+
lines.push(this.functionTable(report.byFunction));
|
|
24
|
+
return lines.join('\n');
|
|
25
|
+
}
|
|
26
|
+
formatCpuReport(report) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push(this.formatHeader(report.header));
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push(chalk.bold('=== CPU SUMMARY ==='));
|
|
31
|
+
lines.push(` Total CPU Time: ${chalk.cyan(fmtTime(report.summary.totalCpuTime))}`);
|
|
32
|
+
lines.push(` Total Wall Time: ${chalk.cyan(fmtTime(report.summary.totalWallTime))}`);
|
|
33
|
+
lines.push(` Total Calls: ${fmtNum(report.summary.totalCallCount)}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(chalk.bold(`=== TOP ${report.byFunction.length} CPU BY FUNCTION ===`));
|
|
36
|
+
lines.push(this.cpuFunctionTable(report.byFunction));
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
formatFullReport(report) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push(this.formatMemoryReport(report.memory));
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push(this.formatCpuReport(report.cpu));
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
formatSummaryReport(report) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push(this.formatHeader(report.header));
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push(chalk.bold('=== PARSE STATS ==='));
|
|
51
|
+
lines.push(` Entries: ${fmtNum(report.parseStats.entries)}`);
|
|
52
|
+
lines.push(` Strings: ${fmtNum(report.parseStats.strings)}`);
|
|
53
|
+
lines.push(` Modules: ${fmtNum(report.parseStats.modules)}`);
|
|
54
|
+
lines.push(` Path Elements: ${fmtNum(report.parseStats.pathElements)}`);
|
|
55
|
+
lines.push(` Errors: ${report.parseStats.errors}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(chalk.bold('=== MODULE OVERVIEW ==='));
|
|
58
|
+
lines.push(this.moduleTable(report.moduleOverview));
|
|
59
|
+
lines.push('');
|
|
60
|
+
if (report.topMemoryLeaks.length > 0) {
|
|
61
|
+
lines.push(chalk.bold(`=== TOP ${report.topMemoryLeaks.length} MEMORY LEAKS ===`));
|
|
62
|
+
lines.push(this.functionTable(report.topMemoryLeaks));
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
if (report.topCpuConsumers.length > 0) {
|
|
66
|
+
lines.push(chalk.bold(`=== TOP ${report.topCpuConsumers.length} CPU CONSUMERS ===`));
|
|
67
|
+
lines.push(this.cpuFunctionTable(report.topCpuConsumers));
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
formatDiffReport(report) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(chalk.bold('=== PROFILE COMPARISON ==='));
|
|
74
|
+
lines.push(` Before: ${report.before.file} (${report.before.targetVersion})`);
|
|
75
|
+
lines.push(` After: ${report.after.file} (${report.after.targetVersion})`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
const md = report.memoryDelta;
|
|
78
|
+
const deltaColor = md.totalRetainedDelta > 0 ? chalk.red : chalk.green;
|
|
79
|
+
lines.push(chalk.bold('=== MEMORY DELTA ==='));
|
|
80
|
+
lines.push(` Retained Before: ${fmtBytes(md.totalRetainedBefore)}`);
|
|
81
|
+
lines.push(` Retained After: ${fmtBytes(md.totalRetainedAfter)}`);
|
|
82
|
+
lines.push(` Delta: ${deltaColor(fmtBytes(md.totalRetainedDelta))}`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
if (md.regressions.length > 0) {
|
|
85
|
+
lines.push(chalk.bold.red(` Regressions (${md.regressions.length}):`));
|
|
86
|
+
lines.push(this.deltaTable(md.regressions));
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
if (md.improvements.length > 0) {
|
|
90
|
+
lines.push(chalk.bold.green(` Improvements (${md.improvements.length}):`));
|
|
91
|
+
lines.push(this.deltaTable(md.improvements));
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
if (md.newLeaks.length > 0) {
|
|
95
|
+
lines.push(chalk.bold.red(` New Leaks: ${md.newLeaks.join(', ')}`));
|
|
96
|
+
}
|
|
97
|
+
if (md.resolvedLeaks.length > 0) {
|
|
98
|
+
lines.push(chalk.bold.green(` Resolved Leaks: ${md.resolvedLeaks.join(', ')}`));
|
|
99
|
+
}
|
|
100
|
+
if (report.cpuDelta) {
|
|
101
|
+
const cd = report.cpuDelta;
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(chalk.bold('=== CPU DELTA ==='));
|
|
104
|
+
lines.push(` CPU Before: ${fmtTime(cd.totalCpuBefore)}`);
|
|
105
|
+
lines.push(` CPU After: ${fmtTime(cd.totalCpuAfter)}`);
|
|
106
|
+
const cpuDeltaColor = cd.totalCpuDelta > 0 ? chalk.red : chalk.green;
|
|
107
|
+
lines.push(` Delta: ${cpuDeltaColor(fmtTime(cd.totalCpuDelta))}`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
formatInfo(header, fileSize) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
lines.push(`Target: ${header.targetName} ${header.targetVersion}`);
|
|
114
|
+
lines.push(`Device: ${header.device} (${header.firmware})`);
|
|
115
|
+
lines.push(`Format: ${header.formatVersion}`);
|
|
116
|
+
if (header.duration > 0) {
|
|
117
|
+
lines.push(`Duration: ${fmtDuration(header.duration)}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push(`Size: ${fmtBytes(fileSize)}`);
|
|
120
|
+
lines.push(`Features: ${header.features.join(', ') || 'none'}`);
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
formatHeader(header) {
|
|
124
|
+
return chalk.bold(`=== ${header.targetName} ${header.targetVersion} ===`) + '\n' +
|
|
125
|
+
`Device: ${header.device} (${header.firmware}) | Format: ${header.formatVersion} | Size: ${fmtBytes(header.fileSize)}`;
|
|
126
|
+
}
|
|
127
|
+
moduleTable(modules) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
const hdr = `${padEnd('Module', 50)} ${pad('Alloc', 12)} ${pad('Free', 12)} ${pad('Retained', 12)} ${pad('Allocs#', 10)} ${pad('Frees#', 10)}`;
|
|
130
|
+
lines.push(hdr);
|
|
131
|
+
lines.push('-'.repeat(hdr.length));
|
|
132
|
+
for (const m of modules) {
|
|
133
|
+
const retained = m.retainedBytes;
|
|
134
|
+
const retStr = retained > 0 ? chalk.red(pad(fmtBytes(retained), 12)) : chalk.green(pad(fmtBytes(retained), 12));
|
|
135
|
+
lines.push(`${padEnd(m.module, 50)} ${pad(fmtBytes(m.allocBytes), 12)} ${pad(fmtBytes(m.freeBytes), 12)} ${retStr} ${pad(fmtNum(m.allocCount), 10)} ${pad(fmtNum(m.freeCount), 10)}`);
|
|
136
|
+
}
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
fileTable(files) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
const hdr = `${pad('#', 4)} ${pad('Retained', 12)} ${pad('Alloc', 12)} ${pad('Free', 12)} ${padEnd('Module', 30)} File`;
|
|
142
|
+
lines.push(hdr);
|
|
143
|
+
lines.push('-'.repeat(hdr.length + 40));
|
|
144
|
+
for (let i = 0; i < files.length; i++) {
|
|
145
|
+
const f = files[i];
|
|
146
|
+
const retStr = f.retainedBytes > 0 ? chalk.red(pad(fmtBytes(f.retainedBytes), 12)) : pad(fmtBytes(f.retainedBytes), 12);
|
|
147
|
+
lines.push(`${pad(i + 1, 4)} ${retStr} ${pad(fmtBytes(f.allocBytes), 12)} ${pad(fmtBytes(f.freeBytes), 12)} ${padEnd(f.module || '', 30)} ${f.file}`);
|
|
148
|
+
}
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
functionTable(funcs) {
|
|
152
|
+
const lines = [];
|
|
153
|
+
const hdr = `${pad('#', 4)} ${pad('Retained', 12)} ${pad('Alloc', 12)} ${pad('Free', 12)} ${pad('Ops', 8)} ${pad('Calls', 8)} ${pad('CPU Self', 10)} ${pad('Wall Self', 10)} Function / File`;
|
|
154
|
+
lines.push(hdr);
|
|
155
|
+
lines.push('-'.repeat(hdr.length + 30));
|
|
156
|
+
for (let i = 0; i < funcs.length; i++) {
|
|
157
|
+
const f = funcs[i];
|
|
158
|
+
const retStr = f.retainedBytes > 0 ? chalk.red(pad(fmtBytes(f.retainedBytes), 12)) : pad(fmtBytes(f.retainedBytes), 12);
|
|
159
|
+
lines.push(`${pad(i + 1, 4)} ${retStr} ${pad(fmtBytes(f.allocBytes), 12)} ${pad(fmtBytes(f.freeBytes), 12)} ${pad(fmtNum(f.allocCount), 8)} ${pad(f.callCount ? fmtNum(f.callCount) : '', 8)} ${pad(fmtTime(f.cpuSelf), 10)} ${pad(fmtTime(f.wallSelf), 10)} ${f.function} (${f.file})`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
cpuFunctionTable(funcs) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
const hdr = `${pad('#', 4)} ${pad('CPU Self', 12)} ${pad('Wall Self', 12)} ${pad('Calls', 10)} Function / File`;
|
|
166
|
+
lines.push(hdr);
|
|
167
|
+
lines.push('-'.repeat(hdr.length + 40));
|
|
168
|
+
for (let i = 0; i < funcs.length; i++) {
|
|
169
|
+
const f = funcs[i];
|
|
170
|
+
lines.push(`${pad(i + 1, 4)} ${pad(fmtTime(f.cpuSelf), 12)} ${pad(fmtTime(f.wallSelf), 12)} ${pad(f.callCount ? fmtNum(f.callCount) : '', 10)} ${f.function} (${f.file})`);
|
|
171
|
+
if (f.callStack && f.callStack.length > 1) {
|
|
172
|
+
for (let j = 1; j < Math.min(f.callStack.length, 6); j++) {
|
|
173
|
+
lines.push(`${' '.repeat(6)}${chalk.dim('← ' + f.callStack[j])}`);
|
|
174
|
+
}
|
|
175
|
+
if (f.callStack.length > 6) {
|
|
176
|
+
lines.push(`${' '.repeat(6)}${chalk.dim(`← ... (${f.callStack.length - 6} more frames)`)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
deltaTable(deltas) {
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const d of deltas) {
|
|
185
|
+
const sign = d.delta > 0 ? '+' : '';
|
|
186
|
+
const color = d.delta > 0 ? chalk.red : chalk.green;
|
|
187
|
+
lines.push(` ${color(sign + fmtBytes(d.delta))} ${d.function} (${d.file})`);
|
|
188
|
+
lines.push(` ${fmtBytes(d.retainedBefore)} → ${fmtBytes(d.retainedAfter)}`);
|
|
189
|
+
}
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=TextFormatter.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function fmtBytes(n: number): string;
|
|
2
|
+
export declare function fmtTime(us: number): string;
|
|
3
|
+
export declare function fmtDuration(ms: number): string;
|
|
4
|
+
export declare function fmtNum(n: number): string;
|
|
5
|
+
export declare function fmtDate(ms: bigint | number): string;
|
|
6
|
+
export declare function pad(s: string | number, n: number): string;
|
|
7
|
+
export declare function padEnd(s: string | number, n: number): string;
|
|
8
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function fmtBytes(n) {
|
|
2
|
+
if (n < 0)
|
|
3
|
+
return '-' + fmtBytes(-n);
|
|
4
|
+
if (n >= 1_048_576)
|
|
5
|
+
return (n / 1_048_576).toFixed(1) + ' MB';
|
|
6
|
+
if (n >= 1024)
|
|
7
|
+
return (n / 1024).toFixed(1) + ' KB';
|
|
8
|
+
return n + ' B';
|
|
9
|
+
}
|
|
10
|
+
export function fmtTime(us) {
|
|
11
|
+
if (us < 0)
|
|
12
|
+
return '-' + fmtTime(-us);
|
|
13
|
+
if (us >= 1_000_000)
|
|
14
|
+
return (us / 1_000_000).toFixed(1) + 's';
|
|
15
|
+
if (us >= 1000)
|
|
16
|
+
return (us / 1000).toFixed(1) + 'ms';
|
|
17
|
+
return us + 'us';
|
|
18
|
+
}
|
|
19
|
+
export function fmtDuration(ms) {
|
|
20
|
+
const s = Math.floor(ms / 1000);
|
|
21
|
+
if (s < 60)
|
|
22
|
+
return `${s}s`;
|
|
23
|
+
const m = Math.floor(s / 60);
|
|
24
|
+
const rs = s % 60;
|
|
25
|
+
if (m < 60)
|
|
26
|
+
return `${m}m ${rs}s`;
|
|
27
|
+
const h = Math.floor(m / 60);
|
|
28
|
+
const rm = m % 60;
|
|
29
|
+
return `${h}h ${rm}m ${rs}s`;
|
|
30
|
+
}
|
|
31
|
+
export function fmtNum(n) {
|
|
32
|
+
return n.toLocaleString('en-US');
|
|
33
|
+
}
|
|
34
|
+
export function fmtDate(ms) {
|
|
35
|
+
const d = new Date(Number(ms));
|
|
36
|
+
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
|
|
37
|
+
}
|
|
38
|
+
export function pad(s, n) {
|
|
39
|
+
return String(s).padStart(n);
|
|
40
|
+
}
|
|
41
|
+
export function padEnd(s, n) {
|
|
42
|
+
return String(s).padEnd(n);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=helpers.js.map
|
package/dist/index.d.ts
ADDED