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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Carlos Joya Carvajal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # bsprof-cli
2
+
3
+ CLI tool for parsing and analyzing BrightScript Profiler (`.bsprof`) files from Roku devices.
4
+
5
+ Supports memory leak detection, CPU hot-path analysis, profile comparison, and multiple export formats including Chrome DevTools traces.
6
+
7
+ ## Getting a .bsprof File
8
+
9
+ To generate a profiling file from your Roku device:
10
+
11
+ 1. **Enable the profiler** in your app's `manifest` file:
12
+ ```
13
+ bs_prof_enabled=true
14
+ ```
15
+ 2. **Sideload and run** your app on the Roku device.
16
+ 3. **Download the profile** via the developer console at `http://<roku-ip>:8080` under the Profiler section, or retrieve it programmatically from the device's profiling endpoint.
17
+
18
+ The resulting `.bsprof` file contains binary-encoded CPU and memory profiling data that this tool decodes and analyzes.
19
+
20
+ For full details on the file format, see the [Roku BrightScript Profiler specification](https://developer.roku.com/docs/developer-program/dev-tools/brs-profiler-file-format.md).
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install -g bsprof-cli
26
+
27
+ # or run directly
28
+ npx bsprof-cli analyze memory ./profile.bsprof
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `bsprof analyze <mode> <file>`
34
+
35
+ Analyze a `.bsprof` file. Modes:
36
+
37
+ | Mode | Description |
38
+ |------|-------------|
39
+ | `memory` | Memory allocation analysis -- retained bytes, leak detection |
40
+ | `cpu` | CPU time analysis -- self time, wall time, hot functions |
41
+ | `full` | Combined memory + CPU report |
42
+ | `summary` | One-page overview with key metrics |
43
+
44
+ ```bash
45
+ # Memory analysis with top 20 leaks
46
+ bsprof analyze memory profile.bsprof --top 20
47
+
48
+ # CPU analysis sorted by wall time, JSON output
49
+ bsprof analyze cpu profile.bsprof --sort wallSelf --format json
50
+
51
+ # Full report filtered to a specific module
52
+ bsprof analyze full profile.bsprof --filter-module "My App"
53
+
54
+ # Summary written to a file
55
+ bsprof analyze summary profile.bsprof --output report.txt
56
+ ```
57
+
58
+ #### Options
59
+
60
+ | Flag | Default | Description |
61
+ |------|---------|-------------|
62
+ | `--format <fmt>` | `text` | Output format: `text`, `json`, `markdown` |
63
+ | `--top <n>` | `30` | Number of entries in ranked lists |
64
+ | `--sort <field>` | varies | Sort field: `retained`, `allocated`, `allocCount`, `cpuSelf`, `wallSelf`, `callCount` |
65
+ | `--filter-module <name>` | -- | Filter results to a specific module/thread |
66
+ | `--filter-file <glob>` | -- | Filter results to files matching glob |
67
+ | `--exclude-module <name>` | -- | Exclude a module (e.g. `roku_ads_lib`) |
68
+ | `--threshold <bytes>` | `0` | Only show entries exceeding threshold |
69
+ | `--output <path>` | stdout | Write output to file |
70
+
71
+ ### `bsprof compare <file1> <file2>`
72
+
73
+ Diff two profiles to detect regressions or verify fixes.
74
+
75
+ ```bash
76
+ bsprof compare before.bsprof after.bsprof
77
+ bsprof compare before.bsprof after.bsprof --format json --output diff.json
78
+ ```
79
+
80
+ Output includes:
81
+ - Delta in retained bytes per function (positive = regression, negative = improvement)
82
+ - New leak sources (functions that appear only in the "after" profile)
83
+ - Resolved leaks (functions in "before" but not "after")
84
+ - CPU time deltas
85
+
86
+ ### `bsprof info <file>`
87
+
88
+ Print header metadata without full parsing.
89
+
90
+ ```bash
91
+ bsprof info profile.bsprof
92
+ ```
93
+
94
+ ```
95
+ Target: My App v2.0.1
96
+ Device: Roku Ultra (14.0.0)
97
+ Format: v3.0.0
98
+ Size: 42.3 MB
99
+ Features: line-specific-data, memory-operations
100
+ ```
101
+
102
+ ### `bsprof export <file>`
103
+
104
+ Export parsed data for external tools.
105
+
106
+ ```bash
107
+ # Chrome DevTools trace (open in chrome://tracing or Perfetto)
108
+ bsprof export profile.bsprof --format chrome-trace --output trace.json
109
+
110
+ # CSV for spreadsheets
111
+ bsprof export profile.bsprof --format csv --output data.csv
112
+
113
+ # Full JSON
114
+ bsprof export profile.bsprof --format json --output full.json
115
+ ```
116
+
117
+ | Format | Description |
118
+ |--------|-------------|
119
+ | `chrome-trace` | Chrome DevTools trace (viewable in `chrome://tracing` or Perfetto) |
120
+ | `csv` | Flat CSV for spreadsheet analysis |
121
+ | `json` | Structured JSON with full analysis data |
122
+
123
+ ## Programmatic API
124
+
125
+ The package also exports a library for use in other tools (e.g. MCP servers):
126
+
127
+ ```typescript
128
+ import { readFileSync } from 'fs';
129
+ import { parseBsprof, analyzeMemory, analyzeCpu, compareBsprof } from 'bsprof-cli';
130
+
131
+ // Parse raw binary
132
+ const profile = parseBsprof(readFileSync('profile.bsprof'));
133
+
134
+ // Memory analysis
135
+ const memoryReport = analyzeMemory(profile, { top: 20, sortBy: 'retained' });
136
+
137
+ // CPU analysis
138
+ const cpuReport = analyzeCpu(profile, { top: 20, sortBy: 'cpuSelf' });
139
+
140
+ // Compare two profiles
141
+ const before = parseBsprof(readFileSync('before.bsprof'));
142
+ const after = parseBsprof(readFileSync('after.bsprof'));
143
+ const diff = compareBsprof(before, after, 'before.bsprof', 'after.bsprof');
144
+ ```
145
+
146
+ ## Example Output
147
+
148
+ ### Memory analysis (`bsprof analyze memory profile.bsprof --top 5`)
149
+
150
+ ```
151
+ === My Roku App v2.0.1 ===
152
+ Device: Roku Ultra (14.0.0) | Format: v3.0.0 | Size: 42.3 MB
153
+
154
+ === MEMORY SUMMARY ===
155
+ Total Allocated: 93.7 MB
156
+ Total Freed: 86.9 MB
157
+ Total Retained: 6.8 MB
158
+ Alloc Count: 1,240,000
159
+ Free Count: 1,180,000
160
+
161
+ === MEMORY BY MODULE/THREAD ===
162
+ Module Alloc Free Retained Allocs# Frees#
163
+ ------------------------------------------------------------------------------------------------------------------------
164
+ My Roku App 50.0 MB 43.7 MB 6.3 MB 800,000 750,000
165
+ roku_ads_lib 43.7 MB 43.2 MB 524.3 KB 440,000 430,000
166
+
167
+ === TOP 5 MEMORY BY FUNCTION (retained bytes) ===
168
+ # Retained Alloc Free Ops Calls CPU Self Wall Self Function / File
169
+ -------------------------------------------------------------------------------------------
170
+ 1 6.6 MB 6.9 MB 307.2 KB 14,000 14,000 0us 0us analytics_track (pkg:/source/analytics.brs)
171
+ 2 204.8 KB 819.2 KB 614.4 KB 2,000 2,000 0us 0us ui_render_list (pkg:/source/ui/list.brs)
172
+ 3 102.4 KB 512.0 KB 409.6 KB 1,500 1,500 0us 0us network_fetch (pkg:/source/net/http.brs)
173
+ 4 51.2 KB 256.0 KB 204.8 KB 800 800 0us 0us parse_response (pkg:/source/net/parser.brs)
174
+ 5 25.6 KB 128.0 KB 102.4 KB 400 400 0us 0us cache_store (pkg:/source/cache.brs)
175
+ ```
176
+
177
+ ### CPU analysis (`bsprof analyze cpu profile.bsprof --top 3`)
178
+
179
+ ```
180
+ === TOP 3 CPU BY FUNCTION ===
181
+ # CPU Self Wall Self Calls Function / File
182
+ ----------------------------------------------------------------
183
+ 1 1.2s 3.4s 14,000 analytics_track (pkg:/source/analytics.brs)
184
+ ← init_analytics (pkg:/source/main.brs:42)
185
+ ← main (pkg:/source/main.brs:10)
186
+ 2 800.0ms 2.1s 2,000 ui_render_list (pkg:/source/ui/list.brs)
187
+ ← on_focus (pkg:/source/ui/screen.brs:88)
188
+ 3 450.0ms 1.8s 1,500 network_fetch (pkg:/source/net/http.brs)
189
+ ```
190
+
191
+ CPU output includes reconstructed call stacks showing the caller chain for each hot function.
192
+
193
+ ## Architecture
194
+
195
+ ```
196
+ src/
197
+ ├── parser/
198
+ │ ├── types.ts # Shared interfaces (ParsedProfile, reports, etc.)
199
+ │ ├── varint.ts # LEB128 varint decoder / buffer reader
200
+ │ └── BsprofParser.ts # Core binary parser for the .bsprof format
201
+ ├── analyzer/
202
+ │ ├── common.ts # Header info / parse stats builders
203
+ │ ├── Aggregator.ts # Group-by module/file/function with filtering
204
+ │ ├── MemoryAnalyzer.ts # Retained bytes, leak detection
205
+ │ ├── CpuAnalyzer.ts # Self-time ranking, call-path reconstruction
206
+ │ └── DiffAnalyzer.ts # Two-profile comparison (regressions/improvements)
207
+ ├── formatter/
208
+ │ ├── helpers.ts # Formatting utils (bytes, time, numbers)
209
+ │ ├── TextFormatter.ts # Colored terminal tables
210
+ │ ├── JsonFormatter.ts # Structured JSON output
211
+ │ ├── MarkdownFormatter.ts # Markdown tables
212
+ │ ├── ChromeTraceExporter.ts # Chrome DevTools trace format
213
+ │ └── CsvExporter.ts # Flat CSV export
214
+ ├── index.ts # CLI entry point (commander)
215
+ └── lib.ts # Programmatic API for library consumers
216
+ ```
217
+
218
+ Data flows through three layers:
219
+
220
+ 1. **Parser** -- reads the binary `.bsprof` file and produces a `ParsedProfile` with raw maps of strings, modules, path elements, memory records, and CPU records.
221
+ 2. **Analyzers** -- aggregate the raw data by module/file/function, apply filters, compute retained bytes, detect leaks, rank hot paths, and diff two profiles.
222
+ 3. **Formatters** -- render analysis results as terminal text (with colors), JSON, Markdown, Chrome traces, or CSV.
223
+
224
+ ## .bsprof Format
225
+
226
+ This tool parses the BrightScript Profiler binary format (v3.0.0) as specified in the [Roku developer documentation](https://developer.roku.com/docs/developer-program/dev-tools/brs-profiler-file-format.md).
227
+
228
+ Supported entry types:
229
+ - String table entries
230
+ - Executable module entries
231
+ - Path elements (root and chained)
232
+ - Memory operations (alloc, free, free_realloc) with address tracking
233
+ - CPU measurements (cpu cycles, wall clock time)
234
+ - Path call count entries
235
+
236
+ ## License
237
+
238
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { ParsedProfile, AnalysisOptions, ModuleStats, FileStats, FunctionStats } from '../parser/types.js';
2
+ export interface AggregatedData {
3
+ byModule: Map<string, ModuleStats>;
4
+ byFile: Map<string, FileStats>;
5
+ byFunction: Map<string, FunctionStats>;
6
+ }
7
+ export declare function aggregate(profile: ParsedProfile, options?: Partial<AnalysisOptions>): AggregatedData;
8
+ /** Sort and slice function stats for ranked output */
9
+ export declare function rankFunctions(funcs: FunctionStats[], sortBy: string, top: number, threshold?: number): FunctionStats[];
10
+ //# sourceMappingURL=Aggregator.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { minimatch } from 'minimatch';
2
+ import { BsprofParser } from '../parser/BsprofParser.js';
3
+ export function aggregate(profile, options) {
4
+ const byModule = new Map();
5
+ const byFile = new Map();
6
+ const byFunction = new Map();
7
+ const { memoryByPE, cpuByPE } = profile.parseResult;
8
+ // Aggregate memory data
9
+ for (const [peId, rec] of memoryByPE) {
10
+ const mod = BsprofParser.resolveModuleName(profile, peId);
11
+ const file = BsprofParser.resolveFileName(profile, peId);
12
+ const func = BsprofParser.resolveFuncName(profile, peId);
13
+ if (!passesFilter(mod, file, options))
14
+ continue;
15
+ accumulateModule(byModule, mod, rec.allocBytes, rec.freeBytes, rec.allocCount, rec.freeCount);
16
+ accumulateFile(byFile, file, mod, rec.allocBytes, rec.freeBytes, rec.allocCount, rec.freeCount);
17
+ accumulateFunction(byFunction, func, file, mod, peId, profile, rec.allocBytes, rec.freeBytes, rec.allocCount, rec.freeCount, 0, 0, 0);
18
+ }
19
+ // Aggregate CPU data
20
+ for (const [peId, cpu] of cpuByPE) {
21
+ const mod = BsprofParser.resolveModuleName(profile, peId);
22
+ const file = BsprofParser.resolveFileName(profile, peId);
23
+ const func = BsprofParser.resolveFuncName(profile, peId);
24
+ if (!passesFilter(mod, file, options))
25
+ continue;
26
+ const fnKey = `${func}|${file}`;
27
+ let fnRec = byFunction.get(fnKey);
28
+ if (!fnRec) {
29
+ fnRec = makeFunctionStats(func, file, mod);
30
+ byFunction.set(fnKey, fnRec);
31
+ }
32
+ fnRec.cpuSelf += cpu.cpuSelf;
33
+ fnRec.wallSelf += cpu.wallSelf;
34
+ if (cpu.callCount)
35
+ fnRec.callCount += cpu.callCount;
36
+ }
37
+ return { byModule, byFile, byFunction };
38
+ }
39
+ function passesFilter(mod, file, options) {
40
+ if (!options)
41
+ return true;
42
+ if (options.filterModule && mod !== options.filterModule)
43
+ return false;
44
+ if (options.excludeModule && mod === options.excludeModule)
45
+ return false;
46
+ if (options.filterFile && !minimatch(file, options.filterFile))
47
+ return false;
48
+ return true;
49
+ }
50
+ function accumulateModule(map, mod, allocBytes, freeBytes, allocCount, freeCount) {
51
+ let rec = map.get(mod);
52
+ if (!rec) {
53
+ rec = { module: mod, allocBytes: 0, freeBytes: 0, retainedBytes: 0, allocCount: 0, freeCount: 0 };
54
+ map.set(mod, rec);
55
+ }
56
+ rec.allocBytes += allocBytes;
57
+ rec.freeBytes += freeBytes;
58
+ rec.retainedBytes = rec.allocBytes - rec.freeBytes;
59
+ rec.allocCount += allocCount;
60
+ rec.freeCount += freeCount;
61
+ }
62
+ function accumulateFile(map, file, mod, allocBytes, freeBytes, allocCount, freeCount) {
63
+ let rec = map.get(file);
64
+ if (!rec) {
65
+ rec = { file, module: mod, allocBytes: 0, freeBytes: 0, retainedBytes: 0, allocCount: 0, freeCount: 0 };
66
+ map.set(file, rec);
67
+ }
68
+ rec.allocBytes += allocBytes;
69
+ rec.freeBytes += freeBytes;
70
+ rec.retainedBytes = rec.allocBytes - rec.freeBytes;
71
+ rec.allocCount += allocCount;
72
+ rec.freeCount += freeCount;
73
+ }
74
+ function makeFunctionStats(func, file, mod) {
75
+ return {
76
+ function: func, file, module: mod,
77
+ allocBytes: 0, freeBytes: 0, retainedBytes: 0,
78
+ allocCount: 0, freeCount: 0,
79
+ cpuSelf: 0, wallSelf: 0, callCount: 0,
80
+ };
81
+ }
82
+ function accumulateFunction(map, func, file, mod, _peId, _profile, allocBytes, freeBytes, allocCount, freeCount, cpuSelf, wallSelf, callCount) {
83
+ const key = `${func}|${file}`;
84
+ let rec = map.get(key);
85
+ if (!rec) {
86
+ rec = makeFunctionStats(func, file, mod);
87
+ map.set(key, rec);
88
+ }
89
+ rec.allocBytes += allocBytes;
90
+ rec.freeBytes += freeBytes;
91
+ rec.retainedBytes = rec.allocBytes - rec.freeBytes;
92
+ rec.allocCount += allocCount;
93
+ rec.freeCount += freeCount;
94
+ rec.cpuSelf += cpuSelf;
95
+ rec.wallSelf += wallSelf;
96
+ rec.callCount += callCount;
97
+ }
98
+ /** Sort and slice function stats for ranked output */
99
+ export function rankFunctions(funcs, sortBy, top, threshold = 0) {
100
+ const sorted = [...funcs].sort((a, b) => {
101
+ switch (sortBy) {
102
+ case 'retained': return b.retainedBytes - a.retainedBytes;
103
+ case 'allocated': return b.allocBytes - a.allocBytes;
104
+ case 'allocCount': return b.allocCount - a.allocCount;
105
+ case 'cpuSelf': return b.cpuSelf - a.cpuSelf;
106
+ case 'wallSelf': return b.wallSelf - a.wallSelf;
107
+ case 'callCount': return b.callCount - a.callCount;
108
+ default: return b.retainedBytes - a.retainedBytes;
109
+ }
110
+ });
111
+ const filtered = threshold > 0
112
+ ? sorted.filter(f => {
113
+ if (sortBy === 'cpuSelf' || sortBy === 'wallSelf')
114
+ return f.cpuSelf >= threshold;
115
+ return f.retainedBytes >= threshold;
116
+ })
117
+ : sorted;
118
+ return filtered.slice(0, top);
119
+ }
120
+ //# sourceMappingURL=Aggregator.js.map
@@ -0,0 +1,3 @@
1
+ import type { ParsedProfile, AnalysisOptions, CpuReport } from '../parser/types.js';
2
+ export declare function analyzeCpu(profile: ParsedProfile, options?: Partial<AnalysisOptions>): CpuReport;
3
+ //# sourceMappingURL=CpuAnalyzer.d.ts.map
@@ -0,0 +1,59 @@
1
+ import { aggregate, rankFunctions } from './Aggregator.js';
2
+ import { buildHeaderInfo, buildParseStats } from './common.js';
3
+ import { BsprofParser } from '../parser/BsprofParser.js';
4
+ export function analyzeCpu(profile, options = {}) {
5
+ const top = options.top ?? 30;
6
+ const sortBy = options.sortBy ?? 'cpuSelf';
7
+ const threshold = options.threshold ?? 0;
8
+ const { byFunction } = aggregate(profile, options);
9
+ const allFuncs = [...byFunction.values()].filter(f => f.cpuSelf > 0 || f.wallSelf > 0 || f.callCount > 0);
10
+ // Attach call stacks for top functions
11
+ const ranked = rankFunctions(allFuncs, sortBy, top, threshold);
12
+ attachCallStacks(ranked, profile);
13
+ const summary = buildCpuSummary(allFuncs, profile);
14
+ return {
15
+ header: buildHeaderInfo(profile),
16
+ summary,
17
+ byFunction: ranked,
18
+ };
19
+ }
20
+ function buildCpuSummary(funcs, profile) {
21
+ let totalCpuTime = 0;
22
+ let totalWallTime = 0;
23
+ let totalCallCount = 0;
24
+ for (const f of funcs) {
25
+ totalCpuTime += f.cpuSelf;
26
+ totalWallTime += f.wallSelf;
27
+ totalCallCount += f.callCount;
28
+ }
29
+ return {
30
+ totalCpuTime,
31
+ totalWallTime,
32
+ totalCallCount,
33
+ parseStats: buildParseStats(profile),
34
+ };
35
+ }
36
+ /**
37
+ * For each ranked function, find a representative path element ID and resolve
38
+ * its call stack by walking the path element chain.
39
+ */
40
+ function attachCallStacks(funcs, profile) {
41
+ // Build a reverse index: "func|file" -> peId
42
+ const funcFileTopeId = new Map();
43
+ for (const [peId] of profile.parseResult.cpuByPE) {
44
+ const func = BsprofParser.resolveFuncName(profile, peId);
45
+ const file = BsprofParser.resolveFileName(profile, peId);
46
+ const key = `${func}|${file}`;
47
+ if (!funcFileTopeId.has(key)) {
48
+ funcFileTopeId.set(key, peId);
49
+ }
50
+ }
51
+ for (const f of funcs) {
52
+ const key = `${f.function}|${f.file}`;
53
+ const peId = funcFileTopeId.get(key);
54
+ if (peId !== undefined) {
55
+ f.callStack = BsprofParser.resolveCallStack(profile, peId);
56
+ }
57
+ }
58
+ }
59
+ //# sourceMappingURL=CpuAnalyzer.js.map
@@ -0,0 +1,3 @@
1
+ import type { ParsedProfile, AnalysisOptions, DiffReport } from '../parser/types.js';
2
+ export declare function compareBsprof(before: ParsedProfile, after: ParsedProfile, beforeFile: string, afterFile: string, options?: Partial<AnalysisOptions>): DiffReport;
3
+ //# sourceMappingURL=DiffAnalyzer.d.ts.map
@@ -0,0 +1,114 @@
1
+ import { aggregate } from './Aggregator.js';
2
+ export function compareBsprof(before, after, beforeFile, afterFile, options = {}) {
3
+ const aggBefore = aggregate(before, options);
4
+ const aggAfter = aggregate(after, options);
5
+ const funcsBefore = new Map();
6
+ for (const f of aggBefore.byFunction.values()) {
7
+ funcsBefore.set(`${f.function}|${f.file}`, f);
8
+ }
9
+ const funcsAfter = new Map();
10
+ for (const f of aggAfter.byFunction.values()) {
11
+ funcsAfter.set(`${f.function}|${f.file}`, f);
12
+ }
13
+ const allKeys = new Set([...funcsBefore.keys(), ...funcsAfter.keys()]);
14
+ const memRegressions = [];
15
+ const memImprovements = [];
16
+ const cpuRegressions = [];
17
+ const cpuImprovements = [];
18
+ const newLeaks = [];
19
+ const resolvedLeaks = [];
20
+ let totalRetainedBefore = 0;
21
+ let totalRetainedAfter = 0;
22
+ let totalCpuBefore = 0;
23
+ let totalCpuAfter = 0;
24
+ for (const key of allKeys) {
25
+ const fb = funcsBefore.get(key);
26
+ const fa = funcsAfter.get(key);
27
+ const retBefore = fb ? fb.retainedBytes : 0;
28
+ const retAfter = fa ? fa.retainedBytes : 0;
29
+ const cpuBefore = fb ? fb.cpuSelf : 0;
30
+ const cpuAfter = fa ? fa.cpuSelf : 0;
31
+ totalRetainedBefore += retBefore;
32
+ totalRetainedAfter += retAfter;
33
+ totalCpuBefore += cpuBefore;
34
+ totalCpuAfter += cpuAfter;
35
+ const retDelta = retAfter - retBefore;
36
+ const cpuDelta = cpuAfter - cpuBefore;
37
+ const parts = key.split('|');
38
+ const funcName = parts[0];
39
+ const file = parts.slice(1).join('|');
40
+ const mod = fa?.module ?? fb?.module ?? '?';
41
+ // Track new / resolved leaks
42
+ if (!fb && fa && fa.retainedBytes > 0) {
43
+ newLeaks.push(funcName);
44
+ }
45
+ if (fb && !fa && fb.retainedBytes > 0) {
46
+ resolvedLeaks.push(funcName);
47
+ }
48
+ if (retDelta !== 0) {
49
+ const delta = {
50
+ function: funcName,
51
+ file,
52
+ module: mod,
53
+ retainedBefore: retBefore,
54
+ retainedAfter: retAfter,
55
+ delta: retDelta,
56
+ cpuBefore,
57
+ cpuAfter,
58
+ cpuDelta,
59
+ };
60
+ if (retDelta > 0) {
61
+ memRegressions.push(delta);
62
+ }
63
+ else {
64
+ memImprovements.push(delta);
65
+ }
66
+ }
67
+ if (cpuDelta !== 0) {
68
+ const delta = {
69
+ function: funcName,
70
+ file,
71
+ module: mod,
72
+ retainedBefore: retBefore,
73
+ retainedAfter: retAfter,
74
+ delta: retDelta,
75
+ cpuBefore,
76
+ cpuAfter,
77
+ cpuDelta,
78
+ };
79
+ if (cpuDelta > 0) {
80
+ cpuRegressions.push(delta);
81
+ }
82
+ else {
83
+ cpuImprovements.push(delta);
84
+ }
85
+ }
86
+ }
87
+ // Sort by absolute delta descending
88
+ memRegressions.sort((a, b) => b.delta - a.delta);
89
+ memImprovements.sort((a, b) => a.delta - b.delta);
90
+ cpuRegressions.sort((a, b) => (b.cpuDelta ?? 0) - (a.cpuDelta ?? 0));
91
+ cpuImprovements.sort((a, b) => (a.cpuDelta ?? 0) - (b.cpuDelta ?? 0));
92
+ const top = options.top ?? 30;
93
+ return {
94
+ before: { file: beforeFile, targetVersion: before.header.targetVersion },
95
+ after: { file: afterFile, targetVersion: after.header.targetVersion },
96
+ memoryDelta: {
97
+ totalRetainedBefore,
98
+ totalRetainedAfter,
99
+ totalRetainedDelta: totalRetainedAfter - totalRetainedBefore,
100
+ regressions: memRegressions.slice(0, top),
101
+ improvements: memImprovements.slice(0, top),
102
+ newLeaks,
103
+ resolvedLeaks,
104
+ },
105
+ cpuDelta: {
106
+ totalCpuBefore,
107
+ totalCpuAfter,
108
+ totalCpuDelta: totalCpuAfter - totalCpuBefore,
109
+ regressions: cpuRegressions.slice(0, top),
110
+ improvements: cpuImprovements.slice(0, top),
111
+ },
112
+ };
113
+ }
114
+ //# sourceMappingURL=DiffAnalyzer.js.map
@@ -0,0 +1,3 @@
1
+ import type { ParsedProfile, AnalysisOptions, MemoryReport } from '../parser/types.js';
2
+ export declare function analyzeMemory(profile: ParsedProfile, options?: Partial<AnalysisOptions>): MemoryReport;
3
+ //# sourceMappingURL=MemoryAnalyzer.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { aggregate, rankFunctions } from './Aggregator.js';
2
+ import { buildHeaderInfo, buildParseStats } from './common.js';
3
+ export function analyzeMemory(profile, options = {}) {
4
+ const top = options.top ?? 30;
5
+ const sortBy = options.sortBy ?? 'retained';
6
+ const threshold = options.threshold ?? 0;
7
+ const { byModule, byFile, byFunction } = aggregate(profile, options);
8
+ const moduleList = sortModules([...byModule.values()]);
9
+ const fileList = sortFiles([...byFile.values()], top);
10
+ const allFuncs = [...byFunction.values()];
11
+ const funcList = rankFunctions(allFuncs, sortBy, top, threshold);
12
+ const summary = buildMemorySummary(allFuncs, profile);
13
+ return {
14
+ header: buildHeaderInfo(profile),
15
+ summary,
16
+ byModule: moduleList,
17
+ byFile: fileList,
18
+ byFunction: funcList,
19
+ };
20
+ }
21
+ function buildMemorySummary(funcs, profile) {
22
+ let totalAllocBytes = 0;
23
+ let totalFreeBytes = 0;
24
+ let totalAllocCount = 0;
25
+ let totalFreeCount = 0;
26
+ for (const f of funcs) {
27
+ totalAllocBytes += f.allocBytes;
28
+ totalFreeBytes += f.freeBytes;
29
+ totalAllocCount += f.allocCount;
30
+ totalFreeCount += f.freeCount;
31
+ }
32
+ return {
33
+ totalAllocBytes,
34
+ totalFreeBytes,
35
+ totalRetainedBytes: totalAllocBytes - totalFreeBytes,
36
+ totalAllocCount,
37
+ totalFreeCount,
38
+ parseStats: buildParseStats(profile),
39
+ };
40
+ }
41
+ function sortModules(modules) {
42
+ return modules.sort((a, b) => b.retainedBytes - a.retainedBytes);
43
+ }
44
+ function sortFiles(files, top) {
45
+ return files.sort((a, b) => b.retainedBytes - a.retainedBytes).slice(0, top);
46
+ }
47
+ //# sourceMappingURL=MemoryAnalyzer.js.map
@@ -0,0 +1,4 @@
1
+ import type { ParsedProfile, HeaderInfo, ParseStats } from '../parser/types.js';
2
+ export declare function buildHeaderInfo(profile: ParsedProfile): HeaderInfo;
3
+ export declare function buildParseStats(profile: ParsedProfile): ParseStats;
4
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1,31 @@
1
+ export function buildHeaderInfo(profile) {
2
+ const h = profile.header;
3
+ const durationMs = profile.timestampEnd !== undefined
4
+ ? Number(profile.timestampEnd - h.timestampStart)
5
+ : 0;
6
+ const features = [];
7
+ if (h.lineSpecificData)
8
+ features.push('line-specific-data');
9
+ if (h.memoryOperations)
10
+ features.push('memory-operations');
11
+ return {
12
+ targetName: h.targetName,
13
+ targetVersion: h.targetVersion,
14
+ device: `${h.deviceVendor} ${h.deviceModel}`.trim(),
15
+ firmware: h.deviceFirmware,
16
+ duration: durationMs,
17
+ fileSize: profile.fileSize,
18
+ formatVersion: `v${h.majorVersion}.${h.minorVersion}.${h.patchLevel}`,
19
+ features,
20
+ };
21
+ }
22
+ export function buildParseStats(profile) {
23
+ return {
24
+ entries: profile.parseResult.count,
25
+ errors: profile.parseResult.errors,
26
+ strings: profile.strings.size,
27
+ modules: profile.modules.size,
28
+ pathElements: profile.pathElements.size,
29
+ };
30
+ }
31
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,13 @@
1
+ import type { ParsedProfile } from '../parser/types.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 declare class ChromeTraceExporter {
10
+ export(profile: ParsedProfile): string;
11
+ private addCallStackEvents;
12
+ }
13
+ //# sourceMappingURL=ChromeTraceExporter.d.ts.map