1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
3 import { CircularArray
, DEFAULT_CIRCULAR_ARRAY_SIZE
} from
'../utils/CircularArray';
4 import { IncomingRequestCommand
, RequestCommand
} from
'../types/ocpp/Requests';
5 import { PerformanceEntry
, PerformanceObserver
, performance
} from
'perf_hooks';
6 import Statistics
, { StatisticsData
, TimeSeries
} from
'../types/Statistics';
8 import { ChargingStationWorkerMessageEvents
} from
'../types/ChargingStationWorker';
9 import Configuration from
'../utils/Configuration';
10 import { MessageType
} from
'../types/ocpp/MessageType';
11 import { URL
} from
'url';
12 import Utils from
'../utils/Utils';
13 import logger from
'../utils/Logger';
14 import { parentPort
} from
'worker_threads';
16 export default class PerformanceStatistics
{
17 private readonly objId
: string;
18 private performanceObserver
: PerformanceObserver
;
19 private readonly statistics
: Statistics
;
20 private displayInterval
: NodeJS
.Timeout
;
22 public constructor(objId
: string, URI
: URL
) {
24 this.initializePerformanceObserver();
25 this.statistics
= { id
: this.objId
?? 'Object id not specified', URI
: URI
.toString(), createdAt
: new Date(), statisticsData
: new Map
<string, Partial
<StatisticsData
>>() };
28 public static beginMeasure(id
: string): string {
29 const markId
= `${id.charAt(0).toUpperCase() + id.slice(1)}~${Utils.generateUUID()}`;
30 performance
.mark(markId
);
34 public static endMeasure(name
: string, markId
: string): void {
35 performance
.measure(name
, markId
);
36 performance
.clearMarks(markId
);
39 public addRequestStatistic(command
: RequestCommand
| IncomingRequestCommand
, messageType
: MessageType
): void {
40 switch (messageType
) {
41 case MessageType
.CALL_MESSAGE
:
42 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countRequest
) {
43 this.statistics
.statisticsData
.get(command
).countRequest
++;
45 this.statistics
.statisticsData
.set(command
, Object.assign({ countRequest
: 1 }, this.statistics
.statisticsData
.get(command
)));
48 case MessageType
.CALL_RESULT_MESSAGE
:
49 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countResponse
) {
50 this.statistics
.statisticsData
.get(command
).countResponse
++;
52 this.statistics
.statisticsData
.set(command
, Object.assign({ countResponse
: 1 }, this.statistics
.statisticsData
.get(command
)));
55 case MessageType
.CALL_ERROR_MESSAGE
:
56 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countError
) {
57 this.statistics
.statisticsData
.get(command
).countError
++;
59 this.statistics
.statisticsData
.set(command
, Object.assign({ countError
: 1 }, this.statistics
.statisticsData
.get(command
)));
63 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
68 public start(): void {
69 this.startLogStatisticsInterval();
70 if (Configuration
.getPerformanceStorage().enabled
) {
71 logger
.info(`${this.logPrefix()} storage enabled: type ${Configuration.getPerformanceStorage().type}, URI: ${Configuration.getPerformanceStorage().URI}`);
76 if (this.displayInterval
) {
77 clearInterval(this.displayInterval
);
79 performance
.clearMarks();
80 this.performanceObserver
?.disconnect();
83 public restart(): void {
88 private initializePerformanceObserver(): void {
89 this.performanceObserver
= new PerformanceObserver((list
) => {
90 const lastPerformanceEntry
= list
.getEntries()[0];
91 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
92 logger
.debug(`${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`, lastPerformanceEntry
);
94 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
97 private logStatistics(): void {
98 logger
.info(this.logPrefix() + ' %j', this.statistics
);
101 private startLogStatisticsInterval(): void {
102 if (Configuration
.getLogStatisticsInterval() > 0) {
103 this.displayInterval
= setInterval(() => {
104 this.logStatistics();
105 }, Configuration
.getLogStatisticsInterval() * 1000);
106 logger
.info(this.logPrefix() + ' logged every ' + Utils
.formatDurationSeconds(Configuration
.getLogStatisticsInterval()));
108 logger
.info(this.logPrefix() + ' log interval is set to ' + Configuration
.getLogStatisticsInterval().toString() + '. Not logging statistics');
112 private median(dataSet
: number[]): number {
113 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
116 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
117 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
118 if (sortedDataSet
.length
% 2) {
119 return sortedDataSet
[middleIndex
/ 2];
121 return (sortedDataSet
[(middleIndex
- 1)] + sortedDataSet
[middleIndex
]) / 2;
124 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
125 private percentile(dataSet
: number[], percentile
: number): number {
126 if (percentile
< 0 && percentile
> 100) {
127 throw new RangeError('Percentile is not between 0 and 100');
129 if (Utils
.isEmptyArray(dataSet
)) {
132 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
133 if (percentile
=== 0) {
134 return sortedDataSet
[0];
136 if (percentile
=== 100) {
137 return sortedDataSet
[sortedDataSet
.length
- 1];
139 const percentileIndex
= ((percentile
/ 100) * sortedDataSet
.length
) - 1;
140 if (Number.isInteger(percentileIndex
)) {
141 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
143 return sortedDataSet
[Math.round(percentileIndex
)];
146 private stdDeviation(dataSet
: number[]): number {
147 let totalDataSet
= 0;
148 for (const data
of dataSet
) {
149 totalDataSet
+= data
;
151 const dataSetMean
= totalDataSet
/ dataSet
.length
;
152 let totalGeometricDeviation
= 0;
153 for (const data
of dataSet
) {
154 const deviation
= data
- dataSetMean
;
155 totalGeometricDeviation
+= deviation
* deviation
;
157 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
160 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
161 let entryName
= entry
.name
;
163 const MAP_NAME
: Record
<string, string> = {};
164 if (MAP_NAME
[entryName
]) {
165 entryName
= MAP_NAME
[entryName
];
167 // Initialize command statistics
168 if (!this.statistics
.statisticsData
.has(entryName
)) {
169 this.statistics
.statisticsData
.set(entryName
, {});
171 // Update current statistics
172 this.statistics
.updatedAt
= new Date();
173 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
174 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
176 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
177 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
178 ? (this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
? entry
.duration
: this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
)
180 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
181 ? (this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
? entry
.duration
: this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
)
183 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
184 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
186 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/ this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
187 Array.isArray(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
)
188 ? this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
189 : this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
= new CircularArray
<TimeSeries
>(DEFAULT_CIRCULAR_ARRAY_SIZE
, { timestamp
: entry
.startTime
, value
: entry
.duration
});
190 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
));
191 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
= this.percentile(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
), 95);
192 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
));
193 if (Configuration
.getPerformanceStorage().enabled
) {
194 parentPort
.postMessage({ id
: ChargingStationWorkerMessageEvents
.PERFORMANCE_STATISTICS
, data
: this.statistics
});
198 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
199 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
202 private logPrefix(): string {
203 return Utils
.logPrefix(` ${this.objId} | Performance statistics`);