X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=inline;f=src%2Fperformance%2FPerformanceStatistics.ts;h=265cb6c963918b1a3970a95e5822742f3202c0d4;hb=a637f99f1d9a63bed1338809a5dbfab26925babe;hp=b4dc60125c378e00031aca42a6a6fee4c9c8fedc;hpb=da55bd34b987cea87c4fd1e508f3a24cee6f5418;p=e-mobility-charging-stations-simulator.git diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index b4dc6012..265cb6c9 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -1,81 +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 { is, mean, median } from 'rambda' + +import { BaseError } from '../exception/index.js' import { + ConfigurationSection, type IncomingRequestCommand, + type LogConfiguration, + MapStringifyFormat, MessageType, type RequestCommand, type Statistics, - type TimestampedData, -} from '../types'; + type StatisticsData, + type StorageConfiguration, + type TimestampedData +} from '../types/index.js' import { + buildPerformanceStatisticsMessage, CircularArray, Configuration, Constants, - JSONStringifyWithMapSupport, - buildPerformanceStatisticsMessage, extractTimeSeriesValues, formatDurationSeconds, generateUUID, - logPrefix, + JSONStringify, logger, - median, + logPrefix, + max, + min, nthPercentile, - stdDeviation, -} from '../utils'; + stdDeviation +} from '../utils/index.js' export class PerformanceStatistics { private static readonly instances: Map = new Map< - string, - PerformanceStatistics - >(); - - private readonly objId: string; - private readonly objName: string; - 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(); + 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 ?? '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)}~${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 (is(Error, 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 { @@ -83,172 +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: JSONStringifyWithMapSupport(this.statistics.statisticsData), - }); + statisticsData: JSON.parse( + JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object) + ) as Map + }) } - private startLogStatisticsInterval(): void { - const logStatisticsInterval = Configuration.getLog().enabled - ? Configuration.getLog().statisticsInterval - : 0; - 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 ${formatDurationSeconds(logStatisticsInterval)}` - ); - } else if (this.displayInterval) { + ) + } else if (this.displayInterval != null) { logger.info( `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}` - ); - } else if (Configuration.getLog().enabled) { + ) + } 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 ?? 0) + 1; - this.statistics.statisticsData.get(entryName).currentTimeMeasurement = entry.duration; - this.statistics.statisticsData.get(entryName).minTimeMeasurement = Math.min( + // 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(entryName)?.minTimeMeasurement ?? Infinity - ); - this.statistics.statisticsData.get(entryName).maxTimeMeasurement = Math.max( + 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(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).countTimeMeasurement; - this.statistics.statisticsData.get(entryName)?.measurementTimeSeries instanceof CircularArray + 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) - ?.measurementTimeSeries?.push({ timestamp: entry.startTime, value: entry.duration }) - : (this.statistics.statisticsData.get(entryName).measurementTimeSeries = + .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 = median( - extractTimeSeriesValues(this.statistics.statisticsData.get(entryName).measurementTimeSeries) - ); - this.statistics.statisticsData.get(entryName).ninetyFiveThPercentileTimeMeasurement = - nthPercentile( - extractTimeSeriesValues( - this.statistics.statisticsData.get(entryName).measurementTimeSeries - ), - 95 - ); - this.statistics.statisticsData.get(entryName).stdDevTimeMeasurement = stdDeviation( - extractTimeSeriesValues(this.statistics.statisticsData.get(entryName).measurementTimeSeries) - ); - 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 logPrefix = (): string => { - return logPrefix(` ${this.objName} | Performance statistics`); - }; + private static readonly logPrefix = (): string => { + return logPrefix(' Performance statistics') + } + + private readonly logPrefix = (): string => { + return logPrefix(` ${this.objName} | Performance statistics`) + } }