Repository: tinylibs/tinybench (ts)

Issue:
Concurrent benchmarking
I was hoping to do some testing of how JS frameworks perform when getting multiple requests at a time. A concurrency limit flag would be dope.

Did some reading of [past issues](https://github.com/tinylibs/tinybench/issues/21) and I don't fully agree with the removal of the previous parallel options. Surely it won't be perfectly accurate, but no benchmark is

Pull request description:
feat: runConcurrently
Resolves #74 

Patch:
diff --git a/README.md b/README.md
index add1b93..9790dc8 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,7 @@ export type Hook = (task: Task, mode: "warmup" | "run") => void | Promise<void>;
 ```
 
 - `async run()`: run the added tasks that were registered using the `add` method
+- `async runConcurrently(limit: number = Infinity)`: similar to the `run` method but runs concurrently rather than sequentially
 - `async warmup()`: warm up the benchmark tasks
 - `reset()`: reset each task and remove its result
 - `add(name: string, fn: Fn, opts?: FnOpts)`: add a benchmark task to the task map
diff --git a/package.json b/package.json
index e6685aa..48e7a21 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "publish": "npm run build && clean-publish",
     "typecheck": "tsc --noEmit",
     "release": "bumpp package.json --commit --push --tag && npm run publish",
-    "test": "vitest --retry=3 --run"
+    "test": "vitest --retry=5 --run"
   },
   "main": "./dist/index.cjs",
   "module": "./dist/index.js",
diff --git a/src/bench.ts b/src/bench.ts
index 4d5465e..a84e02f 100644
--- a/src/bench.ts
+++ b/src/bench.ts
@@ -67,6 +67,11 @@ export default class Bench extends EventTarget {
     }
   }
 
+  runTask(task: Task) {
+    if (this.signal?.aborted) return task;
+    return task.run();
+  }
+
   /**
    * run the added tasks that were registered using the
    * {@link add} method.
@@ -76,13 +81,45 @@ export default class Bench extends EventTarget {
     this.dispatchEvent(createBenchEvent('start'));
     const values: Task[] = [];
     for (const task of [...this._tasks.values()]) {
-      if (this.signal?.aborted) values.push(task);
-      else values.push(await task.run());
+      values.push(await this.runTask(task));
     }
     this.dispatchEvent(createBenchEvent('complete'));
     return values;
   }
 
+  /**
+   * similar to the {@link run} method but runs concurrently rather than sequentially
+   * default limit is Infinity
+   */
+  async runConcurrently(limit = Infinity) {
+    this.dispatchEvent(createBenchEvent('start'));
+
+    const remainingTasks = [...this._tasks.values()];
+    const values: Task[] = [];
+
+    const handleConcurrency = async () => {
+      while (remainingTasks.length > 0) {
+        const runningTasks: (Promise<Task> | Task)[] = [];
+
+        // Start tasks up to the concurrency limit
+        while (runningTasks.length < limit && remainingTasks.length > 0) {
+          const task = remainingTasks.pop()!;
+          runningTasks.push(this.runTask(task));
+        }
+
+        // Wait for all running tasks to complete
+        const completedTasks = await Promise.all(runningTasks);
+        values.push(...completedTasks);
+      }
+    };
+
+    await handleConcurrency();
+
+    this.dispatchEvent(createBenchEvent('complete'));
+
+    return values;
+  }
+
   /**
    * warmup the benchmark tasks.
    * This is not run by default by the {@link run} method.


Test patch:
diff --git a/test/sequential.test.ts b/test/sequential.test.ts
index 59d13a2..80e0c2d 100644
--- a/test/sequential.test.ts
+++ b/test/sequential.test.ts
@@ -28,3 +28,37 @@ test('sequential', async () => {
   await sequentialBench.run();
   expect(isFirstTaskDefined).toBe(true);
 });
+
+test('concurrent', async () => {
+  const concurrentBench = new Bench({
+    time: 0,
+    iterations: 100,
+  });
+
+  let shouldBeDefined1: true;
+  let shouldBeDefined2: true;
+
+  let shouldNotBeDefinedFirst1: true;
+  let shouldNotBeDefinedFirst2: true;
+  concurrentBench
+    .add('sample 1', async () => {
+      shouldBeDefined1 = true;
+      await setTimeout(100);
+      shouldNotBeDefinedFirst1 = true;
+    })
+    .add('sample 2', async () => {
+      shouldBeDefined2 = true;
+      await setTimeout(100);
+      shouldNotBeDefinedFirst2 = true;
+    });
+
+  concurrentBench.runConcurrently();
+  await setTimeout(0);
+  expect(shouldBeDefined1!).toBeDefined();
+  expect(shouldBeDefined2!).toBeDefined();
+  expect(shouldNotBeDefinedFirst1!).toBeUndefined();
+  expect(shouldNotBeDefinedFirst2!).toBeUndefined();
+  await setTimeout(100);
+  expect(shouldNotBeDefinedFirst1!).toBeDefined();
+  expect(shouldNotBeDefinedFirst2!).toBeDefined();
+});