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 Utils from
'./Utils';
11 import logger from
'./Logger';
13 export default class PerformanceStatistics
{
14 private objId
: string;
15 private performanceObserver
: PerformanceObserver
;
16 private statistics
: Statistics
;
17 private displayInterval
: NodeJS
.Timeout
;
19 public constructor(objId
: string) {
21 this.initializePerformanceObserver();
22 this.statistics
= { id
: this.objId
?? 'Object id not specified', statisticsData
: {} };
25 public static beginMeasure(id
: string): string {
26 const beginId
= 'begin' + id
.charAt(0).toUpperCase() + id
.slice(1);
27 performance
.mark(beginId
);
31 public static endMeasure(name
: string, beginId
: string): void {
32 performance
.measure(name
, beginId
);
33 performance
.clearMarks(beginId
);
36 public addRequestStatistic(command
: RequestCommand
| IncomingRequestCommand
, messageType
: MessageType
): void {
37 switch (messageType
) {
38 case MessageType
.CALL_MESSAGE
:
39 if (this.statistics
.statisticsData
[command
] && this.statistics
.statisticsData
[command
].countRequest
) {
40 this.statistics
.statisticsData
[command
].countRequest
++;
42 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
43 this.statistics
.statisticsData
[command
].countRequest
= 1;
46 case MessageType
.CALL_RESULT_MESSAGE
:
47 if (this.statistics
.statisticsData
[command
]) {
48 if (this.statistics
.statisticsData
[command
].countResponse
) {
49 this.statistics
.statisticsData
[command
].countResponse
++;
51 this.statistics
.statisticsData
[command
].countResponse
= 1;
54 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
55 this.statistics
.statisticsData
[command
].countResponse
= 1;
58 case MessageType
.CALL_ERROR_MESSAGE
:
59 if (this.statistics
.statisticsData
[command
]) {
60 if (this.statistics
.statisticsData
[command
].countError
) {
61 this.statistics
.statisticsData
[command
].countError
++;
63 this.statistics
.statisticsData
[command
].countError
= 1;
66 this.statistics
.statisticsData
[command
] = {} as StatisticsData
;
67 this.statistics
.statisticsData
[command
].countError
= 1;
71 logger
.error(`${this.logPrefix()} wrong message type ${messageType}`);
76 public start(): void {
77 this.startDisplayInterval();
81 if (this.displayInterval
) {
82 clearInterval(this.displayInterval
);
84 performance
.clearMarks();
85 this.performanceObserver
?.disconnect();
88 public restart(): void {
93 private initializePerformanceObserver(): void {
94 this.performanceObserver
= new PerformanceObserver((list
) => {
95 this.addPerformanceEntryToStatistics(list
.getEntries()[0]);
96 logger
.debug(`${this.logPrefix()} '${list.getEntries()[0].name}' performance entry: %j`, list
.getEntries()[0]);
98 this.performanceObserver
.observe({ entryTypes
: ['measure'] });
101 private logStatistics(): void {
102 logger
.info(this.logPrefix() + ' %j', this.statistics
);
105 private startDisplayInterval(): void {
106 if (Configuration
.getStatisticsDisplayInterval() > 0) {
107 this.displayInterval
= setInterval(() => {
108 this.logStatistics();
109 }, Configuration
.getStatisticsDisplayInterval() * 1000);
110 logger
.info(this.logPrefix() + ' displayed every ' + Utils
.secondsToHHMMSS(Configuration
.getStatisticsDisplayInterval()));
112 logger
.info(this.logPrefix() + ' display interval is set to ' + Configuration
.getStatisticsDisplayInterval().toString() + '. Not displaying statistics');
116 private median(dataSet
: number[]): number {
117 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
120 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
121 const middleIndex
= Math.floor(sortedDataSet
.length
/ 2);
122 if (sortedDataSet
.length
% 2) {
123 return sortedDataSet
[middleIndex
/ 2];
125 return (sortedDataSet
[(middleIndex
- 1)] + sortedDataSet
[middleIndex
]) / 2;
128 // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
129 private percentile(dataSet
: number[], percentile
: number): number {
130 if (percentile
< 0 && percentile
> 100) {
131 throw new RangeError('Percentile is not between 0 and 100');
133 if (Utils
.isEmptyArray(dataSet
)) {
136 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => (a
- b
));
137 if (percentile
=== 0) {
138 return sortedDataSet
[0];
140 if (percentile
=== 100) {
141 return sortedDataSet
[sortedDataSet
.length
- 1];
143 const percentileIndex
= ((percentile
/ 100) * sortedDataSet
.length
) - 1;
144 if (Number.isInteger(percentileIndex
)) {
145 return (sortedDataSet
[percentileIndex
] + sortedDataSet
[percentileIndex
+ 1]) / 2;
147 return sortedDataSet
[Math.round(percentileIndex
)];
150 private stdDeviation(dataSet
: number[]): number {
151 let totalDataSet
= 0;
152 for (const data
of dataSet
) {
153 totalDataSet
+= data
;
155 const dataSetMean
= totalDataSet
/ dataSet
.length
;
156 let totalGeometricDeviation
= 0;
157 for (const data
of dataSet
) {
158 const deviation
= data
- dataSetMean
;
159 totalGeometricDeviation
+= deviation
* deviation
;
161 return Math.sqrt(totalGeometricDeviation
/ dataSet
.length
);
164 private addPerformanceEntryToStatistics(entry
: PerformanceEntry
): void {
165 let entryName
= entry
.name
;
167 const MAP_NAME
: Record
<string, string> = {};
168 if (MAP_NAME
[entryName
]) {
169 entryName
= MAP_NAME
[entryName
];
171 // Initialize command statistics
172 if (!this.statistics
.statisticsData
[entryName
]) {
173 this.statistics
.statisticsData
[entryName
] = {} as StatisticsData
;
175 // Update current statistics
176 this.statistics
.statisticsData
[entryName
].countTimeMeasurement
= this.statistics
.statisticsData
[entryName
].countTimeMeasurement
? this.statistics
.statisticsData
[entryName
].countTimeMeasurement
+ 1 : 1;
177 this.statistics
.statisticsData
[entryName
].currentTimeMeasurement
= entry
.duration
;
178 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
;
179 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
;
180 this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
= this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
? this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
+ entry
.duration
: entry
.duration
;
181 this.statistics
.statisticsData
[entryName
].avgTimeMeasurement
= this.statistics
.statisticsData
[entryName
].totalTimeMeasurement
/ this.statistics
.statisticsData
[entryName
].countTimeMeasurement
;
182 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
);
183 this.statistics
.statisticsData
[entryName
].medTimeMeasurement
= this.median(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
);
184 this.statistics
.statisticsData
[entryName
].ninetyFiveThPercentileTimeMeasurement
= this.percentile(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
, 95);
185 this.statistics
.statisticsData
[entryName
].stdDevTimeMeasurement
= this.stdDeviation(this.statistics
.statisticsData
[entryName
].timeMeasurementSeries
);
188 private logPrefix(): string {
189 return Utils
.logPrefix(` ${this.objId} | Performance statistics`);