js-self-profiling-utils 0.1.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/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # js-self-profiling-utils
2
+
3
+ [![CI](https://github.com/marco-prontera/js-self-profiling-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/marco-prontera/js-self-profiling-utils/actions/workflows/ci.yml)
4
+ [![npm version](https://badge.fury.io/js/js-self-profiling-utils.svg)](https://www.npmjs.com/package/js-self-profiling-utils)
5
+
6
+ APIs to parse and summarize [JS Self-Profiling](https://wicg.github.io/js-self-profiling/) traces.
7
+
8
+ ## Installation
9
+
10
+ ### npm / yarn / pnpm
11
+
12
+ ```bash
13
+ npm install js-self-profiling-utils
14
+ ```
15
+
16
+ ### CDN (Browser)
17
+
18
+ ```html
19
+ <script src="https://unpkg.com/js-self-profiling-utils"></script>
20
+ <!-- or -->
21
+ <script src="https://cdn.jsdelivr.net/npm/js-self-profiling-utils"></script>
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### ES Modules (TypeScript / Modern JS)
27
+
28
+ ```typescript
29
+ import { summarizeTrace, printCallTree } from 'js-self-profiling-utils';
30
+ import type { ProfilerTrace } from 'js-self-profiling-utils';
31
+
32
+ // After collecting a trace with the JS Self-Profiling API
33
+ const trace: ProfilerTrace = await profiler.stop();
34
+
35
+ const summary = summarizeTrace(trace);
36
+
37
+ console.log('Total samples:', summary.totalSamples);
38
+ console.log('Top functions:', summary.topFunctions);
39
+
40
+ // Print call tree to console
41
+ printCallTree(summary.callTree);
42
+
43
+ // Export folded stacks for flamegraph tools
44
+ const folded = summary.toFoldedStacks();
45
+ ```
46
+
47
+ ### CommonJS (Node.js)
48
+
49
+ ```javascript
50
+ const { summarizeTrace, printCallTree } = require('js-self-profiling-utils');
51
+
52
+ const summary = summarizeTrace(trace);
53
+ ```
54
+
55
+ ### Browser (Script Tag)
56
+
57
+ ```html
58
+ <script src="https://unpkg.com/js-self-profiling-utils"></script>
59
+ <script>
60
+ const { summarizeTrace, printCallTree } = JSSelfProfilingUtils;
61
+
62
+ // Use the APIs
63
+ const summary = summarizeTrace(trace);
64
+ </script>
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### `summarizeTrace(trace, options?)`
70
+
71
+ Analyzes a ProfilerTrace and returns aggregated data.
72
+
73
+ **Options:**
74
+ | Option | Type | Default | Description |
75
+ |--------|------|---------|-------------|
76
+ | `topN` | number | 30 | Number of top functions to return |
77
+ | `minPercent` | number | 0 | Minimum inclusive percentage threshold |
78
+ | `formatFrame` | function | (built-in) | Custom frame formatting function |
79
+
80
+ **Returns:**
81
+ | Property | Type | Description |
82
+ |----------|------|-------------|
83
+ | `totalSamples` | number | Total number of samples in the trace |
84
+ | `topFunctions` | TraceRow[] | Array of top functions with inclusive/exclusive counts |
85
+ | `callTree` | CallTreeNode | Hierarchical call tree structure |
86
+ | `toFoldedStacks()` | () => string | Method to export folded stacks format (for flamegraphs) |
87
+
88
+ ### `printCallTree(node, options?)`
89
+
90
+ Prints a call tree to console with indentation.
91
+
92
+ **Options:**
93
+ | Option | Type | Default | Description |
94
+ |--------|------|---------|-------------|
95
+ | `maxDepth` | number | 8 | Maximum depth to print |
96
+ | `minCount` | number | 1 | Minimum sample count to display |
97
+
98
+ ## Development
99
+
100
+ ### Prerequisites
101
+
102
+ - Node.js 18+
103
+ - Chrome or Chromium (for E2E tests)
104
+
105
+ > ⚠️ **Important:** The JS Self-Profiling API is only available in **Chromium-based browsers** (Chrome, Edge). Make sure to open the dev server URL in Chrome.
106
+
107
+ ### Setup
108
+
109
+ ```bash
110
+ # Install dependencies
111
+ npm install
112
+
113
+ # Install Playwright browsers (for E2E tests)
114
+ npx playwright install chromium
115
+ ```
116
+
117
+ ### Running the Dev Server
118
+
119
+ ```bash
120
+ npm run dev
121
+ ```
122
+
123
+ This starts a Vite dev server at `http://localhost:3000` with the required `Document-Policy: js-profiling` header. Open in **Chrome** to test the profiler interactively.
124
+
125
+ ### Running Tests
126
+
127
+ ```bash
128
+ # Unit tests
129
+ npm test
130
+
131
+ # Unit tests in watch mode
132
+ npm run test:watch
133
+
134
+ # Unit tests with coverage
135
+ npm run test:coverage
136
+
137
+ # E2E tests (requires Chromium)
138
+ npm run test:e2e
139
+
140
+ # E2E tests with UI
141
+ npm run test:e2e:ui
142
+
143
+ # All tests
144
+ npm run test:all
145
+ ```
146
+
147
+ ### Building
148
+
149
+ ```bash
150
+ npm run build
151
+ ```
152
+
153
+ Outputs to `dist/`:
154
+ - `index.js` — ES Module
155
+ - `index.cjs` — CommonJS
156
+ - `index.umd.js` — UMD (for browsers)
157
+ - `index.d.ts` — TypeScript declarations
158
+
159
+ ## License
160
+
161
+ ISC
package/dist/index.cjs ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function b(n,i){const e=n.frames[i];if(!e)return"<unknown>";const c=e.name&&e.name.length?e.name:"(anonymous)",o=typeof e.resourceId=="number"&&n.resources?.[e.resourceId]?n.resources[e.resourceId]:"",t=typeof e.line=="number"?`:${e.line}${typeof e.column=="number"?":"+e.column:""}`:"";return o?`${c} @ ${o}${t}`:`${c}${t}`}function k(n,i){const e=[];let c=i;for(;c!==void 0;){const o=n.stacks[c];if(!o)break;e.push(o.frameId),c=o.parentId}return e.reverse(),e}function x(n){const i=new Map,e=new Map,c=new Map;let o=0;for(const t of n.samples||[]){if(typeof t?.stackId!="number")continue;const m=k(n,t.stackId);if(!m.length)continue;o+=1;for(const d of m)i.set(d,(i.get(d)||0)+1);const v=m[m.length-1];e.set(v,(e.get(v)||0)+1);const p=m.join(";");c.set(p,(c.get(p)||0)+1)}return{totalSamples:o,inclusive:i,exclusive:e,collapsedStacks:c}}function y(n,i={}){const{topN:e=30,minPercent:c=0,formatFrame:o=b}=i,{totalSamples:t,inclusive:m,exclusive:v,collapsedStacks:p}=x(n),d=[];for(const[s,r]of m.entries()){const a=v.get(s)||0,l=t?r/t*100:0,u=t?a/t*100:0;l<c||d.push({frameId:s,function:o(n,s),inclusive:r,inclusivePct:+l.toFixed(2),exclusive:a,exclusivePct:+u.toFixed(2)})}d.sort((s,r)=>r.inclusive-s.inclusive);const $=d.slice(0,e),g={name:"(root)",value:0,children:new Map};for(const[s,r]of p.entries()){const a=s.split(";").map(u=>Number(u));let l=g;l.value+=r;for(const u of a){const f=o(n,u);l.children.has(f)||l.children.set(f,{name:f,value:0,children:new Map}),l=l.children.get(f),l.value+=r}}function h(s){const r=Array.from(s.children.values()).sort((a,l)=>l.value-a.value).map(h);return{name:s.name,value:s.value,children:r}}return{totalSamples:t,topFunctions:$,callTree:h(g),toFoldedStacks(){const s=[];for(const[r,a]of p.entries()){const u=r.split(";").map(f=>Number(f)).map(f=>o(n,f));s.push(`${u.join(";")} ${a}`)}return s.sort((r,a)=>{const l=Number(r.slice(r.lastIndexOf(" ")+1));return Number(a.slice(a.lastIndexOf(" ")+1))-l}),s.join(`
2
+ `)}}}function I(n,i={},e=0){const{maxDepth:c=8,minCount:o=1}=i;if(!(e>c)){e===0&&console.log(`${n.name} [samples=${n.value}]`);for(const t of n.children||[])t.value<o||(console.log(`${" ".repeat(e+1)}- ${t.name} (${t.value})`),I(t,i,e+1))}}exports.printCallTree=I;exports.summarizeTrace=y;
3
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/index.ts"],"sourcesContent":["/**\n * JS Self-Profiling trace reader\n * Works with the ProfilerTrace returned by: await profiler.stop()\n *\n * Produces:\n * - top functions (inclusive/exclusive)\n * - collapsed call tree (like a simple flamegraph aggregation)\n * - folded stacks export\n */\n\n/**\n * A frame is an element in the context of a stack containing information about the current execution state.\n */\nexport interface Frame {\n name?: string;\n resourceId?: number;\n line?: number;\n column?: number;\n}\n\n/**\n * A stack is a list of frames that MUST be ordered sequentially from outermost to innermost frame.\n */\nexport interface Stack {\n frameId: number;\n parentId?: number;\n}\n\n/**\n * A sample is a descriptor of the instantaneous state of execution at a given point in time. Each sample is associated with a stack.\n */\nexport interface Sample {\n stackId?: number;\n timestamp: number;\n}\n\nexport interface ProfilerTrace {\n resources?: string[]; // The resources attribute MUST return the ProfilerResource list set by the take a sample algorithm.\n frames: Frame[];\n stacks: Stack[];\n samples: Sample[];\n}\n\nexport interface TraceRow {\n frameId: number;\n function: string;\n inclusive: number;\n inclusivePct: number;\n exclusive: number;\n exclusivePct: number;\n}\n\nexport interface CallTreeNode {\n name: string;\n value: number;\n children: CallTreeNode[];\n}\n\nexport interface SummarizeOptions {\n topN?: number;\n minPercent?: number;\n formatFrame?: (trace: ProfilerTrace, frameId: number) => string;\n}\n\nexport interface SummarizeResult {\n totalSamples: number;\n topFunctions: TraceRow[];\n callTree: CallTreeNode;\n toFoldedStacks(): string;\n}\n\nexport interface PrintCallTreeOptions {\n maxDepth?: number;\n minCount?: number;\n}\n\nfunction defaultFormatFrame(trace: ProfilerTrace, frameId: number): string {\n const f = trace.frames[frameId];\n if (!f) return \"<unknown>\";\n const name = f.name && f.name.length ? f.name : \"(anonymous)\";\n const url =\n typeof f.resourceId === \"number\" && trace.resources?.[f.resourceId]\n ? trace.resources[f.resourceId]\n : \"\";\n const loc =\n typeof f.line === \"number\"\n ? `:${f.line}${typeof f.column === \"number\" ? \":\" + f.column : \"\"}`\n : \"\";\n return url ? `${name} @ ${url}${loc}` : `${name}${loc}`;\n}\n\n/**\n * Reconstruct a stack (leaf->root in stacks structure) into an array of frameIds root->leaf.\n */\nfunction unwindStackToFrameIds(trace: ProfilerTrace, stackId: number): number[] {\n const out: number[] = [];\n let cur: number | undefined = stackId;\n // stacks entries reference a single frameId + parentId\n while (cur !== undefined) {\n const s: Stack | undefined = trace.stacks[cur];\n if (!s) break;\n out.push(s.frameId);\n cur = s.parentId;\n }\n // out is leaf->root by construction; reverse to root->leaf\n out.reverse();\n return out;\n}\n\n/**\n * Build aggregations from samples.\n * Inclusive: counts every frame that appears in a sample stack.\n * Exclusive: counts only the leaf frame of that sample stack.\n */\ninterface AggregationResult {\n totalSamples: number;\n inclusive: Map<number, number>;\n exclusive: Map<number, number>;\n collapsedStacks: Map<string, number>;\n}\n\nfunction aggregate(trace: ProfilerTrace): AggregationResult {\n const inclusive = new Map<number, number>();\n const exclusive = new Map<number, number>();\n const collapsedStacks = new Map<string, number>(); // key = \"frameId;frameId;...\"\n let totalSamples = 0;\n\n for (const sample of trace.samples || []) {\n if (typeof sample?.stackId !== \"number\") continue;\n const frameIds = unwindStackToFrameIds(trace, sample.stackId);\n if (!frameIds.length) continue;\n\n totalSamples += 1;\n\n // inclusive\n for (const fid of frameIds) {\n inclusive.set(fid, (inclusive.get(fid) || 0) + 1);\n }\n\n // exclusive = leaf\n const leaf = frameIds[frameIds.length - 1];\n exclusive.set(leaf, (exclusive.get(leaf) || 0) + 1);\n\n // collapsed stack key\n const key = frameIds.join(\";\");\n collapsedStacks.set(key, (collapsedStacks.get(key) || 0) + 1);\n }\n\n return { totalSamples, inclusive, exclusive, collapsedStacks };\n}\n\n/**\n * Make a readable report.\n */\nexport function summarizeTrace(\n trace: ProfilerTrace,\n opts: SummarizeOptions = {}\n): SummarizeResult {\n const { topN = 30, minPercent = 0, formatFrame = defaultFormatFrame } = opts;\n\n const { totalSamples, inclusive, exclusive, collapsedStacks } = aggregate(trace);\n\n const rows: TraceRow[] = [];\n for (const [frameId, incl] of inclusive.entries()) {\n const excl = exclusive.get(frameId) || 0;\n const inclPct = totalSamples ? (incl / totalSamples) * 100 : 0;\n const exclPct = totalSamples ? (excl / totalSamples) * 100 : 0;\n if (inclPct < minPercent) continue;\n rows.push({\n frameId,\n function: formatFrame(trace, frameId),\n inclusive: incl,\n inclusivePct: +inclPct.toFixed(2),\n exclusive: excl,\n exclusivePct: +exclPct.toFixed(2),\n });\n }\n\n rows.sort((a, b) => b.inclusive - a.inclusive);\n\n const top = rows.slice(0, topN);\n\n // Build a simple call tree from collapsed stacks\n interface TreeNode {\n name: string;\n value: number;\n children: Map<string, TreeNode>;\n }\n\n const tree: TreeNode = { name: \"(root)\", value: 0, children: new Map() };\n\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n let node = tree;\n node.value += count;\n\n for (const fid of frameIds) {\n const label = formatFrame(trace, fid);\n if (!node.children.has(label)) {\n node.children.set(label, { name: label, value: 0, children: new Map() });\n }\n node = node.children.get(label)!;\n node.value += count;\n }\n }\n\n function freezeNode(n: TreeNode): CallTreeNode {\n const children = Array.from(n.children.values())\n .sort((a, b) => b.value - a.value)\n .map(freezeNode);\n return { name: n.name, value: n.value, children };\n }\n\n return {\n totalSamples,\n topFunctions: top,\n callTree: freezeNode(tree),\n /**\n * Folded stacks: \"A;B;C count\"\n * Useful for flamegraphs.\n */\n toFoldedStacks(): string {\n const lines: string[] = [];\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n const names = frameIds.map((fid) => formatFrame(trace, fid));\n lines.push(`${names.join(\";\")} ${count}`);\n }\n // biggest first is convenient\n lines.sort((a, b) => {\n const ca = Number(a.slice(a.lastIndexOf(\" \") + 1));\n const cb = Number(b.slice(b.lastIndexOf(\" \") + 1));\n return cb - ca;\n });\n return lines.join(\"\\n\");\n },\n };\n}\n\n/**\n * Convenience: pretty-print a call tree to console with indentation.\n */\nexport function printCallTree(\n node: CallTreeNode,\n opts: PrintCallTreeOptions = {},\n _depth: number = 0\n): void {\n const { maxDepth = 8, minCount = 1 } = opts;\n if (_depth > maxDepth) return;\n if (_depth === 0) {\n console.log(`${node.name} [samples=${node.value}]`);\n }\n for (const child of node.children || []) {\n if (child.value < minCount) continue;\n console.log(`${\" \".repeat(_depth + 1)}- ${child.name} (${child.value})`);\n printCallTree(child, opts, _depth + 1);\n }\n}\n"],"names":["defaultFormatFrame","trace","frameId","f","name","url","loc","unwindStackToFrameIds","stackId","out","cur","s","aggregate","inclusive","exclusive","collapsedStacks","totalSamples","sample","frameIds","fid","leaf","key","summarizeTrace","opts","topN","minPercent","formatFrame","rows","incl","excl","inclPct","exclPct","a","b","top","tree","count","x","node","label","freezeNode","n","children","lines","names","ca","printCallTree","_depth","maxDepth","minCount","child"],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ export declare interface CallTreeNode {
2
+ name: string;
3
+ value: number;
4
+ children: CallTreeNode[];
5
+ }
6
+
7
+ /**
8
+ * JS Self-Profiling trace reader
9
+ * Works with the ProfilerTrace returned by: await profiler.stop()
10
+ *
11
+ * Produces:
12
+ * - top functions (inclusive/exclusive)
13
+ * - collapsed call tree (like a simple flamegraph aggregation)
14
+ * - folded stacks export
15
+ */
16
+ /**
17
+ * A frame is an element in the context of a stack containing information about the current execution state.
18
+ */
19
+ export declare interface Frame {
20
+ name?: string;
21
+ resourceId?: number;
22
+ line?: number;
23
+ column?: number;
24
+ }
25
+
26
+ /**
27
+ * Convenience: pretty-print a call tree to console with indentation.
28
+ */
29
+ export declare function printCallTree(node: CallTreeNode, opts?: PrintCallTreeOptions, _depth?: number): void;
30
+
31
+ export declare interface PrintCallTreeOptions {
32
+ maxDepth?: number;
33
+ minCount?: number;
34
+ }
35
+
36
+ export declare interface ProfilerTrace {
37
+ resources?: string[];
38
+ frames: Frame[];
39
+ stacks: Stack[];
40
+ samples: Sample[];
41
+ }
42
+
43
+ /**
44
+ * A sample is a descriptor of the instantaneous state of execution at a given point in time. Each sample is associated with a stack.
45
+ */
46
+ export declare interface Sample {
47
+ stackId?: number;
48
+ timestamp: number;
49
+ }
50
+
51
+ /**
52
+ * A stack is a list of frames that MUST be ordered sequentially from outermost to innermost frame.
53
+ */
54
+ export declare interface Stack {
55
+ frameId: number;
56
+ parentId?: number;
57
+ }
58
+
59
+ export declare interface SummarizeOptions {
60
+ topN?: number;
61
+ minPercent?: number;
62
+ formatFrame?: (trace: ProfilerTrace, frameId: number) => string;
63
+ }
64
+
65
+ export declare interface SummarizeResult {
66
+ totalSamples: number;
67
+ topFunctions: TraceRow[];
68
+ callTree: CallTreeNode;
69
+ toFoldedStacks(): string;
70
+ }
71
+
72
+ /**
73
+ * Make a readable report.
74
+ */
75
+ export declare function summarizeTrace(trace: ProfilerTrace, opts?: SummarizeOptions): SummarizeResult;
76
+
77
+ export declare interface TraceRow {
78
+ frameId: number;
79
+ function: string;
80
+ inclusive: number;
81
+ inclusivePct: number;
82
+ exclusive: number;
83
+ exclusivePct: number;
84
+ }
85
+
86
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ function g(n, i) {
2
+ const e = n.frames[i];
3
+ if (!e) return "<unknown>";
4
+ const c = e.name && e.name.length ? e.name : "(anonymous)", o = typeof e.resourceId == "number" && n.resources?.[e.resourceId] ? n.resources[e.resourceId] : "", t = typeof e.line == "number" ? `:${e.line}${typeof e.column == "number" ? ":" + e.column : ""}` : "";
5
+ return o ? `${c} @ ${o}${t}` : `${c}${t}`;
6
+ }
7
+ function k(n, i) {
8
+ const e = [];
9
+ let c = i;
10
+ for (; c !== void 0; ) {
11
+ const o = n.stacks[c];
12
+ if (!o) break;
13
+ e.push(o.frameId), c = o.parentId;
14
+ }
15
+ return e.reverse(), e;
16
+ }
17
+ function x(n) {
18
+ const i = /* @__PURE__ */ new Map(), e = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map();
19
+ let o = 0;
20
+ for (const t of n.samples || []) {
21
+ if (typeof t?.stackId != "number") continue;
22
+ const m = k(n, t.stackId);
23
+ if (!m.length) continue;
24
+ o += 1;
25
+ for (const d of m)
26
+ i.set(d, (i.get(d) || 0) + 1);
27
+ const v = m[m.length - 1];
28
+ e.set(v, (e.get(v) || 0) + 1);
29
+ const p = m.join(";");
30
+ c.set(p, (c.get(p) || 0) + 1);
31
+ }
32
+ return { totalSamples: o, inclusive: i, exclusive: e, collapsedStacks: c };
33
+ }
34
+ function w(n, i = {}) {
35
+ const { topN: e = 30, minPercent: c = 0, formatFrame: o = g } = i, { totalSamples: t, inclusive: m, exclusive: v, collapsedStacks: p } = x(n), d = [];
36
+ for (const [s, r] of m.entries()) {
37
+ const a = v.get(s) || 0, l = t ? r / t * 100 : 0, u = t ? a / t * 100 : 0;
38
+ l < c || d.push({
39
+ frameId: s,
40
+ function: o(n, s),
41
+ inclusive: r,
42
+ inclusivePct: +l.toFixed(2),
43
+ exclusive: a,
44
+ exclusivePct: +u.toFixed(2)
45
+ });
46
+ }
47
+ d.sort((s, r) => r.inclusive - s.inclusive);
48
+ const $ = d.slice(0, e), h = { name: "(root)", value: 0, children: /* @__PURE__ */ new Map() };
49
+ for (const [s, r] of p.entries()) {
50
+ const a = s.split(";").map((u) => Number(u));
51
+ let l = h;
52
+ l.value += r;
53
+ for (const u of a) {
54
+ const f = o(n, u);
55
+ l.children.has(f) || l.children.set(f, { name: f, value: 0, children: /* @__PURE__ */ new Map() }), l = l.children.get(f), l.value += r;
56
+ }
57
+ }
58
+ function I(s) {
59
+ const r = Array.from(s.children.values()).sort((a, l) => l.value - a.value).map(I);
60
+ return { name: s.name, value: s.value, children: r };
61
+ }
62
+ return {
63
+ totalSamples: t,
64
+ topFunctions: $,
65
+ callTree: I(h),
66
+ /**
67
+ * Folded stacks: "A;B;C count"
68
+ * Useful for flamegraphs.
69
+ */
70
+ toFoldedStacks() {
71
+ const s = [];
72
+ for (const [r, a] of p.entries()) {
73
+ const u = r.split(";").map((f) => Number(f)).map((f) => o(n, f));
74
+ s.push(`${u.join(";")} ${a}`);
75
+ }
76
+ return s.sort((r, a) => {
77
+ const l = Number(r.slice(r.lastIndexOf(" ") + 1));
78
+ return Number(a.slice(a.lastIndexOf(" ") + 1)) - l;
79
+ }), s.join(`
80
+ `);
81
+ }
82
+ };
83
+ }
84
+ function b(n, i = {}, e = 0) {
85
+ const { maxDepth: c = 8, minCount: o = 1 } = i;
86
+ if (!(e > c)) {
87
+ e === 0 && console.log(`${n.name} [samples=${n.value}]`);
88
+ for (const t of n.children || [])
89
+ t.value < o || (console.log(`${" ".repeat(e + 1)}- ${t.name} (${t.value})`), b(t, i, e + 1));
90
+ }
91
+ }
92
+ export {
93
+ b as printCallTree,
94
+ w as summarizeTrace
95
+ };
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * JS Self-Profiling trace reader\n * Works with the ProfilerTrace returned by: await profiler.stop()\n *\n * Produces:\n * - top functions (inclusive/exclusive)\n * - collapsed call tree (like a simple flamegraph aggregation)\n * - folded stacks export\n */\n\n/**\n * A frame is an element in the context of a stack containing information about the current execution state.\n */\nexport interface Frame {\n name?: string;\n resourceId?: number;\n line?: number;\n column?: number;\n}\n\n/**\n * A stack is a list of frames that MUST be ordered sequentially from outermost to innermost frame.\n */\nexport interface Stack {\n frameId: number;\n parentId?: number;\n}\n\n/**\n * A sample is a descriptor of the instantaneous state of execution at a given point in time. Each sample is associated with a stack.\n */\nexport interface Sample {\n stackId?: number;\n timestamp: number;\n}\n\nexport interface ProfilerTrace {\n resources?: string[]; // The resources attribute MUST return the ProfilerResource list set by the take a sample algorithm.\n frames: Frame[];\n stacks: Stack[];\n samples: Sample[];\n}\n\nexport interface TraceRow {\n frameId: number;\n function: string;\n inclusive: number;\n inclusivePct: number;\n exclusive: number;\n exclusivePct: number;\n}\n\nexport interface CallTreeNode {\n name: string;\n value: number;\n children: CallTreeNode[];\n}\n\nexport interface SummarizeOptions {\n topN?: number;\n minPercent?: number;\n formatFrame?: (trace: ProfilerTrace, frameId: number) => string;\n}\n\nexport interface SummarizeResult {\n totalSamples: number;\n topFunctions: TraceRow[];\n callTree: CallTreeNode;\n toFoldedStacks(): string;\n}\n\nexport interface PrintCallTreeOptions {\n maxDepth?: number;\n minCount?: number;\n}\n\nfunction defaultFormatFrame(trace: ProfilerTrace, frameId: number): string {\n const f = trace.frames[frameId];\n if (!f) return \"<unknown>\";\n const name = f.name && f.name.length ? f.name : \"(anonymous)\";\n const url =\n typeof f.resourceId === \"number\" && trace.resources?.[f.resourceId]\n ? trace.resources[f.resourceId]\n : \"\";\n const loc =\n typeof f.line === \"number\"\n ? `:${f.line}${typeof f.column === \"number\" ? \":\" + f.column : \"\"}`\n : \"\";\n return url ? `${name} @ ${url}${loc}` : `${name}${loc}`;\n}\n\n/**\n * Reconstruct a stack (leaf->root in stacks structure) into an array of frameIds root->leaf.\n */\nfunction unwindStackToFrameIds(trace: ProfilerTrace, stackId: number): number[] {\n const out: number[] = [];\n let cur: number | undefined = stackId;\n // stacks entries reference a single frameId + parentId\n while (cur !== undefined) {\n const s: Stack | undefined = trace.stacks[cur];\n if (!s) break;\n out.push(s.frameId);\n cur = s.parentId;\n }\n // out is leaf->root by construction; reverse to root->leaf\n out.reverse();\n return out;\n}\n\n/**\n * Build aggregations from samples.\n * Inclusive: counts every frame that appears in a sample stack.\n * Exclusive: counts only the leaf frame of that sample stack.\n */\ninterface AggregationResult {\n totalSamples: number;\n inclusive: Map<number, number>;\n exclusive: Map<number, number>;\n collapsedStacks: Map<string, number>;\n}\n\nfunction aggregate(trace: ProfilerTrace): AggregationResult {\n const inclusive = new Map<number, number>();\n const exclusive = new Map<number, number>();\n const collapsedStacks = new Map<string, number>(); // key = \"frameId;frameId;...\"\n let totalSamples = 0;\n\n for (const sample of trace.samples || []) {\n if (typeof sample?.stackId !== \"number\") continue;\n const frameIds = unwindStackToFrameIds(trace, sample.stackId);\n if (!frameIds.length) continue;\n\n totalSamples += 1;\n\n // inclusive\n for (const fid of frameIds) {\n inclusive.set(fid, (inclusive.get(fid) || 0) + 1);\n }\n\n // exclusive = leaf\n const leaf = frameIds[frameIds.length - 1];\n exclusive.set(leaf, (exclusive.get(leaf) || 0) + 1);\n\n // collapsed stack key\n const key = frameIds.join(\";\");\n collapsedStacks.set(key, (collapsedStacks.get(key) || 0) + 1);\n }\n\n return { totalSamples, inclusive, exclusive, collapsedStacks };\n}\n\n/**\n * Make a readable report.\n */\nexport function summarizeTrace(\n trace: ProfilerTrace,\n opts: SummarizeOptions = {}\n): SummarizeResult {\n const { topN = 30, minPercent = 0, formatFrame = defaultFormatFrame } = opts;\n\n const { totalSamples, inclusive, exclusive, collapsedStacks } = aggregate(trace);\n\n const rows: TraceRow[] = [];\n for (const [frameId, incl] of inclusive.entries()) {\n const excl = exclusive.get(frameId) || 0;\n const inclPct = totalSamples ? (incl / totalSamples) * 100 : 0;\n const exclPct = totalSamples ? (excl / totalSamples) * 100 : 0;\n if (inclPct < minPercent) continue;\n rows.push({\n frameId,\n function: formatFrame(trace, frameId),\n inclusive: incl,\n inclusivePct: +inclPct.toFixed(2),\n exclusive: excl,\n exclusivePct: +exclPct.toFixed(2),\n });\n }\n\n rows.sort((a, b) => b.inclusive - a.inclusive);\n\n const top = rows.slice(0, topN);\n\n // Build a simple call tree from collapsed stacks\n interface TreeNode {\n name: string;\n value: number;\n children: Map<string, TreeNode>;\n }\n\n const tree: TreeNode = { name: \"(root)\", value: 0, children: new Map() };\n\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n let node = tree;\n node.value += count;\n\n for (const fid of frameIds) {\n const label = formatFrame(trace, fid);\n if (!node.children.has(label)) {\n node.children.set(label, { name: label, value: 0, children: new Map() });\n }\n node = node.children.get(label)!;\n node.value += count;\n }\n }\n\n function freezeNode(n: TreeNode): CallTreeNode {\n const children = Array.from(n.children.values())\n .sort((a, b) => b.value - a.value)\n .map(freezeNode);\n return { name: n.name, value: n.value, children };\n }\n\n return {\n totalSamples,\n topFunctions: top,\n callTree: freezeNode(tree),\n /**\n * Folded stacks: \"A;B;C count\"\n * Useful for flamegraphs.\n */\n toFoldedStacks(): string {\n const lines: string[] = [];\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n const names = frameIds.map((fid) => formatFrame(trace, fid));\n lines.push(`${names.join(\";\")} ${count}`);\n }\n // biggest first is convenient\n lines.sort((a, b) => {\n const ca = Number(a.slice(a.lastIndexOf(\" \") + 1));\n const cb = Number(b.slice(b.lastIndexOf(\" \") + 1));\n return cb - ca;\n });\n return lines.join(\"\\n\");\n },\n };\n}\n\n/**\n * Convenience: pretty-print a call tree to console with indentation.\n */\nexport function printCallTree(\n node: CallTreeNode,\n opts: PrintCallTreeOptions = {},\n _depth: number = 0\n): void {\n const { maxDepth = 8, minCount = 1 } = opts;\n if (_depth > maxDepth) return;\n if (_depth === 0) {\n console.log(`${node.name} [samples=${node.value}]`);\n }\n for (const child of node.children || []) {\n if (child.value < minCount) continue;\n console.log(`${\" \".repeat(_depth + 1)}- ${child.name} (${child.value})`);\n printCallTree(child, opts, _depth + 1);\n }\n}\n"],"names":["defaultFormatFrame","trace","frameId","f","name","url","loc","unwindStackToFrameIds","stackId","out","cur","s","aggregate","inclusive","exclusive","collapsedStacks","totalSamples","sample","frameIds","fid","leaf","key","summarizeTrace","opts","topN","minPercent","formatFrame","rows","incl","excl","inclPct","exclPct","a","b","top","tree","count","x","node","label","freezeNode","n","children","lines","names","ca","printCallTree","_depth","maxDepth","minCount","child"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ (function(m,v){typeof exports=="object"&&typeof module<"u"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(m=typeof globalThis<"u"?globalThis:m||self,v(m.JSSelfProfilingUtils={}))})(this,(function(m){"use strict";function v(n,l){const e=n.frames[l];if(!e)return"<unknown>";const c=e.name&&e.name.length?e.name:"(anonymous)",o=typeof e.resourceId=="number"&&n.resources?.[e.resourceId]?n.resources[e.resourceId]:"",t=typeof e.line=="number"?`:${e.line}${typeof e.column=="number"?":"+e.column:""}`:"";return o?`${c} @ ${o}${t}`:`${c}${t}`}function y(n,l){const e=[];let c=l;for(;c!==void 0;){const o=n.stacks[c];if(!o)break;e.push(o.frameId),c=o.parentId}return e.reverse(),e}function k(n){const l=new Map,e=new Map,c=new Map;let o=0;for(const t of n.samples||[]){if(typeof t?.stackId!="number")continue;const d=y(n,t.stackId);if(!d.length)continue;o+=1;for(const p of d)l.set(p,(l.get(p)||0)+1);const g=d[d.length-1];e.set(g,(e.get(g)||0)+1);const h=d.join(";");c.set(h,(c.get(h)||0)+1)}return{totalSamples:o,inclusive:l,exclusive:e,collapsedStacks:c}}function x(n,l={}){const{topN:e=30,minPercent:c=0,formatFrame:o=v}=l,{totalSamples:t,inclusive:d,exclusive:g,collapsedStacks:h}=k(n),p=[];for(const[s,i]of d.entries()){const u=g.get(s)||0,r=t?i/t*100:0,a=t?u/t*100:0;r<c||p.push({frameId:s,function:o(n,s),inclusive:i,inclusivePct:+r.toFixed(2),exclusive:u,exclusivePct:+a.toFixed(2)})}p.sort((s,i)=>i.inclusive-s.inclusive);const S=p.slice(0,e),b={name:"(root)",value:0,children:new Map};for(const[s,i]of h.entries()){const u=s.split(";").map(a=>Number(a));let r=b;r.value+=i;for(const a of u){const f=o(n,a);r.children.has(f)||r.children.set(f,{name:f,value:0,children:new Map}),r=r.children.get(f),r.value+=i}}function $(s){const i=Array.from(s.children.values()).sort((u,r)=>r.value-u.value).map($);return{name:s.name,value:s.value,children:i}}return{totalSamples:t,topFunctions:S,callTree:$(b),toFoldedStacks(){const s=[];for(const[i,u]of h.entries()){const a=i.split(";").map(f=>Number(f)).map(f=>o(n,f));s.push(`${a.join(";")} ${u}`)}return s.sort((i,u)=>{const r=Number(i.slice(i.lastIndexOf(" ")+1));return Number(u.slice(u.lastIndexOf(" ")+1))-r}),s.join(`
2
+ `)}}}function I(n,l={},e=0){const{maxDepth:c=8,minCount:o=1}=l;if(!(e>c)){e===0&&console.log(`${n.name} [samples=${n.value}]`);for(const t of n.children||[])t.value<o||(console.log(`${" ".repeat(e+1)}- ${t.name} (${t.value})`),I(t,l,e+1))}}m.printCallTree=I,m.summarizeTrace=x,Object.defineProperty(m,Symbol.toStringTag,{value:"Module"})}));
3
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * JS Self-Profiling trace reader\n * Works with the ProfilerTrace returned by: await profiler.stop()\n *\n * Produces:\n * - top functions (inclusive/exclusive)\n * - collapsed call tree (like a simple flamegraph aggregation)\n * - folded stacks export\n */\n\n/**\n * A frame is an element in the context of a stack containing information about the current execution state.\n */\nexport interface Frame {\n name?: string;\n resourceId?: number;\n line?: number;\n column?: number;\n}\n\n/**\n * A stack is a list of frames that MUST be ordered sequentially from outermost to innermost frame.\n */\nexport interface Stack {\n frameId: number;\n parentId?: number;\n}\n\n/**\n * A sample is a descriptor of the instantaneous state of execution at a given point in time. Each sample is associated with a stack.\n */\nexport interface Sample {\n stackId?: number;\n timestamp: number;\n}\n\nexport interface ProfilerTrace {\n resources?: string[]; // The resources attribute MUST return the ProfilerResource list set by the take a sample algorithm.\n frames: Frame[];\n stacks: Stack[];\n samples: Sample[];\n}\n\nexport interface TraceRow {\n frameId: number;\n function: string;\n inclusive: number;\n inclusivePct: number;\n exclusive: number;\n exclusivePct: number;\n}\n\nexport interface CallTreeNode {\n name: string;\n value: number;\n children: CallTreeNode[];\n}\n\nexport interface SummarizeOptions {\n topN?: number;\n minPercent?: number;\n formatFrame?: (trace: ProfilerTrace, frameId: number) => string;\n}\n\nexport interface SummarizeResult {\n totalSamples: number;\n topFunctions: TraceRow[];\n callTree: CallTreeNode;\n toFoldedStacks(): string;\n}\n\nexport interface PrintCallTreeOptions {\n maxDepth?: number;\n minCount?: number;\n}\n\nfunction defaultFormatFrame(trace: ProfilerTrace, frameId: number): string {\n const f = trace.frames[frameId];\n if (!f) return \"<unknown>\";\n const name = f.name && f.name.length ? f.name : \"(anonymous)\";\n const url =\n typeof f.resourceId === \"number\" && trace.resources?.[f.resourceId]\n ? trace.resources[f.resourceId]\n : \"\";\n const loc =\n typeof f.line === \"number\"\n ? `:${f.line}${typeof f.column === \"number\" ? \":\" + f.column : \"\"}`\n : \"\";\n return url ? `${name} @ ${url}${loc}` : `${name}${loc}`;\n}\n\n/**\n * Reconstruct a stack (leaf->root in stacks structure) into an array of frameIds root->leaf.\n */\nfunction unwindStackToFrameIds(trace: ProfilerTrace, stackId: number): number[] {\n const out: number[] = [];\n let cur: number | undefined = stackId;\n // stacks entries reference a single frameId + parentId\n while (cur !== undefined) {\n const s: Stack | undefined = trace.stacks[cur];\n if (!s) break;\n out.push(s.frameId);\n cur = s.parentId;\n }\n // out is leaf->root by construction; reverse to root->leaf\n out.reverse();\n return out;\n}\n\n/**\n * Build aggregations from samples.\n * Inclusive: counts every frame that appears in a sample stack.\n * Exclusive: counts only the leaf frame of that sample stack.\n */\ninterface AggregationResult {\n totalSamples: number;\n inclusive: Map<number, number>;\n exclusive: Map<number, number>;\n collapsedStacks: Map<string, number>;\n}\n\nfunction aggregate(trace: ProfilerTrace): AggregationResult {\n const inclusive = new Map<number, number>();\n const exclusive = new Map<number, number>();\n const collapsedStacks = new Map<string, number>(); // key = \"frameId;frameId;...\"\n let totalSamples = 0;\n\n for (const sample of trace.samples || []) {\n if (typeof sample?.stackId !== \"number\") continue;\n const frameIds = unwindStackToFrameIds(trace, sample.stackId);\n if (!frameIds.length) continue;\n\n totalSamples += 1;\n\n // inclusive\n for (const fid of frameIds) {\n inclusive.set(fid, (inclusive.get(fid) || 0) + 1);\n }\n\n // exclusive = leaf\n const leaf = frameIds[frameIds.length - 1];\n exclusive.set(leaf, (exclusive.get(leaf) || 0) + 1);\n\n // collapsed stack key\n const key = frameIds.join(\";\");\n collapsedStacks.set(key, (collapsedStacks.get(key) || 0) + 1);\n }\n\n return { totalSamples, inclusive, exclusive, collapsedStacks };\n}\n\n/**\n * Make a readable report.\n */\nexport function summarizeTrace(\n trace: ProfilerTrace,\n opts: SummarizeOptions = {}\n): SummarizeResult {\n const { topN = 30, minPercent = 0, formatFrame = defaultFormatFrame } = opts;\n\n const { totalSamples, inclusive, exclusive, collapsedStacks } = aggregate(trace);\n\n const rows: TraceRow[] = [];\n for (const [frameId, incl] of inclusive.entries()) {\n const excl = exclusive.get(frameId) || 0;\n const inclPct = totalSamples ? (incl / totalSamples) * 100 : 0;\n const exclPct = totalSamples ? (excl / totalSamples) * 100 : 0;\n if (inclPct < minPercent) continue;\n rows.push({\n frameId,\n function: formatFrame(trace, frameId),\n inclusive: incl,\n inclusivePct: +inclPct.toFixed(2),\n exclusive: excl,\n exclusivePct: +exclPct.toFixed(2),\n });\n }\n\n rows.sort((a, b) => b.inclusive - a.inclusive);\n\n const top = rows.slice(0, topN);\n\n // Build a simple call tree from collapsed stacks\n interface TreeNode {\n name: string;\n value: number;\n children: Map<string, TreeNode>;\n }\n\n const tree: TreeNode = { name: \"(root)\", value: 0, children: new Map() };\n\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n let node = tree;\n node.value += count;\n\n for (const fid of frameIds) {\n const label = formatFrame(trace, fid);\n if (!node.children.has(label)) {\n node.children.set(label, { name: label, value: 0, children: new Map() });\n }\n node = node.children.get(label)!;\n node.value += count;\n }\n }\n\n function freezeNode(n: TreeNode): CallTreeNode {\n const children = Array.from(n.children.values())\n .sort((a, b) => b.value - a.value)\n .map(freezeNode);\n return { name: n.name, value: n.value, children };\n }\n\n return {\n totalSamples,\n topFunctions: top,\n callTree: freezeNode(tree),\n /**\n * Folded stacks: \"A;B;C count\"\n * Useful for flamegraphs.\n */\n toFoldedStacks(): string {\n const lines: string[] = [];\n for (const [key, count] of collapsedStacks.entries()) {\n const frameIds = key.split(\";\").map((x) => Number(x));\n const names = frameIds.map((fid) => formatFrame(trace, fid));\n lines.push(`${names.join(\";\")} ${count}`);\n }\n // biggest first is convenient\n lines.sort((a, b) => {\n const ca = Number(a.slice(a.lastIndexOf(\" \") + 1));\n const cb = Number(b.slice(b.lastIndexOf(\" \") + 1));\n return cb - ca;\n });\n return lines.join(\"\\n\");\n },\n };\n}\n\n/**\n * Convenience: pretty-print a call tree to console with indentation.\n */\nexport function printCallTree(\n node: CallTreeNode,\n opts: PrintCallTreeOptions = {},\n _depth: number = 0\n): void {\n const { maxDepth = 8, minCount = 1 } = opts;\n if (_depth > maxDepth) return;\n if (_depth === 0) {\n console.log(`${node.name} [samples=${node.value}]`);\n }\n for (const child of node.children || []) {\n if (child.value < minCount) continue;\n console.log(`${\" \".repeat(_depth + 1)}- ${child.name} (${child.value})`);\n printCallTree(child, opts, _depth + 1);\n }\n}\n"],"names":["defaultFormatFrame","trace","frameId","f","name","url","loc","unwindStackToFrameIds","stackId","out","cur","s","aggregate","inclusive","exclusive","collapsedStacks","totalSamples","sample","frameIds","fid","leaf","key","summarizeTrace","opts","topN","minPercent","formatFrame","rows","incl","excl","inclPct","exclPct","a","b","top","tree","count","x","node","label","freezeNode","n","children","lines","names","ca","printCallTree","_depth","maxDepth","minCount","child"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "js-self-profiling-utils",
3
+ "version": "0.1.0",
4
+ "description": "APIs to parse and summarize JS Self-Profiling traces.",
5
+ "homepage": "https://github.com/marco-prontera/js-self-profiling-utils#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/marco-prontera/js-self-profiling-utils/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/marco-prontera/js-self-profiling-utils.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "Marco Prontera",
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "import": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ }
29
+ }
30
+ },
31
+ "unpkg": "./dist/index.umd.js",
32
+ "jsdelivr": "./dist/index.umd.js",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "dev": "vite",
38
+ "build": "vite build",
39
+ "preview": "vite preview",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:coverage": "vitest run --coverage",
44
+ "test:e2e": "playwright test",
45
+ "test:e2e:ui": "playwright test --ui",
46
+ "test:all": "npm run test && npm run test:e2e",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "@playwright/test": "^1.58.0",
51
+ "@vitest/coverage-v8": "^4.0.18",
52
+ "typescript": "^5.9.3",
53
+ "vite": "^7.3.1",
54
+ "vite-plugin-dts": "^4.5.4",
55
+ "vitest": "^4.0.18"
56
+ },
57
+ "keywords": [
58
+ "profiling",
59
+ "js-self-profiling",
60
+ "performance",
61
+ "flamegraph",
62
+ "trace",
63
+ "profiler"
64
+ ]
65
+ }