X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fperformance%2FPerformanceStatistics.ts;h=265cb6c963918b1a3970a95e5822742f3202c0d4;hb=8a4f882a2486682b72195ad978ec1d81604b452b;hp=fdcabb33f6f89adcaf3adde29029b22493cf0771;hpb=9534e74eac653e8cdf02b17a33b273d421251a1c;p=e-mobility-charging-stations-simulator.git diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index fdcabb33..265cb6c9 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -1,62 +1,123 @@ -// 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 { performance, type PerformanceEntry, PerformanceObserver } 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' +import { is, mean, median } from 'rambda' -export default class PerformanceStatistics { +import { BaseError } from '../exception/index.js' +import { + ConfigurationSection, + type IncomingRequestCommand, + type LogConfiguration, + MapStringifyFormat, + MessageType, + type RequestCommand, + type Statistics, + type StatisticsData, + type StorageConfiguration, + type TimestampedData +} from '../types/index.js' +import { + buildPerformanceStatisticsMessage, + CircularArray, + Configuration, + Constants, + extractTimeSeriesValues, + formatDurationSeconds, + generateUUID, + JSONStringify, + logger, + logPrefix, + max, + min, + nthPercentile, + stdDeviation +} from '../utils/index.js' + +export class PerformanceStatistics { private static readonly instances: Map = new Map< - string, - PerformanceStatistics - >(); + string, + PerformanceStatistics + >() - private readonly objId: string; - private readonly objName: string; - private performanceObserver: PerformanceObserver; - private readonly statistics: Statistics; - private displayInterval: NodeJS.Timeout; + 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(); + 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', - name: this.objName ?? 'Object name not specified', + id: this.objId, + name: this.objName, uri: uri.toString(), createdAt: new Date(), - statisticsData: new Map>(), - }; + statisticsData: new Map() + } } - public static getInstance(objId: string, objName: string, uri: URL): PerformanceStatistics { + 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)); + PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri)) + } + return PerformanceStatistics.instances.get(objId) + } + + 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.get(objId); + return PerformanceStatistics.instances.delete(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 beginMeasure (id: string): string { + 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); + public static endMeasure (name: string, markId: string): void { + try { + performance.measure(name, markId) + } catch (error) { + if (is(Error, error) && error.message.includes('performance mark has not been set')) { + /* Ignore */ + } else { + throw error + } + } + performance.clearMarks(markId) + performance.clearMeasures(name) } - public addRequestStatistic( + public addRequestStatistic ( command: RequestCommand | IncomingRequestCommand, messageType: MessageType ): void { @@ -64,233 +125,201 @@ export default class PerformanceStatistics { case MessageType.CALL_MESSAGE: if ( this.statistics.statisticsData.has(command) && - this.statistics.statisticsData.get(command)?.countRequest + this.statistics.statisticsData.get(command)?.requestCount != null ) { - this.statistics.statisticsData.get(command).countRequest++; + // 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)?.responseCount != null ) { - this.statistics.statisticsData.get(command).countResponse++; + // 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)?.errorCount != null ) { - this.statistics.statisticsData.get(command).countError++; + // 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: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - logger.error(`${this.logPrefix()} wrong message type ${messageType}`); - break; + logger.error(`${this.logPrefix()} wrong message type ${messageType}`) + break } } - public start(): void { - this.startLogStatisticsInterval(); - if (Configuration.getPerformanceStorage().enabled) { + public start (): void { + this.startLogStatisticsInterval() + const performanceStorageConfiguration = + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ) + if (performanceStorageConfiguration.enabled === true) { logger.info( - `${this.logPrefix()} storage enabled: type ${ - Configuration.getPerformanceStorage().type - }, uri: ${Configuration.getPerformanceStorage().uri}` - ); + `${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( + JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object) + ) 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); + this.logStatistics() + }, secondsToMilliseconds(logStatisticsInterval)) logger.info( - this.logPrefix() + - ' logged every ' + - Utils.formatDurationSeconds(Configuration.getLogStatisticsInterval()) - ); - } else { + `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}` + ) + } else if (this.displayInterval != null) { 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]; - } - 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; + `${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` + ) } - 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 != null) { + 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 = {}; - 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) + // 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(entryName) - .timeMeasurementSeries.push({ timestamp: entry.startTime, value: entry.duration }) - : (this.statistics.statisticsData.get(entryName).timeMeasurementSeries = - new CircularArray(DEFAULT_CIRCULAR_ARRAY_SIZE, { + .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, - })); - 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, - }); + 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 = mean(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.objName} | Performance statistics`); + private readonly logPrefix = (): string => { + return logPrefix(` ${this.objName} | Performance statistics`) } }