Skip to content

Commit db6f66c

Browse files
authored
Merge pull request #40593 from microsoft/nativePerformanceHooks
Migrate 'ts.performance' to use native performance hooks when available
2 parents 8ed645a + 7b0d049 commit db6f66c

File tree

8 files changed

+174
-69
lines changed

8 files changed

+174
-69
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4719,6 +4719,10 @@
47194719
"code": 6385,
47204720
"reportsDeprecated": true
47214721
},
4722+
"Performance timings for '--diagnostics' or '--extendedDiagnostics' are not available in this session. A native implementation of the Web Performance API could not be found.": {
4723+
"category": "Message",
4724+
"code": 6386
4725+
},
47224726

47234727
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
47244728
"category": "Message",

src/compiler/performance.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
/*@internal*/
22
/** Performance measurements for the compiler. */
33
namespace ts.performance {
4-
declare const onProfilerEvent: { (markName: string): void; profiler: boolean; };
5-
6-
// NOTE: cannot use ts.noop as core.ts loads after this
7-
const profilerEvent: (markName: string) => void = typeof onProfilerEvent === "function" && onProfilerEvent.profiler === true ? onProfilerEvent : () => { /*empty*/ };
8-
9-
let enabled = false;
10-
let profilerStart = 0;
11-
let counts: ESMap<string, number>;
12-
let marks: ESMap<string, number>;
13-
let measures: ESMap<string, number>;
4+
let perfHooks: PerformanceHooks | undefined;
5+
let perfObserver: PerformanceObserver | undefined;
6+
let perfEntryList: PerformanceObserverEntryList | undefined;
7+
// when set, indicates the implementation of `Performance` to use for user timing.
8+
// when unset, indicates user timing is unavailable or disabled.
9+
let performanceImpl: Performance | undefined;
1410

1511
export interface Timer {
1612
enter(): void;
@@ -53,11 +49,7 @@ namespace ts.performance {
5349
* @param markName The name of the mark.
5450
*/
5551
export function mark(markName: string) {
56-
if (enabled) {
57-
marks.set(markName, timestamp());
58-
counts.set(markName, (counts.get(markName) || 0) + 1);
59-
profilerEvent(markName);
60-
}
52+
performanceImpl?.mark(markName);
6153
}
6254

6355
/**
@@ -70,11 +62,7 @@ namespace ts.performance {
7062
* used.
7163
*/
7264
export function measure(measureName: string, startMarkName?: string, endMarkName?: string) {
73-
if (enabled) {
74-
const end = endMarkName && marks.get(endMarkName) || timestamp();
75-
const start = startMarkName && marks.get(startMarkName) || profilerStart;
76-
measures.set(measureName, (measures.get(measureName) || 0) + (end - start));
77-
}
65+
performanceImpl?.measure(measureName, startMarkName, endMarkName);
7866
}
7967

8068
/**
@@ -83,7 +71,7 @@ namespace ts.performance {
8371
* @param markName The name of the mark.
8472
*/
8573
export function getCount(markName: string) {
86-
return counts && counts.get(markName) || 0;
74+
return perfEntryList?.getEntriesByName(markName, "mark").length || 0;
8775
}
8876

8977
/**
@@ -92,7 +80,7 @@ namespace ts.performance {
9280
* @param measureName The name of the measure whose durations should be accumulated.
9381
*/
9482
export function getDuration(measureName: string) {
95-
return measures && measures.get(measureName) || 0;
83+
return perfEntryList?.getEntriesByName(measureName, "measure").reduce((a, entry) => a + entry.duration, 0) || 0;
9684
}
9785

9886
/**
@@ -101,22 +89,31 @@ namespace ts.performance {
10189
* @param cb The action to perform for each measure
10290
*/
10391
export function forEachMeasure(cb: (measureName: string, duration: number) => void) {
104-
measures.forEach((measure, key) => {
105-
cb(key, measure);
106-
});
92+
perfEntryList?.getEntriesByType("measure").forEach(({ name, duration }) => { cb(name, duration); });
93+
}
94+
95+
/**
96+
* Indicates whether the performance API is enabled.
97+
*/
98+
export function isEnabled() {
99+
return !!performanceImpl;
107100
}
108101

109102
/** Enables (and resets) performance measurements for the compiler. */
110103
export function enable() {
111-
counts = new Map<string, number>();
112-
marks = new Map<string, number>();
113-
measures = new Map<string, number>();
114-
enabled = true;
115-
profilerStart = timestamp();
104+
if (!performanceImpl) {
105+
perfHooks ||= tryGetNativePerformanceHooks();
106+
if (!perfHooks) return false;
107+
perfObserver ||= new perfHooks.PerformanceObserver(list => perfEntryList = list);
108+
perfObserver.observe({ entryTypes: ["mark", "measure"] });
109+
performanceImpl = perfHooks.performance;
110+
}
111+
return true;
116112
}
117113

118114
/** Disables performance measurements for the compiler. */
119115
export function disable() {
120-
enabled = false;
116+
perfObserver?.disconnect();
117+
performanceImpl = undefined;
121118
}
122119
}

src/compiler/performanceCore.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*@internal*/
2+
namespace ts {
3+
// The following definitions provide the minimum compatible support for the Web Performance User Timings API
4+
// between browsers and NodeJS:
5+
6+
export interface PerformanceHooks {
7+
performance: Performance;
8+
PerformanceObserver: PerformanceObserverConstructor;
9+
}
10+
11+
export interface Performance {
12+
mark(name: string): void;
13+
measure(name: string, startMark?: string, endMark?: string): void;
14+
now(): number;
15+
timeOrigin: number;
16+
}
17+
18+
export interface PerformanceEntry {
19+
name: string;
20+
entryType: string;
21+
startTime: number;
22+
duration: number;
23+
}
24+
25+
export interface PerformanceObserverEntryList {
26+
getEntries(): PerformanceEntryList;
27+
getEntriesByName(name: string, type?: string): PerformanceEntryList;
28+
getEntriesByType(type: string): PerformanceEntryList;
29+
}
30+
31+
export interface PerformanceObserver {
32+
disconnect(): void;
33+
observe(options: { entryTypes: readonly string[] }): void;
34+
}
35+
36+
export type PerformanceObserverConstructor = new (callback: (list: PerformanceObserverEntryList, observer: PerformanceObserver) => void) => PerformanceObserver;
37+
export type PerformanceEntryList = PerformanceEntry[];
38+
39+
// Browser globals for the Web Performance User Timings API
40+
declare const performance: Performance | undefined;
41+
declare const PerformanceObserver: PerformanceObserverConstructor | undefined;
42+
43+
// eslint-disable-next-line @typescript-eslint/naming-convention
44+
function hasRequiredAPI(performance: Performance | undefined, PerformanceObserver: PerformanceObserverConstructor | undefined) {
45+
return typeof performance === "object" &&
46+
typeof performance.timeOrigin === "number" &&
47+
typeof performance.mark === "function" &&
48+
typeof performance.measure === "function" &&
49+
typeof performance.now === "function" &&
50+
typeof PerformanceObserver === "function";
51+
}
52+
53+
function tryGetWebPerformanceHooks(): PerformanceHooks | undefined {
54+
if (typeof performance === "object" &&
55+
typeof PerformanceObserver === "function" &&
56+
hasRequiredAPI(performance, PerformanceObserver)) {
57+
return {
58+
performance,
59+
PerformanceObserver
60+
};
61+
}
62+
}
63+
64+
function tryGetNodePerformanceHooks(): PerformanceHooks | undefined {
65+
if (typeof module === "object" && typeof require === "function") {
66+
try {
67+
const { performance, PerformanceObserver } = require("perf_hooks") as typeof import("perf_hooks");
68+
if (hasRequiredAPI(performance, PerformanceObserver)) {
69+
// There is a bug in Node's performance.measure prior to 12.16.3/13.13.0 that does not
70+
// match the Web Performance API specification. Node's implementation did not allow
71+
// optional `start` and `end` arguments for `performance.measure`.
72+
// See https://github.com/nodejs/node/pull/32651 for more information.
73+
const version = new Version(process.versions.node);
74+
const range = new VersionRange("<12 || 13 <13.13");
75+
if (range.test(version)) {
76+
return {
77+
performance: {
78+
get timeOrigin() { return performance.timeOrigin; },
79+
now() { return performance.now(); },
80+
mark(name) { return performance.mark(name); },
81+
measure(name, start = "nodeStart", end?) {
82+
if (end === undefined) {
83+
end = "__performance.measure-fix__";
84+
performance.mark(end);
85+
}
86+
performance.measure(name, start, end);
87+
if (end = "__performance.measure-fix__") {
88+
performance.clearMarks("__performance.measure-fix__");
89+
}
90+
}
91+
},
92+
PerformanceObserver
93+
};
94+
}
95+
return {
96+
performance,
97+
PerformanceObserver
98+
};
99+
}
100+
}
101+
catch {
102+
// ignore errors
103+
}
104+
}
105+
}
106+
107+
// Unlike with the native Map/Set 'tryGet' functions in corePublic.ts, we eagerly evaluate these
108+
// since we will need them for `timestamp`, below.
109+
const nativePerformanceHooks = tryGetWebPerformanceHooks() || tryGetNodePerformanceHooks();
110+
const nativePerformance = nativePerformanceHooks?.performance;
111+
112+
export function tryGetNativePerformanceHooks() {
113+
return nativePerformanceHooks;
114+
}
115+
116+
/** Gets a timestamp with (at least) ms resolution */
117+
export const timestamp =
118+
nativePerformance ? () => nativePerformance.now() :
119+
Date.now ? Date.now :
120+
() => +(new Date());
121+
}

src/compiler/performanceTimestamp.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/compiler/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"corePublic.ts",
1414
"core.ts",
1515
"debug.ts",
16-
"performanceTimestamp.ts",
16+
"performanceCore.ts",
1717
"performance.ts",
1818
"perfLogger.ts",
1919
"semver.ts",

src/executeCommandLine/executeCommandLine.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -648,19 +648,22 @@ namespace ts {
648648
reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K");
649649
}
650650

651-
const programTime = performance.getDuration("Program");
652-
const bindTime = performance.getDuration("Bind");
653-
const checkTime = performance.getDuration("Check");
654-
const emitTime = performance.getDuration("Emit");
651+
const isPerformanceEnabled = performance.isEnabled();
652+
const programTime = isPerformanceEnabled ? performance.getDuration("Program") : 0;
653+
const bindTime = isPerformanceEnabled ? performance.getDuration("Bind") : 0;
654+
const checkTime = isPerformanceEnabled ? performance.getDuration("Check") : 0;
655+
const emitTime = isPerformanceEnabled ? performance.getDuration("Emit") : 0;
655656
if (compilerOptions.extendedDiagnostics) {
656657
const caches = program.getRelationCacheSizes();
657658
reportCountStatistic("Assignability cache size", caches.assignable);
658659
reportCountStatistic("Identity cache size", caches.identity);
659660
reportCountStatistic("Subtype cache size", caches.subtype);
660661
reportCountStatistic("Strict subtype cache size", caches.strictSubtype);
661-
performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration));
662+
if (isPerformanceEnabled) {
663+
performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration));
664+
}
662665
}
663-
else {
666+
else if (isPerformanceEnabled) {
664667
// Individual component times.
665668
// Note: To match the behavior of previous versions of the compiler, the reported parse time includes
666669
// I/O read time and processing time for triple-slash references and module imports, and the reported
@@ -672,10 +675,16 @@ namespace ts {
672675
reportTimeStatistic("Check time", checkTime);
673676
reportTimeStatistic("Emit time", emitTime);
674677
}
675-
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
678+
if (isPerformanceEnabled) {
679+
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
680+
}
676681
reportStatistics();
677-
678-
performance.disable();
682+
if (!isPerformanceEnabled) {
683+
sys.write(Diagnostics.Performance_timings_for_diagnostics_or_extendedDiagnostics_are_not_available_in_this_session_A_native_implementation_of_the_Web_Performance_API_could_not_be_found.message + "\n");
684+
}
685+
else {
686+
performance.disable();
687+
}
679688
}
680689

681690
function reportStatistics() {

src/shims/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"outFile": "../../built/local/shims.js"
55
},
66
"files": [
7-
"collectionShims.ts"
7+
"collectionShims.ts",
88
]
99
}

src/testRunner/unittests/tscWatch/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ namespace ts.tscWatch {
4343
return createWatchProgram(compilerHost);
4444
}
4545

46-
const elapsedRegex = /^Elapsed:: [0-9]+ms/;
46+
const elapsedRegex = /^Elapsed:: \d+(?:\.\d+)?ms/;
4747
const buildVerboseLogRegEx = /^.+ \- /;
4848
export enum HostOutputKind {
4949
Log,

0 commit comments

Comments
 (0)