Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
source preview history blame raw 4.4 KiB

Load testing

Vole ships a load harness that drives a real instance on a loopback port with a temporary data directory. It runs the same server code an operator runs, behind the load build tag so go test ./... and the verify workflow never execute it.

Use it to answer a question with a number: did this change make a route faster, and where does it stop? The limits in Configuration were chosen from reasoning first; a run here is what replaces that with measurement.

One command

Every route:

VOLE_LOAD_SCENARIO=all VOLE_LOAD_CONCURRENCY=4 VOLE_LOAD_DURATION=5s \
  go test -tags load,sqlite_fts5 -run '^TestLoad$' -v ./server

One route, or clones past the limit:

VOLE_LOAD_SCENARIO=clone VOLE_LOAD_CONCURRENCY=16 VOLE_LOAD_DURATION=30s \
  go test -tags load,sqlite_fts5 -run '^TestLoad$' -v ./server

The build needs sqlite_fts5 beside libsqlite3 (see Getting started); -tags load on the command line replaces GOFLAGS, so both belong on it.

Knobs

Variable Default What it does
VOLE_LOAD_SCENARIO clone all, or one of clone, clone-large, blob-small, blob-limit, raw-small, raw-limit, raw-range, overview, tree, commits, compare, change, issue, activity, instance-activity, home, search-code, search-find, artifact, artifact-range, site, comment.
VOLE_LOAD_CONCURRENCY 4 Workers driving the scenario in parallel.
VOLE_LOAD_DURATION 10s How long the workers keep going.

What it seeds

Three repositories and a site:

  • demo: 300 commits; files at several sizes including one at the configured max_blob_bytes (the harness sets 8 MiB, so the file is one a push could deliver); an issue with five comments; a change request whose branch carries a 4000-line diff, with five review comments and an approval by a second account; a release tag with one artifact in the LFS store; and a pages branch served at demo.pages.test.
  • small: 50 commits and small files, so a clone is quick.
  • large: 3000 commits and one 16 MiB incompressible blob, so a clone is not.

demo and large are read once before the run so their indexes are built outside the measurement.

The clone scenario gives every worker a distinct X-Forwarded-For address (the fixture trusts the header on loopback), so the global git_http_max_concurrent is the limit that refuses rather than the per-client one. At the configured git_http_max_concurrent the run stays at 200; above it, refusals arrive as 503 with Retry-After, and a clone caught by one reports the curl failure.

The one scenario that writes

Every scenario above reads. comment posts a comment as the fixture’s writer account, which is one commit on the metadata branch that every write in the instance shares: the issue, the change request, the approval, and the release pointer all move the same ref. It is the scenario to run after touching the write path, and the one where throughput should stay flat as workers are added while latency grows with the queue behind it — a rising error count or a falling throughput means writers are fighting over the branch instead of taking turns.

What it reports

Per scenario: requests and throughput, latency at p50/p90/p99, HTTP statuses, failure reasons, and the peak of process RSS, open file descriptors, goroutines, and git child processes during the run.

What it does not tell you

  • The server runs inside the test process, so RSS and goroutines include the harness and its clients.
  • Child processes are counted by name (http-backend, upload-pack, receive-pack). The harness’s own git clone processes are excluded.
  • Loopback has no network cost and no TLS. The numbers are a floor and a way to compare changes, not a prediction of a real deployment.
  • A warm cache is not simulated beyond the index build: the first run after a restart will be slower.
  • The raw-range and artifact-range scenarios send a Range header and expect 206 with only the requested bytes. Before #119 the raw route answered 200 with the whole file, which is why the scenario exists.

Adding a scenario

A scenario is a step function and a deadline: each worker calls the step in a loop until VOLE_LOAD_DURATION expires, and the step reports the HTTP status it saw and any failure. The seeding, sampling and reporting in server/load_test.go are shared, so a new route is a case and a function.