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