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
,
23 export class PerformanceStatistics
{
24 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
29 private readonly objId
: string;
30 private readonly objName
: string;
31 private performanceObserver
!: PerformanceObserver
;
32 private readonly statistics
: Statistics
;
33 private displayInterval
!: NodeJS
.Timeout
;
35 private constructor(objId
: string, objName
: string, uri
: URL
) {
37 this.objName
= objName
;
38 this.initializePerformanceObserver();
40 id
: this.objId
?? 'Object id not specified',
41 name
: this.objName
?? 'Object name not specified',
43 createdAt
: new Date(),
44 statisticsData
: new Map(),
48 public static getInstance(
52 ): PerformanceStatistics
| undefined {
53 if (!PerformanceStatistics
.instances
.has(objId
)) {
54 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
56 return PerformanceStatistics
.instances
.get(objId
);
59 public static beginMeasure(id
: string): string {
60 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
61 performance
.mark(markId
);
65 public static endMeasure(name
: string, markId
: string): void {
66 performance
.measure(name
, markId
);
67 performance
.clearMarks(markId
);
68 performance
.clearMeasures(name
);
71 public addRequestStatistic(
72 command
: RequestCommand
| IncomingRequestCommand
,
73 messageType
: MessageType
75 switch (messageType
) {
76 case MessageType
.CALL_MESSAGE
:
78 this.statistics
.statisticsData
.has(command
) &&
79 this.statistics
.statisticsData
.get(command
)?.countRequest
81 this.statistics
.statisticsData
.get(command
).countRequest
++;
83 this.statistics
.statisticsData
.set(command
, {
84 ...this.statistics
.statisticsData
.get(command
),
89 case MessageType
.CALL_RESULT_MESSAGE
:
91 this.statistics
.statisticsData
.has(command
) &&
92 this.statistics
.statisticsData
.get(command
)?.countResponse
94 this.statistics
.statisticsData
.get(command
).countResponse
++;
96 this.statistics
.statisticsData
.set(command
, {
97 ...this.statistics
.statisticsData
.get(command
),
102 case MessageType
.CALL_ERROR_MESSAGE
:
104 this.statistics
.statisticsData
.has(command
) &&
105 this.statistics
.statisticsData
.get(command
)?.countError
107 this.statistics
.statisticsData
.get(command
).countError
++;
109 this.statistics
.statisticsData
.set(command
, {
110 ...this.statistics
.statisticsData
.get(command
),
116 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
117 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
122 public start(): void {
123 this.startLogStatisticsInterval();
124 if (Configuration
.getPerformanceStorage().enabled
) {
126 `${this.logPrefix()} storage enabled: type ${
127 Configuration.getPerformanceStorage().type
128 }, uri: ${Configuration.getPerformanceStorage().uri}`
133 public stop(): void {
134 this.stopLogStatisticsInterval();
135 performance
.clearMarks();
136 performance
.clearMeasures();
137 this.performanceObserver
?.disconnect();
140 public restart(): void {
145 private initializePerformanceObserver(): void {
146 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
147 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
149 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
150 // lastPerformanceEntry
152 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
154 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
157 private logStatistics(): void {
158 logger
.info(`${this.logPrefix()}`, {
160 statisticsData
: Utils
.JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
164 private startLogStatisticsInterval(): void {
165 const logStatisticsInterval
= Configuration
.getLogStatisticsInterval();
166 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
167 this.displayInterval
= setInterval(() => {
168 this.logStatistics();
169 }, logStatisticsInterval
* 1000);
171 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(logStatisticsInterval)}`
173 } else if (this.displayInterval
) {
175 `${this.logPrefix()} already logged every ${Utils.formatDurationSeconds(
176 logStatisticsInterval
181 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
186 private stopLogStatisticsInterval(): void {
187 if (this.displayInterval
) {
188 clearInterval(this.displayInterval
);
189 delete this.displayInterval
;
193 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
194 const entryName
= entry
.name
;
195 // Initialize command statistics
196 if (!this.statistics
.statisticsData
.has(entryName
)) {
197 this.statistics
.statisticsData
.set(entryName
, {});
199 // Update current statistics
200 this.statistics
.updatedAt
= new Date();
201 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
202 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
203 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
205 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
206 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
207 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
208 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
210 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
212 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
213 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
214 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
216 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
218 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
219 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
220 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
222 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
223 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
224 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
225 this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementSeries
instanceof CircularArray
226 ? this.statistics
.statisticsData
228 ?.timeMeasurementSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
229 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
230 new CircularArray
<TimeSeries
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
231 timestamp
: entry
.startTime
,
232 value
: entry
.duration
,
234 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= Utils
.median(
235 this.extractTimeSeriesValues(
236 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
239 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
241 this.extractTimeSeriesValues(
242 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
246 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= Utils
.stdDeviation(
247 this.extractTimeSeriesValues(
248 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
251 if (Configuration
.getPerformanceStorage().enabled
) {
252 parentPort
?.postMessage(
253 MessageChannelUtils
.buildPerformanceStatisticsMessage(this.statistics
)
258 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
259 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
262 private logPrefix
= (): string => {
263 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);