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';
7 import { secondsToMilliseconds
} from
'date-fns';
11 type IncomingRequestCommand
,
12 type LogConfiguration
,
16 type StorageConfiguration
,
23 JSONStringifyWithMapSupport
,
25 buildPerformanceStatisticsMessage
,
26 extractTimeSeriesValues
,
27 formatDurationSeconds
,
38 export class PerformanceStatistics
{
39 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
44 private readonly objId
: string;
45 private readonly objName
: string;
46 private performanceObserver
!: PerformanceObserver
;
47 private readonly statistics
: Statistics
;
48 private displayInterval
?: NodeJS
.Timeout
;
50 private constructor(objId
: string, objName
: string, uri
: URL
) {
52 this.objName
= objName
;
53 this.initializePerformanceObserver();
55 id
: this.objId
?? 'Object id not specified',
56 name
: this.objName
?? 'Object name not specified',
58 createdAt
: new Date(),
59 statisticsData
: new Map(),
63 public static getInstance(
67 ): PerformanceStatistics
| undefined {
68 if (!PerformanceStatistics
.instances
.has(objId
)) {
69 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
71 return PerformanceStatistics
.instances
.get(objId
);
74 public static beginMeasure(id
: string): string {
75 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
76 performance
.mark(markId
);
80 public static endMeasure(name
: string, markId
: string): void {
82 performance
.measure(name
, markId
);
84 if (error
instanceof Error && error
.message
.includes('performance mark has not been set')) {
90 performance
.clearMarks(markId
);
91 performance
.clearMeasures(name
);
94 public addRequestStatistic(
95 command
: RequestCommand
| IncomingRequestCommand
,
96 messageType
: MessageType
,
98 switch (messageType
) {
99 case MessageType
.CALL_MESSAGE
:
101 this.statistics
.statisticsData
.has(command
) &&
102 this.statistics
.statisticsData
.get(command
)?.requestCount
104 ++this.statistics
.statisticsData
.get(command
)!.requestCount
!;
106 this.statistics
.statisticsData
.set(command
, {
107 ...this.statistics
.statisticsData
.get(command
),
112 case MessageType
.CALL_RESULT_MESSAGE
:
114 this.statistics
.statisticsData
.has(command
) &&
115 this.statistics
.statisticsData
.get(command
)?.responseCount
117 ++this.statistics
.statisticsData
.get(command
)!.responseCount
!;
119 this.statistics
.statisticsData
.set(command
, {
120 ...this.statistics
.statisticsData
.get(command
),
125 case MessageType
.CALL_ERROR_MESSAGE
:
127 this.statistics
.statisticsData
.has(command
) &&
128 this.statistics
.statisticsData
.get(command
)?.errorCount
130 ++this.statistics
.statisticsData
.get(command
)!.errorCount
!;
132 this.statistics
.statisticsData
.set(command
, {
133 ...this.statistics
.statisticsData
.get(command
),
139 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
140 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
145 public start(): void {
146 this.startLogStatisticsInterval();
147 const performanceStorageConfiguration
=
148 Configuration
.getConfigurationSection
<StorageConfiguration
>(
149 ConfigurationSection
.performanceStorage
,
151 if (performanceStorageConfiguration
.enabled
) {
153 `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
154 performanceStorageConfiguration.uri
160 public stop(): void {
161 this.stopLogStatisticsInterval();
162 performance
.clearMarks();
163 performance
.clearMeasures();
164 this.performanceObserver
?.disconnect();
167 public restart(): void {
172 private initializePerformanceObserver(): void {
173 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
174 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
176 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
177 // lastPerformanceEntry,
179 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
181 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
184 private logStatistics(): void {
185 logger
.info(`${this.logPrefix()}`, {
187 statisticsData
: JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
191 private startLogStatisticsInterval(): void {
192 const logConfiguration
= Configuration
.getConfigurationSection
<LogConfiguration
>(
193 ConfigurationSection
.log
,
195 const logStatisticsInterval
= logConfiguration
.enabled
196 ? logConfiguration
.statisticsInterval
!
198 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
199 this.displayInterval
= setInterval(() => {
200 this.logStatistics();
201 }, secondsToMilliseconds(logStatisticsInterval
));
203 `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
205 } else if (this.displayInterval
) {
207 `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
209 } else if (logConfiguration
.enabled
) {
211 `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`,
216 private stopLogStatisticsInterval(): void {
217 if (this.displayInterval
) {
218 clearInterval(this.displayInterval
);
219 delete this.displayInterval
;
223 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
224 // Initialize command statistics
225 if (!this.statistics
.statisticsData
.has(entry
.name
)) {
226 this.statistics
.statisticsData
.set(entry
.name
, {});
228 // Update current statistics
229 this.statistics
.statisticsData
.get(entry
.name
)!.timeMeasurementCount
=
230 (this.statistics
.statisticsData
.get(entry
.name
)?.timeMeasurementCount
?? 0) + 1;
231 this.statistics
.statisticsData
.get(entry
.name
)!.currentTimeMeasurement
= entry
.duration
;
232 this.statistics
.statisticsData
.get(entry
.name
)!.minTimeMeasurement
= min(
234 this.statistics
.statisticsData
.get(entry
.name
)?.minTimeMeasurement
?? Infinity,
236 this.statistics
.statisticsData
.get(entry
.name
)!.maxTimeMeasurement
= max(
238 this.statistics
.statisticsData
.get(entry
.name
)?.maxTimeMeasurement
?? -Infinity,
240 this.statistics
.statisticsData
.get(entry
.name
)!.totalTimeMeasurement
=
241 (this.statistics
.statisticsData
.get(entry
.name
)?.totalTimeMeasurement
?? 0) + entry
.duration
;
242 this.statistics
.statisticsData
.get(entry
.name
)?.measurementTimeSeries
instanceof CircularArray
243 ? this.statistics
.statisticsData
245 ?.measurementTimeSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
246 : (this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
=
247 new CircularArray
<TimestampedData
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
248 timestamp
: entry
.startTime
,
249 value
: entry
.duration
,
251 const timeMeasurementValues
= extractTimeSeriesValues(
252 this.statistics
.statisticsData
.get(entry
.name
)!.measurementTimeSeries
!,
254 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
=
255 average(timeMeasurementValues
);
256 this.statistics
.statisticsData
.get(entry
.name
)!.medTimeMeasurement
=
257 median(timeMeasurementValues
);
258 this.statistics
.statisticsData
.get(entry
.name
)!.ninetyFiveThPercentileTimeMeasurement
=
259 nthPercentile(timeMeasurementValues
, 95);
260 this.statistics
.statisticsData
.get(entry
.name
)!.stdDevTimeMeasurement
= stdDeviation(
261 timeMeasurementValues
,
262 this.statistics
.statisticsData
.get(entry
.name
)!.avgTimeMeasurement
,
264 this.statistics
.updatedAt
= new Date();
266 Configuration
.getConfigurationSection
<StorageConfiguration
>(
267 ConfigurationSection
.performanceStorage
,
270 parentPort
?.postMessage(buildPerformanceStatisticsMessage(this.statistics
));
274 private logPrefix
= (): string => {
275 return logPrefix(` ${this.objName} | Performance statistics`);