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