fix: strict number check
[e-mobility-charging-stations-simulator.git] / src / performance / PerformanceStatistics.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
01f4001e 3import { type PerformanceEntry, PerformanceObserver, performance } from 'node:perf_hooks';
130783a7 4import type { URL } from 'node:url';
01f4001e 5import { parentPort } from 'node:worker_threads';
63b48f77 6
2896e06d 7import { MessageChannelUtils } from '../charging-station';
268a74bb
JB
8import {
9 type IncomingRequestCommand,
10 MessageType,
11 type RequestCommand,
12 type Statistics,
13 type TimeSeries,
14} from '../types';
60a74391 15import { CircularArray, Configuration, Constants, Utils, logger } from '../utils';
7dde0b73 16
268a74bb 17export class PerformanceStatistics {
e7aeea18
JB
18 private static readonly instances: Map<string, PerformanceStatistics> = new Map<
19 string,
20 PerformanceStatistics
21 >();
10068088 22
9e23580d 23 private readonly objId: string;
9f2e3130 24 private readonly objName: string;
1895299d 25 private performanceObserver!: PerformanceObserver;
9e23580d 26 private readonly statistics: Statistics;
1895299d 27 private displayInterval!: NodeJS.Timeout;
560bcf5b 28
9f2e3130 29 private constructor(objId: string, objName: string, uri: URL) {
c0560973 30 this.objId = objId;
9f2e3130 31 this.objName = objName;
aef1b33a 32 this.initializePerformanceObserver();
e7aeea18
JB
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(),
1e4b0e4b 38 statisticsData: new Map(),
e7aeea18 39 };
9f2e3130
JB
40 }
41
844e496b
JB
42 public static getInstance(
43 objId: string,
44 objName: string,
45 uri: URL
46 ): PerformanceStatistics | undefined {
9f2e3130
JB
47 if (!PerformanceStatistics.instances.has(objId)) {
48 PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri));
49 }
50 return PerformanceStatistics.instances.get(objId);
560bcf5b
JB
51 }
52
aef1b33a 53 public static beginMeasure(id: string): string {
14ecae6a 54 const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
c63c21bc
JB
55 performance.mark(markId);
56 return markId;
57939a9d
JB
57 }
58
c63c21bc
JB
59 public static endMeasure(name: string, markId: string): void {
60 performance.measure(name, markId);
61 performance.clearMarks(markId);
c60af6ca 62 performance.clearMeasures(name);
aef1b33a
JB
63 }
64
e7aeea18
JB
65 public addRequestStatistic(
66 command: RequestCommand | IncomingRequestCommand,
67 messageType: MessageType
68 ): void {
7f134aca 69 switch (messageType) {
d2a64eb5 70 case MessageType.CALL_MESSAGE:
e7aeea18
JB
71 if (
72 this.statistics.statisticsData.has(command) &&
73 this.statistics.statisticsData.get(command)?.countRequest
74 ) {
ff4b895e 75 this.statistics.statisticsData.get(command).countRequest++;
7dde0b73 76 } else {
71910904
JB
77 this.statistics.statisticsData.set(command, {
78 ...this.statistics.statisticsData.get(command),
79 countRequest: 1,
80 });
7f134aca
JB
81 }
82 break;
d2a64eb5 83 case MessageType.CALL_RESULT_MESSAGE:
e7aeea18
JB
84 if (
85 this.statistics.statisticsData.has(command) &&
86 this.statistics.statisticsData.get(command)?.countResponse
87 ) {
ff4b895e 88 this.statistics.statisticsData.get(command).countResponse++;
7f134aca 89 } else {
71910904
JB
90 this.statistics.statisticsData.set(command, {
91 ...this.statistics.statisticsData.get(command),
92 countResponse: 1,
93 });
7dde0b73 94 }
7f134aca 95 break;
d2a64eb5 96 case MessageType.CALL_ERROR_MESSAGE:
e7aeea18
JB
97 if (
98 this.statistics.statisticsData.has(command) &&
99 this.statistics.statisticsData.get(command)?.countError
100 ) {
ff4b895e 101 this.statistics.statisticsData.get(command).countError++;
7f134aca 102 } else {
71910904
JB
103 this.statistics.statisticsData.set(command, {
104 ...this.statistics.statisticsData.get(command),
105 countError: 1,
106 });
7f134aca
JB
107 }
108 break;
109 default:
9534e74e 110 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
9f2e3130 111 logger.error(`${this.logPrefix()} wrong message type ${messageType}`);
7f134aca 112 break;
7dde0b73
JB
113 }
114 }
115
aef1b33a 116 public start(): void {
72f041bd
JB
117 this.startLogStatisticsInterval();
118 if (Configuration.getPerformanceStorage().enabled) {
e7aeea18
JB
119 logger.info(
120 `${this.logPrefix()} storage enabled: type ${
121 Configuration.getPerformanceStorage().type
122 }, uri: ${Configuration.getPerformanceStorage().uri}`
123 );
72f041bd 124 }
7dde0b73
JB
125 }
126
aef1b33a 127 public stop(): void {
8f953431 128 this.stopLogStatisticsInterval();
aef1b33a 129 performance.clearMarks();
c60af6ca 130 performance.clearMeasures();
087a502d
JB
131 this.performanceObserver?.disconnect();
132 }
133
134 public restart(): void {
135 this.stop();
136 this.start();
136c90ba
JB
137 }
138
aef1b33a 139 private initializePerformanceObserver(): void {
72092cfc 140 this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
c60af6ca 141 const lastPerformanceEntry = performanceObserverList.getEntries()[0];
9d1dc4b1
JB
142 // logger.debug(
143 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
144 // lastPerformanceEntry
145 // );
eb835fa8 146 this.addPerformanceEntryToStatistics(lastPerformanceEntry);
a0ba4ced 147 });
aef1b33a
JB
148 this.performanceObserver.observe({ entryTypes: ['measure'] });
149 }
150
aef1b33a 151 private logStatistics(): void {
c60af6ca
JB
152 logger.info(`${this.logPrefix()}`, {
153 ...this.statistics,
154 statisticsData: Utils.JSONStringifyWithMapSupport(this.statistics.statisticsData),
155 });
7dde0b73
JB
156 }
157
72f041bd 158 private startLogStatisticsInterval(): void {
8f953431
JB
159 const logStatisticsInterval = Configuration.getLogStatisticsInterval();
160 if (logStatisticsInterval > 0 && !this.displayInterval) {
aef1b33a
JB
161 this.displayInterval = setInterval(() => {
162 this.logStatistics();
8f953431 163 }, logStatisticsInterval * 1000);
e7aeea18 164 logger.info(
8f953431 165 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(logStatisticsInterval)}`
e7aeea18 166 );
dfe81c8f
JB
167 } else if (this.displayInterval) {
168 logger.info(
169 `${this.logPrefix()} already logged every ${Utils.formatDurationSeconds(
8f953431 170 logStatisticsInterval
dfe81c8f
JB
171 )}`
172 );
aef1b33a 173 } else {
e7aeea18 174 logger.info(
8f953431 175 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
e7aeea18 176 );
7dde0b73
JB
177 }
178 }
179
8f953431
JB
180 private stopLogStatisticsInterval(): void {
181 if (this.displayInterval) {
182 clearInterval(this.displayInterval);
183 delete this.displayInterval;
184 }
185 }
186
6bf6769e 187 private median(dataSet: number[]): number {
5f7e72c1 188 if (Array.isArray(dataSet) === true && dataSet.length === 1) {
6bf6769e
JB
189 return dataSet[0];
190 }
e7aeea18 191 const sortedDataSet = dataSet.slice().sort((a, b) => a - b);
6bf6769e 192 const middleIndex = Math.floor(sortedDataSet.length / 2);
260f9dcc 193 if (sortedDataSet.length % 2 === 0) {
6bf6769e
JB
194 return sortedDataSet[middleIndex / 2];
195 }
e7aeea18 196 return (sortedDataSet[middleIndex - 1] + sortedDataSet[middleIndex]) / 2;
6bf6769e
JB
197 }
198
b49422c6
JB
199 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
200 private percentile(dataSet: number[], percentile: number): number {
201 if (percentile < 0 && percentile > 100) {
202 throw new RangeError('Percentile is not between 0 and 100');
203 }
204 if (Utils.isEmptyArray(dataSet)) {
205 return 0;
206 }
e7aeea18 207 const sortedDataSet = dataSet.slice().sort((a, b) => a - b);
b49422c6
JB
208 if (percentile === 0) {
209 return sortedDataSet[0];
210 }
211 if (percentile === 100) {
212 return sortedDataSet[sortedDataSet.length - 1];
213 }
e7aeea18 214 const percentileIndex = (percentile / 100) * sortedDataSet.length - 1;
b49422c6
JB
215 if (Number.isInteger(percentileIndex)) {
216 return (sortedDataSet[percentileIndex] + sortedDataSet[percentileIndex + 1]) / 2;
217 }
218 return sortedDataSet[Math.round(percentileIndex)];
219 }
220
aeada1fa
JB
221 private stdDeviation(dataSet: number[]): number {
222 let totalDataSet = 0;
223 for (const data of dataSet) {
224 totalDataSet += data;
225 }
226 const dataSetMean = totalDataSet / dataSet.length;
227 let totalGeometricDeviation = 0;
228 for (const data of dataSet) {
229 const deviation = data - dataSetMean;
230 totalGeometricDeviation += deviation * deviation;
231 }
232 return Math.sqrt(totalGeometricDeviation / dataSet.length);
233 }
234
b49422c6 235 private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
976d11ec 236 const entryName = entry.name;
7ec46a9a 237 // Initialize command statistics
ff4b895e 238 if (!this.statistics.statisticsData.has(entryName)) {
abe9e9dd 239 this.statistics.statisticsData.set(entryName, {});
7ec46a9a 240 }
b49422c6 241 // Update current statistics
a6b3c6c3 242 this.statistics.updatedAt = new Date();
e7aeea18
JB
243 this.statistics.statisticsData.get(entryName).countTimeMeasurement =
244 this.statistics.statisticsData.get(entryName)?.countTimeMeasurement
245 ? this.statistics.statisticsData.get(entryName).countTimeMeasurement + 1
246 : 1;
ff4b895e 247 this.statistics.statisticsData.get(entryName).currentTimeMeasurement = entry.duration;
e7aeea18
JB
248 this.statistics.statisticsData.get(entryName).minTimeMeasurement =
249 this.statistics.statisticsData.get(entryName)?.minTimeMeasurement
250 ? this.statistics.statisticsData.get(entryName).minTimeMeasurement > entry.duration
251 ? entry.duration
252 : this.statistics.statisticsData.get(entryName).minTimeMeasurement
253 : entry.duration;
254 this.statistics.statisticsData.get(entryName).maxTimeMeasurement =
255 this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement
256 ? this.statistics.statisticsData.get(entryName).maxTimeMeasurement < entry.duration
257 ? entry.duration
258 : this.statistics.statisticsData.get(entryName).maxTimeMeasurement
259 : entry.duration;
260 this.statistics.statisticsData.get(entryName).totalTimeMeasurement =
261 this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement
262 ? this.statistics.statisticsData.get(entryName).totalTimeMeasurement + entry.duration
263 : entry.duration;
264 this.statistics.statisticsData.get(entryName).avgTimeMeasurement =
265 this.statistics.statisticsData.get(entryName).totalTimeMeasurement /
266 this.statistics.statisticsData.get(entryName).countTimeMeasurement;
9a15316c 267 this.statistics.statisticsData.get(entryName)?.timeMeasurementSeries instanceof CircularArray
e7aeea18
JB
268 ? this.statistics.statisticsData
269 .get(entryName)
72092cfc 270 ?.timeMeasurementSeries?.push({ timestamp: entry.startTime, value: entry.duration })
e7aeea18 271 : (this.statistics.statisticsData.get(entryName).timeMeasurementSeries =
9a15316c 272 new CircularArray<TimeSeries>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
e7aeea18
JB
273 timestamp: entry.startTime,
274 value: entry.duration,
275 }));
276 this.statistics.statisticsData.get(entryName).medTimeMeasurement = this.median(
277 this.extractTimeSeriesValues(
278 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
279 )
280 );
281 this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement =
282 this.percentile(
283 this.extractTimeSeriesValues(
284 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
285 ),
286 95
287 );
288 this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = this.stdDeviation(
289 this.extractTimeSeriesValues(
290 this.statistics.statisticsData.get(entryName).timeMeasurementSeries
291 )
292 );
72f041bd 293 if (Configuration.getPerformanceStorage().enabled) {
1895299d 294 parentPort?.postMessage(
32de5a57
LM
295 MessageChannelUtils.buildPerformanceStatisticsMessage(this.statistics)
296 );
72f041bd 297 }
7ec46a9a
JB
298 }
299
0c142310 300 private extractTimeSeriesValues(timeSeries: CircularArray<TimeSeries>): number[] {
72092cfc 301 return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
0c142310
JB
302 }
303
8b7072dc 304 private logPrefix = (): string => {
9f2e3130 305 return Utils.logPrefix(` ${this.objName} | Performance statistics`);
8b7072dc 306 };
7dde0b73 307}