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 static readonly instances
: Map
<string, PerformanceStatistics
> = new Map
<
21 private readonly objId
: string;
22 private readonly objName
: string;
23 private performanceObserver
: PerformanceObserver
;
24 private readonly statistics
: Statistics
;
25 private displayInterval
: NodeJS
.Timeout
;
27 private constructor(objId
: string, objName
: string, uri
: URL
) {
29 this.objName
= objName
;
30 this.initializePerformanceObserver();
32 id
: this.objId
?? 'Object id not specified',
33 name
: this.objName
?? 'Object name not specified',
35 createdAt
: new Date(),
36 statisticsData
: new Map
<string, Partial
<StatisticsData
>>(),
40 public static getInstance(objId
: string, objName
: string, uri
: URL
): PerformanceStatistics
{
41 if (!PerformanceStatistics
.instances
.has(objId
)) {
42 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
44 return PerformanceStatistics
.instances
.get(objId
);
47 public static beginMeasure(id
: string): string {
48 const markId
= `${id.charAt(0).toUpperCase() + id.slice(1)}~${Utils.generateUUID()}`;
49 performance
.mark(markId
);
53 public static endMeasure(name
: string, markId
: string): void {
54 performance
.measure(name
, markId
);
55 performance
.clearMarks(markId
);
58 public addRequestStatistic(
59 command
: RequestCommand
| IncomingRequestCommand
,
60 messageType
: MessageType
62 switch (messageType
) {
63 case MessageType
.CALL_MESSAGE
:
65 this.statistics
.statisticsData
.has(command
) &&
66 this.statistics
.statisticsData
.get(command
)?.countRequest
68 this.statistics
.statisticsData
.get(command
).countRequest
++;
70 this.statistics
.statisticsData
.set(
72 Object.assign({ countRequest
: 1 }, this.statistics
.statisticsData
.get(command
))
76 case MessageType
.CALL_RESULT_MESSAGE
:
78 this.statistics
.statisticsData
.has(command
) &&
79 this.statistics
.statisticsData
.get(command
)?.countResponse
81 this.statistics
.statisticsData
.get(command
).countResponse
++;
83 this.statistics
.statisticsData
.set(
85 Object.assign({ countResponse
: 1 }, this.statistics
.statisticsData
.get(command
))
89 case MessageType
.CALL_ERROR_MESSAGE
:
91 this.statistics
.statisticsData
.has(command
) &&
92 this.statistics
.statisticsData
.get(command
)?.countError
94 this.statistics
.statisticsData
.get(command
).countError
++;
96 this.statistics
.statisticsData
.set(
98 Object.assign({ countError
: 1 }, this.statistics
.statisticsData
.get(command
))
103 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
108 public start(): void {
109 this.startLogStatisticsInterval();
110 if (Configuration
.getPerformanceStorage().enabled
) {
112 `${this.logPrefix()} storage enabled: type ${
113 Configuration.getPerformanceStorage().type
114 }, uri: ${Configuration.getPerformanceStorage().uri}`
119 public stop(): void {
120 if (this.displayInterval
) {
121 clearInterval(this.displayInterval
);
123 performance
.clearMarks();
124 this.performanceObserver
?.disconnect();
127 public restart(): void {
132 private initializePerformanceObserver(): void {
133 this.performanceObserver
= new PerformanceObserver((list
) => {
134 const lastPerformanceEntry
= list
.getEntries()[0];
135 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
137 `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
141 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
144 private logStatistics(): void {
145 logger
.info(this.logPrefix() + ' %j', this.statistics
);
148 private startLogStatisticsInterval(): void {
149 if (Configuration
.getLogStatisticsInterval() > 0) {
150 this.displayInterval
= setInterval(() => {
151 this.logStatistics();
152 }, Configuration
.getLogStatisticsInterval() * 1000);
156 Utils
.formatDurationSeconds(Configuration
.getLogStatisticsInterval())
161 ' log interval is set to ' +
162 Configuration
.getLogStatisticsInterval().toString() +
163 '. Not logging statistics'
168 private median(dataSet
: number[]): number {
169 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
172 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
173 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
174 if (sortedDataSet
.length
% 2) {
175 return sortedDataSet
[middleIndex
/ 2];
177 return (sortedDataSet
[middleIndex
- 1] + sortedDataSet
[middleIndex
]) / 2;
180 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
181 private percentile(dataSet
: number[], percentile
: number): number {
182 if (percentile
< 0 && percentile
> 100) {
183 throw new RangeError('Percentile is not between 0 and 100');
185 if (Utils
.isEmptyArray(dataSet
)) {
188 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
);
189 if (percentile
=== 0) {
190 return sortedDataSet
[0];
192 if (percentile
=== 100) {
193 return sortedDataSet
[sortedDataSet
.length
- 1];
195 const percentileIndex
= (percentile
/ 100) * sortedDataSet
.length
- 1;
196 if (Number.isInteger(percentileIndex
)) {
197 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
199 return sortedDataSet
[Math.round(percentileIndex
)];
202 private stdDeviation(dataSet
: number[]): number {
203 let totalDataSet
= 0;
204 for (const data
of dataSet
) {
205 totalDataSet
+= data
;
207 const dataSetMean
= totalDataSet
/ dataSet
.length
;
208 let totalGeometricDeviation
= 0;
209 for (const data
of dataSet
) {
210 const deviation
= data
- dataSetMean
;
211 totalGeometricDeviation
+= deviation
* deviation
;
213 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
216 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
217 let entryName
= entry
.name
;
219 const MAP_NAME
: Record
<string, string> = {};
220 if (MAP_NAME
[entryName
]) {
221 entryName
= MAP_NAME
[entryName
];
223 // Initialize command statistics
224 if (!this.statistics
.statisticsData
.has(entryName
)) {
225 this.statistics
.statisticsData
.set(entryName
, {});
227 // Update current statistics
228 this.statistics
.updatedAt
= new Date();
229 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
=
230 this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
231 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
233 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
234 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
=
235 this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
236 ? this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
238 : this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
240 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
=
241 this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
242 ? this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
244 : this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
246 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
=
247 this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
248 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
250 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
=
251 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/
252 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
253 Array.isArray(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
)
254 ? this.statistics
.statisticsData
256 .timeMeasurementSeries
.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
257 : (this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
=
258 new CircularArray
<TimeSeries
>(DEFAULT_CIRCULAR_ARRAY_SIZE
, {
259 timestamp
: entry
.startTime
,
260 value
: entry
.duration
,
262 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(
263 this.extractTimeSeriesValues(
264 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
267 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
=
269 this.extractTimeSeriesValues(
270 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
274 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(
275 this.extractTimeSeriesValues(
276 this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
279 if (Configuration
.getPerformanceStorage().enabled
) {
280 parentPort
.postMessage({
281 id
: ChargingStationWorkerMessageEvents
.PERFORMANCE_STATISTICS
,
282 data
: this.statistics
,
287 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
288 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
291 private logPrefix(): string {
292 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);