1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
3 import { CircularArray
, DEFAULT_CIRCULAR_ARRAY_SIZE
} from
'./CircularArray';
4 import { IncomingRequestCommand
, RequestCommand
} from
'../types/ocpp/Requests';
5 import { PerformanceEntry
, PerformanceObserver
, performance
} from
'perf_hooks';
6 import Statistics
, { StatisticsData
} from
'../types/Statistics';
8 import Configuration from
'./Configuration';
9 import { MessageType
} from
'../types/ocpp/MessageType';
10 import { Storage
} from
'./performance-storage/Storage';
11 import { StorageFactory
} from
'./performance-storage/StorageFactory';
12 import Utils from
'./Utils';
13 import logger from
'./Logger';
15 export default class PerformanceStatistics
{
16 private static storage
: Storage
;
17 private objId
: string;
18 private performanceObserver
: PerformanceObserver
;
19 private statistics
: Statistics
;
20 private displayInterval
: NodeJS
.Timeout
;
22 public constructor(objId
: string) {
24 this.initializePerformanceObserver();
25 this.statistics
= { id
: this.objId
?? 'Object id not specified', createdAt
: new Date(), statisticsData
: {} };
28 public static beginMeasure(id
: string): string {
29 const beginId
= 'begin' + id
.charAt(0).toUpperCase() + id
.slice(1);
30 performance
.mark(beginId
);
34 public static endMeasure(name
: string, beginId
: string): void {
35 performance
.measure(name
, beginId
);
36 performance
.clearMarks(beginId
);
39 public addRequestStatistic(command
: RequestCommand
| IncomingRequestCommand
, messageType
: MessageType
): void {
40 switch (messageType
) {
41 case MessageType
.CALL_MESSAGE
:
42 if (this.statistics
.statisticsData
[command
] && this.statistics
.statisticsData
[command
].countRequest
) {
43 this.statistics
.statisticsData
[command
].countRequest
++;
45 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
46 this.statistics
.statisticsData
[command
].countRequest
= 1;
49 case MessageType
.CALL_RESULT_MESSAGE
:
50 if (this.statistics
.statisticsData
[command
]) {
51 if (this.statistics
.statisticsData
[command
].countResponse
) {
52 this.statistics
.statisticsData
[command
].countResponse
++;
54 this.statistics
.statisticsData
[command
].countResponse
= 1;
57 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
58 this.statistics
.statisticsData
[command
].countResponse
= 1;
61 case MessageType
.CALL_ERROR_MESSAGE
:
62 if (this.statistics
.statisticsData
[command
]) {
63 if (this.statistics
.statisticsData
[command
].countError
) {
64 this.statistics
.statisticsData
[command
].countError
++;
66 this.statistics
.statisticsData
[command
].countError
= 1;
69 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
70 this.statistics
.statisticsData
[command
].countError
= 1;
74 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
79 public start(): void {
80 this.startLogStatisticsInterval();
81 if (Configuration
.getPerformanceStorage().enabled
) {
82 logger
.info(`${this.logPrefix()} storage enabled: type ${Configuration.getPerformanceStorage().type}, URI: ${Configuration.getPerformanceStorage().URI}`);
87 if (this.displayInterval
) {
88 clearInterval(this.displayInterval
);
90 performance
.clearMarks();
91 this.performanceObserver
?.disconnect();
94 public restart(): void {
99 private initializePerformanceObserver(): void {
100 this.performanceObserver
= new PerformanceObserver((list
) => {
101 this.addPerformanceEntryToStatistics(list
.getEntries()[0]);
102 logger
.debug(`${this.logPrefix()} '${list.getEntries()[0].name}' performance entry: %j`, list
.getEntries()[0]);
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
.secondsToHHMMSS(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
[entryName
]) {
179 this.statistics
.statisticsData
[entryName
] = {} as StatisticsData
;
181 // Update current statistics
182 this.statistics
.lastUpdatedAt
= new Date();
183 this.statistics
.statisticsData
[entryName
].countTimeMeasurement
= this.statistics
.statisticsData
[entryName
].countTimeMeasurement
? this.statistics
.statisticsData
[entryName
].countTimeMeasurement
+ 1 : 1;
184 this.statistics
.statisticsData
[entryName
].currentTimeMeasurement
= entry
.duration
;
185 this.statistics
.statisticsData
[entryName
].minTimeMeasurement
= this.statistics
.statisticsData
[entryName
].minTimeMeasurement
? (this.statistics
.statisticsData
[entryName
].minTimeMeasurement
> entry
.duration
? entry
.duration
: this.statistics
.statisticsData
[entryName
].minTimeMeasurement
) : entry
.duration
;
186 this.statistics
.statisticsData
[entryName
].maxTimeMeasurement
= this.statistics
.statisticsData
[entryName
].maxTimeMeasurement
? (this.statistics
.statisticsData
[entryName
].maxTimeMeasurement
< entry
.duration
? entry
.duration
: this.statistics
.statisticsData
[entryName
].maxTimeMeasurement
) : entry
.duration
;
187 this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
= this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
? this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
+ entry
.duration
: entry
.duration
;
188 this.statistics
.statisticsData
[entryName
].avgTimeMeasurement
= this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
/ this.statistics
.statisticsData
[entryName
].countTimeMeasurement
;
189 Array.isArray(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
) ? this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
.push(entry
.duration
) : this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
= new CircularArray
<number>(DEFAULT_CIRCULAR_ARRAY_SIZE
, entry
.duration
);
190 this.statistics
.statisticsData
[entryName
].medTimeMeasurement
= this.median(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
);
191 this.statistics
.statisticsData
[entryName
].ninetyFiveThPercentileTimeMeasurement
= this.percentile(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
, 95);
192 this.statistics
.statisticsData
[entryName
].stdDevTimeMeasurement
= this.stdDeviation(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
);
193 if (Configuration
.getPerformanceStorage().enabled
) {
194 this.getStorage().storePerformanceStatistics(this.statistics
);
198 private logPrefix(): string {
199 return Utils
.logPrefix(` ${this.objId} | Performance statistics`);
202 private getStorage(): Storage
{
203 if (!PerformanceStatistics
.storage
) {
204 PerformanceStatistics
.storage
= StorageFactory
.getStorage(Configuration
.getPerformanceStorage().type ,Configuration
.getPerformanceStorage().URI
, this.logPrefix());
206 return PerformanceStatistics
.storage
;