1import * as NodeFS from "node:fs";
2import * as NodeModule from "node:module";
3import * as NodePerfHooks from "node:perf_hooks";
4import * as NodeURL from "node:url";
5
6export function profileStartupModules(session, events, threadId) {
7 const now = () => NodePerfHooks.performance.now();
8 const origin = NodePerfHooks.performance.timeOrigin;
9 const loaded = new Map();
10 const compiling = new Map();
11 const evaluating = new Map();
12 const registryKey = "__t3codeStartupModuleProfiler";
13 const previousRegistry = globalThis[registryKey];
14 const previousModuleRegistry = NodeModule.default[registryKey];
15 const restorers = [];
16 let active = true;
17 let installed = false;
18 let breakpoint;
19 let currentLink;
20 let nextLink = 0;
21
22 const record = (stage, url, start, end, attributes = {}) => {
23 if (!active) return;
24 events.push({
25 name: `${stage} ${url}`,
26 cat: "module.detail",
27 ph: "X",
28 pid: process.pid,
29 tid: threadId,
30 ts: (origin + start) * 1000,
31 dur: (end - start) * 1000,
32 args: { stage, url, ...attributes },
33 });
34 };
35 const toUrl = (file) =>
36 file instanceof URL
37 ? file.href
38 : typeof file === "string"
39 ? file.startsWith("file:")
40 ? file
41 : NodeURL.pathToFileURL(file).href
42 : undefined;
43 const inject = (source, statement, preserveDirectives) => {
44 let offset = source.startsWith("#!") ? source.indexOf("\n") + 1 : 0;
45 if (preserveDirectives) {
46 for (;;) {
47 const trivia = /^(?:\s+|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/.exec(
48 source.slice(offset),
49 )[0];
50 offset += trivia.length;
51 if (!/^["']/.test(source.slice(offset))) break;
52 const directive = /^(?:"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*')\s*;/.exec(
53 source.slice(offset),
54 );
55 // Leave ambiguous semicolon-free prologues untouched rather than changing strict-mode semantics.
56 if (!directive) return undefined;
57 offset += directive[0].length;
58 }
59 }
60 return source.slice(0, offset) + statement + source.slice(offset);
61 };
62 const registry = {
63 begin(url) {
64 if (active) evaluating.set(url, now());
65 },
66 end(url) {
67 const start = evaluating.get(url);
68 if (start === undefined) return;
69 record("evaluate", url, start, now(), {
70 format: "esm",
71 boundary: "module body; top-level await included",
72 });
73 evaluating.delete(url);
74 },
75 cjsBegin(filename) {
76 const url = toUrl(filename),
77 pending = compiling.get(url);
78 if (!pending) return;
79 pending.evaluationStart = now();
80 record("prepare", url, pending.start, pending.evaluationStart, {
81 format: "commonjs",
82 boundary: "_compile entry to module-body entry",
83 });
84 },
85 install(job) {
86 if (installed) return;
87 installed = true;
88 const loader = Object.getPrototypeOf(job.loader);
89 for (const method of [
90 "loadAndTranslate",
91 "loadAndTranslateForRequireInImportedCJS",
92 "loadAndTranslateForImportInRequiredESM",
93 ]) {
94 const original = loader[method];
95 if (typeof original !== "function") continue;
96 loader[method] = function (url, ...args) {
97 const start = now();
98 try {
99 return original.call(this, url, ...args);
100 } finally {
101 const moduleUrl = typeof url === "string" ? url : url.url;
102 const pending = loaded.get(moduleUrl);
103 if (pending && pending.start >= start)
104 record("prepare", moduleUrl, pending.start, now(), {
105 format: pending.format,
106 boundary:
107 "source-ready to loadAndTranslate return; compilation and format preparation",
108 });
109 }
110 };
111 restorers.push(() => {
112 loader[method] = original;
113 });
114 }
115 const prototype = Object.getPrototypeOf(Object.getPrototypeOf(job)),
116 link = prototype.syncLink;
117 prototype.syncLink = function (...args) {
118 const start = now(),
119 parent = currentLink,
120 id = ++nextLink;
121 currentLink = { id, url: this.url };
122 try {
123 return link.apply(this, args);
124 } finally {
125 record("link", this.url, start, now(), {
126 id,
127 parentId: parent?.id,
128 importedBy: parent?.url,
129 boundary: "ModuleJob.syncLink; includes recursive dependency loading",
130 });
131 currentLink = parent;
132 }
133 };
134 restorers.push(() => {
135 prototype.syncLink = link;
136 });
137 },
138 };
139 globalThis[registryKey] = registry;
140 NodeModule.default[registryKey] = registry;
141
142 session.post("Debugger.enable");
143 // One pause obtains the live loader; subsequent measurements use synchronous method boundaries.
144 const source = process.binding("natives")["internal/modules/esm/module_job"].split("\n");
145 const classLine = source.findIndex((line) => line.includes("class ModuleJob extends"));
146 const lineNumber = source.findIndex(
147 (line, index) => index > classLine && line.includes("this.linked = this.link("),
148 );
149 const paused = ({ params }) => {
150 if (!breakpoint || !params.hitBreakpoints?.includes(breakpoint)) return;
151 const start = now();
152 session.post("Debugger.removeBreakpoint", { breakpointId: breakpoint });
153 breakpoint = undefined;
154 session.post(
155 "Debugger.evaluateOnCallFrame",
156 {
157 callFrameId: params.callFrames[0].callFrameId,
158 expression: `globalThis.${registryKey}.install(this)`,
159 silent: true,
160 },
161 (error, result) => {
162 if (error || result?.exceptionDetails)
163 record("instrumentation-error", "node:esm-loader", start, now());
164 },
165 );
166 record("instrumentation-setup", "node:esm-loader", start, now());
167 session.post("Debugger.resume");
168 };
169 session.on("Debugger.paused", paused);
170 if (lineNumber >= 0)
171 session.post(
172 "Debugger.setBreakpointByUrl",
173 {
174 url: "node:internal/modules/esm/module_job",
175 lineNumber,
176 },
177 (error, result) => {
178 if (!error) breakpoint = result.breakpointId;
179 },
180 );
181
182 const readFileSync = NodeFS.default.readFileSync;
183 NodeFS.default.readFileSync = function (file, ...args) {
184 const url = toUrl(file);
185 if (!url || !/\.[cm]?[jt]sx?(?:$|\?)/.test(url)) return readFileSync.call(this, file, ...args);
186 const start = now();
187 try {
188 return readFileSync.call(this, file, ...args);
189 } finally {
190 record("read", url, start, now());
191 }
192 };
193 const compile = NodeModule.default.prototype._compile;
194 NodeModule.default.prototype._compile = function (source, filename, ...args) {
195 const url = toUrl(filename),
196 pending = { start: now() };
197 const format = args[0] ?? loaded.get(url)?.format;
198 const isEsm =
199 format === "module" ||
200 format === "module-typescript" ||
201 /(^|[;\n])\s*(?:import[\s.]|export\s)/.test(source);
202 const annotated = isEsm
203 ? undefined
204 : inject(source, `module.constructor.${registryKey}?.cjsBegin(__filename);`, true);
205 compiling.set(url, pending);
206 try {
207 return compile.call(this, annotated ?? source, filename, ...args);
208 } finally {
209 const end = now();
210 record(isEsm ? "require-esm" : "commonjs", url, pending.start, end, {
211 importedBy: this.parent?.filename ? toUrl(this.parent.filename) : undefined,
212 sourceBytes: Buffer.byteLength(source),
213 });
214 if (pending.evaluationStart !== undefined)
215 record("evaluate", url, pending.evaluationStart, end, {
216 format: "commonjs",
217 includesNestedRequires: true,
218 });
219 compiling.delete(url);
220 }
221 };
222 NodeModule.syncBuiltinESMExports();
223
224 return {
225 sourceReady(url, result) {
226 if (!url.startsWith("file:")) return result;
227 const format = result.format;
228 let source = result.source;
229 if (typeof source !== "string" && ArrayBuffer.isView(source))
230 source = Buffer.from(source.buffer, source.byteOffset, source.byteLength).toString("utf8");
231 if (typeof source === "string" && (format === "module" || format === "module-typescript")) {
232 // A module-local globalThis binding would put the injected access in its temporal dead zone.
233 if (!/\b(?:const|let|var|class|function|as)\s+globalThis\b/.test(source)) {
234 source =
235 inject(source, `globalThis.${registryKey}?.begin(import.meta.url);`, false) +
236 `\n;globalThis.${registryKey}?.end(import.meta.url);\n`;
237 result = { ...result, source };
238 }
239 }
240 loaded.set(url, { start: now(), format });
241 return result;
242 },
243 finish() {
244 const end = now();
245 for (const [url, start] of evaluating)
246 record("evaluate", url, start, end, { incomplete: true });
247 record("instrumentation-status", "node:esm-loader", end, end, { installed });
248 active = false;
249 if (breakpoint) session.post("Debugger.removeBreakpoint", { breakpointId: breakpoint });
250 session.off("Debugger.paused", paused);
251 session.post("Debugger.disable");
252 for (const restore of restorers.reverse()) restore();
253 NodeFS.default.readFileSync = readFileSync;
254 NodeModule.default.prototype._compile = compile;
255 NodeModule.syncBuiltinESMExports();
256 if (previousRegistry === undefined) delete globalThis[registryKey];
257 else globalThis[registryKey] = previousRegistry;
258 if (previousModuleRegistry === undefined) delete NodeModule.default[registryKey];
259 else NodeModule.default[registryKey] = previousModuleRegistry;
260 },
261 };
262}