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.
56 export const availableParallelism
= (): number => {
57 let availableParallelism
= 1
59 availableParallelism
= os
.availableParallelism()
61 const cpus
= os
.cpus()
62 if (Array.isArray(cpus
) && cpus
.length
> 0) {
63 availableParallelism
= cpus
.length
66 return availableParallelism
70 * Returns the worker type of the given worker.
72 * @param worker - The worker to get the type of.
73 * @returns The worker type of the given worker.
76 export const getWorkerType
= (worker
: IWorker
): WorkerType
| undefined => {
77 if (worker
instanceof ThreadWorker
) {
78 return WorkerTypes
.thread
79 } else if (worker
instanceof ClusterWorker
) {
80 return WorkerTypes
.cluster
85 * Returns the worker id of the given worker.
87 * @param worker - The worker to get the id of.
88 * @returns The worker id of the given worker.
91 export const getWorkerId
= (worker
: IWorker
): number | undefined => {
92 if (worker
instanceof ThreadWorker
) {
93 return worker
.threadId
94 } else if (worker
instanceof ClusterWorker
) {
100 * Sleeps for the given amount of milliseconds.
102 * @param ms - The amount of milliseconds to sleep.
103 * @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.
180 export const round
= (num
: number, scale
= 2): number => {
181 const rounder
= Math.pow(10, scale
)
182 return Math.round(num
* rounder
* (1 + Number.EPSILON
)) / rounder
186 * Is the given object a plain object?
188 * @param obj - The object to check.
189 * @returns `true` if the given object is a plain object, `false` otherwise.
192 export const isPlainObject
= (obj
: unknown
): boolean =>
193 typeof obj
=== 'object' &&
195 obj
?.constructor
=== Object &&
196 Object.prototype
.toString
.call(obj
) === '[object Object]'
199 * Detects whether the given value is a kill behavior or not.
201 * @typeParam KB - Which specific KillBehavior type to test against.
202 * @param killBehavior - Which kind of kill behavior to detect.
203 * @param value - Any value.
204 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
207 export const isKillBehavior
= <KB
extends KillBehavior
>(
211 return value
=== killBehavior
215 * Detects whether the given value is an asynchronous function or not.
217 * @param fn - Any value.
218 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
221 export const isAsyncFunction
= (
223 ): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
224 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
228 * Updates the given measurement statistics.
230 * @param measurementStatistics - The measurement statistics to update.
231 * @param measurementRequirements - The measurement statistics requirements.
232 * @param measurementValue - The measurement value.
233 * @param numberOfMeasurements - The number of measurements.
236 export const updateMeasurementStatistics
= (
237 measurementStatistics
: MeasurementStatistics
,
238 measurementRequirements
: MeasurementStatisticsRequirements
,
239 measurementValue
: number
241 if (measurementRequirements
.aggregate
) {
242 measurementStatistics
.aggregate
=
243 (measurementStatistics
.aggregate
?? 0) + measurementValue
244 measurementStatistics
.minimum
= min(
246 measurementStatistics
.minimum
?? Infinity
248 measurementStatistics
.maximum
= max(
250 measurementStatistics
.maximum
?? -Infinity
253 (measurementRequirements
.average
|| measurementRequirements
.median
) &&
254 measurementValue
!= null
256 measurementStatistics
.history
.push(measurementValue
)
257 if (measurementRequirements
.average
) {
258 measurementStatistics
.average
= average(measurementStatistics
.history
)
259 } else if (measurementStatistics
.average
!= null) {
260 delete measurementStatistics
.average
262 if (measurementRequirements
.median
) {
263 measurementStatistics
.median
= median(measurementStatistics
.history
)
264 } else if (measurementStatistics
.median
!= null) {
265 delete measurementStatistics
.median
272 * Generates a cryptographically secure random number in the [0,1[ range
274 * @returns A number in the [0,1[ range
277 export const secureRandom
= (): number => {
278 return webcrypto
.getRandomValues(new Uint32Array(1))[0] / 0x100000000
282 * Returns the minimum of the given numbers.
283 * If no numbers are given, `Infinity` is returned.
285 * @param args - The numbers to get the minimum of.
286 * @returns The minimum of the given numbers.
289 export const min
= (...args
: number[]): number =>
290 args
.reduce((minimum
, num
) => (minimum
< num
? minimum
: num
), Infinity)
293 * Returns the maximum of the given numbers.
294 * If no numbers are given, `-Infinity` is returned.
296 * @param args - The numbers to get the maximum of.
297 * @returns The maximum of the given numbers.
300 export const max
= (...args
: number[]): number =>
301 args
.reduce((maximum
, num
) => (maximum
> num
? maximum
: num
), -Infinity)