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