@mtharrison/pkg-profiler 2.1.0 → 2.2.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 +12 -12
- package/dist/index.cjs +95 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dep-chain.ts +111 -0
- package/src/reporter/aggregate.ts +18 -1
- package/src/reporter/html.ts +16 -2
- package/src/sampler.ts +3 -1
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/logo.
|
|
2
|
+
<img src="assets/logo.png" width="120" height="120" alt="where-you-at logo">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">@mtharrison/pkg-profiler</h1>
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import { start, stop } from
|
|
25
|
+
import { start, stop } from "@mtharrison/pkg-profiler";
|
|
26
26
|
|
|
27
27
|
await start();
|
|
28
28
|
// ... your code here ...
|
|
@@ -33,7 +33,7 @@ result.writeHtml(); // writes an HTML report to cwd
|
|
|
33
33
|
Or use the convenience wrapper:
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
|
-
import { profile } from
|
|
36
|
+
import { profile } from "@mtharrison/pkg-profiler";
|
|
37
37
|
|
|
38
38
|
const result = await profile(async () => {
|
|
39
39
|
await build();
|
|
@@ -51,8 +51,8 @@ A self-contained HTML report that shows exactly which npm packages are eating yo
|
|
|
51
51
|
|
|
52
52
|
Start the V8 CPU sampling profiler. Safe no-op if already profiling.
|
|
53
53
|
|
|
54
|
-
| Option | Type | Default
|
|
55
|
-
|
|
54
|
+
| Option | Type | Default | Description |
|
|
55
|
+
| ---------- | -------- | ---------- | --------------------------------- |
|
|
56
56
|
| `interval` | `number` | V8 default | Sampling interval in microseconds |
|
|
57
57
|
|
|
58
58
|
### `stop()`
|
|
@@ -91,13 +91,13 @@ app.listen(3000);
|
|
|
91
91
|
|
|
92
92
|
Returned by `stop()` and `profile()`. Contains aggregated profiling data.
|
|
93
93
|
|
|
94
|
-
| Property | Type | Description
|
|
95
|
-
|
|
96
|
-
| `timestamp` | `string` | When the profile was captured
|
|
97
|
-
| `totalTimeUs` | `number` | Total sampled wall time in microseconds
|
|
98
|
-
| `packages` | `PackageEntry[]` | Package breakdown sorted by time descending
|
|
99
|
-
| `otherCount` | `number` | Number of packages below reporting threshold
|
|
100
|
-
| `projectName` | `string` | Project name from package.json
|
|
94
|
+
| Property | Type | Description |
|
|
95
|
+
| ------------- | ---------------- | -------------------------------------------- |
|
|
96
|
+
| `timestamp` | `string` | When the profile was captured |
|
|
97
|
+
| `totalTimeUs` | `number` | Total sampled wall time in microseconds |
|
|
98
|
+
| `packages` | `PackageEntry[]` | Package breakdown sorted by time descending |
|
|
99
|
+
| `otherCount` | `number` | Number of packages below reporting threshold |
|
|
100
|
+
| `projectName` | `string` | Project name from package.json |
|
|
101
101
|
|
|
102
102
|
#### `writeHtml(path?)`
|
|
103
103
|
|
package/dist/index.cjs
CHANGED
|
@@ -364,6 +364,10 @@ function escapeHtml(str) {
|
|
|
364
364
|
|
|
365
365
|
//#endregion
|
|
366
366
|
//#region src/reporter/html.ts
|
|
367
|
+
function formatDepChain(depChain) {
|
|
368
|
+
if (!depChain || depChain.length === 0) return "";
|
|
369
|
+
return `<span class="dep-chain">via ${depChain.map((n) => escapeHtml(n)).join(" > ")}</span>`;
|
|
370
|
+
}
|
|
367
371
|
function generateCss() {
|
|
368
372
|
return `
|
|
369
373
|
:root {
|
|
@@ -507,6 +511,7 @@ function generateCss() {
|
|
|
507
511
|
}
|
|
508
512
|
|
|
509
513
|
td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }
|
|
514
|
+
.dep-chain { display: block; font-size: 0.7rem; color: var(--muted); font-family: var(--font-sans); }
|
|
510
515
|
td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
511
516
|
td.async-col { color: var(--bar-fill-async); }
|
|
512
517
|
|
|
@@ -735,6 +740,11 @@ function generateJs() {
|
|
|
735
740
|
.replace(/'/g, ''');
|
|
736
741
|
}
|
|
737
742
|
|
|
743
|
+
function depChainHtml(depChain) {
|
|
744
|
+
if (!depChain || depChain.length === 0) return '';
|
|
745
|
+
return '<span class="dep-chain">via ' + depChain.map(function(n) { return escapeHtml(n); }).join(' > ') + '</span>';
|
|
746
|
+
}
|
|
747
|
+
|
|
738
748
|
var sortBy = 'cpu';
|
|
739
749
|
|
|
740
750
|
function metricTime(entry) {
|
|
@@ -805,6 +815,7 @@ function generateJs() {
|
|
|
805
815
|
pct: pkg.pct,
|
|
806
816
|
isFirstParty: pkg.isFirstParty,
|
|
807
817
|
sampleCount: pkg.sampleCount,
|
|
818
|
+
depChain: pkg.depChain,
|
|
808
819
|
asyncTimeUs: pkg.asyncTimeUs,
|
|
809
820
|
asyncPct: pkg.asyncPct,
|
|
810
821
|
asyncOpCount: pkg.asyncOpCount,
|
|
@@ -826,7 +837,7 @@ function generateJs() {
|
|
|
826
837
|
var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
827
838
|
var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
|
|
828
839
|
rows += '<tr class="' + cls + '">' +
|
|
829
|
-
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
840
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + depChainHtml(pkg.depChain) + '</td>' +
|
|
830
841
|
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
831
842
|
'<td class="bar-cell"><div class="bar-container">' +
|
|
832
843
|
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
@@ -880,6 +891,7 @@ function generateJs() {
|
|
|
880
891
|
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
881
892
|
html += '<span class="tree-label pkg">pkg</span>';
|
|
882
893
|
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
894
|
+
html += depChainHtml(pkg.depChain);
|
|
883
895
|
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
884
896
|
html += asyncStats(pkg);
|
|
885
897
|
html += '</summary>';
|
|
@@ -980,7 +992,7 @@ function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
|
|
|
980
992
|
const pctVal = totalTimeUs > 0 ? pkg.timeUs / totalTimeUs * 100 : 0;
|
|
981
993
|
rows += `
|
|
982
994
|
<tr class="${cls}">
|
|
983
|
-
<td class="pkg-name">${escapeHtml(pkg.name)}</td>
|
|
995
|
+
<td class="pkg-name">${escapeHtml(pkg.name)}${formatDepChain(pkg.depChain)}</td>
|
|
984
996
|
<td class="numeric">${escapeHtml(formatTime(pkg.timeUs))}</td>
|
|
985
997
|
<td class="bar-cell">
|
|
986
998
|
<div class="bar-container">
|
|
@@ -1032,6 +1044,7 @@ function renderTree(packages, otherCount, totalTimeUs, hasAsync) {
|
|
|
1032
1044
|
html += `<summary>`;
|
|
1033
1045
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
1034
1046
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
1047
|
+
html += formatDepChain(pkg.depChain);
|
|
1035
1048
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
1036
1049
|
if (hasAsync) html += formatAsyncStats(pkg);
|
|
1037
1050
|
html += `</summary>`;
|
|
@@ -1181,6 +1194,73 @@ var PkgProfile = class {
|
|
|
1181
1194
|
}
|
|
1182
1195
|
};
|
|
1183
1196
|
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/dep-chain.ts
|
|
1199
|
+
/**
|
|
1200
|
+
* Resolve dependency chains for transitive npm packages.
|
|
1201
|
+
*
|
|
1202
|
+
* BFS through node_modules package.json files starting from the project's
|
|
1203
|
+
* direct dependencies to find the shortest path to each profiled package.
|
|
1204
|
+
*/
|
|
1205
|
+
function readPkgJson(dir, cache) {
|
|
1206
|
+
if (cache.has(dir)) return cache.get(dir);
|
|
1207
|
+
try {
|
|
1208
|
+
const raw = (0, node_fs.readFileSync)((0, node_path.join)(dir, "package.json"), "utf-8");
|
|
1209
|
+
const parsed = JSON.parse(raw);
|
|
1210
|
+
cache.set(dir, parsed);
|
|
1211
|
+
return parsed;
|
|
1212
|
+
} catch {
|
|
1213
|
+
cache.set(dir, null);
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function depsOf(pkg) {
|
|
1218
|
+
return [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.optionalDependencies ?? {})];
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Resolve the shortest dependency chain from the project's direct deps
|
|
1222
|
+
* to each of the given package names.
|
|
1223
|
+
*
|
|
1224
|
+
* @param projectRoot - Absolute path to the project root (contains package.json and node_modules/)
|
|
1225
|
+
* @param packageNames - Set of package names that appeared in profiling data
|
|
1226
|
+
* @param maxDepth - Maximum BFS depth to search (default 5)
|
|
1227
|
+
* @returns Map from package name to chain array (e.g. `["express", "qs"]` means project -> express -> qs)
|
|
1228
|
+
*/
|
|
1229
|
+
function resolveDependencyChains(projectRoot, packageNames, maxDepth = 5) {
|
|
1230
|
+
const result = /* @__PURE__ */ new Map();
|
|
1231
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1232
|
+
const rootPkg = readPkgJson(projectRoot, cache);
|
|
1233
|
+
if (!rootPkg) return result;
|
|
1234
|
+
const directDeps = new Set(depsOf(rootPkg));
|
|
1235
|
+
for (const name of packageNames) if (directDeps.has(name)) {}
|
|
1236
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1237
|
+
for (const name of packageNames) if (!directDeps.has(name)) targets.add(name);
|
|
1238
|
+
if (targets.size === 0) return result;
|
|
1239
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1240
|
+
const queue = [];
|
|
1241
|
+
for (const dep of directDeps) {
|
|
1242
|
+
queue.push([dep, [dep]]);
|
|
1243
|
+
visited.add(dep);
|
|
1244
|
+
}
|
|
1245
|
+
let qi = 0;
|
|
1246
|
+
while (qi < queue.length && targets.size > 0) {
|
|
1247
|
+
const [pkgName, chain] = queue[qi++];
|
|
1248
|
+
if (chain.length > maxDepth) continue;
|
|
1249
|
+
if (targets.has(pkgName)) {
|
|
1250
|
+
result.set(pkgName, chain.slice(0, -1));
|
|
1251
|
+
targets.delete(pkgName);
|
|
1252
|
+
if (targets.size === 0) break;
|
|
1253
|
+
}
|
|
1254
|
+
const pkg = readPkgJson((0, node_path.join)(projectRoot, "node_modules", pkgName), cache);
|
|
1255
|
+
if (!pkg) continue;
|
|
1256
|
+
for (const child of depsOf(pkg)) if (!visited.has(child)) {
|
|
1257
|
+
visited.add(child);
|
|
1258
|
+
queue.push([child, [...chain, child]]);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1184
1264
|
//#endregion
|
|
1185
1265
|
//#region src/reporter/aggregate.ts
|
|
1186
1266
|
/**
|
|
@@ -1199,7 +1279,7 @@ function sumStore(store) {
|
|
|
1199
1279
|
* @param asyncStore - Optional SampleStore with async wait time data
|
|
1200
1280
|
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
1201
1281
|
*/
|
|
1202
|
-
function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs) {
|
|
1282
|
+
function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs, projectRoot) {
|
|
1203
1283
|
const totalTimeUs = sumStore(store);
|
|
1204
1284
|
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
1205
1285
|
const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
|
|
@@ -1299,6 +1379,16 @@ function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs
|
|
|
1299
1379
|
}
|
|
1300
1380
|
packages.push(pkgEntry);
|
|
1301
1381
|
}
|
|
1382
|
+
if (projectRoot) {
|
|
1383
|
+
const thirdPartyNames = new Set(packages.filter((p) => !p.isFirstParty).map((p) => p.name));
|
|
1384
|
+
if (thirdPartyNames.size > 0) {
|
|
1385
|
+
const chains = resolveDependencyChains(projectRoot, thirdPartyNames);
|
|
1386
|
+
for (const pkg of packages) {
|
|
1387
|
+
const chain = chains.get(pkg.name);
|
|
1388
|
+
if (chain && chain.length > 0) pkg.depChain = chain;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1302
1392
|
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
1303
1393
|
const result = {
|
|
1304
1394
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
@@ -1462,7 +1552,8 @@ function stopSync() {
|
|
|
1462
1552
|
asyncTracker = null;
|
|
1463
1553
|
}
|
|
1464
1554
|
processProfile(profile);
|
|
1465
|
-
const
|
|
1555
|
+
const cwd = process.cwd();
|
|
1556
|
+
const data = aggregate(store, readProjectName(cwd), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs, cwd);
|
|
1466
1557
|
store.clear();
|
|
1467
1558
|
asyncStore.clear();
|
|
1468
1559
|
return new PkgProfile(data);
|