X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Futils.ts;h=20493767db87d80689a6cb067df3e22f72b759ce;hb=00e1bdeb5c50b0eede8fe2f72d47bf8992e4aede;hp=77f889c9b417ad008b0352b8c2e1779788ceb6d9;hpb=4bffc0628edc126828fd7262740dc0f2ac94517c;p=poolifier.git diff --git a/src/utils.ts b/src/utils.ts index 77f889c9..20493767 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,14 @@ import * as os from 'node:os' +import { getRandomValues } from 'node:crypto' +import { Worker as ClusterWorker } from 'node:cluster' +import { Worker as ThreadWorker } from 'node:worker_threads' +import { cpus } from 'node:os' import type { - MeasurementStatisticsRequirements, - WorkerChoiceStrategyOptions + InternalWorkerChoiceStrategyOptions, + MeasurementStatisticsRequirements } from './pools/selection-strategies/selection-strategies-types' import type { KillBehavior } from './worker/worker-options' -import type { MeasurementStatistics } from './pools/worker' +import { type IWorker, type WorkerType, WorkerTypes } from './pools/worker' /** * Default task name. @@ -19,15 +23,21 @@ export const EMPTY_FUNCTION: () => void = Object.freeze(() => { }) /** - * Default worker choice strategy options. + * Gets default worker choice strategy options. + * + * @param retries - The number of worker choice retries. + * @returns The default worker choice strategy options. */ -export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions = - { - choiceRetries: 6, +const getDefaultInternalWorkerChoiceStrategyOptions = ( + retries: number +): InternalWorkerChoiceStrategyOptions => { + return { + retries, runTime: { median: false }, waitTime: { median: false }, elu: { median: false } } +} /** * Default measurement statistics requirements. @@ -44,37 +54,99 @@ 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 try { availableParallelism = os.availableParallelism() } catch { - const numberOfCpus = os.cpus() - if (Array.isArray(numberOfCpus) && numberOfCpus.length > 0) { - availableParallelism = numberOfCpus.length + const cpus = os.cpus() + if (Array.isArray(cpus) && cpus.length > 0) { + availableParallelism = cpus.length } } return availableParallelism } -// /** -// * Computes the retry delay in milliseconds using an exponential back off algorithm. -// * -// * @param retryNumber - The number of retries that have already been attempted -// * @param maxDelayRatio - The maximum ratio of the delay that can be randomized -// * @returns Delay in milliseconds -// * @internal -// */ -// export const exponentialDelay = ( -// retryNumber = 0, -// maxDelayRatio = 0.2 -// ): number => { -// const delay = Math.pow(2, retryNumber) * 100 -// const randomSum = delay * maxDelayRatio * Math.random() // 0-(maxDelayRatio*100)% of the delay -// return delay + randomSum -// } +/** + * Returns the worker type of the given worker. + * + * @param worker - The worker to get the type of. + * @returns The worker type of the given worker. + * @internal + */ +export const getWorkerType = (worker: IWorker): WorkerType | undefined => { + if (worker instanceof ThreadWorker) { + return WorkerTypes.thread + } else if (worker instanceof ClusterWorker) { + return WorkerTypes.cluster + } +} + +/** + * Returns the worker id of the given worker. + * + * @param worker - The worker to get the id of. + * @returns The worker id of the given worker. + * @internal + */ +export const getWorkerId = (worker: IWorker): number | undefined => { + if (worker instanceof ThreadWorker) { + return worker.threadId + } else if (worker instanceof ClusterWorker) { + return worker.id + } +} + +/** + * 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. + * @internal + */ +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. @@ -105,6 +177,7 @@ export const median = (dataSet: number[]): number => { * @param num - The number to round. * @param scale - The scale to round to. * @returns The rounded number. + * @internal */ export const round = (num: number, scale = 2): number => { const rounder = Math.pow(10, scale) @@ -116,6 +189,7 @@ export const round = (num: number, scale = 2): number => { * * @param obj - The object to check. * @returns `true` if the given object is a plain object, `false` otherwise. + * @internal */ export const isPlainObject = (obj: unknown): boolean => typeof obj === 'object' && @@ -144,6 +218,7 @@ export const isKillBehavior = ( * * @param fn - Any value. * @returns `true` if `fn` was an asynchronous function, otherwise `false`. + * @internal */ export const isAsyncFunction = ( fn: unknown @@ -152,62 +227,99 @@ export const isAsyncFunction = ( } /** - * Updates the given measurement statistics. + * Generates a cryptographically secure random number in the [0,1[ range * - * @param measurementStatistics - The measurement statistics to update. - * @param measurementRequirements - The measurement statistics requirements. - * @param measurementValue - The measurement value. - * @param numberOfMeasurements - The number of measurements. + * @returns A number in the [0,1[ range * @internal */ -export const updateMeasurementStatistics = ( - measurementStatistics: MeasurementStatistics, - measurementRequirements: MeasurementStatisticsRequirements, - measurementValue: number, - numberOfMeasurements: number -): void => { - if (measurementRequirements.aggregate) { - measurementStatistics.aggregate = - (measurementStatistics.aggregate ?? 0) + measurementValue - measurementStatistics.minimum = Math.min( - measurementValue, - measurementStatistics.minimum ?? Infinity - ) - measurementStatistics.maximum = Math.max( - measurementValue, - measurementStatistics.maximum ?? -Infinity - ) - if (measurementRequirements.average && numberOfMeasurements !== 0) { - measurementStatistics.average = - measurementStatistics.aggregate / numberOfMeasurements - } - if (measurementRequirements.median && measurementValue != null) { - measurementStatistics.history.push(measurementValue) - measurementStatistics.median = median(measurementStatistics.history) - } - } +export const secureRandom = (): number => { + return getRandomValues(new Uint32Array(1))[0] / 0x100000000 } /** - * Executes a function once at a time. + * Returns the minimum of the given numbers. + * If no numbers are given, `Infinity` is returned. * - * @param fn - The function to execute. + * @param args - The numbers to get the minimum of. + * @returns The minimum of the given numbers. + * @internal + */ +export const min = (...args: number[]): number => + args.reduce((minimum, num) => (minimum < num ? minimum : num), Infinity) + +/** + * Returns the maximum of the given numbers. + * If no numbers are given, `-Infinity` is returned. + * + * @param args - The numbers to get the maximum of. + * @returns The maximum of the given numbers. + * @internal + */ +export const max = (...args: number[]): number => + args.reduce((maximum, num) => (maximum > num ? maximum : num), -Infinity) + +/** + * Wraps a function so that it can only be called once. + * + * @param fn - The function to wrap. * @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 + * @returns The wrapped function. + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const once = ( + fn: (...args: A) => R, + context: T +): ((...args: A) => R) => { + let result: R + return (...args: A) => { + if (fn != null) { + result = fn.apply(context, args) + ;(fn as unknown as undefined) = (context as unknown as undefined) = + undefined } + return result + } +} + +const clone = (object: T): T => { + return JSON.parse(JSON.stringify(object)) as T +} + +export const buildInternalWorkerChoiceStrategyOptions = ( + poolMaxSize: number, + opts?: InternalWorkerChoiceStrategyOptions +): InternalWorkerChoiceStrategyOptions => { + opts = clone(opts ?? {}) + if (opts.weights == null) { + opts.weights = getDefaultWeights(poolMaxSize) + } + return { + ...getDefaultInternalWorkerChoiceStrategyOptions( + poolMaxSize + Object.keys(opts?.weights ?? {}).length + ), + ...opts + } +} + +const getDefaultWeights = ( + poolMaxSize: number, + defaultWorkerWeight: number = getDefaultWorkerWeight() +): Record => { + const weights: Record = {} + for (let workerNodeKey = 0; workerNodeKey < poolMaxSize; workerNodeKey++) { + weights[workerNodeKey] = defaultWorkerWeight + } + return weights +} + +const getDefaultWorkerWeight = (): number => { + let cpusCycleTimeWeight = 0 + for (const cpu of cpus()) { + // CPU estimated cycle time + const numberOfDigits = cpu.speed.toString().length - 1 + const cpuCycleTime = 1 / (cpu.speed / Math.pow(10, numberOfDigits)) + cpusCycleTimeWeight += cpuCycleTime * Math.pow(10, numberOfDigits) } + return Math.round(cpusCycleTimeWeight / cpus().length) }