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';
8 type IncomingRequestCommand
,
18 JSONStringifyWithMapSupport
,
19 buildPerformanceStatisticsMessage
,
20 extractTimeSeriesValues
,
21 formatDurationSeconds
,
30 export class PerformanceStatistics
{
31 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
36 private readonly objId
: string;
37 private readonly objName
: string;
38 private performanceObserver
!: PerformanceObserver
;
39 private readonly statistics
: Statistics
;
40 private displayInterval
!: NodeJS
.Timeout
;
42 private constructor(objId
: string, objName
: string, uri
: URL
) {
44 this.objName
= objName
;
45 this.initializePerformanceObserver();
47 id
: this.objId
?? 'Object id not specified',
48 name
: this.objName
?? 'Object name not specified',
50 createdAt
: new Date(),
51 statisticsData
: new Map(),
55 public static getInstance(
59 ): PerformanceStatistics
| undefined {
60 if (!PerformanceStatistics
.instances
.has(objId
)) {
61 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
63 return PerformanceStatistics
.instances
.get(objId
);
66 public static beginMeasure(id
: string): string {
67 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
68 performance
.mark(markId
);
72 public static endMeasure(name
: string, markId
: string): void {
73 performance
.measure(name
, markId
);
74 performance
.clearMarks(markId
);
75 performance
.clearMeasures(name
);
78 public addRequestStatistic(
79 command
: RequestCommand
| IncomingRequestCommand
,
80 messageType
: MessageType
82 switch (messageType
) {
83 case MessageType
.CALL_MESSAGE
:
85 this.statistics
.statisticsData
.has(command
) &&
86 this.statistics
.statisticsData
.get(command
)?.requestCount
88 ++this.statistics
.statisticsData
.get(command
).requestCount
;
90 this.statistics
.statisticsData
.set(command
, {
91 ...this.statistics
.statisticsData
.get(command
),
96 case MessageType
.CALL_RESULT_MESSAGE
:
98 this.statistics
.statisticsData
.has(command
) &&
99 this.statistics
.statisticsData
.get(command
)?.responseCount
101 ++this.statistics
.statisticsData
.get(command
).responseCount
;
103 this.statistics
.statisticsData
.set(command
, {
104 ...this.statistics
.statisticsData
.get(command
),
109 case MessageType
.CALL_ERROR_MESSAGE
:
111 this.statistics
.statisticsData
.has(command
) &&
112 this.statistics
.statisticsData
.get(command
)?.errorCount
114 ++this.statistics
.statisticsData
.get(command
).errorCount
;
116 this.statistics
.statisticsData
.set(command
, {
117 ...this.statistics
.statisticsData
.get(command
),
123 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
124 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
129 public start(): void {
130 this.startLogStatisticsInterval();
131 if (Configuration
.getPerformanceStorage().enabled
) {
133 `${this.logPrefix()} storage enabled: type ${
134 Configuration.getPerformanceStorage().type
135 }, uri: ${Configuration.getPerformanceStorage().uri}`
140 public stop(): void {
141 this.stopLogStatisticsInterval();
142 performance
.clearMarks();
143 performance
.clearMeasures();
144 this.performanceObserver
?.disconnect();
147 public restart(): void {
152 private initializePerformanceObserver(): void {
153 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
154 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
156 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
157 // lastPerformanceEntry
159 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
161 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
164 private logStatistics(): void {
165 logger
.info(`${this.logPrefix()}`, {
167 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
171 private startLogStatisticsInterval(): void {
172 const logStatisticsInterval
= Configuration
.getLog().enabled
173 ? Configuration
.getLog().statisticsInterval
175 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
176 this.displayInterval
= setInterval(() => {
177 this.logStatistics();
178 }, logStatisticsInterval
* 1000);
180 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
182 } else if (this.displayInterval
) {
184 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
186 } else if (Configuration
.getLog().enabled
) {
188 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
193 private stopLogStatisticsInterval(): void {
194 if (this.displayInterval
) {
195 clearInterval(this.displayInterval
);
196 delete this.displayInterval
;
200 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
201 const entryName
= entry
.name
;
202 // Initialize command statistics
203 if (!this.statistics
.statisticsData
.has(entryName
)) {
204 this.statistics
.statisticsData
.set(entryName
, {});
206 // Update current statistics
207 this.statistics
.updatedAt
= new Date();
208 this.statistics
.statisticsData
.get(entryName
).timeMeasurementCount
=
209 (this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementCount
?? 0) + 1;
210 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
211 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
= Math.min(
213 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
?? Infinity
215 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
= Math.max(
217 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
?? -Infinity
219 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
220 (this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
?? 0) + entry
.duration
;
221 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
222 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
223 this.statistics
.statisticsData
.get(entryName
).timeMeasurementCount
;
224 this.statistics
.statisticsData
.get(entryName
)?.measurementTimeSeries
instanceof CircularArray
225 ? this.statistics
.statisticsData
227 ?.measurementTimeSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
228 : (this.statistics
.statisticsData
.get(entryName
).measurementTimeSeries
=
229 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
230 timestamp
: entry
.startTime
,
231 value
: entry
.duration
,
233 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= median(
234 extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).measurementTimeSeries
)
236 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
238 extractTimeSeriesValues(
239 this.statistics
.statisticsData
.get(entryName
).measurementTimeSeries
243 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= stdDeviation(
244 extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).measurementTimeSeries
)
246 if (Configuration
.getPerformanceStorage().enabled
) {
247 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
251 private logPrefix
= (): string => {
252 return logPrefix(` ${this.objName} | Performance statistics`);