Skip to content

Commit

Permalink
Merge pull request #40593 from microsoft/nativePerformanceHooks
Browse files Browse the repository at this point in the history
Migrate 'ts.performance' to use native performance hooks when available
  • Loading branch information
rbuckton authored Oct 24, 2020
2 parents 8ed645a + 7b0d049 commit db6f66c
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 69 deletions.
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
59 changes: 28 additions & 31 deletions src/compiler/performance.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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;
}
}
121 changes: 121 additions & 0 deletions src/compiler/performanceCore.ts
Original file line number Diff line number Diff line change
@@ -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());
}
26 changes: 0 additions & 26 deletions src/compiler/performanceTimestamp.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"corePublic.ts",
"core.ts",
"debug.ts",
"performanceTimestamp.ts",
"performanceCore.ts",
"performance.ts",
"perfLogger.ts",
"semver.ts",
Expand Down
27 changes: 18 additions & 9 deletions src/executeCommandLine/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/shims/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"outFile": "../../built/local/shims.js"
},
"files": [
"collectionShims.ts"
"collectionShims.ts",
]
}
2 changes: 1 addition & 1 deletion src/testRunner/unittests/tscWatch/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit db6f66c

Please sign in to comment.