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