Merge branch 'master' of github.com:jerome-benoit/poolifier
[poolifier.git] / src / utils.ts
... / ...
CommitLineData
1import * as os from 'node:os'
2import { webcrypto } from 'node:crypto'
3import { Worker as ClusterWorker } from 'node:cluster'
4import { Worker as ThreadWorker } from 'node:worker_threads'
5import type {
6 MeasurementStatisticsRequirements,
7 WorkerChoiceStrategyOptions
8} from './pools/selection-strategies/selection-strategies-types'
9import type { KillBehavior } from './worker/worker-options'
10import {
11 type IWorker,
12 type MeasurementStatistics,
13 type WorkerType,
14 WorkerTypes
15} from './pools/worker'
16
17/**
18 * Default task name.
19 */
20export const DEFAULT_TASK_NAME = 'default'
21
22/**
23 * An intentional empty function.
24 */
25export const EMPTY_FUNCTION: () => void = Object.freeze(() => {
26 /* Intentionally empty */
27})
28
29/**
30 * Default worker choice strategy options.
31 */
32export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions =
33 {
34 retries: 6,
35 runTime: { median: false },
36 waitTime: { median: false },
37 elu: { median: false }
38 }
39
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
50/**
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.
55 * @internal
56 */
57export const availableParallelism = (): number => {
58 let availableParallelism = 1
59 try {
60 availableParallelism = os.availableParallelism()
61 } catch {
62 const cpus = os.cpus()
63 if (Array.isArray(cpus) && cpus.length > 0) {
64 availableParallelism = cpus.length
65 }
66 }
67 return availableParallelism
68}
69
70/**
71 * Returns the worker type of the given worker.
72 *
73 * @param worker - The worker to get the type of.
74 * @returns The worker type of the given worker.
75 * @internal
76 */
77export const getWorkerType = <Worker extends IWorker>(
78 worker: Worker
79): WorkerType | undefined => {
80 if (worker instanceof ThreadWorker) {
81 return WorkerTypes.thread
82 } else if (worker instanceof ClusterWorker) {
83 return WorkerTypes.cluster
84 }
85}
86
87/**
88 * Returns the worker id of the given worker.
89 *
90 * @param worker - The worker to get the id of.
91 * @returns The worker id of the given worker.
92 * @internal
93 */
94export const getWorkerId = <Worker extends IWorker>(
95 worker: Worker
96): number | undefined => {
97 if (worker instanceof ThreadWorker) {
98 return worker.threadId
99 } else if (worker instanceof ClusterWorker) {
100 return worker.id
101 }
102}
103
104/**
105 * Sleeps for the given amount of milliseconds.
106 *
107 * @param ms - The amount of milliseconds to sleep.
108 * @returns A promise that resolves after the given amount of milliseconds.
109 */
110export const sleep = async (ms: number): Promise<void> => {
111 await new Promise((resolve) => {
112 setTimeout(resolve, ms)
113 })
114}
115
116/**
117 * Computes the retry delay in milliseconds using an exponential back off algorithm.
118 *
119 * @param retryNumber - The number of retries that have already been attempted
120 * @param delayFactor - The base delay factor in milliseconds
121 * @returns Delay in milliseconds
122 * @internal
123 */
124export const exponentialDelay = (
125 retryNumber = 0,
126 delayFactor = 100
127): number => {
128 const delay = Math.pow(2, retryNumber) * delayFactor
129 const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
130 return delay + randomSum
131}
132
133/**
134 * Computes the average of the given data set.
135 *
136 * @param dataSet - Data set.
137 * @returns The average of the given data set.
138 * @internal
139 */
140export const average = (dataSet: number[]): number => {
141 if (Array.isArray(dataSet) && dataSet.length === 0) {
142 return 0
143 }
144 if (Array.isArray(dataSet) && dataSet.length === 1) {
145 return dataSet[0]
146 }
147 return (
148 dataSet.reduce((accumulator, number) => accumulator + number, 0) /
149 dataSet.length
150 )
151}
152
153/**
154 * Computes the median of the given data set.
155 *
156 * @param dataSet - Data set.
157 * @returns The median of the given data set.
158 * @internal
159 */
160export const median = (dataSet: number[]): number => {
161 if (Array.isArray(dataSet) && dataSet.length === 0) {
162 return 0
163 }
164 if (Array.isArray(dataSet) && dataSet.length === 1) {
165 return dataSet[0]
166 }
167 const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
168 return (
169 (sortedDataSet[(sortedDataSet.length - 1) >> 1] +
170 sortedDataSet[sortedDataSet.length >> 1]) /
171 2
172 )
173}
174
175/**
176 * Rounds the given number to the given scale.
177 * The rounding is done using the "round half away from zero" method.
178 *
179 * @param num - The number to round.
180 * @param scale - The scale to round to.
181 * @returns The rounded number.
182 */
183export const round = (num: number, scale = 2): number => {
184 const rounder = Math.pow(10, scale)
185 return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
186}
187
188/**
189 * Is the given object a plain object?
190 *
191 * @param obj - The object to check.
192 * @returns `true` if the given object is a plain object, `false` otherwise.
193 */
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]'
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`.
207 * @internal
208 */
209export const isKillBehavior = <KB extends KillBehavior>(
210 killBehavior: KB,
211 value: unknown
212): value is KB => {
213 return value === killBehavior
214}
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`.
221 */
222export const isAsyncFunction = (
223 fn: unknown
224): fn is (...args: unknown[]) => Promise<unknown> => {
225 return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'
226}
227
228/**
229 * Updates the given measurement statistics.
230 *
231 * @param measurementStatistics - The measurement statistics to update.
232 * @param measurementRequirements - The measurement statistics requirements.
233 * @param measurementValue - The measurement value.
234 * @param numberOfMeasurements - The number of measurements.
235 * @internal
236 */
237export const updateMeasurementStatistics = (
238 measurementStatistics: MeasurementStatistics,
239 measurementRequirements: MeasurementStatisticsRequirements,
240 measurementValue: number
241): void => {
242 if (measurementRequirements.aggregate) {
243 measurementStatistics.aggregate =
244 (measurementStatistics.aggregate ?? 0) + measurementValue
245 measurementStatistics.minimum = Math.min(
246 measurementValue,
247 measurementStatistics.minimum ?? Infinity
248 )
249 measurementStatistics.maximum = Math.max(
250 measurementValue,
251 measurementStatistics.maximum ?? -Infinity
252 )
253 if (
254 (measurementRequirements.average || measurementRequirements.median) &&
255 measurementValue != null
256 ) {
257 measurementStatistics.history.push(measurementValue)
258 if (measurementRequirements.average) {
259 measurementStatistics.average = average(measurementStatistics.history)
260 } else if (measurementStatistics.average != null) {
261 delete measurementStatistics.average
262 }
263 if (measurementRequirements.median) {
264 measurementStatistics.median = median(measurementStatistics.history)
265 } else if (measurementStatistics.median != null) {
266 delete measurementStatistics.median
267 }
268 }
269 }
270}
271
272/**
273 * Generate a cryptographically secure random number in the [0,1[ range
274 *
275 * @returns A number in the [0,1[ range
276 */
277export const secureRandom = (): number => {
278 return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000
279}