refactor(simulator): switch utils to internal module export/import
[e-mobility-charging-stations-simulator.git] / src / performance / PerformanceStatistics.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import type { URL } from 'node:url';
4 import { PerformanceEntry, PerformanceObserver, performance } from 'perf_hooks';
5 import { parentPort } from '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(
78 command,
79 Object.assign({ countRequest: 1 }, this.statistics.statisticsData.get(command))
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(
91 command,
92 Object.assign({ countResponse: 1 }, this.statistics.statisticsData.get(command))
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(
104 command,
105 Object.assign({ countError: 1 }, this.statistics.statisticsData.get(command))
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 if (this.displayInterval) {
129 clearInterval(this.displayInterval);
130 }
131 performance.clearMarks();
132 performance.clearMeasures();
133 this.performanceObserver?.disconnect();
134 }
135
136 public restart(): void {
137 this.stop();
138 this.start();
139 }
140
141 private initializePerformanceObserver(): void {
142 this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
143 const lastPerformanceEntry = performanceObserverList.getEntries()[0];
144 // logger.debug(
145 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
146 // lastPerformanceEntry
147 // );
148 this.addPerformanceEntryToStatistics(lastPerformanceEntry);
149 });
150 this.performanceObserver.observe({ entryTypes: ['measure'] });
151 }
152
153 private logStatistics(): void {
154 logger.info(`${this.logPrefix()}`, {
155 ...this.statistics,
156 statisticsData: Utils.JSONStringifyWithMapSupport(this.statistics.statisticsData),
157 });
158 }
159
160 private startLogStatisticsInterval(): void {
161 if (Configuration.getLogStatisticsInterval() > 0) {
162 this.displayInterval = setInterval(() => {
163 this.logStatistics();
164 }, Configuration.getLogStatisticsInterval() * 1000);
165 logger.info(
166 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(
167 Configuration.getLogStatisticsInterval()
168 )}`
169 );
170 } else {
171 logger.info(
172 `${this.logPrefix()} log interval is set to ${Configuration.getLogStatisticsInterval()?.toString()}. Not logging statistics`
173 );
174 }
175 }
176
177 private median(dataSet: number[]): number {
178 if (Array.isArray(dataSet) === true && dataSet.length === 1) {
179 return dataSet[0];
180 }
181 const sortedDataSet = dataSet.slice().sort((a, b) => a - b);
182 const middleIndex = Math.floor(sortedDataSet.length / 2);
183 if (sortedDataSet.length % 2) {
184 return sortedDataSet[middleIndex / 2];
185 }
186 return (sortedDataSet[middleIndex - 1] + sortedDataSet[middleIndex]) / 2;
187 }
188
189 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
190 private percentile(dataSet: number[], percentile: number): number {
191 if (percentile < 0 && percentile > 100) {
192 throw new RangeError('Percentile is not between 0 and 100');
193 }
194 if (Utils.isEmptyArray(dataSet)) {
195 return 0;
196 }
197 const sortedDataSet = dataSet.slice().sort((a, b) => a - b);
198 if (percentile === 0) {
199 return sortedDataSet[0];
200 }
201 if (percentile === 100) {
202 return sortedDataSet[sortedDataSet.length - 1];
203 }
204 const percentileIndex = (percentile / 100) * sortedDataSet.length - 1;
205 if (Number.isInteger(percentileIndex)) {
206 return (sortedDataSet[percentileIndex] + sortedDataSet[percentileIndex + 1]) / 2;
207 }
208 return sortedDataSet[Math.round(percentileIndex)];
209 }
210
211 private stdDeviation(dataSet: number[]): number {
212 let totalDataSet = 0;
213 for (const data of dataSet) {
214 totalDataSet += data;
215 }
216 const dataSetMean = totalDataSet / dataSet.length;
217 let totalGeometricDeviation = 0;
218 for (const data of dataSet) {
219 const deviation = data - dataSetMean;
220 totalGeometricDeviation += deviation * deviation;
221 }
222 return Math.sqrt(totalGeometricDeviation / dataSet.length);
223 }
224
225 private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
226 const entryName = entry.name;
227 // Initialize command statistics
228 if (!this.statistics.statisticsData.has(entryName)) {
229 this.statistics.statisticsData.set(entryName, {});
230 }
231 // Update current statistics
232 this.statistics.updatedAt = new Date();
233 this.statistics.statisticsData.get(entryName).countTimeMeasurement =
234 this.statistics.statisticsData.get(entryName)?.countTimeMeasurement
235 ? this.statistics.statisticsData.get(entryName).countTimeMeasurement + 1
236 : 1;
237 this.statistics.statisticsData.get(entryName).currentTimeMeasurement = entry.duration;
238 this.statistics.statisticsData.get(entryName).minTimeMeasurement =
239 this.statistics.statisticsData.get(entryName)?.minTimeMeasurement
240 ? this.statistics.statisticsData.get(entryName).minTimeMeasurement > entry.duration
241 ? entry.duration
242 : this.statistics.statisticsData.get(entryName).minTimeMeasurement
243 : entry.duration;
244 this.statistics.statisticsData.get(entryName).maxTimeMeasurement =
245 this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement
246 ? this.statistics.statisticsData.get(entryName).maxTimeMeasurement < entry.duration
247 ? entry.duration
248 : this.statistics.statisticsData.get(entryName).maxTimeMeasurement
249 : entry.duration;
250 this.statistics.statisticsData.get(entryName).totalTimeMeasurement =
251 this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement
252 ? this.statistics.statisticsData.get(entryName).totalTimeMeasurement + entry.duration
253 : entry.duration;
254 this.statistics.statisticsData.get(entryName).avgTimeMeasurement =
255 this.statistics.statisticsData.get(entryName).totalTimeMeasurement /
256 this.statistics.statisticsData.get(entryName).countTimeMeasurement;
257 this.statistics.statisticsData.get(entryName)?.timeMeasurementSeries instanceof CircularArray
258 ? this.statistics.statisticsData
259 .get(entryName)
260 ?.timeMeasurementSeries?.push({ timestamp: entry.startTime, value: entry.duration })
261 : (this.statistics.statisticsData.get(entryName).timeMeasurementSeries =
262 new CircularArray<TimeSeries>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
263 timestamp: entry.startTime,
264 value: entry.duration,
265 }));
266 this.statistics.statisticsData.get(entryName).medTimeMeasurement = this.median(
267 this.extractTimeSeriesValues(
268 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
269 )
270 );
271 this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement =
272 this.percentile(
273 this.extractTimeSeriesValues(
274 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
275 ),
276 95
277 );
278 this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = this.stdDeviation(
279 this.extractTimeSeriesValues(
280 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
281 )
282 );
283 if (Configuration.getPerformanceStorage().enabled) {
284 parentPort?.postMessage(
285 MessageChannelUtils.buildPerformanceStatisticsMessage(this.statistics)
286 );
287 }
288 }
289
290 private extractTimeSeriesValues(timeSeries: CircularArray<TimeSeries>): number[] {
291 return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
292 }
293
294 private logPrefix = (): string => {
295 return Utils.logPrefix(` ${this.objName} | Performance statistics`);
296 };
297 }