@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.
Files changed (47) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +155 -0
  3. package/build/index.d.ts +23 -0
  4. package/build/index.js +23 -0
  5. package/build/src/cli/commands/diagnose.d.ts +27 -0
  6. package/build/src/cli/commands/diagnose.js +67 -0
  7. package/build/src/cli/commands/xray.d.ts +7 -0
  8. package/build/src/cli/commands/xray.js +49 -0
  9. package/build/src/cli/main.d.ts +2 -0
  10. package/build/src/cli/main.js +30 -0
  11. package/build/src/profiler/collector.d.ts +16 -0
  12. package/build/src/profiler/collector.js +142 -0
  13. package/build/src/profiler/hooks.d.ts +27 -0
  14. package/build/src/profiler/hooks.js +45 -0
  15. package/build/src/profiler/loader.d.ts +1 -0
  16. package/build/src/profiler/loader.js +111 -0
  17. package/build/src/profiler/profiler.d.ts +9 -0
  18. package/build/src/profiler/profiler.js +117 -0
  19. package/build/src/profiler/registries/categories.d.ts +7 -0
  20. package/build/src/profiler/registries/categories.js +87 -0
  21. package/build/src/profiler/registries/index.d.ts +2 -0
  22. package/build/src/profiler/registries/index.js +2 -0
  23. package/build/src/profiler/registries/symbols.d.ts +42 -0
  24. package/build/src/profiler/registries/symbols.js +71 -0
  25. package/build/src/profiler/reporters/base_reporter.d.ts +19 -0
  26. package/build/src/profiler/reporters/base_reporter.js +9 -0
  27. package/build/src/profiler/reporters/console_reporter.d.ts +8 -0
  28. package/build/src/profiler/reporters/console_reporter.js +237 -0
  29. package/build/src/profiler/reporters/format.d.ts +62 -0
  30. package/build/src/profiler/reporters/format.js +84 -0
  31. package/build/src/profiler/reporters/tui_reporter.d.ts +8 -0
  32. package/build/src/profiler/reporters/tui_reporter.js +32 -0
  33. package/build/src/types.d.ts +167 -0
  34. package/build/src/types.js +9 -0
  35. package/build/src/xray/components/ListView.d.ts +9 -0
  36. package/build/src/xray/components/ListView.js +69 -0
  37. package/build/src/xray/components/ModuleView.d.ts +9 -0
  38. package/build/src/xray/components/ModuleView.js +144 -0
  39. package/build/src/xray/components/ProviderListView.d.ts +8 -0
  40. package/build/src/xray/components/ProviderListView.js +57 -0
  41. package/build/src/xray/components/ProviderView.d.ts +7 -0
  42. package/build/src/xray/components/ProviderView.js +35 -0
  43. package/build/src/xray/components/XRayApp.d.ts +7 -0
  44. package/build/src/xray/components/XRayApp.js +78 -0
  45. package/build/src/xray/tree.d.ts +22 -0
  46. package/build/src/xray/tree.js +85 -0
  47. 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
@@ -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,7 @@
1
+ export declare const xrayCommand: import("citty").CommandDef<{
2
+ readonly entry: {
3
+ readonly type: "string";
4
+ readonly description: "Entry point to profile";
5
+ readonly default: "bin/server.ts";
6
+ };
7
+ }>;
@@ -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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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 {};