2021-01-13 22:01:53 +01:00
|
|
|
/* Tracing events for the compiler. */
|
|
|
|
|
2020-08-06 20:02:51 +02:00
|
|
|
/*@internal*/
|
2021-01-13 22:01:53 +01:00
|
|
|
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()
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
// `tracingEnabled` should never be used directly, only through the above
|
|
|
|
namespace tracingEnabled { // eslint-disable-line one-namespace-per-file
|
|
|
|
type Mode = "project" | "build" | "server";
|
2020-11-16 18:26:28 +01:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
let fs: typeof import("fs");
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
let traceCount = 0;
|
|
|
|
let traceFd = 0;
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
let mode: Mode;
|
2020-11-16 18:26:28 +01:00
|
|
|
|
2021-03-25 23:52:04 +01:00
|
|
|
const typeCatalog: Type[] = []; // NB: id is index + 1
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
let legendPath: string | undefined;
|
|
|
|
const legend: TraceRecord[] = [];
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
// 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)[];
|
|
|
|
};
|
2020-11-24 23:33:00 +01:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
/** Starts tracing for the given project. */
|
|
|
|
export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) {
|
|
|
|
Debug.assert(!tracing, "Tracing already started");
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
if (fs === undefined) {
|
|
|
|
try {
|
|
|
|
fs = require("fs");
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`);
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
mode = tracingMode;
|
2021-03-25 23:52:04 +01:00
|
|
|
typeCatalog.length = 0;
|
2020-11-16 18:26:28 +01:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
if (legendPath === undefined) {
|
|
|
|
legendPath = combinePaths(traceDir, "legend.json");
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
// Note that writing will fail later on if it exists and is not a directory
|
|
|
|
if (!fs.existsSync(traceDir)) {
|
|
|
|
fs.mkdirSync(traceDir, { recursive: true });
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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"));
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
/** Stops tracing for the in-progress project and dumps the type catalog. */
|
2021-03-25 23:52:04 +01:00
|
|
|
export function stopTracing() {
|
2021-02-09 19:16:37 +01:00
|
|
|
Debug.assert(tracing, "Tracing is not in progress");
|
2021-03-25 23:52:04 +01:00
|
|
|
Debug.assert(!!typeCatalog.length === (mode !== "server")); // Have a type catalog iff not in server mode
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
fs.writeSync(traceFd, `\n]\n`);
|
|
|
|
fs.closeSync(traceFd);
|
|
|
|
tracing = undefined;
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-03-25 23:52:04 +01:00
|
|
|
if (typeCatalog.length) {
|
2021-02-09 19:16:37 +01:00
|
|
|
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;
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-03-25 23:52:04 +01:00
|
|
|
export function recordType(type: Type): void {
|
|
|
|
if (mode !== "server") {
|
|
|
|
typeCatalog.push(type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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",
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
export function instant(phase: Phase, name: string, args?: Args) {
|
|
|
|
writeEvent("I", phase, name, args, `"s":"g"`);
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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 });
|
2020-11-11 21:58:00 +01:00
|
|
|
}
|
2021-02-09 19:16:37 +01:00
|
|
|
export function pop() {
|
|
|
|
Debug.assert(eventStack.length > 0);
|
|
|
|
writeStackEvent(eventStack.length - 1, 1000 * timestamp());
|
|
|
|
eventStack.length--;
|
2020-11-16 18:26:28 +01:00
|
|
|
}
|
2021-02-09 19:16:37 +01:00
|
|
|
export function popAll() {
|
|
|
|
const endTime = 1000 * timestamp();
|
|
|
|
for (let i = eventStack.length - 1; i >= 0; i--) {
|
|
|
|
writeStackEvent(i, endTime);
|
|
|
|
}
|
|
|
|
eventStack.length = 0;
|
2020-11-11 21:58:00 +01:00
|
|
|
}
|
2021-02-09 19:16:37 +01:00
|
|
|
// 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);
|
|
|
|
}
|
2020-11-11 21:58:00 +01:00
|
|
|
}
|
2020-11-16 18:26:28 +01:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
function writeEvent(eventType: string, phase: Phase, name: string, args: Args | undefined, extras?: string,
|
|
|
|
time: number = 1000 * timestamp()) {
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
// 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;
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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");
|
|
|
|
}
|
2020-08-22 02:39:03 +02:00
|
|
|
|
2021-03-01 18:22:15 +01:00
|
|
|
function getLocation(node: Node | undefined) {
|
|
|
|
const file = getSourceFileOfNode(node);
|
|
|
|
return !file
|
|
|
|
? undefined
|
|
|
|
: {
|
|
|
|
path: file.path,
|
|
|
|
start: indexFromOne(getLineAndCharacterOfPosition(file, node!.pos)),
|
|
|
|
end: indexFromOne(getLineAndCharacterOfPosition(file, node!.end)),
|
|
|
|
};
|
|
|
|
|
|
|
|
function indexFromOne(lc: LineAndCharacter): LineAndCharacter {
|
|
|
|
return {
|
|
|
|
line: lc.line + 1,
|
|
|
|
character: lc.character + 1,
|
|
|
|
};
|
|
|
|
}
|
2021-02-09 19:16:37 +01:00
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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<object, number>();
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
let indexedAccessProperties: object = {};
|
|
|
|
if (type.flags & TypeFlags.IndexedAccess) {
|
|
|
|
const indexedAccessType = type as IndexedAccessType;
|
|
|
|
indexedAccessProperties = {
|
|
|
|
indexedAccessObjectType: indexedAccessType.objectType?.id,
|
|
|
|
indexedAccessIndexType: indexedAccessType.indexType?.id,
|
|
|
|
};
|
2020-08-06 20:02:51 +02:00
|
|
|
}
|
2021-02-09 19:16:37 +01:00
|
|
|
|
|
|
|
let referenceProperties: object = {};
|
|
|
|
if (objectFlags & ObjectFlags.Reference) {
|
|
|
|
const referenceType = type as TypeReference;
|
|
|
|
referenceProperties = {
|
|
|
|
instantiatedType: referenceType.target?.id,
|
|
|
|
typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id),
|
2021-03-01 18:22:15 +01:00
|
|
|
referenceLocation: getLocation(referenceType.node),
|
2021-02-09 19:16:37 +01:00
|
|
|
};
|
2020-08-06 20:02:51 +02:00
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-03-01 18:22:15 +01:00
|
|
|
let substitutionProperties: object = {};
|
|
|
|
if (type.flags & TypeFlags.Substitution) {
|
|
|
|
const substitutionType = type as SubstitutionType;
|
|
|
|
substitutionProperties = {
|
|
|
|
substitutionBaseType: substitutionType.baseType?.id,
|
|
|
|
substituteType: substitutionType.substitute?.id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let reverseMappedProperties: object = {};
|
|
|
|
if (objectFlags & ObjectFlags.ReverseMapped) {
|
|
|
|
const reverseMappedType = type as ReverseMappedType;
|
|
|
|
reverseMappedProperties = {
|
|
|
|
reverseMappedSourceType: reverseMappedType.source?.id,
|
|
|
|
reverseMappedMappedType: reverseMappedType.mappedType?.id,
|
|
|
|
reverseMappedConstraintType: reverseMappedType.constraintType?.id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let evolvingArrayProperties: object = {};
|
|
|
|
if (objectFlags & ObjectFlags.EvolvingArray) {
|
|
|
|
const evolvingArrayType = type as EvolvingArrayType;
|
|
|
|
evolvingArrayProperties = {
|
|
|
|
evolvingArrayElementType: evolvingArrayType.elementType.id,
|
|
|
|
evolvingArrayFinalType: evolvingArrayType.finalArrayType?.id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
const descriptor = {
|
|
|
|
id: type.id,
|
|
|
|
intrinsicName: (type as any).intrinsicName,
|
|
|
|
symbolName: symbol?.escapedName && unescapeLeadingUnderscores(symbol.escapedName),
|
|
|
|
recursionId: recursionToken,
|
2021-03-01 18:22:15 +01:00
|
|
|
isTuple: objectFlags & ObjectFlags.Tuple ? true : undefined,
|
2021-02-09 19:16:37 +01:00
|
|
|
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,
|
2021-03-01 18:22:15 +01:00
|
|
|
...substitutionProperties,
|
|
|
|
...reverseMappedProperties,
|
|
|
|
...evolvingArrayProperties,
|
|
|
|
destructuringPattern: getLocation(type.pattern),
|
|
|
|
firstDeclaration: getLocation(symbol?.declarations?.[0]),
|
2021-02-09 19:16:37 +01:00
|
|
|
flags: Debug.formatTypeFlags(type.flags).split("|"),
|
|
|
|
display,
|
2020-08-06 20:02:51 +02:00
|
|
|
};
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
fs.writeSync(typesFd, JSON.stringify(descriptor));
|
|
|
|
if (i < numTypes - 1) {
|
|
|
|
fs.writeSync(typesFd, ",\n");
|
2020-08-22 02:39:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
fs.writeSync(typesFd, "]\n");
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
fs.closeSync(typesFd);
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
performance.mark("endDumpTypes");
|
|
|
|
performance.measure("Dump types", "beginDumpTypes", "endDumpTypes");
|
|
|
|
}
|
2020-08-06 20:02:51 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
export function dumpLegend() {
|
|
|
|
if (!legendPath) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
fs.writeFileSync(legendPath, JSON.stringify(legend));
|
2020-08-21 03:04:38 +02:00
|
|
|
}
|
|
|
|
|
2021-02-09 19:16:37 +01:00
|
|
|
interface TraceRecord {
|
|
|
|
configFilePath?: string;
|
|
|
|
tracePath: string;
|
|
|
|
typesPath?: string;
|
|
|
|
}
|
2020-08-21 03:04:38 +02:00
|
|
|
}
|
2021-01-13 22:01:53 +01:00
|
|
|
|
|
|
|
// define after tracingEnabled is initialized
|
|
|
|
export const startTracing = tracingEnabled.startTracing;
|
2021-03-16 18:57:10 +01:00
|
|
|
export const dumpTracingLegend = tracingEnabled.dumpLegend;
|
2021-01-13 22:01:53 +01:00
|
|
|
}
|