-
Notifications
You must be signed in to change notification settings - Fork 55
/
measurement-util.ts
151 lines (131 loc) · 5.66 KB
/
measurement-util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { PerformanceObserver, performance } from 'perf_hooks';
export interface MarkerDetail<T> {
target: T;
id: number;
}
export interface MeasurementData {
/** Stats for targets added with {@link MeasurementUtil.addMeasureMarker} */
time?: {
total: number;
min: number;
max: number;
avg: number;
}; // only for measurement markers done with `addMeasureMarker`
/** The number of times {@link MeasurementUtil.addStartMarker} was called for a target */
calls_count: number;
}
export type MeasurementResults<T extends string> = Record<T, MeasurementData>;
/**
* Wrapper over node perf hooks.
* T is a user defined string union of the targets that are being
* measured (e.g. 'wallet-initialization | input-selection').
*/
export class MeasurementUtil<T extends string> {
#performanceObserver: PerformanceObserver;
/** Starts observing performance measurements. Must be called before adding markers. */
start(): void {
this.#performanceObserver = new PerformanceObserver(() => {
/* just an empty callback because we don't want to do anything when markers are added */
});
this.#performanceObserver.observe({ buffered: true, entryTypes: ['measure', 'mark'] });
}
/** Stops observing performance measurements and clears previous markers */
stop(): void {
performance.clearMarks();
performance.clearMeasures();
this.#performanceObserver.disconnect();
}
/**
* Add a start time marker in the timeline.
*
* @param target target for which we are setting the marker (e.g. 'wallet-initialization').
* @param id a unique numeric id useful when monitoring many instances. Statistics are aggregated for all ids.
*/
addStartMarker(target: T, id: number): void {
const detail: MarkerDetail<T> = { id, target };
performance.mark(this.#getStartLabel(target, id), { detail });
}
/**
* Add a stop time marker in the timeline.
* Useful when the {@link MeasurementUtil.addMeasureMarker} is done separately/later than the start&stop markers.
* In that case, the measurement is done from startMarker to stopMarker, instead of startMarker to measurementMarker.
*
* @param target same as {@link MeasurementUtil.addStartMarker}
* @param id same as {@link MeasurementUtil.addStartMarker}
*/
addStopMarker(target: T, id: number): void {
performance.mark(this.#getStopLabel(target, id));
}
/**
* Adds a measurement marker in the timeline for `target` with `id`.
* It measures the time since {@link MeasurementUtil.addStartMarker} with the same `target` and `id` was called.
* If `useStopMarker` is specified, it measures the time between {@link MeasurementUtil.addStartMarker} and
* {@link addStopMarker} called with the same `target` and `id`.
*
* @param target same as {@link MeasurementUtil.addStartMarker}
* @param id same as {@link MeasurementUtil.addStartMarker}
* @param useStopMarker Values:
* - `false | undefined`: Measure from start marker until this measure marker.
* - `true`: Measure from start marker until the associated stop marker.
*/
addMeasureMarker(target: T, id: number, useStopMarker = false) {
const detail: MarkerDetail<T> = { id, target };
performance.measure(this.#getMeasureLabel(target, id), {
detail,
end: useStopMarker ? this.#getStopLabel(target, id) : undefined,
start: this.#getStartLabel(target, id)
});
}
/**
* Calculates statistics for all `targets` added with {@link MeasurementUtil.addMeasureMarker}.
*
* @param targets an array of user defined strings for which measurement markers were added.
* @returns a record indexed with the `target` and having as value a {@link MeasurementData} object.
*/
getMeasurements(targets: T[]): MeasurementResults<T> {
const measurementData: Record<T, MeasurementData> = {} as Record<T, MeasurementData>;
const measurementCounters: Record<T, number> = {} as Record<T, number>;
for (const target of targets) measurementData[target] = { calls_count: 0 };
for (const measureEntry of performance
.getEntriesByType('measure')
.map(({ duration, detail }) => ({ duration, target: (detail as MarkerDetail<T>).target }))
.filter(({ target }) => targets?.some((t) => t === target))) {
const data = measurementData[measureEntry.target];
if (!data.time) {
measurementCounters[measureEntry.target] = 1;
data.time = {
avg: measureEntry.duration,
max: measureEntry.duration,
min: measureEntry.duration,
total: measureEntry.duration
};
} else {
measurementCounters[measureEntry.target]++;
data.time.total += measureEntry.duration;
data.time.min = Math.min(measureEntry.duration, data.time.min);
data.time.max = Math.max(measureEntry.duration, data.time.max);
data.time.avg = data.time.total / measurementCounters[measureEntry.target];
}
}
// Count all the start markers. They all have the 'target' detail added. Stop markers don't have it
for (const targetName of performance
.getEntriesByType('mark')
.map(({ detail }) => (detail as MarkerDetail<T>)?.target)
.filter((target) => target && targets?.some((t) => t === target))) {
measurementData[targetName].calls_count++;
}
return measurementData;
}
#getStopLabel(target: string, id: number): string {
return this.#getLabel(target, id, 'stop');
}
#getStartLabel(target: string, id: number): string {
return this.#getLabel(target, id, 'start');
}
#getLabel(target: string, id: number, type: 'start' | 'stop'): string {
return `${target}-${type}-${id}`;
}
#getMeasureLabel(target: string, id: number) {
return `${target}-${id}`;
}
}