Add support for capturing cpu profiles into tsc itself (#33586)

* Add support for capturing cpu profiles into tsc itself

* Accept baseline for new compiler option in showConfig

* Fix lints

* Support profiling build mode, only ever have one live profiling session

* Minor modification to enable/disable semaphore, accept re-cased baseline

* Add pid into autognerated cpuprofile path

* Rename to fix case

* Sanitize filepaths in emitted cpuprofile for easier adoption by enterprise people, add inspector to browser field
This commit is contained in:
Wesley Wigham 2019-09-27 13:34:44 -07:00 committed by GitHub
parent 8d7fd2e691
commit 558ece72cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 16 deletions

View file

@ -127,7 +127,8 @@
"crypto": false,
"buffer": false,
"@microsoft/typescript-etw": false,
"source-map-support": false
"source-map-support": false,
"inspector": false
},
"dependencies": {}
}

View file

@ -141,6 +141,14 @@ namespace ts {
category: Diagnostics.Advanced_Options,
description: Diagnostics.Show_verbose_diagnostic_information
},
{
name: "generateCpuProfile",
type: "string",
isFilePath: true,
paramType: Diagnostics.FILE_OR_DIRECTORY,
category: Diagnostics.Advanced_Options,
description: Diagnostics.Generates_a_CPU_profile
},
{
name: "incremental",
shortName: "i",

View file

@ -4068,6 +4068,10 @@
"category": "Message",
"code": 6222
},
"Generates a CPU profile.": {
"category": "Message",
"code": 6223
},
"Projects to reference": {
"category": "Message",

View file

@ -667,6 +667,8 @@ namespace ts {
createSHA256Hash?(data: string): string;
getMemoryUsage?(): number;
exit(exitCode?: number): void;
/*@internal*/ enableCPUProfiler?(path: string, continuation: () => void): boolean;
/*@internal*/ disableCPUProfiler?(continuation: () => void): boolean;
realpath?(path: string): string;
/*@internal*/ getEnvironmentVariable(name: string): string;
/*@internal*/ tryEnableSourceMapsForHost?(): void;
@ -694,6 +696,7 @@ namespace ts {
declare const process: any;
declare const global: any;
declare const __filename: string;
declare const __dirname: string;
export function getNodeMajorVersion(): number | undefined {
if (typeof process === "undefined") {
@ -744,8 +747,9 @@ namespace ts {
const byteOrderMarkIndicator = "\uFEFF";
function getNodeSystem(): System {
const _fs = require("fs");
const _path = require("path");
const nativePattern = /^native |^\([^)]+\)$|^(internal[\\/]|[a-zA-Z0-9_\s]+(\.js)?$)/;
const _fs: typeof import("fs") = require("fs");
const _path: typeof import("path") = require("path");
const _os = require("os");
// crypto can be absent on reduced node installations
let _crypto: typeof import("crypto") | undefined;
@ -755,6 +759,8 @@ namespace ts {
catch {
_crypto = undefined;
}
let activeSession: import("inspector").Session | "stopping" | undefined;
let profilePath = "./profile.cpuprofile";
const Buffer: {
new (input: string, encoding?: string): any;
@ -843,8 +849,10 @@ namespace ts {
return 0;
},
exit(exitCode?: number): void {
process.exit(exitCode);
disableCPUProfiler(() => process.exit(exitCode));
},
enableCPUProfiler,
disableCPUProfiler,
realpath,
debugMode: some(<string[]>process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)),
tryEnableSourceMapsForHost() {
@ -871,6 +879,92 @@ namespace ts {
};
return nodeSystem;
/**
* Uses the builtin inspector APIs to capture a CPU profile
* See https://nodejs.org/api/inspector.html#inspector_example_usage for details
*/
function enableCPUProfiler(path: string, cb: () => void) {
if (activeSession) {
cb();
return false;
}
const inspector: typeof import("inspector") = require("inspector");
if (!inspector || !inspector.Session) {
cb();
return false;
}
const session = new inspector.Session();
session.connect();
session.post("Profiler.enable", () => {
session.post("Profiler.start", () => {
activeSession = session;
profilePath = path;
cb();
});
});
return true;
}
/**
* Strips non-TS paths from the profile, so users with private projects shouldn't
* need to worry about leaking paths by submitting a cpu profile to us
*/
function cleanupPaths(profile: import("inspector").Profiler.Profile) {
let externalFileCounter = 0;
const remappedPaths = createMap<string>();
const normalizedDir = normalizeSlashes(__dirname);
// Windows rooted dir names need an extra `/` prepended to be valid file:/// urls
const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`;
for (const node of profile.nodes) {
if (node.callFrame.url) {
const url = normalizeSlashes(node.callFrame.url);
if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) {
node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true);
}
else if (!nativePattern.test(url)) {
node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!;
externalFileCounter++;
}
}
}
return profile;
}
function disableCPUProfiler(cb: () => void) {
if (activeSession && activeSession !== "stopping") {
const s = activeSession;
activeSession.post("Profiler.stop", (err, { profile }) => {
if (!err) {
try {
if (_fs.statSync(profilePath).isDirectory()) {
profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
}
}
catch {
// do nothing and ignore fallible fs operation
}
try {
_fs.mkdirSync(_path.dirname(profilePath), { recursive: true });
}
catch {
// do nothing and ignore fallible fs operation
}
_fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile)));
}
activeSession = undefined;
s.disconnect();
cb();
});
activeSession = "stopping";
return true;
}
else {
cb();
return false;
}
}
function bufferFrom(input: string, encoding?: string): Buffer {
// See https://github.com/Microsoft/TypeScript/issues/25652
return Buffer.from && (Buffer.from as Function) !== Int8Array.from

View file

@ -174,6 +174,7 @@ namespace ts {
/* @internal */ diagnostics?: boolean;
/* @internal */ extendedDiagnostics?: boolean;
/* @internal */ locale?: string;
/* @internal */ generateCpuProfile?: string;
[option: string]: CompilerOptionsValue | undefined;
}

View file

@ -4805,6 +4805,7 @@ namespace ts {
emitDecoratorMetadata?: boolean;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
/*@internal*/generateCpuProfile?: string;
/*@internal*/help?: boolean;
importHelpers?: boolean;
/*@internal*/init?: boolean;

View file

@ -52,16 +52,7 @@ namespace ts {
filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView);
}
export function executeCommandLine(args: string[]): void {
if (args.length > 0 && args[0].charCodeAt(0) === CharacterCodes.minus) {
const firstOption = args[0].slice(args[0].charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
if (firstOption === "build" || firstOption === "b") {
return performBuild(args.slice(1));
}
}
const commandLine = parseCommandLine(args);
function executeCommandLineWorker(commandLine: ParsedCommandLine) {
if (commandLine.options.build) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_build_must_be_the_first_command_line_argument));
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
@ -174,6 +165,24 @@ namespace ts {
}
}
export function executeCommandLine(args: string[]): void {
if (args.length > 0 && args[0].charCodeAt(0) === CharacterCodes.minus) {
const firstOption = args[0].slice(args[0].charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
if (firstOption === "build" || firstOption === "b") {
return performBuild(args.slice(1));
}
}
const commandLine = parseCommandLine(args);
if (commandLine.options.generateCpuProfile && sys.enableCPUProfiler) {
sys.enableCPUProfiler(commandLine.options.generateCpuProfile, () => executeCommandLineWorker(commandLine));
}
else {
executeCommandLineWorker(commandLine);
}
}
function reportWatchModeWithoutSysSupport() {
if (!sys.watchFile || !sys.watchDirectory) {
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
@ -181,8 +190,7 @@ namespace ts {
}
}
function performBuild(args: string[]) {
const { buildOptions, projects, errors } = parseBuildCommand(args);
function performBuildWorker(buildOptions: BuildOptions, projects: string[], errors: Diagnostic[]) {
// Update to pretty if host supports it
updateReportDiagnostic(buildOptions);
@ -229,6 +237,16 @@ namespace ts {
return sys.exit(buildOptions.clean ? builder.clean() : builder.build());
}
function performBuild(args: string[]) {
const { buildOptions, projects, errors } = parseBuildCommand(args);
if (buildOptions.generateCpuProfile && sys.enableCPUProfiler) {
sys.enableCPUProfiler(buildOptions.generateCpuProfile, () => performBuildWorker(buildOptions, projects, errors));
}
else {
performBuildWorker(buildOptions, projects, errors);
}
}
function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined {
return shouldBePretty(options) ?
errorCount => sys.write(getErrorSummaryText(errorCount, sys.newLine)) :

View file

@ -0,0 +1,5 @@
{
"compilerOptions": {
"generateCpuProfile": "./someString"
}
}