@mtharrison/pkg-profiler 1.1.0 → 2.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/README.md +62 -7
- package/dist/index.cjs +774 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +91 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +769 -151
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/async-tracker.ts +178 -0
- package/src/frame-parser.ts +3 -0
- package/src/index.ts +10 -3
- package/src/pkg-profile.ts +73 -0
- package/src/reporter/aggregate.ts +149 -64
- package/src/reporter/format.ts +10 -0
- package/src/reporter/html.ts +330 -9
- package/src/sampler.ts +186 -38
- package/src/types.ts +22 -0
- package/src/reporter.ts +0 -42
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transform raw SampleStore data into a sorted
|
|
2
|
+
* Transform raw SampleStore data into a sorted ReportData structure.
|
|
3
3
|
*
|
|
4
|
-
* Pure function: reads nested Maps from SampleStore,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* timeUs descending at each level.
|
|
4
|
+
* Pure function: reads nested Maps from SampleStore, computes percentages,
|
|
5
|
+
* and sorts by timeUs descending at each level. No threshold filtering —
|
|
6
|
+
* all entries are included so the HTML report can apply thresholds client-side.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import type { SampleStore } from '../sample-store.js';
|
|
@@ -15,27 +14,35 @@ import type {
|
|
|
15
14
|
FunctionEntry,
|
|
16
15
|
} from '../types.js';
|
|
17
16
|
|
|
18
|
-
const THRESHOLD_PCT = 0.05;
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param store - SampleStore with accumulated microseconds and sample counts
|
|
24
|
-
* @param projectName - Name of the first-party project (for isFirstParty flag)
|
|
25
|
-
* @returns ReportData with packages sorted desc by time, thresholded at 5%
|
|
18
|
+
* Sum all microseconds in a SampleStore.
|
|
26
19
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let totalTimeUs = 0;
|
|
20
|
+
function sumStore(store: SampleStore): number {
|
|
21
|
+
let total = 0;
|
|
30
22
|
for (const fileMap of store.packages.values()) {
|
|
31
23
|
for (const funcMap of fileMap.values()) {
|
|
32
24
|
for (const us of funcMap.values()) {
|
|
33
|
-
|
|
25
|
+
total += us;
|
|
34
26
|
}
|
|
35
27
|
}
|
|
36
28
|
}
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Aggregate SampleStore data into a ReportData structure.
|
|
34
|
+
*
|
|
35
|
+
* @param store - SampleStore with accumulated microseconds and sample counts
|
|
36
|
+
* @param projectName - Name of the first-party project (for isFirstParty flag)
|
|
37
|
+
* @param asyncStore - Optional SampleStore with async wait time data
|
|
38
|
+
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
39
|
+
*/
|
|
40
|
+
export function aggregate(store: SampleStore, projectName: string, asyncStore?: SampleStore): ReportData {
|
|
41
|
+
// 1. Calculate total user-attributed time
|
|
42
|
+
const totalTimeUs = sumStore(store);
|
|
43
|
+
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
37
44
|
|
|
38
|
-
if (totalTimeUs === 0) {
|
|
45
|
+
if (totalTimeUs === 0 && totalAsyncTimeUs === 0) {
|
|
39
46
|
return {
|
|
40
47
|
timestamp: new Date().toLocaleString(),
|
|
41
48
|
totalTimeUs: 0,
|
|
@@ -45,26 +52,29 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
|
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
// Collect all package names from both stores
|
|
56
|
+
const allPackageNames = new Set<string>();
|
|
57
|
+
for (const name of store.packages.keys()) allPackageNames.add(name);
|
|
58
|
+
if (asyncStore) {
|
|
59
|
+
for (const name of asyncStore.packages.keys()) allPackageNames.add(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
const packages: PackageEntry[] = [];
|
|
50
|
-
let topLevelOtherCount = 0;
|
|
51
63
|
|
|
52
64
|
// 2. Process each package
|
|
53
|
-
for (const
|
|
54
|
-
|
|
65
|
+
for (const packageName of allPackageNames) {
|
|
66
|
+
const fileMap = store.packages.get(packageName);
|
|
67
|
+
|
|
68
|
+
// Sum total CPU time for this package
|
|
55
69
|
let packageTimeUs = 0;
|
|
56
|
-
|
|
57
|
-
for (const
|
|
58
|
-
|
|
70
|
+
if (fileMap) {
|
|
71
|
+
for (const funcMap of fileMap.values()) {
|
|
72
|
+
for (const us of funcMap.values()) {
|
|
73
|
+
packageTimeUs += us;
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
// Apply threshold at package level
|
|
63
|
-
if (packageTimeUs < threshold) {
|
|
64
|
-
topLevelOtherCount++;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
78
|
// Sum total sample count for this package
|
|
69
79
|
let packageSampleCount = 0;
|
|
70
80
|
const countFileMap = store.sampleCountsByPackage.get(packageName);
|
|
@@ -76,21 +86,46 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
|
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
//
|
|
89
|
+
// Async totals for this package
|
|
90
|
+
let packageAsyncTimeUs = 0;
|
|
91
|
+
let packageAsyncOpCount = 0;
|
|
92
|
+
const asyncFileMap = asyncStore?.packages.get(packageName);
|
|
93
|
+
const asyncCountFileMap = asyncStore?.sampleCountsByPackage.get(packageName);
|
|
94
|
+
if (asyncFileMap) {
|
|
95
|
+
for (const funcMap of asyncFileMap.values()) {
|
|
96
|
+
for (const us of funcMap.values()) {
|
|
97
|
+
packageAsyncTimeUs += us;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (asyncCountFileMap) {
|
|
102
|
+
for (const countFuncMap of asyncCountFileMap.values()) {
|
|
103
|
+
for (const count of countFuncMap.values()) {
|
|
104
|
+
packageAsyncOpCount += count;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Collect all file names from both stores for this package
|
|
110
|
+
const allFileNames = new Set<string>();
|
|
111
|
+
if (fileMap) {
|
|
112
|
+
for (const name of fileMap.keys()) allFileNames.add(name);
|
|
113
|
+
}
|
|
114
|
+
if (asyncFileMap) {
|
|
115
|
+
for (const name of asyncFileMap.keys()) allFileNames.add(name);
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
const files: FileEntry[] = [];
|
|
81
|
-
let fileOtherCount = 0;
|
|
82
119
|
|
|
83
|
-
for (const
|
|
84
|
-
|
|
85
|
-
let fileTimeUs = 0;
|
|
86
|
-
for (const us of funcMap.values()) {
|
|
87
|
-
fileTimeUs += us;
|
|
88
|
-
}
|
|
120
|
+
for (const fileName of allFileNames) {
|
|
121
|
+
const funcMap = fileMap?.get(fileName);
|
|
89
122
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
123
|
+
// Sum CPU time for this file
|
|
124
|
+
let fileTimeUs = 0;
|
|
125
|
+
if (funcMap) {
|
|
126
|
+
for (const us of funcMap.values()) {
|
|
127
|
+
fileTimeUs += us;
|
|
128
|
+
}
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
// Sum sample count for this file
|
|
@@ -102,62 +137,112 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
|
|
|
102
137
|
}
|
|
103
138
|
}
|
|
104
139
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
// Async totals for this file
|
|
141
|
+
let fileAsyncTimeUs = 0;
|
|
142
|
+
let fileAsyncOpCount = 0;
|
|
143
|
+
const asyncFuncMap = asyncFileMap?.get(fileName);
|
|
144
|
+
const asyncCountFuncMap = asyncCountFileMap?.get(fileName);
|
|
145
|
+
if (asyncFuncMap) {
|
|
146
|
+
for (const us of asyncFuncMap.values()) {
|
|
147
|
+
fileAsyncTimeUs += us;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (asyncCountFuncMap) {
|
|
151
|
+
for (const count of asyncCountFuncMap.values()) {
|
|
152
|
+
fileAsyncOpCount += count;
|
|
114
153
|
}
|
|
154
|
+
}
|
|
115
155
|
|
|
156
|
+
// 4. Collect all function names from both stores for this file
|
|
157
|
+
const allFuncNames = new Set<string>();
|
|
158
|
+
if (funcMap) {
|
|
159
|
+
for (const name of funcMap.keys()) allFuncNames.add(name);
|
|
160
|
+
}
|
|
161
|
+
if (asyncFuncMap) {
|
|
162
|
+
for (const name of asyncFuncMap.keys()) allFuncNames.add(name);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const functions: FunctionEntry[] = [];
|
|
166
|
+
|
|
167
|
+
for (const funcName of allFuncNames) {
|
|
168
|
+
const funcTimeUs = funcMap?.get(funcName) ?? 0;
|
|
116
169
|
const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
|
|
170
|
+
const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;
|
|
171
|
+
const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;
|
|
117
172
|
|
|
118
|
-
|
|
173
|
+
const entry: FunctionEntry = {
|
|
119
174
|
name: funcName,
|
|
120
175
|
timeUs: funcTimeUs,
|
|
121
|
-
pct: (funcTimeUs / totalTimeUs) * 100,
|
|
176
|
+
pct: totalTimeUs > 0 ? (funcTimeUs / totalTimeUs) * 100 : 0,
|
|
122
177
|
sampleCount: funcSampleCount,
|
|
123
|
-
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (totalAsyncTimeUs > 0) {
|
|
181
|
+
entry.asyncTimeUs = funcAsyncTimeUs;
|
|
182
|
+
entry.asyncPct = (funcAsyncTimeUs / totalAsyncTimeUs) * 100;
|
|
183
|
+
entry.asyncOpCount = funcAsyncOpCount;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
functions.push(entry);
|
|
124
187
|
}
|
|
125
188
|
|
|
126
189
|
// Sort functions by timeUs descending
|
|
127
190
|
functions.sort((a, b) => b.timeUs - a.timeUs);
|
|
128
191
|
|
|
129
|
-
|
|
192
|
+
const fileEntry: FileEntry = {
|
|
130
193
|
name: fileName,
|
|
131
194
|
timeUs: fileTimeUs,
|
|
132
|
-
pct: (fileTimeUs / totalTimeUs) * 100,
|
|
195
|
+
pct: totalTimeUs > 0 ? (fileTimeUs / totalTimeUs) * 100 : 0,
|
|
133
196
|
sampleCount: fileSampleCount,
|
|
134
197
|
functions,
|
|
135
|
-
otherCount:
|
|
136
|
-
}
|
|
198
|
+
otherCount: 0,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (totalAsyncTimeUs > 0) {
|
|
202
|
+
fileEntry.asyncTimeUs = fileAsyncTimeUs;
|
|
203
|
+
fileEntry.asyncPct = (fileAsyncTimeUs / totalAsyncTimeUs) * 100;
|
|
204
|
+
fileEntry.asyncOpCount = fileAsyncOpCount;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
files.push(fileEntry);
|
|
137
208
|
}
|
|
138
209
|
|
|
139
210
|
// Sort files by timeUs descending
|
|
140
211
|
files.sort((a, b) => b.timeUs - a.timeUs);
|
|
141
212
|
|
|
142
|
-
|
|
213
|
+
const pkgEntry: PackageEntry = {
|
|
143
214
|
name: packageName,
|
|
144
215
|
timeUs: packageTimeUs,
|
|
145
|
-
pct: (packageTimeUs / totalTimeUs) * 100,
|
|
216
|
+
pct: totalTimeUs > 0 ? (packageTimeUs / totalTimeUs) * 100 : 0,
|
|
146
217
|
isFirstParty: packageName === projectName,
|
|
147
218
|
sampleCount: packageSampleCount,
|
|
148
219
|
files,
|
|
149
|
-
otherCount:
|
|
150
|
-
}
|
|
220
|
+
otherCount: 0,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (totalAsyncTimeUs > 0) {
|
|
224
|
+
pkgEntry.asyncTimeUs = packageAsyncTimeUs;
|
|
225
|
+
pkgEntry.asyncPct = (packageAsyncTimeUs / totalAsyncTimeUs) * 100;
|
|
226
|
+
pkgEntry.asyncOpCount = packageAsyncOpCount;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
packages.push(pkgEntry);
|
|
151
230
|
}
|
|
152
231
|
|
|
153
232
|
// Sort packages by timeUs descending
|
|
154
233
|
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
155
234
|
|
|
156
|
-
|
|
235
|
+
const result: ReportData = {
|
|
157
236
|
timestamp: new Date().toLocaleString(),
|
|
158
237
|
totalTimeUs,
|
|
159
238
|
packages,
|
|
160
|
-
otherCount:
|
|
239
|
+
otherCount: 0,
|
|
161
240
|
projectName,
|
|
162
241
|
};
|
|
242
|
+
|
|
243
|
+
if (totalAsyncTimeUs > 0) {
|
|
244
|
+
result.totalAsyncTimeUs = totalAsyncTimeUs;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result;
|
|
163
248
|
}
|
package/src/reporter/format.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* - < 1s: shows rounded milliseconds (e.g. "432ms")
|
|
11
11
|
* - Sub-millisecond values round up to 1ms (never shows "0ms" for nonzero input)
|
|
12
12
|
* - Zero returns "0ms"
|
|
13
|
+
*
|
|
14
|
+
* @param us - Time value in microseconds.
|
|
15
|
+
* @returns Human-readable time string.
|
|
13
16
|
*/
|
|
14
17
|
export function formatTime(us: number): string {
|
|
15
18
|
if (us === 0) return '0ms';
|
|
@@ -28,6 +31,10 @@ export function formatTime(us: number): string {
|
|
|
28
31
|
/**
|
|
29
32
|
* Convert microseconds to percentage of total with one decimal place.
|
|
30
33
|
* Returns "0.0%" when totalUs is zero (avoids division by zero).
|
|
34
|
+
*
|
|
35
|
+
* @param us - Time value in microseconds.
|
|
36
|
+
* @param totalUs - Total time in microseconds (denominator).
|
|
37
|
+
* @returns Percentage string like `"12.3%"`.
|
|
31
38
|
*/
|
|
32
39
|
export function formatPct(us: number, totalUs: number): string {
|
|
33
40
|
if (totalUs === 0) return '0.0%';
|
|
@@ -38,6 +45,9 @@ export function formatPct(us: number, totalUs: number): string {
|
|
|
38
45
|
* Escape HTML-special characters to prevent broken markup.
|
|
39
46
|
* Handles: & < > " '
|
|
40
47
|
* Ampersand is replaced first to avoid double-escaping.
|
|
48
|
+
*
|
|
49
|
+
* @param str - Raw string to escape.
|
|
50
|
+
* @returns HTML-safe string.
|
|
41
51
|
*/
|
|
42
52
|
export function escapeHtml(str: string): string {
|
|
43
53
|
return str
|