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 |
Duel 0.1.2 asks a project to regenerate its workflows with `duel init`. The Perf workflow loses its Comment job to a new Perf comment workflow, which runs once Perf has, with a token that can write, so a pull request from a fork gets a comment too. The regenerated Perf workflow also runs for a merge queue, cancels a pull request's stale runs, and no longer checks for a missing commit before a push, which Duel now does itself. .duel.rb takes up the new settings. A push to main is compared with the last commit Duel passed (duel/passed/main), so small slowdowns add up until one fails. A regression there warns, and the workflow comments on the commit, instead of failing the run. Pull requests still fail, unless they have the "duel: accept" label.
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.
After a merge, each push to
mainis compared with the last commit Duel passed, which the workflow marks with aduel/passed/mainbranch, so small slowdowns add up until one is caught. A regression in a push doesn't fail the run. Instead, the workflow comments on the commit with the report. A pull request that's slower on purpose can pass with aduel: acceptlabel (the workflow needs re-running after it's added).