Merge branch 'master' of github.com:poolifier/poolifier
[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 // * Computes the retry delay in milliseconds using an exponential back off algorithm.
64 // *
65 // * @param retryNumber - The number of retries that have already been attempted
66 // * @param maxDelayRatio - The maximum ratio of the delay that can be randomized
67 // * @returns Delay in milliseconds
68 // * @internal
69 // */
70 // export const exponentialDelay = (
71 // retryNumber = 0,
72 // maxDelayRatio = 0.2
73 // ): number => {
74 // const delay = Math.pow(2, retryNumber) * 100
75 // const randomSum = delay * maxDelayRatio * Math.random() // 0-(maxDelayRatio*100)% of the delay
76 // return delay + randomSum
77 // }
78
79 /**
80 * Computes the median of the given data set.
81 *
82 * @param dataSet - Data set.
83 * @returns The median of the given data set.
84 * @internal
85 */
86 export const median = (dataSet: number[]): number => {
87 if (Array.isArray(dataSet) && dataSet.length === 0) {
88 return 0
89 }
90 if (Array.isArray(dataSet) && dataSet.length === 1) {
91 return dataSet[0]
92 }
93 const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
94 return (
95 (sortedDataSet[(sortedDataSet.length - 1) >> 1] +
96 sortedDataSet[sortedDataSet.length >> 1]) /
97 2
98 )
99 }
100
101 /**
102 * Rounds the given number to the given scale.
103 * The rounding is done using the "round half away from zero" method.
104 *
105 * @param num - The number to round.
106 * @param scale - The scale to round to.
107 * @returns The rounded number.
108 */
109 export const round = (num: number, scale = 2): number => {
110 const rounder = Math.pow(10, scale)
111 return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
112 }
113
114 /**
115 * Is the given object a plain object?
116 *
117 * @param obj - The object to check.
118 * @returns `true` if the given object is a plain object, `false` otherwise.
119 */
120 export const isPlainObject = (obj: unknown): boolean =>
121 typeof obj === 'object' &&
122 obj !== null &&
123 obj?.constructor === Object &&
124 Object.prototype.toString.call(obj) === '[object Object]'
125
126 /**
127 * Detects whether the given value is a kill behavior or not.
128 *
129 * @typeParam KB - Which specific KillBehavior type to test against.
130 * @param killBehavior - Which kind of kill behavior to detect.
131 * @param value - Any value.
132 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
133 * @internal
134 */
135 export const isKillBehavior = <KB extends KillBehavior>(
136 killBehavior: KB,
137 value: unknown
138 ): value is KB => {
139 return value === killBehavior
140 }
141
142 /**
143 * Detects whether the given value is an asynchronous function or not.
144 *
145 * @param fn - Any value.
146 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
147 */
148 export const isAsyncFunction = (
149 fn: unknown
150 ): fn is (...args: unknown[]) => Promise<unknown> => {
151 return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'
152 }
153
154 /**
155 * Updates the given measurement statistics.
156 *
157 * @param measurementStatistics - The measurement statistics to update.
158 * @param measurementRequirements - The measurement statistics requirements.
159 * @param measurementValue - The measurement value.
160 * @param numberOfMeasurements - The number of measurements.
161 * @internal
162 */
163 export const updateMeasurementStatistics = (
164 measurementStatistics: MeasurementStatistics,
165 measurementRequirements: MeasurementStatisticsRequirements,
166 measurementValue: number,
167 numberOfMeasurements: number
168 ): void => {
169 if (measurementRequirements.aggregate) {
170 measurementStatistics.aggregate =
171 (measurementStatistics.aggregate ?? 0) + measurementValue
172 measurementStatistics.minimum = Math.min(
173 measurementValue,
174 measurementStatistics.minimum ?? Infinity
175 )
176 measurementStatistics.maximum = Math.max(
177 measurementValue,
178 measurementStatistics.maximum ?? -Infinity
179 )
180 if (measurementRequirements.average && numberOfMeasurements !== 0) {
181 measurementStatistics.average =
182 measurementStatistics.aggregate / numberOfMeasurements
183 }
184 if (measurementRequirements.median && measurementValue != null) {
185 measurementStatistics.history.push(measurementValue)
186 measurementStatistics.median = median(measurementStatistics.history)
187 }
188 }
189 }
190
191 /**
192 * Executes a function once at a time.
193 *
194 * @param fn - The function to execute.
195 * @param context - The context to bind the function to.
196 * @returns The function to execute.
197 */
198 export const once = (
199 // eslint-disable-next-line @typescript-eslint/no-explicit-any
200 fn: (...args: any[]) => void,
201 context: unknown
202 // eslint-disable-next-line @typescript-eslint/no-explicit-any
203 ): ((...args: any[]) => void) => {
204 let called = false
205 // eslint-disable-next-line @typescript-eslint/no-explicit-any
206 return function (...args: any[]): void {
207 if (!called) {
208 called = true
209 fn.apply(context, args)
210 called = false
211 }
212 }
213 }