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