@densetsuuu/docteur 0.1.1-beta.1 → 0.1.1-beta.3

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
@@ -52,11 +52,12 @@ Interactive TUI for exploring module dependencies.
52
52
  docteur xray [options]
53
53
  ```
54
54
 
55
- | Option | Description | Default |
56
- | --------- | ------------------------- | --------------- |
57
- | `--entry` | Custom entry point | `bin/server.ts` |
55
+ | Option | Description | Default |
56
+ | --------- | ------------------ | --------------- |
57
+ | `--entry` | Custom entry point | `bin/server.ts` |
58
58
 
59
59
  **Features:**
60
+
60
61
  - Browse slowest modules
61
62
  - Drill down into module dependencies
62
63
  - See why a module was loaded (import chain)
@@ -64,6 +65,7 @@ docteur xray [options]
64
65
  - Explore provider lifecycle times (register, boot, start, ready)
65
66
 
66
67
  **Keyboard shortcuts:**
68
+
67
69
  - `↑/↓` Navigate
68
70
  - `Enter` Select
69
71
  - `Tab` Switch between Modules/Providers view
@@ -6,7 +6,8 @@ export interface PackageGroup {
6
6
  }
7
7
  export declare class ProfileCollector {
8
8
  #private;
9
- constructor(modules?: ModuleTiming[], providers?: ProviderTiming[]);
9
+ constructor(modules?: ModuleTiming[], providerPhases?: Map<string, Record<string, number>>);
10
+ static buildProviderTimings(phases: Map<string, Record<string, number>>): ProviderTiming[];
10
11
  groupAppFilesByCategory(): AppFileGroup[];
11
12
  groupModulesByPackage(): PackageGroup[];
12
13
  computeSummary(): ProfileSummary;
@@ -12,14 +12,25 @@ import { categories } from './registries/index.js';
12
12
  export class ProfileCollector {
13
13
  #modules;
14
14
  #providers;
15
- constructor(modules = [], providers = []) {
15
+ constructor(modules = [], providerPhases = new Map()) {
16
16
  this.#modules = modules;
17
- this.#providers = providers;
17
+ this.#providers = ProfileCollector.buildProviderTimings(providerPhases);
18
18
  // Compute subtree times if not already done (skip after filtering to avoid incomplete graph)
19
19
  if (modules.length > 0 && modules[0].subtreeTime === undefined) {
20
20
  this.#populateSubtreeTimes();
21
21
  }
22
22
  }
23
+ static buildProviderTimings(phases) {
24
+ return [...phases.entries()].map(([name, t]) => ({
25
+ name,
26
+ registerTime: t.register || 0,
27
+ bootTime: t.boot || 0,
28
+ startTime: t.start || 0,
29
+ readyTime: t.ready || 0,
30
+ shutdownTime: t.shutdown || 0,
31
+ totalTime: (t.register || 0) + (t.boot || 0) + (t.start || 0) + (t.ready || 0),
32
+ }));
33
+ }
23
34
  #populateSubtreeTimes() {
24
35
  const byUrl = new Map(this.#modules.map((m) => [m.resolvedUrl, m]));
25
36
  const children = new Map();
@@ -10,25 +10,13 @@
10
10
  */
11
11
  import { performance } from 'node:perf_hooks';
12
12
  let port;
13
- const queue = [];
14
- let pending = false;
15
- function send(msg) {
16
- queue.push(msg);
17
- if (!pending) {
18
- pending = true;
19
- setImmediate(() => {
20
- pending = false;
21
- port?.postMessage({ type: 'batch', messages: queue.splice(0) });
22
- });
23
- }
24
- }
25
13
  export function initialize(data) {
26
14
  port = data.port;
27
15
  }
28
16
  export async function resolve(specifier, context, next) {
29
17
  const result = await next(specifier, context);
30
18
  if (context.parentURL && result.url.startsWith('file://')) {
31
- send({ type: 'parent', child: result.url, parent: context.parentURL });
19
+ port.postMessage({ type: 'parent', child: result.url, parent: context.parentURL });
32
20
  }
33
21
  return result;
34
22
  }
@@ -39,7 +27,7 @@ export async function load(url, context, next) {
39
27
  const start = performance.now();
40
28
  const result = await next(url, context);
41
29
  if (result.format === 'module') {
42
- send({ type: 'timing', url, loadTime: performance.now() - start });
30
+ port.postMessage({ type: 'timing', url, loadTime: performance.now() - start });
43
31
  }
44
32
  return result;
45
33
  }
@@ -7,10 +7,12 @@
7
7
  | tracing channels for provider lifecycle timing.
8
8
  |
9
9
  */
10
- import { register } from 'node:module';
10
+ import { createRequire, register } from 'node:module';
11
+ import { join } from 'node:path';
11
12
  import { performance } from 'node:perf_hooks';
12
13
  import { MessageChannel } from 'node:worker_threads';
13
- const { tracingChannels } = (await import('@adonisjs/application'));
14
+ const require = createRequire(join(process.cwd(), 'node_modules', '_'));
15
+ const { tracingChannels } = require('@adonisjs/application');
14
16
  // Module timing data from hooks
15
17
  const parents = new Map();
16
18
  const loadTimes = new Map();
@@ -20,16 +22,12 @@ const providerStarts = new Map();
20
22
  const asyncCalls = new Set();
21
23
  // Set up message channel for hooks
22
24
  const { port1, port2 } = new MessageChannel();
23
- port1.unref?.();
25
+ port1.unref();
24
26
  port1.on('message', (msg) => {
25
- if (msg.type !== 'batch' || !msg.messages)
26
- return;
27
- for (const m of msg.messages) {
28
- if (m.type === 'parent')
29
- parents.set(m.child, m.parent);
30
- else if (m.type === 'timing')
31
- loadTimes.set(m.url, m.loadTime);
32
- }
27
+ if (msg.type === 'parent')
28
+ parents.set(msg.child, msg.parent);
29
+ else if (msg.type === 'timing')
30
+ loadTimes.set(msg.url, msg.loadTime);
33
31
  });
34
32
  register('./hooks.js', {
35
33
  parentURL: import.meta.url,
@@ -39,43 +37,41 @@ register('./hooks.js', {
39
37
  // Subscribe to provider lifecycle phases
40
38
  // For async methods: start -> end -> asyncStart -> asyncEnd (we wait for asyncEnd)
41
39
  // For sync methods: start -> end (we record on end, but defer to check if async fires)
40
+ function getProviderName(msg) {
41
+ return msg.provider.constructor.name;
42
+ }
43
+ function recordPhase(name, phase, endTime) {
44
+ const key = `${name}:${phase}`;
45
+ const start = providerStarts.get(key);
46
+ if (start === undefined)
47
+ return;
48
+ const phases = providerPhases.get(name) || {};
49
+ phases[phase] = endTime - start;
50
+ providerPhases.set(name, phases);
51
+ providerStarts.delete(key);
52
+ }
42
53
  function subscribePhase(channel, phase) {
43
- const getName = (msg) => msg.provider.constructor.name;
44
54
  channel.subscribe({
45
55
  start(msg) {
46
- providerStarts.set(`${getName(msg)}:${phase}`, performance.now());
56
+ providerStarts.set(`${getProviderName(msg)}:${phase}`, performance.now());
47
57
  },
48
58
  end(msg) {
49
- const name = getName(msg);
59
+ const name = getProviderName(msg);
50
60
  const key = `${name}:${phase}`;
51
61
  const endTime = performance.now();
52
62
  // Defer to check if this becomes async (asyncStart fires before our setTimeout)
53
63
  setTimeout(() => {
54
- if (asyncCalls.has(key))
55
- return;
56
- const start = providerStarts.get(key);
57
- if (start !== undefined) {
58
- const phases = providerPhases.get(name) || {};
59
- phases[phase] = endTime - start;
60
- providerPhases.set(name, phases);
61
- providerStarts.delete(key);
62
- }
64
+ if (!asyncCalls.has(key))
65
+ recordPhase(name, phase, endTime);
63
66
  }, 0);
64
67
  },
65
68
  asyncStart(msg) {
66
- asyncCalls.add(`${getName(msg)}:${phase}`);
69
+ asyncCalls.add(`${getProviderName(msg)}:${phase}`);
67
70
  },
68
71
  asyncEnd(msg) {
69
- const name = getName(msg);
70
- const key = `${name}:${phase}`;
71
- const start = providerStarts.get(key);
72
- if (start !== undefined) {
73
- const phases = providerPhases.get(name) || {};
74
- phases[phase] = performance.now() - start;
75
- providerPhases.set(name, phases);
76
- providerStarts.delete(key);
77
- }
78
- asyncCalls.delete(key);
72
+ const name = getProviderName(msg);
73
+ recordPhase(name, phase, performance.now());
74
+ asyncCalls.delete(`${name}:${phase}`);
79
75
  },
80
76
  error() { },
81
77
  });
@@ -90,21 +86,12 @@ if (process.send) {
90
86
  process.on('message', (msg) => {
91
87
  if (msg.type !== 'getResults')
92
88
  return;
93
- const providers = [...providerPhases.entries()].map(([name, t]) => ({
94
- name,
95
- registerTime: t.register || 0,
96
- bootTime: t.boot || 0,
97
- startTime: t.start || 0,
98
- readyTime: t.ready || 0,
99
- shutdownTime: t.shutdown || 0,
100
- totalTime: (t.register || 0) + (t.boot || 0) + (t.start || 0) + (t.ready || 0),
101
- }));
102
89
  process.send({
103
90
  type: 'results',
104
91
  data: {
105
92
  loadTimes: Object.fromEntries(loadTimes),
106
93
  parents: Object.fromEntries(parents),
107
- providers,
94
+ providerPhases: Object.fromEntries(providerPhases),
108
95
  },
109
96
  });
110
97
  });
@@ -8,14 +8,13 @@
8
8
  |
9
9
  */
10
10
  import { fork } from 'node:child_process';
11
- import { fileURLToPath } from 'node:url';
12
11
  import { join } from 'node:path';
13
12
  import { existsSync } from 'node:fs';
14
13
  import { ProfileCollector } from './collector.js';
15
14
  import { simplifyUrl } from './reporters/format.js';
16
15
  const TIMEOUT_MS = 30_000;
17
16
  export function findLoaderPath() {
18
- return fileURLToPath(import.meta.resolve('@densetsuuu/docteur/profiler/loader'));
17
+ return import.meta.resolve('@densetsuuu/docteur/profiler/loader');
19
18
  }
20
19
  export function findEntryPoint(cwd, entry) {
21
20
  const entryPath = join(cwd, entry || 'bin/server.ts');
@@ -37,7 +36,7 @@ function runProfiledProcess(loaderPath, entryPoint, cwd, options) {
37
36
  const suppressOutput = options.suppressOutput ?? false;
38
37
  return new Promise((resolve, reject) => {
39
38
  const state = {
40
- providers: [],
39
+ providerPhases: new Map(),
41
40
  loadTimes: new Map(),
42
41
  parents: new Map(),
43
42
  done: false,
@@ -86,7 +85,7 @@ function runProfiledProcess(loaderPath, entryPoint, cwd, options) {
86
85
  const data = msg.data;
87
86
  state.loadTimes = new Map(Object.entries(data.loadTimes));
88
87
  state.parents = new Map(Object.entries(data.parents || {}));
89
- state.providers = data.providers || [];
88
+ state.providerPhases = new Map(Object.entries(data.providerPhases || {}));
90
89
  complete();
91
90
  }
92
91
  });
@@ -109,7 +108,7 @@ function buildResults(state, cwd) {
109
108
  loadTime,
110
109
  parentUrl: state.parents.get(url),
111
110
  }));
112
- const collector = new ProfileCollector(modules, state.providers);
111
+ const collector = new ProfileCollector(modules, state.providerPhases);
113
112
  const bootTimeMs = state.bootDuration
114
113
  ? state.bootDuration[0] * 1000 + state.bootDuration[1] / 1_000_000
115
114
  : 0;
@@ -65,7 +65,7 @@ export class ConsoleReporter {
65
65
  ui.logger.log('');
66
66
  }
67
67
  #printSlowestModules(result, config, cwd) {
68
- const collector = new ProfileCollector(result.modules, result.providers);
68
+ const collector = new ProfileCollector(result.modules);
69
69
  const filtered = collector.filterModules(config);
70
70
  const slowest = new ProfileCollector(filtered).getTopSlowest(config.topModules);
71
71
  if (slowest.length === 0) {
@@ -102,7 +102,7 @@ export class ConsoleReporter {
102
102
  ui.logger.log('');
103
103
  }
104
104
  #printPackageGroups(result, config) {
105
- const collector = new ProfileCollector(result.modules, result.providers);
105
+ const collector = new ProfileCollector(result.modules);
106
106
  const filtered = collector.filterModules(config);
107
107
  const groups = new ProfileCollector(filtered).groupModulesByPackage();
108
108
  if (groups.length === 0) {
@@ -176,7 +176,7 @@ export class ConsoleReporter {
176
176
  if (result.summary.totalModules > 500) {
177
177
  recommendations.push(`Loading ${result.summary.totalModules} modules. Consider code splitting or lazy imports.`);
178
178
  }
179
- const collector = new ProfileCollector(result.modules, result.providers);
179
+ const collector = new ProfileCollector(result.modules);
180
180
  const filtered = collector.filterModules(config);
181
181
  const verySlowModules = filtered.filter((m) => getEffectiveTime(m) > 100);
182
182
  if (verySlowModules.length > 0) {
@@ -124,7 +124,7 @@ export function ModuleView({ node, tree: _tree, onNavigate, onBack }) {
124
124
  }
125
125
  onNavigate(item.value);
126
126
  };
127
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', '\uf21e', " Module Details"] }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "green", children: [' ', node.displayName] }) }), _jsxs(Text, { dimColor: true, children: [" ", node.timing.resolvedUrl] })] }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Total time: " }), _jsx(Text, { color: getTimeColor(time), children: formatDuration(time) }), _jsx(Text, { dimColor: true, children: " (with dependencies)" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Self time: " }), _jsx(Text, { color: getTimeColor(selfTime), children: formatDuration(selfTime) }), _jsx(Text, { dimColor: true, children: " (this file only)" })] })] }), lazyImportCandidates.length > 0 && !isDependency(node.timing.resolvedUrl) && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [symbols.lightbulb, " Optimization tip:"] }), _jsxs(Text, { dimColor: true, children: [' ', "This file imports ", lazyImportCandidates.length, " heavy package(s) that could be"] }), _jsx(Text, { dimColor: true, children: " lazy-loaded with dynamic imports to reduce cold start time:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " Before: import xlsx from 'xlsx'" }), _jsx(Text, { dimColor: true, children: " After: const xlsx = await import('xlsx')" })] })] })), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ItemComponent }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Left/ESC/Backspace: back | q: quit" }) })] }));
127
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', '\uf21e', " Module Details"] }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "green", children: [' ', node.displayName] }) }), _jsxs(Text, { dimColor: true, children: [" ", node.timing.resolvedUrl] })] }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Total time: " }), _jsx(Text, { color: getTimeColor(time), children: formatDuration(time) }), _jsx(Text, { dimColor: true, children: " (with dependencies)" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Self time: " }), _jsx(Text, { color: getTimeColor(selfTime), children: formatDuration(selfTime) }), _jsx(Text, { dimColor: true, children: " (this file only)" })] })] }), lazyImportCandidates.length > 0 && !isDependency(node.timing.resolvedUrl) && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [symbols.lightbulb, " Optimization tip:"] }), _jsxs(Text, { dimColor: true, children: [' ', "This file imports ", lazyImportCandidates.length, " heavy package(s) that could be"] }), _jsx(Text, { dimColor: true, children: " lazy-loaded with dynamic imports to reduce cold start time:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " Before: import xlsx from 'xlsx'" }), _jsx(Text, { dimColor: true, children: " After: const xlsx = await import('xlsx')" })] })] })), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ItemComponent }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Left/ESC/Backspace: back | q: quit" }) })] }));
128
128
  }
129
129
  function extractPackageName(url) {
130
130
  const match = url.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
@@ -23,9 +23,7 @@ export function ProviderView({ provider, onBack }) {
23
23
  { label: 'ready()', value: provider.readyTime },
24
24
  ];
25
25
  const maxPhaseTime = Math.max(...phases.map((p) => p.value), 1);
26
- const items = [
27
- { key: 'back', label: `${symbols.arrowLeft} Back`, value: 'back' },
28
- ];
26
+ const items = [{ key: 'back', label: `${symbols.arrowLeft} Back`, value: 'back' }];
29
27
  const handleSelect = (item) => {
30
28
  if (item.value === 'back') {
31
29
  onBack();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@densetsuuu/docteur",
3
3
  "description": "AdonisJS cold start profiler - analyze and optimize your application boot time",
4
- "version": "0.1.1-beta.1",
4
+ "version": "0.1.1-beta.3",
5
5
  "engines": {
6
6
  "node": ">=21.0.0"
7
7
  },