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