@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 +5 -3
- package/build/src/profiler/collector.d.ts +2 -1
- package/build/src/profiler/collector.js +13 -2
- package/build/src/profiler/hooks.js +2 -14
- package/build/src/profiler/loader.js +31 -44
- package/build/src/profiler/profiler.js +4 -5
- package/build/src/profiler/reporters/console_reporter.js +3 -3
- package/build/src/xray/components/ModuleView.js +1 -1
- package/build/src/xray/components/ProviderView.js +1 -3
- package/package.json +1 -1
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
|
|
56
|
-
| --------- |
|
|
57
|
-
| `--entry` | Custom entry point
|
|
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[],
|
|
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 = [],
|
|
15
|
+
constructor(modules = [], providerPhases = new Map()) {
|
|
16
16
|
this.#modules = modules;
|
|
17
|
-
this.#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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(`${
|
|
56
|
+
providerStarts.set(`${getProviderName(msg)}:${phase}`, performance.now());
|
|
47
57
|
},
|
|
48
58
|
end(msg) {
|
|
49
|
-
const name =
|
|
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
|
-
|
|
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(`${
|
|
69
|
+
asyncCalls.add(`${getProviderName(msg)}:${phase}`);
|
|
67
70
|
},
|
|
68
71
|
asyncEnd(msg) {
|
|
69
|
-
const name =
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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();
|