From 840ca85d7c40a6ee6f3a85a50e68dbea2f90acb8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 21 May 2024 14:57:50 +0200 Subject: [PATCH] perf: use mnemonist CirculerBuffer MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Expected regression: performance statistics storage miss statistics for now Signed-off-by: Jérôme Benoit --- src/performance/PerformanceStatistics.ts | 28 +++-- src/types/Statistics.ts | 5 +- src/utils/CircularArray.ts | 96 --------------- src/utils/Constants.ts | 2 +- src/utils/MessageChannelUtils.ts | 5 +- src/utils/Utils.ts | 5 +- src/utils/index.ts | 1 - tests/utils/CircularArray.test.ts | 147 ----------------------- tests/utils/Utils.test.ts | 21 ++-- 9 files changed, 42 insertions(+), 268 deletions(-) delete mode 100644 src/utils/CircularArray.ts delete mode 100644 tests/utils/CircularArray.test.ts diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index 265cb6c9..558fb8ca 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -5,6 +5,7 @@ import type { URL } from 'node:url' import { parentPort } from 'node:worker_threads' import { secondsToMilliseconds } from 'date-fns' +import { CircularBuffer } from 'mnemonist' import { is, mean, median } from 'rambda' import { BaseError } from '../exception/index.js' @@ -22,7 +23,6 @@ import { } from '../types/index.js' import { buildPerformanceStatisticsMessage, - CircularArray, Configuration, Constants, extractTimeSeriesValues, @@ -277,16 +277,22 @@ export class PerformanceStatistics { // 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 - })) + if ( + !( + this.statistics.statisticsData.get(entry.name)?.measurementTimeSeries instanceof + CircularBuffer + ) + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.statistics.statisticsData.get(entry.name)!.measurementTimeSeries = + new CircularBuffer( + Array, + Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY + ) + } + this.statistics.statisticsData + .get(entry.name) + ?.measurementTimeSeries?.push({ 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! diff --git a/src/types/Statistics.ts b/src/types/Statistics.ts index 4c03dd9d..89511b02 100644 --- a/src/types/Statistics.ts +++ b/src/types/Statistics.ts @@ -1,4 +1,5 @@ -import type { CircularArray } from '../utils/index.js' +import type { CircularBuffer } from 'mnemonist' + import type { WorkerData } from '../worker/index.js' import type { IncomingRequestCommand, RequestCommand } from './ocpp/Requests.js' @@ -12,7 +13,7 @@ export type StatisticsData = Partial<{ responseCount: number errorCount: number timeMeasurementCount: number - measurementTimeSeries: CircularArray + measurementTimeSeries: CircularBuffer currentTimeMeasurement: number minTimeMeasurement: number maxTimeMeasurement: number diff --git a/src/utils/CircularArray.ts b/src/utils/CircularArray.ts deleted file mode 100644 index 8fc7c60d..00000000 --- a/src/utils/CircularArray.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright Jerome Benoit. 2021-2024. All Rights Reserved. - -export const DEFAULT_CIRCULAR_ARRAY_SIZE = 385 - -/** - * Array with a maximum length and shifting items when full. - */ -export class CircularArray extends Array { - public size: number - - constructor (size: number = DEFAULT_CIRCULAR_ARRAY_SIZE, ...items: T[]) { - super() - this.checkSize(size) - this.size = size - if (arguments.length > 1) { - this.push(...items) - } - } - - public push (...items: T[]): number { - const length = super.push(...items) - if (length > this.size) { - super.splice(0, length - this.size) - } - return this.length - } - - public unshift (...items: T[]): number { - const length = super.unshift(...items) - if (length > this.size) { - super.splice(this.size, items.length) - } - return this.length - } - - public concat (...items: Array>): CircularArray { - const concatenatedCircularArray = super.concat(items as T[]) as CircularArray - concatenatedCircularArray.size = this.size - if (concatenatedCircularArray.length > concatenatedCircularArray.size) { - concatenatedCircularArray.splice( - 0, - concatenatedCircularArray.length - concatenatedCircularArray.size - ) - } - return concatenatedCircularArray - } - - public splice (start: number, deleteCount?: number, ...items: T[]): CircularArray { - let itemsRemoved: T[] = [] - if (arguments.length >= 3 && deleteCount != null) { - itemsRemoved = super.splice(start, deleteCount, ...items) - if (this.length > this.size) { - const itemsOverflowing = super.splice(0, this.length - this.size) - itemsRemoved = new CircularArray( - itemsRemoved.length + itemsOverflowing.length, - ...itemsRemoved, - ...itemsOverflowing - ) - } - } else if (arguments.length === 2) { - itemsRemoved = super.splice(start, deleteCount) - } else { - itemsRemoved = super.splice(start) - } - return itemsRemoved as CircularArray - } - - public resize (size: number): void { - this.checkSize(size) - if (size === 0) { - this.length = 0 - } else if (size < this.size) { - for (let i = size; i < this.size; i++) { - super.pop() - } - } - this.size = size - } - - public empty (): boolean { - return this.length === 0 - } - - public full (): boolean { - return this.length === this.size - } - - private checkSize (size: number): void { - if (!Number.isSafeInteger(size)) { - throw new TypeError(`Invalid circular array size: ${size} is not a safe integer`) - } - if (size < 0) { - throw new RangeError(`Invalid circular array size: ${size} < 0`) - } - } -} diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 3746c4fd..8103b474 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -67,7 +67,7 @@ export class Constants { stopAbsoluteDuration: false }) - static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 4096 + static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 385 static readonly DEFAULT_HASH_ALGORITHM = 'sha384' diff --git a/src/utils/MessageChannelUtils.ts b/src/utils/MessageChannelUtils.ts index 4164c36b..2fb95256 100644 --- a/src/utils/MessageChannelUtils.ts +++ b/src/utils/MessageChannelUtils.ts @@ -1,3 +1,5 @@ +import { clone } from 'rambda' + import type { ChargingStation } from '../charging-station/index.js' import { type ChargingStationData, @@ -62,7 +64,8 @@ export const buildPerformanceStatisticsMessage = ( ): ChargingStationWorkerMessage => { return { event: ChargingStationWorkerMessageEvents.performanceStatistics, - data: statistics + // FIXME: CircularBuffer is not structured-cloneable, rambda clone strips the whole statisticsData Map + data: clone(statistics) } } diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index aeed9b0f..2c042217 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -12,6 +12,7 @@ import { minutesToSeconds, secondsToMilliseconds } from 'date-fns' +import type { CircularBuffer } from 'mnemonist' import { is } from 'rambda' import { @@ -200,8 +201,8 @@ export const getRandomFloatFluctuatedRounded = ( ) } -export const extractTimeSeriesValues = (timeSeries: TimestampedData[]): number[] => { - return timeSeries.map(timeSeriesItem => timeSeriesItem.value) +export const extractTimeSeriesValues = (timeSeries: CircularBuffer): number[] => { + return (timeSeries.toArray() as TimestampedData[]).map(timeSeriesItem => timeSeriesItem.value) } export const clone = (object: T): T => { diff --git a/src/utils/index.ts b/src/utils/index.ts index e1d6734c..a35887c7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,7 +5,6 @@ export { buildEvsesStatus, OutputFormat } from './ChargingStationConfigurationUtils.js' -export { CircularArray } from './CircularArray.js' export { Configuration } from './Configuration.js' export { Constants } from './Constants.js' export { ACElectricUtils, DCElectricUtils } from './ElectricUtils.js' diff --git a/tests/utils/CircularArray.test.ts b/tests/utils/CircularArray.test.ts deleted file mode 100644 index 74be0210..00000000 --- a/tests/utils/CircularArray.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { describe, it } from 'node:test' - -import { expect } from 'expect' - -import { CircularArray, DEFAULT_CIRCULAR_ARRAY_SIZE } from '../../src/utils/CircularArray.js' - -await describe('CircularArray test suite', async () => { - await it('Verify that circular array can be instantiated', () => { - const circularArray = new CircularArray() - expect(circularArray).toBeInstanceOf(CircularArray) - }) - - await it('Verify circular array default size at instance creation', () => { - const circularArray = new CircularArray() - expect(circularArray.size).toBe(DEFAULT_CIRCULAR_ARRAY_SIZE) - }) - - await it('Verify that circular array size can be set at instance creation', () => { - const circularArray = new CircularArray(1000) - expect(circularArray.size).toBe(1000) - }) - - await it('Verify that circular array size and items can be set at instance creation', () => { - let circularArray = new CircularArray(1000, 1, 2, 3, 4, 5) - expect(circularArray.size).toBe(1000) - expect(circularArray.length).toBe(5) - circularArray = new CircularArray(4, 1, 2, 3, 4, 5) - expect(circularArray.size).toBe(4) - expect(circularArray.length).toBe(4) - }) - - await it('Verify that circular array size is valid at instance creation', () => { - expect(() => new CircularArray(0.25)).toThrow( - new TypeError('Invalid circular array size: 0.25 is not a safe integer') - ) - expect(() => new CircularArray(-1)).toThrow( - new RangeError('Invalid circular array size: -1 < 0') - ) - expect(() => new CircularArray(Number.MAX_SAFE_INTEGER + 1)).toThrow( - new TypeError( - `Invalid circular array size: ${Number.MAX_SAFE_INTEGER + 1} is not a safe integer` - ) - ) - }) - - await it('Verify that circular array empty works as intended', () => { - const circularArray = new CircularArray() - expect(circularArray.empty()).toBe(true) - }) - - await it('Verify that circular array full works as intended', () => { - const circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - expect(circularArray.full()).toBe(true) - }) - - await it('Verify that circular array push works as intended', () => { - let circularArray = new CircularArray(4) - let arrayLength = circularArray.push(1, 2, 3, 4, 5) - expect(arrayLength).toBe(circularArray.size) - expect(circularArray.length).toBe(circularArray.size) - expect(circularArray).toStrictEqual(new CircularArray(4, 2, 3, 4, 5)) - arrayLength = circularArray.push(6, 7) - expect(arrayLength).toBe(circularArray.size) - expect(circularArray.length).toBe(circularArray.size) - expect(circularArray).toStrictEqual(new CircularArray(4, 4, 5, 6, 7)) - circularArray = new CircularArray(100) - arrayLength = circularArray.push(1, 2, 3, 4, 5) - expect(arrayLength).toBe(5) - expect(circularArray.size).toBe(100) - expect(circularArray.length).toBe(5) - expect(circularArray).toStrictEqual(new CircularArray(100, 1, 2, 3, 4, 5)) - }) - - await it('Verify that circular array splice works as intended', () => { - let circularArray = new CircularArray(1000, 1, 2, 3, 4, 5) - let deletedItems = circularArray.splice(2) - expect(deletedItems).toStrictEqual(new CircularArray(3, 3, 4, 5)) - expect(circularArray.length).toBe(2) - expect(circularArray).toStrictEqual(new CircularArray(1000, 1, 2)) - circularArray = new CircularArray(1000, 1, 2, 3, 4, 5) - deletedItems = circularArray.splice(2, 1) - expect(deletedItems).toStrictEqual(new CircularArray(1, 3)) - expect(circularArray.length).toBe(4) - expect(circularArray).toStrictEqual(new CircularArray(1000, 1, 2, 4, 5)) - circularArray = new CircularArray(4, 1, 2, 3, 4) - deletedItems = circularArray.splice(2, 1, 5, 6) - expect(deletedItems).toStrictEqual(new CircularArray(2, 3, 1)) - expect(circularArray.length).toBe(4) - expect(circularArray).toStrictEqual(new CircularArray(4, 2, 5, 6, 4)) - }) - - await it('Verify that circular array concat works as intended', () => { - let circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - circularArray = circularArray.concat(6, 7) - expect(circularArray.length).toBe(5) - expect(circularArray).toStrictEqual(new CircularArray(5, 3, 4, 5, 6, 7)) - circularArray = new CircularArray(1) - circularArray = circularArray.concat(6, 7) - expect(circularArray.length).toBe(1) - expect(circularArray).toStrictEqual(new CircularArray(1, 7)) - }) - - await it('Verify that circular array unshift works as intended', () => { - let circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - let arrayLength = circularArray.unshift(6, 7) - expect(arrayLength).toBe(5) - expect(circularArray.length).toBe(5) - expect(circularArray).toStrictEqual(new CircularArray(5, 6, 7, 1, 2, 3)) - circularArray = new CircularArray(1) - arrayLength = circularArray.unshift(6, 7) - expect(arrayLength).toBe(1) - expect(circularArray.length).toBe(1) - expect(circularArray).toStrictEqual(new CircularArray(1, 6)) - }) - - await it('Verify that circular array resize works as intended', () => { - expect(() => { - new CircularArray().resize(0.25) - }).toThrow(new TypeError('Invalid circular array size: 0.25 is not a safe integer')) - expect(() => { - new CircularArray().resize(-1) - }).toThrow(new RangeError('Invalid circular array size: -1 < 0')) - expect(() => { - new CircularArray().resize(Number.MAX_SAFE_INTEGER + 1) - }).toThrow( - new TypeError( - `Invalid circular array size: ${Number.MAX_SAFE_INTEGER + 1} is not a safe integer` - ) - ) - let circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - circularArray.resize(0) - expect(circularArray.size).toBe(0) - expect(circularArray).toStrictEqual(new CircularArray(0)) - circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - circularArray.resize(1) - expect(circularArray.size).toBe(1) - expect(circularArray).toStrictEqual(new CircularArray(1, 1)) - circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - circularArray.resize(3) - expect(circularArray.size).toBe(3) - expect(circularArray).toStrictEqual(new CircularArray(3, 1, 2, 3)) - circularArray = new CircularArray(5, 1, 2, 3, 4, 5) - circularArray.resize(8) - expect(circularArray.size).toBe(8) - expect(circularArray).toStrictEqual(new CircularArray(8, 1, 2, 3, 4, 5)) - }) -}) diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index 5672b032..8ab1332b 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -4,8 +4,10 @@ import { describe, it } from 'node:test' import { hoursToMilliseconds, hoursToSeconds } from 'date-fns' import { expect } from 'expect' +import { CircularBuffer } from 'mnemonist' import { satisfies } from 'semver' +import type { TimestampedData } from '../../src/types/index.js' import { Constants } from '../../src/utils/Constants.js' import { clone, @@ -188,14 +190,19 @@ await describe('Utils test suite', async () => { }) await it('Verify extractTimeSeriesValues()', () => { - expect(extractTimeSeriesValues([])).toEqual([]) - expect(extractTimeSeriesValues([{ timestamp: Date.now(), value: 1.1 }])).toEqual([1.1]) expect( - extractTimeSeriesValues([ - { timestamp: Date.now(), value: 1.1 }, - { timestamp: Date.now(), value: 2.2 } - ]) - ).toEqual([1.1, 2.2]) + extractTimeSeriesValues( + new CircularBuffer(Array, Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY) + ) + ).toEqual([]) + const circularBuffer = new CircularBuffer( + Array, + Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY + ) + circularBuffer.push({ timestamp: Date.now(), value: 1.1 }) + circularBuffer.push({ timestamp: Date.now(), value: 2.2 }) + circularBuffer.push({ timestamp: Date.now(), value: 3.3 }) + expect(extractTimeSeriesValues(circularBuffer)).toEqual([1.1, 2.2, 3.3]) }) await it('Verify isObject()', () => { -- 2.34.1