1 // Partial Copyright Jerome Benoit. 2021-2023. 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'
11 type IncomingRequestCommand
,
12 type LogConfiguration
,
16 type StorageConfiguration
,
18 } from
'../types/index.js'
23 JSONStringifyWithMapSupport
,
25 buildPerformanceStatisticsMessage
,
26 extractTimeSeriesValues
,
27 formatDurationSeconds
,
36 } from
'../utils/index.js'
38 export class PerformanceStatistics
{
39 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
44 private readonly objId
: string
45 private readonly objName
: string
46 private performanceObserver
!: PerformanceObserver
47 private readonly statistics
: Statistics
48 private displayInterval
?: NodeJS
.Timeout
50 private constructor (objId
: string, objName
: string, uri
: URL
) {
52 this.objName
= objName
53 this.initializePerformanceObserver()
55 id
: this.objId
?? 'Object id not specified',
56 name
: this.objName
?? 'Object name not specified',
58 createdAt
: new Date(),
59 statisticsData
: new Map()
63 public static getInstance (
67 ): PerformanceStatistics
| undefined {
68 if (!PerformanceStatistics
.instances
.has(objId
)) {
69 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
))
71 return PerformanceStatistics
.instances
.get(objId
)
74 public static beginMeasure (id
: string): string {
75 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
76 performance
.mark(markId
)
80 public static endMeasure (name
: string, markId
: string): void {
82 performance
.measure(name
, markId
)
84 if (error
instanceof Error && error
.message
.includes('performance mark has not been set')) {
90 performance
.clearMarks(markId
)
91 performance
.clearMeasures(name
)
94 public addRequestStatistic (
95 command
: RequestCommand
| IncomingRequestCommand
,
96 messageType
: MessageType
98 switch (messageType
) {
99 case MessageType
.CALL_MESSAGE
:
101 this.statistics
.statisticsData
.has(command
) &&
102 this.statistics
.statisticsData
.get(command
)?.requestCount
!= null
104 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
105 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!
107 this.statistics
.statisticsData
.set(command
, {
108 ...this.statistics
.statisticsData
.get(command
),
113 case MessageType
.CALL_RESULT_MESSAGE
:
115 this.statistics
.statisticsData
.has(command
) &&
116 this.statistics
.statisticsData
.get(command
)?.responseCount
!= null
118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!
121 this.statistics
.statisticsData
.set(command
, {
122 ...this.statistics
.statisticsData
.get(command
),
127 case MessageType
.CALL_ERROR_MESSAGE
:
129 this.statistics
.statisticsData
.has(command
) &&
130 this.statistics
.statisticsData
.get(command
)?.errorCount
!= null
132 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
133 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!
135 this.statistics
.statisticsData
.set(command
, {
136 ...this.statistics
.statisticsData
.get(command
),
142 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
143 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`)
148 public start (): void {
149 this.startLogStatisticsInterval()
150 const performanceStorageConfiguration
=
151 Configuration
.getConfigurationSection
<StorageConfiguration
>(
152 ConfigurationSection
.performanceStorage
154 if (performanceStorageConfiguration
.enabled
=== true) {
156 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
157 performanceStorageConfiguration.uri
163 public stop (): void {
164 this.stopLogStatisticsInterval()
165 performance
.clearMarks()
166 performance
.clearMeasures()
167 this.performanceObserver
?.disconnect()
170 public restart (): void {
175 private initializePerformanceObserver (): void {
176 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
177 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0]
179 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
180 // lastPerformanceEntry
182 this.addPerformanceEntryToStatistics(lastPerformanceEntry
)
184 this.performanceObserver
.observe({ entryTypes
: ['measure'] })
187 private logStatistics (): void {
188 logger
.info(`${this.logPrefix()}`, {
190 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
)
194 private startLogStatisticsInterval (): void {
195 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
196 ConfigurationSection
.log
198 const logStatisticsInterval
=
199 logConfiguration
.enabled
=== true
200 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
201 logConfiguration
.statisticsInterval
!
203 if (logStatisticsInterval
> 0 && this.displayInterval
== null) {
204 this.displayInterval
= setInterval(() => {
206 }, secondsToMilliseconds(logStatisticsInterval
))
208 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
210 } else if (this.displayInterval
!= null) {
212 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
214 } else if (logConfiguration
.enabled
=== true) {
216 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`
221 private stopLogStatisticsInterval (): void {
222 if (this.displayInterval
!= null) {
223 clearInterval(this.displayInterval
)
224 delete this.displayInterval
228 private addPerformanceEntryToStatistics (entry
: PerformanceEntry
): void {
229 // Initialize command statistics
230 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
231 this.statistics
.statisticsData
.set(entry
.name
, {})
233 // Update current statistics
234 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
235 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
236 (this.statistics
.statisticsData
.get(entry
.name
)?.timeMeasurementCount
?? 0) + 1
237 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
238 this.statistics
.statisticsData
.get(entry
.name
)!.currentTimeMeasurement
= entry
.duration
239 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
242 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Infinity
244 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
245 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
247 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? -Infinity
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
251 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
252 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof CircularArray
253 ? this.statistics
.statisticsData
255 ?.measurementTimeSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
256 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
257 (this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
=
258 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
259 timestamp
: entry
.startTime
,
260 value
: entry
.duration
262 const timeMeasurementValues
= extractTimeSeriesValues(
263 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
264 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
!
266 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
267 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
=
268 average(timeMeasurementValues
)
269 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
271 median(timeMeasurementValues
)
272 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
274 nthPercentile(timeMeasurementValues
, 95)
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
277 timeMeasurementValues
,
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
281 this.statistics
.updatedAt
= new Date()
283 Configuration
.getConfigurationSection
<StorageConfiguration
>(
284 ConfigurationSection
.performanceStorage
287 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
))
291 private readonly logPrefix
= (): string => {
292 return logPrefix(` ${this.objName} | Performance statistics`)