perf: add dynamic thread pool to continuous benchmark
[poolifier.git] / benchmarks / benchmarks-utils.mjs
1 import crypto from 'node:crypto'
2 import assert from 'node:assert'
3 import fs from 'node:fs'
4 import Benchmark from 'benchmark'
5 import {
6 DynamicClusterPool,
7 DynamicThreadPool,
8 FixedClusterPool,
9 FixedThreadPool,
10 PoolTypes,
11 WorkerChoiceStrategies,
12 WorkerTypes
13 } from '../lib/index.mjs'
14 import { TaskFunctions } from './benchmarks-types.mjs'
15
16 export const buildPoolifierPool = (
17 workerType,
18 poolType,
19 poolSize,
20 poolOptions
21 ) => {
22 switch (poolType) {
23 case PoolTypes.fixed:
24 switch (workerType) {
25 case WorkerTypes.thread:
26 return new FixedThreadPool(
27 poolSize,
28 './benchmarks/internal/thread-worker.mjs',
29 poolOptions
30 )
31 case WorkerTypes.cluster:
32 return new FixedClusterPool(
33 poolSize,
34 './benchmarks/internal/cluster-worker.mjs',
35 poolOptions
36 )
37 }
38 break
39 case PoolTypes.dynamic:
40 switch (workerType) {
41 case WorkerTypes.thread:
42 return new DynamicThreadPool(
43 Math.floor(poolSize / 2),
44 poolSize,
45 './benchmarks/internal/thread-worker.mjs',
46 poolOptions
47 )
48 case WorkerTypes.cluster:
49 return new DynamicClusterPool(
50 Math.floor(poolSize / 2),
51 poolSize,
52 './benchmarks/internal/cluster-worker.mjs',
53 poolOptions
54 )
55 }
56 break
57 }
58 }
59
60 export const runPoolifierPool = async (
61 pool,
62 { taskExecutions, workerData }
63 ) => {
64 return await new Promise((resolve, reject) => {
65 let executions = 0
66 for (let i = 1; i <= taskExecutions; i++) {
67 pool
68 .execute(workerData)
69 .then(() => {
70 ++executions
71 if (executions === taskExecutions) {
72 resolve({ ok: 1 })
73 }
74 return null
75 })
76 .catch(err => {
77 console.error(err)
78 reject(err)
79 })
80 }
81 })
82 }
83
84 export const runPoolifierPoolBenchmark = async (
85 name,
86 pool,
87 { taskExecutions, workerData }
88 ) => {
89 return await new Promise((resolve, reject) => {
90 try {
91 const suite = new Benchmark.Suite(name)
92 for (const workerChoiceStrategy of Object.values(
93 WorkerChoiceStrategies
94 )) {
95 for (const enableTasksQueue of [false, true]) {
96 suite.add(
97 `${name}|${workerChoiceStrategy}|${
98 enableTasksQueue ? 'with' : 'without'
99 } tasks queue`,
100 async () => {
101 pool.setWorkerChoiceStrategy(workerChoiceStrategy)
102 pool.enableTasksQueue(enableTasksQueue)
103 assert.strictEqual(
104 pool.opts.workerChoiceStrategy,
105 workerChoiceStrategy
106 )
107 assert.strictEqual(pool.opts.enableTasksQueue, enableTasksQueue)
108 await runPoolifierPool(pool, {
109 taskExecutions,
110 workerData
111 })
112 }
113 )
114 }
115 }
116 suite
117 .on('cycle', event => {
118 console.info(event.target.toString())
119 })
120 .on('complete', async function () {
121 console.info(
122 'Fastest is ' +
123 LIST_FORMATTER.format(this.filter('fastest').map('name'))
124 )
125 await pool.destroy()
126 resolve()
127 })
128 .run({ async: true })
129 } catch (error) {
130 reject(error)
131 }
132 })
133 }
134
135 export const LIST_FORMATTER = new Intl.ListFormat('en-US', {
136 style: 'long',
137 type: 'conjunction'
138 })
139
140 export const executeAsyncFn = async fn => {
141 try {
142 await fn()
143 } catch (e) {
144 console.error(e)
145 // eslint-disable-next-line n/no-process-exit
146 process.exit(1)
147 }
148 }
149
150 export const generateRandomInteger = (
151 max = Number.MAX_SAFE_INTEGER,
152 min = 0
153 ) => {
154 if (max < min || max < 0 || min < 0) {
155 throw new RangeError('Invalid interval')
156 }
157 max = Math.floor(max)
158 if (min != null && min !== 0) {
159 min = Math.ceil(min)
160 return Math.floor(Math.random() * (max - min + 1)) + min
161 }
162 return Math.floor(Math.random() * (max + 1))
163 }
164
165 const jsonIntegerSerialization = n => {
166 for (let i = 0; i < n; i++) {
167 const o = {
168 a: i
169 }
170 JSON.stringify(o)
171 }
172 return { ok: 1 }
173 }
174
175 /**
176 * Intentionally inefficient implementation.
177 * @param {number} n - The number of fibonacci numbers to generate.
178 * @returns {number} - The nth fibonacci number.
179 */
180 const fibonacci = n => {
181 if (n <= 1) return n
182 return fibonacci(n - 1) + fibonacci(n - 2)
183 }
184
185 /**
186 * Intentionally inefficient implementation.
187 * @param {number} n - The number to calculate the factorial of.
188 * @returns {number} - The factorial of n.
189 */
190 const factorial = n => {
191 if (n === 0) {
192 return 1
193 }
194 return factorial(n - 1) * n
195 }
196
197 const readWriteFiles = (
198 n,
199 baseDirectory = `/tmp/poolifier-benchmarks/${crypto.randomInt(
200 281474976710655
201 )}`
202 ) => {
203 if (fs.existsSync(baseDirectory) === true) {
204 fs.rmSync(baseDirectory, { recursive: true })
205 }
206 fs.mkdirSync(baseDirectory, { recursive: true })
207 for (let i = 0; i < n; i++) {
208 const filePath = `${baseDirectory}/${i}`
209 fs.writeFileSync(filePath, i.toString(), {
210 encoding: 'utf8',
211 flag: 'a'
212 })
213 fs.readFileSync(filePath, 'utf8')
214 }
215 fs.rmSync(baseDirectory, { recursive: true })
216 return { ok: 1 }
217 }
218
219 export const executeTaskFunction = data => {
220 switch (data.function) {
221 case TaskFunctions.jsonIntegerSerialization:
222 return jsonIntegerSerialization(data.taskSize || 1000)
223 case TaskFunctions.fibonacci:
224 return fibonacci(data.taskSize || 1000)
225 case TaskFunctions.factorial:
226 return factorial(data.taskSize || 1000)
227 case TaskFunctions.readWriteFiles:
228 return readWriteFiles(data.taskSize || 1000)
229 default:
230 throw new Error('Unknown task function')
231 }
232 }