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