You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
benchmark.yml ran every benchmark but never checked the numbers, so a regression or a broken measurement could not fail the job. This PR saves each result and validates it with a new .github/scripts/benchmark_check.sh.
Measurement bugs fixed along the way:
iperf direct baseline missing in 24 of the last 27 runs. The client connected before iperf -s was listening (tcp connect failed: Connection refused), and iperf still exits 0, so tcp_proxy_server had nothing to be compared against. The step now waits for both ports to listen and uses -y C for parseable output.
tinyhttpd closed the connection after every file response.http_send_file called hio_close at EOF even for keep-alive requests, so wrk saw half of its requests unanswered and "tinyhttpd send file" mostly measured connection setup. Fixed in examples/tinyhttpd.c; the next request is now read only after the file is fully sent, so pipelined responses cannot interleave with the file body.
tinyhttpd then stalled 40 ms per file request. With the connection kept open, Nagle held the body (written after the head) until the client's delayed ACK: 2.4k req/s, p50 41 ms. TCP_NODELAY is now set on accepted connections: 96k req/s, 1 ms.
Checks
Absolute numbers swing by up to 2x between GitHub-hosted runners (e.g. httpd send file 110k–243k req/s), so performance is judged by ratios against peers measured in the same run. nginx serves /ping from its default site on port 80 (return 200 "pong") for the ping comparisons. Ratios are compared at one decimal place; every check fails the job.
check (same run)
limit
libhv echo / median of the other echo servers
>= 1.0
tcp proxy / direct (iperf)
>= 0.4
httpd file (with FileCache) / nginx file
>= 1.2
tinyhttpd file (no FileCache) / nginx file
>= 0.8
httpd 404 / nginx 404
>= 0.8
tinyhttpd 404 / nginx 404
>= 0.8
httpd ping / nginx ping
>= 0.8
tinyhttpd ping / nginx ping
>= 1.0
wrk parse errors also fail the job (printed only when found), and every endpoint must return the expected status (200 / 404) before it is benchmarked. Results go to the job summary, with both sides of each ratio (MB/s, Gbit/s or req/s). Steps run with bash -eo pipefail so a failing command is not hidden by tee, and the job has a 30-minute timeout.
Verification
Over the last 27 runs plus this PR's runs, every limit passes at one decimal place (libhv echo min 0.99, httpd file min 1.33, httpd 404 min 0.95, tinyhttpd 404 min 1.02; tcp proxy 0.45–0.56 over 5 runs; tinyhttpd file 1.06 with both fixes).
The first CI run of this PR, before TCP_NODELAY, fails the tinyhttpd file check (0.0 < 0.8), so that regression would now be caught.
tinyhttpd under ASan/UBSan: two file requests reuse one connection; Connection: close and HTTP/1.0 close after the response; file, /ping, /404, file on one connection; a 1 MB multi-chunk file twice on one connection, byte-identical; two pipelined requests answered in order. No sanitizer reports.
http_send_file closed the connection at EOF even when the response said
"Connection: keep-alive", so every file request cost a new connection and
wrk saw half of its requests go unanswered. Finish the request the same
way as other responses once the whole file has been sent, and only then
read the next request so pipelined responses cannot interleave with the
file body.
Co-authored-by: Cursor <cursoragent@cursor.com>
benchmark.yml ran every benchmark but never looked at the numbers. Save
each result and check it with .github/scripts/benchmark_check.sh:
- correctness: every echo server reports a throughput, libhv HTTP
endpoints leave no more unanswered requests than connections, wrk sees
no parse errors, and every endpoint answers with the expected status
before it is benchmarked
- performance: ratios against peers measured in the same run (nginx and
the other echo servers), since absolute numbers swing by up to 2x
between GitHub-hosted runners
Also wait for iperf and tcp_proxy_server to listen before measuring; the
direct baseline was missing in 24 of the last 27 runs because the client
connected before the server was up.
Co-authored-by: Cursor <cursoragent@cursor.com>
A file response goes out as two writes, the head and then the body. Now
that the connection stays open, Nagle held the body until the client's
delayed ACK, adding ~40 ms to every file request (2.4k req/s in the
benchmark run, latency p50 41 ms).
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject zero or non-numeric requests/sec measurements
.github/scripts/benchmark_check.sh:63
A zero numerator is considered valid here. In particular, wrk reports integer requests/sec, so tinyhttpd_file can complete only one response, stall one request on each of the 100 connections, report Requests/sec: 0, satisfy MAX_UNANSWERED, and then pass because its ratio is info-only. Reject zero/non-numeric inputs before applying the mode so a broken measurement cannot pass the job.
Drop the unanswered-request and per-echo-server checks, compare ratios at
one decimal place, and make all six ratios hard limits:
libhv echo / median >= 1.0
tcp proxy / direct (iperf) >= 0.4
httpd file (with FileCache) / nginx file >= 1.2
tinyhttpd file (no FileCache) / nginx file >= 0.8
httpd 404 / nginx 404 >= 0.8
tinyhttpd 404 / nginx 404 >= 0.8
wrk parse errors still fail the job but are only printed when found.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keeping the connection open here breaks empty-file responses. http_response_dump only emits Content-Length when it is greater than zero, so a successful GET of a zero-byte file has neither a length nor chunked framing; once this path keeps the socket open, an HTTP/1.1 client cannot detect the response boundary and both sides wait indefinitely. Emit Content-Length: 0 for file responses (or mark the response nonpersistent before its headers are sent).
Keep-alive response lacks framing for zero-byte files
examples/tinyhttpd.c:159
Keeping the connection alive is not valid for a zero-byte file response: http_response_dump only emits Content-Length when it is greater than zero, so the client has no framing signal and waits for a body or connection close. Preserve close-delimited framing for zero-length responses (or emit Content-Length: 0) before reusing the connection.
Write the default site explicitly (same root, index and try_files as the
Ubuntu default) with a /ping location, instead of a separate server on
port 8000, so every nginx test hits the same server block.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add an "a / b" column (MB/s, Gbit/s or req/s) next to the ratio, in the
step output and the job summary.
Co-authored-by: Cursor <cursoragent@cursor.com>
Emit Content-Length: 0 or close connection for empty files
examples/tinyhttpd.c:159
A zero-byte file now leaves this connection open even though http_response_dump omits Content-Length when the length is 0. For an HTTP/1.1 response with neither a length nor chunked encoding, clients must wait for EOF to delimit the body, so the request can hang and a pipelined response cannot be parsed. Emit Content-Length: 0 for empty files, or close rather than reusing this connection.
🧠 Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
benchmark.ymlran every benchmark but never checked the numbers, so a regression or a broken measurement could not fail the job. This PR saves each result and validates it with a new.github/scripts/benchmark_check.sh.Measurement bugs fixed along the way:
iperf -swas listening (tcp connect failed: Connection refused), and iperf still exits 0, sotcp_proxy_serverhad nothing to be compared against. The step now waits for both ports to listen and uses-y Cfor parseable output.http_send_filecalledhio_closeat EOF even for keep-alive requests, so wrk saw half of its requests unanswered and "tinyhttpd send file" mostly measured connection setup. Fixed inexamples/tinyhttpd.c; the next request is now read only after the file is fully sent, so pipelined responses cannot interleave with the file body.TCP_NODELAYis now set on accepted connections: 96k req/s, 1 ms.Checks
Absolute numbers swing by up to 2x between GitHub-hosted runners (e.g. httpd send file 110k–243k req/s), so performance is judged by ratios against peers measured in the same run. nginx serves
/pingfrom its default site on port 80 (return 200 "pong") for the ping comparisons. Ratios are compared at one decimal place; every check fails the job.wrk parse errors also fail the job (printed only when found), and every endpoint must return the expected status (200 / 404) before it is benchmarked. Results go to the job summary, with both sides of each ratio (MB/s, Gbit/s or req/s). Steps run with
bash -eo pipefailso a failing command is not hidden bytee, and the job has a 30-minute timeout.Verification
TCP_NODELAY, fails the tinyhttpd file check (0.0 < 0.8), so that regression would now be caught.Connection: closeand HTTP/1.0 close after the response; file,/ping,/404, file on one connection; a 1 MB multi-chunk file twice on one connection, byte-identical; two pipelined requests answered in order. No sanitizer reports.