refactor: cleanup exponential delay code
[poolifier.git] / src / utils.ts
1 import * as os from 'node:os'
2 import type {
3 MeasurementStatisticsRequirements,
4 WorkerChoiceStrategyOptions
5 } from './pools/selection-strategies/selection-strategies-types'
6 import type { KillBehavior } from './worker/worker-options'
7 import type { MeasurementStatistics } from './pools/worker'
8
9 /**
10 * Default task name.
11 */
12 export const DEFAULT_TASK_NAME = 'default'
13
14 /**
15 * An intentional empty function.
16 */
17 export const EMPTY_FUNCTION: () => void = Object.freeze(() => {
18 /* Intentionally empty */
19 })
20
21 /**
22 * Default worker choice strategy options.
23 */
24 export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions =
25 {
26 choiceRetries: 6,
27 runTime: { median: false },
28 waitTime: { median: false },
29 elu: { median: false }
30 }
31
32 /**
33 * Default measurement statistics requirements.
34 */
35 export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS: MeasurementStatisticsRequirements =
36 {
37 aggregate: false,
38 average: false,
39 median: false
40 }
41
42 /**
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.
47 * @internal
48 */
49 export const availableParallelism = (): number => {
50 let availableParallelism = 1
51 try {
52 availableParallelism = os.availableParallelism()
53 } catch {
54 const numberOfCpus = os.cpus()
55 if (Array.isArray(numberOfCpus) && numberOfCpus.length > 0) {
56 availableParallelism = numberOfCpus.length
57 }
58 }
59 return availableParallelism
60 }
61
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 */
68 export 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
78 * @param delayFactor - The base delay factor in milliseconds
79 * @returns Delay in milliseconds
80 * @internal
81 */
82 export const exponentialDelay = (
83 retryNumber = 0,
84 delayFactor = 100
85 ): number => {
86 const delay = Math.pow(2, retryNumber) * delayFactor
87 const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
88 return delay + randomSum
89 }
90
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 */
98 export 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
111 /**
112 * Computes the median of the given data set.
113 *
114 * @param dataSet - Data set.
115 * @returns The median of the given data set.
116 * @internal
117 */
118 export const median = (dataSet: number[]): number => {
119 if (Array.isArray(dataSet) && dataSet.length === 0) {
120 return 0
121 }
122 if (Array.isArray(dataSet) && dataSet.length === 1) {
123 return dataSet[0]
124 }
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 )
131 }
132
133 /**
134 * Rounds the given number to the given scale.
135 * The rounding is done using the "round half away from zero" method.
136 *
137 * @param num - The number to round.
138 * @param scale - The scale to round to.
139 * @returns The rounded number.
140 */
141 export 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
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 */
152 export const isPlainObject = (obj: unknown): boolean =>
153 typeof obj === 'object' &&
154 obj !== null &&
155 obj?.constructor === Object &&
156 Object.prototype.toString.call(obj) === '[object Object]'
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`.
165 * @internal
166 */
167 export const isKillBehavior = <KB extends KillBehavior>(
168 killBehavior: KB,
169 value: unknown
170 ): value is KB => {
171 return value === killBehavior
172 }
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 */
180 export const isAsyncFunction = (
181 fn: unknown
182 ): fn is (...args: unknown[]) => Promise<unknown> => {
183 return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'
184 }
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.
192 * @param numberOfMeasurements - The number of measurements.
193 * @internal
194 */
195 export const updateMeasurementStatistics = (
196 measurementStatistics: MeasurementStatistics,
197 measurementRequirements: MeasurementStatisticsRequirements,
198 measurementValue: number
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 )
211 if (
212 (measurementRequirements.average || measurementRequirements.median) &&
213 measurementValue != null
214 ) {
215 measurementStatistics.history.push(measurementValue)
216 if (measurementRequirements.average) {
217 measurementStatistics.average = average(measurementStatistics.history)
218 }
219 if (measurementRequirements.median) {
220 measurementStatistics.median = median(measurementStatistics.history)
221 }
222 }
223 }
224 }
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 */
233 export const once = (
234 // eslint-disable-next-line @typescript-eslint/no-explicit-any
235 fn: (...args: any[]) => void,
236 context: unknown
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 }
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 */
255 const secureRandom = (): number => {
256 return crypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000
257 }