1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import { type PerformanceEntry
, PerformanceObserver
, performance
} 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
,
17 type StorageConfiguration
,
19 } from
'../types/index.js'
24 JSONStringifyWithMapSupport
,
26 buildPerformanceStatisticsMessage
,
27 extractTimeSeriesValues
,
28 formatDurationSeconds
,
37 } from
'../utils/index.js'
39 export class PerformanceStatistics
{
40 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
45 private readonly objId
: string | undefined
46 private readonly objName
: string | undefined
47 private performanceObserver
!: PerformanceObserver
48 private readonly statistics
: Statistics
49 private displayInterval
?: NodeJS
.Timeout
51 private constructor (objId
: string, objName
: string, uri
: URL
) {
53 this.objName
= objName
54 this.initializePerformanceObserver()
59 createdAt
: new Date(),
60 statisticsData
: new Map()
64 public static getInstance (
65 objId
: string | undefined,
66 objName
: string | undefined,
68 ): PerformanceStatistics
| undefined {
69 const logPfx
= logPrefix(' Performance statistics')
71 const errMsg
= 'Cannot get performance statistics instance without specifying object id'
72 logger
.error(`${logPfx} ${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(`${logPfx} ${errMsg}`)
78 throw new BaseError(errMsg
)
81 const errMsg
= 'Cannot get performance statistics instance without specifying object uri'
82 logger
.error(`${logPfx} ${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 beginMeasure (id
: string): string {
92 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
93 performance
.mark(markId
)
97 public static endMeasure (name
: string, markId
: string): void {
99 performance
.measure(name
, markId
)
101 if (error
instanceof Error && error
.message
.includes('performance mark has not been set')) {
107 performance
.clearMarks(markId
)
108 performance
.clearMeasures(name
)
111 public addRequestStatistic (
112 command
: RequestCommand
| IncomingRequestCommand
,
113 messageType
: MessageType
115 switch (messageType
) {
116 case MessageType
.CALL_MESSAGE
:
118 this.statistics
.statisticsData
.has(command
) &&
119 this.statistics
.statisticsData
.get(command
)?.requestCount
!= null
121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
122 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!
124 this.statistics
.statisticsData
.set(command
, {
125 ...this.statistics
.statisticsData
.get(command
),
130 case MessageType
.CALL_RESULT_MESSAGE
:
132 this.statistics
.statisticsData
.has(command
) &&
133 this.statistics
.statisticsData
.get(command
)?.responseCount
!= null
135 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
136 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!
138 this.statistics
.statisticsData
.set(command
, {
139 ...this.statistics
.statisticsData
.get(command
),
144 case MessageType
.CALL_ERROR_MESSAGE
:
146 this.statistics
.statisticsData
.has(command
) &&
147 this.statistics
.statisticsData
.get(command
)?.errorCount
!= null
149 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
150 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!
152 this.statistics
.statisticsData
.set(command
, {
153 ...this.statistics
.statisticsData
.get(command
),
159 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
160 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`)
165 public start (): void {
166 this.startLogStatisticsInterval()
167 const performanceStorageConfiguration
=
168 Configuration
.getConfigurationSection
<StorageConfiguration
>(
169 ConfigurationSection
.performanceStorage
171 if (performanceStorageConfiguration
.enabled
=== true) {
173 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
174 performanceStorageConfiguration.uri
180 public stop (): void {
181 this.stopLogStatisticsInterval()
182 performance
.clearMarks()
183 performance
.clearMeasures()
184 this.performanceObserver
.disconnect()
187 public restart (): void {
192 private initializePerformanceObserver (): void {
193 this.performanceObserver
= new PerformanceObserver(performanceObserverList
=> {
194 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0]
196 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
197 // lastPerformanceEntry
199 this.addPerformanceEntryToStatistics(lastPerformanceEntry
)
201 this.performanceObserver
.observe({ entryTypes
: ['measure'] })
204 private logStatistics (): void {
205 logger
.info(this.logPrefix(), {
207 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
)
211 private startLogStatisticsInterval (): void {
212 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
213 ConfigurationSection
.log
215 const logStatisticsInterval
=
216 logConfiguration
.enabled
=== true
217 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 logConfiguration
.statisticsInterval
!
220 if (logStatisticsInterval
> 0 && this.displayInterval
== null) {
221 this.displayInterval
= setInterval(() => {
223 }, secondsToMilliseconds(logStatisticsInterval
))
225 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
227 } else if (this.displayInterval
!= null) {
229 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
231 } else if (logConfiguration
.enabled
=== true) {
233 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
238 private stopLogStatisticsInterval (): void {
239 if (this.displayInterval
!= null) {
240 clearInterval(this.displayInterval
)
241 delete this.displayInterval
245 private addPerformanceEntryToStatistics (entry
: PerformanceEntry
): void {
246 // Initialize command statistics
247 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
248 this.statistics
.statisticsData
.set(entry
.name
, {})
250 // Update current statistics
251 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
252 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
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
257 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
259 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Infinity
261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
264 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? -Infinity
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
268 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
269 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof CircularArray
270 ? this.statistics
.statisticsData
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
=
275 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
276 timestamp
: entry
.startTime
,
277 value
: entry
.duration
279 const timeMeasurementValues
= extractTimeSeriesValues(
280 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
281 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
!
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
=
285 average(timeMeasurementValues
)
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
288 median(timeMeasurementValues
)
289 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
290 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
291 nthPercentile(timeMeasurementValues
, 95)
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
294 timeMeasurementValues
,
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
298 this.statistics
.updatedAt
= new Date()
300 Configuration
.getConfigurationSection
<StorageConfiguration
>(
301 ConfigurationSection
.performanceStorage
304 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
))
308 private readonly logPrefix
= (): string => {
309 return logPrefix(` ${this.objName} | Performance statistics`)