Add benchmark for quick select algorithm evaluation. (#255)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sat, 6 Mar 2021 19:22:31 +0000 (20:22 +0100)
committerGitHub <noreply@github.com>
Sat, 6 Mar 2021 19:22:31 +0000 (20:22 +0100)
.eslintrc.js
README.md
benchmarks/README.md
benchmarks/internal/benchmark-utils.js
benchmarks/internal/cluster/dynamic.js
benchmarks/internal/cluster/fixed.js
benchmarks/internal/quick-select.js [new file with mode: 0644]
benchmarks/internal/thread/dynamic.js
benchmarks/internal/thread/fixed.js
benchmarks/versus-external-pools/bench.sh

index 10597e776699b03b200b20a14e1881cb28256ef5..850110e874a0b8d0bc67d997ece11e7b12f0342e 100644 (file)
@@ -52,6 +52,7 @@ module.exports = {
       {
         skipWords: [
           'christopher',
+          'comparator',
           'ecma',
           'enum',
           'inheritdoc',
index 7bb71d8033c84f7b5fdd26732286f90139039675..1ddad503512fa3c8f3e2107006c9ca16cf165128 100644 (file)
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@
     <img alt="No dependencies" src="https://img.shields.io/static/v1?label=dependencies&message=no%20dependencies&color=brightgreen"></a>
 </p>
 
-## Why Poolifier? 
+## Why Poolifier?
 
 Poolifier is used to perform CPU intensive and I/O intensive tasks on nodejs servers, it implements worker pools (yes, more worker pool implementations, so you can choose which one fit better for you) using [worker-threads](https://nodejs.org/api/worker_threads.html#worker_threads_worker_threads) and cluster pools using [Node.js cluster](https://nodejs.org/api/cluster.html) modules.  
 With poolifier you can improve your **performance** and resolve problems related to the event loop.  
index d368e86121a2d57cfcac3ae825100ff1d0022360..61b57518fecd5346ae814c8b90dd25bb82e89eef 100644 (file)
@@ -19,7 +19,7 @@ External pools with which we compared the poolifier results:
 
 Those are our results:
 
-- CPU Intensive task with 100k operations submitted to each pool [BENCH-100000.MD](./versus-external-pools/BENCH-100000.MD).  
+- CPU Intensive task with 100k operations submitted to each pool [BENCH-100000.md](./versus-external-pools/BENCH-100000.md).  
   This benchmark ran on a MacBook Pro 2015, 2,2 GHz Intel Core i7 quad-core, 16 GB 1600 MHz DDR3.
 
 > :warning: **We would need funds to run our benchmarks more often and on Cloud VMs, please consider to sponsor this project**
index f84227d3ff01b52ec21335e2cf88fcd880325dee..cbf69307cdad7e5b07f5b8998099700ffafba2f7 100644 (file)
@@ -1,4 +1,4 @@
-async function runTest (pool, { tasks, workerData }) {
+async function runPoolifierTest (pool, { tasks, workerData }) {
   return new Promise((resolve, reject) => {
     let executions = 0
     for (let i = 0; i <= tasks; i++) {
@@ -16,4 +16,11 @@ async function runTest (pool, { tasks, workerData }) {
   })
 }
 
-module.exports = { runTest }
+function generateRandomInteger (max, min = 0) {
+  if (min) {
+    return Math.floor(Math.random() * (max - min + 1) + min)
+  }
+  return Math.floor(Math.random() * max + 1)
+}
+
+module.exports = { runPoolifierTest, generateRandomInteger }
index 7ad9dd282d5e27a7afcdcd10e9a0d4c860e4e75c..6838e07a9cad0e882f62c68d8d890f5ec4a003ea 100644 (file)
@@ -2,7 +2,7 @@ const {
   DynamicClusterPool,
   WorkerChoiceStrategies
 } = require('../../../lib/index')
-const { runTest } = require('../benchmark-utils')
+const { runPoolifierTest } = require('../benchmark-utils')
 
 const size = 30
 
@@ -22,13 +22,13 @@ const dynamicPoolLessRecentlyUsed = new DynamicClusterPool(
 async function dynamicClusterTest (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(dynamicPool, { tasks, workerData })
+  return runPoolifierTest(dynamicPool, { tasks, workerData })
 }
 
 async function dynamicClusterTestLessRecentlyUsed (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(dynamicPoolLessRecentlyUsed, { tasks, workerData })
+  return runPoolifierTest(dynamicPoolLessRecentlyUsed, { tasks, workerData })
 }
 
 module.exports = {
index 60a5b938f8a5a6f46fc474a1e0e115152dba05e0..62c19aabf87fad362973abff99f2f390a8015712 100644 (file)
@@ -1,5 +1,5 @@
 const { FixedClusterPool } = require('../../../lib/index')
-const { runTest } = require('../benchmark-utils')
+const { runPoolifierTest } = require('../benchmark-utils')
 
 const size = 30
 
@@ -11,7 +11,7 @@ const fixedPool = new FixedClusterPool(
 async function fixedClusterTest (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(fixedPool, { tasks, workerData })
+  return runPoolifierTest(fixedPool, { tasks, workerData })
 }
 
 module.exports = { fixedClusterTest }
diff --git a/benchmarks/internal/quick-select.js b/benchmarks/internal/quick-select.js
new file mode 100644 (file)
index 0000000..dcebaaf
--- /dev/null
@@ -0,0 +1,233 @@
+const Benchmark = require('benchmark')
+const { generateRandomInteger } = require('./benchmark-utils')
+
+const suite = new Benchmark.Suite()
+
+const LIST_FORMATTER = new Intl.ListFormat('en-US', {
+  style: 'long',
+  type: 'conjunction'
+})
+
+const tasksMap = new Map([
+  [0, generateRandomInteger(10)],
+  [1, generateRandomInteger(10)],
+  [2, generateRandomInteger(10)],
+  [3, generateRandomInteger(10)],
+  [4, generateRandomInteger(10)],
+  [5, generateRandomInteger(10)],
+  [6, generateRandomInteger(10)],
+  [7, generateRandomInteger(10)],
+  [8, generateRandomInteger(10)],
+  [9, generateRandomInteger(10)],
+  [10, generateRandomInteger(10)],
+  [11, generateRandomInteger(10)],
+  [12, generateRandomInteger(10)],
+  [13, generateRandomInteger(10)],
+  [14, generateRandomInteger(10)],
+  [15, generateRandomInteger(10)],
+  [16, generateRandomInteger(10)],
+  [17, generateRandomInteger(10)],
+  [18, generateRandomInteger(10)],
+  [19, generateRandomInteger(10)],
+  [20, generateRandomInteger(10)],
+  [21, generateRandomInteger(10)],
+  [22, generateRandomInteger(10)],
+  [23, generateRandomInteger(10)],
+  [24, generateRandomInteger(10)],
+  [25, generateRandomInteger(10)],
+  [26, generateRandomInteger(10)],
+  [27, generateRandomInteger(10)],
+  [28, generateRandomInteger(10)],
+  [29, generateRandomInteger(10)]
+])
+
+function loopSelect (tasksMap) {
+  let minValue = Infinity
+  let minKey
+  for (const [key, value] of tasksMap) {
+    if (value === 0) {
+      return key
+    } else if (value < minValue) {
+      minKey = key
+      minValue = value
+    }
+  }
+  return [minKey, minValue]
+}
+
+function arraySortSelect (tasksMap) {
+  const tasksArray = Array.from(tasksMap)
+  return tasksArray.sort((a, b) => {
+    if (a[1] < b[1]) {
+      return -1
+    } else if (a[1] > b[1]) {
+      return 1
+    }
+    return 0
+  })[0]
+}
+
+const defaultComparator = (a, b) => {
+  return a < b
+}
+
+const defaultPivotIndexSelect = (leftIndex, rightIndex) => {
+  return leftIndex + Math.floor((rightIndex - leftIndex) / 2)
+}
+
+function swap (array, index1, index2) {
+  const tmp = array[index1]
+  array[index1] = array[index2]
+  array[index2] = tmp
+}
+
+function partition (
+  array,
+  leftIndex,
+  rightIndex,
+  pivotIndex,
+  compare = defaultComparator
+) {
+  const pivotValue = array[pivotIndex]
+  swap(array, pivotIndex, rightIndex)
+  let storeIndex = leftIndex
+  for (let i = leftIndex; i < rightIndex; i += 1) {
+    if (compare(array[i], pivotValue)) {
+      swap(array, storeIndex, i)
+      storeIndex += 1
+    }
+  }
+  swap(array, rightIndex, storeIndex)
+  return storeIndex
+}
+
+function selectLoop (
+  array,
+  k,
+  leftIndex,
+  rightIndex,
+  compare = defaultComparator,
+  pivotIndexSelect = defaultPivotIndexSelect
+) {
+  while (true) {
+    if (leftIndex === rightIndex) return array[leftIndex]
+    let pivotIndex = pivotIndexSelect(leftIndex, rightIndex)
+    pivotIndex = partition(array, leftIndex, rightIndex, pivotIndex, compare)
+    if (k === pivotIndex) {
+      return array[k]
+    } else if (k < pivotIndex) {
+      rightIndex = pivotIndex - 1
+    } else {
+      leftIndex = pivotIndex + 1
+    }
+  }
+}
+
+function selectRecursion (
+  array,
+  k,
+  leftIndex,
+  rightIndex,
+  compare = defaultComparator,
+  pivotIndexSelect = defaultPivotIndexSelect
+) {
+  if (leftIndex === rightIndex) return array[leftIndex]
+  let pivotIndex = pivotIndexSelect(leftIndex, rightIndex)
+  pivotIndex = partition(array, leftIndex, rightIndex, pivotIndex, compare)
+  if (k === pivotIndex) {
+    return array[k]
+  } else if (k < pivotIndex) {
+    return selectRecursion(array, k, leftIndex, pivotIndex - 1, compare)
+  } else {
+    return selectRecursion(array, k, pivotIndex + 1, rightIndex, k, compare)
+  }
+}
+
+function quickSelectLoop (tasksMap) {
+  const tasksArray = Array.from(tasksMap)
+
+  return selectLoop(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => {
+    return a[1] < b[1]
+  })
+}
+
+function quickSelectLoopRandomPivot (tasksMap) {
+  const tasksArray = Array.from(tasksMap)
+
+  return selectLoop(
+    tasksArray,
+    0,
+    0,
+    tasksArray.length - 1,
+    (a, b) => {
+      return a[1] < b[1]
+    },
+    (leftIndex, rightIndex) => {
+      return generateRandomInteger(leftIndex, rightIndex)
+    }
+  )
+}
+
+function quickSelectRecursion (tasksMap) {
+  const tasksArray = Array.from(tasksMap)
+
+  return selectRecursion(tasksArray, 0, 0, tasksArray.length - 1, (a, b) => {
+    return a[1] < b[1]
+  })
+}
+
+function quickSelectRecursionRandomPivot (tasksMap) {
+  const tasksArray = Array.from(tasksMap)
+
+  return selectRecursion(
+    tasksArray,
+    0,
+    0,
+    tasksArray.length - 1,
+    (a, b) => {
+      return a[1] < b[1]
+    },
+    (leftIndex, rightIndex) => {
+      return generateRandomInteger(leftIndex, rightIndex)
+    }
+  )
+}
+
+// console.log(Array.from(tasksMap))
+// console.log(loopSelect(tasksMap))
+// console.log(arraySortSelect(tasksMap))
+// console.log(quickSelectLoop(tasksMap))
+// console.log(quickSelectLoopRandomPivot(tasksMap))
+// console.log(quickSelectRecursion(tasksMap))
+// console.log(quickSelectRecursionRandomPivot(tasksMap))
+
+suite
+  .add('Loop select', function () {
+    loopSelect(tasksMap)
+  })
+  .add('Array sort select', function () {
+    arraySortSelect(tasksMap)
+  })
+  .add('Quick select loop', function () {
+    quickSelectLoop(tasksMap)
+  })
+  .add('Quick select loop with random pivot', function () {
+    quickSelectLoopRandomPivot(tasksMap)
+  })
+  .add('Quick select recursion', function () {
+    quickSelectRecursion(tasksMap)
+  })
+  .add('Quick select recursion with random pivot', function () {
+    quickSelectRecursionRandomPivot(tasksMap)
+  })
+  .on('cycle', function (event) {
+    console.log(event.target.toString())
+  })
+  .on('complete', function () {
+    console.log(
+      'Fastest is ' + LIST_FORMATTER.format(this.filter('fastest').map('name'))
+    )
+    // eslint-disable-next-line no-process-exit
+    process.exit()
+  })
+  .run()
index bc9b3d8194e91e14305419eb2265bc4bced929b3..fb74d8985d8d6dd85ba2c88e3f7c327526003c35 100644 (file)
@@ -2,7 +2,7 @@ const {
   DynamicThreadPool,
   WorkerChoiceStrategies
 } = require('../../../lib/index')
-const { runTest } = require('../benchmark-utils')
+const { runPoolifierTest } = require('../benchmark-utils')
 
 const size = 30
 
@@ -16,19 +16,19 @@ const dynamicPoolLessRecentlyUsed = new DynamicThreadPool(
   size / 2,
   size * 3,
   './benchmarks/internal/thread/worker.js',
-  { workerChoiceStrategy: DynamicThreadPool.LESS_RECENTLY_USED }
+  { workerChoiceStrategy: WorkerChoiceStrategies.LESS_RECENTLY_USED }
 )
 
 async function dynamicThreadTest (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(dynamicPool, { tasks, workerData })
+  return runPoolifierTest(dynamicPool, { tasks, workerData })
 }
 
 async function dynamicThreadTestLessRecentlyUsed (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(dynamicPoolLessRecentlyUsed, { tasks, workerData })
+  return runPoolifierTest(dynamicPoolLessRecentlyUsed, { tasks, workerData })
 }
 
 module.exports = {
index 7ad60eeb3cf0b5b5ff1b3601b725f4adf58a6469..966fc0e19ce42bae944a3ea8a4f2484e64870dc1 100644 (file)
@@ -1,5 +1,5 @@
 const { FixedThreadPool } = require('../../../lib/index')
-const { runTest } = require('../benchmark-utils')
+const { runPoolifierTest } = require('../benchmark-utils')
 
 const size = 30
 
@@ -11,7 +11,7 @@ const fixedPool = new FixedThreadPool(
 async function fixedThreadTest (
   { tasks, workerData } = { tasks: 1, workerData: { proof: 'ok' } }
 ) {
-  return runTest(fixedPool, { tasks, workerData })
+  return runPoolifierTest(fixedPool, { tasks, workerData })
 }
 
 module.exports = { fixedThreadTest }
index cf17aabef6ea9196e53d210d4cfd10f8a43ec250..b6c339f84a1f3b408aaa4b30349b3f745cf26acf 100755 (executable)
@@ -15,7 +15,7 @@ export TASK_TYPE=$taskType
 export NODE_ENV=production
 export POOL_SIZE=10
 export NUM_ITERATIONS=100000
-hyperfine --export-markdown BENCH-100000.MD --min-runs 10 \
+hyperfine --export-markdown BENCH-100000.md --min-runs 10 \
   --prepare 'sleep 15' \
   'node dynamic-piscina.js' \
   'node fixed-piscina.js' \