@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.
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Transform raw SampleStore data into a sorted, thresholded ReportData structure.
2
+ * Transform raw SampleStore data into a sorted ReportData structure.
3
3
  *
4
- * Pure function: reads nested Maps from SampleStore, applies a 5% threshold
5
- * at every level (package, file, function) relative to the total profiled time,
6
- * aggregates below-threshold entries into an "other" count, and sorts by
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
- * Aggregate SampleStore data into a ReportData structure.
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
- export function aggregate(store: SampleStore, projectName: string): ReportData {
28
- // 1. Calculate total user-attributed time
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
- totalTimeUs += us;
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
- const threshold = totalTimeUs * THRESHOLD_PCT;
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 [packageName, fileMap] of store.packages) {
54
- // Sum total time for this package
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
- for (const funcMap of fileMap.values()) {
57
- for (const us of funcMap.values()) {
58
- packageTimeUs += us;
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
- // 3. Process files within the package
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 [fileName, funcMap] of fileMap) {
84
- // Sum time for this file
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
- // Apply threshold at file level (relative to total)
91
- if (fileTimeUs < threshold) {
92
- fileOtherCount++;
93
- continue;
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
- // 4. Process functions within the file
106
- const functions: FunctionEntry[] = [];
107
- let funcOtherCount = 0;
108
-
109
- for (const [funcName, funcTimeUs] of funcMap) {
110
- // Apply threshold at function level (relative to total)
111
- if (funcTimeUs < threshold) {
112
- funcOtherCount++;
113
- continue;
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
- functions.push({
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
- files.push({
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: funcOtherCount,
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
- packages.push({
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: fileOtherCount,
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
- return {
235
+ const result: ReportData = {
157
236
  timestamp: new Date().toLocaleString(),
158
237
  totalTimeUs,
159
238
  packages,
160
- otherCount: topLevelOtherCount,
239
+ otherCount: 0,
161
240
  projectName,
162
241
  };
242
+
243
+ if (totalAsyncTimeUs > 0) {
244
+ result.totalAsyncTimeUs = totalAsyncTimeUs;
245
+ }
246
+
247
+ return result;
163
248
  }
@@ -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