@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/logo.svg" width="120" height="120" alt="where-you-at 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 '@mtharrison/pkg-profiler';
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 '@mtharrison/pkg-profiler';
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 | Description |
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(" &gt; ")}</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, '&#39;');
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(' &gt; ') + '</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)) + ' &middot; ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' &middot; ' + 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))} &middot; ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} &middot; ${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 data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs);
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);