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
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { BsprofParser } from './parser/BsprofParser.js';
|
|
5
|
+
import { analyzeMemory } from './analyzer/MemoryAnalyzer.js';
|
|
6
|
+
import { analyzeCpu } from './analyzer/CpuAnalyzer.js';
|
|
7
|
+
import { compareBsprof } from './analyzer/DiffAnalyzer.js';
|
|
8
|
+
import { buildHeaderInfo, buildParseStats } from './analyzer/common.js';
|
|
9
|
+
import { aggregate, rankFunctions } from './analyzer/Aggregator.js';
|
|
10
|
+
import { TextFormatter } from './formatter/TextFormatter.js';
|
|
11
|
+
import { JsonFormatter } from './formatter/JsonFormatter.js';
|
|
12
|
+
import { MarkdownFormatter } from './formatter/MarkdownFormatter.js';
|
|
13
|
+
import { ChromeTraceExporter } from './formatter/ChromeTraceExporter.js';
|
|
14
|
+
import { CsvExporter } from './formatter/CsvExporter.js';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('bsprof')
|
|
18
|
+
.description('CLI tool for parsing and analyzing BrightScript Profiler (.bsprof) files')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
// --- analyze command ---
|
|
21
|
+
program
|
|
22
|
+
.command('analyze')
|
|
23
|
+
.description('Analyze a .bsprof file')
|
|
24
|
+
.argument('<mode>', 'Analysis mode: memory, cpu, full, summary')
|
|
25
|
+
.argument('<file>', 'Path to .bsprof file')
|
|
26
|
+
.option('--format <fmt>', 'Output format: text, json, markdown', 'text')
|
|
27
|
+
.option('--top <n>', 'Number of entries in ranked lists', '30')
|
|
28
|
+
.option('--sort <field>', 'Sort field (mode-dependent)')
|
|
29
|
+
.option('--filter-module <name>', 'Filter results to a specific module/thread')
|
|
30
|
+
.option('--filter-file <glob>', 'Filter results to files matching glob')
|
|
31
|
+
.option('--exclude-module <name>', 'Exclude a module')
|
|
32
|
+
.option('--threshold <bytes>', 'Only show entries exceeding threshold', '0')
|
|
33
|
+
.option('--output <path>', 'Write output to file instead of stdout')
|
|
34
|
+
.action((mode, file, opts) => {
|
|
35
|
+
const analysisMode = mode;
|
|
36
|
+
if (!['memory', 'cpu', 'full', 'summary'].includes(analysisMode)) {
|
|
37
|
+
console.error(`Unknown mode: ${mode}. Use: memory, cpu, full, summary`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const profile = parseFile(file);
|
|
41
|
+
const options = {
|
|
42
|
+
top: parseInt(opts.top, 10),
|
|
43
|
+
sortBy: opts.sort,
|
|
44
|
+
filterModule: opts.filterModule,
|
|
45
|
+
filterFile: opts.filterFile,
|
|
46
|
+
excludeModule: opts.excludeModule,
|
|
47
|
+
threshold: parseInt(opts.threshold, 10),
|
|
48
|
+
};
|
|
49
|
+
const format = opts.format;
|
|
50
|
+
let output = '';
|
|
51
|
+
switch (analysisMode) {
|
|
52
|
+
case 'memory': {
|
|
53
|
+
const report = analyzeMemory(profile, options);
|
|
54
|
+
output = formatOutput(format, 'memory', report);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'cpu': {
|
|
58
|
+
const report = analyzeCpu(profile, options);
|
|
59
|
+
output = formatOutput(format, 'cpu', report);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'full': {
|
|
63
|
+
const memReport = analyzeMemory(profile, options);
|
|
64
|
+
const cpuReport = analyzeCpu(profile, options);
|
|
65
|
+
const fullReport = {
|
|
66
|
+
header: memReport.header,
|
|
67
|
+
memory: memReport,
|
|
68
|
+
cpu: cpuReport,
|
|
69
|
+
};
|
|
70
|
+
output = formatOutput(format, 'full', fullReport);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'summary': {
|
|
74
|
+
const top = options.top ?? 10;
|
|
75
|
+
const agg = aggregate(profile, options);
|
|
76
|
+
const allFuncs = [...agg.byFunction.values()];
|
|
77
|
+
const summaryReport = {
|
|
78
|
+
header: buildHeaderInfo(profile),
|
|
79
|
+
parseStats: buildParseStats(profile),
|
|
80
|
+
topMemoryLeaks: rankFunctions(allFuncs, 'retained', top),
|
|
81
|
+
topCpuConsumers: rankFunctions(allFuncs.filter(f => f.cpuSelf > 0), 'cpuSelf', top),
|
|
82
|
+
moduleOverview: [...agg.byModule.values()].sort((a, b) => b.retainedBytes - a.retainedBytes),
|
|
83
|
+
};
|
|
84
|
+
output = formatOutput(format, 'summary', summaryReport);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
writeOutput(output, opts.output);
|
|
89
|
+
});
|
|
90
|
+
// --- compare command ---
|
|
91
|
+
program
|
|
92
|
+
.command('compare')
|
|
93
|
+
.description('Compare two .bsprof files to detect regressions')
|
|
94
|
+
.argument('<file1>', 'Before profile')
|
|
95
|
+
.argument('<file2>', 'After profile')
|
|
96
|
+
.option('--format <fmt>', 'Output format: text, json, markdown', 'text')
|
|
97
|
+
.option('--top <n>', 'Number of entries in ranked lists', '30')
|
|
98
|
+
.option('--filter-module <name>', 'Filter results to a specific module/thread')
|
|
99
|
+
.option('--exclude-module <name>', 'Exclude a module')
|
|
100
|
+
.option('--output <path>', 'Write output to file instead of stdout')
|
|
101
|
+
.action((file1, file2, opts) => {
|
|
102
|
+
const before = parseFile(file1);
|
|
103
|
+
const after = parseFile(file2);
|
|
104
|
+
const options = {
|
|
105
|
+
top: parseInt(opts.top, 10),
|
|
106
|
+
filterModule: opts.filterModule,
|
|
107
|
+
excludeModule: opts.excludeModule,
|
|
108
|
+
};
|
|
109
|
+
const report = compareBsprof(before, after, file1, file2, options);
|
|
110
|
+
const format = opts.format;
|
|
111
|
+
const formatter = getFormatter(format);
|
|
112
|
+
const output = formatter.formatDiffReport(report);
|
|
113
|
+
writeOutput(output, opts.output);
|
|
114
|
+
});
|
|
115
|
+
// --- info command ---
|
|
116
|
+
program
|
|
117
|
+
.command('info')
|
|
118
|
+
.description('Print header metadata from a .bsprof file')
|
|
119
|
+
.argument('<file>', 'Path to .bsprof file')
|
|
120
|
+
.option('--format <fmt>', 'Output format: text, json, markdown', 'text')
|
|
121
|
+
.action((file, opts) => {
|
|
122
|
+
const buf = readFileSync(file);
|
|
123
|
+
const parser = new BsprofParser(buf);
|
|
124
|
+
const { header, fileSize } = parser.parseHeaderOnly();
|
|
125
|
+
// Minimal info — we need a full parse for duration but we can show what we have
|
|
126
|
+
const headerInfo = buildHeaderInfo({
|
|
127
|
+
header,
|
|
128
|
+
strings: new Map(),
|
|
129
|
+
modules: new Map(),
|
|
130
|
+
pathElements: new Map(),
|
|
131
|
+
parseResult: { count: 0, errors: 0, memoryByPE: new Map(), cpuByPE: new Map() },
|
|
132
|
+
fileSize,
|
|
133
|
+
});
|
|
134
|
+
const format = opts.format;
|
|
135
|
+
const formatter = getFormatter(format);
|
|
136
|
+
const output = formatter.formatInfo(headerInfo, fileSize);
|
|
137
|
+
console.log(output);
|
|
138
|
+
});
|
|
139
|
+
// --- export command ---
|
|
140
|
+
program
|
|
141
|
+
.command('export')
|
|
142
|
+
.description('Export parsed data for external tools')
|
|
143
|
+
.argument('<file>', 'Path to .bsprof file')
|
|
144
|
+
.option('--format <fmt>', 'Export format: chrome-trace, csv, json', 'json')
|
|
145
|
+
.option('--filter-module <name>', 'Filter results to a specific module/thread')
|
|
146
|
+
.option('--exclude-module <name>', 'Exclude a module')
|
|
147
|
+
.option('--output <path>', 'Write output to file instead of stdout')
|
|
148
|
+
.action((file, opts) => {
|
|
149
|
+
const profile = parseFile(file);
|
|
150
|
+
const exportFormat = opts.format;
|
|
151
|
+
const options = {
|
|
152
|
+
filterModule: opts.filterModule,
|
|
153
|
+
excludeModule: opts.excludeModule,
|
|
154
|
+
};
|
|
155
|
+
let output;
|
|
156
|
+
switch (exportFormat) {
|
|
157
|
+
case 'chrome-trace': {
|
|
158
|
+
const exporter = new ChromeTraceExporter();
|
|
159
|
+
output = exporter.export(profile);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'csv': {
|
|
163
|
+
const exporter = new CsvExporter();
|
|
164
|
+
output = exporter.export(profile, options);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'json': {
|
|
168
|
+
const memReport = analyzeMemory(profile, { ...options, top: Infinity });
|
|
169
|
+
output = JSON.stringify(memReport, null, 2);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
default: {
|
|
173
|
+
console.error(`Unknown export format: ${exportFormat}. Use: chrome-trace, csv, json`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
writeOutput(output, opts.output);
|
|
178
|
+
});
|
|
179
|
+
// --- helpers ---
|
|
180
|
+
function parseFile(filePath) {
|
|
181
|
+
try {
|
|
182
|
+
const buf = readFileSync(filePath);
|
|
183
|
+
const parser = new BsprofParser(buf);
|
|
184
|
+
return parser.parse();
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
console.error(`Error parsing ${filePath}: ${msg}`);
|
|
189
|
+
return process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function getFormatter(format) {
|
|
193
|
+
switch (format) {
|
|
194
|
+
case 'json': return new JsonFormatter();
|
|
195
|
+
case 'markdown': return new MarkdownFormatter();
|
|
196
|
+
case 'text':
|
|
197
|
+
default: return new TextFormatter();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function formatOutput(format, mode, report) {
|
|
201
|
+
const formatter = getFormatter(format);
|
|
202
|
+
switch (mode) {
|
|
203
|
+
case 'memory': return formatter.formatMemoryReport(report);
|
|
204
|
+
case 'cpu': return formatter.formatCpuReport(report);
|
|
205
|
+
case 'full': return formatter.formatFullReport(report);
|
|
206
|
+
case 'summary': return formatter.formatSummaryReport(report);
|
|
207
|
+
default: return JSON.stringify(report, null, 2);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function writeOutput(output, filePath) {
|
|
211
|
+
if (filePath) {
|
|
212
|
+
writeFileSync(filePath, output, 'utf-8');
|
|
213
|
+
console.error(`Output written to ${filePath}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.log(output);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
program.parse();
|
|
220
|
+
//# sourceMappingURL=index.js.map
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { BsprofParser } from './parser/BsprofParser.js';
|
|
2
|
+
export { BufferReader } from './parser/varint.js';
|
|
3
|
+
export { analyzeMemory } from './analyzer/MemoryAnalyzer.js';
|
|
4
|
+
export { analyzeCpu } from './analyzer/CpuAnalyzer.js';
|
|
5
|
+
export { compareBsprof } from './analyzer/DiffAnalyzer.js';
|
|
6
|
+
export { aggregate, rankFunctions } from './analyzer/Aggregator.js';
|
|
7
|
+
export { buildHeaderInfo, buildParseStats } from './analyzer/common.js';
|
|
8
|
+
export { TextFormatter } from './formatter/TextFormatter.js';
|
|
9
|
+
export { JsonFormatter } from './formatter/JsonFormatter.js';
|
|
10
|
+
export { MarkdownFormatter } from './formatter/MarkdownFormatter.js';
|
|
11
|
+
export { ChromeTraceExporter } from './formatter/ChromeTraceExporter.js';
|
|
12
|
+
export { CsvExporter } from './formatter/CsvExporter.js';
|
|
13
|
+
export type { BsprofHeader, PathElement, ModuleEntry, MemoryRecord, CpuRecord, ParseResult, ParsedProfile, AnalysisOptions, MemorySortField, CpuSortField, ModuleStats, FileStats, FunctionStats, HeaderInfo, ParseStats, MemorySummary, MemoryReport, CpuSummary, CpuReport, FullReport, SummaryReport, FunctionDelta, DiffReport, ChromeTraceEvent, OutputFormat, ExportFormat, AnalysisMode, } from './parser/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Convenience: parse a Buffer and return the full profile.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { readFileSync } from 'fs';
|
|
20
|
+
* import { parseBsprof, analyzeMemory } from 'bsprof-cli';
|
|
21
|
+
*
|
|
22
|
+
* const profile = parseBsprof(readFileSync('profile.bsprof'));
|
|
23
|
+
* const report = analyzeMemory(profile, { top: 20 });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseBsprof(buffer: Buffer): import("./parser/types.js").ParsedProfile;
|
|
27
|
+
//# sourceMappingURL=lib.d.ts.map
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BsprofParser } from './parser/BsprofParser.js';
|
|
2
|
+
export { BsprofParser } from './parser/BsprofParser.js';
|
|
3
|
+
export { BufferReader } from './parser/varint.js';
|
|
4
|
+
export { analyzeMemory } from './analyzer/MemoryAnalyzer.js';
|
|
5
|
+
export { analyzeCpu } from './analyzer/CpuAnalyzer.js';
|
|
6
|
+
export { compareBsprof } from './analyzer/DiffAnalyzer.js';
|
|
7
|
+
export { aggregate, rankFunctions } from './analyzer/Aggregator.js';
|
|
8
|
+
export { buildHeaderInfo, buildParseStats } from './analyzer/common.js';
|
|
9
|
+
export { TextFormatter } from './formatter/TextFormatter.js';
|
|
10
|
+
export { JsonFormatter } from './formatter/JsonFormatter.js';
|
|
11
|
+
export { MarkdownFormatter } from './formatter/MarkdownFormatter.js';
|
|
12
|
+
export { ChromeTraceExporter } from './formatter/ChromeTraceExporter.js';
|
|
13
|
+
export { CsvExporter } from './formatter/CsvExporter.js';
|
|
14
|
+
/**
|
|
15
|
+
* Convenience: parse a Buffer and return the full profile.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { readFileSync } from 'fs';
|
|
20
|
+
* import { parseBsprof, analyzeMemory } from 'bsprof-cli';
|
|
21
|
+
*
|
|
22
|
+
* const profile = parseBsprof(readFileSync('profile.bsprof'));
|
|
23
|
+
* const report = analyzeMemory(profile, { top: 20 });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function parseBsprof(buffer) {
|
|
27
|
+
const parser = new BsprofParser(buffer);
|
|
28
|
+
return parser.parse();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=lib.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BsprofHeader, ParsedProfile } from './types.js';
|
|
2
|
+
export declare class BsprofParser {
|
|
3
|
+
private buffer;
|
|
4
|
+
private reader;
|
|
5
|
+
private strings;
|
|
6
|
+
private modules;
|
|
7
|
+
private pathElements;
|
|
8
|
+
private header;
|
|
9
|
+
private allocSizes;
|
|
10
|
+
constructor(buffer: Buffer);
|
|
11
|
+
parse(): ParsedProfile;
|
|
12
|
+
/** Parse only the header — used by the `info` command for fast metadata reads */
|
|
13
|
+
parseHeaderOnly(): {
|
|
14
|
+
header: BsprofHeader;
|
|
15
|
+
fileSize: number;
|
|
16
|
+
};
|
|
17
|
+
private parseHeader;
|
|
18
|
+
private parseBody;
|
|
19
|
+
private readFooterTimestamp;
|
|
20
|
+
static resolveString(profile: ParsedProfile, id: number): string;
|
|
21
|
+
static resolveModuleName(profile: ParsedProfile, peId: number, depth?: number): string;
|
|
22
|
+
static resolveFileName(profile: ParsedProfile, peId: number): string;
|
|
23
|
+
static resolveFuncName(profile: ParsedProfile, peId: number): string;
|
|
24
|
+
/** Walk the path element chain to reconstruct the full call stack */
|
|
25
|
+
static resolveCallStack(profile: ParsedProfile, peId: number, maxDepth?: number): string[];
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=BsprofParser.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { BufferReader } from './varint.js';
|
|
2
|
+
const MAGIC = 'bsprof';
|
|
3
|
+
export class BsprofParser {
|
|
4
|
+
buffer;
|
|
5
|
+
reader;
|
|
6
|
+
strings = new Map();
|
|
7
|
+
modules = new Map();
|
|
8
|
+
pathElements = new Map();
|
|
9
|
+
header;
|
|
10
|
+
allocSizes = new Map();
|
|
11
|
+
constructor(buffer) {
|
|
12
|
+
this.buffer = buffer;
|
|
13
|
+
this.reader = new BufferReader(buffer);
|
|
14
|
+
}
|
|
15
|
+
parse() {
|
|
16
|
+
this.parseHeader();
|
|
17
|
+
const parseResult = this.parseBody();
|
|
18
|
+
const timestampEnd = this.readFooterTimestamp();
|
|
19
|
+
return {
|
|
20
|
+
header: this.header,
|
|
21
|
+
strings: this.strings,
|
|
22
|
+
modules: this.modules,
|
|
23
|
+
pathElements: this.pathElements,
|
|
24
|
+
parseResult,
|
|
25
|
+
timestampEnd,
|
|
26
|
+
fileSize: this.buffer.length,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Parse only the header — used by the `info` command for fast metadata reads */
|
|
30
|
+
parseHeaderOnly() {
|
|
31
|
+
this.parseHeader();
|
|
32
|
+
return { header: this.header, fileSize: this.buffer.length };
|
|
33
|
+
}
|
|
34
|
+
parseHeader() {
|
|
35
|
+
const magic = this.reader.readAscii(6);
|
|
36
|
+
if (magic !== MAGIC) {
|
|
37
|
+
throw new Error(`Not a .bsprof file (magic: "${magic}", expected "${MAGIC}")`);
|
|
38
|
+
}
|
|
39
|
+
this.reader.pos = 8; // skip 2 padding bytes after magic
|
|
40
|
+
this.header = {
|
|
41
|
+
majorVersion: this.reader.vi(),
|
|
42
|
+
minorVersion: this.reader.vi(),
|
|
43
|
+
patchLevel: this.reader.vi(),
|
|
44
|
+
headerSize: this.reader.vi(),
|
|
45
|
+
requestedSampleRatio: this.reader.readFloat32LE(),
|
|
46
|
+
actualSampleRatio: this.reader.readFloat32LE(),
|
|
47
|
+
lineSpecificData: this.reader.vi() !== 0,
|
|
48
|
+
memoryOperations: this.reader.vi() !== 0,
|
|
49
|
+
timestampStart: this.reader.vi64(),
|
|
50
|
+
targetName: this.reader.readUtf8z(),
|
|
51
|
+
supplementalInfo: this.reader.readUtf8z(),
|
|
52
|
+
targetVersion: this.reader.readUtf8z(),
|
|
53
|
+
deviceVendor: this.reader.readUtf8z(),
|
|
54
|
+
deviceModel: this.reader.readUtf8z(),
|
|
55
|
+
deviceFirmware: this.reader.readUtf8z(),
|
|
56
|
+
};
|
|
57
|
+
this.reader.pos = this.header.headerSize;
|
|
58
|
+
}
|
|
59
|
+
parseBody() {
|
|
60
|
+
const hasLine = this.header.lineSpecificData;
|
|
61
|
+
let count = 0;
|
|
62
|
+
let errors = 0;
|
|
63
|
+
const len = this.reader.length;
|
|
64
|
+
const memoryByPE = new Map();
|
|
65
|
+
const cpuByPE = new Map();
|
|
66
|
+
while (this.reader.pos < len) {
|
|
67
|
+
const startPos = this.reader.pos;
|
|
68
|
+
try {
|
|
69
|
+
const tag = this.reader.readVarint();
|
|
70
|
+
if (tag === 0n)
|
|
71
|
+
break;
|
|
72
|
+
const type = Number(tag & 0x7n);
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 0: { // String table entry
|
|
75
|
+
const strId = Number(tag >> 3n);
|
|
76
|
+
const str = this.reader.readUtf8z();
|
|
77
|
+
if (!this.strings.has(strId)) {
|
|
78
|
+
this.strings.set(strId, str);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 1: { // Executable module entry
|
|
83
|
+
const modId = Number(tag >> 3n);
|
|
84
|
+
const nameStrId = this.reader.vi();
|
|
85
|
+
if (!this.modules.has(modId)) {
|
|
86
|
+
this.modules.set(modId, { nameStrId });
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 2: { // Path element entry
|
|
91
|
+
const peId = Number(tag >> 3n);
|
|
92
|
+
const callingId = this.reader.vi();
|
|
93
|
+
if (callingId === 0) {
|
|
94
|
+
// Root path element
|
|
95
|
+
const modId = this.reader.vi();
|
|
96
|
+
const fileStrId = this.reader.vi();
|
|
97
|
+
const lineNumber = this.reader.vi();
|
|
98
|
+
const funcStrId = this.reader.vi();
|
|
99
|
+
this.pathElements.set(peId, {
|
|
100
|
+
callingId: 0, modId, fileStrId, funcStrId, lineNumber, root: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Non-root (chained) path element
|
|
105
|
+
const lineOff = hasLine ? this.reader.vi() : 0;
|
|
106
|
+
const fileStrId = this.reader.vi();
|
|
107
|
+
const lineNumber = this.reader.vi();
|
|
108
|
+
const funcStrId = this.reader.vi();
|
|
109
|
+
this.pathElements.set(peId, {
|
|
110
|
+
callingId, lineOff, fileStrId, funcStrId, lineNumber, root: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 3: { // Memory operation entry
|
|
116
|
+
const opType = Number((tag >> 3n) & 0x3n);
|
|
117
|
+
const peId = Number(tag >> 5n);
|
|
118
|
+
if (hasLine)
|
|
119
|
+
this.reader.vi(); // line offset (consumed but unused here)
|
|
120
|
+
const addr = this.reader.vi64();
|
|
121
|
+
let size = 0;
|
|
122
|
+
if (opType === 0) {
|
|
123
|
+
size = this.reader.vi();
|
|
124
|
+
}
|
|
125
|
+
let rec = memoryByPE.get(peId);
|
|
126
|
+
if (!rec) {
|
|
127
|
+
rec = { allocCount: 0, freeCount: 0, allocBytes: 0, freeBytes: 0 };
|
|
128
|
+
memoryByPE.set(peId, rec);
|
|
129
|
+
}
|
|
130
|
+
if (opType === 0) { // alloc
|
|
131
|
+
rec.allocCount++;
|
|
132
|
+
rec.allocBytes += size;
|
|
133
|
+
this.allocSizes.set(addr, size);
|
|
134
|
+
}
|
|
135
|
+
else { // free or free_realloc
|
|
136
|
+
rec.freeCount++;
|
|
137
|
+
const origSize = this.allocSizes.get(addr) || 0;
|
|
138
|
+
rec.freeBytes += origSize;
|
|
139
|
+
this.allocSizes.delete(addr);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 4: { // CPU measurement entry
|
|
144
|
+
const peId = Number(tag >> 3n);
|
|
145
|
+
if (hasLine)
|
|
146
|
+
this.reader.vi(); // line offset
|
|
147
|
+
const cpuCycles = this.reader.vi();
|
|
148
|
+
const wallTime = this.reader.vi();
|
|
149
|
+
let rec = cpuByPE.get(peId);
|
|
150
|
+
if (!rec) {
|
|
151
|
+
rec = { cpuSelf: 0, wallSelf: 0 };
|
|
152
|
+
cpuByPE.set(peId, rec);
|
|
153
|
+
}
|
|
154
|
+
rec.cpuSelf += cpuCycles;
|
|
155
|
+
rec.wallSelf += wallTime;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 5: { // Path call count entry
|
|
159
|
+
const peId = Number(tag >> 3n);
|
|
160
|
+
const callCount = this.reader.vi();
|
|
161
|
+
let rec = cpuByPE.get(peId);
|
|
162
|
+
if (!rec) {
|
|
163
|
+
rec = { cpuSelf: 0, wallSelf: 0 };
|
|
164
|
+
cpuByPE.set(peId, rec);
|
|
165
|
+
}
|
|
166
|
+
rec.callCount = (rec.callCount || 0) + callCount;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 6: // reserved
|
|
170
|
+
case 7: // reserved
|
|
171
|
+
break;
|
|
172
|
+
default: {
|
|
173
|
+
errors++;
|
|
174
|
+
if (errors > 1000) {
|
|
175
|
+
return { count, memoryByPE, cpuByPE, errors };
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
count++;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
errors++;
|
|
184
|
+
this.reader.pos = startPos + 1;
|
|
185
|
+
if (errors > 1000) {
|
|
186
|
+
return { count, memoryByPE, cpuByPE, errors };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { count, memoryByPE, cpuByPE, errors };
|
|
191
|
+
}
|
|
192
|
+
readFooterTimestamp() {
|
|
193
|
+
try {
|
|
194
|
+
if (this.reader.remaining >= 1) {
|
|
195
|
+
return this.reader.vi64();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Footer may not always be present
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
// --- Lookup helpers exposed on the profile ---
|
|
204
|
+
static resolveString(profile, id) {
|
|
205
|
+
const s = profile.strings.get(id);
|
|
206
|
+
return s !== undefined ? s : `<str:${id}>`;
|
|
207
|
+
}
|
|
208
|
+
static resolveModuleName(profile, peId, depth = 0) {
|
|
209
|
+
if (depth > 60)
|
|
210
|
+
return '?';
|
|
211
|
+
const pe = profile.pathElements.get(peId);
|
|
212
|
+
if (!pe)
|
|
213
|
+
return '?';
|
|
214
|
+
if (pe.root) {
|
|
215
|
+
const mod = profile.modules.get(pe.modId);
|
|
216
|
+
if (!mod)
|
|
217
|
+
return `Module ${pe.modId}`;
|
|
218
|
+
const name = profile.strings.get(mod.nameStrId);
|
|
219
|
+
return (name !== undefined && name !== '') ? name : `Thread ${pe.modId}`;
|
|
220
|
+
}
|
|
221
|
+
return BsprofParser.resolveModuleName(profile, pe.callingId, depth + 1);
|
|
222
|
+
}
|
|
223
|
+
static resolveFileName(profile, peId) {
|
|
224
|
+
const pe = profile.pathElements.get(peId);
|
|
225
|
+
return pe ? BsprofParser.resolveString(profile, pe.fileStrId) : '?';
|
|
226
|
+
}
|
|
227
|
+
static resolveFuncName(profile, peId) {
|
|
228
|
+
const pe = profile.pathElements.get(peId);
|
|
229
|
+
if (!pe)
|
|
230
|
+
return '?';
|
|
231
|
+
return BsprofParser.resolveString(profile, pe.funcStrId);
|
|
232
|
+
}
|
|
233
|
+
/** Walk the path element chain to reconstruct the full call stack */
|
|
234
|
+
static resolveCallStack(profile, peId, maxDepth = 60) {
|
|
235
|
+
const stack = [];
|
|
236
|
+
let current = peId;
|
|
237
|
+
let depth = 0;
|
|
238
|
+
while (current > 0 && depth < maxDepth) {
|
|
239
|
+
const pe = profile.pathElements.get(current);
|
|
240
|
+
if (!pe)
|
|
241
|
+
break;
|
|
242
|
+
const func = BsprofParser.resolveString(profile, pe.funcStrId);
|
|
243
|
+
const file = BsprofParser.resolveString(profile, pe.fileStrId);
|
|
244
|
+
stack.push(`${func} (${file}:${pe.lineNumber})`);
|
|
245
|
+
current = pe.callingId;
|
|
246
|
+
depth++;
|
|
247
|
+
}
|
|
248
|
+
return stack;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=BsprofParser.js.map
|