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