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