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
<string, PerformanceStatistics
>();
18 private readonly objId
: string;
19 private readonly objName
: string;
20 private performanceObserver
: PerformanceObserver
;
21 private readonly statistics
: Statistics
;
22 private displayInterval
: NodeJS
.Timeout
;
24 private constructor(objId
: string, objName
: string, uri
: URL
) {
26 this.objName
= objName
;
27 this.initializePerformanceObserver();
28 this.statistics
= { id
: this.objId
?? 'Object id not specified', name
: this.objName
?? 'Object name not specified', uri
: uri
.toString(), createdAt
: new Date(), statisticsData
: new Map
<string, Partial
<StatisticsData
>>() };
31 public static getInstance(objId
: string, objName
: string, uri
: URL
): PerformanceStatistics
{
32 if (!PerformanceStatistics
.instances
.has(objId
)) {
33 PerformanceStatistics
.instances
.set(objId
, new PerformanceStatistics(objId
, objName
, uri
));
35 return PerformanceStatistics
.instances
.get(objId
);
38 public static beginMeasure(id
: string): string {
39 const markId
= `${id.charAt(0).toUpperCase() + id.slice(1)}~${Utils.generateUUID()}`;
40 performance
.mark(markId
);
44 public static endMeasure(name
: string, markId
: string): void {
45 performance
.measure(name
, markId
);
46 performance
.clearMarks(markId
);
49 public addRequestStatistic(command
: RequestCommand
| IncomingRequestCommand
, messageType
: MessageType
): void {
50 switch (messageType
) {
51 case MessageType
.CALL_MESSAGE
:
52 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countRequest
) {
53 this.statistics
.statisticsData
.get(command
).countRequest
++;
55 this.statistics
.statisticsData
.set(command
, Object.assign({ countRequest
: 1 }, this.statistics
.statisticsData
.get(command
)));
58 case MessageType
.CALL_RESULT_MESSAGE
:
59 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countResponse
) {
60 this.statistics
.statisticsData
.get(command
).countResponse
++;
62 this.statistics
.statisticsData
.set(command
, Object.assign({ countResponse
: 1 }, this.statistics
.statisticsData
.get(command
)));
65 case MessageType
.CALL_ERROR_MESSAGE
:
66 if (this.statistics
.statisticsData
.has(command
) && this.statistics
.statisticsData
.get(command
)?.countError
) {
67 this.statistics
.statisticsData
.get(command
).countError
++;
69 this.statistics
.statisticsData
.set(command
, Object.assign({ countError
: 1 }, this.statistics
.statisticsData
.get(command
)));
73 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
78 public start(): void {
79 this.startLogStatisticsInterval();
80 if (Configuration
.getPerformanceStorage().enabled
) {
81 logger
.info(`${this.logPrefix()} storage enabled: type ${Configuration.getPerformanceStorage().type}, uri: ${Configuration.getPerformanceStorage().uri}`);
86 if (this.displayInterval
) {
87 clearInterval(this.displayInterval
);
89 performance
.clearMarks();
90 this.performanceObserver
?.disconnect();
93 public restart(): void {
98 private initializePerformanceObserver(): void {
99 this.performanceObserver
= new PerformanceObserver((list
) => {
100 const lastPerformanceEntry
= list
.getEntries()[0];
101 this.addPerformanceEntryToStatistics(lastPerformanceEntry
);
102 logger
.debug(`${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`, lastPerformanceEntry
);
104 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
107 private logStatistics(): void {
108 logger
.info(this.logPrefix() + ' %j', this.statistics
);
111 private startLogStatisticsInterval(): void {
112 if (Configuration
.getLogStatisticsInterval() > 0) {
113 this.displayInterval
= setInterval(() => {
114 this.logStatistics();
115 }, Configuration
.getLogStatisticsInterval() * 1000);
116 logger
.info(this.logPrefix() + ' logged every ' + Utils
.formatDurationSeconds(Configuration
.getLogStatisticsInterval()));
118 logger
.info(this.logPrefix() + ' log interval is set to ' + Configuration
.getLogStatisticsInterval().toString() + '. Not logging statistics');
122 private median(dataSet
: number[]): number {
123 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
126 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
127 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
128 if (sortedDataSet
.length
% 2) {
129 return sortedDataSet
[middleIndex
/ 2];
131 return (sortedDataSet
[(middleIndex
- 1)] + sortedDataSet
[middleIndex
]) / 2;
134 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
135 private percentile(dataSet
: number[], percentile
: number): number {
136 if (percentile
< 0 && percentile
> 100) {
137 throw new RangeError('Percentile is not between 0 and 100');
139 if (Utils
.isEmptyArray(dataSet
)) {
142 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
143 if (percentile
=== 0) {
144 return sortedDataSet
[0];
146 if (percentile
=== 100) {
147 return sortedDataSet
[sortedDataSet
.length
- 1];
149 const percentileIndex
= ((percentile
/ 100) * sortedDataSet
.length
) - 1;
150 if (Number.isInteger(percentileIndex
)) {
151 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
153 return sortedDataSet
[Math.round(percentileIndex
)];
156 private stdDeviation(dataSet
: number[]): number {
157 let totalDataSet
= 0;
158 for (const data
of dataSet
) {
159 totalDataSet
+= data
;
161 const dataSetMean
= totalDataSet
/ dataSet
.length
;
162 let totalGeometricDeviation
= 0;
163 for (const data
of dataSet
) {
164 const deviation
= data
- dataSetMean
;
165 totalGeometricDeviation
+= deviation
* deviation
;
167 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
170 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
171 let entryName
= entry
.name
;
173 const MAP_NAME
: Record
<string, string> = {};
174 if (MAP_NAME
[entryName
]) {
175 entryName
= MAP_NAME
[entryName
];
177 // Initialize command statistics
178 if (!this.statistics
.statisticsData
.has(entryName
)) {
179 this.statistics
.statisticsData
.set(entryName
, {});
181 // Update current statistics
182 this.statistics
.updatedAt
= new Date();
183 this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.countTimeMeasurement
184 ? this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
+ 1
186 this.statistics
.statisticsData
.get(entryName
).currentTimeMeasurement
= entry
.duration
;
187 this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.minTimeMeasurement
188 ? (this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
> entry
.duration
? entry
.duration
: this.statistics
.statisticsData
.get(entryName
).minTimeMeasurement
)
190 this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.maxTimeMeasurement
191 ? (this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
< entry
.duration
? entry
.duration
: this.statistics
.statisticsData
.get(entryName
).maxTimeMeasurement
)
193 this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
)?.totalTimeMeasurement
194 ? this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
+ entry
.duration
196 this.statistics
.statisticsData
.get(entryName
).avgTimeMeasurement
= this.statistics
.statisticsData
.get(entryName
).totalTimeMeasurement
/ this.statistics
.statisticsData
.get(entryName
).countTimeMeasurement
;
197 Array.isArray(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
)
198 ? this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
.push({ timestamp
: entry
.startTime
, value
: entry
.duration
})
199 : this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
= new CircularArray
<TimeSeries
>(DEFAULT_CIRCULAR_ARRAY_SIZE
, { timestamp
: entry
.startTime
, value
: entry
.duration
});
200 this.statistics
.statisticsData
.get(entryName
).medTimeMeasurement
= this.median(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
));
201 this.statistics
.statisticsData
.get(entryName
).ninetyFiveThPercentileTimeMeasurement
= this.percentile(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
), 95);
202 this.statistics
.statisticsData
.get(entryName
).stdDevTimeMeasurement
= this.stdDeviation(this.extractTimeSeriesValues(this.statistics
.statisticsData
.get(entryName
).timeMeasurementSeries
));
203 if (Configuration
.getPerformanceStorage().enabled
) {
204 parentPort
.postMessage({ id
: ChargingStationWorkerMessageEvents
.PERFORMANCE_STATISTICS
, data
: this.statistics
});
208 private extractTimeSeriesValues(timeSeries
: CircularArray
<TimeSeries
>): number[] {
209 return timeSeries
.map((timeSeriesItem
) => timeSeriesItem
.value
);
212 private logPrefix(): string {
213 return Utils
.logPrefix(` ${this.objName} | Performance statistics`);