diff --git a/package.json b/package.json index 7a59b95a130..9775b38b8d2 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,8 @@ "crypto": false, "buffer": false, "@microsoft/typescript-etw": false, - "source-map-support": false + "source-map-support": false, + "inspector": false }, "dependencies": {} } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 8e0a5b8d10a..d2f4e0b27c5 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -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", diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f7673d4dc9d..d7471501619 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4068,6 +4068,10 @@ "category": "Message", "code": 6222 }, + "Generates a CPU profile.": { + "category": "Message", + "code": 6223 + }, "Projects to reference": { "category": "Message", diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6a3f4ed86bd..1304ab19349 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -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(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(); + 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 diff --git a/src/compiler/tsbuild.ts b/src/compiler/tsbuild.ts index 2f152d294a7..aea85d1fb17 100644 --- a/src/compiler/tsbuild.ts +++ b/src/compiler/tsbuild.ts @@ -174,6 +174,7 @@ namespace ts { /* @internal */ diagnostics?: boolean; /* @internal */ extendedDiagnostics?: boolean; /* @internal */ locale?: string; + /* @internal */ generateCpuProfile?: string; [option: string]: CompilerOptionsValue | undefined; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 73d3785d286..241bce63316 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4805,6 +4805,7 @@ namespace ts { emitDecoratorMetadata?: boolean; experimentalDecorators?: boolean; forceConsistentCasingInFileNames?: boolean; + /*@internal*/generateCpuProfile?: string; /*@internal*/help?: boolean; importHelpers?: boolean; /*@internal*/init?: boolean; diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index fc75e28d504..b6388528223 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -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)) : diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/generateCpuProfile/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/generateCpuProfile/tsconfig.json new file mode 100644 index 00000000000..40278d79fa7 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/generateCpuProfile/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "generateCpuProfile": "./someString" + } +}