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