element-web/src/performance/index.ts

175 lines
5.6 KiB
TypeScript
Raw Normal View History

/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { PerformanceEntryNames } from "./entry-names";
interface GetEntriesOptions {
name?: string;
type?: string;
}
type PerformanceCallbackFunction = (entry: PerformanceEntry[]) => void;
interface PerformanceDataListener {
entryNames?: string[];
callback: PerformanceCallbackFunction;
}
export default class PerformanceMonitor {
private static _instance: PerformanceMonitor;
2021-06-29 12:11:58 +00:00
private START_PREFIX = "start:";
private STOP_PREFIX = "stop:";
2021-06-29 12:11:58 +00:00
private listeners: PerformanceDataListener[] = [];
private entries: PerformanceEntry[] = [];
public static get instance(): PerformanceMonitor {
if (!PerformanceMonitor._instance) {
PerformanceMonitor._instance = new PerformanceMonitor();
}
return PerformanceMonitor._instance;
}
/**
* Starts a performance recording
* @param name Name of the recording
* @param id Specify an identifier appended to the measurement name
* @returns {void}
*/
public start(name: string, id?: string): void {
if (!this.supportsPerformanceApi()) {
return;
}
const key = this.buildKey(name, id);
if (performance.getEntriesByName(this.START_PREFIX + key).length > 0) {
logger.warn(`Recording already started for: ${name}`);
return;
}
performance.mark(this.START_PREFIX + key);
}
/**
* Stops a performance recording and stores delta duration
* with the start marker
* @param name Name of the recording
* @param id Specify an identifier appended to the measurement name
* @returns The measurement
*/
public stop(name: string, id?: string): PerformanceEntry {
if (!this.supportsPerformanceApi()) {
return;
}
const key = this.buildKey(name, id);
if (performance.getEntriesByName(this.START_PREFIX + key).length === 0) {
logger.warn(`No recording started for: ${name}`);
return;
}
performance.mark(this.STOP_PREFIX + key);
2022-12-12 11:24:14 +00:00
performance.measure(key, this.START_PREFIX + key, this.STOP_PREFIX + key);
this.clear(name, id);
const measurement = performance.getEntriesByName(key).pop();
// Keeping a reference to all PerformanceEntry created
// by this abstraction for historical events collection
// when adding a data callback
this.entries.push(measurement);
2022-12-12 11:24:14 +00:00
this.listeners.forEach((listener) => {
if (this.shouldEmit(listener, measurement)) {
2021-06-29 12:11:58 +00:00
listener.callback([measurement]);
}
});
return measurement;
}
public clear(name: string, id?: string): void {
if (!this.supportsPerformanceApi()) {
return;
}
const key = this.buildKey(name, id);
performance.clearMarks(this.START_PREFIX + key);
performance.clearMarks(this.STOP_PREFIX + key);
}
public getEntries({ name, type }: GetEntriesOptions = {}): PerformanceEntry[] {
2022-12-12 11:24:14 +00:00
return this.entries.filter((entry) => {
const satisfiesName = !name || entry.name === name;
const satisfiedType = !type || entry.entryType === type;
return satisfiesName && satisfiedType;
});
}
public addPerformanceDataCallback(listener: PerformanceDataListener, buffer = false) {
this.listeners.push(listener);
if (buffer) {
2022-12-12 11:24:14 +00:00
const toEmit = this.entries.filter((entry) => this.shouldEmit(listener, entry));
if (toEmit.length > 0) {
listener.callback(toEmit);
}
}
}
public removePerformanceDataCallback(callback?: PerformanceCallbackFunction) {
if (!callback) {
this.listeners = [];
} else {
this.listeners.splice(
2022-12-12 11:24:14 +00:00
this.listeners.findIndex((listener) => listener.callback === callback),
1,
);
}
}
/**
* Tor browser does not support the Performance API
* @returns {boolean} true if the Performance API is supported
*/
private supportsPerformanceApi(): boolean {
return performance !== undefined && performance.mark !== undefined;
}
private shouldEmit(listener: PerformanceDataListener, entry: PerformanceEntry): boolean {
return !listener.entryNames || listener.entryNames.includes(entry.name);
}
/**
* Internal utility to ensure consistent name for the recording
* @param name Name of the recording
* @param id Specify an identifier appended to the measurement name
* @returns {string} a compound of the name and identifier if present
*/
private buildKey(name: string, id?: string): string {
2022-12-12 11:24:14 +00:00
const suffix = id ? `:${id}` : "";
return `${name}${suffix}`;
}
}
// Convenience exports
2022-12-12 11:24:14 +00:00
export { PerformanceEntryNames };
// Exposing those to the window object to bridge them from tests
window.mxPerformanceMonitor = PerformanceMonitor.instance;
window.mxPerformanceEntryNames = PerformanceEntryNames;