perf: use mnemonist CirculerBuffer
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 21 May 2024 12:57:50 +0000 (14:57 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 21 May 2024 12:57:50 +0000 (14:57 +0200)
Expected regression: performance statistics storage miss statistics for
now

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/performance/PerformanceStatistics.ts
src/types/Statistics.ts
src/utils/CircularArray.ts [deleted file]
src/utils/Constants.ts
src/utils/MessageChannelUtils.ts
src/utils/Utils.ts
src/utils/index.ts
tests/utils/CircularArray.test.ts [deleted file]
tests/utils/Utils.test.ts

index 265cb6c963918b1a3970a95e5822742f3202c0d4..558fb8caba2b13b9d9ff2caeaf69a9117283c6aa 100644 (file)
@@ -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<TimestampedData>(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<TimestampedData>(
+          Array<TimestampedData>,
+          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!
index 4c03dd9d73c62c06f35d432bbc18cf9be62ec082..89511b0291ba22054c70b6f6583960f4ec4b8129 100644 (file)
@@ -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<TimestampedData>
+  measurementTimeSeries: CircularBuffer<TimestampedData>
   currentTimeMeasurement: number
   minTimeMeasurement: number
   maxTimeMeasurement: number
diff --git a/src/utils/CircularArray.ts b/src/utils/CircularArray.ts
deleted file mode 100644 (file)
index 8fc7c60..0000000
+++ /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<T> extends Array<T> {
-  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<T | ConcatArray<T>>): CircularArray<T> {
-    const concatenatedCircularArray = super.concat(items as T[]) as CircularArray<T>
-    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<T> {
-    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<T>(
-          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<T>
-  }
-
-  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`)
-    }
-  }
-}
index 3746c4fdd91e8277ebc5167c563cbd3cf88fb673..8103b47404cdaaf7c871caad5eaaa2586183bc4b 100644 (file)
@@ -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'
 
index 4164c36bf11c96f18b8250cfd47be98bdeb897d5..2fb952560b49e25b82cfd50fff951cbfe27bcccf 100644 (file)
@@ -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<Statistics> => {
   return {
     event: ChargingStationWorkerMessageEvents.performanceStatistics,
-    data: statistics
+    // FIXME: CircularBuffer is not structured-cloneable, rambda clone strips the whole statisticsData Map
+    data: clone(statistics)
   }
 }
 
index aeed9b0fa44b4a073bff682583c95695cbb954ac..2c042217ce00854c0cbb6b11a55f2960bdbaaa9c 100644 (file)
@@ -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<TimestampedData>): number[] => {
+  return (timeSeries.toArray() as TimestampedData[]).map(timeSeriesItem => timeSeriesItem.value)
 }
 
 export const clone = <T>(object: T): T => {
index e1d6734c9c1fba31a6bf7eaeaad68e3e44abd913..a35887c7d3b53c48bf035d9511fb4933a6bad58e 100644 (file)
@@ -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 (file)
index 74be021..0000000
+++ /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))
-  })
-})
index 5672b032187e336ba48e6a85c73a0d5b5f981a02..8ab1332baf76eabc8bfae51ae6354769adfc3331 100644 (file)
@@ -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<TimestampedData>(Array, Constants.DEFAULT_CIRCULAR_BUFFER_CAPACITY)
+      )
+    ).toEqual([])
+    const circularBuffer = new CircularBuffer<TimestampedData>(
+      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()', () => {