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'
8 import { CircularBuffer
} from
'mnemonist'
9 import { is, mean
, median
} from
'rambda'
11 import { BaseError
} from
'../exception/index.js'
14 type IncomingRequestCommand
,
15 type LogConfiguration
,
21 type StorageConfiguration
,
23 } from
'../types/index.js'
25 buildPerformanceStatisticsMessage
,
28 extractTimeSeriesValues
,
29 formatDurationSeconds
,
38 } from
'../utils/index.js'
40 export class PerformanceStatistics
{
41 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
46 private readonly objId
: string | undefined
47 private readonly objName
: string | undefined
48 private performanceObserver
!: PerformanceObserver
49 private readonly statistics
: Statistics
50 private displayInterval
?: NodeJS
.Timeout
52 private constructor (objId
: string, objName
: string, uri
: URL
) {
54 this.objName
= objName
55 this.initializePerformanceObserver()
60 createdAt
: new Date(),
61 statisticsData
: new Map()
65 public static getInstance (
66 objId
: string | undefined,
67 objName
: string | undefined,
69 ): PerformanceStatistics
| undefined {
71 const errMsg
= 'Cannot get performance statistics instance without specifying object id'
72 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
73 throw new BaseError(errMsg
)
75 if (objName
== null) {
76 const errMsg
= 'Cannot get performance statistics instance without specifying object name'
77 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
78 throw new BaseError(errMsg
)
81 const errMsg
= 'Cannot get performance statistics instance without specifying object uri'
82 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
83 throw new BaseError(errMsg
)
85 if (!PerformanceStatistics
.instances
.has(objId
)) {
86 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
))
88 return PerformanceStatistics
.instances
.get(objId
)
91 public static deleteInstance (objId
: string | undefined): boolean {
93 const errMsg
= 'Cannot delete performance statistics instance without specifying object id'
94 logger
.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
95 throw new BaseError(errMsg
)
97 return PerformanceStatistics
.instances
.delete(objId
)
100 public static beginMeasure (id
: string): string {
101 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
102 performance
.mark(markId
)
106 public static endMeasure (name
: string, markId
: string): void {
108 performance
.measure(name
, markId
)
110 if (is(Error, error
) && error
.message
.includes('performance mark has not been set')) {
116 performance
.clearMarks(markId
)
117 performance
.clearMeasures(name
)
120 public addRequestStatistic (
121 command
: RequestCommand
| IncomingRequestCommand
,
122 messageType
: MessageType
124 switch (messageType
) {
125 case MessageType
.CALL_MESSAGE
:
127 this.statistics
.statisticsData
.has(command
) &&
128 this.statistics
.statisticsData
.get(command
)?.requestCount
!= null
130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
131 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!
133 this.statistics
.statisticsData
.set(command
, {
134 ...this.statistics
.statisticsData
.get(command
),
139 case MessageType
.CALL_RESULT_MESSAGE
:
141 this.statistics
.statisticsData
.has(command
) &&
142 this.statistics
.statisticsData
.get(command
)?.responseCount
!= null
144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
145 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!
147 this.statistics
.statisticsData
.set(command
, {
148 ...this.statistics
.statisticsData
.get(command
),
153 case MessageType
.CALL_ERROR_MESSAGE
:
155 this.statistics
.statisticsData
.has(command
) &&
156 this.statistics
.statisticsData
.get(command
)?.errorCount
!= null
158 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!
161 this.statistics
.statisticsData
.set(command
, {
162 ...this.statistics
.statisticsData
.get(command
),
168 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
169 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`)
174 public start (): void {
175 this.startLogStatisticsInterval()
176 const performanceStorageConfiguration
=
177 Configuration
.getConfigurationSection
<StorageConfiguration
>(
178 ConfigurationSection
.performanceStorage
180 if (performanceStorageConfiguration
.enabled
=== true) {
182 `${this.logPrefix()} storage enabled: type ${
183 performanceStorageConfiguration.type
184 }, uri: ${performanceStorageConfiguration.uri}`
189 public stop (): void {
190 this.stopLogStatisticsInterval()
191 performance
.clearMarks()
192 performance
.clearMeasures()
193 this.performanceObserver
.disconnect()
196 public restart (): void {
201 private initializePerformanceObserver (): void {
202 this.performanceObserver
= new PerformanceObserver(performanceObserverList
=> {
203 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0]
205 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
206 // lastPerformanceEntry
208 this.addPerformanceEntryToStatistics(lastPerformanceEntry
)
210 this.performanceObserver
.observe({ entryTypes
: ['measure'] })
213 private logStatistics (): void {
214 logger
.info(this.logPrefix(), {
216 statisticsData
: JSON
.parse(
217 JSONStringify(this.statistics
.statisticsData
, undefined, MapStringifyFormat
.object
)
218 ) as Map
<string | RequestCommand
| IncomingRequestCommand
, StatisticsData
>
222 private startLogStatisticsInterval (): void {
223 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
224 ConfigurationSection
.log
226 const logStatisticsInterval
=
227 logConfiguration
.enabled
=== true
228 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
229 logConfiguration
.statisticsInterval
!
231 if (logStatisticsInterval
> 0 && this.displayInterval
== null) {
232 this.displayInterval
= setInterval(() => {
234 }, secondsToMilliseconds(logStatisticsInterval
))
236 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
238 } else if (this.displayInterval
!= null) {
240 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
242 } else if (logConfiguration
.enabled
=== true) {
244 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
249 private stopLogStatisticsInterval (): void {
250 if (this.displayInterval
!= null) {
251 clearInterval(this.displayInterval
)
252 delete this.displayInterval
256 private addPerformanceEntryToStatistics (entry
: PerformanceEntry
): void {
257 // Initialize command statistics
258 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
259 this.statistics
.statisticsData
.set(entry
.name
, {})
261 // Update current statistics
262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
263 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
264 (this.statistics
.statisticsData
.get(entry
.name
)?.timeMeasurementCount
?? 0) + 1
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 this.statistics
.statisticsData
.get(entry
.name
)!.currentTimeMeasurement
= entry
.duration
267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
270 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Number.POSITIVE_INFINITY
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
275 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? Number.NEGATIVE_INFINITY
277 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
278 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
279 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
282 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
=
288 new CircularBuffer
<TimestampedData
>(
289 Array<TimestampedData
>,
290 Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
293 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
?.push({
294 timestamp
: entry
.startTime
,
295 value
: entry
.duration
297 const timeMeasurementValues
= extractTimeSeriesValues(
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 this.statistics
.statisticsData
.get(entry
.name
)!
300 .measurementTimeSeries
as CircularBuffer
<TimestampedData
>
302 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
303 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
= mean(timeMeasurementValues
)
304 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
305 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
306 median(timeMeasurementValues
)
307 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
309 nthPercentile(timeMeasurementValues
, 95)
310 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
311 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
312 timeMeasurementValues
,
313 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
314 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
316 this.statistics
.updatedAt
= new Date()
318 Configuration
.getConfigurationSection
<StorageConfiguration
>(
319 ConfigurationSection
.performanceStorage
322 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
))
326 private static readonly logPrefix
= (): string => {
327 return logPrefix(' Performance statistics')
330 private readonly logPrefix
= (): string => {
331 return logPrefix(` ${this.objName} | Performance statistics`)