Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate 'ts.performance' to use native performance hooks when available #40593

Merged
merged 6 commits into from
Oct 24, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[];
rbuckton marked this conversation as resolved.
Show resolved Hide resolved

// 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 :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that I copied this in my earlier comment, so just in case: didn't you say that Date.now should be wrapped for proper invocation in browsers?

() => +(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
26 changes: 17 additions & 9 deletions src/executeCommandLine/executeCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,19 +648,21 @@ 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 programTime = performance.isEnabled() ? performance.getDuration("Program") : 0;
rbuckton marked this conversation as resolved.
Show resolved Hide resolved
const bindTime = performance.isEnabled() ? performance.getDuration("Bind") : 0;
const checkTime = performance.isEnabled() ? performance.getDuration("Check") : 0;
const emitTime = performance.isEnabled() ? 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 (performance.isEnabled()) {
performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration));
}
}
else {
else if (performance.isEnabled()) {
// 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 +674,16 @@ namespace ts {
reportTimeStatistic("Check time", checkTime);
reportTimeStatistic("Emit time", emitTime);
}
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
if (performance.isEnabled()) {
reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime);
}
reportStatistics();

performance.disable();
if (!performance.isEnabled()) {
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