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