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