test: improve IWRR coverage
[poolifier.git] / src / utils.ts
CommitLineData
aa4bf4b2 1import * as os from 'node:os'
304d379e 2import { getRandomValues } from 'node:crypto'
75de9f41
JB
3import { Worker as ClusterWorker } from 'node:cluster'
4import { Worker as ThreadWorker } from 'node:worker_threads'
00e1bdeb 5import { cpus } from 'node:os'
3c93feb9 6import type {
26ce26ca
JB
7 InternalWorkerChoiceStrategyOptions,
8 MeasurementStatisticsRequirements
3c93feb9 9} from './pools/selection-strategies/selection-strategies-types'
59317253 10import type { KillBehavior } from './worker/worker-options'
bfc75cca 11import { type IWorker, type WorkerType, WorkerTypes } from './pools/worker'
bbeadd16 12
ff128cc9
JB
13/**
14 * Default task name.
15 */
16export const DEFAULT_TASK_NAME = 'default'
17
6e9d10db
JB
18/**
19 * An intentional empty function.
20 */
4f3c3d89 21export const EMPTY_FUNCTION: () => void = Object.freeze(() => {
6e9d10db 22 /* Intentionally empty */
4f3c3d89 23})
78099a15
JB
24
25/**
449cd154 26 * Gets default worker choice strategy options.
26ce26ca 27 *
449cd154 28 * @param retries - The number of worker choice retries.
26ce26ca 29 * @returns The default worker choice strategy options.
bbeadd16 30 */
00e1bdeb 31const getDefaultInternalWorkerChoiceStrategyOptions = (
449cd154 32 retries: number
26ce26ca
JB
33): InternalWorkerChoiceStrategyOptions => {
34 return {
449cd154 35 retries,
932fc8be 36 runTime: { median: false },
5df69fab
JB
37 waitTime: { median: false },
38 elu: { median: false }
bbeadd16 39 }
26ce26ca 40}
bbeadd16 41
3c93feb9
JB
42/**
43 * Default measurement statistics requirements.
44 */
45export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS: MeasurementStatisticsRequirements =
46 {
47 aggregate: false,
48 average: false,
49 median: false
50 }
51
51474716 52/**
ab80dc46
JB
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.
55 *
56 * @returns The host OS optimized maximum pool size.
51474716
JB
57 */
58export const availableParallelism = (): number => {
59 let availableParallelism = 1
60 try {
aa4bf4b2 61 availableParallelism = os.availableParallelism()
51474716 62 } catch {
562a4037
JB
63 const cpus = os.cpus()
64 if (Array.isArray(cpus) && cpus.length > 0) {
65 availableParallelism = cpus.length
51474716
JB
66 }
67 }
68 return availableParallelism
69}
70
9fe8fd69
JB
71/**
72 * Returns the worker type of the given worker.
73 *
74 * @param worker - The worker to get the type of.
75 * @returns The worker type of the given worker.
76 * @internal
77 */
187601ff 78export const getWorkerType = (worker: IWorker): WorkerType | undefined => {
9fe8fd69
JB
79 if (worker instanceof ThreadWorker) {
80 return WorkerTypes.thread
81 } else if (worker instanceof ClusterWorker) {
82 return WorkerTypes.cluster
83 }
84}
85
86/**
87 * Returns the worker id of the given worker.
88 *
89 * @param worker - The worker to get the id of.
90 * @returns The worker id of the given worker.
91 * @internal
92 */
187601ff 93export const getWorkerId = (worker: IWorker): number | undefined => {
9fe8fd69
JB
94 if (worker instanceof ThreadWorker) {
95 return worker.threadId
96 } else if (worker instanceof ClusterWorker) {
97 return worker.id
98 }
99}
100
68cbdc84
JB
101/**
102 * Sleeps for the given amount of milliseconds.
103 *
104 * @param ms - The amount of milliseconds to sleep.
105 * @returns A promise that resolves after the given amount of milliseconds.
57a29f75 106 * @internal
68cbdc84
JB
107 */
108export const sleep = async (ms: number): Promise<void> => {
041dc05b 109 await new Promise(resolve => {
68cbdc84
JB
110 setTimeout(resolve, ms)
111 })
112}
113
114/**
115 * Computes the retry delay in milliseconds using an exponential back off algorithm.
116 *
117 * @param retryNumber - The number of retries that have already been attempted
147be6fe 118 * @param delayFactor - The base delay factor in milliseconds
68cbdc84
JB
119 * @returns Delay in milliseconds
120 * @internal
121 */
122export const exponentialDelay = (
123 retryNumber = 0,
147be6fe 124 delayFactor = 100
68cbdc84 125): number => {
147be6fe
JB
126 const delay = Math.pow(2, retryNumber) * delayFactor
127 const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
68cbdc84
JB
128 return delay + randomSum
129}
8990357d 130
dc021bcc
JB
131/**
132 * Computes the average of the given data set.
133 *
134 * @param dataSet - Data set.
135 * @returns The average of the given data set.
136 * @internal
137 */
138export const average = (dataSet: number[]): number => {
139 if (Array.isArray(dataSet) && dataSet.length === 0) {
140 return 0
141 }
142 if (Array.isArray(dataSet) && dataSet.length === 1) {
143 return dataSet[0]
144 }
145 return (
146 dataSet.reduce((accumulator, number) => accumulator + number, 0) /
147 dataSet.length
148 )
149}
150
bbeadd16 151/**
afe0d5bf 152 * Computes the median of the given data set.
78099a15
JB
153 *
154 * @param dataSet - Data set.
155 * @returns The median of the given data set.
4bffc062 156 * @internal
78099a15
JB
157 */
158export const median = (dataSet: number[]): number => {
4a45e8d2
JB
159 if (Array.isArray(dataSet) && dataSet.length === 0) {
160 return 0
161 }
78099a15
JB
162 if (Array.isArray(dataSet) && dataSet.length === 1) {
163 return dataSet[0]
164 }
c6f42dd6
JB
165 const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
166 return (
167 (sortedDataSet[(sortedDataSet.length - 1) >> 1] +
168 sortedDataSet[sortedDataSet.length >> 1]) /
169 2
170 )
78099a15 171}
0d80593b 172
afe0d5bf
JB
173/**
174 * Rounds the given number to the given scale.
64383951 175 * The rounding is done using the "round half away from zero" method.
afe0d5bf
JB
176 *
177 * @param num - The number to round.
178 * @param scale - The scale to round to.
179 * @returns The rounded number.
57a29f75 180 * @internal
afe0d5bf
JB
181 */
182export const round = (num: number, scale = 2): number => {
183 const rounder = Math.pow(10, scale)
184 return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
185}
186
3c653a03
JB
187/**
188 * Is the given object a plain object?
189 *
190 * @param obj - The object to check.
191 * @returns `true` if the given object is a plain object, `false` otherwise.
57a29f75 192 * @internal
3c653a03 193 */
0d80593b
JB
194export const isPlainObject = (obj: unknown): boolean =>
195 typeof obj === 'object' &&
196 obj !== null &&
197 obj?.constructor === Object &&
198 Object.prototype.toString.call(obj) === '[object Object]'
59317253
JB
199
200/**
201 * Detects whether the given value is a kill behavior or not.
202 *
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`.
4bffc062 207 * @internal
59317253
JB
208 */
209export const isKillBehavior = <KB extends KillBehavior>(
210 killBehavior: KB,
211 value: unknown
212): value is KB => {
213 return value === killBehavior
214}
49d1b48c
JB
215
216/**
217 * Detects whether the given value is an asynchronous function or not.
218 *
219 * @param fn - Any value.
220 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
57a29f75 221 * @internal
49d1b48c
JB
222 */
223export const isAsyncFunction = (
224 fn: unknown
225): fn is (...args: unknown[]) => Promise<unknown> => {
226 return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'
227}
e4f20deb 228
68cbdc84 229/**
57a29f75 230 * Generates a cryptographically secure random number in the [0,1[ range
68cbdc84
JB
231 *
232 * @returns A number in the [0,1[ range
57a29f75 233 * @internal
68cbdc84 234 */
970b38d6 235export const secureRandom = (): number => {
304d379e 236 return getRandomValues(new Uint32Array(1))[0] / 0x100000000
68cbdc84 237}
68e7ed58 238
57a29f75
JB
239/**
240 * Returns the minimum of the given numbers.
241 * If no numbers are given, `Infinity` is returned.
242 *
243 * @param args - The numbers to get the minimum of.
244 * @returns The minimum of the given numbers.
245 * @internal
246 */
90d6701c
JB
247export const min = (...args: number[]): number =>
248 args.reduce((minimum, num) => (minimum < num ? minimum : num), Infinity)
249
57a29f75
JB
250/**
251 * Returns the maximum of the given numbers.
252 * If no numbers are given, `-Infinity` is returned.
253 *
254 * @param args - The numbers to get the maximum of.
255 * @returns The maximum of the given numbers.
256 * @internal
257 */
90d6701c
JB
258export const max = (...args: number[]): number =>
259 args.reduce((maximum, num) => (maximum > num ? maximum : num), -Infinity)
d91689fd
JB
260
261/**
262 * Wraps a function so that it can only be called once.
263 *
264 * @param fn - The function to wrap.
265 * @param context - The context to bind the function to.
266 * @returns The wrapped function.
267 * @internal
268 */
269// eslint-disable-next-line @typescript-eslint/no-explicit-any
270export const once = <T, A extends any[], R>(
271 fn: (...args: A) => R,
272 context: T
273): ((...args: A) => R) => {
274 let result: R
275 return (...args: A) => {
276 if (fn != null) {
277 result = fn.apply<T, A, R>(context, args)
278 ;(fn as unknown as undefined) = (context as unknown as undefined) =
279 undefined
280 }
281 return result
282 }
283}
00e1bdeb
JB
284
285const clone = <T extends object>(object: T): T => {
286 return JSON.parse(JSON.stringify(object)) as T
287}
288
289export const buildInternalWorkerChoiceStrategyOptions = (
290 poolMaxSize: number,
291 opts?: InternalWorkerChoiceStrategyOptions
292): InternalWorkerChoiceStrategyOptions => {
293 opts = clone(opts ?? {})
69bba5e2 294 if (opts?.weights == null) {
00e1bdeb
JB
295 opts.weights = getDefaultWeights(poolMaxSize)
296 }
297 return {
298 ...getDefaultInternalWorkerChoiceStrategyOptions(
69bba5e2 299 poolMaxSize + Object.keys(opts.weights).length
00e1bdeb
JB
300 ),
301 ...opts
302 }
303}
304
305const getDefaultWeights = (
306 poolMaxSize: number,
ee79e8e5 307 defaultWorkerWeight?: number
00e1bdeb 308): Record<number, number> => {
ee79e8e5 309 defaultWorkerWeight = defaultWorkerWeight ?? getDefaultWorkerWeight()
00e1bdeb
JB
310 const weights: Record<number, number> = {}
311 for (let workerNodeKey = 0; workerNodeKey < poolMaxSize; workerNodeKey++) {
312 weights[workerNodeKey] = defaultWorkerWeight
313 }
314 return weights
315}
316
317const getDefaultWorkerWeight = (): number => {
318 let cpusCycleTimeWeight = 0
319 for (const cpu of cpus()) {
320 // CPU estimated cycle time
321 const numberOfDigits = cpu.speed.toString().length - 1
322 const cpuCycleTime = 1 / (cpu.speed / Math.pow(10, numberOfDigits))
323 cpusCycleTimeWeight += cpuCycleTime * Math.pow(10, numberOfDigits)
324 }
325 return Math.round(cpusCycleTimeWeight / cpus().length)
326}