1 import * as os from
'node:os'
2 import { webcrypto
} from
'node:crypto'
3 import { Worker
as ClusterWorker
} from
'node:cluster'
4 import { Worker
as ThreadWorker
} from
'node:worker_threads'
6 MeasurementStatisticsRequirements
,
7 WorkerChoiceStrategyOptions
8 } from
'./pools/selection-strategies/selection-strategies-types'
9 import type { KillBehavior
} from
'./worker/worker-options'
12 type MeasurementStatistics
,
15 } from
'./pools/worker'
20 export const DEFAULT_TASK_NAME
= 'default'
23 * An intentional empty function.
25 export const EMPTY_FUNCTION
: () => void = Object.freeze(() => {
26 /* Intentionally empty */
30 * Default worker choice strategy options.
32 export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS
: WorkerChoiceStrategyOptions
=
35 runTime
: { median
: false },
36 waitTime
: { median
: false },
37 elu
: { median
: false }
41 * Default measurement statistics requirements.
43 export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS
: MeasurementStatisticsRequirements
=
51 * Returns safe host OS optimized estimate of the default amount of parallelism a pool should use.
52 * Always returns a value greater than zero.
54 * @returns The host OS optimized maximum pool size.
57 export const availableParallelism
= (): number => {
58 let availableParallelism
= 1
60 availableParallelism
= os
.availableParallelism()
62 const cpus
= os
.cpus()
63 if (Array.isArray(cpus
) && cpus
.length
> 0) {
64 availableParallelism
= cpus
.length
67 return availableParallelism
71 * Returns the worker type of the given worker.
73 * @param worker - The worker to get the type of.
74 * @returns The worker type of the given worker.
77 export const getWorkerType
= (worker
: IWorker
): WorkerType
| undefined => {
78 if (worker
instanceof ThreadWorker
) {
79 return WorkerTypes
.thread
80 } else if (worker
instanceof ClusterWorker
) {
81 return WorkerTypes
.cluster
86 * Returns the worker id of the given worker.
88 * @param worker - The worker to get the id of.
89 * @returns The worker id of the given worker.
92 export const getWorkerId
= (worker
: IWorker
): number | undefined => {
93 if (worker
instanceof ThreadWorker
) {
94 return worker
.threadId
95 } else if (worker
instanceof ClusterWorker
) {
101 * Sleeps for the given amount of milliseconds.
103 * @param ms - The amount of milliseconds to sleep.
104 * @returns A promise that resolves after the given amount of milliseconds.
106 export const sleep
= async (ms
: number): Promise
<void> => {
107 await new Promise((resolve
) => {
108 setTimeout(resolve
, ms
)
113 * Computes the retry delay in milliseconds using an exponential back off algorithm.
115 * @param retryNumber - The number of retries that have already been attempted
116 * @param delayFactor - The base delay factor in milliseconds
117 * @returns Delay in milliseconds
120 export const exponentialDelay
= (
124 const delay
= Math.pow(2, retryNumber
) * delayFactor
125 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
126 return delay
+ randomSum
130 * Computes the average of the given data set.
132 * @param dataSet - Data set.
133 * @returns The average of the given data set.
136 export const average
= (dataSet
: number[]): number => {
137 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
140 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
144 dataSet
.reduce((accumulator
, number) => accumulator
+ number, 0) /
150 * Computes the median of the given data set.
152 * @param dataSet - Data set.
153 * @returns The median of the given data set.
156 export const median
= (dataSet
: number[]): number => {
157 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
160 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
163 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
)
165 (sortedDataSet
[(sortedDataSet
.length
- 1) >> 1] +
166 sortedDataSet
[sortedDataSet
.length
>> 1]) /
172 * Rounds the given number to the given scale.
173 * The rounding is done using the "round half away from zero" method.
175 * @param num - The number to round.
176 * @param scale - The scale to round to.
177 * @returns The rounded number.
179 export const round
= (num
: number, scale
= 2): number => {
180 const rounder
= Math.pow(10, scale
)
181 return Math.round(num
* rounder
* (1 + Number.EPSILON
)) / rounder
185 * Is the given object a plain object?
187 * @param obj - The object to check.
188 * @returns `true` if the given object is a plain object, `false` otherwise.
190 export const isPlainObject
= (obj
: unknown
): boolean =>
191 typeof obj
=== 'object' &&
193 obj
?.constructor
=== Object &&
194 Object.prototype
.toString
.call(obj
) === '[object Object]'
197 * Detects whether the given value is a kill behavior or not.
199 * @typeParam KB - Which specific KillBehavior type to test against.
200 * @param killBehavior - Which kind of kill behavior to detect.
201 * @param value - Any value.
202 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
205 export const isKillBehavior
= <KB
extends KillBehavior
>(
209 return value
=== killBehavior
213 * Detects whether the given value is an asynchronous function or not.
215 * @param fn - Any value.
216 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
218 export const isAsyncFunction
= (
220 ): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
221 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
225 * Updates the given measurement statistics.
227 * @param measurementStatistics - The measurement statistics to update.
228 * @param measurementRequirements - The measurement statistics requirements.
229 * @param measurementValue - The measurement value.
230 * @param numberOfMeasurements - The number of measurements.
233 export const updateMeasurementStatistics
= (
234 measurementStatistics
: MeasurementStatistics
,
235 measurementRequirements
: MeasurementStatisticsRequirements
,
236 measurementValue
: number
238 if (measurementRequirements
.aggregate
) {
239 measurementStatistics
.aggregate
=
240 (measurementStatistics
.aggregate
?? 0) + measurementValue
241 measurementStatistics
.minimum
= Math.min(
243 measurementStatistics
.minimum
?? Infinity
245 measurementStatistics
.maximum
= Math.max(
247 measurementStatistics
.maximum
?? -Infinity
250 (measurementRequirements
.average
|| measurementRequirements
.median
) &&
251 measurementValue
!= null
253 measurementStatistics
.history
.push(measurementValue
)
254 if (measurementRequirements
.average
) {
255 measurementStatistics
.average
= average(measurementStatistics
.history
)
256 } else if (measurementStatistics
.average
!= null) {
257 delete measurementStatistics
.average
259 if (measurementRequirements
.median
) {
260 measurementStatistics
.median
= median(measurementStatistics
.history
)
261 } else if (measurementStatistics
.median
!= null) {
262 delete measurementStatistics
.median
269 * Generate a cryptographically secure random number in the [0,1[ range
271 * @returns A number in the [0,1[ range
273 export const secureRandom
= (): number => {
274 return webcrypto
.getRandomValues(new Uint32Array(1))[0] / 0x100000000