@mtharrison/pkg-profiler 1.0.1 → 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,55 +14,67 @@ 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,
42
49
  packages: [],
43
50
  otherCount: 0,
51
+ projectName,
44
52
  };
45
53
  }
46
54
 
47
- 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
+
48
62
  const packages: PackageEntry[] = [];
49
- let topLevelOtherCount = 0;
50
63
 
51
64
  // 2. Process each package
52
- for (const [packageName, fileMap] of store.packages) {
53
- // 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
54
69
  let packageTimeUs = 0;
55
- for (const funcMap of fileMap.values()) {
56
- for (const us of funcMap.values()) {
57
- packageTimeUs += us;
70
+ if (fileMap) {
71
+ for (const funcMap of fileMap.values()) {
72
+ for (const us of funcMap.values()) {
73
+ packageTimeUs += us;
74
+ }
58
75
  }
59
76
  }
60
77
 
61
- // Apply threshold at package level
62
- if (packageTimeUs < threshold) {
63
- topLevelOtherCount++;
64
- continue;
65
- }
66
-
67
78
  // Sum total sample count for this package
68
79
  let packageSampleCount = 0;
69
80
  const countFileMap = store.sampleCountsByPackage.get(packageName);
@@ -75,21 +86,46 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
75
86
  }
76
87
  }
77
88
 
78
- // 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
+
79
118
  const files: FileEntry[] = [];
80
- let fileOtherCount = 0;
81
119
 
82
- for (const [fileName, funcMap] of fileMap) {
83
- // Sum time for this file
84
- let fileTimeUs = 0;
85
- for (const us of funcMap.values()) {
86
- fileTimeUs += us;
87
- }
120
+ for (const fileName of allFileNames) {
121
+ const funcMap = fileMap?.get(fileName);
88
122
 
89
- // Apply threshold at file level (relative to total)
90
- if (fileTimeUs < threshold) {
91
- fileOtherCount++;
92
- 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
+ }
93
129
  }
94
130
 
95
131
  // Sum sample count for this file
@@ -101,61 +137,112 @@ export function aggregate(store: SampleStore, projectName: string): ReportData {
101
137
  }
102
138
  }
103
139
 
104
- // 4. Process functions within the file
105
- const functions: FunctionEntry[] = [];
106
- let funcOtherCount = 0;
107
-
108
- for (const [funcName, funcTimeUs] of funcMap) {
109
- // Apply threshold at function level (relative to total)
110
- if (funcTimeUs < threshold) {
111
- funcOtherCount++;
112
- 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;
113
153
  }
154
+ }
114
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;
115
169
  const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
170
+ const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;
171
+ const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;
116
172
 
117
- functions.push({
173
+ const entry: FunctionEntry = {
118
174
  name: funcName,
119
175
  timeUs: funcTimeUs,
120
- pct: (funcTimeUs / totalTimeUs) * 100,
176
+ pct: totalTimeUs > 0 ? (funcTimeUs / totalTimeUs) * 100 : 0,
121
177
  sampleCount: funcSampleCount,
122
- });
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);
123
187
  }
124
188
 
125
189
  // Sort functions by timeUs descending
126
190
  functions.sort((a, b) => b.timeUs - a.timeUs);
127
191
 
128
- files.push({
192
+ const fileEntry: FileEntry = {
129
193
  name: fileName,
130
194
  timeUs: fileTimeUs,
131
- pct: (fileTimeUs / totalTimeUs) * 100,
195
+ pct: totalTimeUs > 0 ? (fileTimeUs / totalTimeUs) * 100 : 0,
132
196
  sampleCount: fileSampleCount,
133
197
  functions,
134
- otherCount: funcOtherCount,
135
- });
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);
136
208
  }
137
209
 
138
210
  // Sort files by timeUs descending
139
211
  files.sort((a, b) => b.timeUs - a.timeUs);
140
212
 
141
- packages.push({
213
+ const pkgEntry: PackageEntry = {
142
214
  name: packageName,
143
215
  timeUs: packageTimeUs,
144
- pct: (packageTimeUs / totalTimeUs) * 100,
216
+ pct: totalTimeUs > 0 ? (packageTimeUs / totalTimeUs) * 100 : 0,
145
217
  isFirstParty: packageName === projectName,
146
218
  sampleCount: packageSampleCount,
147
219
  files,
148
- otherCount: fileOtherCount,
149
- });
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);
150
230
  }
151
231
 
152
232
  // Sort packages by timeUs descending
153
233
  packages.sort((a, b) => b.timeUs - a.timeUs);
154
234
 
155
- return {
235
+ const result: ReportData = {
156
236
  timestamp: new Date().toLocaleString(),
157
237
  totalTimeUs,
158
238
  packages,
159
- otherCount: topLevelOtherCount,
239
+ otherCount: 0,
240
+ projectName,
160
241
  };
242
+
243
+ if (totalAsyncTimeUs > 0) {
244
+ result.totalAsyncTimeUs = totalAsyncTimeUs;
245
+ }
246
+
247
+ return result;
161
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