1 import * as os from
'node:os'
2 import { getRandomValues
} from
'node:crypto'
3 import { Worker
as ClusterWorker
} from
'node:cluster'
4 import { Worker
as ThreadWorker
} from
'node:worker_threads'
5 import { cpus
} from
'node:os'
7 InternalWorkerChoiceStrategyOptions
,
8 MeasurementStatisticsRequirements
9 } from
'./pools/selection-strategies/selection-strategies-types'
10 import type { KillBehavior
} from
'./worker/worker-options'
11 import { type IWorker
, type WorkerType
, WorkerTypes
} from
'./pools/worker'
16 export const DEFAULT_TASK_NAME
= 'default'
19 * An intentional empty function.
21 export const EMPTY_FUNCTION
: () => void = Object.freeze(() => {
22 /* Intentionally empty */
26 * Gets default worker choice strategy options.
28 * @param retries - The number of worker choice retries.
29 * @returns The default worker choice strategy options.
31 const getDefaultInternalWorkerChoiceStrategyOptions
= (
33 ): InternalWorkerChoiceStrategyOptions
=> {
36 runTime
: { median
: false },
37 waitTime
: { median
: false },
38 elu
: { median
: false }
43 * Default measurement statistics requirements.
45 export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS
: MeasurementStatisticsRequirements
=
53 * Returns safe host OS optimized estimate of the default amount of parallelism a pool should use.
54 * Always returns a value greater than zero.
56 * @returns The host OS optimized maximum pool size.
58 export const availableParallelism
= (): number => {
59 let availableParallelism
= 1
61 availableParallelism
= os
.availableParallelism()
63 const cpus
= os
.cpus()
64 if (Array.isArray(cpus
) && cpus
.length
> 0) {
65 availableParallelism
= cpus
.length
68 return availableParallelism
72 * Returns the worker type of the given worker.
74 * @param worker - The worker to get the type of.
75 * @returns The worker type of the given worker.
78 export const getWorkerType
= (worker
: IWorker
): WorkerType
| undefined => {
79 if (worker
instanceof ThreadWorker
) {
80 return WorkerTypes
.thread
81 } else if (worker
instanceof ClusterWorker
) {
82 return WorkerTypes
.cluster
87 * Returns the worker id of the given worker.
89 * @param worker - The worker to get the id of.
90 * @returns The worker id of the given worker.
93 export const getWorkerId
= (worker
: IWorker
): number | undefined => {
94 if (worker
instanceof ThreadWorker
) {
95 return worker
.threadId
96 } else if (worker
instanceof ClusterWorker
) {
102 * Sleeps for the given amount of milliseconds.
104 * @param ms - The amount of milliseconds to sleep.
105 * @returns A promise that resolves after the given amount of milliseconds.
108 export const sleep
= async (ms
: number): Promise
<void> => {
109 await new Promise(resolve
=> {
110 setTimeout(resolve
, ms
)
115 * Computes the retry delay in milliseconds using an exponential back off algorithm.
117 * @param retryNumber - The number of retries that have already been attempted
118 * @param delayFactor - The base delay factor in milliseconds
119 * @returns Delay in milliseconds
122 export const exponentialDelay
= (
126 const delay
= Math.pow(2, retryNumber
) * delayFactor
127 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
128 return delay
+ randomSum
132 * Computes the average of the given data set.
134 * @param dataSet - Data set.
135 * @returns The average of the given data set.
138 export const average
= (dataSet
: number[]): number => {
139 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
142 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
146 dataSet
.reduce((accumulator
, number) => accumulator
+ number, 0) /
152 * Computes the median of the given data set.
154 * @param dataSet - Data set.
155 * @returns The median of the given data set.
158 export const median
= (dataSet
: number[]): number => {
159 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
162 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
165 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
)
167 (sortedDataSet
[(sortedDataSet
.length
- 1) >> 1] +
168 sortedDataSet
[sortedDataSet
.length
>> 1]) /
174 * Rounds the given number to the given scale.
175 * The rounding is done using the "round half away from zero" method.
177 * @param num - The number to round.
178 * @param scale - The scale to round to.
179 * @returns The rounded number.
182 export const round
= (num
: number, scale
= 2): number => {
183 const rounder
= Math.pow(10, scale
)
184 return Math.round(num
* rounder
* (1 + Number.EPSILON
)) / rounder
188 * Is the given object a plain object?
190 * @param obj - The object to check.
191 * @returns `true` if the given object is a plain object, `false` otherwise.
194 export const isPlainObject
= (obj
: unknown
): boolean =>
195 typeof obj
=== 'object' &&
197 obj
?.constructor
=== Object &&
198 Object.prototype
.toString
.call(obj
) === '[object Object]'
201 * Detects whether the given value is a kill behavior or not.
203 * @typeParam KB - Which specific KillBehavior type to test against.
204 * @param killBehavior - Which kind of kill behavior to detect.
205 * @param value - Any value.
206 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
209 export const isKillBehavior
= <KB
extends KillBehavior
>(
213 return value
=== killBehavior
217 * Detects whether the given value is an asynchronous function or not.
219 * @param fn - Any value.
220 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
223 export const isAsyncFunction
= (
225 ): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
226 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
230 * Generates a cryptographically secure random number in the [0,1[ range
232 * @returns A number in the [0,1[ range
235 export const secureRandom
= (): number => {
236 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
240 * Returns the minimum of the given numbers.
241 * If no numbers are given, `Infinity` is returned.
243 * @param args - The numbers to get the minimum of.
244 * @returns The minimum of the given numbers.
247 export const min
= (...args
: number[]): number =>
248 args
.reduce((minimum
, num
) => (minimum
< num
? minimum
: num
), Infinity)
251 * Returns the maximum of the given numbers.
252 * If no numbers are given, `-Infinity` is returned.
254 * @param args - The numbers to get the maximum of.
255 * @returns The maximum of the given numbers.
258 export const max
= (...args
: number[]): number =>
259 args
.reduce((maximum
, num
) => (maximum
> num
? maximum
: num
), -Infinity)
262 * Wraps a function so that it can only be called once.
264 * @param fn - The function to wrap.
265 * @param context - The context to bind the function to.
266 * @returns The wrapped function.
269 // eslint-disable-next-line @typescript-eslint/no-explicit-any
270 export const once
= <T
, A
extends any[], R
>(
271 fn
: (...args
: A
) => R
,
273 ): ((...args
: A
) => R
) => {
275 return (...args
: A
) => {
277 result
= fn
.apply
<T
, A
, R
>(context
, args
)
278 ;(fn
as unknown
as undefined) = (context
as unknown
as undefined) =
285 const clone
= <T
extends object
>(object
: T
): T
=> {
286 return JSON
.parse(JSON
.stringify(object
)) as T
289 export const buildInternalWorkerChoiceStrategyOptions
= (
291 opts
?: InternalWorkerChoiceStrategyOptions
292 ): InternalWorkerChoiceStrategyOptions
=> {
293 opts
= clone(opts
?? {})
294 if (opts
.weights
== null) {
295 opts
.weights
= getDefaultWeights(poolMaxSize
)
298 ...getDefaultInternalWorkerChoiceStrategyOptions(
299 poolMaxSize
+ Object.keys(opts
?.weights
?? {}).length
305 const getDefaultWeights
= (
307 defaultWorkerWeight
: number = getDefaultWorkerWeight()
308 ): Record
<number, number> => {
309 const weights
: Record
<number, number> = {}
310 for (let workerNodeKey
= 0; workerNodeKey
< poolMaxSize
; workerNodeKey
++) {
311 weights
[workerNodeKey
] = defaultWorkerWeight
316 const getDefaultWorkerWeight
= (): number => {
317 let cpusCycleTimeWeight
= 0
318 for (const cpu
of cpus()) {
319 // CPU estimated cycle time
320 const numberOfDigits
= cpu
.speed
.toString().length
- 1
321 const cpuCycleTime
= 1 / (cpu
.speed
/ Math.pow(10, numberOfDigits
))
322 cpusCycleTimeWeight
+= cpuCycleTime
* Math.pow(10, numberOfDigits
)
324 return Math.round(cpusCycleTimeWeight
/ cpus().length
)