Skip to content

Commit 558ece7

Browse files
authored
Add support for capturing cpu profiles into tsc itself (microsoft#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
1 parent 8d7fd2e commit 558ece7

File tree

8 files changed

+148
-16
lines changed

8 files changed

+148
-16
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@
127127
"crypto": false,
128128
"buffer": false,
129129
"@microsoft/typescript-etw": false,
130-
"source-map-support": false
130+
"source-map-support": false,
131+
"inspector": false
131132
},
132133
"dependencies": {}
133134
}

src/compiler/commandLineParser.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ namespace ts {
141141
category: Diagnostics.Advanced_Options,
142142
description: Diagnostics.Show_verbose_diagnostic_information
143143
},
144+
{
145+
name: "generateCpuProfile",
146+
type: "string",
147+
isFilePath: true,
148+
paramType: Diagnostics.FILE_OR_DIRECTORY,
149+
category: Diagnostics.Advanced_Options,
150+
description: Diagnostics.Generates_a_CPU_profile
151+
},
144152
{
145153
name: "incremental",
146154
shortName: "i",

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4068,6 +4068,10 @@
40684068
"category": "Message",
40694069
"code": 6222
40704070
},
4071+
"Generates a CPU profile.": {
4072+
"category": "Message",
4073+
"code": 6223
4074+
},
40714075

40724076
"Projects to reference": {
40734077
"category": "Message",

src/compiler/sys.ts

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ namespace ts {
667667
createSHA256Hash?(data: string): string;
668668
getMemoryUsage?(): number;
669669
exit(exitCode?: number): void;
670+
/*@internal*/ enableCPUProfiler?(path: string, continuation: () => void): boolean;
671+
/*@internal*/ disableCPUProfiler?(continuation: () => void): boolean;
670672
realpath?(path: string): string;
671673
/*@internal*/ getEnvironmentVariable(name: string): string;
672674
/*@internal*/ tryEnableSourceMapsForHost?(): void;
@@ -694,6 +696,7 @@ namespace ts {
694696
declare const process: any;
695697
declare const global: any;
696698
declare const __filename: string;
699+
declare const __dirname: string;
697700

698701
export function getNodeMajorVersion(): number | undefined {
699702
if (typeof process === "undefined") {
@@ -744,8 +747,9 @@ namespace ts {
744747
const byteOrderMarkIndicator = "\uFEFF";
745748

746749
function getNodeSystem(): System {
747-
const _fs = require("fs");
748-
const _path = require("path");
750+
const nativePattern = /^native |^\([^)]+\)$|^(internal[\\/]|[a-zA-Z0-9_\s]+(\.js)?$)/;
751+
const _fs: typeof import("fs") = require("fs");
752+
const _path: typeof import("path") = require("path");
749753
const _os = require("os");
750754
// crypto can be absent on reduced node installations
751755
let _crypto: typeof import("crypto") | undefined;
@@ -755,6 +759,8 @@ namespace ts {
755759
catch {
756760
_crypto = undefined;
757761
}
762+
let activeSession: import("inspector").Session | "stopping" | undefined;
763+
let profilePath = "./profile.cpuprofile";
758764

759765
const Buffer: {
760766
new (input: string, encoding?: string): any;
@@ -843,8 +849,10 @@ namespace ts {
843849
return 0;
844850
},
845851
exit(exitCode?: number): void {
846-
process.exit(exitCode);
852+
disableCPUProfiler(() => process.exit(exitCode));
847853
},
854+
enableCPUProfiler,
855+
disableCPUProfiler,
848856
realpath,
849857
debugMode: some(<string[]>process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)),
850858
tryEnableSourceMapsForHost() {
@@ -871,6 +879,92 @@ namespace ts {
871879
};
872880
return nodeSystem;
873881

882+
/**
883+
* Uses the builtin inspector APIs to capture a CPU profile
884+
* See https://nodejs.org/api/inspector.html#inspector_example_usage for details
885+
*/
886+
function enableCPUProfiler(path: string, cb: () => void) {
887+
if (activeSession) {
888+
cb();
889+
return false;
890+
}
891+
const inspector: typeof import("inspector") = require("inspector");
892+
if (!inspector || !inspector.Session) {
893+
cb();
894+
return false;
895+
}
896+
const session = new inspector.Session();
897+
session.connect();
898+
899+
session.post("Profiler.enable", () => {
900+
session.post("Profiler.start", () => {
901+
activeSession = session;
902+
profilePath = path;
903+
cb();
904+
});
905+
});
906+
return true;
907+
}
908+
909+
/**
910+
* Strips non-TS paths from the profile, so users with private projects shouldn't
911+
* need to worry about leaking paths by submitting a cpu profile to us
912+
*/
913+
function cleanupPaths(profile: import("inspector").Profiler.Profile) {
914+
let externalFileCounter = 0;
915+
const remappedPaths = createMap<string>();
916+
const normalizedDir = normalizeSlashes(__dirname);
917+
// Windows rooted dir names need an extra `/` prepended to be valid file:/// urls
918+
const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`;
919+
for (const node of profile.nodes) {
920+
if (node.callFrame.url) {
921+
const url = normalizeSlashes(node.callFrame.url);
922+
if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) {
923+
node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true);
924+
}
925+
else if (!nativePattern.test(url)) {
926+
node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!;
927+
externalFileCounter++;
928+
}
929+
}
930+
}
931+
return profile;
932+
}
933+
934+
function disableCPUProfiler(cb: () => void) {
935+
if (activeSession && activeSession !== "stopping") {
936+
const s = activeSession;
937+
activeSession.post("Profiler.stop", (err, { profile }) => {
938+
if (!err) {
939+
try {
940+
if (_fs.statSync(profilePath).isDirectory()) {
941+
profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
942+
}
943+
}
944+
catch {
945+
// do nothing and ignore fallible fs operation
946+
}
947+
try {
948+
_fs.mkdirSync(_path.dirname(profilePath), { recursive: true });
949+
}
950+
catch {
951+
// do nothing and ignore fallible fs operation
952+
}
953+
_fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile)));
954+
}
955+
activeSession = undefined;
956+
s.disconnect();
957+
cb();
958+
});
959+
activeSession = "stopping";
960+
return true;
961+
}
962+
else {
963+
cb();
964+
return false;
965+
}
966+
}
967+
874968
function bufferFrom(input: string, encoding?: string): Buffer {
875969
// See https://github.com/Microsoft/TypeScript/issues/25652
876970
return Buffer.from && (Buffer.from as Function) !== Int8Array.from

src/compiler/tsbuild.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ namespace ts {
174174
/* @internal */ diagnostics?: boolean;
175175
/* @internal */ extendedDiagnostics?: boolean;
176176
/* @internal */ locale?: string;
177+
/* @internal */ generateCpuProfile?: string;
177178

178179
[option: string]: CompilerOptionsValue | undefined;
179180
}

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4805,6 +4805,7 @@ namespace ts {
48054805
emitDecoratorMetadata?: boolean;
48064806
experimentalDecorators?: boolean;
48074807
forceConsistentCasingInFileNames?: boolean;
4808+
/*@internal*/generateCpuProfile?: string;
48084809
/*@internal*/help?: boolean;
48094810
importHelpers?: boolean;
48104811
/*@internal*/init?: boolean;

src/tsc/tsc.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,7 @@ namespace ts {
5252
filter(optionDeclarations.slice(), v => !!v.showInSimplifiedHelpView);
5353
}
5454

55-
export function executeCommandLine(args: string[]): void {
56-
if (args.length > 0 && args[0].charCodeAt(0) === CharacterCodes.minus) {
57-
const firstOption = args[0].slice(args[0].charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
58-
if (firstOption === "build" || firstOption === "b") {
59-
return performBuild(args.slice(1));
60-
}
61-
}
62-
63-
const commandLine = parseCommandLine(args);
64-
55+
function executeCommandLineWorker(commandLine: ParsedCommandLine) {
6556
if (commandLine.options.build) {
6657
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_build_must_be_the_first_command_line_argument));
6758
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
@@ -174,15 +165,32 @@ namespace ts {
174165
}
175166
}
176167

168+
export function executeCommandLine(args: string[]): void {
169+
if (args.length > 0 && args[0].charCodeAt(0) === CharacterCodes.minus) {
170+
const firstOption = args[0].slice(args[0].charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
171+
if (firstOption === "build" || firstOption === "b") {
172+
return performBuild(args.slice(1));
173+
}
174+
}
175+
176+
const commandLine = parseCommandLine(args);
177+
178+
if (commandLine.options.generateCpuProfile && sys.enableCPUProfiler) {
179+
sys.enableCPUProfiler(commandLine.options.generateCpuProfile, () => executeCommandLineWorker(commandLine));
180+
}
181+
else {
182+
executeCommandLineWorker(commandLine);
183+
}
184+
}
185+
177186
function reportWatchModeWithoutSysSupport() {
178187
if (!sys.watchFile || !sys.watchDirectory) {
179188
reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"));
180189
sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
181190
}
182191
}
183192

184-
function performBuild(args: string[]) {
185-
const { buildOptions, projects, errors } = parseBuildCommand(args);
193+
function performBuildWorker(buildOptions: BuildOptions, projects: string[], errors: Diagnostic[]) {
186194
// Update to pretty if host supports it
187195
updateReportDiagnostic(buildOptions);
188196

@@ -229,6 +237,16 @@ namespace ts {
229237
return sys.exit(buildOptions.clean ? builder.clean() : builder.build());
230238
}
231239

240+
function performBuild(args: string[]) {
241+
const { buildOptions, projects, errors } = parseBuildCommand(args);
242+
if (buildOptions.generateCpuProfile && sys.enableCPUProfiler) {
243+
sys.enableCPUProfiler(buildOptions.generateCpuProfile, () => performBuildWorker(buildOptions, projects, errors));
244+
}
245+
else {
246+
performBuildWorker(buildOptions, projects, errors);
247+
}
248+
}
249+
232250
function createReportErrorSummary(options: CompilerOptions | BuildOptions): ReportEmitErrorSummary | undefined {
233251
return shouldBePretty(options) ?
234252
errorCount => sys.write(getErrorSummaryText(errorCount, sys.newLine)) :
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"compilerOptions": {
3+
"generateCpuProfile": "./someString"
4+
}
5+
}

0 commit comments

Comments
 (0)