@densetsuuu/docteur 0.1.1-beta
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/LICENSE.md +9 -0
- package/README.md +155 -0
- package/build/index.d.ts +23 -0
- package/build/index.js +23 -0
- package/build/src/cli/commands/diagnose.d.ts +27 -0
- package/build/src/cli/commands/diagnose.js +67 -0
- package/build/src/cli/commands/xray.d.ts +7 -0
- package/build/src/cli/commands/xray.js +49 -0
- package/build/src/cli/main.d.ts +2 -0
- package/build/src/cli/main.js +30 -0
- package/build/src/profiler/collector.d.ts +16 -0
- package/build/src/profiler/collector.js +142 -0
- package/build/src/profiler/hooks.d.ts +27 -0
- package/build/src/profiler/hooks.js +45 -0
- package/build/src/profiler/loader.d.ts +1 -0
- package/build/src/profiler/loader.js +111 -0
- package/build/src/profiler/profiler.d.ts +9 -0
- package/build/src/profiler/profiler.js +117 -0
- package/build/src/profiler/registries/categories.d.ts +7 -0
- package/build/src/profiler/registries/categories.js +87 -0
- package/build/src/profiler/registries/index.d.ts +2 -0
- package/build/src/profiler/registries/index.js +2 -0
- package/build/src/profiler/registries/symbols.d.ts +42 -0
- package/build/src/profiler/registries/symbols.js +71 -0
- package/build/src/profiler/reporters/base_reporter.d.ts +19 -0
- package/build/src/profiler/reporters/base_reporter.js +9 -0
- package/build/src/profiler/reporters/console_reporter.d.ts +8 -0
- package/build/src/profiler/reporters/console_reporter.js +237 -0
- package/build/src/profiler/reporters/format.d.ts +62 -0
- package/build/src/profiler/reporters/format.js +84 -0
- package/build/src/profiler/reporters/tui_reporter.d.ts +8 -0
- package/build/src/profiler/reporters/tui_reporter.js +32 -0
- package/build/src/types.d.ts +167 -0
- package/build/src/types.js +9 -0
- package/build/src/xray/components/ListView.d.ts +9 -0
- package/build/src/xray/components/ListView.js +69 -0
- package/build/src/xray/components/ModuleView.d.ts +9 -0
- package/build/src/xray/components/ModuleView.js +144 -0
- package/build/src/xray/components/ProviderListView.d.ts +8 -0
- package/build/src/xray/components/ProviderListView.js +57 -0
- package/build/src/xray/components/ProviderView.d.ts +7 -0
- package/build/src/xray/components/ProviderView.js +35 -0
- package/build/src/xray/components/XRayApp.d.ts +7 -0
- package/build/src/xray/components/XRayApp.js +78 -0
- package/build/src/xray/tree.d.ts +22 -0
- package/build/src/xray/tree.js +85 -0
- package/package.json +110 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Docteur
|
|
2
|
+
|
|
3
|
+
> AdonisJS cold start profiler - Analyze and optimize your application boot time
|
|
4
|
+
|
|
5
|
+
Docteur profiles your AdonisJS application's cold start performance. It measures how long each module takes to load and helps you identify bottlenecks in your boot process.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install globally:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @densetsuuu/docteur
|
|
13
|
+
# or
|
|
14
|
+
pnpm add -g @densetsuuu/docteur
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Navigate to your AdonisJS project and run:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Quick diagnosis
|
|
23
|
+
docteur diagnose
|
|
24
|
+
|
|
25
|
+
# Interactive explorer
|
|
26
|
+
docteur xray
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Commands
|
|
30
|
+
|
|
31
|
+
#### `docteur diagnose`
|
|
32
|
+
|
|
33
|
+
Analyzes cold start performance and displays a report.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
docteur diagnose [options]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Option | Description | Default |
|
|
40
|
+
| ------------------- | --------------------------------------- | --------------- |
|
|
41
|
+
| `--top` | Number of slowest modules to display | 20 |
|
|
42
|
+
| `--threshold` | Only show modules slower than this (ms) | 1 |
|
|
43
|
+
| `--no-node-modules` | Exclude node_modules from analysis | false |
|
|
44
|
+
| `--no-group` | Don't group modules by package | false |
|
|
45
|
+
| `--entry` | Custom entry point to profile | `bin/server.ts` |
|
|
46
|
+
|
|
47
|
+
#### `docteur xray`
|
|
48
|
+
|
|
49
|
+
Interactive TUI for exploring module dependencies.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
docteur xray [options]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Option | Description | Default |
|
|
56
|
+
| --------- | ------------------------- | --------------- |
|
|
57
|
+
| `--entry` | Custom entry point | `bin/server.ts` |
|
|
58
|
+
|
|
59
|
+
**Features:**
|
|
60
|
+
- Browse slowest modules
|
|
61
|
+
- Drill down into module dependencies
|
|
62
|
+
- See why a module was loaded (import chain)
|
|
63
|
+
- View lazy import recommendations for heavy dependencies
|
|
64
|
+
- Explore provider lifecycle times (register, boot, start, ready)
|
|
65
|
+
|
|
66
|
+
**Keyboard shortcuts:**
|
|
67
|
+
- `↑/↓` Navigate
|
|
68
|
+
- `Enter` Select
|
|
69
|
+
- `Tab` Switch between Modules/Providers view
|
|
70
|
+
- `←/Backspace/ESC` Go back
|
|
71
|
+
- `q` Quit
|
|
72
|
+
|
|
73
|
+
### Example Output
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
🩺 Docteur - Cold Start Analysis
|
|
77
|
+
─────────────────────────────────
|
|
78
|
+
|
|
79
|
+
📊 Summary
|
|
80
|
+
|
|
81
|
+
Total boot time: 459.26ms
|
|
82
|
+
Total modules loaded: 447
|
|
83
|
+
App modules: 19
|
|
84
|
+
Node modules: 221
|
|
85
|
+
AdonisJS modules: 186
|
|
86
|
+
Module import time: 72.91ms
|
|
87
|
+
Provider exec time: 12.35ms
|
|
88
|
+
|
|
89
|
+
📁 App Files by Category
|
|
90
|
+
|
|
91
|
+
🚀 Start Files (4 files, 15.23ms)
|
|
92
|
+
┌───────────────────────┬──────────┬──────────┬────────────────────────┐
|
|
93
|
+
│ routes.ts │ 10.53ms │ 0.19ms │ █████████████████████ │
|
|
94
|
+
│ kernel.ts │ 2.15ms │ 0.08ms │ ████░░░░░░░░░░░░░░░░░ │
|
|
95
|
+
│ env.ts │ 1.89ms │ 0.12ms │ ████░░░░░░░░░░░░░░░░░ │
|
|
96
|
+
└───────────────────────┴──────────┴──────────┴────────────────────────┘
|
|
97
|
+
|
|
98
|
+
⚡ Provider Execution Times
|
|
99
|
+
Time spent in register() and boot() methods
|
|
100
|
+
|
|
101
|
+
┌─────────────────────┬──────────┬──────────┬──────────┬────────────┐
|
|
102
|
+
│ Provider │ Register │ Boot │ Total │ │
|
|
103
|
+
├─────────────────────┼──────────┼──────────┼──────────┼────────────┤
|
|
104
|
+
│ EdgeServiceProvider │ 0.15ms │ 2.76ms │ 2.91ms │ ████████ │
|
|
105
|
+
│ HashServiceProvider │ 0.08ms │ 1.23ms │ 1.31ms │ ████ │
|
|
106
|
+
└─────────────────────┴──────────┴──────────┴──────────┴────────────┘
|
|
107
|
+
|
|
108
|
+
✅ No major issues detected!
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How It Works
|
|
112
|
+
|
|
113
|
+
Docteur measures cold start performance through two complementary approaches:
|
|
114
|
+
|
|
115
|
+
1. **Module Loading**: Uses Node.js ESM loader hooks to intercept every `import()` and measure how long each module takes to load.
|
|
116
|
+
|
|
117
|
+
2. **Provider Lifecycle**: Subscribes to AdonisJS's built-in tracing channels to measure the duration of provider lifecycle methods (register, boot, start, ready).
|
|
118
|
+
|
|
119
|
+
### Timing Columns
|
|
120
|
+
|
|
121
|
+
- **Total**: Time including all transitive dependencies (cascading impact)
|
|
122
|
+
- **Self**: Time for just that file (excluding dependencies)
|
|
123
|
+
|
|
124
|
+
### Execution Flow
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
1. docteur diagnose
|
|
128
|
+
│
|
|
129
|
+
▼
|
|
130
|
+
2. Spawns child process with --import loader.ts
|
|
131
|
+
│
|
|
132
|
+
▼
|
|
133
|
+
3. loader.ts registers ESM hooks + subscribes to tracing channels
|
|
134
|
+
│
|
|
135
|
+
▼
|
|
136
|
+
4. hooks.ts intercepts every import, measures load time
|
|
137
|
+
│
|
|
138
|
+
▼
|
|
139
|
+
5. AdonisJS app starts, HTTP server listens
|
|
140
|
+
│
|
|
141
|
+
▼
|
|
142
|
+
6. CLI detects "started HTTP server", requests results
|
|
143
|
+
│
|
|
144
|
+
▼
|
|
145
|
+
7. Results sent via IPC, report displayed
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- AdonisJS v7+
|
|
151
|
+
- Node.js 21+
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export types for consumers
|
|
3
|
+
*/
|
|
4
|
+
export type { ModuleTiming, ProviderTiming, ProfileResult, ProfileSummary, DocteurConfig, ResolvedConfig, AppFileCategory, AppFileGroup, } from './src/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Export profiler for programmatic usage
|
|
7
|
+
*/
|
|
8
|
+
export { profile, isAdonisProject, findEntryPoint } from './src/profiler/profiler.js';
|
|
9
|
+
/**
|
|
10
|
+
* Export collector class for advanced usage
|
|
11
|
+
*/
|
|
12
|
+
export { ProfileCollector, type PackageGroup } from './src/profiler/collector.js';
|
|
13
|
+
/**
|
|
14
|
+
* Export reporters for custom reporting
|
|
15
|
+
*/
|
|
16
|
+
export type { Reporter, ReportContext } from './src/profiler/reporters/base_reporter.js';
|
|
17
|
+
export { ConsoleReporter } from './src/profiler/reporters/console_reporter.js';
|
|
18
|
+
export { TuiReporter } from './src/profiler/reporters/tui_reporter.js';
|
|
19
|
+
export { colorDuration, createBar, formatDuration, getCategoryIcon, getEffectiveTime, simplifyUrl, ui, } from './src/profiler/reporters/format.js';
|
|
20
|
+
/**
|
|
21
|
+
* Export registries for customization
|
|
22
|
+
*/
|
|
23
|
+
export { categories, fileIcons, symbols, type CategoryDefinition, type SymbolKey, } from './src/profiler/registries/index.js';
|
package/build/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Package entrypoint
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Export values from the package entrypoint as you see fit.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Export profiler for programmatic usage
|
|
11
|
+
*/
|
|
12
|
+
export { profile, isAdonisProject, findEntryPoint } from './src/profiler/profiler.js';
|
|
13
|
+
/**
|
|
14
|
+
* Export collector class for advanced usage
|
|
15
|
+
*/
|
|
16
|
+
export { ProfileCollector } from './src/profiler/collector.js';
|
|
17
|
+
export { ConsoleReporter } from './src/profiler/reporters/console_reporter.js';
|
|
18
|
+
export { TuiReporter } from './src/profiler/reporters/tui_reporter.js';
|
|
19
|
+
export { colorDuration, createBar, formatDuration, getCategoryIcon, getEffectiveTime, simplifyUrl, ui, } from './src/profiler/reporters/format.js';
|
|
20
|
+
/**
|
|
21
|
+
* Export registries for customization
|
|
22
|
+
*/
|
|
23
|
+
export { categories, fileIcons, symbols, } from './src/profiler/registries/index.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const diagnoseCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly top: {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "Number of slowest modules to display";
|
|
5
|
+
readonly default: "20";
|
|
6
|
+
};
|
|
7
|
+
readonly threshold: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
readonly description: "Only show modules slower than this threshold (in ms)";
|
|
10
|
+
readonly default: "1";
|
|
11
|
+
};
|
|
12
|
+
readonly 'node-modules': {
|
|
13
|
+
readonly type: "boolean";
|
|
14
|
+
readonly description: "Include node_modules in the analysis";
|
|
15
|
+
readonly default: true;
|
|
16
|
+
};
|
|
17
|
+
readonly group: {
|
|
18
|
+
readonly type: "boolean";
|
|
19
|
+
readonly description: "Group modules by package name";
|
|
20
|
+
readonly default: true;
|
|
21
|
+
};
|
|
22
|
+
readonly entry: {
|
|
23
|
+
readonly type: "string";
|
|
24
|
+
readonly description: "Entry point to profile";
|
|
25
|
+
readonly default: "bin/server.ts";
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { isAdonisProject, profile } from '../../profiler/profiler.js';
|
|
3
|
+
import { ConsoleReporter } from '../../profiler/reporters/console_reporter.js';
|
|
4
|
+
import { ui } from '../../profiler/reporters/format.js';
|
|
5
|
+
export const diagnoseCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'diagnose',
|
|
8
|
+
description: 'Analyze cold start performance',
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
'top': {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Number of slowest modules to display',
|
|
14
|
+
default: '20',
|
|
15
|
+
},
|
|
16
|
+
'threshold': {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Only show modules slower than this threshold (in ms)',
|
|
19
|
+
default: '1',
|
|
20
|
+
},
|
|
21
|
+
'node-modules': {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
description: 'Include node_modules in the analysis',
|
|
24
|
+
default: true,
|
|
25
|
+
},
|
|
26
|
+
'group': {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description: 'Group modules by package name',
|
|
29
|
+
default: true,
|
|
30
|
+
},
|
|
31
|
+
'entry': {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Entry point to profile',
|
|
34
|
+
default: 'bin/server.ts',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async run({ args }) {
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
if (!isAdonisProject(cwd)) {
|
|
40
|
+
ui.logger.error('Not an AdonisJS project. Make sure adonisrc.ts exists.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
ui.logger.info('Starting cold start analysis...');
|
|
44
|
+
ui.logger.info(`Entry point: ${args.entry}`);
|
|
45
|
+
try {
|
|
46
|
+
const result = await profile(cwd, { entryPoint: args.entry });
|
|
47
|
+
const reporter = new ConsoleReporter();
|
|
48
|
+
reporter.render({
|
|
49
|
+
result,
|
|
50
|
+
cwd,
|
|
51
|
+
config: {
|
|
52
|
+
topModules: Number.parseInt(args.top, 10),
|
|
53
|
+
threshold: Number.parseInt(args.threshold, 10),
|
|
54
|
+
includeNodeModules: args['node-modules'],
|
|
55
|
+
groupByPackage: args.group,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
ui.logger.error('Profiling failed');
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
ui.logger.error(error.message);
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { isAdonisProject, profile } from '../../profiler/profiler.js';
|
|
3
|
+
import { TuiReporter } from '../../profiler/reporters/tui_reporter.js';
|
|
4
|
+
import { ui } from '../../profiler/reporters/format.js';
|
|
5
|
+
export const xrayCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'xray',
|
|
8
|
+
description: 'Interactive module dependency explorer',
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
entry: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'Entry point to profile',
|
|
14
|
+
default: 'bin/server.ts',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
if (!isAdonisProject(cwd)) {
|
|
20
|
+
ui.logger.error('Not an AdonisJS project. Make sure adonisrc.ts exists.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
ui.logger.info('Starting cold start analysis...');
|
|
24
|
+
try {
|
|
25
|
+
const result = await profile(cwd, {
|
|
26
|
+
entryPoint: args.entry,
|
|
27
|
+
suppressOutput: true,
|
|
28
|
+
});
|
|
29
|
+
const reporter = new TuiReporter();
|
|
30
|
+
await reporter.render({
|
|
31
|
+
result,
|
|
32
|
+
cwd,
|
|
33
|
+
config: {
|
|
34
|
+
topModules: 20,
|
|
35
|
+
threshold: 1,
|
|
36
|
+
includeNodeModules: true,
|
|
37
|
+
groupByPackage: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
ui.logger.error('Profiling failed');
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
ui.logger.error(error.message);
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
|--------------------------------------------------------------------------
|
|
4
|
+
| Docteur CLI
|
|
5
|
+
|--------------------------------------------------------------------------
|
|
6
|
+
|
|
|
7
|
+
| Standalone CLI for profiling AdonisJS cold start performance.
|
|
8
|
+
| Can be installed globally and run in any AdonisJS project.
|
|
9
|
+
|
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { defineCommand, runMain } from 'citty';
|
|
15
|
+
import { diagnoseCommand } from './commands/diagnose.js';
|
|
16
|
+
import { xrayCommand } from './commands/xray.js';
|
|
17
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
const main = defineCommand({
|
|
20
|
+
meta: {
|
|
21
|
+
name: 'docteur',
|
|
22
|
+
version: pkg.version,
|
|
23
|
+
description: pkg.description,
|
|
24
|
+
},
|
|
25
|
+
subCommands: {
|
|
26
|
+
diagnose: diagnoseCommand,
|
|
27
|
+
xray: xrayCommand,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
runMain(main);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AppFileGroup, ModuleTiming, ProfileResult, ProfileSummary, ProviderTiming, ResolvedConfig } from '../types.js';
|
|
2
|
+
export interface PackageGroup {
|
|
3
|
+
name: string;
|
|
4
|
+
totalTime: number;
|
|
5
|
+
modules: ModuleTiming[];
|
|
6
|
+
}
|
|
7
|
+
export declare class ProfileCollector {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(modules?: ModuleTiming[], providers?: ProviderTiming[]);
|
|
10
|
+
groupAppFilesByCategory(): AppFileGroup[];
|
|
11
|
+
groupModulesByPackage(): PackageGroup[];
|
|
12
|
+
computeSummary(): ProfileSummary;
|
|
13
|
+
filterModules(config: ResolvedConfig): ModuleTiming[];
|
|
14
|
+
getTopSlowest(count: number): ModuleTiming[];
|
|
15
|
+
collectResults(totalTime: number): ProfileResult;
|
|
16
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Profile Collector
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Collects and analyzes module timing data. Computes subtree times
|
|
7
|
+
| (cascading impact including dependencies) and groups modules by
|
|
8
|
+
| category or package.
|
|
9
|
+
|
|
|
10
|
+
*/
|
|
11
|
+
import { categories } from './registries/index.js';
|
|
12
|
+
export class ProfileCollector {
|
|
13
|
+
#modules;
|
|
14
|
+
#providers;
|
|
15
|
+
constructor(modules = [], providers = []) {
|
|
16
|
+
this.#modules = modules;
|
|
17
|
+
this.#providers = providers;
|
|
18
|
+
// Compute subtree times if not already done (skip after filtering to avoid incomplete graph)
|
|
19
|
+
if (modules.length > 0 && modules[0].subtreeTime === undefined) {
|
|
20
|
+
this.#populateSubtreeTimes();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
#populateSubtreeTimes() {
|
|
24
|
+
const byUrl = new Map(this.#modules.map((m) => [m.resolvedUrl, m]));
|
|
25
|
+
const children = new Map();
|
|
26
|
+
for (const m of this.#modules) {
|
|
27
|
+
if (m.parentUrl) {
|
|
28
|
+
const list = children.get(m.parentUrl) || [];
|
|
29
|
+
list.push(m.resolvedUrl);
|
|
30
|
+
children.set(m.parentUrl, list);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const compute = (url, seen) => {
|
|
34
|
+
if (seen.has(url))
|
|
35
|
+
return 0;
|
|
36
|
+
seen.add(url);
|
|
37
|
+
const mod = byUrl.get(url);
|
|
38
|
+
if (!mod)
|
|
39
|
+
return 0;
|
|
40
|
+
let total = mod.loadTime;
|
|
41
|
+
for (const child of children.get(url) || []) {
|
|
42
|
+
total += compute(child, seen);
|
|
43
|
+
}
|
|
44
|
+
return total;
|
|
45
|
+
};
|
|
46
|
+
for (const m of this.#modules) {
|
|
47
|
+
m.subtreeTime = compute(m.resolvedUrl, new Set());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
#time(m) {
|
|
51
|
+
return m.subtreeTime ?? m.loadTime;
|
|
52
|
+
}
|
|
53
|
+
#sumTime(modules) {
|
|
54
|
+
return modules.reduce((sum, m) => sum + this.#time(m), 0);
|
|
55
|
+
}
|
|
56
|
+
#sortByTime(modules) {
|
|
57
|
+
return [...modules].sort((a, b) => this.#time(b) - this.#time(a));
|
|
58
|
+
}
|
|
59
|
+
#moduleCategory(url) {
|
|
60
|
+
if (url.startsWith('node:'))
|
|
61
|
+
return 'node';
|
|
62
|
+
if (url.includes('node_modules/@adonisjs/'))
|
|
63
|
+
return 'adonis';
|
|
64
|
+
if (url.includes('node_modules/'))
|
|
65
|
+
return 'node_modules';
|
|
66
|
+
return 'user';
|
|
67
|
+
}
|
|
68
|
+
#appFileCategory(url) {
|
|
69
|
+
const path = url.toLowerCase();
|
|
70
|
+
for (const [cat, def] of Object.entries(categories)) {
|
|
71
|
+
if (def.patterns.some((p) => path.includes(p))) {
|
|
72
|
+
return cat;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 'other';
|
|
76
|
+
}
|
|
77
|
+
#packageName(url) {
|
|
78
|
+
const match = url.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
|
|
79
|
+
return match?.[1] || 'app';
|
|
80
|
+
}
|
|
81
|
+
groupAppFilesByCategory() {
|
|
82
|
+
const userModules = this.#modules.filter((m) => this.#moduleCategory(m.resolvedUrl) === 'user');
|
|
83
|
+
const grouped = Object.groupBy(userModules, (m) => this.#appFileCategory(m.resolvedUrl));
|
|
84
|
+
return Object.entries(grouped)
|
|
85
|
+
.filter((e) => e[1] !== undefined)
|
|
86
|
+
.map(([category, files]) => ({
|
|
87
|
+
category,
|
|
88
|
+
displayName: categories[category].displayName,
|
|
89
|
+
files: this.#sortByTime(files),
|
|
90
|
+
totalTime: this.#sumTime(files),
|
|
91
|
+
}))
|
|
92
|
+
.sort((a, b) => b.totalTime - a.totalTime);
|
|
93
|
+
}
|
|
94
|
+
groupModulesByPackage() {
|
|
95
|
+
const grouped = Object.groupBy(this.#modules, (m) => this.#packageName(m.resolvedUrl));
|
|
96
|
+
return Object.entries(grouped)
|
|
97
|
+
.filter((e) => e[1] !== undefined)
|
|
98
|
+
.map(([name, mods]) => ({
|
|
99
|
+
name,
|
|
100
|
+
totalTime: this.#sumTime(mods),
|
|
101
|
+
modules: this.#sortByTime(mods),
|
|
102
|
+
}))
|
|
103
|
+
.sort((a, b) => b.totalTime - a.totalTime);
|
|
104
|
+
}
|
|
105
|
+
computeSummary() {
|
|
106
|
+
const grouped = Object.groupBy(this.#modules, (m) => this.#moduleCategory(m.resolvedUrl));
|
|
107
|
+
return {
|
|
108
|
+
totalModules: this.#modules.length,
|
|
109
|
+
userModules: grouped.user?.length ?? 0,
|
|
110
|
+
nodeModules: grouped.node_modules?.length ?? 0,
|
|
111
|
+
adonisModules: grouped.adonis?.length ?? 0,
|
|
112
|
+
totalModuleTime: this.#sumTime(this.#modules),
|
|
113
|
+
totalProviderTime: this.#providers.reduce((sum, p) => sum + p.totalTime, 0),
|
|
114
|
+
appFileGroups: this.groupAppFilesByCategory(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
filterModules(config) {
|
|
118
|
+
return this.#modules.filter((m) => {
|
|
119
|
+
if (this.#time(m) < config.threshold)
|
|
120
|
+
return false;
|
|
121
|
+
if (m.resolvedUrl.startsWith('node:'))
|
|
122
|
+
return false;
|
|
123
|
+
if (!config.includeNodeModules) {
|
|
124
|
+
const cat = this.#moduleCategory(m.resolvedUrl);
|
|
125
|
+
if (cat === 'node_modules' || cat === 'adonis')
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
getTopSlowest(count) {
|
|
132
|
+
return this.#sortByTime(this.#modules).slice(0, count);
|
|
133
|
+
}
|
|
134
|
+
collectResults(totalTime) {
|
|
135
|
+
return {
|
|
136
|
+
totalTime,
|
|
137
|
+
modules: this.#modules,
|
|
138
|
+
providers: this.#providers,
|
|
139
|
+
summary: this.computeSummary(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MessagePort } from 'node:worker_threads';
|
|
2
|
+
type ResolveFn = (specifier: string, context?: {
|
|
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: {
|
|
14
|
+
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
|
+
}>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| ESM Loader Hooks
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Tracks module loading times and parent-child relationships.
|
|
7
|
+
| - resolve: captures which module imported which (for subtree calculation)
|
|
8
|
+
| - load: measures how long each module takes to load
|
|
9
|
+
|
|
|
10
|
+
*/
|
|
11
|
+
import { performance } from 'node:perf_hooks';
|
|
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
|
+
export function initialize(data) {
|
|
26
|
+
port = data.port;
|
|
27
|
+
}
|
|
28
|
+
export async function resolve(specifier, context, next) {
|
|
29
|
+
const result = await next(specifier, context);
|
|
30
|
+
if (context.parentURL && result.url.startsWith('file://')) {
|
|
31
|
+
send({ type: 'parent', child: result.url, parent: context.parentURL });
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export async function load(url, context, next) {
|
|
36
|
+
if (!url.startsWith('file://')) {
|
|
37
|
+
return next(url, context);
|
|
38
|
+
}
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
const result = await next(url, context);
|
|
41
|
+
if (result.format === 'module') {
|
|
42
|
+
send({ type: 'timing', url, loadTime: performance.now() - start });
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|