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