refactor: cleanup worker choice strategies options namespace
[poolifier.git] / src / utils.ts
CommitLineData
aa4bf4b2 1import * as os from 'node:os'
98446b39 2import { webcrypto } from 'node:crypto'
75de9f41
JB
3import { Worker as ClusterWorker } from 'node:cluster'
4import { Worker as ThreadWorker } from 'node:worker_threads'
3c93feb9
JB
5import type {
6 MeasurementStatisticsRequirements,
7 WorkerChoiceStrategyOptions
8} from './pools/selection-strategies/selection-strategies-types'
59317253 9import type { KillBehavior } from './worker/worker-options'
75de9f41
JB
10import {
11 type IWorker,
12 type MeasurementStatistics,
13 type WorkerType,
14 WorkerTypes
15} from './pools/worker'
bbeadd16 16
ff128cc9
JB
17/**
18 * Default task name.
19 */
20export const DEFAULT_TASK_NAME = 'default'
21
6e9d10db
JB
22/**
23 * An intentional empty function.
24 */
4f3c3d89 25export const EMPTY_FUNCTION: () => void = Object.freeze(() => {
6e9d10db 26 /* Intentionally empty */
4f3c3d89 27})
78099a15
JB
28
29/**
bbeadd16
JB
30 * Default worker choice strategy options.
31 */
32export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions =
33 {
8c0b113f 34 retries: 6,
932fc8be 35 runTime: { median: false },
5df69fab
JB
36 waitTime: { median: false },
37 elu: { median: false }
bbeadd16
JB
38 }
39
3c93feb9
JB
40/**
41 * Default measurement statistics requirements.
42 */
43export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS: MeasurementStatisticsRequirements =
44 {
45 aggregate: false,
46 average: false,
47 median: false
48 }
49
51474716 50/**
ab80dc46
JB
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.
53 *
54 * @returns The host OS optimized maximum pool size.
4bffc062 55 * @internal
51474716
JB
56 */
57export const availableParallelism = (): number => {
58 let availableParallelism = 1
59 try {
aa4bf4b2 60 availableParallelism = os.availableParallelism()
51474716 61 } catch {
aa4bf4b2 62 const numberOfCpus = os.cpus()
2845f2a5
JB
63 if (Array.isArray(numberOfCpus) && numberOfCpus.length > 0) {
64 availableParallelism = numberOfCpus.length
51474716
JB
65 }
66 }
67 return availableParallelism
68}
69
68cbdc84
JB
70/**
71 * Sleeps for the given amount of milliseconds.
72 *
73 * @param ms - The amount of milliseconds to sleep.
74 * @returns A promise that resolves after the given amount of milliseconds.
75 */
76export const sleep = async (ms: number): Promise<void> => {
77 await new Promise((resolve) => {
78 setTimeout(resolve, ms)
79 })
80}
81
82/**
83 * Computes the retry delay in milliseconds using an exponential back off algorithm.
84 *
85 * @param retryNumber - The number of retries that have already been attempted
147be6fe 86 * @param delayFactor - The base delay factor in milliseconds
68cbdc84
JB
87 * @returns Delay in milliseconds
88 * @internal
89 */
90export const exponentialDelay = (
91 retryNumber = 0,
147be6fe 92 delayFactor = 100
68cbdc84 93): number => {
147be6fe
JB
94 const delay = Math.pow(2, retryNumber) * delayFactor
95 const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
68cbdc84
JB
96 return delay + randomSum
97}
8990357d 98
dc021bcc
JB
99/**
100 * Computes the average of the given data set.
101 *
102 * @param dataSet - Data set.
103 * @returns The average of the given data set.
104 * @internal
105 */
106export const average = (dataSet: number[]): number => {
107 if (Array.isArray(dataSet) && dataSet.length === 0) {
108 return 0
109 }
110 if (Array.isArray(dataSet) && dataSet.length === 1) {
111 return dataSet[0]
112 }
113 return (
114 dataSet.reduce((accumulator, number) => accumulator + number, 0) /
115 dataSet.length
116 )
117}
118
75de9f41
JB
119/**
120 * Returns the worker type of the given worker.
121 *
122 * @param worker - The worker to get the type of.
123 * @returns The worker type of the given worker.
124 * @internal
125 */
126export const getWorkerType = <Worker extends IWorker>(
127 worker: Worker
128): WorkerType | undefined => {
129 if (worker instanceof ThreadWorker) {
130 return WorkerTypes.thread
131 }
132 if (worker instanceof ClusterWorker) {
133 return WorkerTypes.cluster
134 }
135}
136
137/**
138 * Returns the worker id of the given worker.
139 *
140 * @param worker - The worker to get the id of.
141 * @returns The worker id of the given worker.
142 * @internal
143 */
144export const getWorkerId = <Worker extends IWorker>(
145 worker: Worker
146): number | undefined => {
147 if (worker instanceof ThreadWorker) {
148 return worker.threadId
149 } else if (worker instanceof ClusterWorker) {
150 return worker.id
151 }
152}
153
bbeadd16 154/**
afe0d5bf 155 * Computes the median of the given data set.
78099a15
JB
156 *
157 * @param dataSet - Data set.
158 * @returns The median of the given data set.
4bffc062 159 * @internal
78099a15
JB
160 */
161export const median = (dataSet: number[]): number => {
4a45e8d2
JB
162 if (Array.isArray(dataSet) && dataSet.length === 0) {
163 return 0
164 }
78099a15
JB
165 if (Array.isArray(dataSet) && dataSet.length === 1) {
166 return dataSet[0]
167 }
c6f42dd6
JB
168 const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
169 return (
170 (sortedDataSet[(sortedDataSet.length - 1) >> 1] +
171 sortedDataSet[sortedDataSet.length >> 1]) /
172 2
173 )
78099a15 174}
0d80593b 175
afe0d5bf
JB
176/**
177 * Rounds the given number to the given scale.
64383951 178 * The rounding is done using the "round half away from zero" method.
afe0d5bf
JB
179 *
180 * @param num - The number to round.
181 * @param scale - The scale to round to.
182 * @returns The rounded number.
183 */
184export const round = (num: number, scale = 2): number => {
185 const rounder = Math.pow(10, scale)
186 return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
187}
188
3c653a03
JB
189/**
190 * Is the given object a plain object?
191 *
192 * @param obj - The object to check.
193 * @returns `true` if the given object is a plain object, `false` otherwise.
194 */
0d80593b
JB
195export const isPlainObject = (obj: unknown): boolean =>
196 typeof obj === 'object' &&
197 obj !== null &&
198 obj?.constructor === Object &&
199 Object.prototype.toString.call(obj) === '[object Object]'
59317253
JB
200
201/**
202 * Detects whether the given value is a kill behavior or not.
203 *
204 * @typeParam KB - Which specific KillBehavior type to test against.
205 * @param killBehavior - Which kind of kill behavior to detect.
206 * @param value - Any value.
207 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
4bffc062 208 * @internal
59317253
JB
209 */
210export const isKillBehavior = <KB extends KillBehavior>(
211 killBehavior: KB,
212 value: unknown
213): value is KB => {
214 return value === killBehavior
215}
49d1b48c
JB
216
217/**
218 * Detects whether the given value is an asynchronous function or not.
219 *
220 * @param fn - Any value.
221 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
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
JB
228
229/**
230 * Updates the given measurement statistics.
231 *
232 * @param measurementStatistics - The measurement statistics to update.
233 * @param measurementRequirements - The measurement statistics requirements.
234 * @param measurementValue - The measurement value.
008512c7 235 * @param numberOfMeasurements - The number of measurements.
4bffc062 236 * @internal
e4f20deb
JB
237 */
238export const updateMeasurementStatistics = (
239 measurementStatistics: MeasurementStatistics,
240 measurementRequirements: MeasurementStatisticsRequirements,
dc021bcc 241 measurementValue: number
e4f20deb
JB
242): void => {
243 if (measurementRequirements.aggregate) {
244 measurementStatistics.aggregate =
245 (measurementStatistics.aggregate ?? 0) + measurementValue
246 measurementStatistics.minimum = Math.min(
247 measurementValue,
248 measurementStatistics.minimum ?? Infinity
249 )
250 measurementStatistics.maximum = Math.max(
251 measurementValue,
252 measurementStatistics.maximum ?? -Infinity
253 )
dc021bcc
JB
254 if (
255 (measurementRequirements.average || measurementRequirements.median) &&
256 measurementValue != null
257 ) {
e4f20deb 258 measurementStatistics.history.push(measurementValue)
dc021bcc
JB
259 if (measurementRequirements.average) {
260 measurementStatistics.average = average(measurementStatistics.history)
261 }
262 if (measurementRequirements.median) {
263 measurementStatistics.median = median(measurementStatistics.history)
264 }
e4f20deb
JB
265 }
266 }
267}
c3f0a074
JB
268
269/**
270 * Executes a function once at a time.
271 *
272 * @param fn - The function to execute.
273 * @param context - The context to bind the function to.
274 * @returns The function to execute.
275 */
276export const once = (
277 // eslint-disable-next-line @typescript-eslint/no-explicit-any
278 fn: (...args: any[]) => void,
f7426dd9 279 context: unknown
c3f0a074
JB
280 // eslint-disable-next-line @typescript-eslint/no-explicit-any
281): ((...args: any[]) => void) => {
282 let called = false
283 // eslint-disable-next-line @typescript-eslint/no-explicit-any
284 return function (...args: any[]): void {
285 if (!called) {
286 called = true
287 fn.apply(context, args)
288 called = false
289 }
290 }
291}
68cbdc84
JB
292
293/**
294 * Generate a cryptographically secure random number in the [0,1[ range
295 *
296 * @returns A number in the [0,1[ range
297 */
970b38d6 298export const secureRandom = (): number => {
98446b39 299 return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000
68cbdc84 300}