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/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