Skip to content

Report on actionable emit hotspots #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@typescript/analyze-trace",
"author": "Microsoft Corp.",
"homepage": "https://github.com/microsoft/typescript-analyze-trace#readme",
"version": "0.4.0",
"version": "0.5.0",
"license": "MIT",
"description": "Analyze the output of tsc --generatetrace",
"keywords": [
Expand Down
30 changes: 25 additions & 5 deletions src/analyze-trace-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import yargs = require("yargs");
import getTypeTree = require("./get-type-tree");
import normalizePositions = require("./normalize-positions");
import { commandLineOptions, checkCommandLineOptions } from "./analyze-trace-options";
import countImportExpressions = require("./count-import-expressions");

const argv = yargs(process.argv.slice(2))
.command("$0 <tracePath> [typesPath]", "Preprocess tracing type dumps", yargs => yargs
Expand All @@ -30,6 +31,7 @@ const typesPath = argv.typesPath;
const thresholdDuration = argv.forceMillis * 1000; // microseconds
const minDuration = argv.skipMillis * 1000; // microseconds
const minPercentage = 0.6;
const importExpressionThreshold = 10;

main().catch(err => console.error(`Internal Error: ${err.message}\n${err.stack}`));

Expand Down Expand Up @@ -326,7 +328,7 @@ async function makePrintableTree(curr: EventSpan, currentFile: string | undefine
}

if (curr.event) {
const eventStr = eventToString();
const eventStr = await eventToString();
if (eventStr) {
let result = {};
result[`${eventStr} (${Math.round((curr.end - curr.start) / 1000)}ms)`] = childTree;
Expand All @@ -336,15 +338,33 @@ async function makePrintableTree(curr: EventSpan, currentFile: string | undefine

return childTree;

function eventToString(): string | undefined {
async function eventToString(): Promise<string | undefined> {
const event = curr.event!;
switch (event.name) {
// TODO (https://github.com/microsoft/typescript-analyze-trace/issues/2)
// case "findSourceFile":
// return `Load file ${event.args!.fileName}`;
// TODO (https://github.com/microsoft/typescript-analyze-trace/issues/3)
// case "emit":
// return `Emit`;
case "emitDeclarationFileOrBundle":
const dtsPath = event.args.declarationFilePath;
if (!dtsPath || !fs.existsSync(dtsPath)) {
return undefined;
}
try {
const sourceStream = fs.createReadStream(dtsPath, { encoding: "utf-8" });
const frequency = await countImportExpressions(sourceStream);
const sorted = Array.from(frequency.entries()).sort(([import1, count1], [import2, count2]) => count2 - count1 || import1.localeCompare(import2)).filter(([_, count]) => count >= importExpressionThreshold);
if (sorted.length === 0) {
return undefined;
}
for (const [imp, count] of sorted) {
// Directly modifying childTree is pretty hacky
childTree[`Consider adding \`${chalk.cyan(`import ${chalk.cyan(imp)}`)}\` which is used in ${count} places`] = {};
}
return `Emit declarations file ${formatPath(dtsPath)}`;
}
catch {
return undefined;
}
case "checkSourceFile":
return `Check file ${formatPath(currentFile!)}`;
case "structuredTypeRelatedTo":
Expand Down
81 changes: 81 additions & 0 deletions src/count-import-expressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import stream = require("stream");
import TriviaStateMachine = require("./trivia-state-machine");

/**
* @param stream A stream of string chunks with respect to which `positions` should be normalized.
* @returns A frequency table of imported module names.
*/
function countImportExpressions(stream: stream.Readable): Promise<Map<string, number>> {
return new Promise<Map<string, number>>((resolve, reject) => {
let prevCh = -1;

stream.on("error", err => reject(err));

// The actual event handling is in onChar and onEof below.
// These handlers provided a simplified current- and next-char view.
stream.on("data", chunk => {
const text = chunk as string;
const length = text.length;
for (let i = 0; i < length; i++) {
const ch = text.charCodeAt(i);
if (prevCh >= 0) {
onChar(prevCh, ch);
}
prevCh = ch;
}
});

stream.on("close", () => {
if (prevCh >= 0) {
onChar(prevCh, -1);
}

onEof();
});

const target = "import("; // tsc doesn't emit a space before the lparen
let targetPos = 0;

const buf: number[] = [];

const frequency = new Map<string, number>();

let stateMachine = TriviaStateMachine.create();

function onChar(ch: number, nextCh: number) {
const { charKind } = stateMachine.step(ch, nextCh);

if (targetPos === target.length) {
if (charKind === "string") {
buf.push(ch);
}
else {
const name = String.fromCharCode(...buf);

if (!frequency.has(name)) {
frequency.set(name, 0);
}
frequency.set(name, frequency.get(name)! + 1);

targetPos = 0;
buf.length = 0;
}
}
else if (charKind === "code" && ch === target.charCodeAt(targetPos)) {
targetPos++;
}
else {
targetPos = 0;
}
}

function onEof() {
resolve(frequency);
}
});
}

export = countImportExpressions;
Loading