build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / performance / PerformanceStatistics.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
01f4001e 3import { type PerformanceEntry, PerformanceObserver, performance } from 'node:perf_hooks';
130783a7 4import type { URL } from 'node:url';
01f4001e 5import { parentPort } from 'node:worker_threads';
63b48f77 6
268a74bb
JB
7import {
8 type IncomingRequestCommand,
9 MessageType,
10 type RequestCommand,
11 type Statistics,
f6778d74 12 type TimestampedData,
268a74bb 13} from '../types';
7671fa0b
JB
14import {
15 CircularArray,
16 Configuration,
17 Constants,
9bf0ef23 18 JSONStringifyWithMapSupport,
c8faabc8 19 buildPerformanceStatisticsMessage,
da55bd34 20 extractTimeSeriesValues,
9bf0ef23
JB
21 formatDurationSeconds,
22 generateUUID,
23 logPrefix,
7671fa0b 24 logger,
4884b8d3
JB
25 median,
26 nthPercentile,
27 stdDeviation,
7671fa0b 28} from '../utils';
7dde0b73 29
268a74bb 30export class PerformanceStatistics {
e7aeea18
JB
31 private static readonly instances: Map<string, PerformanceStatistics> = new Map<
32 string,
33 PerformanceStatistics
34 >();
10068088 35
9e23580d 36 private readonly objId: string;
9f2e3130 37 private readonly objName: string;
1895299d 38 private performanceObserver!: PerformanceObserver;
9e23580d 39 private readonly statistics: Statistics;
e1d9a0f4 40 private displayInterval?: NodeJS.Timeout;
560bcf5b 41
9f2e3130 42 private constructor(objId: string, objName: string, uri: URL) {
c0560973 43 this.objId = objId;
9f2e3130 44 this.objName = objName;
aef1b33a 45 this.initializePerformanceObserver();
e7aeea18
JB
46 this.statistics = {
47 id: this.objId ?? 'Object id not specified',
48 name: this.objName ?? 'Object name not specified',
49 uri: uri.toString(),
50 createdAt: new Date(),
1e4b0e4b 51 statisticsData: new Map(),
e7aeea18 52 };
9f2e3130
JB
53 }
54
844e496b
JB
55 public static getInstance(
56 objId: string,
57 objName: string,
5edd8ba0 58 uri: URL,
844e496b 59 ): PerformanceStatistics | undefined {
9f2e3130
JB
60 if (!PerformanceStatistics.instances.has(objId)) {
61 PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri));
62 }
63 return PerformanceStatistics.instances.get(objId);
560bcf5b
JB
64 }
65
aef1b33a 66 public static beginMeasure(id: string): string {
9bf0ef23 67 const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
c63c21bc
JB
68 performance.mark(markId);
69 return markId;
57939a9d
JB
70 }
71
c63c21bc
JB
72 public static endMeasure(name: string, markId: string): void {
73 performance.measure(name, markId);
74 performance.clearMarks(markId);
c60af6ca 75 performance.clearMeasures(name);
aef1b33a
JB
76 }
77
e7aeea18
JB
78 public addRequestStatistic(
79 command: RequestCommand | IncomingRequestCommand,
5edd8ba0 80 messageType: MessageType,
e7aeea18 81 ): void {
7f134aca 82 switch (messageType) {
d2a64eb5 83 case MessageType.CALL_MESSAGE:
e7aeea18
JB
84 if (
85 this.statistics.statisticsData.has(command) &&
c36e3cf0 86 this.statistics.statisticsData.get(command)?.requestCount
e7aeea18 87 ) {
e1d9a0f4 88 ++this.statistics.statisticsData.get(command)!.requestCount!;
7dde0b73 89 } else {
71910904
JB
90 this.statistics.statisticsData.set(command, {
91 ...this.statistics.statisticsData.get(command),
c36e3cf0 92 requestCount: 1,
71910904 93 });
7f134aca
JB
94 }
95 break;
d2a64eb5 96 case MessageType.CALL_RESULT_MESSAGE:
e7aeea18
JB
97 if (
98 this.statistics.statisticsData.has(command) &&
c36e3cf0 99 this.statistics.statisticsData.get(command)?.responseCount
e7aeea18 100 ) {
e1d9a0f4 101 ++this.statistics.statisticsData.get(command)!.responseCount!;
7f134aca 102 } else {
71910904
JB
103 this.statistics.statisticsData.set(command, {
104 ...this.statistics.statisticsData.get(command),
c36e3cf0 105 responseCount: 1,
71910904 106 });
7dde0b73 107 }
7f134aca 108 break;
d2a64eb5 109 case MessageType.CALL_ERROR_MESSAGE:
e7aeea18
JB
110 if (
111 this.statistics.statisticsData.has(command) &&
c36e3cf0 112 this.statistics.statisticsData.get(command)?.errorCount
e7aeea18 113 ) {
e1d9a0f4 114 ++this.statistics.statisticsData.get(command)!.errorCount!;
7f134aca 115 } else {
71910904
JB
116 this.statistics.statisticsData.set(command, {
117 ...this.statistics.statisticsData.get(command),
c36e3cf0 118 errorCount: 1,
71910904 119 });
7f134aca
JB
120 }
121 break;
122 default:
9534e74e 123 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
9f2e3130 124 logger.error(`${this.logPrefix()} wrong message type ${messageType}`);
7f134aca 125 break;
7dde0b73
JB
126 }
127 }
128
aef1b33a 129 public start(): void {
72f041bd
JB
130 this.startLogStatisticsInterval();
131 if (Configuration.getPerformanceStorage().enabled) {
e7aeea18
JB
132 logger.info(
133 `${this.logPrefix()} storage enabled: type ${
134 Configuration.getPerformanceStorage().type
5edd8ba0 135 }, uri: ${Configuration.getPerformanceStorage().uri}`,
e7aeea18 136 );
72f041bd 137 }
7dde0b73
JB
138 }
139
aef1b33a 140 public stop(): void {
8f953431 141 this.stopLogStatisticsInterval();
aef1b33a 142 performance.clearMarks();
c60af6ca 143 performance.clearMeasures();
087a502d
JB
144 this.performanceObserver?.disconnect();
145 }
146
147 public restart(): void {
148 this.stop();
149 this.start();
136c90ba
JB
150 }
151
aef1b33a 152 private initializePerformanceObserver(): void {
72092cfc 153 this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
c60af6ca 154 const lastPerformanceEntry = performanceObserverList.getEntries()[0];
9d1dc4b1
JB
155 // logger.debug(
156 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
e1d9a0f4 157 // lastPerformanceEntry,
9d1dc4b1 158 // );
eb835fa8 159 this.addPerformanceEntryToStatistics(lastPerformanceEntry);
a0ba4ced 160 });
aef1b33a
JB
161 this.performanceObserver.observe({ entryTypes: ['measure'] });
162 }
163
aef1b33a 164 private logStatistics(): void {
c60af6ca
JB
165 logger.info(`${this.logPrefix()}`, {
166 ...this.statistics,
9bf0ef23 167 statisticsData: JSONStringifyWithMapSupport(this.statistics.statisticsData),
c60af6ca 168 });
7dde0b73
JB
169 }
170
72f041bd 171 private startLogStatisticsInterval(): void {
3d48c1c1 172 const logStatisticsInterval = Configuration.getLog().enabled
e1d9a0f4 173 ? Configuration.getLog().statisticsInterval!
3d48c1c1 174 : 0;
8f953431 175 if (logStatisticsInterval > 0 && !this.displayInterval) {
aef1b33a
JB
176 this.displayInterval = setInterval(() => {
177 this.logStatistics();
8f953431 178 }, logStatisticsInterval * 1000);
e7aeea18 179 logger.info(
5edd8ba0 180 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
e7aeea18 181 );
dfe81c8f
JB
182 } else if (this.displayInterval) {
183 logger.info(
5edd8ba0 184 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
dfe81c8f 185 );
3d48c1c1 186 } else if (Configuration.getLog().enabled) {
e7aeea18 187 logger.info(
5edd8ba0 188 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`,
e7aeea18 189 );
7dde0b73
JB
190 }
191 }
192
8f953431
JB
193 private stopLogStatisticsInterval(): void {
194 if (this.displayInterval) {
195 clearInterval(this.displayInterval);
196 delete this.displayInterval;
197 }
198 }
199
b49422c6 200 private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
976d11ec 201 const entryName = entry.name;
7ec46a9a 202 // Initialize command statistics
ff4b895e 203 if (!this.statistics.statisticsData.has(entryName)) {
abe9e9dd 204 this.statistics.statisticsData.set(entryName, {});
7ec46a9a 205 }
b49422c6 206 // Update current statistics
a6b3c6c3 207 this.statistics.updatedAt = new Date();
e1d9a0f4 208 this.statistics.statisticsData.get(entryName)!.timeMeasurementCount =
c36e3cf0 209 (this.statistics.statisticsData.get(entryName)?.timeMeasurementCount ?? 0) + 1;
e1d9a0f4
JB
210 this.statistics.statisticsData.get(entryName)!.currentTimeMeasurement = entry.duration;
211 this.statistics.statisticsData.get(entryName)!.minTimeMeasurement = Math.min(
a8735ef9 212 entry.duration,
5edd8ba0 213 this.statistics.statisticsData.get(entryName)?.minTimeMeasurement ?? Infinity,
a8735ef9 214 );
e1d9a0f4 215 this.statistics.statisticsData.get(entryName)!.maxTimeMeasurement = Math.max(
a8735ef9 216 entry.duration,
5edd8ba0 217 this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement ?? -Infinity,
a8735ef9 218 );
e1d9a0f4 219 this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement =
a8735ef9 220 (this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement ?? 0) + entry.duration;
e1d9a0f4
JB
221 this.statistics.statisticsData.get(entryName)!.avgTimeMeasurement =
222 this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement! /
223 this.statistics.statisticsData.get(entryName)!.timeMeasurementCount!;
f6778d74 224 this.statistics.statisticsData.get(entryName)?.measurementTimeSeries instanceof CircularArray
e7aeea18
JB
225 ? this.statistics.statisticsData
226 .get(entryName)
f6778d74 227 ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration })
e1d9a0f4 228 : (this.statistics.statisticsData.get(entryName)!.measurementTimeSeries =
f6778d74 229 new CircularArray<TimestampedData>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
e7aeea18
JB
230 timestamp: entry.startTime,
231 value: entry.duration,
232 }));
e1d9a0f4
JB
233 this.statistics.statisticsData.get(entryName)!.medTimeMeasurement = median(
234 extractTimeSeriesValues(
235 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
236 ),
e7aeea18 237 );
e1d9a0f4 238 this.statistics.statisticsData.get(entryName)!.ninetyFiveThPercentileTimeMeasurement =
4884b8d3 239 nthPercentile(
da55bd34 240 extractTimeSeriesValues(
e1d9a0f4 241 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
e7aeea18 242 ),
5edd8ba0 243 95,
e7aeea18 244 );
e1d9a0f4
JB
245 this.statistics.statisticsData.get(entryName)!.stdDevTimeMeasurement = stdDeviation(
246 extractTimeSeriesValues(
247 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
248 ),
e7aeea18 249 );
72f041bd 250 if (Configuration.getPerformanceStorage().enabled) {
c8faabc8 251 parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics));
72f041bd 252 }
7ec46a9a
JB
253 }
254
8b7072dc 255 private logPrefix = (): string => {
9bf0ef23 256 return logPrefix(` ${this.objName} | Performance statistics`);
8b7072dc 257 };
7dde0b73 258}