@densetsuuu/docteur 0.1.1-beta.2 → 0.1.1-beta.4
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 +6 -4
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/src/cli/commands/diagnose.js +3 -3
- package/build/src/cli/commands/xray.js +3 -3
- package/build/src/profiler/collector.d.ts +3 -2
- package/build/src/profiler/collector.js +14 -3
- package/build/src/profiler/hooks.d.ts +4 -24
- package/build/src/profiler/hooks.js +8 -20
- package/build/src/profiler/loader.js +46 -61
- package/build/src/profiler/profiler.d.ts +1 -1
- package/build/src/profiler/profiler.js +8 -8
- package/build/src/profiler/reporters/base_reporter.d.ts +1 -1
- package/build/src/profiler/reporters/console_reporter.js +5 -5
- package/build/src/profiler/reporters/format.d.ts +2 -8
- package/build/src/profiler/reporters/format.js +1 -1
- package/build/src/profiler/reporters/tui_reporter.js +1 -1
- package/build/src/{profiler/registries → registries}/categories.d.ts +1 -1
- package/build/src/xray/components/ListView.js +1 -1
- package/build/src/xray/components/ModuleView.js +2 -2
- package/build/src/xray/components/ProviderListView.d.ts +1 -1
- package/build/src/xray/components/ProviderListView.js +1 -1
- package/build/src/xray/components/ProviderView.d.ts +1 -1
- package/build/src/xray/components/ProviderView.js +2 -4
- package/build/src/xray/components/XRayApp.d.ts +1 -1
- package/build/src/xray/tree.d.ts +2 -2
- package/build/src/xray/tree.js +2 -2
- package/package.json +8 -2
- /package/build/src/{profiler/registries → registries}/categories.js +0 -0
- /package/build/src/{profiler/registries → registries}/index.d.ts +0 -0
- /package/build/src/{profiler/registries → registries}/index.js +0 -0
- /package/build/src/{profiler/registries → registries}/symbols.d.ts +0 -0
- /package/build/src/{profiler/registries → registries}/symbols.js +0 -0
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
|
|
@@ -148,7 +150,7 @@ Docteur measures cold start performance through two complementary approaches:
|
|
|
148
150
|
## Requirements
|
|
149
151
|
|
|
150
152
|
- AdonisJS v7+
|
|
151
|
-
- Node.js
|
|
153
|
+
- Node.js 22+
|
|
152
154
|
|
|
153
155
|
## License
|
|
154
156
|
|
package/build/index.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export { colorDuration, createBar, formatDuration, getCategoryIcon, getEffective
|
|
|
20
20
|
/**
|
|
21
21
|
* Export registries for customization
|
|
22
22
|
*/
|
|
23
|
-
export { categories, fileIcons, symbols, type CategoryDefinition, type SymbolKey, } from './src/
|
|
23
|
+
export { categories, fileIcons, symbols, type CategoryDefinition, type SymbolKey, } from './src/registries/index.js';
|
package/build/index.js
CHANGED
|
@@ -20,4 +20,4 @@ export { colorDuration, createBar, formatDuration, getCategoryIcon, getEffective
|
|
|
20
20
|
/**
|
|
21
21
|
* Export registries for customization
|
|
22
22
|
*/
|
|
23
|
-
export { categories, fileIcons, symbols, } from './src/
|
|
23
|
+
export { categories, fileIcons, symbols, } from './src/registries/index.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import { isAdonisProject, profile } from '
|
|
3
|
-
import { ConsoleReporter } from '
|
|
4
|
-
import { ui } from '
|
|
2
|
+
import { isAdonisProject, profile } from '#profiler/profiler';
|
|
3
|
+
import { ConsoleReporter } from '#profiler/reporters/console_reporter';
|
|
4
|
+
import { ui } from '#profiler/reporters/format';
|
|
5
5
|
export const diagnoseCommand = defineCommand({
|
|
6
6
|
meta: {
|
|
7
7
|
name: 'diagnose',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import { isAdonisProject, profile } from '
|
|
3
|
-
import { TuiReporter } from '
|
|
4
|
-
import { ui } from '
|
|
2
|
+
import { isAdonisProject, profile } from '#profiler/profiler';
|
|
3
|
+
import { TuiReporter } from '#profiler/reporters/tui_reporter';
|
|
4
|
+
import { ui } from '#profiler/reporters/format';
|
|
5
5
|
export const xrayCommand = defineCommand({
|
|
6
6
|
meta: {
|
|
7
7
|
name: 'xray',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppFileGroup, ModuleTiming, ProfileResult, ProfileSummary, ProviderTiming, ResolvedConfig } from '
|
|
1
|
+
import type { AppFileGroup, ModuleTiming, ProfileResult, ProfileSummary, ProviderTiming, ResolvedConfig } from '#types';
|
|
2
2
|
export interface PackageGroup {
|
|
3
3
|
name: string;
|
|
4
4
|
totalTime: number;
|
|
@@ -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;
|
|
@@ -8,18 +8,29 @@
|
|
|
8
8
|
| category or package.
|
|
9
9
|
|
|
|
10
10
|
*/
|
|
11
|
-
import { categories } from '
|
|
11
|
+
import { categories } from '#registries/index';
|
|
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();
|
|
@@ -1,27 +1,7 @@
|
|
|
1
|
+
import type { InitializeHook, LoadHook, ResolveHook } from 'node:module';
|
|
1
2
|
import type { MessagePort } from 'node:worker_threads';
|
|
2
|
-
|
|
3
|
-
parentURL?: string;
|
|
4
|
-
}) => Promise<{
|
|
5
|
-
url: string;
|
|
6
|
-
}>;
|
|
7
|
-
type LoadFn = (url: string, context?: {
|
|
8
|
-
format?: string;
|
|
9
|
-
}) => Promise<{
|
|
10
|
-
format: string;
|
|
11
|
-
source: string | ArrayBuffer | SharedArrayBuffer;
|
|
12
|
-
}>;
|
|
13
|
-
export declare function initialize(data: {
|
|
3
|
+
export declare const initialize: InitializeHook<{
|
|
14
4
|
port: MessagePort;
|
|
15
|
-
}): void;
|
|
16
|
-
export declare function resolve(specifier: string, context: {
|
|
17
|
-
parentURL?: string;
|
|
18
|
-
}, next: ResolveFn): Promise<{
|
|
19
|
-
url: string;
|
|
20
|
-
}>;
|
|
21
|
-
export declare function load(url: string, context: {
|
|
22
|
-
format?: string;
|
|
23
|
-
}, next: LoadFn): Promise<{
|
|
24
|
-
format: string;
|
|
25
|
-
source: string | ArrayBuffer | SharedArrayBuffer;
|
|
26
5
|
}>;
|
|
27
|
-
export
|
|
6
|
+
export declare const resolve: ResolveHook;
|
|
7
|
+
export declare const load: LoadHook;
|
|
@@ -10,36 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { performance } from 'node:perf_hooks';
|
|
12
12
|
let port;
|
|
13
|
-
const
|
|
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
|
-
export function initialize(data) {
|
|
13
|
+
export const initialize = (data) => {
|
|
26
14
|
port = data.port;
|
|
27
|
-
}
|
|
28
|
-
export async
|
|
15
|
+
};
|
|
16
|
+
export const resolve = async (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
|
-
}
|
|
35
|
-
export async
|
|
22
|
+
};
|
|
23
|
+
export const load = async (url, context, next) => {
|
|
36
24
|
if (!url.startsWith('file://')) {
|
|
37
25
|
return next(url, context);
|
|
38
26
|
}
|
|
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
|
+
};
|
|
@@ -13,100 +13,85 @@ import { performance } from 'node:perf_hooks';
|
|
|
13
13
|
import { MessageChannel } from 'node:worker_threads';
|
|
14
14
|
const require = createRequire(join(process.cwd(), 'node_modules', '_'));
|
|
15
15
|
const { tracingChannels } = require('@adonisjs/application');
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Module timing data collected via ESM hooks
|
|
18
|
+
*/
|
|
17
19
|
const parents = new Map();
|
|
18
20
|
const loadTimes = new Map();
|
|
19
|
-
// Provider timing data
|
|
20
|
-
const providerPhases = new Map();
|
|
21
|
-
const providerStarts = new Map();
|
|
22
|
-
const asyncCalls = new Set();
|
|
23
|
-
// Set up message channel for hooks
|
|
24
21
|
const { port1, port2 } = new MessageChannel();
|
|
25
|
-
port1.unref
|
|
22
|
+
port1.unref();
|
|
26
23
|
port1.on('message', (msg) => {
|
|
27
|
-
if (msg.type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
parents.set(m.child, m.parent);
|
|
32
|
-
else if (m.type === 'timing')
|
|
33
|
-
loadTimes.set(m.url, m.loadTime);
|
|
34
|
-
}
|
|
24
|
+
if (msg.type === 'parent')
|
|
25
|
+
parents.set(msg.child, msg.parent);
|
|
26
|
+
else if (msg.type === 'timing')
|
|
27
|
+
loadTimes.set(msg.url, msg.loadTime);
|
|
35
28
|
});
|
|
36
29
|
register('./hooks.js', {
|
|
37
30
|
parentURL: import.meta.url,
|
|
38
31
|
data: { port: port2 },
|
|
39
32
|
transferList: [port2],
|
|
40
33
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Provider lifecycle timing via tracing channels.
|
|
36
|
+
*
|
|
37
|
+
* Tracing channels emit: start → end (sync) or start → end → asyncStart → asyncEnd (async).
|
|
38
|
+
* We defer sync recording with setTimeout so asyncStart can claim the phase first.
|
|
39
|
+
*/
|
|
40
|
+
const providerPhases = new Map();
|
|
41
|
+
const starts = new Map();
|
|
42
|
+
const asyncPhases = new Set();
|
|
43
|
+
function name(msg) {
|
|
44
|
+
return msg.provider.constructor.name;
|
|
45
|
+
}
|
|
46
|
+
function record(provider, phase, endTime) {
|
|
47
|
+
const start = starts.get(`${provider}:${phase}`);
|
|
48
|
+
if (start === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const phases = providerPhases.get(provider) ?? {};
|
|
51
|
+
phases[phase] = endTime - start;
|
|
52
|
+
providerPhases.set(provider, phases);
|
|
53
|
+
starts.delete(`${provider}:${phase}`);
|
|
54
|
+
}
|
|
55
|
+
const phases = ['register', 'boot', 'start', 'ready', 'shutdown'];
|
|
56
|
+
for (const phase of phases) {
|
|
57
|
+
const channelKey = `provider${phase[0].toUpperCase()}${phase.slice(1)}`;
|
|
58
|
+
tracingChannels[channelKey].subscribe({
|
|
47
59
|
start(msg) {
|
|
48
|
-
|
|
60
|
+
starts.set(`${name(msg)}:${phase}`, performance.now());
|
|
49
61
|
},
|
|
50
62
|
end(msg) {
|
|
51
|
-
const
|
|
52
|
-
const key = `${name}:${phase}`;
|
|
63
|
+
const provider = name(msg);
|
|
53
64
|
const endTime = performance.now();
|
|
54
|
-
|
|
65
|
+
const key = `${provider}:${phase}`;
|
|
55
66
|
setTimeout(() => {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
const start = providerStarts.get(key);
|
|
59
|
-
if (start !== undefined) {
|
|
60
|
-
const phases = providerPhases.get(name) || {};
|
|
61
|
-
phases[phase] = endTime - start;
|
|
62
|
-
providerPhases.set(name, phases);
|
|
63
|
-
providerStarts.delete(key);
|
|
64
|
-
}
|
|
67
|
+
if (!asyncPhases.has(key))
|
|
68
|
+
record(provider, phase, endTime);
|
|
65
69
|
}, 0);
|
|
66
70
|
},
|
|
67
71
|
asyncStart(msg) {
|
|
68
|
-
|
|
72
|
+
asyncPhases.add(`${name(msg)}:${phase}`);
|
|
69
73
|
},
|
|
70
74
|
asyncEnd(msg) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (start !== undefined) {
|
|
75
|
-
const phases = providerPhases.get(name) || {};
|
|
76
|
-
phases[phase] = performance.now() - start;
|
|
77
|
-
providerPhases.set(name, phases);
|
|
78
|
-
providerStarts.delete(key);
|
|
79
|
-
}
|
|
80
|
-
asyncCalls.delete(key);
|
|
75
|
+
const provider = name(msg);
|
|
76
|
+
record(provider, phase, performance.now());
|
|
77
|
+
asyncPhases.delete(`${provider}:${phase}`);
|
|
81
78
|
},
|
|
82
79
|
error() { },
|
|
83
80
|
});
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
subscribePhase(tracingChannels.providerReady, 'ready');
|
|
89
|
-
subscribePhase(tracingChannels.providerShutdown, 'shutdown');
|
|
90
|
-
// Send results to parent process when requested
|
|
82
|
+
/**
|
|
83
|
+
* IPC: send collected results to parent process
|
|
84
|
+
*/
|
|
91
85
|
if (process.send) {
|
|
92
86
|
process.on('message', (msg) => {
|
|
93
87
|
if (msg.type !== 'getResults')
|
|
94
88
|
return;
|
|
95
|
-
const providers = [...providerPhases.entries()].map(([name, t]) => ({
|
|
96
|
-
name,
|
|
97
|
-
registerTime: t.register || 0,
|
|
98
|
-
bootTime: t.boot || 0,
|
|
99
|
-
startTime: t.start || 0,
|
|
100
|
-
readyTime: t.ready || 0,
|
|
101
|
-
shutdownTime: t.shutdown || 0,
|
|
102
|
-
totalTime: (t.register || 0) + (t.boot || 0) + (t.start || 0) + (t.ready || 0),
|
|
103
|
-
}));
|
|
104
89
|
process.send({
|
|
105
90
|
type: 'results',
|
|
106
91
|
data: {
|
|
107
92
|
loadTimes: Object.fromEntries(loadTimes),
|
|
108
93
|
parents: Object.fromEntries(parents),
|
|
109
|
-
|
|
94
|
+
providerPhases: Object.fromEntries(providerPhases),
|
|
110
95
|
},
|
|
111
96
|
});
|
|
112
97
|
});
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
|
|
|
9
9
|
*/
|
|
10
10
|
import { fork } from 'node:child_process';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
11
|
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
14
|
import { ProfileCollector } from './collector.js';
|
|
15
|
-
import { simplifyUrl } from '
|
|
15
|
+
import { simplifyUrl } from '#profiler/reporters/format';
|
|
16
16
|
const TIMEOUT_MS = 30_000;
|
|
17
17
|
export function findLoaderPath() {
|
|
18
|
-
return
|
|
18
|
+
return import.meta.resolve('@densetsuuu/docteur/profiler/loader');
|
|
19
19
|
}
|
|
20
20
|
export function findEntryPoint(cwd, entry) {
|
|
21
21
|
const entryPath = join(cwd, entry || 'bin/server.ts');
|
|
@@ -37,12 +37,12 @@ function runProfiledProcess(loaderPath, entryPoint, cwd, options) {
|
|
|
37
37
|
const suppressOutput = options.suppressOutput ?? false;
|
|
38
38
|
return new Promise((resolve, reject) => {
|
|
39
39
|
const state = {
|
|
40
|
-
|
|
40
|
+
providerPhases: new Map(),
|
|
41
41
|
loadTimes: new Map(),
|
|
42
42
|
parents: new Map(),
|
|
43
43
|
done: false,
|
|
44
44
|
};
|
|
45
|
-
const child = fork(entryPoint, [], {
|
|
45
|
+
const child = fork(pathToFileURL(entryPoint).href, [], {
|
|
46
46
|
cwd,
|
|
47
47
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
48
48
|
execArgv: ['--import', loaderPath, '--import', '@poppinss/ts-exec', '--no-warnings'],
|
|
@@ -86,7 +86,7 @@ function runProfiledProcess(loaderPath, entryPoint, cwd, options) {
|
|
|
86
86
|
const data = msg.data;
|
|
87
87
|
state.loadTimes = new Map(Object.entries(data.loadTimes));
|
|
88
88
|
state.parents = new Map(Object.entries(data.parents || {}));
|
|
89
|
-
state.
|
|
89
|
+
state.providerPhases = new Map(Object.entries(data.providerPhases || {}));
|
|
90
90
|
complete();
|
|
91
91
|
}
|
|
92
92
|
});
|
|
@@ -109,7 +109,7 @@ function buildResults(state, cwd) {
|
|
|
109
109
|
loadTime,
|
|
110
110
|
parentUrl: state.parents.get(url),
|
|
111
111
|
}));
|
|
112
|
-
const collector = new ProfileCollector(modules, state.
|
|
112
|
+
const collector = new ProfileCollector(modules, state.providerPhases);
|
|
113
113
|
const bootTimeMs = state.bootDuration
|
|
114
114
|
? state.bootDuration[0] * 1000 + state.bootDuration[1] / 1_000_000
|
|
115
115
|
: 0;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
| Renders profiling results to the terminal using @poppinss/cliui.
|
|
7
7
|
|
|
|
8
8
|
*/
|
|
9
|
-
import { ProfileCollector } from '
|
|
10
|
-
import { symbols } from '
|
|
9
|
+
import { ProfileCollector } from '#profiler/collector';
|
|
10
|
+
import { symbols } from '#registries/index';
|
|
11
11
|
import { colorDuration, createBar, formatDuration, getCategoryIcon, getEffectiveTime, simplifyUrl, ui, } from './format.js';
|
|
12
12
|
export class ConsoleReporter {
|
|
13
13
|
/**
|
|
@@ -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) {
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import type { AppFileCategory, ModuleTiming } from '
|
|
1
|
+
import type { AppFileCategory, ModuleTiming } from '#types';
|
|
2
2
|
/**
|
|
3
3
|
* Shared UI instance for consistent styling
|
|
4
4
|
*/
|
|
5
5
|
export declare const ui: {
|
|
6
6
|
colors: import("@poppinss/colors/types").Colors;
|
|
7
7
|
logger: import("@poppinss/cliui").Logger;
|
|
8
|
-
table: (tableOptions
|
|
9
|
-
/**
|
|
10
|
-
* Colors a duration based on how slow it is
|
|
11
|
-
*/
|
|
12
|
-
? /**
|
|
13
|
-
* Colors a duration based on how slow it is
|
|
14
|
-
*/: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
|
|
8
|
+
table: (tableOptions?: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
|
|
15
9
|
tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
|
|
16
10
|
steps: () => import("@poppinss/cliui").Steps;
|
|
17
11
|
icons: {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { render } from 'ink';
|
|
12
|
-
import { XRayApp } from '
|
|
12
|
+
import { XRayApp } from '#xray/components/XRayApp';
|
|
13
13
|
export class TuiReporter {
|
|
14
14
|
/**
|
|
15
15
|
* Renders an interactive TUI report using the XRay explorer.
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import { formatDuration, getEffectiveTime, getFileIcon, getTimeColor, isDependency, } from '../tree.js';
|
|
5
|
-
import { symbols } from '
|
|
5
|
+
import { symbols } from '#registries/index';
|
|
6
6
|
function ItemComponent({ isSelected = false, label, timeStr, timeColor, fileIcon, displayName, }) {
|
|
7
7
|
// Spacer
|
|
8
8
|
if (!label) {
|
|
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import SelectInput from 'ink-select-input';
|
|
5
5
|
import { formatDuration, getEffectiveTime, getImportChain, getFileIcon, getSourceIcon, getTimeColor, isDependency, } from '../tree.js';
|
|
6
|
-
import { symbols } from '
|
|
6
|
+
import { symbols } from '#registries/index';
|
|
7
7
|
function ItemComponent({ isSelected = false, label }) {
|
|
8
8
|
if (!label) {
|
|
9
9
|
return _jsx(Text, { children: " " });
|
|
@@ -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\/(@[^/]+\/[^/]+|[^/]+)/);
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import { formatDuration, getTimeColor } from '../tree.js';
|
|
5
|
-
import { symbols } from '
|
|
5
|
+
import { symbols } from '#registries/index';
|
|
6
6
|
function ItemComponent({ isSelected = false, timeStr, timeColor, name, label }) {
|
|
7
7
|
// Spacer
|
|
8
8
|
if (!label) {
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import { formatDuration, getTimeColor } from '../tree.js';
|
|
5
|
-
import { symbols } from '
|
|
5
|
+
import { symbols } from '#registries/index';
|
|
6
6
|
function Bar({ value, max, width = 30 }) {
|
|
7
7
|
const ratio = max > 0 ? Math.min(value / max, 1) : 0;
|
|
8
8
|
const filled = Math.round(ratio * width);
|
|
@@ -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/build/src/xray/tree.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ModuleTiming } from '
|
|
2
|
-
import { formatDuration, getEffectiveTime } from '
|
|
1
|
+
import type { ModuleTiming } from '#types';
|
|
2
|
+
import { formatDuration, getEffectiveTime } from '#profiler/reporters/format';
|
|
3
3
|
export interface ModuleNode {
|
|
4
4
|
timing: ModuleTiming;
|
|
5
5
|
displayName: string;
|
package/build/src/xray/tree.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
| Provides utilities for traversing and displaying the tree.
|
|
8
8
|
|
|
|
9
9
|
*/
|
|
10
|
-
import { formatDuration, getEffectiveTime, simplifyUrl } from '
|
|
11
|
-
import { fileIcons, symbols } from '
|
|
10
|
+
import { formatDuration, getEffectiveTime, simplifyUrl } from '#profiler/reporters/format';
|
|
11
|
+
import { fileIcons, symbols } from '#registries/index';
|
|
12
12
|
export function buildDependencyTree(modules, cwd) {
|
|
13
13
|
const nodeMap = new Map();
|
|
14
14
|
const time = (n) => getEffectiveTime(n.timing);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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.
|
|
4
|
+
"version": "0.1.1-beta.4",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=22.0.0"
|
|
7
7
|
},
|
|
8
8
|
"main": "./build/index.js",
|
|
9
9
|
"types": "./build/index.d.ts",
|
|
@@ -92,6 +92,12 @@
|
|
|
92
92
|
"optional": true
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
|
+
"imports": {
|
|
96
|
+
"#registries/*": "./build/src/registries/*.js",
|
|
97
|
+
"#types": "./build/src/types.js",
|
|
98
|
+
"#profiler/*": "./build/src/profiler/*.js",
|
|
99
|
+
"#xray/*": "./build/src/xray/*.js"
|
|
100
|
+
},
|
|
95
101
|
"publishConfig": {
|
|
96
102
|
"access": "public",
|
|
97
103
|
"tag": "latest"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|