• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* Tracing events for the compiler. */
2
3/*@internal*/
4namespace 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 */
12namespace 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*/
300namespace ts { // eslint-disable-line one-namespace-per-file
301    // define after tracingEnabled is initialized
302    export const startTracing = tracingEnabled.startTracing;
303}
304