1// Opt in with T3CODE_STARTUP_PROFILE_DIR=/absolute/path NODE_OPTIONS="--no-node-snapshot --import=/absolute/path/to/this/file".
2import * as NodeFS from "node:fs";
3import * as NodeInspector from "node:inspector";
4import * as NodeChildProcess from "node:child_process";
5import * as NodeModule from "node:module";
6import * as NodePath from "node:path";
7import * as NodePerfHooks from "node:perf_hooks";
8import * as NodeWorkerThreads from "node:worker_threads";
9import * as NodeURL from "node:url";
10
11import { profileStartupModules } from "./profile-startup-modules.mjs";
12
13const directory = process.env.T3CODE_STARTUP_PROFILE_DIR;
14const isLauncher = process.argv[1] === NodeURL.fileURLToPath(import.meta.url);
15if (directory && !isLauncher) {
16 NodeFS.mkdirSync(directory, { recursive: true });
17 const { threadId } = NodeWorkerThreads;
18 const prefix = NodePath.join(directory, `${process.pid}-${threadId}`);
19 const origin = NodePerfHooks.performance.timeOrigin;
20 const events = [];
21 const session = new NodeInspector.Session();
22 session.connect();
23 session.post("Profiler.setSamplingInterval", { interval: 1000 });
24 session.post("Profiler.enable");
25 const profileStartTimeUnixMs = origin + NodePerfHooks.performance.now();
26 session.post("Profiler.start");
27
28 const moduleDetails = profileStartupModules(session, events, threadId);
29
30 const measure = (name, operation, cat = "module", attributes = {}) => {
31 const start = NodePerfHooks.performance.now();
32 try {
33 return operation();
34 } finally {
35 events.push({
36 name,
37 cat,
38 ph: "X",
39 pid: process.pid,
40 tid: threadId,
41 ts: (origin + start) * 1000,
42 dur: (NodePerfHooks.performance.now() - start) * 1000,
43 args: attributes,
44 });
45 }
46 };
47
48 const recordMeasures = (entries) => {
49 for (const entry of entries) {
50 events.push({
51 name: entry.name,
52 cat: "startup",
53 ph: "X",
54 pid: process.pid,
55 tid: threadId,
56 ts: (origin + entry.startTime) * 1000,
57 dur: entry.duration * 1000,
58 });
59 }
60 };
61 const observer = new NodePerfHooks.PerformanceObserver((list) =>
62 recordMeasures(list.getEntries()),
63 );
64 observer.observe({ entryTypes: ["measure"] });
65 const execFileSync = NodeChildProcess.default.execFileSync;
66 NodeChildProcess.default.execFileSync = function (file, ...args) {
67 return measure(
68 `execFileSync ${file}`,
69 () => execFileSync.call(this, file, ...args),
70 "subprocess",
71 );
72 };
73 NodeModule.syncBuiltinESMExports();
74
75 const hooks = NodeModule.registerHooks({
76 resolve(specifier, context, next) {
77 const attributes = { specifier, parentURL: context.parentURL };
78 return measure(
79 `resolve ${specifier}`,
80 () => {
81 const result = next(specifier, context);
82 attributes.url = result.url;
83 attributes.format = result.format;
84 return result;
85 },
86 "module",
87 attributes,
88 );
89 },
90 load(url, context, next) {
91 const result = measure(`load ${url}`, () => next(url, context), "module", {
92 url,
93 format: context.format,
94 });
95 return moduleDetails.sourceReady(url, result);
96 },
97 });
98
99 let finished = false;
100 const finish = () => {
101 if (finished) return;
102 finished = true;
103 hooks.deregister();
104 moduleDetails.finish();
105 recordMeasures(observer.takeRecords());
106 observer.disconnect();
107 NodeChildProcess.default.execFileSync = execFileSync;
108 NodeModule.syncBuiltinESMExports();
109 session.post("Profiler.stop", (error, result) => {
110 if (!error) NodeFS.writeFileSync(`${prefix}.cpuprofile`, JSON.stringify(result.profile));
111 session.disconnect();
112 });
113 NodeFS.writeFileSync(`${prefix}.modules.json`, JSON.stringify({ traceEvents: events }));
114 NodeFS.writeFileSync(
115 `${prefix}.process.json`,
116 JSON.stringify({
117 pid: process.pid,
118 parentPid: process.ppid,
119 threadId,
120 nodeVersion: process.version,
121 executable: process.execPath,
122 entry: process.argv[1],
123 timeOrigin: origin,
124 profileStartTimeUnixMs,
125 captureEnd: origin + NodePerfHooks.performance.now(),
126 cpu: process.cpuUsage(),
127 memory: process.memoryUsage(),
128 }),
129 );
130 };
131 // Bound the capture without keeping dev servers or short-lived helpers alive.
132 setTimeout(finish, 20_000).unref();
133 process.once("exit", finish);
134}
135
136if (isLauncher) {
137 const output = NodePath.resolve(
138 directory ??
139 NodePath.join(".t3/startup-profiles", new Date().toISOString().replaceAll(":", "-")),
140 );
141 console.log(`[profile-startup] Writing CPU profiles and timeline events to ${output}`);
142 const child = NodeChildProcess.spawn(
143 process.execPath,
144 ["scripts/dev-runner.ts", "dev", ...process.argv.slice(2)],
145 {
146 stdio: "inherit",
147 env: {
148 ...process.env,
149 T3CODE_STARTUP_PROFILE_DIR: output,
150 // Node 26's snapshotted loader cannot accept the debugger breakpoint used by this profiler.
151 NODE_OPTIONS:
152 `${process.env.NODE_OPTIONS ?? ""} --no-node-snapshot --import=${import.meta.url}`.trim(),
153 },
154 },
155 );
156 process.on("SIGINT", () => child.kill("SIGINT"));
157 process.on("SIGTERM", () => child.kill("SIGTERM"));
158 child.on("error", (error) => {
159 console.error(error);
160 process.exitCode = 1;
161 });
162 child.on("exit", (code) => {
163 process.exitCode = code ?? 1;
164 });
165}