X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=sidebyside;f=src%2Fperformance%2FPerformanceStatistics.ts;h=e90595225278d3a926059a2ca3ab7821b800d644;hb=6027002ff1398a1cb3bb55d4994e4615efded2c0;hp=f9be1922f3ef87e98c1af83a39565933b2a5d698;hpb=0c142310bb891fd23f2c40234af1fddd88ba71d5;p=e-mobility-charging-stations-simulator.git diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index f9be1922..e9059522 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -1,197 +1,326 @@ -// Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2024. 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 { type PerformanceEntry, PerformanceObserver, performance } from 'node:perf_hooks' +import type { URL } from 'node:url' +import { parentPort } from 'node:worker_threads' -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'; +import { secondsToMilliseconds } from 'date-fns' -export default class PerformanceStatistics { - private readonly objId: string; - private performanceObserver: PerformanceObserver; - private readonly statistics: Statistics; - private displayInterval: NodeJS.Timeout; +import { BaseError } from '../exception/index.js' +import { + ConfigurationSection, + type IncomingRequestCommand, + type LogConfiguration, + MessageType, + type RequestCommand, + type Statistics, + type StatisticsData, + type StorageConfiguration, + type TimestampedData +} from '../types/index.js' +import { + CircularArray, + Configuration, + Constants, + JSONStringifyWithMapSupport, + average, + buildPerformanceStatisticsMessage, + extractTimeSeriesValues, + formatDurationSeconds, + generateUUID, + logPrefix, + logger, + max, + median, + min, + nthPercentile, + stdDeviation +} from '../utils/index.js' - public constructor(objId: string, URI: URL) { - this.objId = objId; - this.initializePerformanceObserver(); - this.statistics = { id: this.objId ?? 'Object id not specified', URI: URI.toString(), createdAt: new Date(), statisticsData: new Map>() }; +export class PerformanceStatistics { + private static readonly instances: Map = new Map< + string, + PerformanceStatistics + >() + + private readonly objId: string | undefined + private readonly objName: string | undefined + private performanceObserver!: PerformanceObserver + private readonly statistics: Statistics + private displayInterval?: NodeJS.Timeout + + private constructor (objId: string, objName: string, uri: URL) { + this.objId = objId + this.objName = objName + this.initializePerformanceObserver() + this.statistics = { + id: this.objId, + name: this.objName, + uri: uri.toString(), + createdAt: new Date(), + statisticsData: new Map() + } + } + + public static getInstance ( + objId: string | undefined, + objName: string | undefined, + uri: URL | undefined + ): PerformanceStatistics | undefined { + if (objId == null) { + const errMsg = 'Cannot get performance statistics instance without specifying object id' + logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`) + throw new BaseError(errMsg) + } + if (objName == null) { + const errMsg = 'Cannot get performance statistics instance without specifying object name' + logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`) + throw new BaseError(errMsg) + } + if (uri == null) { + const errMsg = 'Cannot get performance statistics instance without specifying object uri' + logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`) + throw new BaseError(errMsg) + } + 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()}`; - performance.mark(markId); - return markId; + public static deleteInstance (objId: string | undefined): boolean { + if (objId == null) { + const errMsg = 'Cannot delete performance statistics instance without specifying object id' + logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`) + throw new BaseError(errMsg) + } + return PerformanceStatistics.instances.delete(objId) } - public static endMeasure(name: string, markId: string): void { - performance.measure(name, markId); - performance.clearMarks(markId); + public static beginMeasure (id: string): string { + const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}` + performance.mark(markId) + return markId } - public addRequestStatistic(command: RequestCommand | IncomingRequestCommand, messageType: MessageType): void { + public static endMeasure (name: string, markId: string): void { + try { + performance.measure(name, markId) + } catch (error) { + if (error instanceof Error && error.message.includes('performance mark has not been set')) { + /* Ignore */ + } else { + throw error + } + } + performance.clearMarks(markId) + performance.clearMeasures(name) + } + + 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 != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++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; + 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 != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++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; + 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 != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++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; + break default: - logger.error(`${this.logPrefix()} wrong message type ${messageType}`); - break; + // 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}`); + public start (): void { + this.startLogStatisticsInterval() + const performanceStorageConfiguration = + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ) + if (performanceStorageConfiguration.enabled === true) { + logger.info( + `${this.logPrefix()} storage enabled: type ${performanceStorageConfiguration.type}, uri: ${ + performanceStorageConfiguration.uri + }` + ) } } - public stop(): void { - if (this.displayInterval) { - clearInterval(this.displayInterval); - } - performance.clearMarks(); - this.performanceObserver?.disconnect(); + public stop (): void { + this.stopLogStatisticsInterval() + performance.clearMarks() + performance.clearMeasures() + this.performanceObserver.disconnect() } - public restart(): void { - this.stop(); - this.start(); + public restart (): void { + this.stop() + this.start() } - private initializePerformanceObserver(): void { - this.performanceObserver = new PerformanceObserver((list) => { - const lastPerformanceEntry = list.getEntries()[0]; - this.addPerformanceEntryToStatistics(lastPerformanceEntry); - logger.debug(`${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`, lastPerformanceEntry); - }); - this.performanceObserver.observe({ entryTypes: ['measure'] }); + private initializePerformanceObserver (): void { + this.performanceObserver = new PerformanceObserver(performanceObserverList => { + const lastPerformanceEntry = performanceObserverList.getEntries()[0] + // logger.debug( + // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`, + // lastPerformanceEntry + // ) + this.addPerformanceEntryToStatistics(lastPerformanceEntry) + }) + this.performanceObserver.observe({ entryTypes: ['measure'] }) } - private logStatistics(): void { - logger.info(this.logPrefix() + ' %j', this.statistics); + private logStatistics (): void { + logger.info(this.logPrefix(), { + ...this.statistics, + statisticsData: JSON.parse( + JSONStringifyWithMapSupport(this.statistics.statisticsData) + ) as Map + }) } - private startLogStatisticsInterval(): void { - if (Configuration.getLogStatisticsInterval() > 0) { + private startLogStatisticsInterval (): void { + const logConfiguration = Configuration.getConfigurationSection( + ConfigurationSection.log + ) + const logStatisticsInterval = + logConfiguration.enabled === true + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + logConfiguration.statisticsInterval! + : 0 + if (logStatisticsInterval > 0 && this.displayInterval == null) { 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'); + this.logStatistics() + }, secondsToMilliseconds(logStatisticsInterval)) + logger.info( + `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}` + ) + } else if (this.displayInterval != null) { + logger.info( + `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}` + ) + } else if (logConfiguration.enabled === true) { + logger.info( + `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics` + ) } } - private median(dataSet: number[]): number { - if (Array.isArray(dataSet) && dataSet.length === 1) { - return dataSet[0]; + private stopLogStatisticsInterval (): void { + if (this.displayInterval != null) { + clearInterval(this.displayInterval) + delete this.displayInterval } - 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; - } - return Math.sqrt(totalGeometricDeviation / dataSet.length); - } - - private addPerformanceEntryToStatistics(entry: PerformanceEntry): void { - let entryName = entry.name; - // Rename entry name - const MAP_NAME: Record = {}; - if (MAP_NAME[entryName]) { - entryName = MAP_NAME[entryName]; - } + private addPerformanceEntryToStatistics (entry: PerformanceEntry): void { // Initialize command statistics - if (!this.statistics.statisticsData.has(entryName)) { - this.statistics.statisticsData.set(entryName, {}); + if (!this.statistics.statisticsData.has(entry.name)) { + this.statistics.statisticsData.set(entry.name, {}) } // 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(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 }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.timeMeasurementCount = + (this.statistics.statisticsData.get(entry.name)?.timeMeasurementCount ?? 0) + 1 + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.currentTimeMeasurement = entry.duration + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.minTimeMeasurement = min( + entry.duration, + this.statistics.statisticsData.get(entry.name)?.minTimeMeasurement ?? Infinity + ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.maxTimeMeasurement = max( + entry.duration, + this.statistics.statisticsData.get(entry.name)?.maxTimeMeasurement ?? -Infinity + ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.totalTimeMeasurement = + (this.statistics.statisticsData.get(entry.name)?.totalTimeMeasurement ?? 0) + entry.duration + this.statistics.statisticsData.get(entry.name)?.measurementTimeSeries instanceof CircularArray + ? this.statistics.statisticsData + .get(entry.name) + ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration }) + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (this.statistics.statisticsData.get(entry.name)!.measurementTimeSeries = + new CircularArray(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, { + timestamp: entry.startTime, + value: entry.duration + })) + const timeMeasurementValues = extractTimeSeriesValues( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.measurementTimeSeries! + ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.avgTimeMeasurement = + average(timeMeasurementValues) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.medTimeMeasurement = + median(timeMeasurementValues) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.ninetyFiveThPercentileTimeMeasurement = + nthPercentile(timeMeasurementValues, 95) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.stdDevTimeMeasurement = stdDeviation( + timeMeasurementValues, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.avgTimeMeasurement + ) + this.statistics.updatedAt = new Date() + if ( + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ).enabled === true + ) { + parentPort?.postMessage(buildPerformanceStatisticsMessage(this.statistics)) } } - private extractTimeSeriesValues(timeSeries: CircularArray): number[] { - return timeSeries.map((timeSeriesItem) => timeSeriesItem.value); + private static readonly logPrefix = (): string => { + return logPrefix(' Performance statistics') } - private logPrefix(): string { - return Utils.logPrefix(` ${this.objId} | Performance statistics`); + private readonly logPrefix = (): string => { + return logPrefix(` ${this.objName} | Performance statistics`) } }