-// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
-
-import { CircularArray, DEFAULT_CIRCULAR_ARRAY_SIZE } from '../utils/CircularArray';
-import { IncomingRequestCommand, RequestCommand } from '../types/ocpp/Requests';
-import { PerformanceEntry, PerformanceObserver, performance } from 'perf_hooks';
-import Statistics, { StatisticsData, TimeSeries } from '../types/Statistics';
-
-import { ChargingStationWorkerMessageEvents } from '../types/ChargingStationWorker';
-import Configuration from '../utils/Configuration';
-import { MessageType } from '../types/ocpp/MessageType';
-import { URL } from 'url';
-import Utils from '../utils/Utils';
-import logger from '../utils/Logger';
-import { parentPort } from 'worker_threads';
-
-export default class PerformanceStatistics {
+// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
+
+import { type PerformanceEntry, PerformanceObserver, performance } from 'node:perf_hooks';
+import type { URL } from 'node:url';
+import { parentPort } from 'node:worker_threads';
+
+import { secondsToMilliseconds } from 'date-fns';
+
+import {
+ ConfigurationSection,
+ type IncomingRequestCommand,
+ type LogConfiguration,
+ MessageType,
+ type RequestCommand,
+ type Statistics,
+ type StorageConfiguration,
+ type TimestampedData,
+} from '../types';
+import {
+ CircularArray,
+ Configuration,
+ Constants,
+ JSONStringifyWithMapSupport,
+ buildPerformanceStatisticsMessage,
+ extractTimeSeriesValues,
+ formatDurationSeconds,
+ generateUUID,
+ logPrefix,
+ logger,
+ median,
+ nthPercentile,
+ stdDeviation,
+} from '../utils';
+
+export class PerformanceStatistics {
+ private static readonly instances: Map<string, PerformanceStatistics> = new Map<
+ string,
+ PerformanceStatistics
+ >();
+
private readonly objId: string;
- private performanceObserver: PerformanceObserver;
+ private readonly objName: string;
+ private performanceObserver!: PerformanceObserver;
private readonly statistics: Statistics;
- private displayInterval: NodeJS.Timeout;
+ private displayInterval?: NodeJS.Timeout;
- public constructor(objId: string, uri: URL) {
+ private constructor(objId: string, objName: string, uri: URL) {
this.objId = objId;
+ this.objName = objName;
this.initializePerformanceObserver();
- this.statistics = { id: this.objId ?? 'Object id not specified', uri: uri.toString(), createdAt: new Date(), statisticsData: new Map<string, Partial<StatisticsData>>() };
+ 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(),
+ };
+ }
+
+ public static getInstance(
+ objId: string,
+ objName: string,
+ uri: URL,
+ ): PerformanceStatistics | undefined {
+ if (!PerformanceStatistics.instances.has(objId)) {
+ PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri));
+ }
+ return PerformanceStatistics.instances.get(objId);
}
public static beginMeasure(id: string): string {
- const markId = `${id.charAt(0).toUpperCase() + id.slice(1)}~${Utils.generateUUID()}`;
+ const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`;
performance.mark(markId);
return markId;
}
public static endMeasure(name: string, markId: string): void {
performance.measure(name, markId);
performance.clearMarks(markId);
+ performance.clearMeasures(name);
}
- public addRequestStatistic(command: RequestCommand | IncomingRequestCommand, messageType: MessageType): void {
+ public addRequestStatistic(
+ command: RequestCommand | IncomingRequestCommand,
+ messageType: MessageType,
+ ): void {
switch (messageType) {
case MessageType.CALL_MESSAGE:
- if (this.statistics.statisticsData.has(command) && this.statistics.statisticsData.get(command)?.countRequest) {
- this.statistics.statisticsData.get(command).countRequest++;
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.requestCount
+ ) {
+ ++this.statistics.statisticsData.get(command)!.requestCount!;
} else {
- this.statistics.statisticsData.set(command, Object.assign({ countRequest: 1 }, this.statistics.statisticsData.get(command)));
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ requestCount: 1,
+ });
}
break;
case MessageType.CALL_RESULT_MESSAGE:
- if (this.statistics.statisticsData.has(command) && this.statistics.statisticsData.get(command)?.countResponse) {
- this.statistics.statisticsData.get(command).countResponse++;
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.responseCount
+ ) {
+ ++this.statistics.statisticsData.get(command)!.responseCount!;
} else {
- this.statistics.statisticsData.set(command, Object.assign({ countResponse: 1 }, this.statistics.statisticsData.get(command)));
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ responseCount: 1,
+ });
}
break;
case MessageType.CALL_ERROR_MESSAGE:
- if (this.statistics.statisticsData.has(command) && this.statistics.statisticsData.get(command)?.countError) {
- this.statistics.statisticsData.get(command).countError++;
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.errorCount
+ ) {
+ ++this.statistics.statisticsData.get(command)!.errorCount!;
} else {
- this.statistics.statisticsData.set(command, Object.assign({ countError: 1 }, this.statistics.statisticsData.get(command)));
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ errorCount: 1,
+ });
}
break;
default:
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
logger.error(`${this.logPrefix()} wrong message type ${messageType}`);
break;
}
public start(): void {
this.startLogStatisticsInterval();
- if (Configuration.getPerformanceStorage().enabled) {
- logger.info(`${this.logPrefix()} storage enabled: type ${Configuration.getPerformanceStorage().type}, uri: ${Configuration.getPerformanceStorage().uri}`);
+ const performanceStorageConfiguration =
+ Configuration.getConfigurationSection<StorageConfiguration>(
+ ConfigurationSection.performanceStorage,
+ );
+ if (performanceStorageConfiguration.enabled) {
+ logger.info(
+ `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${
+ performanceStorageConfiguration.uri
+ }`,
+ );
}
}
public stop(): void {
- if (this.displayInterval) {
- clearInterval(this.displayInterval);
- }
+ this.stopLogStatisticsInterval();
performance.clearMarks();
+ performance.clearMeasures();
this.performanceObserver?.disconnect();
}
}
private initializePerformanceObserver(): void {
- this.performanceObserver = new PerformanceObserver((list) => {
- const lastPerformanceEntry = list.getEntries()[0];
+ this.performanceObserver = new PerformanceObserver((performanceObserverList) => {
+ const lastPerformanceEntry = performanceObserverList.getEntries()[0];
+ // logger.debug(
+ // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
+ // lastPerformanceEntry,
+ // );
this.addPerformanceEntryToStatistics(lastPerformanceEntry);
- logger.debug(`${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`, lastPerformanceEntry);
});
this.performanceObserver.observe({ entryTypes: ['measure'] });
}
private logStatistics(): void {
- logger.info(this.logPrefix() + ' %j', this.statistics);
+ logger.info(`${this.logPrefix()}`, {
+ ...this.statistics,
+ statisticsData: JSONStringifyWithMapSupport(this.statistics.statisticsData),
+ });
}
private startLogStatisticsInterval(): void {
- if (Configuration.getLogStatisticsInterval() > 0) {
+ const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+ ConfigurationSection.log,
+ );
+ const logStatisticsInterval = logConfiguration.enabled
+ ? logConfiguration.statisticsInterval!
+ : 0;
+ if (logStatisticsInterval > 0 && !this.displayInterval) {
this.displayInterval = setInterval(() => {
this.logStatistics();
- }, Configuration.getLogStatisticsInterval() * 1000);
- logger.info(this.logPrefix() + ' logged every ' + Utils.formatDurationSeconds(Configuration.getLogStatisticsInterval()));
- } else {
- logger.info(this.logPrefix() + ' log interval is set to ' + Configuration.getLogStatisticsInterval().toString() + '. Not logging statistics');
- }
- }
-
- private median(dataSet: number[]): number {
- if (Array.isArray(dataSet) && dataSet.length === 1) {
- return dataSet[0];
+ }, secondsToMilliseconds(logStatisticsInterval));
+ logger.info(
+ `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`,
+ );
+ } else if (this.displayInterval) {
+ logger.info(
+ `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`,
+ );
+ } else if (logConfiguration.enabled) {
+ logger.info(
+ `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics`,
+ );
}
- const sortedDataSet = dataSet.slice().sort((a, b) => (a - b));
- const middleIndex = Math.floor(sortedDataSet.length / 2);
- if (sortedDataSet.length % 2) {
- return sortedDataSet[middleIndex / 2];
- }
- return (sortedDataSet[(middleIndex - 1)] + sortedDataSet[middleIndex]) / 2;
- }
-
- // TODO: use order statistics tree https://en.wikipedia.org/wiki/Order_statistic_tree
- private percentile(dataSet: number[], percentile: number): number {
- if (percentile < 0 && percentile > 100) {
- throw new RangeError('Percentile is not between 0 and 100');
- }
- if (Utils.isEmptyArray(dataSet)) {
- return 0;
- }
- const sortedDataSet = dataSet.slice().sort((a, b) => (a - b));
- if (percentile === 0) {
- return sortedDataSet[0];
- }
- if (percentile === 100) {
- return sortedDataSet[sortedDataSet.length - 1];
- }
- const percentileIndex = ((percentile / 100) * sortedDataSet.length) - 1;
- if (Number.isInteger(percentileIndex)) {
- return (sortedDataSet[percentileIndex] + sortedDataSet[percentileIndex + 1]) / 2;
- }
- return sortedDataSet[Math.round(percentileIndex)];
}
- private stdDeviation(dataSet: number[]): number {
- let totalDataSet = 0;
- for (const data of dataSet) {
- totalDataSet += data;
- }
- const dataSetMean = totalDataSet / dataSet.length;
- let totalGeometricDeviation = 0;
- for (const data of dataSet) {
- const deviation = data - dataSetMean;
- totalGeometricDeviation += deviation * deviation;
+ private stopLogStatisticsInterval(): void {
+ if (this.displayInterval) {
+ clearInterval(this.displayInterval);
+ delete this.displayInterval;
}
- return Math.sqrt(totalGeometricDeviation / dataSet.length);
}
private addPerformanceEntryToStatistics(entry: PerformanceEntry): void {
- let entryName = entry.name;
- // Rename entry name
- const MAP_NAME: Record<string, string> = {};
- if (MAP_NAME[entryName]) {
- entryName = MAP_NAME[entryName];
- }
+ const entryName = entry.name;
// Initialize command statistics
if (!this.statistics.statisticsData.has(entryName)) {
this.statistics.statisticsData.set(entryName, {});
}
// Update current statistics
this.statistics.updatedAt = new Date();
- this.statistics.statisticsData.get(entryName).countTimeMeasurement = this.statistics.statisticsData.get(entryName)?.countTimeMeasurement
- ? this.statistics.statisticsData.get(entryName).countTimeMeasurement + 1
- : 1;
- this.statistics.statisticsData.get(entryName).currentTimeMeasurement = entry.duration;
- this.statistics.statisticsData.get(entryName).minTimeMeasurement = this.statistics.statisticsData.get(entryName)?.minTimeMeasurement
- ? (this.statistics.statisticsData.get(entryName).minTimeMeasurement > entry.duration ? entry.duration : this.statistics.statisticsData.get(entryName).minTimeMeasurement)
- : entry.duration;
- this.statistics.statisticsData.get(entryName).maxTimeMeasurement = this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement
- ? (this.statistics.statisticsData.get(entryName).maxTimeMeasurement < entry.duration ? entry.duration : this.statistics.statisticsData.get(entryName).maxTimeMeasurement)
- : entry.duration;
- this.statistics.statisticsData.get(entryName).totalTimeMeasurement = this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement
- ? this.statistics.statisticsData.get(entryName).totalTimeMeasurement + entry.duration
- : entry.duration;
- this.statistics.statisticsData.get(entryName).avgTimeMeasurement = this.statistics.statisticsData.get(entryName).totalTimeMeasurement / this.statistics.statisticsData.get(entryName).countTimeMeasurement;
- Array.isArray(this.statistics.statisticsData.get(entryName).timeMeasurementSeries)
- ? this.statistics.statisticsData.get(entryName).timeMeasurementSeries.push({ timestamp: entry.startTime, value: entry.duration })
- : this.statistics.statisticsData.get(entryName).timeMeasurementSeries = new CircularArray<TimeSeries>(DEFAULT_CIRCULAR_ARRAY_SIZE, { timestamp: entry.startTime, value: entry.duration });
- this.statistics.statisticsData.get(entryName).medTimeMeasurement = this.median(this.extractTimeSeriesValues(this.statistics.statisticsData.get(entryName).timeMeasurementSeries));
- this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement = this.percentile(this.extractTimeSeriesValues(this.statistics.statisticsData.get(entryName).timeMeasurementSeries), 95);
- this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = this.stdDeviation(this.extractTimeSeriesValues(this.statistics.statisticsData.get(entryName).timeMeasurementSeries));
- if (Configuration.getPerformanceStorage().enabled) {
- parentPort.postMessage({ id: ChargingStationWorkerMessageEvents.PERFORMANCE_STATISTICS, data: this.statistics });
+ this.statistics.statisticsData.get(entryName)!.timeMeasurementCount =
+ (this.statistics.statisticsData.get(entryName)?.timeMeasurementCount ?? 0) + 1;
+ this.statistics.statisticsData.get(entryName)!.currentTimeMeasurement = entry.duration;
+ this.statistics.statisticsData.get(entryName)!.minTimeMeasurement = Math.min(
+ entry.duration,
+ this.statistics.statisticsData.get(entryName)?.minTimeMeasurement ?? Infinity,
+ );
+ this.statistics.statisticsData.get(entryName)!.maxTimeMeasurement = Math.max(
+ entry.duration,
+ this.statistics.statisticsData.get(entryName)?.maxTimeMeasurement ?? -Infinity,
+ );
+ this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement =
+ (this.statistics.statisticsData.get(entryName)?.totalTimeMeasurement ?? 0) + entry.duration;
+ this.statistics.statisticsData.get(entryName)!.avgTimeMeasurement =
+ this.statistics.statisticsData.get(entryName)!.totalTimeMeasurement! /
+ this.statistics.statisticsData.get(entryName)!.timeMeasurementCount!;
+ this.statistics.statisticsData.get(entryName)?.measurementTimeSeries instanceof CircularArray
+ ? this.statistics.statisticsData
+ .get(entryName)
+ ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration })
+ : (this.statistics.statisticsData.get(entryName)!.measurementTimeSeries =
+ new CircularArray<TimestampedData>(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, {
+ timestamp: entry.startTime,
+ value: entry.duration,
+ }));
+ this.statistics.statisticsData.get(entryName)!.medTimeMeasurement = median(
+ extractTimeSeriesValues(
+ this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
+ ),
+ );
+ this.statistics.statisticsData.get(entryName)!.ninetyFiveThPercentileTimeMeasurement =
+ nthPercentile(
+ extractTimeSeriesValues(
+ this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
+ ),
+ 95,
+ );
+ this.statistics.statisticsData.get(entryName)!.stdDevTimeMeasurement = stdDeviation(
+ extractTimeSeriesValues(
+ this.statistics.statisticsData.get(entryName)!.measurementTimeSeries as TimestampedData[],
+ ),
+ );
+ if (
+ Configuration.getConfigurationSection<StorageConfiguration>(
+ ConfigurationSection.performanceStorage,
+ ).enabled
+ ) {
+ parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics));
}
}
- private extractTimeSeriesValues(timeSeries: CircularArray<TimeSeries>): number[] {
- return timeSeries.map((timeSeriesItem) => timeSeriesItem.value);
- }
-
- private logPrefix(): string {
- return Utils.logPrefix(` ${this.objId} | Performance statistics`);
- }
+ private logPrefix = (): string => {
+ return logPrefix(` ${this.objName} | Performance statistics`);
+ };
}