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