Skip to content

Add an optional benchmark workflow - #57

Merged
Frenzie merged 1 commit into
koreader:masterfrom
pid1:ci/benchmark
Sep 23, 2026
Merged

Frenzie merged 1 commit into
koreader:masterfrom
pid1:ci/benchmark

Conversation

@pid1

@pid1 pid1 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Adds .github/workflows/benchmark.yaml, scripts/benchmark.sh and scripts/benchmark.lua. The shell builds two refs with the repo Dockerfile, starts a container for each and runs the interleaved loop; the Lua seeds redis, drives the load, reads INFO commandstats and writes the job summary.

Advisory. workflow_dispatch only, so it never runs on a push or a pull request and cannot turn anything red. It exits non-zero only when a build fails, a container never answers /healthcheck, or more than 1% of requests fail, never on a timing.

The driver runs inside the server container under /opt/openresty/bin/resty, which the image already installs. HTTP and redis are then both on loopback, so there is no container networking to arrange and redis needs no exposed port. The driver is copied in at run time, so it also works against a ref that predates it.

Method: arms interleaved across repetitions, FLUSHDB and a seed write before every measurement, a discarded warmup, CONFIG RESETSTAT immediately before each measured window, and every figure a paired ratio against the base within the same repetition. Redis commands per request are reported separately from latency and throughput, because the first is exact and the second is not trustworthy on shared hardware.

Run it from Actions, Benchmark, Run workflow. Locally: ./scripts/benchmark.sh --engine podman --base origin/master --head HEAD.

Verified under podman on an M3 Pro, d941a63 against current master, 3 repetitions of 5 seconds at 2 connections:

| endpoint                 | base | head | change |
| PUT /syncs/progress      | 6.00 | 5.00 |  -1.00 |
| GET /syncs/progress/C1   | 4.00 | 3.00 |  -1.00 |

PUT p50 254us -> 193us, -24.9% paired (-25.1..-23.1), rps +32.4% (+30.5..+32.9)
GET p50 243us -> 194us, -21.0% paired (-22.1..-17.7), rps +27.0% (+19.9..+27.0)
555395 requests, 0 non-200 or failed.

The per-command table attributes all of it to select, which is #58. Base and head both on master gives the noise floor on that machine: paired p50 within 7.2% and rps within 5.8%.

actionlint is clean; shellcheck reports only SC2329 on the trap handler.

The workflow has not run on a GitHub runner, so the timeout, the two builds against a cold cache, and whether the timing half is legible on two shared vCPUs are guesses. The command counts do not depend on any of that.

No new dependencies: everything the driver uses is in the image already.

@Frenzie

Frenzie commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Doing it in Python seems amusing. ;-)

It's potentially worth keeping in mind that GHA could pull https://github.com/koreader/koreader-sync-server/pkgs/container/kosync which I think contains the necessary Lua equivalents. It could even query Redis with redis-lua. But the networking might be annoying.

tl;dr Since you're using Claude extensively here see to what extent it thinks the suggestion is feasible. Not an objection to Python, mainly curiosity.

@pid1

pid1 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

I'm happy to do whatever language. Python was by my direction, as I wasn't sure if this was something y'all would even be interested in. I can do a Lua rewrite, if that's preferable.

@Frenzie

Frenzie commented Sep 22, 2026

Copy link
Copy Markdown
Member

It's possible it'd be more awkward to do the same thing with Lua; it's just that I think it's the obvious first choice to try in a Lua repo. ;-) I'd say give it a go since it'll just take the LLM some five minutes to figure it out regardless; if it's too awkward we'll go with the Python.

Builds a base ref and a head ref with the repo Dockerfile, runs both in
containers, and drives the same v1 requests at each.

Manual dispatch only. It reports into the job summary, gates nothing, and
exits non-zero only when a build fails, a container never answers
/healthcheck, or more than 1% of requests fail, never on a timing.

Arms are interleaved across repetitions and every figure is a ratio against
the base measured in the same repetition, because a shared runner drifts by
more than most changes worth measuring. Redis commands per request are
reported separately from latency and throughput: those counts are exact, and
the wall clock on shared hardware is not.

scripts/benchmark.sh builds and runs the two containers. scripts/benchmark.lua
drives the load and writes the report, and runs inside the server container
under the resty CLI the image already carries, so the HTTP port and redis are
both on loopback and there is no container networking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pid1

pid1 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Swapped it over to Lua. The driver runs inside the container with resty, so HTTP and redis are both on loopback and there's no networking to arrange.

It's a bit bigger than the Python, 571 lines against 423, mostly because the image has no HTTP client so it writes the requests itself.

Worth noting the rewrite caught something the Python had wrong: nginx closing a keepalive connection after keepalive_requests was being counted as a failed request.

@Frenzie

Frenzie commented Sep 22, 2026

Copy link
Copy Markdown
Member

It's a bit bigger than the Python, 571 lines against 423, mostly because the image has no HTTP client so it writes the requests itself.

It probably doesn't matter too much, but it includes LuaSocket which definitely comes with one. Something like local http = require("socket.http").

@pid1

pid1 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

You're right that it's in there. It's blocking though, so under resty it would serialise the ngx.thread coroutines and the timings would come out wrong. Cosockets are the nonblocking equivalent, which is why it ended up hand-rolled.

@Frenzie
Frenzie merged commit 46ef6b8 into koreader:master Sep 23, 2026
1 check 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.

2 participants