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
,
23 JSONStringifyWithMapSupport
,
24 buildPerformanceStatisticsMessage
,
25 extractTimeSeriesValues
,
26 formatDurationSeconds
,
35 export class PerformanceStatistics
{
36 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
41 private readonly objId
: string;
42 private readonly objName
: string;
43 private performanceObserver
!: PerformanceObserver
;
44 private readonly statistics
: Statistics
;
45 private displayInterval
?: NodeJS
.Timeout
;
47 private constructor(objId
: string, objName
: string, uri
: URL
) {
49 this.objName
= objName
;
50 this.initializePerformanceObserver();
52 id
: this.objId
?? 'Object id not specified',
53 name
: this.objName
?? 'Object name not specified',
55 createdAt
: new Date(),
56 statisticsData
: new Map(),
60 public static getInstance(
64 ): PerformanceStatistics
| undefined {
65 if (!PerformanceStatistics
.instances
.has(objId
)) {
66 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
68 return PerformanceStatistics
.instances
.get(objId
);
71 public static beginMeasure(id
: string): string {
72 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
73 performance
.mark(markId
);
77 public static endMeasure(name
: string, markId
: string): void {
78 performance
.measure(name
, markId
);
79 performance
.clearMarks(markId
);
80 performance
.clearMeasures(name
);
83 public addRequestStatistic(
84 command
: RequestCommand
| IncomingRequestCommand
,
85 messageType
: MessageType
,
87 switch (messageType
) {
88 case MessageType
.CALL_MESSAGE
:
90 this.statistics
.statisticsData
.has(command
) &&
91 this.statistics
.statisticsData
.get(command
)?.requestCount
93 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!;
95 this.statistics
.statisticsData
.set(command
, {
96 ...this.statistics
.statisticsData
.get(command
),
101 case MessageType
.CALL_RESULT_MESSAGE
:
103 this.statistics
.statisticsData
.has(command
) &&
104 this.statistics
.statisticsData
.get(command
)?.responseCount
106 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!;
108 this.statistics
.statisticsData
.set(command
, {
109 ...this.statistics
.statisticsData
.get(command
),
114 case MessageType
.CALL_ERROR_MESSAGE
:
116 this.statistics
.statisticsData
.has(command
) &&
117 this.statistics
.statisticsData
.get(command
)?.errorCount
119 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!;
121 this.statistics
.statisticsData
.set(command
, {
122 ...this.statistics
.statisticsData
.get(command
),
128 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
129 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
134 public start(): void {
135 this.startLogStatisticsInterval();
136 const performanceStorageConfiguration
=
137 Configuration
.getConfigurationSection
<StorageConfiguration
>(
138 ConfigurationSection
.performanceStorage
,
140 if (performanceStorageConfiguration
.enabled
) {
142 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
143 performanceStorageConfiguration.uri
149 public stop(): void {
150 this.stopLogStatisticsInterval();
151 performance
.clearMarks();
152 performance
.clearMeasures();
153 this.performanceObserver
?.disconnect();
156 public restart(): void {
161 private initializePerformanceObserver(): void {
162 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
163 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
165 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
166 // lastPerformanceEntry,
168 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
170 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
173 private logStatistics(): void {
174 logger
.info(`${this.logPrefix()}`, {
176 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
180 private startLogStatisticsInterval(): void {
181 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
182 ConfigurationSection
.log
,
184 const logStatisticsInterval
= logConfiguration
.enabled
185 ? logConfiguration
.statisticsInterval
!
187 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
188 this.displayInterval
= setInterval(() => {
189 this.logStatistics();
190 }, secondsToMilliseconds(logStatisticsInterval
));
192 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
194 } else if (this.displayInterval
) {
196 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
198 } else if (logConfiguration
.enabled
) {
200 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`,
205 private stopLogStatisticsInterval(): void {
206 if (this.displayInterval
) {
207 clearInterval(this.displayInterval
);
208 delete this.displayInterval
;
212 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
213 const entryName
= entry
.name
;
214 // Initialize command statistics
215 if (!this.statistics
.statisticsData
.has(entryName
)) {
216 this.statistics
.statisticsData
.set(entryName
, {});
218 // Update current statistics
219 this.statistics
.updatedAt
= new Date();
220 this.statistics
.statisticsData
.get(entryName
)!.timeMeasurementCount
=
221 (this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementCount
?? 0) + 1;
222 this.statistics
.statisticsData
.get(entryName
)!.currentTimeMeasurement
= entry
.duration
;
223 this.statistics
.statisticsData
.get(entryName
)!.minTimeMeasurement
= Math.min(
225 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
?? Infinity,
227 this.statistics
.statisticsData
.get(entryName
)!.maxTimeMeasurement
= Math.max(
229 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
?? -Infinity,
231 this.statistics
.statisticsData
.get(entryName
)!.totalTimeMeasurement
=
232 (this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
?? 0) + entry
.duration
;
233 this.statistics
.statisticsData
.get(entryName
)!.avgTimeMeasurement
=
234 this.statistics
.statisticsData
.get(entryName
)!.totalTimeMeasurement
! /
235 this.statistics
.statisticsData
.get(entryName
)!.timeMeasurementCount
!;
236 this.statistics
.statisticsData
.get(entryName
)?.measurementTimeSeries
instanceof CircularArray
237 ? this.statistics
.statisticsData
239 ?.measurementTimeSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
240 : (this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
=
241 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
242 timestamp
: entry
.startTime
,
243 value
: entry
.duration
,
245 this.statistics
.statisticsData
.get(entryName
)!.medTimeMeasurement
= median(
246 extractTimeSeriesValues(
247 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
250 this.statistics
.statisticsData
.get(entryName
)!.ninetyFiveThPercentileTimeMeasurement
=
252 extractTimeSeriesValues(
253 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
257 this.statistics
.statisticsData
.get(entryName
)!.stdDevTimeMeasurement
= stdDeviation(
258 extractTimeSeriesValues(
259 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
263 Configuration
.getConfigurationSection
<StorageConfiguration
>(
264 ConfigurationSection
.performanceStorage
,
267 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
271 private logPrefix
= (): string => {
272 return logPrefix(` ${this.objName} | Performance statistics`);