Repository navigation
Conversation
Duel passed: none of 31 benchmarks slower
31 benchmarks in 13s. A timing is slower when a 99.0% confidence interval for its ratio, the candidate's to the baseline's, is above 1.02.
Across the board
UnsettledA timing too close to the limit to settle doesn't fail the run, but one that's unsettled in run after run is noisy: 30 benchmarks unchanged
|
Add Duel benchmarks in duel/ that guard the hot paths: URI parsing, normalization and joining, building and merging headers, building and writing requests, multipart encoding, parsing responses with llhttp, chaining and merging options, and whole requests end to end. The end-to-end benchmarks serve fixed responses from memory through the socket_class option, so they time steadily. One request over a loopback TCP connection is judged on its allocation counts alone. Add the Perf workflow, which compares each pull request with main and comments with the report, the duel Rake task, the Duel badge, and include .duel.rb in RuboCop. Duel needs Ruby 3.4 or later, so the Gemfile and Rakefile skip it on older Rubies. The Duel job only reads, and a Comment job of its own, which checks nothing out and runs none of the branch's code, comments on the pull request with the report. A change to only the benchmarks, .duel.rb or the Perf workflow is compared too.
|
I updated the Perf workflow here after a review of the same workflow in another project. The Duel job now only reads, and its checkout keeps no credentials. A separate Comment job, which checks nothing out and runs none of the branch's code, posts the report. |
The Perf workflow now comments on a pull request only when Duel fails, as c.comment says by default, and posts a new comment instead of editing the last one. Duel 0.1.1 watches the benchmarks and .duel.rb itself, so .duel.rb only adds the workflow.
|
I updated this to Duel 0.1.1. The Perf workflow now only comments when Duel fails, posts a new comment instead of editing the old one, and the report is much shorter. |
🟢 Duel: 100% of benchmarks passedCompared e7a320e with cdc0d63 (where HEAD left Details: 31 benchmarks in 24s, 5 unsettled
An unsettled timing was too close to the limit to settle. It doesn't fail the run, but one that's unsettled in run after run is noisy. Across the board:
A timing is slower when a 99.0% confidence interval for its ratio, the candidate's to the baseline's, is above 1.02. How Duel decides |
This adds Duel, a gem I wrote to catch performance regressions in CI. On every pull request it runs the benchmarks in
duel/againstmainand the branch side by side, and fails the build if the branch is slower or allocates more.Benchmarking an HTTP client is tricky because sockets are too noisy to time. So most of these 31 benchmarks pass a small in-memory socket through the existing
socket_classoption, which serves a fixed response. That way a whole request (building it, writing it, parsing the response with llhttp, and reading the body) runs without making any system calls, so it can be timed reliably. There's only one benchmark over a real loopback connection, and Duel only checks its allocations.The rest cover URI parsing and normalization, building and merging headers, building and writing requests, multipart encoding, response parsing, and chaining options. Large bodies turned out to be too noisy, because that time is mostly spent copying memory, so those benchmarks use 8 to 128 KB bodies.
To check that it actually catches things, I added a
.duptoRequest::Writer#join_headers, and Duel failed 11 benchmarks. Extra work in the parser'son_header_fieldcallback failed 10, and the profile in the report pointed straight at that method. Comparingmainwith itself passed three times in a row.I didn't benchmark TLS, IDNs, or cookies, since that time is spent in OpenSSL, Addressable, and http-cookie rather than in this gem.
Duel requires Ruby 3.4, so the Gemfile only adds it on 3.4 and newer, and the Rake task is skipped when it isn't installed. Everything else still works on older Rubies.