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