1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { PerformanceEntry
, PerformanceObserver
, performance
} from
'perf_hooks';
4 import type { URL
} from
'url';
5 import { parentPort
} from
'worker_threads';
7 import { MessageChannelUtils
} from
'../charging-station/MessageChannelUtils';
8 import { MessageType
} from
'../types/ocpp/MessageType';
9 import type { IncomingRequestCommand
, RequestCommand
} from
'../types/ocpp/Requests';
10 import type { Statistics
, TimeSeries
} from
'../types/Statistics';
11 import { CircularArray
, DEFAULT_CIRCULAR_ARRAY_SIZE
} from
'../utils/CircularArray';
12 import Configuration from
'../utils/Configuration';
13 import logger from
'../utils/Logger';
14 import Utils from
'../utils/Utils';
16 export default class PerformanceStatistics
{
17 private static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
22 private readonly objId
: string;
23 private readonly objName
: string;
24 private performanceObserver
: PerformanceObserver
;
25 private readonly statistics
: Statistics
;
26 private displayInterval
: NodeJS
.Timeout
;
28 private constructor(objId
: string, objName
: string, uri
: URL
) {
30 this.objName
= objName
;
31 this.initializePerformanceObserver();
33 id
: this.objId
?? 'Object id not specified',
34 name
: this.objName
?? 'Object name not specified',
36 createdAt
: new Date(),
37 statisticsData
: new Map(),
41 public static getInstance(
45 ): PerformanceStatistics
| undefined {
46 if (!PerformanceStatistics
.instances
.has(objId
)) {
47 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
49 return PerformanceStatistics
.instances
.get(objId
);
52 public static beginMeasure(id
: string): string {
53 const markId
= `${id.charAt(0).toUpperCase()}${id.slice(1)}~${Utils.generateUUID()}`;
54 performance
.mark(markId
);
58 public static endMeasure(name
: string, markId
: string): void {
59 performance
.measure(name
, markId
);
60 performance
.clearMarks(markId
);
61 performance
.clearMeasures(name
);
64 public addRequestStatistic(
65 command
: RequestCommand
| IncomingRequestCommand
,
66 messageType
: MessageType
68 switch (messageType
) {
69 case MessageType
.CALL_MESSAGE
:
71 this.statistics
.statisticsData
.has(command
) &&
72 this.statistics
.statisticsData
.get(command
)?.countRequest
74 this.statistics
.statisticsData
.get(command
).countRequest
++;
76 this.statistics
.statisticsData
.set(
78 Object.assign({ countRequest
: 1 }, this.statistics
.statisticsData
.get(command
))
82 case MessageType
.CALL_RESULT_MESSAGE
:
84 this.statistics
.statisticsData
.has(command
) &&
85 this.statistics
.statisticsData
.get(command
)?.countResponse
87 this.statistics
.statisticsData
.get(command
).countResponse
++;
89 this.statistics
.statisticsData
.set(
91 Object.assign({ countResponse
: 1 }, this.statistics
.statisticsData
.get(command
))
95 case MessageType
.CALL_ERROR_MESSAGE
:
97 this.statistics
.statisticsData
.has(command
) &&
98 this.statistics
.statisticsData
.get(command
)?.countError
100 this.statistics
.statisticsData
.get(command
).countError
++;
102 this.statistics
.statisticsData
.set(
104 Object.assign({ countError
: 1 }, this.statistics
.statisticsData
.get(command
))
109 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
110 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
115 public start(): void {
116 this.startLogStatisticsInterval();
117 if (Configuration
.getPerformanceStorage().enabled
) {
119 `${this.logPrefix()} storage enabled: type ${
120 Configuration.getPerformanceStorage().type
121 }, uri: ${Configuration.getPerformanceStorage().uri}`
126 public stop(): void {
127 if (this.displayInterval
) {
128 clearInterval(this.displayInterval
);
130 performance
.clearMarks();
131 performance
.clearMeasures();
132 this.performanceObserver
?.disconnect();
135 public restart(): void {
140 private initializePerformanceObserver(): void {
141 this.performanceObserver
= new PerformanceObserver((performanceObserverList
) => {
142 const lastPerformanceEntry
= performanceObserverList
.getEntries()[0];
144 // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
145 // lastPerformanceEntry
147 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
149 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
152 private logStatistics(): void {
153 logger
.info(`${this.logPrefix()}`, {
155 statisticsData
: Utils
.JSONStringifyWithMapSupport(this.statistics
.statisticsData
),
159 private startLogStatisticsInterval(): void {
160 if (Configuration
.getLogStatisticsInterval() > 0) {
161 this.displayInterval
= setInterval(() => {
162 this.logStatistics();
163 }, Configuration
.getLogStatisticsInterval() * 1000);
165 `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(
166 Configuration.getLogStatisticsInterval()
171 `${this.logPrefix()} log interval is set to ${Configuration.getLogStatisticsInterval().toString()}. Not logging statistics`
176 private median(dataSet
: number[]): number {
177 if (Array.isArray(dataSet
) === true && dataSet
.length
=== 1) {
180 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
181 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
182 if (sortedDataSet
.length
% 2) {
183 return sortedDataSet
[middleIndex
/ 2];
185 return (sortedDataSet
[middleIndex
- 1] + sortedDataSet
[middleIndex
]) / 2;
188 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
189 private percentile(dataSet
: number[], percentile
: number): number {
190 if (percentile
< 0 && percentile
> 100) {
191 throw new RangeError('Percentile is not between 0 and 100');
193 if (Utils
.isEmptyArray(dataSet
)) {
196 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
197 if (percentile
=== 0) {
198 return sortedDataSet
[0];
200 if (percentile
=== 100) {
201 return sortedDataSet
[sortedDataSet
.length
- 1];
203 const percentileIndex
= (percentile
/ 100) * sortedDataSet
.length
- 1;
204 if (Number.isInteger(percentileIndex
)) {
205 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
207 return sortedDataSet
[Math.round(percentileIndex
)];
210 private stdDeviation(dataSet
: number[]): number {
211 let totalDataSet
= 0;
212 for (const data
of dataSet
) {
213 totalDataSet
+= data
;
215 const dataSetMean
= totalDataSet
/ dataSet
.length
;
216 let totalGeometricDeviation
= 0;
217 for (const data
of dataSet
) {
218 const deviation
= data
- dataSetMean
;
219 totalGeometricDeviation
+= deviation
* deviation
;
221 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
224 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
225 const entryName
= entry
.name
;
226 // Initialize command statistics
227 if (!this.statistics
.statisticsData
.has(entryName
)) {
228 this.statistics
.statisticsData
.set(entryName
, {});
230 // Update current statistics
231 this.statistics
.updatedAt
= new Date();
232 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
233 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
234 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
236 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
237 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
238 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
239 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
241 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
243 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
244 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
245 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
247 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
249 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
250 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
251 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
253 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
254 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
255 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
256 Array.isArray(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
) === true
257 ? this.statistics
.statisticsData
259 .timeMeasurementSeries
.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
260 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
261 new CircularArray
<TimeSeries
>(DEFAULT_CIRCULAR_ARRAY_SIZE
, {
262 timestamp
: entry
.startTime
,
263 value
: entry
.duration
,
265 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(
266 this.extractTimeSeriesValues(
267 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
270 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
272 this.extractTimeSeriesValues(
273 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
277 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(
278 this.extractTimeSeriesValues(
279 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
282 if (Configuration
.getPerformanceStorage().enabled
) {
283 parentPort
.postMessage(
284 MessageChannelUtils
.buildPerformanceStatisticsMessage(this.statistics
)
289 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
290 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
293 private logPrefix(): string {
294 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);