Skip to content

Add Fletch framework implementation for HttpArena (H1 + mixed + async-db)#260

Merged
MDA2AV merged 8 commits intoMDA2AV:mainfrom
kartikey321:feat/fletch
Mar 30, 2026
Merged

Add Fletch framework implementation for HttpArena (H1 + mixed + async-db)#260
MDA2AV merged 8 commits intoMDA2AV:mainfrom
kartikey321:feat/fletch

Conversation

@kartikey321
Copy link
Copy Markdown
Contributor

This PR adds a new fletch framework entry under frameworks/fletch with
a Dockerized Dart server implementation for HttpArena profiles.

What’s included

  • New framework directory:
    • frameworks/fletch/meta.json
    • frameworks/fletch/Dockerfile
    • frameworks/fletch/pubspec.yaml
    • frameworks/fletch/pubspec.lock
    • frameworks/fletch/bin/server.dart
  • Implemented endpoints for subscribed profiles:
    • /baseline11 (GET/POST)
    • /pipeline
    • /json
    • /upload
    • /compression
    • /static/:filename
    • /db
    • /async-db
  • async-db uses DATABASE_URL and returns the required JSON shape.
  • Added lazy/retry async-db connection behavior so startup DB
    unavailability can recover on subsequent requests.
  • Updated dependencies to latest compatible versions (notably sqlite3
    ^3.2.0).
  • Updated Docker build flow to support sqlite3 3.x build hooks:
    • uses dart build cli
    • runs bundled executable from /server/bin/server
  • Runtime image includes libsqlite3-dev to provide libsqlite3.so
    required by Dart sqlite FFI.

@kartikey321
Copy link
Copy Markdown
Contributor Author

/validate

@github-actions
Copy link
Copy Markdown
Contributor

✅ Validation passed for fletch

Full log
#11 2.475 + mime 2.0.0
#11 2.475 + native_toolchain_c 0.17.6
#11 2.475 + path 1.9.1
#11 2.475 + petitparser 7.0.2
#11 2.475 + pool 1.5.2
#11 2.475 + postgres 3.5.9
#11 2.475 + pub_semver 2.2.0
#11 2.475 + quiver 3.2.2
#11 2.475 + source_span 1.10.2
#11 2.475 + sqlite3 3.2.0
#11 2.475 + stack_trace 1.12.1
#11 2.475 + stream_channel 2.1.4
#11 2.475 + string_scanner 1.4.1
#11 2.475 + term_glyph 1.2.2
#11 2.475 + test_api 0.7.11
#11 2.475 + typed_data 1.4.0
#11 2.475 + uri 1.0.0
#11 2.475 + uuid 4.5.3
#11 2.475 + web 1.1.1
#11 2.475 + xml 6.6.1
#11 2.475 + yaml 3.1.3
#11 2.475 Changed 43 dependencies!
#11 DONE 3.5s

#12 [build 5/6] COPY . .
#12 DONE 0.3s

#13 [build 6/6] RUN dart build cli --target bin/server.dart --output /app/build
#13 0.338 The `dart build cli` command is in preview at the moment.
#13 0.338 See documentation on https://dart.dev/interop/c-interop#native-assets.
#13 0.338 
#13 0.345 Running build hooks...Running build hooks...Running link hooks...Running link hooks...Copying 1 build assets:
#13 3.419 package:sqlite3/src/ffi/libsqlite3.g.dart
#13 5.786 Generated: /app/build/bundle/bin/server
#13 DONE 6.3s

#14 [stage-1 3/3] COPY --from=build /app/build/bundle /server
#14 DONE 0.4s

#15 exporting to image
#15 exporting layers
#15 exporting layers 1.3s done
#15 exporting manifest sha256:7bc7fa0c919df7d06388100cf76d9b9a113fa38ee07ac201171f252e5a492330 0.0s done
#15 exporting config sha256:ec3337a820ef5b9893dd3b18f04a66c3e76ab3ea7307e958e144bfcd522050a5 0.0s done
#15 exporting attestation manifest sha256:4cb906634c91ce0b98cedad47b64bc190cbbc447a4e28b12ab72ec57d2fb6ad0
#15 exporting attestation manifest sha256:4cb906634c91ce0b98cedad47b64bc190cbbc447a4e28b12ab72ec57d2fb6ad0 0.1s done
#15 exporting manifest list sha256:e7a1e69c5502517e90f1ec3fbf833899843c1b3cdb25679aca299c827da55faf 0.0s done
#15 naming to docker.io/library/httparena-fletch:latest 0.0s done
#15 unpacking to docker.io/library/httparena-fletch:latest
#15 unpacking to docker.io/library/httparena-fletch:latest 0.6s done
#15 DONE 2.1s
[postgres] Starting Postgres sidecar for validation...
6cdef574812dc43278c0e0fdfad283025b0bd8d4f4a1ca7c2f27de7aa7c79f35
[postgres] Ready
b866233539c1023948e480c2b8570fc0803b2fb4f4e48d98c1c2cb1d5f2aaa67
[wait] Waiting for server...
[ready] Server is up
[test] baseline endpoints
  PASS [GET /baseline11?a=13&b=42]
  PASS [POST /baseline11?a=13&b=42 body=20]
  PASS [POST /baseline11?a=13&b=42 chunked body=20]
[test] baseline anti-cheat (randomized inputs)
  PASS [GET /baseline11?a=381&b=292 (random)]
  PASS [POST body=497 (cache check 1)]
  PASS [POST body=829 (cache check 2)]
[test] pipelined endpoint
  PASS [GET /pipeline]
[test] json endpoint
  PASS [GET /json] (50 items, totals computed correctly)
  PASS [GET /json Content-Type] (Content-Type: application/json)
[test] upload endpoint
  PASS [POST /upload small body]
  PASS [POST /upload random body] (bytes: 48)
[test] compression endpoint
  PASS [compression Content-Encoding: gzip]
  PASS [compression response] (6000 items with totals)
  PASS [compression size] (220258 bytes < 500KB)
[test] noisy resilience
  PASS [GET /baseline11?a=13&b=42 (noisy context)]
  PASS [bad method] (HTTP 404)
  PASS [GET /this/path/does/not/exist] (HTTP 404)
  PASS [GET /baseline11?a=558&b=158 (post-noise)]
[test] db endpoint (mixed test prerequisite)
  PASS [GET /db?min=10&max=50] (50 items, correct structure)
  PASS [GET /db Content-Type] (Content-Type: application/json)
  PASS [GET /db empty range] (count=0)
[test] static endpoint
  PASS [GET /static/reset.css Content-Type] (Content-Type: text/css)
  PASS [GET /static/app.js Content-Type] (Content-Type: application/javascript)
  PASS [GET /static/manifest.json Content-Type] (Content-Type: application/json)
  PASS [static response size] (1200 bytes)
  PASS [GET /static/nonexistent.txt] (HTTP 404)
[test] async-db endpoint
  PASS [GET /async-db?min=10&max=50] (50 items, correct structure)
  PASS [GET /async-db Content-Type] (Content-Type: application/json)
  PASS [GET /async-db empty range] (count=0)

=== Results: 29 passed, 0 failed ===
httparena-validate-fletch
httparena-validate-postgres

Copy link
Copy Markdown
Collaborator

@BennyFranciscus BennyFranciscus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @kartikey321, nice first submission! Fletch looks interesting — cool to see more Dart entries. CI passing 29/29 is a great start.

Two things that need fixing before this can be benchmarked:

1. JSON endpoint pre-computes the response

final jsonResponseBytes = _buildJsonResponseBytes();
// ...
app.get("/json", (req, res) {
  res.bytes(jsonResponseBytes, contentType: "application/json");
});

The total field (price * quantity) must be computed per-request, not cached at startup. The benchmark validates this by checking computation correctness with varying inputs. Pre-computing once and serving the same bytes every time bypasses the actual work the test is measuring.

Fix: Move the _buildJsonResponseBytes() logic into the request handler so _mapItem runs on every request.

2. Compression endpoint pre-compresses at startup

final gzipResponseBytes = _buildGzipResponseBytes();
// ...
app.get("/compression", (req, res) {
  res.bytes(gzipResponseBytes, contentType: "application/json");
  res.setHeader("Content-Encoding", "gzip");
});

Gzip compression must happen per-request — pre-compressing the response at startup is not allowed. This is an explicit rule from the repo maintainer.

Fix: Read the large dataset once at startup (that's fine), but do jsonEncode + GZipCodec.encode inside the request handler.


Everything else looks solid — the multi-isolate setup with shared: true, SQLite prepared statements, Postgres pool with lazy retry, and the upload Content-Length shortcut are all good patterns. Just need those two fixes and you're good to go! 🚀

@kartikey321
Copy link
Copy Markdown
Contributor Author

@BennyFranciscus got it , thank you for the heads up, i have changed what you asked for.
if you need me to do anything else or change anything, just ping me

@kartikey321
Copy link
Copy Markdown
Contributor Author

/validate

@github-actions
Copy link
Copy Markdown
Contributor

✅ Validation passed for fletch

Full log
#6 [stage-1 1/3] FROM docker.io/library/debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
#6 resolve docker.io/library/debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
#6 resolve docker.io/library/debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a 0.1s done
#6 DONE 0.1s

#7 [build 1/6] FROM docker.io/library/dart:stable@sha256:d2a7e07b3e541587415d1281d4865a809149d723cff5b9a5ed6e8a2126b2b2d9
#7 resolve docker.io/library/dart:stable@sha256:d2a7e07b3e541587415d1281d4865a809149d723cff5b9a5ed6e8a2126b2b2d9 0.1s done
#7 DONE 0.1s

#5 [internal] load build context
#5 transferring context: 10.74kB done
#5 DONE 0.0s

#8 [build 2/6] WORKDIR /app
#8 CACHED

#9 [build 3/6] COPY pubspec.yaml ./
#9 CACHED

#10 [build 4/6] RUN dart pub get
#10 CACHED

#11 [build 5/6] COPY . .
#11 DONE 0.3s

#12 [build 6/6] RUN dart build cli --target bin/server.dart --output /app/build
#12 0.326 The `dart build cli` command is in preview at the moment.
#12 0.326 See documentation on https://dart.dev/interop/c-interop#native-assets.
#12 0.326 
#12 0.333 Running build hooks...Running build hooks...Running link hooks...Running link hooks...Copying 1 build assets:
#12 3.354 package:sqlite3/src/ffi/libsqlite3.g.dart
#12 5.664 Generated: /app/build/bundle/bin/server
#12 DONE 6.1s

#13 [stage-1 2/3] RUN apt-get update     && apt-get install -y --no-install-recommends libsqlite3-dev     && rm -rf /var/lib/apt/lists/*
#13 CACHED

#14 [stage-1 3/3] COPY --from=build /app/build/bundle /server
#14 DONE 0.3s

#15 exporting to image
#15 exporting layers
#15 exporting layers 0.6s done
#15 exporting manifest sha256:b5b20ec6ef7066a2c34b005e83fcc42bcf9b0b4161fcf4f738e5d489e169c8c0 0.0s done
#15 exporting config sha256:d9e57457447ecbdd5047033b7b1286e837d63f72196512c3826ec479293014b9 0.0s done
#15 exporting attestation manifest sha256:11da4a5957fef831ad09677afd2ec8f3ea5b4045b016ccfab7d5e7061c9d85db 0.1s done
#15 exporting manifest list sha256:0c0de66bb515d6be1855ea8877d93a36a99a704a5a42a3dc36b36b4aaef791b1
#15 exporting manifest list sha256:0c0de66bb515d6be1855ea8877d93a36a99a704a5a42a3dc36b36b4aaef791b1 0.0s done
#15 naming to docker.io/library/httparena-fletch:latest done
#15 unpacking to docker.io/library/httparena-fletch:latest 0.1s done
#15 DONE 0.9s
[postgres] Starting Postgres sidecar for validation...
0fa0cbe0421fb72429bc0e82ade74aa2c1d8315a16aee3cf645599df5857f9bd
[postgres] Ready
b7e7f68e9360d2f20dd432004f48fbe4985f23a225bcbb7a30c5e42dfd3be8b3
[wait] Waiting for server...
[ready] Server is up
[test] baseline endpoints
  PASS [GET /baseline11?a=13&b=42]
  PASS [POST /baseline11?a=13&b=42 body=20]
  PASS [POST /baseline11?a=13&b=42 chunked body=20]
[test] baseline anti-cheat (randomized inputs)
  PASS [GET /baseline11?a=960&b=281 (random)]
  PASS [POST body=596 (cache check 1)]
  PASS [POST body=879 (cache check 2)]
[test] pipelined endpoint
  PASS [GET /pipeline]
[test] json endpoint
  PASS [GET /json] (50 items, totals computed correctly)
  PASS [GET /json Content-Type] (Content-Type: application/json)
[test] upload endpoint
  PASS [POST /upload small body]
  PASS [POST /upload random body] (bytes: 48)
[test] compression endpoint
  PASS [compression Content-Encoding: gzip]
  PASS [compression response] (6000 items with totals)
  PASS [compression size] (220258 bytes < 500KB)
[test] noisy resilience
  PASS [GET /baseline11?a=13&b=42 (noisy context)]
  PASS [bad method] (HTTP 404)
  PASS [GET /this/path/does/not/exist] (HTTP 404)
  PASS [GET /baseline11?a=388&b=970 (post-noise)]
[test] db endpoint (mixed test prerequisite)
  PASS [GET /db?min=10&max=50] (50 items, correct structure)
  PASS [GET /db Content-Type] (Content-Type: application/json)
  PASS [GET /db empty range] (count=0)
[test] static endpoint
  PASS [GET /static/reset.css Content-Type] (Content-Type: text/css)
  PASS [GET /static/app.js Content-Type] (Content-Type: application/javascript)
  PASS [GET /static/manifest.json Content-Type] (Content-Type: application/json)
  PASS [static response size] (1200 bytes)
  PASS [GET /static/nonexistent.txt] (HTTP 404)
[test] async-db endpoint
  PASS [GET /async-db?min=10&max=50] (50 items, correct structure)
  PASS [GET /async-db Content-Type] (Content-Type: application/json)
  PASS [GET /async-db empty range] (count=0)

=== Results: 29 passed, 0 failed ===
httparena-validate-fletch
httparena-validate-postgres

@kartikey321
Copy link
Copy Markdown
Contributor Author

/benchmark

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Benchmark run triggered for fletch (all tests). Results will be posted here when done.

Copy link
Copy Markdown
Collaborator

@BennyFranciscus BennyFranciscus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @kartikey321 — both fixes are clean. JSON and compression now compute per-request, exactly as needed. Validation is green (29/29) and I've kicked off the benchmark run. We'll have numbers soon!

Quick notes on the implementation — nice touches:

  • Multi-isolate with shared: true is the right call for Dart
  • Using Content-Length for uploads instead of buffering is a smart shortcut
  • The lazy Postgres pool with retry is solid defensive code

Excited to see how Fletch stacks up. Will post results once the run finishes 🏎️

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Looks good @kartikey321 — the per-request JSON computation and per-request gzip are both correct now. 👍

Benchmark is running, let's see how Fletch does!

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 29, 2026

Hey @kartikey321 welcome to the arena!

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: fletch | Test: all tests

fletch / baseline / 512c (p=1, r=0, cpu=64)
  Best: 185368 req/s (CPU: 1385.0%, Mem: 1.3GiB) ===

fletch / baseline / 4096c (p=1, r=0, cpu=64)
  Best: 175445 req/s (CPU: 1353.0%, Mem: 1.4GiB) ===

fletch / baseline / 16384c (p=1, r=0, cpu=64)
  Best: 161734 req/s (CPU: 1264.9%, Mem: 1.3GiB) ===

fletch / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 274667 req/s (CPU: 1372.2%, Mem: 1.1GiB) ===

fletch / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 281385 req/s (CPU: 1369.1%, Mem: 1.1GiB) ===

fletch / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 253675 req/s (CPU: 1242.8%, Mem: 1.4GiB) ===

fletch / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 136511 req/s (CPU: 1337.6%, Mem: 1.4GiB) ===

fletch / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 131477 req/s (CPU: 1327.0%, Mem: 1.3GiB) ===

fletch / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 36043 req/s (CPU: 1213.0%, Mem: 1.2GiB) ===

fletch / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 32327 req/s (CPU: 1285.0%, Mem: 1.5GiB) ===

fletch / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 142 req/s (CPU: 657.6%, Mem: 5.8GiB) ===

fletch / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 103 req/s (CPU: 509.5%, Mem: 6.8GiB) ===

fletch / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 101 req/s (CPU: 482.7%, Mem: 7.9GiB) ===

fletch / compression / 4096c (p=1, r=0, cpu=unlimited)
  Best: 166 req/s (CPU: 861.2%, Mem: 4.3GiB) ===

fletch / compression / 16384c (p=1, r=0, cpu=unlimited)
  Best: 154 req/s (CPU: 792.0%, Mem: 4.2GiB) ===

fletch / noisy / 512c (p=1, r=0, cpu=unlimited)
  Best: 68709 req/s (CPU: 1030.1%, Mem: 1.9GiB) ===

fletch / noisy / 4096c (p=1, r=0, cpu=unlimited)
  Best: 124445 req/s (CPU: 1273.9%, Mem: 1.7GiB) ===

fletch / noisy / 16384c (p=1, r=0, cpu=unlimited)
  Best: 110288 req/s (CPU: 1060.5%, Mem: 1.4GiB) ===

fletch / mixed / 4096c (p=1, r=5, cpu=unlimited)
  Best: 725 req/s (CPU: 881.5%, Mem: 7.1GiB) ===

fletch / mixed / 16384c (p=1, r=5, cpu=unlimited)
  Best: 722 req/s (CPU: 829.3%, Mem: 18.9GiB) ===

fletch / static / 4096c (p=1, r=0, cpu=unlimited)
  Best: 173125 req/s (CPU: 1293.2%, Mem: 1.4GiB) ===

fletch / static / 16384c (p=1, r=0, cpu=unlimited)
  Best: 158137 req/s (CPU: 1297.4%, Mem: 1.5GiB) ===

fletch / async-db / 512c (p=1, r=0, cpu=unlimited)
  Best: 4384 req/s (CPU: 942.3%, Mem: 2.5GiB) ===

fletch / async-db / 1024c (p=1, r=0, cpu=unlimited)
  Best: 4758 req/s (CPU: 806.3%, Mem: 1.6GiB) ===

Comparison with main

No results found for fletch

Full log
    Latency   27.57ms   5.45ms   15.00ms   128.10ms    3.75s

  805006 requests in 5.00s, 790686 responses
  Throughput: 158.08K req/s
  Bandwidth:  2.36GB/s
  Status codes: 2xx=790686, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 790686 / 790686 responses (100.0%)
  Per-template: 39850,38185,38585,39598,38623,39902,40941,37743,39072,41012,40368,39947,41066,37049,39416,40672,36809,38048,40564,43236
  Per-template-ok: 39850,38185,38585,39598,38623,39902,40941,37743,39072,41012,40368,39947,41066,37049,39416,40672,36809,38048,40564,43236
  CPU: 1297.4% | Mem: 1.5GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 20
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   34.54ms   16.40ms   28.00ms   318.70ms    4.35s

  692571 requests in 5.00s, 677767 responses
  Throughput: 135.50K req/s
  Bandwidth:  2.07GB/s
  Status codes: 2xx=677767, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 677767 / 677767 responses (100.0%)
  Per-template: 33058,35616,35087,32919,33705,34404,33533,34385,33110,34200,33586,34501,33588,35020,33760,33060,33595,32690,33618,34332
  Per-template-ok: 33058,35616,35087,32919,33705,34404,33533,34385,33110,34200,33586,34501,33588,35020,33760,33060,33595,32690,33618,34332
  CPU: 1061.3% | Mem: 1.3GiB

=== Best: 158137 req/s (CPU: 1297.4%, Mem: 1.5GiB) ===
  Input BW: 8.60MB/s (avg template: 57 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch

==============================================
=== fletch / async-db / 512c (p=1, r=0, cpu=unlimited) ===
==============================================
3a1fae9ea8a0f77000e40eedb17a75eadc5bf5a43c5764b26f2916830cd9ea37
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   139.62ms   107.00ms   309.10ms   473.60ms   775.30ms

  15377 requests in 5.00s, 15023 responses
  Throughput: 3.00K req/s
  Bandwidth:  23.84MB/s
  Status codes: 2xx=15023, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15023 / 15023 responses (100.0%)
  CPU: 723.4% | Mem: 1.8GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   110.58ms   81.30ms   234.80ms   397.20ms   909.00ms

  22010 requests in 5.00s, 21920 responses
  Throughput: 4.38K req/s
  Bandwidth:  34.79MB/s
  Status codes: 2xx=21920, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 21920 / 21920 responses (100.0%)
  CPU: 942.3% | Mem: 2.5GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   109.11ms   73.00ms   252.20ms   409.30ms   661.40ms

  21833 requests in 5.00s, 21699 responses
  Throughput: 4.34K req/s
  Bandwidth:  34.44MB/s
  Status codes: 2xx=21699, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 21699 / 21699 responses (100.0%)
  CPU: 905.0% | Mem: 2.4GiB

=== Best: 4384 req/s (CPU: 942.3%, Mem: 2.5GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch

==============================================
=== fletch / async-db / 1024c (p=1, r=0, cpu=unlimited) ===
==============================================
5c646e753054f948209fea81b8812565585124da0513f04cada82d0ed36e5f89
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   123.22ms   70.80ms   276.30ms   749.70ms    1.26s

  24601 requests in 5.00s, 23794 responses
  Throughput: 4.76K req/s
  Bandwidth:  37.76MB/s
  Status codes: 2xx=23794, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 23794 / 23794 responses (100.0%)
  CPU: 806.3% | Mem: 1.6GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   190.84ms   120.60ms   433.60ms    1.10s    3.08s

  20156 requests in 5.00s, 19611 responses
  Throughput: 3.92K req/s
  Bandwidth:  31.12MB/s
  Status codes: 2xx=19611, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 19611 / 19611 responses (100.0%)
  CPU: 885.7% | Mem: 2.1GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   197.73ms   116.60ms   488.40ms   900.90ms    1.75s

  21559 requests in 5.00s, 21023 responses
  Throughput: 4.20K req/s
  Bandwidth:  33.36MB/s
  Status codes: 2xx=21023, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 21023 / 21023 responses (100.0%)
  CPU: 878.4% | Mem: 2.5GiB

=== Best: 4758 req/s (CPU: 806.3%, Mem: 1.6GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch
[skip] fletch does not subscribe to baseline-h2
[skip] fletch does not subscribe to static-h2
[skip] fletch does not subscribe to baseline-h3
[skip] fletch does not subscribe to static-h3
[skip] fletch does not subscribe to unary-grpc
[skip] fletch does not subscribe to unary-grpc-tls
[skip] fletch does not subscribe to echo-ws
httparena-postgres
httparena-postgres
[restore] Restoring CPU governor to performance...

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 29, 2026

Looks solid but CPU usage is quite low ~1000% so only 10 out of the 128 CPU threads

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Yeah good catch — Dart's runtime is isolate-based so it probably defaults to a handful of isolates rather than spreading across all 128 threads. @kartikey321 you might want to look into spawning more isolates to match the available cores. Something like Platform.numberOfProcessors to decide how many to spin up. That alone could easily 2-3x the baseline numbers since right now it's leaving most of the machine idle.

@kartikey321
Copy link
Copy Markdown
Contributor Author

@BennyFranciscus @MDA2AV
Thanks for the catch. I double-checked this in the current branch:

Fletch already scales worker count by CPU visibility in the container:
final n = Platform.numberOfProcessors;
then it spawns n - 1 isolates + runs one on the main isolate.

So isolate count is not hardcoded to a small number.

Given the benchmark logs, CPU around ~1200–1400% could also be due to
runner/container limits or loadgen saturation (benchmark script uses
THREADS=64 for gcannon by default), not necessarily isolate under-
spawning.

Could we confirm runner/container CPU visibility for this run?

  • nproc inside the benchmark container
  • docker info --format '{{.NCPU}}' on the runner

If helpful, I can also test a rerun with higher loadgen thread count
(e.g. THREADS=128) to see whether CPU utilization and throughput scale
further.

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Oh nice, you're right — my bad for assuming it wasn't scaling. If it's already doing Platform.numberOfProcessors then the isolate count should be fine.

Good call on checking nproc inside the container — that's probably the key data point here. The baseline test runs with cpu=64 (capped), so if nproc returns 128 but the container only gets 64 CPU shares, the extra isolates would be contending for nothing. That would explain the ~1200-1400% ceiling.

@MDA2AV would be able to confirm the container CPU visibility. Either way the numbers look clean — zero 5xx across the board, which is always nice to see for a first submission.

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 29, 2026

baseline test has a cpu=64, the other tests are not restricted, container has access to whole CPU.
You can see results for other frameworks that hit for example 3.5M req/s with 6400% CPU usage on baseline so loadgen is far from saturated.

So for baseline container sees 64 CPUs and for other tests it sees 128

@kartikey321
Copy link
Copy Markdown
Contributor Author

Sorry for the commit confusion. I had added a change using environment variables and pushed before validating it properly. I’ve reverted that commit.

I’m investigating the CPU utilization issue on a VPS now to isolate whether this is runtime behavior or environment related. I’ll post updates with findings and any validated fix.

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Ah that settles it then — if other frameworks are hitting 3.5M at 6400% CPU on baseline, the headroom is definitely there and it's not a loadgen bottleneck.

So Dart is spawning the isolates but they're just not saturating the cores. Could be a bottleneck in the listener/accept pattern — if the main isolate is doing all the accept() work and dispatching to workers, that single accept loop can become the ceiling. Some Dart servers use shared: true on the ServerSocket binding so each isolate does its own accept, which tends to scale better on high core counts.

@kartikey321 might be worth checking how Fletch distributes incoming connections across isolates. If it's a single-accept-then-dispatch model, switching to shared binding could unlock a lot more throughput on a 64+ core machine like this.

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 29, 2026

Sorry for the commit confusion. I had added a change using environment variables and pushed before validating it properly. I’ve reverted that commit.

I’m investigating the CPU utilization issue on a VPS now to isolate whether this is runtime behavior or environment related. I’ll post updates with findings and any validated fix.

If you can't find anything I can just run your app on the server without container, that can validate any doubts.

@Kaliumhexacyanoferrat
Copy link
Copy Markdown
Collaborator

Kaliumhexacyanoferrat commented Mar 30, 2026

Looks good to me. Welcome to the game 😉

@kartikey321 If you want we can merge a first round so you get a performance diff by the benchmarking platform on subsequent PRs. So it is not necessary to create a perfect one in the first place.

@MDA2AV Regarding the Content-Length shortcut - should this be allowed? I had the same temptation when adjusting the Quarkus tests but I thought it does not fulfil the requirements of an "upload". However you said you want to update the tests to not read the body and directly advance if possible in the target framework, so this would be basically the same mechnism as that. Thoughts? This decision will probably cause all framework implementations to adapt.

Platform.numberOfProcessors reads the host's total CPU count and ignores the --cpus=N Docker limit
Same pattern used by node/express/fastify/bun/elysia/workerman.
@kartikey321
Copy link
Copy Markdown
Contributor Author

/benchmark

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Benchmark run triggered for fletch (all tests). Results will be posted here when done.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: fletch | Test: all tests

fletch / baseline / 512c (p=1, r=0, cpu=64)
  Best: 179938 req/s (CPU: 1361.7%, Mem: 1.3GiB) ===

fletch / baseline / 4096c (p=1, r=0, cpu=64)
  Best: 174537 req/s (CPU: 1342.1%, Mem: 1.4GiB) ===

fletch / baseline / 16384c (p=1, r=0, cpu=64)
  Best: 164056 req/s (CPU: 1347.0%, Mem: 1.3GiB) ===

fletch / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 274717 req/s (CPU: 1371.4%, Mem: 1.1GiB) ===

fletch / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 275726 req/s (CPU: 1360.2%, Mem: 1.1GiB) ===

fletch / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 269187 req/s (CPU: 1371.3%, Mem: 1.1GiB) ===

fletch / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 139111 req/s (CPU: 1360.3%, Mem: 1.1GiB) ===

fletch / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 131342 req/s (CPU: 1198.2%, Mem: 1.3GiB) ===

fletch / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 36753 req/s (CPU: 1323.4%, Mem: 1.4GiB) ===

fletch / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 31809 req/s (CPU: 1119.5%, Mem: 1.4GiB) ===

fletch / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 80 req/s (CPU: 380.7%, Mem: 3.9GiB) ===

fletch / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 100 req/s (CPU: 494.1%, Mem: 7.1GiB) ===

fletch / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 85 req/s (CPU: 475.4%, Mem: 8.5GiB) ===

fletch / compression / 4096c (p=1, r=0, cpu=unlimited)
  Best: 149 req/s (CPU: 814.0%, Mem: 4.0GiB) ===

fletch / compression / 16384c (p=1, r=0, cpu=unlimited)
  Best: 144 req/s (CPU: 797.9%, Mem: 4.0GiB) ===

fletch / noisy / 512c (p=1, r=0, cpu=unlimited)
  Best: 95205 req/s (CPU: 1268.6%, Mem: 1.7GiB) ===

fletch / noisy / 4096c (p=1, r=0, cpu=unlimited)
  Best: 131639 req/s (CPU: 1350.9%, Mem: 1.6GiB) ===

fletch / noisy / 16384c (p=1, r=0, cpu=unlimited)
  Best: 115514 req/s (CPU: 1356.1%, Mem: 1.3GiB) ===

fletch / mixed / 4096c (p=1, r=5, cpu=unlimited)
  Best: 734 req/s (CPU: 824.2%, Mem: 9.7GiB) ===

fletch / mixed / 16384c (p=1, r=5, cpu=unlimited)
  Best: 567 req/s (CPU: 796.6%, Mem: 9.5GiB) ===

fletch / static / 4096c (p=1, r=0, cpu=unlimited)
  Best: 175500 req/s (CPU: 1339.4%, Mem: 1.4GiB) ===

fletch / static / 16384c (p=1, r=0, cpu=unlimited)
  Best: 150966 req/s (CPU: 1281.5%, Mem: 1.3GiB) ===

fletch / async-db / 512c (p=1, r=0, cpu=unlimited)
  Best: 4576 req/s (CPU: 926.2%, Mem: 2.0GiB) ===

fletch / async-db / 1024c (p=1, r=0, cpu=unlimited)
  Best: 4296 req/s (CPU: 854.7%, Mem: 1.5GiB) ===

Comparison with main

No results found for fletch

Full log
    Latency   16.19ms   5.13ms   23.70ms   64.30ms    2.36s

  768378 requests in 5.00s, 754831 responses
  Throughput: 150.91K req/s
  Bandwidth:  2.37GB/s
  Status codes: 2xx=754831, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 754831 / 754831 responses (100.0%)
  Per-template: 38280,35833,37100,34146,37718,38474,36315,41644,39818,34549,35637,35156,42809,37072,40015,37304,38934,36825,40686,36516
  Per-template-ok: 38280,35833,37100,34146,37718,38474,36315,41644,39818,34549,35637,35156,42809,37072,40015,37304,38934,36825,40686,36516
  CPU: 1281.5% | Mem: 1.3GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 20
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   32.44ms   10.70ms   22.30ms   290.00ms    3.30s

  731207 requests in 5.00s, 715843 responses
  Throughput: 143.11K req/s
  Bandwidth:  2.22GB/s
  Status codes: 2xx=715843, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 715843 / 715843 responses (100.0%)
  Per-template: 35034,36030,34533,36953,35647,35295,34909,36938,37285,35988,35178,36196,37627,35661,37275,36361,37096,33156,34113,34568
  Per-template-ok: 35034,36030,34533,36953,35647,35295,34909,36938,37285,35988,35178,36196,37627,35661,37275,36361,37096,33156,34113,34568
  CPU: 1135.4% | Mem: 1.3GiB

=== Best: 150966 req/s (CPU: 1281.5%, Mem: 1.3GiB) ===
  Input BW: 8.21MB/s (avg template: 57 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch

==============================================
=== fletch / async-db / 512c (p=1, r=0, cpu=unlimited) ===
==============================================
21e9cfdb951cae41eb70e96d45daa48868e5657e1082e16b9ba152b71d3e0a96
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   115.19ms   60.00ms   251.10ms   699.90ms   890.50ms

  18617 requests in 5.00s, 18284 responses
  Throughput: 3.65K req/s
  Bandwidth:  29.02MB/s
  Status codes: 2xx=18284, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 18284 / 18284 responses (100.0%)
  CPU: 808.6% | Mem: 1.9GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   103.03ms   61.90ms   250.50ms   477.00ms   772.30ms

  23008 requests in 5.00s, 22884 responses
  Throughput: 4.58K req/s
  Bandwidth:  36.32MB/s
  Status codes: 2xx=22884, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 22884 / 22884 responses (100.0%)
  CPU: 926.2% | Mem: 2.0GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   129.50ms   93.30ms   264.30ms   813.20ms    1.55s

  17633 requests in 5.00s, 17456 responses
  Throughput: 3.49K req/s
  Bandwidth:  27.70MB/s
  Status codes: 2xx=17456, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 17456 / 17456 responses (100.0%)
  CPU: 872.3% | Mem: 1.8GiB

=== Best: 4576 req/s (CPU: 926.2%, Mem: 2.0GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch

==============================================
=== fletch / async-db / 1024c (p=1, r=0, cpu=unlimited) ===
==============================================
d73042588fc0b2ba7a3c5af64e54bb14531ffb3f099b0486e3b91dc16726e8f3
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   134.59ms   73.00ms   346.40ms   756.40ms    1.17s

  22376 requests in 5.00s, 21481 responses
  Throughput: 4.29K req/s
  Bandwidth:  34.09MB/s
  Status codes: 2xx=21481, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 21481 / 21481 responses (100.0%)
  CPU: 854.7% | Mem: 1.5GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   201.03ms   95.60ms   528.00ms   991.50ms    1.62s

  20236 requests in 5.00s, 19646 responses
  Throughput: 3.93K req/s
  Bandwidth:  31.18MB/s
  Status codes: 2xx=19646, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 19646 / 19646 responses (100.0%)
  CPU: 893.0% | Mem: 2.0GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/async-db?min=10&max=50
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   206.06ms   118.30ms   466.90ms    1.09s    3.30s

  18726 requests in 5.00s, 18192 responses
  Throughput: 3.64K req/s
  Bandwidth:  28.87MB/s
  Status codes: 2xx=18192, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 18192 / 18192 responses (100.0%)
  CPU: 884.2% | Mem: 2.4GiB

=== Best: 4296 req/s (CPU: 854.7%, Mem: 1.5GiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-fletch
httparena-bench-fletch
[skip] fletch does not subscribe to baseline-h2
[skip] fletch does not subscribe to static-h2
[skip] fletch does not subscribe to baseline-h3
[skip] fletch does not subscribe to static-h3
[skip] fletch does not subscribe to unary-grpc
[skip] fletch does not subscribe to unary-grpc-tls
[skip] fletch does not subscribe to echo-ws
httparena-postgres
httparena-postgres
[restore] Restoring CPU governor to performance...

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

Numbers are pretty consistent with the last run — baseline hovering around 175-180K, CPU still sitting at ~1360%. So the isolate scaling thing is definitely a Dart runtime characteristic rather than run-to-run variance.

+1 on @Kaliumhexacyanoferrat's suggestion to merge this and iterate. Having the baseline on main means you'll get automatic diffs on follow-up PRs, which makes it way easier to experiment with different accept patterns or isolate strategies and see what actually moves the needle.

Re: the Content-Length shortcut question — that's a fair point. If the upload test is meant to measure body-read throughput, skipping the read via Content-Length kind of defeats the purpose. But if the direction is toward "advance without reading when possible" then it's more of a framework-level optimization that should be allowed. Probably needs a clear call from @MDA2AV on the intent of the test so all implementations play by the same rules.

@kartikey321
Copy link
Copy Markdown
Contributor Author

Performance fix: isolate count now respects the --cpus cgroup quota

Identified and fixed a core scaling issue: Platform.numberOfProcessors in Dart calls sysconf(_SC_NPROCESSORS_ONLN) which reads the host's total CPU count, completely ignoring Docker's --cpus=N
cgroup limit.

On a 128-CPU runner with --cpus=64 (as the baseline profile sets), the server was spawning 128 isolates competing for 64 CPUs — each isolate getting half the connections it needed to stay busy,
leaving ~50% of the available cores idle. This explains the ~1200% CPU usage instead of the expected ~6400%.

The fix follows the same pattern used by every other framework in this benchmark that explicitly manages worker count:

node / express / fastify / koa / hono - os.availableParallelism()
bun / elysia / hono-bun - nproc in entrypoint.sh
workerman (PHP)- shell_exec('nproc')
kemal (Crystal)-nproc in run.sh

Added entrypoint.sh that passes $(nproc) as a CLI argument to the server binary. nproc on Linux reads the cgroup CPU quota directly (/sys/fs/cgroup/cpu.max), returning 64 inside --cpus=64
regardless of the host's actual CPU count. server.dart uses this value when provided and falls back to Platform.numberOfProcessors outside Docker.

And somehow its not improved. I didnt get the time to debug it on bare metal.

Regarding the content length shortcut, please do notify me if its not upto the mark i will change it.

Ya i agree that we can merge it , when i get the time i will set up a profiler on a linux machine to debug it, alongside native dart:io to see whats the issue, i feel that dart maps isolates to raw cpu cores, but as of now i can't tell whats the issue

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

That's a really solid detective job tracing it to Platform.numberOfProcessors vs cgroup limits — and yeah, every other framework in the benchmark hits the same thing and works around it with nproc.

The fact that it didn't improve is actually the interesting part though. If you're now spawning 64 isolates on 64 CPUs and still only hitting ~1360% CPU, the bottleneck isn't isolate count — it's something upstream preventing those isolates from saturating their cores.

A few things worth investigating:

  1. Accept queue contention — are you using HttpServer.bind(..., shared: true) across isolates? Even with shared: true, Dart's implementation goes through a single accept lock internally. At 64 cores that lock can become the bottleneck itself, with most isolates spinning waiting to accept rather than doing work.

  2. Single-listener distribution — if connections are accepted on one isolate and then handed off, that main isolate becomes the ceiling. Some Dart servers use SO_REUSEPORT via FFI to let the kernel distribute across sockets directly, which avoids that entirely.

  3. Event loop saturation per isolate — Dart isolates are single-threaded event loops. If each isolate can only push ~14K req/s (180K / ~13 active cores), that might just be the ceiling of dart:io's HTTP parser per event loop. Worth comparing against a raw dart:io server without Fletch's routing layer to see if the framework adds overhead or if it's the runtime itself.

The profiler idea is probably the fastest path to an answer — specifically tracking where each isolate spends its time (accept vs parse vs handler vs write). That'll tell you immediately if it's contention or per-isolate throughput.

Re: Content-Length — no worries, let's wait for the call on that and iterate.

Want me to kick off another benchmark run after you push changes, or should we merge as-is and iterate from main?

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 30, 2026

/benchmark --save

running this to persist results

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Benchmark run triggered for fletch (all tests) with --save. Results will be posted here when done.

@MDA2AV MDA2AV merged commit c93da4a into MDA2AV:main Mar 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants