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