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