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
,
19 buildPerformanceStatisticsMessage
,
26 export class PerformanceStatistics
{
27 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
32 private readonly objId
: string;
33 private readonly objName
: string;
34 private performanceObserver
!: PerformanceObserver
;
35 private readonly statistics
: Statistics
;
36 private displayInterval
!: NodeJS
.Timeout
;
38 private constructor(objId
: string, objName
: string, uri
: URL
) {
40 this.objName
= objName
;
41 this.initializePerformanceObserver();
43 id
: this.objId
?? 'Object id not specified',
44 name
: this.objName
?? 'Object name not specified',
46 createdAt
: new Date(),
47 statisticsData
: new Map(),
51 public static getInstance(
55 ): PerformanceStatistics
| undefined {
56 if (!PerformanceStatistics
.instances
.has(objId
)) {
57 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
59 return PerformanceStatistics
.instances
.get(objId
);
62 public static beginMeasure(id
: string): string {
63 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
64 performance
.mark(markId
);
68 public static endMeasure(name
: string, markId
: string): void {
69 performance
.measure(name
, markId
);
70 performance
.clearMarks(markId
);
71 performance
.clearMeasures(name
);
74 public addRequestStatistic(
75 command
: RequestCommand
| IncomingRequestCommand
,
76 messageType
: MessageType
78 switch (messageType
) {
79 case MessageType
.CALL_MESSAGE
:
81 this.statistics
.statisticsData
.has(command
) &&
82 this.statistics
.statisticsData
.get(command
)?.countRequest
84 ++this.statistics
.statisticsData
.get(command
).countRequest
;
86 this.statistics
.statisticsData
.set(command
, {
87 ...this.statistics
.statisticsData
.get(command
),
92 case MessageType
.CALL_RESULT_MESSAGE
:
94 this.statistics
.statisticsData
.has(command
) &&
95 this.statistics
.statisticsData
.get(command
)?.countResponse
97 ++this.statistics
.statisticsData
.get(command
).countResponse
;
99 this.statistics
.statisticsData
.set(command
, {
100 ...this.statistics
.statisticsData
.get(command
),
105 case MessageType
.CALL_ERROR_MESSAGE
:
107 this.statistics
.statisticsData
.has(command
) &&
108 this.statistics
.statisticsData
.get(command
)?.countError
110 ++this.statistics
.statisticsData
.get(command
).countError
;
112 this.statistics
.statisticsData
.set(command
, {
113 ...this.statistics
.statisticsData
.get(command
),
119 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
120 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
125 public start(): void {
126 this.startLogStatisticsInterval();
127 if (Configuration
.getPerformanceStorage().enabled
) {
129 `${this.logPrefix()} storage enabled: type ${
130 Configuration.getPerformanceStorage().type
131 }, uri: ${Configuration.getPerformanceStorage().uri}`
136 public stop(): void {
137 this.stopLogStatisticsInterval();
138 performance
.clearMarks();
139 performance
.clearMeasures();
140 this.performanceObserver
?.disconnect();
143 public restart(): void {
148 private initializePerformanceObserver(): void {
149 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
150 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
152 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
153 // lastPerformanceEntry
155 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
157 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
160 private logStatistics(): void {
161 logger
.info(`${this.logPrefix()}`, {
163 statisticsData
: Utils
.JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
167 private startLogStatisticsInterval(): void {
168 const logStatisticsInterval
= Configuration
.getLog().enabled
169 ? Configuration
.getLog().statisticsInterval
171 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
172 this.displayInterval
= setInterval(() => {
173 this.logStatistics();
174 }, logStatisticsInterval
* 1000);
176 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(logStatisticsInterval)}`
178 } else if (this.displayInterval
) {
180 `${this.logPrefix()} already logged every ${Utils.formatDurationSeconds(
181 logStatisticsInterval
184 } else if (Configuration
.getLog().enabled
) {
186 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
191 private stopLogStatisticsInterval(): void {
192 if (this.displayInterval
) {
193 clearInterval(this.displayInterval
);
194 delete this.displayInterval
;
198 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
199 const entryName
= entry
.name
;
200 // Initialize command statistics
201 if (!this.statistics
.statisticsData
.has(entryName
)) {
202 this.statistics
.statisticsData
.set(entryName
, {});
204 // Update current statistics
205 this.statistics
.updatedAt
= new Date();
206 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
207 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
208 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
210 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
211 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
212 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
213 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
215 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
217 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
218 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
219 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
221 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
223 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
224 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
225 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
227 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
228 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
229 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
230 this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementSeries
instanceof CircularArray
231 ? this.statistics
.statisticsData
233 ?.timeMeasurementSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
234 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
235 new CircularArray
<TimeSeries
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
236 timestamp
: entry
.startTime
,
237 value
: entry
.duration
,
239 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= median(
240 this.extractTimeSeriesValues(
241 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
244 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
246 this.extractTimeSeriesValues(
247 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
251 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= stdDeviation(
252 this.extractTimeSeriesValues(
253 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
256 if (Configuration
.getPerformanceStorage().enabled
) {
257 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
261 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
262 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
265 private logPrefix
= (): string => {
266 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);