refactor: cleanup log messages
[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();
864e5f8d 134 const performanceStorageConfiguration =
5d049829
JB
135 Configuration.getConfigurationSection<StorageConfiguration>(
136 ConfigurationSection.performanceStorage,
864e5f8d
JB
137 );
138 if (performanceStorageConfiguration.enabled) {
e7aeea18 139 logger.info(
864e5f8d
JB
140 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
141 performanceStorageConfiguration.uri
5d049829 142 }`,
e7aeea18 143 );
72f041bd 144 }
7dde0b73
JB
145 }
146
aef1b33a 147 public stop(): void {
8f953431 148 this.stopLogStatisticsInterval();
aef1b33a 149 performance.clearMarks();
c60af6ca 150 performance.clearMeasures();
087a502d
JB
151 this.performanceObserver?.disconnect();
152 }
153
154 public restart(): void {
155 this.stop();
156 this.start();
136c90ba
JB
157 }
158
aef1b33a 159 private initializePerformanceObserver(): void {
72092cfc 160 this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
c60af6ca 161 const lastPerformanceEntry = performanceObserverList.getEntries()[0];
9d1dc4b1
JB
162 // logger.debug(
163 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
e1d9a0f4 164 // lastPerformanceEntry,
9d1dc4b1 165 // );
eb835fa8 166 this.addPerformanceEntryToStatistics(lastPerformanceEntry);
a0ba4ced 167 });
aef1b33a
JB
168 this.performanceObserver.observe({ entryTypes: ['measure'] });
169 }
170
aef1b33a 171 private logStatistics(): void {
c60af6ca
JB
172 logger.info(`${this.logPrefix()}`, {
173 ...this.statistics,
9bf0ef23 174 statisticsData: JSONStringifyWithMapSupport(this.statistics.statisticsData),
c60af6ca 175 });
7dde0b73
JB
176 }
177
72f041bd 178 private startLogStatisticsInterval(): void {
864e5f8d 179 const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
5d049829 180 ConfigurationSection.log,
864e5f8d
JB
181 );
182 const logStatisticsInterval = logConfiguration.enabled
183 ? logConfiguration.statisticsInterval!
3d48c1c1 184 : 0;
8f953431 185 if (logStatisticsInterval > 0 && !this.displayInterval) {
aef1b33a
JB
186 this.displayInterval = setInterval(() => {
187 this.logStatistics();
8f953431 188 }, logStatisticsInterval * 1000);
e7aeea18 189 logger.info(
5edd8ba0 190 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
e7aeea18 191 );
dfe81c8f
JB
192 } else if (this.displayInterval) {
193 logger.info(
5edd8ba0 194 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
dfe81c8f 195 );
864e5f8d 196 } else if (logConfiguration.enabled) {
e7aeea18 197 logger.info(
04c32a95 198 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`,
e7aeea18 199 );
7dde0b73
JB
200 }
201 }
202
8f953431
JB
203 private stopLogStatisticsInterval(): void {
204 if (this.displayInterval) {
205 clearInterval(this.displayInterval);
206 delete this.displayInterval;
207 }
208 }
209
b49422c6 210 private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
976d11ec 211 const entryName = entry.name;
7ec46a9a 212 // Initialize command statistics
ff4b895e 213 if (!this.statistics.statisticsData.has(entryName)) {
abe9e9dd 214 this.statistics.statisticsData.set(entryName, {});
7ec46a9a 215 }
b49422c6 216 // Update current statistics
a6b3c6c3 217 this.statistics.updatedAt = new Date();
e1d9a0f4 218 this.statistics.statisticsData.get(entryName)!.timeMeasurementCount =
c36e3cf0 219 (this.statistics.statisticsData.get(entryName)?.timeMeasurementCount ?? 0) + 1;
e1d9a0f4
JB
220 this.statistics.statisticsData.get(entryName)!.currentTimeMeasurement = entry.duration;
221 this.statistics.statisticsData.get(entryName)!.minTimeMeasurement = Math.min(
a8735ef9 222 entry.duration,
5edd8ba0 223 this.statistics.statisticsData.get(entryName)?.minTimeMeasurement ?? Infinity,
a8735ef9 224 );
e1d9a0f4 225 this.statistics.statisticsData.get(entryName)!.maxTimeMeasurement = Math.max(
a8735ef9 226 entry.duration,
5edd8ba0 227 this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement ?? -Infinity,
a8735ef9 228 );
e1d9a0f4 229 this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement =
a8735ef9 230 (this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement ?? 0) + entry.duration;
e1d9a0f4
JB
231 this.statistics.statisticsData.get(entryName)!.avgTimeMeasurement =
232 this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement! /
233 this.statistics.statisticsData.get(entryName)!.timeMeasurementCount!;
f6778d74 234 this.statistics.statisticsData.get(entryName)?.measurementTimeSeries instanceof CircularArray
e7aeea18
JB
235 ? this.statistics.statisticsData
236 .get(entryName)
f6778d74 237 ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration })
e1d9a0f4 238 : (this.statistics.statisticsData.get(entryName)!.measurementTimeSeries =
f6778d74 239 new CircularArray<TimestampedData>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
e7aeea18
JB
240 timestamp: entry.startTime,
241 value: entry.duration,
242 }));
e1d9a0f4
JB
243 this.statistics.statisticsData.get(entryName)!.medTimeMeasurement = median(
244 extractTimeSeriesValues(
245 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
246 ),
e7aeea18 247 );
e1d9a0f4 248 this.statistics.statisticsData.get(entryName)!.ninetyFiveThPercentileTimeMeasurement =
4884b8d3 249 nthPercentile(
da55bd34 250 extractTimeSeriesValues(
e1d9a0f4 251 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
e7aeea18 252 ),
5edd8ba0 253 95,
e7aeea18 254 );
e1d9a0f4
JB
255 this.statistics.statisticsData.get(entryName)!.stdDevTimeMeasurement = stdDeviation(
256 extractTimeSeriesValues(
257 this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
258 ),
e7aeea18 259 );
5d049829
JB
260 if (
261 Configuration.getConfigurationSection<StorageConfiguration>(
262 ConfigurationSection.performanceStorage,
263 ).enabled
264 ) {
c8faabc8 265 parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics));
72f041bd 266 }
7ec46a9a
JB
267 }
268
8b7072dc 269 private logPrefix = (): string => {
9bf0ef23 270 return logPrefix(` ${this.objName} | Performance statistics`);
8b7072dc 271 };
7dde0b73 272}