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 formatDurationSeconds
,
29 export class PerformanceStatistics
{
30 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
35 private readonly objId
: string;
36 private readonly objName
: string;
37 private performanceObserver
!: PerformanceObserver
;
38 private readonly statistics
: Statistics
;
39 private displayInterval
!: NodeJS
.Timeout
;
41 private constructor(objId
: string, objName
: string, uri
: URL
) {
43 this.objName
= objName
;
44 this.initializePerformanceObserver();
46 id
: this.objId
?? 'Object id not specified',
47 name
: this.objName
?? 'Object name not specified',
49 createdAt
: new Date(),
50 statisticsData
: new Map(),
54 public static getInstance(
58 ): PerformanceStatistics
| undefined {
59 if (!PerformanceStatistics
.instances
.has(objId
)) {
60 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
62 return PerformanceStatistics
.instances
.get(objId
);
65 public static beginMeasure(id
: string): string {
66 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
67 performance
.mark(markId
);
71 public static endMeasure(name
: string, markId
: string): void {
72 performance
.measure(name
, markId
);
73 performance
.clearMarks(markId
);
74 performance
.clearMeasures(name
);
77 public addRequestStatistic(
78 command
: RequestCommand
| IncomingRequestCommand
,
79 messageType
: MessageType
81 switch (messageType
) {
82 case MessageType
.CALL_MESSAGE
:
84 this.statistics
.statisticsData
.has(command
) &&
85 this.statistics
.statisticsData
.get(command
)?.countRequest
87 ++this.statistics
.statisticsData
.get(command
).countRequest
;
89 this.statistics
.statisticsData
.set(command
, {
90 ...this.statistics
.statisticsData
.get(command
),
95 case MessageType
.CALL_RESULT_MESSAGE
:
97 this.statistics
.statisticsData
.has(command
) &&
98 this.statistics
.statisticsData
.get(command
)?.countResponse
100 ++this.statistics
.statisticsData
.get(command
).countResponse
;
102 this.statistics
.statisticsData
.set(command
, {
103 ...this.statistics
.statisticsData
.get(command
),
108 case MessageType
.CALL_ERROR_MESSAGE
:
110 this.statistics
.statisticsData
.has(command
) &&
111 this.statistics
.statisticsData
.get(command
)?.countError
113 ++this.statistics
.statisticsData
.get(command
).countError
;
115 this.statistics
.statisticsData
.set(command
, {
116 ...this.statistics
.statisticsData
.get(command
),
122 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
123 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
128 public start(): void {
129 this.startLogStatisticsInterval();
130 if (Configuration
.getPerformanceStorage().enabled
) {
132 `${this.logPrefix()} storage enabled: type ${
133 Configuration.getPerformanceStorage().type
134 }, uri: ${Configuration.getPerformanceStorage().uri}`
139 public stop(): void {
140 this.stopLogStatisticsInterval();
141 performance
.clearMarks();
142 performance
.clearMeasures();
143 this.performanceObserver
?.disconnect();
146 public restart(): void {
151 private initializePerformanceObserver(): void {
152 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
153 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
155 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
156 // lastPerformanceEntry
158 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
160 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
163 private logStatistics(): void {
164 logger
.info(`${this.logPrefix()}`, {
166 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
170 private startLogStatisticsInterval(): void {
171 const logStatisticsInterval
= Configuration
.getLog().enabled
172 ? Configuration
.getLog().statisticsInterval
174 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
175 this.displayInterval
= setInterval(() => {
176 this.logStatistics();
177 }, logStatisticsInterval
* 1000);
179 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
181 } else if (this.displayInterval
) {
183 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
185 } else if (Configuration
.getLog().enabled
) {
187 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
192 private stopLogStatisticsInterval(): void {
193 if (this.displayInterval
) {
194 clearInterval(this.displayInterval
);
195 delete this.displayInterval
;
199 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
200 const entryName
= entry
.name
;
201 // Initialize command statistics
202 if (!this.statistics
.statisticsData
.has(entryName
)) {
203 this.statistics
.statisticsData
.set(entryName
, {});
205 // Update current statistics
206 this.statistics
.updatedAt
= new Date();
207 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
208 (this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
?? 0) + 1;
209 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
210 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
= Math.min(
212 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
?? Infinity
214 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
= Math.max(
216 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
?? -Infinity
218 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
219 (this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
?? 0) + entry
.duration
;
220 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
221 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
222 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
223 this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementSeries
instanceof CircularArray
224 ? this.statistics
.statisticsData
226 ?.timeMeasurementSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
227 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
228 new CircularArray
<TimeSeries
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
229 timestamp
: entry
.startTime
,
230 value
: entry
.duration
,
232 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= median(
233 this.extractTimeSeriesValues(
234 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
237 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
239 this.extractTimeSeriesValues(
240 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
244 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= stdDeviation(
245 this.extractTimeSeriesValues(
246 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
249 if (Configuration
.getPerformanceStorage().enabled
) {
250 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
254 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
255 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
258 private logPrefix
= (): string => {
259 return logPrefix(` ${this.objName} | Performance statistics`);