• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Tracing events for the compiler. */
2 
3 /*@internal*/
4 namespace ts { // eslint-disable-line one-namespace-per-file
5     // should be used as tracing?.___
6     export let tracing: typeof tracingEnabled | undefined;
7     // enable the above using startTracing()
8 }
9 
10 // `tracingEnabled` should never be used directly, only through the above
11 /* @internal */
12 namespace ts.tracingEnabled { // eslint-disable-line one-namespace-per-file
13     export const enum Mode {
14         Project,
15         Build,
16         Server,
17     }
18 
19     let fs: typeof import("fs");
20 
21     let traceCount = 0;
22     let traceFd = 0;
23 
24     let mode: Mode;
25 
26     let legendPath: string | undefined;
27     const legend: TraceRecord[] = [];
28 
29     // The actual constraint is that JSON.stringify be able to serialize it without throwing.
30     interface Args {
31         [key: string]: string | number | boolean | null | undefined | Args | readonly (string | number | boolean | null | undefined | Args)[];
32     };
33 
34     /** Starts tracing for the given project. */
35     export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) {
36         Debug.assert(!tracing, "Tracing already started");
37 
38         if (fs === undefined) {
39             try {
40                 fs = require("fs");
41             }
42             catch (e) {
43                 throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`);
44             }
45         }
46 
47         mode = tracingMode;
48 
49         if (legendPath === undefined) {
50             legendPath = combinePaths(traceDir, "legend.json");
51         }
52 
53         // Note that writing will fail later on if it exists and is not a directory
54         if (!fs.existsSync(traceDir)) {
55             fs.mkdirSync(traceDir, { recursive: true });
56         }
57 
58         const countPart =
59             mode === Mode.Build ? `.${process.pid}-${++traceCount}`
60             : mode === Mode.Server ? `.${process.pid}`
61             : ``;
62         const tracePath = combinePaths(traceDir, `trace${countPart}.json`);
63         const typesPath = combinePaths(traceDir, `types${countPart}.json`);
64 
65         legend.push({
66             configFilePath,
67             tracePath,
68             typesPath,
69         });
70 
71         traceFd = fs.openSync(tracePath, "w");
72         tracing = tracingEnabled; // only when traceFd is properly set
73 
74         // Start with a prefix that contains some metadata that the devtools profiler expects (also avoids a warning on import)
75         const meta = { cat: "__metadata", ph: "M", ts: 1000 * timestamp(), pid: 1, tid: 1 };
76         fs.writeSync(traceFd,
77             "[\n"
78             + [{ name: "process_name", args: { name: "tsc" }, ...meta },
79             { name: "thread_name", args: { name: "Main" }, ...meta },
80             { name: "TracingStartedInBrowser", ...meta, cat: "disabled-by-default-devtools.timeline" }]
81                 .map(v => JSON.stringify(v)).join(",\n"));
82     }
83 
84     /** Stops tracing for the in-progress project and dumps the type catalog. */
85     export function stopTracing(typeCatalog?: readonly Type[]) {
86         Debug.assert(tracing, "Tracing is not in progress");
87         Debug.assert(!!typeCatalog === (mode !== Mode.Server)); // Have a type catalog iff not in server mode
88 
89         fs.writeSync(traceFd, `\n]\n`);
90         fs.closeSync(traceFd);
91         tracing = undefined;
92 
93         if (typeCatalog) {
94             dumpTypes(typeCatalog);
95         }
96         else {
97             // We pre-computed this path for convenience, but clear it
98             // now that the file won't be created.
99             legend[legend.length - 1].typesPath = undefined;
100         }
101     }
102 
103     export const enum Phase {
104         Parse = "parse",
105         Program = "program",
106         Bind = "bind",
107         Check = "check", // Before we get into checking types (e.g. checkSourceFile)
108         CheckTypes = "checkTypes",
109         Emit = "emit",
110         Session = "session",
111     }
112 
113     export function instant(phase: Phase, name: string, args?: Args) {
114         writeEvent("I", phase, name, args, `"s":"g"`);
115     }
116 
117     const eventStack: { phase: Phase, name: string, args?: Args, time: number, separateBeginAndEnd: boolean }[] = [];
118 
119     /**
120      * @param separateBeginAndEnd - used for special cases where we need the trace point even if the event
121      * never terminates (typically for reducing a scenario too big to trace to one that can be completed).
122      * In the future we might implement an exit handler to dump unfinished events which would deprecate
123      * these operations.
124      */
125     export function push(phase: Phase, name: string, args?: Args, separateBeginAndEnd = false) {
126         if (separateBeginAndEnd) {
127             writeEvent("B", phase, name, args);
128         }
129         eventStack.push({ phase, name, args, time: 1000 * timestamp(), separateBeginAndEnd });
130     }
131     export function pop() {
132         Debug.assert(eventStack.length > 0);
133         writeStackEvent(eventStack.length - 1, 1000 * timestamp());
134         eventStack.length--;
135     }
136     export function popAll() {
137         const endTime = 1000 * timestamp();
138         for (let i = eventStack.length - 1; i >= 0; i--) {
139             writeStackEvent(i, endTime);
140         }
141         eventStack.length = 0;
142     }
143     // sample every 10ms
144     const sampleInterval = 1000 * 10;
145     function writeStackEvent(index: number, endTime: number) {
146         const { phase, name, args, time, separateBeginAndEnd } = eventStack[index];
147         if (separateBeginAndEnd) {
148             writeEvent("E", phase, name, args, /*extras*/ undefined, endTime);
149         }
150         // test if [time,endTime) straddles a sampling point
151         else if (sampleInterval - (time % sampleInterval) <= endTime - time) {
152             writeEvent("X", phase, name, args, `"dur":${endTime - time}`, time);
153         }
154     }
155 
156     function writeEvent(eventType: string, phase: Phase, name: string, args: Args | undefined, extras?: string,
157         time: number = 1000 * timestamp()) {
158 
159         // In server mode, there's no easy way to dump type information, so we drop events that would require it.
160         if (mode === Mode.Server && phase === Phase.CheckTypes) return;
161 
162         performance.mark("beginTracing");
163         fs.writeSync(traceFd, `,\n{"pid":1,"tid":1,"ph":"${eventType}","cat":"${phase}","ts":${time},"name":"${name}"`);
164         if (extras) fs.writeSync(traceFd, `,${extras}`);
165         if (args) fs.writeSync(traceFd, `,"args":${JSON.stringify(args)}`);
166         fs.writeSync(traceFd, `}`);
167         performance.mark("endTracing");
168         performance.measure("Tracing", "beginTracing", "endTracing");
169     }
170 
171     function indexFromOne(lc: LineAndCharacter): LineAndCharacter {
172         return {
173             line: lc.line + 1,
174             character: lc.character + 1,
175         };
176     }
177 
178     function dumpTypes(types: readonly Type[]) {
179         performance.mark("beginDumpTypes");
180 
181         const typesPath = legend[legend.length - 1].typesPath!;
182         const typesFd = fs.openSync(typesPath, "w");
183 
184         const recursionIdentityMap = new Map<object, number>();
185 
186         // Cleverness: no line break here so that the type ID will match the line number
187         fs.writeSync(typesFd, "[");
188 
189         const numTypes = types.length;
190         for (let i = 0; i < numTypes; i++) {
191             const type = types[i];
192             const objectFlags = (type as any).objectFlags;
193             const symbol = type.aliasSymbol ?? type.symbol;
194             const firstDeclaration = symbol?.declarations?.[0];
195             const firstFile = firstDeclaration && getSourceFileOfNode(firstDeclaration);
196 
197             // It's slow to compute the display text, so skip it unless it's really valuable (or cheap)
198             let display: string | undefined;
199             if ((objectFlags & ObjectFlags.Anonymous) | (type.flags & TypeFlags.Literal)) {
200                 try {
201                     display = type.checker?.typeToString(type);
202                 }
203                 catch {
204                     display = undefined;
205                 }
206             }
207 
208             let indexedAccessProperties: object = {};
209             if (type.flags & TypeFlags.IndexedAccess) {
210                 const indexedAccessType = type as IndexedAccessType;
211                 indexedAccessProperties = {
212                     indexedAccessObjectType: indexedAccessType.objectType?.id,
213                     indexedAccessIndexType: indexedAccessType.indexType?.id,
214                 };
215             }
216 
217             let referenceProperties: object = {};
218             if (objectFlags & ObjectFlags.Reference) {
219                 const referenceType = type as TypeReference;
220                 referenceProperties = {
221                     instantiatedType: referenceType.target?.id,
222                     typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id),
223                 };
224             }
225 
226             let conditionalProperties: object = {};
227             if (type.flags & TypeFlags.Conditional) {
228                 const conditionalType = type as ConditionalType;
229                 conditionalProperties = {
230                     conditionalCheckType: conditionalType.checkType?.id,
231                     conditionalExtendsType: conditionalType.extendsType?.id,
232                     conditionalTrueType: conditionalType.resolvedTrueType?.id ?? -1,
233                     conditionalFalseType: conditionalType.resolvedFalseType?.id ?? -1,
234                 };
235             }
236 
237             // We can't print out an arbitrary object, so just assign each one a unique number.
238             // Don't call it an "id" so people don't treat it as a type id.
239             let recursionToken: number | undefined;
240             const recursionIdentity = type.checker.getRecursionIdentity(type);
241             if (recursionIdentity) {
242                 recursionToken = recursionIdentityMap.get(recursionIdentity);
243                 if (!recursionToken) {
244                     recursionToken = recursionIdentityMap.size;
245                     recursionIdentityMap.set(recursionIdentity, recursionToken);
246                 }
247             }
248 
249             const descriptor = {
250                 id: type.id,
251                 intrinsicName: (type as any).intrinsicName,
252                 symbolName: symbol?.escapedName && unescapeLeadingUnderscores(symbol.escapedName),
253                 recursionId: recursionToken,
254                 unionTypes: (type.flags & TypeFlags.Union) ? (type as UnionType).types?.map(t => t.id) : undefined,
255                 intersectionTypes: (type.flags & TypeFlags.Intersection) ? (type as IntersectionType).types.map(t => t.id) : undefined,
256                 aliasTypeArguments: type.aliasTypeArguments?.map(t => t.id),
257                 keyofType: (type.flags & TypeFlags.Index) ? (type as IndexType).type?.id : undefined,
258                 ...indexedAccessProperties,
259                 ...referenceProperties,
260                 ...conditionalProperties,
261                 firstDeclaration: firstDeclaration && {
262                     path: firstFile.path,
263                     start: indexFromOne(getLineAndCharacterOfPosition(firstFile, firstDeclaration.pos)),
264                     end: indexFromOne(getLineAndCharacterOfPosition(getSourceFileOfNode(firstDeclaration), firstDeclaration.end)),
265                 },
266                 flags: Debug.formatTypeFlags(type.flags).split("|"),
267                 display,
268             };
269 
270             fs.writeSync(typesFd, JSON.stringify(descriptor));
271             if (i < numTypes - 1) {
272                 fs.writeSync(typesFd, ",\n");
273             }
274         }
275 
276         fs.writeSync(typesFd, "]\n");
277 
278         fs.closeSync(typesFd);
279 
280         performance.mark("endDumpTypes");
281         performance.measure("Dump types", "beginDumpTypes", "endDumpTypes");
282     }
283 
284     export function dumpLegend() {
285         if (!legendPath) {
286             return;
287         }
288 
289         fs.writeFileSync(legendPath, JSON.stringify(legend));
290     }
291 
292     interface TraceRecord {
293         configFilePath?: string;
294         tracePath: string;
295         typesPath?: string;
296     }
297 }
298 
299 /*@internal*/
300 namespace ts { // eslint-disable-line one-namespace-per-file
301     // define after tracingEnabled is initialized
302     export const startTracing = tracingEnabled.startTracing;
303 }
304