Benchmark Results

May 13, 2026 ยท View on GitHub

This document presents benchmark results comparing synchronous execution with parallel execution using ext-parallel and ext-swoole.

Environment

  • Container: Docker (php:8.4-zts)
  • Extensions: ext-parallel, ext-swoole
  • Database: MySQL 8.0
  • CPU: Host machine CPU (parallelism depends on available cores)

Running Benchmarks

cd demo
docker compose up -d --wait parallel
docker compose exec parallel composer install

# Cold one-shot CLI reference
docker compose exec parallel composer parallel-benchmark

# Steady-state HTTP benchmark with wrk
docker compose exec parallel composer steady-state-parallel
composer steady-state-matrix

# Swoole uses the separate swoole image
docker compose up -d --wait swoole
docker compose exec swoole composer install
docker compose exec swoole composer swoole-benchmark
docker compose exec swoole composer steady-state-swoole

Simulation Methodology

Each embedded resource uses SlowQueryInterceptor to add artificial delay, simulating realistic I/O latency. This approach provides reproducible benchmarks that reflect real-world conditions:

OperationTypical Latency
Simple SELECT1-5ms
JOIN/Aggregation10-50ms
Network latency (RDS)1-10ms
Template processing1-10ms

The default 10ms delay represents a conservative, realistic per-resource overhead combining SQL execution, network latency, and processing time.

The demo now separates two benchmark profiles:

  • Cold one-shot CLI: includes startup work such as DI lookup and, for ext-parallel, one-time parallel\Runtime spawn.
  • Steady-state HTTP: starts a long-running server and uses wrk to issue repeated HTTP requests after the server is ready and warmed. Request-local async result caches are reset between requests.

The demo ext-parallel HTTP server is a multi-process harness. Each HTTP worker process keeps its own long-running parallel\Runtime pool, so higher WRK_CONNECTIONS values can be used for HTTP concurrency measurements. Swoole uses its actual long-running HTTP server.

You can verify the cold CLI benchmark path in our CI benchmark workflow, which runs on every push and pull request.

When to Choose Parallel

For a read-only resource graph that embeds multiple independent GET resources, parallel execution should be the first candidate when the runtime supports it and the downstream database or API capacity is sized for the extra concurrency. This is the core design point of BEAR.Async: application code declares the resource graph with #[Embed], and the Linker implementation decides whether the graph is resolved sequentially, with Swoole coroutines, or with ext-parallel workers.

Preconditions

  • Embedded resources are read-only GET resources with no ordering dependency.
  • The runtime extension is available: ext-swoole or ext-parallel.
  • Downstream capacity is sized for internal parallelism, not just HTTP request concurrency.
  • Swoole uses a coroutine-aware connection pool. If the goal is to avoid queueing, size PDO_POOL_SIZE for roughly embed_count * request_concurrency. A smaller pool is still valid when you intentionally want backpressure.
  • ext-parallel uses a parallel\Runtime pool per resident HTTP worker process. To run all embeds in one dashboard request concurrently, set PARALLEL_POOL_SIZE >= embed_count for each worker. Database connections grow with the number of HTTP workers and their runtime pools.
  • ext-parallel steady-state measurements require a process that keeps the runtime pool warm, such as the bundled benchmark HTTP server or another resident worker model. One-shot CLI runs include Runtime startup cost and are cold-start references, not steady-state per-request measurements.

Adapter Guide

SituationRecommended adapter
Swoole HTTP server is acceptable and high throughput is neededSwoole adapter
A resident process can keep ext-parallel runtimes warmext-parallel adapter
Extension support is unavailable or portability is the prioritySync adapter

Cases with Little or No Gain

  • The downstream database or API cannot absorb the added concurrency because of pool limits, saturation, or rate limits.
  • Each embedded resource is already extremely fast; this demo does not measure the boundary where fixed runtime overhead dominates.
  • Embedded resources have real ordering dependencies or share mutable request-local state.
  • One-shot CLI and cron-style jobs can still use the adapters, but they should be read as cold-start behavior. In this demo the ext-parallel one-shot CLI run is about 64 ms slower than Sync because Runtime startup is included.

Cold One-Shot CLI Results

These numbers are reference values for a single CLI invocation of the dashboard resource with 8 embeds. They include startup work such as DI lookup and, for ext-parallel, Runtime spawn.

ProfileRuntimeTimevs profile Sync
ext-parallel CLISync137.97 msbaseline
ext-parallel CLIext-parallel201.87 ms+63.90 ms (0.68x)
Swoole CLISync140.45 msbaseline
Swoole CLISwoole47.64 ms-92.81 ms (2.95x)

The ext-parallel cold result should not be used to judge steady-state per-request performance. It is useful for understanding one-shot behavior and for keeping the benchmark honest about startup cost.

Steady-State HTTP Results

The following numbers were measured locally with Docker Compose on 2026-05-13. Each row is the median of three 30-second wrk runs against /dashboard?user_id=1. The c=1,t=2 combination is omitted because wrk requires the number of connections to be greater than or equal to the number of threads.

The CPU, memory, and MySQL columns are a single mid-run sample for orientation; they are not peak measurements. Socket timeouts are summed across the three runs.

RuntimeConfigConnectionsThreadsRunsMedian req/sMedian latencySocket timeoutsSample CPUSample memoryMySQL threads
ext-parallelworkers=1,pool=811321.7945.82 ms05.37%96.74MiB / 7.652GiB9
ext-parallelworkers=4,pool=841383.1947.99 ms015.22%284MiB / 7.652GiB33
ext-parallelworkers=4,pool=842383.3847.90 ms019.43%284.5MiB / 7.652GiB33
ext-parallelworkers=16,pool=81613318.4250.02 ms032.01%1.001GiB / 7.652GiB129
ext-parallelworkers=16,pool=81623317.5850.26 ms0109.19%1.001GiB / 7.652GiB129
Swoolepdo_pool=6411354.1018.48 ms039.80%69.89MiB / 7.652GiB65
Swoolepdo_pool=64413158.7325.18 ms085.40%56.07MiB / 7.652GiB65
Swoolepdo_pool=64423157.7525.33 ms089.57%59.73MiB / 7.652GiB65
Swoolepdo_pool=641613279.6657.16 ms0162.77%71.85MiB / 7.652GiB65
Swoolepdo_pool=641623279.1557.25 ms0158.54%64.89MiB / 7.652GiB65
Swoolepdo_pool=811354.2718.41 ms040.72%54.27MiB / 7.652GiB9
Swoolepdo_pool=841359.6267.02 ms041.04%54.14MiB / 7.652GiB9
Swoolepdo_pool=842359.3267.32 ms041.64%53.79MiB / 7.652GiB9
Swoolepdo_pool=8161360.07265.27 ms040.08%347.1MiB / 7.652GiB9
Swoolepdo_pool=8162359.90265.73 ms039.25%61.7MiB / 7.652GiB9

Interpretation

  • The throughput and latency numbers are internally consistent. For example, ext-parallel at 16 connections reports about 50 ms latency, and 16 / 0.050s is about 320 req/s. Swoole with PDO_POOL_SIZE=64 at 16 connections reports about 57 ms latency, and 16 / 0.057s is about 280 req/s.
  • Read this as a comparison of demo server configurations, not as a universal runtime ranking. The ext-parallel benchmark uses multiple HTTP worker processes, each with its own runtime pool. Swoole uses one HTTP server with a coroutine PDO pool.
  • ext-parallel scales with the number of benchmark HTTP workers. A single worker serves one dashboard request in about 46 ms. At 16 connections it reaches about 318 req/s with about 50 ms median latency and no socket timeouts. The cost is resource growth: this configuration creates 16 HTTP worker processes, each with an 8-runtime pool, and the sampled MySQL connection count is 129.
  • Swoole with PDO_POOL_SIZE=64 is much lighter in memory and has better single-connection latency. It reaches about 279 req/s at 16 connections, with a sampled MySQL connection count of 65. The high connection count is intentional in this profile because the demo Swoole server pre-fills the configured PDO pool before serving requests.
  • Swoole with PDO_POOL_SIZE=8 demonstrates pool backpressure. It keeps MySQL connections low, but throughput stays around 60 req/s and latency grows as concurrent HTTP requests queue behind the smaller pool.
  • Increasing WRK_THREADS from 1 to 2 does not materially change the result for this matrix. Connection count and runtime/pool sizing dominate.
  • ext-parallel warms the shared DI/cache before worker startup, then warms each HTTP worker before wrk; the runtime pool startup cost is therefore outside the measured 30-second run.
  • The CPU and memory samples are useful for orientation only. They are single mid-run Docker samples, not peak or average resource measurements, so they should not be used as the primary basis for resource sizing.

Benchmark Scenarios

Each benchmark tests the dashboard resource, which embeds 8 independent SQL resources. Each embedded resource simulates database latency.

Scenario Configuration

ScenarioEmbedded ResourcesDelay per Resource
Dashboard810ms

Expected Results

Theoretical Performance

ScenarioSync TimeParallel TimeSpeedup
Dashboard (8 embeds)~80ms + SQL/render overhead~10ms + SQL/render/thread overheadup to 8x

In synchronous mode, total time approaches the sum of all embedded resource costs. With parallel execution, steady-state HTTP time approaches the maximum single embedded resource cost plus runtime overhead. Cold one-shot CLI results also include startup costs and should be read as reference values, not steady-state per-request latency.

Real-World Factors

Actual speedup varies based on:

  • Runtime startup: cold one-shot CLI includes DI and runtime setup work
  • Database connection pool: pool size, queueing, and connection creation
  • CPU cores: ext-parallel benefits from multiple cores
  • I/O vs CPU bound: Parallel execution excels for I/O-bound operations

Comparison: ext-parallel vs ext-swoole

Featureext-parallelext-swoole
Execution ModelThread poolCoroutines
Memory IsolationIsolated per threadShared (requires pooling)
PDO HandlingEach thread gets own PDOMust use connection pool
Best ForResident worker pools with isolated runtimesLong-running coroutine HTTP server
Use Casewarmed worker process or benchmark harnessSwoole HTTP Server

Module Selection Guide

Use the bin/async.php entrypoint (ext-parallel) when:

  • Running a CLI entrypoint that should resolve embeds through ext-parallel
  • Each request requires isolated memory
  • No special PDO connection management needed
  • For steady-state performance evaluation, use a resident process that keeps the parallel\Runtime pool warm

Use AsyncSwooleModule when:

  • Running Swoole HTTP Server
  • Need coroutine-based concurrency
  • Must use PdoPoolModule for database connections

Sample Output

BEAR.Async Parallel Benchmark
==============================
8 embedded SQL resources, cold one-shot reference
This includes DI lookup and one-time ext-parallel Runtime spawn cost.
Use composer steady-state-parallel for HTTP steady-state measurements.

Sync execution (prod-hal-app)...
  Elapsed: 120.00 ms

Parallel execution (prod-hal-app + ParallelRuntimeModule override)...
  Elapsed: 350.00 ms (includes one-time thread pool spawn cost)

Note: this one-shot CLI run includes ext-parallel Runtime spawn.
      It is a cold-start reference, not a steady-state per-request benchmark.