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