refactor: cleanup performance statistics variables namespace
[e-mobility-charging-stations-simulator.git] / src / performance / PerformanceStatistics.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import { type PerformanceEntry, PerformanceObserver, performance } from 'node:perf_hooks';
4 import type { URL } from 'node:url';
5 import { parentPort } from 'node:worker_threads';
6
7 import {
8 type IncomingRequestCommand,
9 MessageType,
10 type RequestCommand,
11 type Statistics,
12 type TimestampedData,
13 } from '../types';
14 import {
15 CircularArray,
16 Configuration,
17 Constants,
18 JSONStringifyWithMapSupport,
19 buildPerformanceStatisticsMessage,
20 formatDurationSeconds,
21 generateUUID,
22 logPrefix,
23 logger,
24 median,
25 nthPercentile,
26 stdDeviation,
27 } from '../utils';
28
29 export class PerformanceStatistics {
30 private static readonly instances: Map<string, PerformanceStatistics> = new Map<
31 string,
32 PerformanceStatistics
33 >();
34
35 private readonly objId: string;
36 private readonly objName: string;
37 private performanceObserver!: PerformanceObserver;
38 private readonly statistics: Statistics;
39 private displayInterval!: NodeJS.Timeout;
40
41 private constructor(objId: string, objName: string, uri: URL) {
42 this.objId = objId;
43 this.objName = objName;
44 this.initializePerformanceObserver();
45 this.statistics = {
46 id: this.objId ?? 'Object id not specified',
47 name: this.objName ?? 'Object name not specified',
48 uri: uri.toString(),
49 createdAt: new Date(),
50 statisticsData: new Map(),
51 };
52 }
53
54 public static getInstance(
55 objId: string,
56 objName: string,
57 uri: URL
58 ): PerformanceStatistics | undefined {
59 if (!PerformanceStatistics.instances.has(objId)) {
60 PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri));
61 }
62 return PerformanceStatistics.instances.get(objId);
63 }
64
65 public static beginMeasure(id: string): string {
66 const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
67 performance.mark(markId);
68 return markId;
69 }
70
71 public static endMeasure(name: string, markId: string): void {
72 performance.measure(name, markId);
73 performance.clearMarks(markId);
74 performance.clearMeasures(name);
75 }
76
77 public addRequestStatistic(
78 command: RequestCommand | IncomingRequestCommand,
79 messageType: MessageType
80 ): void {
81 switch (messageType) {
82 case MessageType.CALL_MESSAGE:
83 if (
84 this.statistics.statisticsData.has(command) &&
85 this.statistics.statisticsData.get(command)?.countRequest
86 ) {
87 ++this.statistics.statisticsData.get(command).countRequest;
88 } else {
89 this.statistics.statisticsData.set(command, {
90 ...this.statistics.statisticsData.get(command),
91 countRequest: 1,
92 });
93 }
94 break;
95 case MessageType.CALL_RESULT_MESSAGE:
96 if (
97 this.statistics.statisticsData.has(command) &&
98 this.statistics.statisticsData.get(command)?.countResponse
99 ) {
100 ++this.statistics.statisticsData.get(command).countResponse;
101 } else {
102 this.statistics.statisticsData.set(command, {
103 ...this.statistics.statisticsData.get(command),
104 countResponse: 1,
105 });
106 }
107 break;
108 case MessageType.CALL_ERROR_MESSAGE:
109 if (
110 this.statistics.statisticsData.has(command) &&
111 this.statistics.statisticsData.get(command)?.countError
112 ) {
113 ++this.statistics.statisticsData.get(command).countError;
114 } else {
115 this.statistics.statisticsData.set(command, {
116 ...this.statistics.statisticsData.get(command),
117 countError: 1,
118 });
119 }
120 break;
121 default:
122 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
123 logger.error(`${this.logPrefix()} wrong message type ${messageType}`);
124 break;
125 }
126 }
127
128 public start(): void {
129 this.startLogStatisticsInterval();
130 if (Configuration.getPerformanceStorage().enabled) {
131 logger.info(
132 `${this.logPrefix()} storage enabled: type ${
133 Configuration.getPerformanceStorage().type
134 }, uri: ${Configuration.getPerformanceStorage().uri}`
135 );
136 }
137 }
138
139 public stop(): void {
140 this.stopLogStatisticsInterval();
141 performance.clearMarks();
142 performance.clearMeasures();
143 this.performanceObserver?.disconnect();
144 }
145
146 public restart(): void {
147 this.stop();
148 this.start();
149 }
150
151 private initializePerformanceObserver(): void {
152 this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
153 const lastPerformanceEntry = performanceObserverList.getEntries()[0];
154 // logger.debug(
155 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
156 // lastPerformanceEntry
157 // );
158 this.addPerformanceEntryToStatistics(lastPerformanceEntry);
159 });
160 this.performanceObserver.observe({ entryTypes: ['measure'] });
161 }
162
163 private logStatistics(): void {
164 logger.info(`${this.logPrefix()}`, {
165 ...this.statistics,
166 statisticsData: JSONStringifyWithMapSupport(this.statistics.statisticsData),
167 });
168 }
169
170 private startLogStatisticsInterval(): void {
171 const logStatisticsInterval = Configuration.getLog().enabled
172 ? Configuration.getLog().statisticsInterval
173 : 0;
174 if (logStatisticsInterval > 0 && !this.displayInterval) {
175 this.displayInterval = setInterval(() => {
176 this.logStatistics();
177 }, logStatisticsInterval * 1000);
178 logger.info(
179 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
180 );
181 } else if (this.displayInterval) {
182 logger.info(
183 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
184 );
185 } else if (Configuration.getLog().enabled) {
186 logger.info(
187 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
188 );
189 }
190 }
191
192 private stopLogStatisticsInterval(): void {
193 if (this.displayInterval) {
194 clearInterval(this.displayInterval);
195 delete this.displayInterval;
196 }
197 }
198
199 private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
200 const entryName = entry.name;
201 // Initialize command statistics
202 if (!this.statistics.statisticsData.has(entryName)) {
203 this.statistics.statisticsData.set(entryName, {});
204 }
205 // Update current statistics
206 this.statistics.updatedAt = new Date();
207 this.statistics.statisticsData.get(entryName).countTimeMeasurement =
208 (this.statistics.statisticsData.get(entryName)?.countTimeMeasurement ?? 0) + 1;
209 this.statistics.statisticsData.get(entryName).currentTimeMeasurement = entry.duration;
210 this.statistics.statisticsData.get(entryName).minTimeMeasurement = Math.min(
211 entry.duration,
212 this.statistics.statisticsData.get(entryName)?.minTimeMeasurement ?? Infinity
213 );
214 this.statistics.statisticsData.get(entryName).maxTimeMeasurement = Math.max(
215 entry.duration,
216 this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement ?? -Infinity
217 );
218 this.statistics.statisticsData.get(entryName).totalTimeMeasurement =
219 (this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement ?? 0) + entry.duration;
220 this.statistics.statisticsData.get(entryName).avgTimeMeasurement =
221 this.statistics.statisticsData.get(entryName).totalTimeMeasurement /
222 this.statistics.statisticsData.get(entryName).countTimeMeasurement;
223 this.statistics.statisticsData.get(entryName)?.measurementTimeSeries instanceof CircularArray
224 ? this.statistics.statisticsData
225 .get(entryName)
226 ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration })
227 : (this.statistics.statisticsData.get(entryName).measurementTimeSeries =
228 new CircularArray<TimestampedData>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
229 timestamp: entry.startTime,
230 value: entry.duration,
231 }));
232 this.statistics.statisticsData.get(entryName).medTimeMeasurement = median(
233 this.extractTimeSeriesValues(
234 this.statistics.statisticsData.get(entryName).measurementTimeSeries
235 )
236 );
237 this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement =
238 nthPercentile(
239 this.extractTimeSeriesValues(
240 this.statistics.statisticsData.get(entryName).measurementTimeSeries
241 ),
242 95
243 );
244 this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = stdDeviation(
245 this.extractTimeSeriesValues(
246 this.statistics.statisticsData.get(entryName).measurementTimeSeries
247 )
248 );
249 if (Configuration.getPerformanceStorage().enabled) {
250 parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics));
251 }
252 }
253
254 private extractTimeSeriesValues(timeSeries: CircularArray<TimestampedData>): number[] {
255 return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
256 }
257
258 private logPrefix = (): string => {
259 return logPrefix(` ${this.objName} | Performance statistics`);
260 };
261 }