feat!: handle Set at JSON serialization to string
[e-mobility-charging-stations-simulator.git] / src / performance / PerformanceStatistics.ts
CommitLineData
a19b897d 1// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
c8eeb62b 2
4c3f6c20 3import { performance, type PerformanceEntry, PerformanceObserver } from 'node:perf_hooks'
66a7748d
JB
4import type { URL } from 'node:url'
5import { parentPort } from 'node:worker_threads'
63b48f77 6
66a7748d 7import { secondsToMilliseconds } from 'date-fns'
be4c6702 8
2466918c 9import { BaseError } from '../exception/index.js'
268a74bb 10import {
5d049829 11 ConfigurationSection,
268a74bb 12 type IncomingRequestCommand,
5d049829 13 type LogConfiguration,
276e05ae 14 MapStringifyFormat,
268a74bb
JB
15 MessageType,
16 type RequestCommand,
17 type Statistics,
94032f3e 18 type StatisticsData,
5d049829 19 type StorageConfiguration,
66a7748d
JB
20 type TimestampedData
21} from '../types/index.js'
7671fa0b 22import {
4c3f6c20
JB
23 average,
24 buildPerformanceStatisticsMessage,
7671fa0b
JB
25 CircularArray,
26 Configuration,
27 Constants,
da55bd34 28 extractTimeSeriesValues,
9bf0ef23
JB
29 formatDurationSeconds,
30 generateUUID,
276e05ae 31 JSONStringify,
7671fa0b 32 logger,
4c3f6c20 33 logPrefix,
5adf6ca4 34 max,
4884b8d3 35 median,
5adf6ca4 36 min,
4884b8d3 37 nthPercentile,
66a7748d
JB
38 stdDeviation
39} from '../utils/index.js'
7dde0b73 40
268a74bb 41export class PerformanceStatistics {
e7aeea18 42 private static readonly instances: Map<string, PerformanceStatistics> = new Map<
66a7748d
JB
43 string,
44 PerformanceStatistics
45 >()
10068088 46
5199f9fd
JB
47 private readonly objId: string | undefined
48 private readonly objName: string | undefined
66a7748d
JB
49 private performanceObserver!: PerformanceObserver
50 private readonly statistics: Statistics
51 private displayInterval?: NodeJS.Timeout
560bcf5b 52
2466918c
JB
53 private constructor (objId: string, objName: string, uri: URL) {
54 this.objId = objId
55 this.objName = objName
66a7748d 56 this.initializePerformanceObserver()
e7aeea18 57 this.statistics = {
5199f9fd
JB
58 id: this.objId,
59 name: this.objName,
e7aeea18
JB
60 uri: uri.toString(),
61 createdAt: new Date(),
66a7748d
JB
62 statisticsData: new Map()
63 }
9f2e3130
JB
64 }
65
66a7748d 66 public static getInstance (
2466918c
JB
67 objId: string | undefined,
68 objName: string | undefined,
69 uri: URL | undefined
844e496b 70 ): PerformanceStatistics | undefined {
2466918c
JB
71 if (objId == null) {
72 const errMsg = 'Cannot get performance statistics instance without specifying object id'
09e5a7a8 73 logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
2466918c
JB
74 throw new BaseError(errMsg)
75 }
76 if (objName == null) {
77 const errMsg = 'Cannot get performance statistics instance without specifying object name'
09e5a7a8 78 logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
2466918c
JB
79 throw new BaseError(errMsg)
80 }
81 if (uri == null) {
82 const errMsg = 'Cannot get performance statistics instance without specifying object uri'
09e5a7a8 83 logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
2466918c
JB
84 throw new BaseError(errMsg)
85 }
9f2e3130 86 if (!PerformanceStatistics.instances.has(objId)) {
66a7748d 87 PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri))
9f2e3130 88 }
66a7748d 89 return PerformanceStatistics.instances.get(objId)
560bcf5b
JB
90 }
91
09e5a7a8
JB
92 public static deleteInstance (objId: string | undefined): boolean {
93 if (objId == null) {
94 const errMsg = 'Cannot delete performance statistics instance without specifying object id'
95 logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
96 throw new BaseError(errMsg)
97 }
98 return PerformanceStatistics.instances.delete(objId)
99 }
100
66a7748d
JB
101 public static beginMeasure (id: string): string {
102 const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
103 performance.mark(markId)
104 return markId
57939a9d
JB
105 }
106
66a7748d 107 public static endMeasure (name: string, markId: string): void {
b1bd4a15 108 try {
66a7748d 109 performance.measure(name, markId)
b1bd4a15
JB
110 } catch (error) {
111 if (error instanceof Error && error.message.includes('performance mark has not been set')) {
1fb21482 112 /* Ignore */
b1bd4a15 113 } else {
66a7748d 114 throw error
b1bd4a15
JB
115 }
116 }
66a7748d
JB
117 performance.clearMarks(markId)
118 performance.clearMeasures(name)
aef1b33a
JB
119 }
120
66a7748d 121 public addRequestStatistic (
e7aeea18 122 command: RequestCommand | IncomingRequestCommand,
66a7748d 123 messageType: MessageType
e7aeea18 124 ): void {
7f134aca 125 switch (messageType) {
d2a64eb5 126 case MessageType.CALL_MESSAGE:
e7aeea18
JB
127 if (
128 this.statistics.statisticsData.has(command) &&
66a7748d 129 this.statistics.statisticsData.get(command)?.requestCount != null
e7aeea18 130 ) {
66a7748d
JB
131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132 ++this.statistics.statisticsData.get(command)!.requestCount!
7dde0b73 133 } else {
71910904
JB
134 this.statistics.statisticsData.set(command, {
135 ...this.statistics.statisticsData.get(command),
66a7748d
JB
136 requestCount: 1
137 })
7f134aca 138 }
66a7748d 139 break
d2a64eb5 140 case MessageType.CALL_RESULT_MESSAGE:
e7aeea18
JB
141 if (
142 this.statistics.statisticsData.has(command) &&
66a7748d 143 this.statistics.statisticsData.get(command)?.responseCount != null
e7aeea18 144 ) {
66a7748d
JB
145 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
146 ++this.statistics.statisticsData.get(command)!.responseCount!
7f134aca 147 } else {
71910904
JB
148 this.statistics.statisticsData.set(command, {
149 ...this.statistics.statisticsData.get(command),
66a7748d
JB
150 responseCount: 1
151 })
7dde0b73 152 }
66a7748d 153 break
d2a64eb5 154 case MessageType.CALL_ERROR_MESSAGE:
e7aeea18
JB
155 if (
156 this.statistics.statisticsData.has(command) &&
66a7748d 157 this.statistics.statisticsData.get(command)?.errorCount != null
e7aeea18 158 ) {
66a7748d
JB
159 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
160 ++this.statistics.statisticsData.get(command)!.errorCount!
7f134aca 161 } else {
71910904
JB
162 this.statistics.statisticsData.set(command, {
163 ...this.statistics.statisticsData.get(command),
66a7748d
JB
164 errorCount: 1
165 })
7f134aca 166 }
66a7748d 167 break
7f134aca 168 default:
9534e74e 169 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d
JB
170 logger.error(`${this.logPrefix()} wrong message type ${messageType}`)
171 break
7dde0b73
JB
172 }
173 }
174
66a7748d
JB
175 public start (): void {
176 this.startLogStatisticsInterval()
864e5f8d 177 const performanceStorageConfiguration =
5d049829 178 Configuration.getConfigurationSection<StorageConfiguration>(
66a7748d
JB
179 ConfigurationSection.performanceStorage
180 )
181 if (performanceStorageConfiguration.enabled === true) {
e7aeea18 182 logger.info(
864e5f8d
JB
183 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
184 performanceStorageConfiguration.uri
66a7748d
JB
185 }`
186 )
72f041bd 187 }
7dde0b73
JB
188 }
189
66a7748d
JB
190 public stop (): void {
191 this.stopLogStatisticsInterval()
192 performance.clearMarks()
193 performance.clearMeasures()
5199f9fd 194 this.performanceObserver.disconnect()
087a502d
JB
195 }
196
66a7748d
JB
197 public restart (): void {
198 this.stop()
199 this.start()
136c90ba
JB
200 }
201
66a7748d 202 private initializePerformanceObserver (): void {
a974c8e4 203 this.performanceObserver = new PerformanceObserver(performanceObserverList => {
66a7748d 204 const lastPerformanceEntry = performanceObserverList.getEntries()[0]
9d1dc4b1
JB
205 // logger.debug(
206 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
66a7748d
JB
207 // lastPerformanceEntry
208 // )
209 this.addPerformanceEntryToStatistics(lastPerformanceEntry)
210 })
211 this.performanceObserver.observe({ entryTypes: ['measure'] })
aef1b33a
JB
212 }
213
66a7748d 214 private logStatistics (): void {
5199f9fd 215 logger.info(this.logPrefix(), {
c60af6ca 216 ...this.statistics,
94032f3e 217 statisticsData: JSON.parse(
276e05ae 218 JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
94032f3e 219 ) as Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
66a7748d 220 })
7dde0b73
JB
221 }
222
66a7748d 223 private startLogStatisticsInterval (): void {
864e5f8d 224 const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
66a7748d
JB
225 ConfigurationSection.log
226 )
227 const logStatisticsInterval =
228 logConfiguration.enabled === true
229 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
230 logConfiguration.statisticsInterval!
231 : 0
232 if (logStatisticsInterval > 0 && this.displayInterval == null) {
aef1b33a 233 this.displayInterval = setInterval(() => {
66a7748d
JB
234 this.logStatistics()
235 }, secondsToMilliseconds(logStatisticsInterval))
e7aeea18 236 logger.info(
66a7748d
JB
237 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
238 )
239 } else if (this.displayInterval != null) {
dfe81c8f 240 logger.info(
66a7748d
JB
241 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
242 )
243 } else if (logConfiguration.enabled === true) {
e7aeea18 244 logger.info(
66a7748d
JB
245 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
246 )
7dde0b73
JB
247 }
248 }
249
66a7748d
JB
250 private stopLogStatisticsInterval (): void {
251 if (this.displayInterval != null) {
252 clearInterval(this.displayInterval)
253 delete this.displayInterval
8f953431
JB
254 }
255 }
256
66a7748d 257 private addPerformanceEntryToStatistics (entry: PerformanceEntry): void {
7ec46a9a 258 // Initialize command statistics
d71b025f 259 if (!this.statistics.statisticsData.has(entry.name)) {
66a7748d 260 this.statistics.statisticsData.set(entry.name, {})
7ec46a9a 261 }
b49422c6 262 // Update current statistics
66a7748d 263 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 264 this.statistics.statisticsData.get(entry.name)!.timeMeasurementCount =
66a7748d
JB
265 (this.statistics.statisticsData.get(entry.name)?.timeMeasurementCount ?? 0) + 1
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.statistics.statisticsData.get(entry.name)!.currentTimeMeasurement = entry.duration
268 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 269 this.statistics.statisticsData.get(entry.name)!.minTimeMeasurement = min(
a8735ef9 270 entry.duration,
66a7748d
JB
271 this.statistics.statisticsData.get(entry.name)?.minTimeMeasurement ?? Infinity
272 )
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 274 this.statistics.statisticsData.get(entry.name)!.maxTimeMeasurement = max(
a8735ef9 275 entry.duration,
66a7748d
JB
276 this.statistics.statisticsData.get(entry.name)?.maxTimeMeasurement ?? -Infinity
277 )
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 279 this.statistics.statisticsData.get(entry.name)!.totalTimeMeasurement =
66a7748d 280 (this.statistics.statisticsData.get(entry.name)?.totalTimeMeasurement ?? 0) + entry.duration
d71b025f 281 this.statistics.statisticsData.get(entry.name)?.measurementTimeSeries instanceof CircularArray
e7aeea18 282 ? this.statistics.statisticsData
66a7748d
JB
283 .get(entry.name)
284 ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration })
285 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
286 (this.statistics.statisticsData.get(entry.name)!.measurementTimeSeries =
f6778d74 287 new CircularArray<TimestampedData>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
e7aeea18 288 timestamp: entry.startTime,
66a7748d
JB
289 value: entry.duration
290 }))
d4004f32 291 const timeMeasurementValues = extractTimeSeriesValues(
66a7748d
JB
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 this.statistics.statisticsData.get(entry.name)!.measurementTimeSeries!
294 )
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 296 this.statistics.statisticsData.get(entry.name)!.avgTimeMeasurement =
66a7748d
JB
297 average(timeMeasurementValues)
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 299 this.statistics.statisticsData.get(entry.name)!.medTimeMeasurement =
66a7748d
JB
300 median(timeMeasurementValues)
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 302 this.statistics.statisticsData.get(entry.name)!.ninetyFiveThPercentileTimeMeasurement =
66a7748d
JB
303 nthPercentile(timeMeasurementValues, 95)
304 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d71b025f 305 this.statistics.statisticsData.get(entry.name)!.stdDevTimeMeasurement = stdDeviation(
d4004f32 306 timeMeasurementValues,
66a7748d
JB
307 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308 this.statistics.statisticsData.get(entry.name)!.avgTimeMeasurement
309 )
310 this.statistics.updatedAt = new Date()
5d049829
JB
311 if (
312 Configuration.getConfigurationSection<StorageConfiguration>(
66a7748d
JB
313 ConfigurationSection.performanceStorage
314 ).enabled === true
5d049829 315 ) {
66a7748d 316 parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics))
72f041bd 317 }
7ec46a9a
JB
318 }
319
09e5a7a8
JB
320 private static readonly logPrefix = (): string => {
321 return logPrefix(' Performance statistics')
322 }
323
66a7748d
JB
324 private readonly logPrefix = (): string => {
325 return logPrefix(` ${this.objName} | Performance statistics`)
326 }
7dde0b73 327}