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 { MessageChannelUtils
} from
'../charging-station';
9 type IncomingRequestCommand
,
15 import { CircularArray
, Configuration
, Constants
, Utils
, logger
} from
'../utils';
17 export class PerformanceStatistics
{
18 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
23 private readonly objId
: string;
24 private readonly objName
: string;
25 private performanceObserver
!: PerformanceObserver
;
26 private readonly statistics
: Statistics
;
27 private displayInterval
!: NodeJS
.Timeout
;
29 private constructor(objId
: string, objName
: string, uri
: URL
) {
31 this.objName
= objName
;
32 this.initializePerformanceObserver();
34 id
: this.objId
?? 'Object id not specified',
35 name
: this.objName
?? 'Object name not specified',
37 createdAt
: new Date(),
38 statisticsData
: new Map(),
42 public static getInstance(
46 ): PerformanceStatistics
| undefined {
47 if (!PerformanceStatistics
.instances
.has(objId
)) {
48 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
50 return PerformanceStatistics
.instances
.get(objId
);
53 public static beginMeasure(id
: string): string {
54 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
55 performance
.mark(markId
);
59 public static endMeasure(name
: string, markId
: string): void {
60 performance
.measure(name
, markId
);
61 performance
.clearMarks(markId
);
62 performance
.clearMeasures(name
);
65 public addRequestStatistic(
66 command
: RequestCommand
| IncomingRequestCommand
,
67 messageType
: MessageType
69 switch (messageType
) {
70 case MessageType
.CALL_MESSAGE
:
72 this.statistics
.statisticsData
.has(command
) &&
73 this.statistics
.statisticsData
.get(command
)?.countRequest
75 this.statistics
.statisticsData
.get(command
).countRequest
++;
77 this.statistics
.statisticsData
.set(command
, {
78 ...this.statistics
.statisticsData
.get(command
),
83 case MessageType
.CALL_RESULT_MESSAGE
:
85 this.statistics
.statisticsData
.has(command
) &&
86 this.statistics
.statisticsData
.get(command
)?.countResponse
88 this.statistics
.statisticsData
.get(command
).countResponse
++;
90 this.statistics
.statisticsData
.set(command
, {
91 ...this.statistics
.statisticsData
.get(command
),
96 case MessageType
.CALL_ERROR_MESSAGE
:
98 this.statistics
.statisticsData
.has(command
) &&
99 this.statistics
.statisticsData
.get(command
)?.countError
101 this.statistics
.statisticsData
.get(command
).countError
++;
103 this.statistics
.statisticsData
.set(command
, {
104 ...this.statistics
.statisticsData
.get(command
),
110 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
111 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
116 public start(): void {
117 this.startLogStatisticsInterval();
118 if (Configuration
.getPerformanceStorage().enabled
) {
120 `${this.logPrefix()} storage enabled: type ${
121 Configuration.getPerformanceStorage().type
122 }, uri: ${Configuration.getPerformanceStorage().uri}`
127 public stop(): void {
128 this.stopLogStatisticsInterval();
129 performance
.clearMarks();
130 performance
.clearMeasures();
131 this.performanceObserver
?.disconnect();
134 public restart(): void {
139 private initializePerformanceObserver(): void {
140 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
141 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
143 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
144 // lastPerformanceEntry
146 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
148 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
151 private logStatistics(): void {
152 logger
.info(`${this.logPrefix()}`, {
154 statisticsData
: Utils
.JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
158 private startLogStatisticsInterval(): void {
159 const logStatisticsInterval
= Configuration
.getLogStatisticsInterval();
160 if (logStatisticsInterval
> 0 && !this.displayInterval
) {
161 this.displayInterval
= setInterval(() => {
162 this.logStatistics();
163 }, logStatisticsInterval
* 1000);
165 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(logStatisticsInterval)}`
167 } else if (this.displayInterval
) {
169 `${this.logPrefix()} already logged every ${Utils.formatDurationSeconds(
170 logStatisticsInterval
175 `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics`
180 private stopLogStatisticsInterval(): void {
181 if (this.displayInterval
) {
182 clearInterval(this.displayInterval
);
183 delete this.displayInterval
;
187 private median(dataSet
: number[]): number {
188 if (Array.isArray(dataSet
) === true && dataSet
.length
=== 1) {
191 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
192 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
193 if (sortedDataSet
.length
% 2) {
194 return sortedDataSet
[middleIndex
/ 2];
196 return (sortedDataSet
[middleIndex
- 1] + sortedDataSet
[middleIndex
]) / 2;
199 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
200 private percentile(dataSet
: number[], percentile
: number): number {
201 if (percentile
< 0 && percentile
> 100) {
202 throw new RangeError('Percentile is not between 0 and 100');
204 if (Utils
.isEmptyArray(dataSet
)) {
207 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
208 if (percentile
=== 0) {
209 return sortedDataSet
[0];
211 if (percentile
=== 100) {
212 return sortedDataSet
[sortedDataSet
.length
- 1];
214 const percentileIndex
= (percentile
/ 100) * sortedDataSet
.length
- 1;
215 if (Number.isInteger(percentileIndex
)) {
216 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
218 return sortedDataSet
[Math.round(percentileIndex
)];
221 private stdDeviation(dataSet
: number[]): number {
222 let totalDataSet
= 0;
223 for (const data
of dataSet
) {
224 totalDataSet
+= data
;
226 const dataSetMean
= totalDataSet
/ dataSet
.length
;
227 let totalGeometricDeviation
= 0;
228 for (const data
of dataSet
) {
229 const deviation
= data
- dataSetMean
;
230 totalGeometricDeviation
+= deviation
* deviation
;
232 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
235 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
236 const entryName
= entry
.name
;
237 // Initialize command statistics
238 if (!this.statistics
.statisticsData
.has(entryName
)) {
239 this.statistics
.statisticsData
.set(entryName
, {});
241 // Update current statistics
242 this.statistics
.updatedAt
= new Date();
243 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
244 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
245 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
247 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
248 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
249 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
250 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
252 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
254 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
255 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
256 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
258 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
260 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
261 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
262 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
264 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
265 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
266 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
267 this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementSeries
instanceof CircularArray
268 ? this.statistics
.statisticsData
270 ?.timeMeasurementSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
271 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
272 new CircularArray
<TimeSeries
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
273 timestamp
: entry
.startTime
,
274 value
: entry
.duration
,
276 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(
277 this.extractTimeSeriesValues(
278 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
281 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
283 this.extractTimeSeriesValues(
284 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
288 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(
289 this.extractTimeSeriesValues(
290 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
293 if (Configuration
.getPerformanceStorage().enabled
) {
294 parentPort
?.postMessage(
295 MessageChannelUtils
.buildPerformanceStatisticsMessage(this.statistics
)
300 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
301 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
304 private logPrefix
= (): string => {
305 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);