X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Futils.ts;h=b84c4c803efe974b1dde4d8e3988465bef6d94b7;hb=98446b391031d7add0e0d7f7e310e25a014370e2;hp=cc71491a2fbe9396a9f0411ad32a9438cf6f7d35;hpb=e4f20deb9ad8a6e0113306f0ec19ba6d4676d24a;p=poolifier.git diff --git a/src/utils.ts b/src/utils.ts index cc71491a..b84c4c80 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import * as os from 'node:os' +import { webcrypto } from 'node:crypto' import type { MeasurementStatisticsRequirements, WorkerChoiceStrategyOptions @@ -23,6 +24,7 @@ export const EMPTY_FUNCTION: () => void = Object.freeze(() => { */ export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions = { + choiceRetries: 6, runTime: { median: false }, waitTime: { median: false }, elu: { median: false } @@ -43,6 +45,7 @@ export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS: MeasurementStatisticsR * Always returns a value greater than zero. * * @returns The host OS optimized maximum pool size. + * @internal */ export const availableParallelism = (): number => { let availableParallelism = 1 @@ -57,11 +60,61 @@ export const availableParallelism = (): number => { return availableParallelism } +/** + * Sleeps for the given amount of milliseconds. + * + * @param ms - The amount of milliseconds to sleep. + * @returns A promise that resolves after the given amount of milliseconds. + */ +export const sleep = async (ms: number): Promise => { + await new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +/** + * Computes the retry delay in milliseconds using an exponential back off algorithm. + * + * @param retryNumber - The number of retries that have already been attempted + * @param delayFactor - The base delay factor in milliseconds + * @returns Delay in milliseconds + * @internal + */ +export const exponentialDelay = ( + retryNumber = 0, + delayFactor = 100 +): number => { + const delay = Math.pow(2, retryNumber) * delayFactor + const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay + return delay + randomSum +} + +/** + * Computes the average of the given data set. + * + * @param dataSet - Data set. + * @returns The average of the given data set. + * @internal + */ +export const average = (dataSet: number[]): number => { + if (Array.isArray(dataSet) && dataSet.length === 0) { + return 0 + } + if (Array.isArray(dataSet) && dataSet.length === 1) { + return dataSet[0] + } + return ( + dataSet.reduce((accumulator, number) => accumulator + number, 0) / + dataSet.length + ) +} + /** * Computes the median of the given data set. * * @param dataSet - Data set. * @returns The median of the given data set. + * @internal */ export const median = (dataSet: number[]): number => { if (Array.isArray(dataSet) && dataSet.length === 0) { @@ -110,6 +163,7 @@ export const isPlainObject = (obj: unknown): boolean => * @param killBehavior - Which kind of kill behavior to detect. * @param value - Any value. * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`. + * @internal */ export const isKillBehavior = ( killBehavior: KB, @@ -136,13 +190,13 @@ export const isAsyncFunction = ( * @param measurementStatistics - The measurement statistics to update. * @param measurementRequirements - The measurement statistics requirements. * @param measurementValue - The measurement value. - * @param tasksExecuted - The number of tasks executed. + * @param numberOfMeasurements - The number of measurements. + * @internal */ export const updateMeasurementStatistics = ( measurementStatistics: MeasurementStatistics, measurementRequirements: MeasurementStatisticsRequirements, - measurementValue: number, - tasksExecuted: number + measurementValue: number ): void => { if (measurementRequirements.aggregate) { measurementStatistics.aggregate = @@ -155,13 +209,50 @@ export const updateMeasurementStatistics = ( measurementValue, measurementStatistics.maximum ?? -Infinity ) - if (measurementRequirements.average && tasksExecuted !== 0) { - measurementStatistics.average = - measurementStatistics.aggregate / tasksExecuted - } - if (measurementRequirements.median && measurementValue != null) { + if ( + (measurementRequirements.average || measurementRequirements.median) && + measurementValue != null + ) { measurementStatistics.history.push(measurementValue) - measurementStatistics.median = median(measurementStatistics.history) + if (measurementRequirements.average) { + measurementStatistics.average = average(measurementStatistics.history) + } + if (measurementRequirements.median) { + measurementStatistics.median = median(measurementStatistics.history) + } + } + } +} + +/** + * Executes a function once at a time. + * + * @param fn - The function to execute. + * @param context - The context to bind the function to. + * @returns The function to execute. + */ +export const once = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fn: (...args: any[]) => void, + context: unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): ((...args: any[]) => void) => { + let called = false + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (...args: any[]): void { + if (!called) { + called = true + fn.apply(context, args) + called = false } } } + +/** + * Generate a cryptographically secure random number in the [0,1[ range + * + * @returns A number in the [0,1[ range + */ +export const secureRandom = (): number => { + return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000 +}