Merge pull request #40593 from microsoft/nativePerformanceHooks

Migrate 'ts.performance' to use native performance hooks when available
This commit is contained in:
Ron Buckton 2020-10-23 17:32:55 -07:00 committed by GitHub
commit db6f66cc4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 69 deletions

View File

@ -4719,6 +4719,10 @@
"code": 6385,
"reportsDeprecated": true
},
"Performance timings for '--diagnostics' or '--extendedDiagnostics' are not available in this session. A native implementation of the Web Performance API could not be found.": {
"category": "Message",
"code": 6386
},
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",

View File

@ -1,16 +1,12 @@
/*@internal*/
/** Performance measurements for the compiler. */
namespace ts.performance {
declare const onProfilerEvent: { (markName: string): void; profiler: boolean; };
// NOTE: cannot use ts.noop as core.ts loads after this
const profilerEvent: (markName: string) => void = typeof onProfilerEvent === "function" && onProfilerEvent.profiler === true ? onProfilerEvent : () => { /*empty*/ };
let enabled = false;
let profilerStart = 0;
let counts: ESMap<string, number>;
let marks: ESMap<string, number>;
let measures: ESMap<string, number>;
let perfHooks: PerformanceHooks | undefined;
let perfObserver: PerformanceObserver | undefined;
let perfEntryList: PerformanceObserverEntryList | undefined;
// when set, indicates the implementation of `Performance` to use for user timing.
// when unset, indicates user timing is unavailable or disabled.
let performanceImpl: Performance | undefined;
export interface Timer {
enter(): void;
@ -53,11 +49,7 @@ namespace ts.performance {
* @param markName The name of the mark.
*/
export function mark(markName: string) {
if (enabled) {
marks.set(markName, timestamp());
counts.set(markName, (counts.get(markName) || 0) + 1);
profilerEvent(markName);
}
performanceImpl?.mark(markName);
}
/**
@ -70,11 +62,7 @@ namespace ts.performance {
* used.
*/
export function measure(measureName: string, startMarkName?: string, endMarkName?: string) {
if (enabled) {
const end = endMarkName && marks.get(endMarkName) || timestamp();
const start = startMarkName && marks.get(startMarkName) || profilerStart;
measures.set(measureName, (measures.get(measureName) || 0) + (end - start));
}
performanceImpl?.measure(measureName, startMarkName, endMarkName);
}
/**
@ -83,7 +71,7 @@ namespace ts.performance {
* @param markName The name of the mark.
*/
export function getCount(markName: string) {
return counts && counts.get(markName) || 0;
return perfEntryList?.getEntriesByName(markName, "mark").length || 0;
}
/**
@ -92,7 +80,7 @@ namespace ts.performance {
* @param measureName The name of the measure whose durations should be accumulated.
*/
export function getDuration(measureName: string) {
return measures && measures.get(measureName) || 0;
return perfEntryList?.getEntriesByName(measureName, "measure").reduce((a, entry) => a + entry.duration, 0) || 0;
}
/**
@ -101,22 +89,31 @@ namespace ts.performance {
* @param cb The action to perform for each measure
*/
export function forEachMeasure(cb: (measureName: string, duration: number) => void) {
measures.forEach((measure, key) => {
cb(key, measure);
});
perfEntryList?.getEntriesByType("measure").forEach(({ name, duration }) => { cb(name, duration); });
}
/**
* Indicates whether the performance API is enabled.
*/
export function isEnabled() {
return !!performanceImpl;
}
/** Enables (and resets) performance measurements for the compiler. */
export function enable() {
counts = new Map<string, number>();
marks = new Map<string, number>();
measures = new Map<string, number>();
enabled = true;
profilerStart = timestamp();
if (!performanceImpl) {
perfHooks ||= tryGetNativePerformanceHooks();
if (!perfHooks) return false;
perfObserver ||= new perfHooks.PerformanceObserver(list => perfEntryList = list);
perfObserver.observe({ entryTypes: ["mark", "measure"] });
performanceImpl = perfHooks.performance;
}
return true;
}
/** Disables performance measurements for the compiler. */
export function disable() {
enabled = false;
perfObserver?.disconnect();
performanceImpl = undefined;
}
}

View File

@ -0,0 +1,121 @@
/*@internal*/
namespace ts {
// The following definitions provide the minimum compatible support for the Web Performance User Timings API
// between browsers and NodeJS:
export interface PerformanceHooks {
performance: Performance;
PerformanceObserver: PerformanceObserverConstructor;
}
export interface Performance {
mark(name: string): void;
measure(name: string, startMark?: string, endMark?: string): void;
now(): number;
timeOrigin: number;
}
export interface PerformanceEntry {
name: string;
entryType: string;
startTime: number;
duration: number;
}
export interface PerformanceObserverEntryList {
getEntries(): PerformanceEntryList;
getEntriesByName(name: string, type?: string): PerformanceEntryList;
getEntriesByType(type: string): PerformanceEntryList;
}
export interface PerformanceObserver {
disconnect(): void;
observe(options: { entryTypes: readonly string[] }): void;
}
export type PerformanceObserverConstructor = new (callback: (list: PerformanceObserverEntryList, observer: PerformanceObserver) => void) => PerformanceObserver;
export type PerformanceEntryList = PerformanceEntry[];
// Browser globals for the Web Performance User Timings API
declare const performance: Performance | undefined;
declare const PerformanceObserver: PerformanceObserverConstructor | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
function hasRequiredAPI(performance: Performance | undefined, PerformanceObserver: PerformanceObserverConstructor | undefined) {
return typeof performance === "object" &&
typeof performance.timeOrigin === "number" &&
typeof performance.mark === "function" &&
typeof performance.measure === "function" &&
typeof performance.now === "function" &&
typeof PerformanceObserver === "function";
}
function tryGetWebPerformanceHooks(): PerformanceHooks | undefined {
if (typeof performance === "object" &&
typeof PerformanceObserver === "function" &&
hasRequiredAPI(performance, PerformanceObserver)) {
return {
performance,
PerformanceObserver
};
}
}
function tryGetNodePerformanceHooks(): PerformanceHooks | undefined {
if (typeof module === "object" && typeof require === "function") {
try {
const { performance, PerformanceObserver } = require("perf_hooks") as typeof import("perf_hooks");
if (hasRequiredAPI(performance, PerformanceObserver)) {
// There is a bug in Node's performance.measure prior to 12.16.3/13.13.0 that does not
// match the Web Performance API specification. Node's implementation did not allow
// optional `start` and `end` arguments for `performance.measure`.
// See https://github.com/nodejs/node/pull/32651 for more information.
const version = new Version(process.versions.node);
const range = new VersionRange("<12 || 13 <13.13");
if (range.test(version)) {
return {
performance: {
get timeOrigin() { return performance.timeOrigin; },
now() { return performance.now(); },
mark(name) { return performance.mark(name); },
measure(name, start = "nodeStart", end?) {
if (end === undefined) {
end = "__performance.measure-fix__";
performance.mark(end);
}
performance.measure(name, start, end);
if (end = "__performance.measure-fix__") {
performance.clearMarks("__performance.measure-fix__");
}
}
},
PerformanceObserver
};
}
return {
performance,
PerformanceObserver
};
}
}
catch {
// ignore errors
}
}
}
// Unlike with the native Map/Set 'tryGet' functions in corePublic.ts, we eagerly evaluate these
// since we will need them for `timestamp`, below.
const nativePerformanceHooks = tryGetWebPerformanceHooks() || tryGetNodePerformanceHooks();
const nativePerformance = nativePerformanceHooks?.performance;
export function tryGetNativePerformanceHooks() {
return nativePerformanceHooks;
}
/** Gets a timestamp with (at least) ms resolution */
export const timestamp =
nativePerformance ? () => nativePerformance.now() :
Date.now ? Date.now :
() => +(new Date());
}

View File

@ -1,26 +0,0 @@
/*@internal*/
namespace ts {
declare const performance: { now?(): number } | undefined;
function tryGlobalPerformanceNow() { // browsers
if (typeof performance !== "undefined") return () => performance.now!();
}
function tryNodePerformanceNow() { // node
try {
const perf_hooks = require("perf_hooks") as typeof import("perf_hooks");
if (perf_hooks.performance) return () => perf_hooks.performance.now();
}
// eslint-disable-next-line no-empty
catch {
}
}
function tryDateNow() {
if (Date.now) return () => Date.now();
}
/** Gets a timestamp with (at least) ms resolution */
export const timestamp: () => number =
tryGlobalPerformanceNow()
|| tryNodePerformanceNow()
|| tryDateNow()
|| (() => +(new Date()));
}

View File

@ -13,7 +13,7 @@
"corePublic.ts",
"core.ts",
"debug.ts",
"performanceTimestamp.ts",
"performanceCore.ts",
"performance.ts",
"perfLogger.ts",
"semver.ts",

View File

@ -648,19 +648,22 @@ namespace ts {
reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K");
}
const programTime = performance.getDuration("Program");
const bindTime = performance.getDuration("Bind");
const checkTime = performance.getDuration("Check");
const emitTime = performance.getDuration("Emit");
const isPerformanceEnabled = performance.isEnabled();
const programTime = isPerformanceEnabled ? performance.getDuration("Program") : 0;
const bindTime = isPerformanceEnabled ? performance.getDuration("Bind") : 0;
const checkTime = isPerformanceEnabled ? performance.getDuration("Check") : 0;
const emitTime = isPerformanceEnabled ? performance.getDuration("Emit") : 0;
if (compilerOptions.extendedDiagnostics) {
const caches = program.getRelationCacheSizes();
reportCountStatistic("Assignability cache size", caches.assignable);
reportCountStatistic("Identity cache size", caches.identity);
reportCountStatistic("Subtype cache size", caches.subtype);
reportCountStatistic("Strict subtype cache size", caches.strictSubtype);
performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration));
if (isPerformanceEnabled) {
performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration));
}
}
else {
else if (isPerformanceEnabled) {
// Individual component times.
// Note: To match the behavior of previous versions of the compiler, the reported parse time includes
// I/O read time and processing time for triple-slash references and module imports, and the reported
@ -672,10 +675,16 @@ namespace ts {
reportTimeStatistic("Check time", checkTime);
reportTimeStatistic("Emit time", emitTime);
}
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
if (isPerformanceEnabled) {
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
}
reportStatistics();
performance.disable();
if (!isPerformanceEnabled) {
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");
}
else {
performance.disable();
}
}
function reportStatistics() {

View File

@ -4,6 +4,6 @@
"outFile": "../../built/local/shims.js"
},
"files": [
"collectionShims.ts"
"collectionShims.ts",
]
}

View File

@ -43,7 +43,7 @@ namespace ts.tscWatch {
return createWatchProgram(compilerHost);
}
const elapsedRegex = /^Elapsed:: [0-9]+ms/;
const elapsedRegex = /^Elapsed:: \d+(?:\.\d+)?ms/;
const buildVerboseLogRegEx = /^.+ \- /;
export enum HostOutputKind {
Log,