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