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