bryancall commented on PR #13545:
URL: https://github.com/apache/trafficserver/pull/13545#issuecomment-5344363665
I benchmarked this migration on dedicated hardware rather than reasoning
about it, because a harness swap of this size deserves numbers. Summary up
front: **Uranium is 7.4x faster at 32-way parallelism, loses no code coverage,
uses less memory, and has fewer flaky tests.** Details and methodology below,
including the parts that do not favour the new harness.
## Methodology
Two hosts, both 32 hardware threads, 30 GB RAM, Fedora, gcc 16.1.1, Proxy
Verifier v3.1.3 on both sides (verified identical, checksum `342286244d...`).
Timing on one host, coverage on the other, and each host ran **both** harnesses
so every comparison is within-host.
- master at `a2ea029215`, this branch at `7fcbc9d69`
- Python pinned identically for every run, dependencies resolved from the
checked-in `tests/uv.lock`
- Timing used Release builds; coverage used separate gcov builds, since
`-O0` plus instrumentation distorts wall clock by about 1.5x
- Coverage counters were reset before each run and **only the integration
suite** was measured. Unit tests are identical on both sides and including them
would dilute exactly the delta in question.
**The proxy binary is the same on both sides.** I checked this rather than
assuming it, because the whole comparison depends on it. After normalizing
embedded build paths, both binaries disassemble to **1,341,244 instructions**
and the instruction streams are identical once addresses are masked. The
residual byte differences are relocation displacements from embedded path and
timestamp strings of different lengths, spread across 1,265 unrelated symbols
in single-byte runs. So every number below is harness overhead, not a proxy
change.
## Performance
Full suite, same host, same binary:
| Workers | AuTest | Uranium | Speedup |
|---|---|---|---|
| 4 | 3230s | 914s | 3.5x |
| 8 | 2254s | 497s | 4.5x |
| 16 | 1749s | 293s | 6.0x |
| 32 | 1591s, 1596s | 215s, 215s, 202s | **7.4x** |
The gap widens with worker count, which points at the mechanism:
| | AuTest | Uranium |
|---|---|---|
| Total work | 4873s of worker time | 3899s of test time |
| Wall clock | 1031s | 215s |
| **Effective parallelism** | **4.7x** | **18.1x** |
AuTest's worker durations at `-j32` were min 113s, median 155s, **max
889s**. One worker set the wall clock while roughly 30 sat idle behind it.
**The cause is that AuTest's load balancing never engages.**
`autest-parallel.py` has an LPT balancer that reads
`<sandbox>/test-timings.json`, but that file is never written: the save at line
1135 is gated on `tests_timed > 0` and the per-worker timing dictionaries come
back empty. Every run logs `Using round-robin partitioning` verbatim, and I
confirmed no timings file exists after seven runs. This is not a cold-start
artifact, it is the steady state. Worth knowing regardless of this PR's
outcome, since it means the current suite is leaving most of the machine idle.
Uranium's per-test distribution for reference: p50 1.13s, p95 9.17s, max
90.6s.
## Code coverage
gcov plus gcovr 8.6, identical flags and exclusions on both sides,
integration suite only:
| | AuTest | Uranium | Delta |
|---|---|---|---|
| Lines | 51.0% (96,576 / 189,482) | **51.1%** (96,784 / 189,482) | +208 |
| Functions | 60.7% (11,376 / 18,726) | **60.8%** (11,392 / 18,726) | +16 |
| Branches | 25.6% (77,497 / 302,664) | 25.6% (77,583 / 302,664) | +86 |
**No coverage is lost.** Uranium is fractionally ahead on all three
measures. This is the number that matters most, since a 7.4x speedup naturally
raises the question of whether the suite is simply doing less. It is not. The
denominators are identical on both sides, which independently corroborates the
binary-equivalence check above.
## Memory
Peak resident memory across the whole process tree:
| Workers | AuTest | Uranium |
|---|---|---|
| 4 | 3.78 GB | 3.66 GB |
| 8 | 5.48 GB | 4.52 GB |
| 16 | 7.47 GB | 5.69 GB |
| 32 | 11.0 to 12.26 GB | 9.64 to 10.03 GB |
Uranium uses less memory at every level, with the gap widening to about 31%
at 16 workers. Neither harness came close to exhausting 30 GB.
Caveat worth stating: these are summed RSS across all processes, which
double counts pages shared between the many concurrent Traffic Server
instances, so treat them as upper bounds. The idle baseline was 0.97 GB, so
roughly 92% of each figure is genuine workload. I am re-measuring with
proportional set size to remove the double counting and will follow up if it
changes the picture.
## Flakiness
Seven AuTest runs and six Uranium runs, across all parallelism levels:
| Harness | Test | Failed in | Verdict |
|---|---|---|---|
| AuTest | `per_client_connection_max` | 7/7 | deterministic |
| AuTest | `cripts` | 7/7 | deterministic |
| AuTest | `h2_malformed_request_logging` | 2/7 | **flaky** |
| AuTest | `session_id` | 1/7 | **flaky** |
| Uranium | `test_all_bespoke_tests_are_available_to_pytest` | 6/6 |
deterministic |
| Uranium | `log_mstsms` | 2/6 | **flaky** |
Two observations that only a repeated run surfaces.
**A flaky test was distorting the timing.** AuTest at `-j32` came in
bimodal: 1031s, 1039s, then 1591s, 1596s. The fast runs are the ones where
`h2_malformed_request_logging` **failed**; the slow ones are where it
**passed**. Its success path costs roughly 550 seconds more than its failure
path. A single measurement would have reported a figure that flattered AuTest
by 35%.
**AuTest does not run a deterministic set of tests.** Executed totals across
runs were 586, 586, 586, 586, 586, 583, 581. At `-j8` three tests silently
disappear and at `-j4` five do, with no diagnostic. Uranium collected exactly
**1207 items in all six runs** with no drift. For a suite whose purpose is
regression detection, that difference matters as much as the speed.
## Test inventory
I mapped all 564 master `*.test.py` files against the branch. 388 matched by
directory and stem; I resolved the remaining 176 individually by reading each
successor rather than inferring from names.
**Exactly one test has no successor:
`tests/gold_tests/cache/cache-write-lock-contention.test.py`.** Severity is low
since it was already `SkipUnless(RUN_CACHE_CONTENTION_TEST=1)`, so CI signal is
unchanged, but the scenario is gone and the gate variable is now dead plumbing:
`tests/tools/uranium/runner.py` still forwards `RUN_CACHE_CONTENTION_TEST` into
the container and nothing reads it. Either restore the scenario or drop the
plumbing.
Everything else is verified consolidation, and the counts hold up: the 18
`tls_hooks` files became a 17-entry parametrization plus one function, the 7
`cont_schedule` files became a 7-entry parametrization, 38 `txn_box` tests
became 35 manifests with three merged pairs, and `cache` grew from 39 to 70. I
also confirmed the master merge converted the two tests #13547 added after my
baseline rather than dropping them.
## Other findings
- **`ci/coverage` cannot collect coverage as written.** It calls the
build-tree `./urtest.sh -n "$NPROCS"` without `--no-run-in-docker`. Since
`choose_docker_mode()` returns `not is_container()`, on any non-container host
that shells out to Docker and discards the gcov-instrumented build the script
just made. I had to bypass this script to get the coverage numbers above.
- **`ci/regression` and `ci/jenkins/bin/autest.sh` no longer exercise the
tree they build.** Source-mode `urtest.sh` always runs its own `cmake --preset
urtest` into `build-urtest-container`, so `$DSTROOT` and `${INSTALL}` are
ignored and the build above the test call is dead weight.
- **`tests/uv.lock` never reaches the build tree.** `tests/CMakeLists.txt`
copies only `pyproject.toml`, but `runner.py` runs `uv --project
<build>/tests`, so the vetted lock is unused and every first run resolves fresh
against PyPI. This bit me concretely: without an explicit pin, `uv` selected
**Python 3.12.12** on a host whose system interpreter is 3.14. Copying
`uv.lock` alongside `pyproject.toml` is a one-line fix and makes runs
reproducible.
- **`test_all_bespoke_tests_are_available_to_pytest` fails
deterministically**, in all six runs. A framework self-check reporting that the
declared inventory does not match what pytest collects is worth resolving
before this lands, since it is the test that would otherwise catch a conversion
gap.
- **The documentation contradicts the code on Docker detection.**
`uranium-tests.en.rst` describes a two-condition rule (container **and** Fedora
44); `choose_docker_mode()` checks only `is_container()`.
- **`if(DEFINED ENABLE_AUTEST)` fires even for `-DENABLE_AUTEST=OFF`**,
which will surprise anyone carrying that flag in a script. The companion guard
for `AUTEST_SANDBOX` / `AUTEST_OPTIONS` / `PYTEST_OPTIONS` sits inside
`if(ENABLE_URTEST)`, so it misses the common case.
- **Replay manifests cannot be marked serial.** `_is_serial_test` matches
only `.py` paths and `ReplayItem.runtest()` always takes the shared lock, so a
`.test.yaml` listed in `serial_tests.txt` would be silently ignored. Latent
today since the file lists only `.py` entries, but it is a trap for whoever
first needs an exclusive replay.
- **Sharding moved into the repository, which the PR body understates.**
`SHARD` and `SHARDCNT` appear nowhere on master; the 1of4 through 4of4 split is
external Jenkins configuration. This PR implements sharding in-repo as a modulo
stripe over sorted node IDs, so any existing per-shard intuition about which
tests land where is void, and `ci/jenkins/bin/autest.sh` now passes no shard
flags at all. Worth confirming the Jenkins job definitions move in lockstep.
## Overall
The performance and determinism case is strong and I would not have
predicted the margin. The two things I would want resolved before this leaves
draft are the `ci/coverage` Docker bug, since it silently disables coverage
collection, and the deterministic
`test_all_bespoke_tests_are_available_to_pytest` failure. The single lost test
and the `uv.lock` plumbing are small and easy.
Happy to share the raw logs, per-test timing data, or the gcovr HTML reports
for either side.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]