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