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';
9 type IncomingRequestCommand
,
10 type LogConfiguration
,
14 type StorageConfiguration
,
21 JSONStringifyWithMapSupport
,
22 buildPerformanceStatisticsMessage
,
23 extractTimeSeriesValues
,
24 formatDurationSeconds
,
33 export class PerformanceStatistics
{
34 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
39 private readonly objId
: string;
40 private readonly objName
: string;
41 private performanceObserver
!: PerformanceObserver
;
42 private readonly statistics
: Statistics
;
43 private displayInterval
?: NodeJS
.Timeout
;
45 private constructor(objId
: string, objName
: string, uri
: URL
) {
47 this.objName
= objName
;
48 this.initializePerformanceObserver();
50 id
: this.objId
?? 'Object id not specified',
51 name
: this.objName
?? 'Object name not specified',
53 createdAt
: new Date(),
54 statisticsData
: new Map(),
58 public static getInstance(
62 ): PerformanceStatistics
| undefined {
63 if (!PerformanceStatistics
.instances
.has(objId
)) {
64 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
66 return PerformanceStatistics
.instances
.get(objId
);
69 public static beginMeasure(id
: string): string {
70 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
71 performance
.mark(markId
);
75 public static endMeasure(name
: string, markId
: string): void {
76 performance
.measure(name
, markId
);
77 performance
.clearMarks(markId
);
78 performance
.clearMeasures(name
);
81 public addRequestStatistic(
82 command
: RequestCommand
| IncomingRequestCommand
,
83 messageType
: MessageType
,
85 switch (messageType
) {
86 case MessageType
.CALL_MESSAGE
:
88 this.statistics
.statisticsData
.has(command
) &&
89 this.statistics
.statisticsData
.get(command
)?.requestCount
91 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!;
93 this.statistics
.statisticsData
.set(command
, {
94 ...this.statistics
.statisticsData
.get(command
),
99 case MessageType
.CALL_RESULT_MESSAGE
:
101 this.statistics
.statisticsData
.has(command
) &&
102 this.statistics
.statisticsData
.get(command
)?.responseCount
104 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!;
106 this.statistics
.statisticsData
.set(command
, {
107 ...this.statistics
.statisticsData
.get(command
),
112 case MessageType
.CALL_ERROR_MESSAGE
:
114 this.statistics
.statisticsData
.has(command
) &&
115 this.statistics
.statisticsData
.get(command
)?.errorCount
117 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!;
119 this.statistics
.statisticsData
.set(command
, {
120 ...this.statistics
.statisticsData
.get(command
),
126 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
127 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
132 public start(): void {
133 this.startLogStatisticsInterval();
135 Configuration
.getConfigurationSection
<StorageConfiguration
>(
136 ConfigurationSection
.performanceStorage
,
140 `${this.logPrefix()} storage enabled: type ${
141 Configuration.getConfigurationSection<StorageConfiguration>(
142 ConfigurationSection.performanceStorage,
145 Configuration.getConfigurationSection<StorageConfiguration>(
146 ConfigurationSection.performanceStorage,
153 public stop(): void {
154 this.stopLogStatisticsInterval();
155 performance
.clearMarks();
156 performance
.clearMeasures();
157 this.performanceObserver
?.disconnect();
160 public restart(): void {
165 private initializePerformanceObserver(): void {
166 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
167 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
169 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
170 // lastPerformanceEntry,
172 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
174 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
177 private logStatistics(): void {
178 logger
.info(`${this.logPrefix()}`, {
180 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
184 private startLogStatisticsInterval(): void {
185 const logStatisticsInterval
= Configuration
.getConfigurationSection
<LogConfiguration
>(
186 ConfigurationSection
.log
,
188 ? Configuration
.getConfigurationSection
<LogConfiguration
>(ConfigurationSection
.log
)
191 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
192 this.displayInterval
= setInterval(() => {
193 this.logStatistics();
194 }, logStatisticsInterval
* 1000);
196 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
198 } else if (this.displayInterval
) {
200 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
203 Configuration
.getConfigurationSection
<LogConfiguration
>(ConfigurationSection
.log
).enabled
206 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`,
211 private stopLogStatisticsInterval(): void {
212 if (this.displayInterval
) {
213 clearInterval(this.displayInterval
);
214 delete this.displayInterval
;
218 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
219 const entryName
= entry
.name
;
220 // Initialize command statistics
221 if (!this.statistics
.statisticsData
.has(entryName
)) {
222 this.statistics
.statisticsData
.set(entryName
, {});
224 // Update current statistics
225 this.statistics
.updatedAt
= new Date();
226 this.statistics
.statisticsData
.get(entryName
)!.timeMeasurementCount
=
227 (this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementCount
?? 0) + 1;
228 this.statistics
.statisticsData
.get(entryName
)!.currentTimeMeasurement
= entry
.duration
;
229 this.statistics
.statisticsData
.get(entryName
)!.minTimeMeasurement
= Math.min(
231 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
?? Infinity,
233 this.statistics
.statisticsData
.get(entryName
)!.maxTimeMeasurement
= Math.max(
235 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
?? -Infinity,
237 this.statistics
.statisticsData
.get(entryName
)!.totalTimeMeasurement
=
238 (this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
?? 0) + entry
.duration
;
239 this.statistics
.statisticsData
.get(entryName
)!.avgTimeMeasurement
=
240 this.statistics
.statisticsData
.get(entryName
)!.totalTimeMeasurement
! /
241 this.statistics
.statisticsData
.get(entryName
)!.timeMeasurementCount
!;
242 this.statistics
.statisticsData
.get(entryName
)?.measurementTimeSeries
instanceof CircularArray
243 ? this.statistics
.statisticsData
245 ?.measurementTimeSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
246 : (this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
=
247 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
248 timestamp
: entry
.startTime
,
249 value
: entry
.duration
,
251 this.statistics
.statisticsData
.get(entryName
)!.medTimeMeasurement
= median(
252 extractTimeSeriesValues(
253 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
256 this.statistics
.statisticsData
.get(entryName
)!.ninetyFiveThPercentileTimeMeasurement
=
258 extractTimeSeriesValues(
259 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
263 this.statistics
.statisticsData
.get(entryName
)!.stdDevTimeMeasurement
= stdDeviation(
264 extractTimeSeriesValues(
265 this.statistics
.statisticsData
.get(entryName
)!.measurementTimeSeries
as TimestampedData
[],
269 Configuration
.getConfigurationSection
<StorageConfiguration
>(
270 ConfigurationSection
.performanceStorage
,
273 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
277 private logPrefix
= (): string => {
278 return logPrefix(` ${this.objName} | Performance statistics`);