/* Tracing events for the compiler. */ /*@internal*/ namespace ts { // eslint-disable-line one-namespace-per-file // should be used as tracing?.___ export let tracing: typeof tracingEnabled | undefined; // enable the above using startTracing() // `tracingEnabled` should never be used directly, only through the above namespace tracingEnabled { // eslint-disable-line one-namespace-per-file type Mode = "project" | "build" | "server"; let fs: typeof import("fs"); let traceCount = 0; let traceFd = 0; let mode: Mode; let legendPath: string | undefined; const legend: TraceRecord[] = []; // The actual constraint is that JSON.stringify be able to serialize it without throwing. interface Args { [key: string]: string | number | boolean | null | undefined | Args | readonly (string | number | boolean | null | undefined | Args)[]; }; /** Starts tracing for the given project. */ export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) { Debug.assert(!tracing, "Tracing already started"); if (fs === undefined) { try { fs = require("fs"); } catch (e) { throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`); } } mode = tracingMode; if (legendPath === undefined) { legendPath = combinePaths(traceDir, "legend.json"); } // Note that writing will fail later on if it exists and is not a directory if (!fs.existsSync(traceDir)) { fs.mkdirSync(traceDir, { recursive: true }); } const countPart = mode === "build" ? `.${process.pid}-${++traceCount}` : mode === "server" ? `.${process.pid}` : ``; const tracePath = combinePaths(traceDir, `trace${countPart}.json`); const typesPath = combinePaths(traceDir, `types${countPart}.json`); legend.push({ configFilePath, tracePath, typesPath, }); traceFd = fs.openSync(tracePath, "w"); tracing = tracingEnabled; // only when traceFd is properly set // Start with a prefix that contains some metadata that the devtools profiler expects (also avoids a warning on import) const meta = { cat: "__metadata", ph: "M", ts: 1000 * timestamp(), pid: 1, tid: 1 }; fs.writeSync(traceFd, "[\n" + [{ name: "process_name", args: { name: "tsc" }, ...meta }, { name: "thread_name", args: { name: "Main" }, ...meta }, { name: "TracingStartedInBrowser", ...meta, cat: "disabled-by-default-devtools.timeline" }] .map(v => JSON.stringify(v)).join(",\n")); } /** Stops tracing for the in-progress project and dumps the type catalog. */ export function stopTracing(typeCatalog?: readonly Type[]) { Debug.assert(tracing, "Tracing is not in progress"); Debug.assert(!!typeCatalog === (mode !== "server")); // Have a type catalog iff not in server mode fs.writeSync(traceFd, `\n]\n`); fs.closeSync(traceFd); tracing = undefined; if (typeCatalog) { dumpTypes(typeCatalog); } else { // We pre-computed this path for convenience, but clear it // now that the file won't be created. legend[legend.length - 1].typesPath = undefined; } } export const enum Phase { Parse = "parse", Program = "program", Bind = "bind", Check = "check", // Before we get into checking types (e.g. checkSourceFile) CheckTypes = "checkTypes", Emit = "emit", Session = "session", } export function instant(phase: Phase, name: string, args?: Args) { writeEvent("I", phase, name, args, `"s":"g"`); } const eventStack: { phase: Phase, name: string, args?: Args, time: number, separateBeginAndEnd: boolean }[] = []; /** * @param separateBeginAndEnd - used for special cases where we need the trace point even if the event * never terminates (typically for reducing a scenario too big to trace to one that can be completed). * In the future we might implement an exit handler to dump unfinished events which would deprecate * these operations. */ export function push(phase: Phase, name: string, args?: Args, separateBeginAndEnd = false) { if (separateBeginAndEnd) { writeEvent("B", phase, name, args); } eventStack.push({ phase, name, args, time: 1000 * timestamp(), separateBeginAndEnd }); } export function pop() { Debug.assert(eventStack.length > 0); writeStackEvent(eventStack.length - 1, 1000 * timestamp()); eventStack.length--; } export function popAll() { const endTime = 1000 * timestamp(); for (let i = eventStack.length - 1; i >= 0; i--) { writeStackEvent(i, endTime); } eventStack.length = 0; } // sample every 10ms const sampleInterval = 1000 * 10; function writeStackEvent(index: number, endTime: number) { const { phase, name, args, time, separateBeginAndEnd } = eventStack[index]; if (separateBeginAndEnd) { writeEvent("E", phase, name, args, /*extras*/ undefined, endTime); } // test if [time,endTime) straddles a sampling point else if (sampleInterval - (time % sampleInterval) <= endTime - time) { writeEvent("X", phase, name, args, `"dur":${endTime - time}`, time); } } function writeEvent(eventType: string, phase: Phase, name: string, args: Args | undefined, extras?: string, time: number = 1000 * timestamp()) { // In server mode, there's no easy way to dump type information, so we drop events that would require it. if (mode === "server" && phase === Phase.CheckTypes) return; performance.mark("beginTracing"); fs.writeSync(traceFd, `,\n{"pid":1,"tid":1,"ph":"${eventType}","cat":"${phase}","ts":${time},"name":"${name}"`); if (extras) fs.writeSync(traceFd, `,${extras}`); if (args) fs.writeSync(traceFd, `,"args":${JSON.stringify(args)}`); fs.writeSync(traceFd, `}`); performance.mark("endTracing"); performance.measure("Tracing", "beginTracing", "endTracing"); } function indexFromOne(lc: LineAndCharacter): LineAndCharacter { return { line: lc.line + 1, character: lc.character + 1, }; } function dumpTypes(types: readonly Type[]) { performance.mark("beginDumpTypes"); const typesPath = legend[legend.length - 1].typesPath!; const typesFd = fs.openSync(typesPath, "w"); const recursionIdentityMap = new Map(); // Cleverness: no line break here so that the type ID will match the line number fs.writeSync(typesFd, "["); const numTypes = types.length; for (let i = 0; i < numTypes; i++) { const type = types[i]; const objectFlags = (type as any).objectFlags; const symbol = type.aliasSymbol ?? type.symbol; const firstDeclaration = symbol?.declarations?.[0]; const firstFile = firstDeclaration && getSourceFileOfNode(firstDeclaration); // It's slow to compute the display text, so skip it unless it's really valuable (or cheap) let display: string | undefined; if ((objectFlags & ObjectFlags.Anonymous) | (type.flags & TypeFlags.Literal)) { try { display = type.checker?.typeToString(type); } catch { display = undefined; } } let indexedAccessProperties: object = {}; if (type.flags & TypeFlags.IndexedAccess) { const indexedAccessType = type as IndexedAccessType; indexedAccessProperties = { indexedAccessObjectType: indexedAccessType.objectType?.id, indexedAccessIndexType: indexedAccessType.indexType?.id, }; } let referenceProperties: object = {}; if (objectFlags & ObjectFlags.Reference) { const referenceType = type as TypeReference; referenceProperties = { instantiatedType: referenceType.target?.id, typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id), }; } let conditionalProperties: object = {}; if (type.flags & TypeFlags.Conditional) { const conditionalType = type as ConditionalType; conditionalProperties = { conditionalCheckType: conditionalType.checkType?.id, conditionalExtendsType: conditionalType.extendsType?.id, conditionalTrueType: conditionalType.resolvedTrueType?.id ?? -1, conditionalFalseType: conditionalType.resolvedFalseType?.id ?? -1, }; } // We can't print out an arbitrary object, so just assign each one a unique number. // Don't call it an "id" so people don't treat it as a type id. let recursionToken: number | undefined; const recursionIdentity = type.checker.getRecursionIdentity(type); if (recursionIdentity) { recursionToken = recursionIdentityMap.get(recursionIdentity); if (!recursionToken) { recursionToken = recursionIdentityMap.size; recursionIdentityMap.set(recursionIdentity, recursionToken); } } const descriptor = { id: type.id, intrinsicName: (type as any).intrinsicName, symbolName: symbol?.escapedName && unescapeLeadingUnderscores(symbol.escapedName), recursionId: recursionToken, unionTypes: (type.flags & TypeFlags.Union) ? (type as UnionType).types?.map(t => t.id) : undefined, intersectionTypes: (type.flags & TypeFlags.Intersection) ? (type as IntersectionType).types.map(t => t.id) : undefined, aliasTypeArguments: type.aliasTypeArguments?.map(t => t.id), keyofType: (type.flags & TypeFlags.Index) ? (type as IndexType).type?.id : undefined, ...indexedAccessProperties, ...referenceProperties, ...conditionalProperties, firstDeclaration: firstDeclaration && { path: firstFile.path, start: indexFromOne(getLineAndCharacterOfPosition(firstFile, firstDeclaration.pos)), end: indexFromOne(getLineAndCharacterOfPosition(getSourceFileOfNode(firstDeclaration), firstDeclaration.end)), }, flags: Debug.formatTypeFlags(type.flags).split("|"), display, }; fs.writeSync(typesFd, JSON.stringify(descriptor)); if (i < numTypes - 1) { fs.writeSync(typesFd, ",\n"); } } fs.writeSync(typesFd, "]\n"); fs.closeSync(typesFd); performance.mark("endDumpTypes"); performance.measure("Dump types", "beginDumpTypes", "endDumpTypes"); } export function dumpLegend() { if (!legendPath) { return; } fs.writeFileSync(legendPath, JSON.stringify(legend)); } interface TraceRecord { configFilePath?: string; tracePath: string; typesPath?: string; } } // define after tracingEnabled is initialized export const startTracing = tracingEnabled.startTracing; }