X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fperformance%2FPerformanceStatistics.ts;h=6363be1c6b2196842b49dc1140ca9887d5b4b385;hb=c17a8d29b2177a087f7fea1db4ada2b3795a7a31;hp=cd0ff3dff6592e1709cc1655f253efe56fec1eb0;hpb=c8faabc815e314d10188b9c285c61e1e4c367f8c;p=e-mobility-charging-stations-simulator.git diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index cd0ff3df..6363be1c 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -1,74 +1,123 @@ -// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2024. 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 { performance, type PerformanceEntry, PerformanceObserver } from 'node:perf_hooks' +import type { URL } from 'node:url' +import { parentPort } from 'node:worker_threads' +import { secondsToMilliseconds } from 'date-fns' +import { mean, median } from 'rambda' + +import { BaseError } from '../exception/index.js' import { + ConfigurationSection, type IncomingRequestCommand, + type LogConfiguration, + MapStringifyFormat, MessageType, type RequestCommand, type Statistics, - type TimeSeries, -} from '../types'; + type StatisticsData, + type StorageConfiguration, + type TimestampedData +} from '../types/index.js' import { + buildPerformanceStatisticsMessage, CircularArray, Configuration, Constants, - Utils, - buildPerformanceStatisticsMessage, + extractTimeSeriesValues, + formatDurationSeconds, + generateUUID, + JSONStringify, logger, -} from '../utils'; + 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 + 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); + 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); - performance.clearMeasures(name); + public static beginMeasure (id: string): string { + const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}` + performance.mark(markId) + return markId } - public addRequestStatistic( + 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 { @@ -76,188 +125,201 @@ export 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, { ...this.statistics.statisticsData.get(command), - countRequest: 1, - }); + 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, { ...this.statistics.statisticsData.get(command), - countResponse: 1, - }); + 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, { ...this.statistics.statisticsData.get(command), - countError: 1, - }); + 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 { - this.stopLogStatisticsInterval(); - performance.clearMarks(); - performance.clearMeasures(); - 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((performanceObserverList) => { - const lastPerformanceEntry = performanceObserverList.getEntries()[0]; + 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'] }); + // ) + this.addPerformanceEntryToStatistics(lastPerformanceEntry) + }) + this.performanceObserver.observe({ entryTypes: ['measure'] }) } - private logStatistics(): void { - logger.info(`${this.logPrefix()}`, { + private logStatistics (): void { + logger.info(this.logPrefix(), { ...this.statistics, - statisticsData: Utils.JSONStringifyWithMapSupport(this.statistics.statisticsData), - }); + statisticsData: JSON.parse( + JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object) + ) as Map + }) } - private startLogStatisticsInterval(): void { - const logStatisticsInterval = Configuration.getLogStatisticsInterval(); - if (logStatisticsInterval > 0 && !this.displayInterval) { + 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(); - }, logStatisticsInterval * 1000); + this.logStatistics() + }, secondsToMilliseconds(logStatisticsInterval)) logger.info( - `${this.logPrefix()} logged every ${Utils.formatDurationSeconds(logStatisticsInterval)}` - ); - } else if (this.displayInterval) { + `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}` + ) + } else if (this.displayInterval != null) { logger.info( - `${this.logPrefix()} already logged every ${Utils.formatDurationSeconds( - logStatisticsInterval - )}` - ); - } else { + `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}` + ) + } else if (logConfiguration.enabled === true) { logger.info( - `${this.logPrefix()} log interval is set to ${logStatisticsInterval?.toString()}. Not logging statistics` - ); + `${this.logPrefix()} log interval is set to ${logStatisticsInterval}. Not logging statistics` + ) } } - private stopLogStatisticsInterval(): void { - if (this.displayInterval) { - clearInterval(this.displayInterval); - delete this.displayInterval; + private stopLogStatisticsInterval (): void { + if (this.displayInterval != null) { + clearInterval(this.displayInterval) + delete this.displayInterval } } - private addPerformanceEntryToStatistics(entry: PerformanceEntry): void { - const entryName = entry.name; + 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; - this.statistics.statisticsData.get(entryName)?.timeMeasurementSeries instanceof CircularArray + // 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(Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY, { + .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 = Utils.median( - this.extractTimeSeriesValues( - this.statistics.statisticsData.get(entryName).timeMeasurementSeries - ) - ); - this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement = - Utils.percentile( - this.extractTimeSeriesValues( - this.statistics.statisticsData.get(entryName).timeMeasurementSeries - ), - 95 - ); - this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = Utils.stdDeviation( - this.extractTimeSeriesValues( - this.statistics.statisticsData.get(entryName).timeMeasurementSeries - ) - ); - if (Configuration.getPerformanceStorage().enabled) { - parentPort?.postMessage(buildPerformanceStatisticsMessage(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`) + } }