1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import type { URL
} from
'node:url';
4 import { PerformanceEntry
, PerformanceObserver
, performance
} from
'perf_hooks';
5 import { parentPort
} from
'worker_threads';
7 import { MessageChannelUtils
} from
'../charging-station/MessageChannelUtils';
9 type IncomingRequestCommand
,
15 import { CircularArray
} from
'../utils/CircularArray';
16 import { Configuration
} from
'../utils/Configuration';
17 import { Constants
} from
'../utils/Constants';
18 import { logger
} from
'../utils/Logger';
19 import { Utils
} from
'../utils/Utils';
21 export class PerformanceStatistics
{
22 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
27 private readonly objId
: string;
28 private readonly objName
: string;
29 private performanceObserver
!: PerformanceObserver
;
30 private readonly statistics
: Statistics
;
31 private displayInterval
!: NodeJS
.Timeout
;
33 private constructor(objId
: string, objName
: string, uri
: URL
) {
35 this.objName
= objName
;
36 this.initializePerformanceObserver();
38 id
: this.objId
?? 'Object id not specified',
39 name
: this.objName
?? 'Object name not specified',
41 createdAt
: new Date(),
42 statisticsData
: new Map(),
46 public static getInstance(
50 ): PerformanceStatistics
| undefined {
51 if (!PerformanceStatistics
.instances
.has(objId
)) {
52 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
54 return PerformanceStatistics
.instances
.get(objId
);
57 public static beginMeasure(id
: string): string {
58 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
59 performance
.mark(markId
);
63 public static endMeasure(name
: string, markId
: string): void {
64 performance
.measure(name
, markId
);
65 performance
.clearMarks(markId
);
66 performance
.clearMeasures(name
);
69 public addRequestStatistic(
70 command
: RequestCommand
| IncomingRequestCommand
,
71 messageType
: MessageType
73 switch (messageType
) {
74 case MessageType
.CALL_MESSAGE
:
76 this.statistics
.statisticsData
.has(command
) &&
77 this.statistics
.statisticsData
.get(command
)?.countRequest
79 this.statistics
.statisticsData
.get(command
).countRequest
++;
81 this.statistics
.statisticsData
.set(
83 Object.assign({ countRequest
: 1 }, this.statistics
.statisticsData
.get(command
))
87 case MessageType
.CALL_RESULT_MESSAGE
:
89 this.statistics
.statisticsData
.has(command
) &&
90 this.statistics
.statisticsData
.get(command
)?.countResponse
92 this.statistics
.statisticsData
.get(command
).countResponse
++;
94 this.statistics
.statisticsData
.set(
96 Object.assign({ countResponse
: 1 }, this.statistics
.statisticsData
.get(command
))
100 case MessageType
.CALL_ERROR_MESSAGE
:
102 this.statistics
.statisticsData
.has(command
) &&
103 this.statistics
.statisticsData
.get(command
)?.countError
105 this.statistics
.statisticsData
.get(command
).countError
++;
107 this.statistics
.statisticsData
.set(
109 Object.assign({ countError
: 1 }, this.statistics
.statisticsData
.get(command
))
114 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
115 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
120 public start(): void {
121 this.startLogStatisticsInterval();
122 if (Configuration
.getPerformanceStorage().enabled
) {
124 `${this.logPrefix()} storage enabled: type ${
125 Configuration.getPerformanceStorage().type
126 }, uri: ${Configuration.getPerformanceStorage().uri}`
131 public stop(): void {
132 if (this.displayInterval
) {
133 clearInterval(this.displayInterval
);
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 if (Configuration
.getLogStatisticsInterval() > 0) {
166 this.displayInterval
= setInterval(() => {
167 this.logStatistics();
168 }, Configuration
.getLogStatisticsInterval() * 1000);
170 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(
171 Configuration.getLogStatisticsInterval()
176 `${this.logPrefix()} log interval is set to ${Configuration.getLogStatisticsInterval()?.toString()}. Not logging statistics`
181 private median(dataSet
: number[]): number {
182 if (Array.isArray(dataSet
) === true && dataSet
.length
=== 1) {
185 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
186 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
187 if (sortedDataSet
.length
% 2) {
188 return sortedDataSet
[middleIndex
/ 2];
190 return (sortedDataSet
[middleIndex
- 1] + sortedDataSet
[middleIndex
]) / 2;
193 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
194 private percentile(dataSet
: number[], percentile
: number): number {
195 if (percentile
< 0 && percentile
> 100) {
196 throw new RangeError('Percentile is not between 0 and 100');
198 if (Utils
.isEmptyArray(dataSet
)) {
201 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
202 if (percentile
=== 0) {
203 return sortedDataSet
[0];
205 if (percentile
=== 100) {
206 return sortedDataSet
[sortedDataSet
.length
- 1];
208 const percentileIndex
= (percentile
/ 100) * sortedDataSet
.length
- 1;
209 if (Number.isInteger(percentileIndex
)) {
210 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
212 return sortedDataSet
[Math.round(percentileIndex
)];
215 private stdDeviation(dataSet
: number[]): number {
216 let totalDataSet
= 0;
217 for (const data
of dataSet
) {
218 totalDataSet
+= data
;
220 const dataSetMean
= totalDataSet
/ dataSet
.length
;
221 let totalGeometricDeviation
= 0;
222 for (const data
of dataSet
) {
223 const deviation
= data
- dataSetMean
;
224 totalGeometricDeviation
+= deviation
* deviation
;
226 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
229 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
230 const entryName
= entry
.name
;
231 // Initialize command statistics
232 if (!this.statistics
.statisticsData
.has(entryName
)) {
233 this.statistics
.statisticsData
.set(entryName
, {});
235 // Update current statistics
236 this.statistics
.updatedAt
= new Date();
237 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
238 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
239 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
241 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
242 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
243 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
244 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
246 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
248 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
249 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
250 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
252 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
254 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
255 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
256 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
258 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
259 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
260 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
261 this.statistics
.statisticsData
.get(entryName
)?.timeMeasurementSeries
instanceof CircularArray
262 ? this.statistics
.statisticsData
264 ?.timeMeasurementSeries
?.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
265 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
266 new CircularArray
<TimeSeries
>(Constants
.DEFAULT_CIRCULAR_BUFFER_CAPACITY
, {
267 timestamp
: entry
.startTime
,
268 value
: entry
.duration
,
270 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(
271 this.extractTimeSeriesValues(
272 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
275 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
277 this.extractTimeSeriesValues(
278 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
282 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(
283 this.extractTimeSeriesValues(
284 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
287 if (Configuration
.getPerformanceStorage().enabled
) {
288 parentPort
?.postMessage(
289 MessageChannelUtils
.buildPerformanceStatisticsMessage(this.statistics
)
294 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
295 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
298 private logPrefix
= (): string => {
299 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);