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