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
,
18 type StorageConfiguration
,
20 } from
'../types/index.js'
25 JSONStringifyWithMapSupport
,
27 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 {
70 const logPfx
= logPrefix(' Performance statistics')
72 const errMsg
= 'Cannot get performance statistics instance without specifying object id'
73 logger
.error(`${logPfx} ${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(`${logPfx} ${errMsg}`)
79 throw new BaseError(errMsg
)
82 const errMsg
= 'Cannot get performance statistics instance without specifying object uri'
83 logger
.error(`${logPfx} ${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 beginMeasure (id
: string): string {
93 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
94 performance
.mark(markId
)
98 public static endMeasure (name
: string, markId
: string): void {
100 performance
.measure(name
, markId
)
102 if (error
instanceof Error && error
.message
.includes('performance mark has not been set')) {
108 performance
.clearMarks(markId
)
109 performance
.clearMeasures(name
)
112 public addRequestStatistic (
113 command
: RequestCommand
| IncomingRequestCommand
,
114 messageType
: MessageType
116 switch (messageType
) {
117 case MessageType
.CALL_MESSAGE
:
119 this.statistics
.statisticsData
.has(command
) &&
120 this.statistics
.statisticsData
.get(command
)?.requestCount
!= null
122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
123 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!
125 this.statistics
.statisticsData
.set(command
, {
126 ...this.statistics
.statisticsData
.get(command
),
131 case MessageType
.CALL_RESULT_MESSAGE
:
133 this.statistics
.statisticsData
.has(command
) &&
134 this.statistics
.statisticsData
.get(command
)?.responseCount
!= null
136 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
137 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!
139 this.statistics
.statisticsData
.set(command
, {
140 ...this.statistics
.statisticsData
.get(command
),
145 case MessageType
.CALL_ERROR_MESSAGE
:
147 this.statistics
.statisticsData
.has(command
) &&
148 this.statistics
.statisticsData
.get(command
)?.errorCount
!= null
150 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
151 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!
153 this.statistics
.statisticsData
.set(command
, {
154 ...this.statistics
.statisticsData
.get(command
),
160 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
161 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`)
166 public start (): void {
167 this.startLogStatisticsInterval()
168 const performanceStorageConfiguration
=
169 Configuration
.getConfigurationSection
<StorageConfiguration
>(
170 ConfigurationSection
.performanceStorage
172 if (performanceStorageConfiguration
.enabled
=== true) {
174 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
175 performanceStorageConfiguration.uri
181 public stop (): void {
182 this.stopLogStatisticsInterval()
183 performance
.clearMarks()
184 performance
.clearMeasures()
185 this.performanceObserver
.disconnect()
188 public restart (): void {
193 private initializePerformanceObserver (): void {
194 this.performanceObserver
= new PerformanceObserver(performanceObserverList
=> {
195 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0]
197 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
198 // lastPerformanceEntry
200 this.addPerformanceEntryToStatistics(lastPerformanceEntry
)
202 this.performanceObserver
.observe({ entryTypes
: ['measure'] })
205 private logStatistics (): void {
206 logger
.info(this.logPrefix(), {
208 statisticsData
: JSON
.parse(
209 JSONStringifyWithMapSupport(this.statistics
.statisticsData
)
210 ) as Map
<string | RequestCommand
| IncomingRequestCommand
, StatisticsData
>
214 private startLogStatisticsInterval (): void {
215 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
216 ConfigurationSection
.log
218 const logStatisticsInterval
=
219 logConfiguration
.enabled
=== true
220 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
221 logConfiguration
.statisticsInterval
!
223 if (logStatisticsInterval
> 0 && this.displayInterval
== null) {
224 this.displayInterval
= setInterval(() => {
226 }, secondsToMilliseconds(logStatisticsInterval
))
228 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
230 } else if (this.displayInterval
!= null) {
232 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
234 } else if (logConfiguration
.enabled
=== true) {
236 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
241 private stopLogStatisticsInterval (): void {
242 if (this.displayInterval
!= null) {
243 clearInterval(this.displayInterval
)
244 delete this.displayInterval
248 private addPerformanceEntryToStatistics (entry
: PerformanceEntry
): void {
249 // Initialize command statistics
250 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
251 this.statistics
.statisticsData
.set(entry
.name
, {})
253 // Update current statistics
254 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
255 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
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
260 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
262 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Infinity
264 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
265 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
267 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? -Infinity
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
271 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
272 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof CircularArray
273 ? this.statistics
.statisticsData
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
=
278 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
279 timestamp
: entry
.startTime
,
280 value
: entry
.duration
282 const timeMeasurementValues
= extractTimeSeriesValues(
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
!
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
=
288 average(timeMeasurementValues
)
289 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
290 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
291 median(timeMeasurementValues
)
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
294 nthPercentile(timeMeasurementValues
, 95)
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
297 timeMeasurementValues
,
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
301 this.statistics
.updatedAt
= new Date()
303 Configuration
.getConfigurationSection
<StorageConfiguration
>(
304 ConfigurationSection
.performanceStorage
307 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
))
311 private readonly logPrefix
= (): string => {
312 return logPrefix(` ${this.objName} | Performance statistics`)