Refactor the tracing namespace organization

Also changes the `tracingEnabled.Mode.*` enum to a string union.
This commit is contained in:
Eli Barzilay 2021-02-09 13:16:37 -05:00
parent b7922147d3
commit 4fc9c8446d
3 changed files with 257 additions and 265 deletions

View file

@ -5,299 +5,291 @@ 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
/* @internal */
namespace ts.tracingEnabled { // eslint-disable-line one-namespace-per-file
export const enum Mode {
Project,
Build,
Server,
}
// `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 fs: typeof import("fs");
let traceCount = 0;
let traceFd = 0;
let traceCount = 0;
let traceFd = 0;
let mode: Mode;
let mode: Mode;
let legendPath: string | undefined;
const legend: TraceRecord[] = [];
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 === Mode.Build ? `.${process.pid}-${++traceCount}`
: mode === 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 !== 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 === 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,
// 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)[];
};
}
function dumpTypes(types: readonly Type[]) {
performance.mark("beginDumpTypes");
/** Starts tracing for the given project. */
export function startTracing(tracingMode: Mode, traceDir: string, configFilePath?: string) {
Debug.assert(!tracing, "Tracing already started");
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;
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)) {
if (fs === undefined) {
try {
display = type.checker?.typeToString(type);
fs = require("fs");
}
catch {
display = undefined;
catch (e) {
throw new Error(`tracing requires having fs\n(original error: ${e.message || e})`);
}
}
let indexedAccessProperties: object = {};
if (type.flags & TypeFlags.IndexedAccess) {
const indexedAccessType = type as IndexedAccessType;
indexedAccessProperties = {
indexedAccessObjectType: indexedAccessType.objectType?.id,
indexedAccessIndexType: indexedAccessType.indexType?.id,
};
mode = tracingMode;
if (legendPath === undefined) {
legendPath = combinePaths(traceDir, "legend.json");
}
let referenceProperties: object = {};
if (objectFlags & ObjectFlags.Reference) {
const referenceType = type as TypeReference;
referenceProperties = {
instantiatedType: referenceType.target?.id,
typeArguments: referenceType.resolvedTypeArguments?.map(t => t.id),
};
// Note that writing will fail later on if it exists and is not a directory
if (!fs.existsSync(traceDir)) {
fs.mkdirSync(traceDir, { recursive: true });
}
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,
};
}
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`);
// 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);
}
}
legend.push({
configFilePath,
tracePath,
typesPath,
});
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,
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,
};
}
fs.writeSync(typesFd, JSON.stringify(descriptor));
if (i < numTypes - 1) {
fs.writeSync(typesFd, ",\n");
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;
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");
}
fs.writeSync(typesFd, "]\n");
export function dumpLegend() {
if (!legendPath) {
return;
}
fs.closeSync(typesFd);
performance.mark("endDumpTypes");
performance.measure("Dump types", "beginDumpTypes", "endDumpTypes");
}
export function dumpLegend() {
if (!legendPath) {
return;
fs.writeFileSync(legendPath, JSON.stringify(legend));
}
fs.writeFileSync(legendPath, JSON.stringify(legend));
interface TraceRecord {
configFilePath?: string;
tracePath: string;
typesPath?: string;
}
}
interface TraceRecord {
configFilePath?: string;
tracePath: string;
typesPath?: string;
}
}
/*@internal*/
namespace ts { // eslint-disable-line one-namespace-per-file
// define after tracingEnabled is initialized
export const startTracing = tracingEnabled.startTracing;
}

View file

@ -666,7 +666,7 @@ namespace ts {
}
if (canTrace(system, compilerOptions)) {
startTracing(isBuildMode ? tracingEnabled.Mode.Build : tracingEnabled.Mode.Project,
startTracing(isBuildMode ? "build" : "project",
compilerOptions.generateTrace!, compilerOptions.configFilePath);
}
}

View file

@ -861,7 +861,7 @@ namespace ts.server {
? stripQuotes(commandLineTraceDir)
: process.env.TSS_TRACE;
if (traceDir) {
startTracing(tracingEnabled.Mode.Server, traceDir);
startTracing("server", traceDir);
}
const ioSession = new IOSession();