1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { performance
, type PerformanceEntry
, PerformanceObserver
} from
'node:perf_hooks'
4 import type { URL
} from
'node:url'
5 import { parentPort
} from
'node:worker_threads'
7 import { secondsToMilliseconds
} from
'date-fns'
9 import { BaseError
} from
'../exception/index.js'
12 type IncomingRequestCommand
,
13 type LogConfiguration
,
19 type StorageConfiguration
,
21 } from
'../types/index.js'
24 buildPerformanceStatisticsMessage
,
28 extractTimeSeriesValues
,
29 formatDurationSeconds
,
39 } from
'../utils/index.js'
41 export class PerformanceStatistics
{
42 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
47 private readonly objId
: string | undefined
48 private readonly objName
: string | undefined
49 private performanceObserver
!: PerformanceObserver
50 private readonly statistics
: Statistics
51 private displayInterval
?: NodeJS
.Timeout
53 private constructor (objId
: string, objName
: string, uri
: URL
) {
55 this.objName
= objName
56 this.initializePerformanceObserver()
61 createdAt
: new Date(),
62 statisticsData
: new Map()
66 public static getInstance (
67 objId
: string | undefined,
68 objName
: string | undefined,
70 ): PerformanceStatistics
| undefined {
72 const errMsg
= 'Cannot get performance statistics instance without specifying object id'
73 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
74 throw new BaseError(errMsg
)
76 if (objName
== null) {
77 const errMsg
= 'Cannot get performance statistics instance without specifying object name'
78 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
79 throw new BaseError(errMsg
)
82 const errMsg
= 'Cannot get performance statistics instance without specifying object uri'
83 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
84 throw new BaseError(errMsg
)
86 if (!PerformanceStatistics
.instances
.has(objId
)) {
87 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
))
89 return PerformanceStatistics
.instances
.get(objId
)
92 public static deleteInstance (objId
: string | undefined): boolean {
94 const errMsg
= 'Cannot delete performance statistics instance without specifying object id'
95 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
96 throw new BaseError(errMsg
)
98 return PerformanceStatistics
.instances
.delete(objId
)
101 public static beginMeasure (id
: string): string {
102 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
103 performance
.mark(markId
)
107 public static endMeasure (name
: string, markId
: string): void {
109 performance
.measure(name
, markId
)
111 if (error
instanceof Error && error
.message
.includes('performance mark has not been set')) {
117 performance
.clearMarks(markId
)
118 performance
.clearMeasures(name
)
121 public addRequestStatistic (
122 command
: RequestCommand
| IncomingRequestCommand
,
123 messageType
: MessageType
125 switch (messageType
) {
126 case MessageType
.CALL_MESSAGE
:
128 this.statistics
.statisticsData
.has(command
) &&
129 this.statistics
.statisticsData
.get(command
)?.requestCount
!= null
131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!
134 this.statistics
.statisticsData
.set(command
, {
135 ...this.statistics
.statisticsData
.get(command
),
140 case MessageType
.CALL_RESULT_MESSAGE
:
142 this.statistics
.statisticsData
.has(command
) &&
143 this.statistics
.statisticsData
.get(command
)?.responseCount
!= null
145 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
146 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!
148 this.statistics
.statisticsData
.set(command
, {
149 ...this.statistics
.statisticsData
.get(command
),
154 case MessageType
.CALL_ERROR_MESSAGE
:
156 this.statistics
.statisticsData
.has(command
) &&
157 this.statistics
.statisticsData
.get(command
)?.errorCount
!= null
159 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
160 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!
162 this.statistics
.statisticsData
.set(command
, {
163 ...this.statistics
.statisticsData
.get(command
),
169 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
170 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`)
175 public start (): void {
176 this.startLogStatisticsInterval()
177 const performanceStorageConfiguration
=
178 Configuration
.getConfigurationSection
<StorageConfiguration
>(
179 ConfigurationSection
.performanceStorage
181 if (performanceStorageConfiguration
.enabled
=== true) {
183 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
184 performanceStorageConfiguration.uri
190 public stop (): void {
191 this.stopLogStatisticsInterval()
192 performance
.clearMarks()
193 performance
.clearMeasures()
194 this.performanceObserver
.disconnect()
197 public restart (): void {
202 private initializePerformanceObserver (): void {
203 this.performanceObserver
= new PerformanceObserver(performanceObserverList
=> {
204 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0]
206 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
207 // lastPerformanceEntry
209 this.addPerformanceEntryToStatistics(lastPerformanceEntry
)
211 this.performanceObserver
.observe({ entryTypes
: ['measure'] })
214 private logStatistics (): void {
215 logger
.info(this.logPrefix(), {
217 statisticsData
: JSON
.parse(
218 JSONStringify(this.statistics
.statisticsData
, undefined, MapStringifyFormat
.object
)
219 ) as Map
<string | RequestCommand
| IncomingRequestCommand
, StatisticsData
>
223 private startLogStatisticsInterval (): void {
224 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
225 ConfigurationSection
.log
227 const logStatisticsInterval
=
228 logConfiguration
.enabled
=== true
229 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
230 logConfiguration
.statisticsInterval
!
232 if (logStatisticsInterval
> 0 && this.displayInterval
== null) {
233 this.displayInterval
= setInterval(() => {
235 }, secondsToMilliseconds(logStatisticsInterval
))
237 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
239 } else if (this.displayInterval
!= null) {
241 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
243 } else if (logConfiguration
.enabled
=== true) {
245 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
250 private stopLogStatisticsInterval (): void {
251 if (this.displayInterval
!= null) {
252 clearInterval(this.displayInterval
)
253 delete this.displayInterval
257 private addPerformanceEntryToStatistics (entry
: PerformanceEntry
): void {
258 // Initialize command statistics
259 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
260 this.statistics
.statisticsData
.set(entry
.name
, {})
262 // Update current statistics
263 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
264 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
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
269 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
271 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Infinity
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
276 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? -Infinity
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
280 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
281 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof CircularArray
282 ? this.statistics
.statisticsData
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
=
287 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
288 timestamp
: entry
.startTime
,
289 value
: entry
.duration
291 const timeMeasurementValues
= extractTimeSeriesValues(
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
!
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
=
297 average(timeMeasurementValues
)
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
300 median(timeMeasurementValues
)
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
303 nthPercentile(timeMeasurementValues
, 95)
304 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
305 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
306 timeMeasurementValues
,
307 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
310 this.statistics
.updatedAt
= new Date()
312 Configuration
.getConfigurationSection
<StorageConfiguration
>(
313 ConfigurationSection
.performanceStorage
316 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
))
320 private static readonly logPrefix
= (): string => {
321 return logPrefix(' Performance statistics')
324 private readonly logPrefix
= (): string => {
325 return logPrefix(` ${this.objName} | Performance statistics`)