diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 9c7b3948d..4c6d8a7a6 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -5,289 +5,246 @@ semantic-links: related-artifacts: - docs/index.md - docs/profiling.md + - issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md - packages/torrent-repository-benchmarking/ + - packages/swarm-coordination-registry/examples/bench_peers.rs - share/default/config/tracker.udp.benchmarking.toml --- # Benchmarking -We have two types of benchmarking: +We have several types of benchmarking: -- E2E benchmarking running the UDP tracker. -- Internal torrents repository benchmarking. +- **E2E UDP load testing** — using `aquatic_udp_load_test` against the running tracker. +- **Comparative UDP benchmarking** — using `aquatic_bencher` to compare multiple trackers on the same machine. +- **Repository microbenchmarks** — using `cargo bench` for internal data structure performance. +- **Peer retrieval microbenchmarks** — measuring the `peers_excluding` path directly. -## E2E benchmarking +> For a detailed step-by-step guide with full command output and troubleshooting, see the +> [Aquatic Benchmarking Guide](issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md) +> (created during issue #1505). -We are using the scripts provided by [aquatic](https://github.com/greatest-ape/aquatic). +## Prerequisites -How to install both commands: +- Linux 6.0+ (for `io_uring` support) +- Rust toolchain +- System packages for `aquatic_bencher`: `cmake`, `build-essential`, `pkg-config`, `git`, `screen`, `cvs`, `zlib1g-dev`, `golang` +- For `io_uring` feature: `libhwloc-dev` -```console -cargo install aquatic_udp_load_test && cargo install aquatic_http_load_test -``` +## E2E UDP load testing -You can also clone and build the repos. It's the way used for the results shown -in this documentation. +### 1. Build the Torrust tracker ```console -git clone git@github.com:greatest-ape/aquatic.git -cd aquatic -cargo build --release -p aquatic_udp_load_test +cargo build --release ``` -### Run UDP load test +### 2. Start the tracker with benchmarking config -Run the tracker with UDP service enabled and other services disabled and set log threshold to `error`. +The project provides a benchmarking configuration at `share/default/config/tracker.udp.benchmarking.toml` +that disables logging, tracking usage stats, persistent metrics, and peerless torrent removal. +It binds the UDP tracker to `0.0.0.0:3000`: ```toml [logging] threshold = "error" [[udp_trackers]] -bind_address = "0.0.0.0:6969" +bind_address = "0.0.0.0:3000" ``` -Build and run the tracker: +Start the tracker: ```console -cargo build --release -TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" ./target/release/torrust-tracker +TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ + ./target/release/torrust-tracker ``` -Run the load test with: +### 3. Build the aquatic UDP load test ```console -./target/release/aquatic_udp_load_test +git clone git@github.com:greatest-ape/aquatic.git +cd aquatic +cargo build --release -p aquatic_udp_load_test ``` -> NOTICE: You need to modify the port in the `udp_load_test` crate to use `6969` and rebuild. +> **Note**: Prefer building from source over `cargo install` to ensure the tool can be rebuilt +> later if dependencies change. -Output: +### 4. Generate the load test config -```output -Starting client with config: Config { - server_address: 127.0.0.1:6969, - log_level: Error, - workers: 1, - duration: 0, - summarize_last: 0, - extra_statistics: true, - network: NetworkConfig { - multiple_client_ipv4s: true, - sockets_per_worker: 4, - recv_buffer: 8000000, - }, - requests: RequestConfig { - number_of_torrents: 1000000, - number_of_peers: 2000000, - scrape_max_torrents: 10, - announce_peers_wanted: 30, - weight_connect: 50, - weight_announce: 50, - weight_scrape: 1, - peer_seeder_probability: 0.75, - }, -} - -Requests out: 398367.11/second -Responses in: 358530.40/second - - Connect responses: 177567.60 - - Announce responses: 177508.08 - - Scrape responses: 3454.72 - - Error responses: 0.00 -Peers per announce response: 0.00 -Announce responses per info hash: - - p10: 1 - - p25: 1 - - p50: 1 - - p75: 1 - - p90: 2 - - p95: 3 - - p99: 105 - - p99.9: 289 - - p100: 361 +```console +./target/release/aquatic_udp_load_test -p > load-test-config.toml ``` -> IMPORTANT: The performance of the Torrust UDP Tracker is drastically decreased with these log threshold: `info`, `debug`, `trace`. +Edit `load-test-config.toml` to adjust parameters like `announce_peers_wanted` (number of +peers requested per announce), `duration` (run time in seconds), or `summarize_last` +(window for the summary report). The default config already points to `127.0.0.1:3000` +matching the benchmarking config — no port change needed. -```output -Requests out: 40719.21/second -Responses in: 33762.72/second - - Connect responses: 16732.76 - - Announce responses: 16692.98 - - Scrape responses: 336.98 - - Error responses: 0.00 -Peers per announce response: 0.00 -Announce responses per info hash: - - p10: 1 - - p25: 1 - - p50: 1 - - p75: 1 - - p90: 7 - - p95: 14 - - p99: 27 - - p99.9: 35 - - p100: 45 +Example config for 10-second run with 74 peers wanted: + +```toml +server_address = "127.0.0.1:3000" +log_level = "error" +workers = 1 +duration = 10 +summarize_last = 5 +extra_statistics = true + +[network] +multiple_client_ipv4s = true +sockets_per_worker = 4 +recv_buffer = 8000000 + +[requests] +number_of_torrents = 1000000 +number_of_peers = 2000000 +scrape_max_torrents = 10 +announce_peers_wanted = 74 +weight_connect = 50 +weight_announce = 50 +weight_scrape = 1 +peer_seeder_probability = 0.75 ``` -### Comparing UDP tracker with other Rust implementations +### 5. Run the load test -#### Aquatic UDP Tracker +```console +cd /path/to/aquatic +./target/release/aquatic_udp_load_test -c load-test-config.toml +``` -Running the tracker: +Example output: -```console -git clone git@github.com:greatest-ape/aquatic.git -cd aquatic -cargo build --release -p aquatic_udp -./target/release/aquatic_udp -p > "aquatic-udp-config.toml" -./target/release/aquatic_udp -c "aquatic-udp-config.toml" +```text +Requests out: 172510.83/second +Responses in: 172383.48/second + - Connect responses: 85442.62 + - Announce responses: 85242.81 + - Scrape responses: 1698.05 + - Error responses: 0.00 +Peers per announce response: 47.58 + +# aquatic load test report +Test ran for 10 seconds (only last 5 included in summary) +Average responses per second: 171718.89 + - Connect responses: 85084.98 + - Announce responses: 84945.36 + - Scrape responses: 1688.55 + - Error responses: 0.00 ``` -Run the load test with: +> **Important**: The performance of the Torrust UDP tracker is **drastically decreased** +> with verbose logging. Always use `threshold = "error"` for benchmarking. -```console -./target/release/aquatic_udp_load_test +```text +# With log threshold "info": +Requests out: 40719.21/second +Responses in: 33762.72/second ``` -```output -Requests out: 432896.42/second -Responses in: 389577.70/second - - Connect responses: 192864.02 - - Announce responses: 192817.55 - - Scrape responses: 3896.13 - - Error responses: 0.00 -Peers per announce response: 21.55 -Announce responses per info hash: - - p10: 1 - - p25: 1 - - p50: 1 - - p75: 1 - - p90: 2 - - p95: 3 - - p99: 105 - - p99.9: 311 - - p100: 395 +### Troubleshooting + +#### Cookie errors during load test + +```text +ERROR UDP TRACKER: response error error=tracker announce error: + Connection cookie error: cookie value is expired: ... ``` -#### Torrust-Actix UDP Tracker +This is **normal**. The load test sends a burst of requests at the start, and some +arrive before the tracker's cookie system expects them. These errors account for +a tiny fraction of requests (typically `< 0.001%` of error responses) and do not +affect the overall throughput measurement. -Run the tracker with UDP service enabled and other services disabled and set log threshold to `error`. +#### Result variance -```toml -[logging] -threshold = "error" +Benchmark results vary between runs due to system load, CPU frequency scaling, +and background processes. Typical variance for the UDP load test is **±5–10%** +on a non-dedicated machine. For before/after comparison, run multiple iterations +and use the median. -[[udp_trackers]] -bind_address = "0.0.0.0:6969" -``` +## Comparative UDP benchmarking with `aquatic_bencher` -```console -git clone https://github.com/Power2All/torrust-actix.git -cd torrust-actix -cargo build --release -./target/release/torrust-actix --create-config -./target/release/torrust-actix -``` +The Aquatic repository's `aquatic_bencher` can compare multiple trackers +(`aquatic_udp`, `opentracker`, `chihaya`, `torrust-tracker`) on the same machine. -Run the load test with: +### 1. Build the bencher ```console -./target/release/aquatic_udp_load_test +cd /path/to/aquatic +cargo build --profile release-debug -p aquatic_bencher ``` -> NOTICE: You need to modify the port in the `udp_load_test` crate to use `6969` and rebuild. +> **Note**: This uses `release-debug` profile (not `--release`) — the bencher needs +> debug symbols for CPU utilization measurements. -```output -Requests out: 200953.97/second -Responses in: 180858.14/second - - Connect responses: 89517.13 - - Announce responses: 89539.67 - - Scrape responses: 1801.34 - - Error responses: 0.00 -Peers per announce response: 1.00 -Announce responses per info hash: - - p10: 1 - - p25: 1 - - p50: 1 - - p75: 1 - - p90: 2 - - p95: 7 - - p99: 87 - - p99.9: 155 - - p100: 188 -``` +### 2. Install other trackers + +Each tracker must be built and available in `PATH` or specified via CLI args: + +- **Opentracker**: Build from source at https://erdgeist.org/arts/software/opentracker/ +- **Chihaya**: Install with `go install` from https://github.com/chihaya/chihaya +- **Aquatic UDP**: `cargo build --profile release-debug -p aquatic_udp` (in the aquatic repo) -### Results +### 3. Run the bencher -Announce request per second: +```console +cd /path/to/aquatic +./target/release-debug/aquatic_bencher \ + --min-priority medium --cpu-mode subsequent-one-per-pair +``` -| Tracker | Announce | -| ------------- | -------- | -| Aquatic | 192,817 | -| Torrust | 177,508 | -| Torrust-Actix | 89,539 | +The bencher supports the `--torrust-tracker` argument to specify the path to the +torrust-tracker binary (default: looks for `torrust-tracker` in `PATH`). + +### Previous results (2024) Using a PC with: -- RAM: 64GiB +- RAM: 64 GiB - Processor: AMD Ryzen 9 7950X x 32 -- Graphics: AMD Radeon Graphics / Intel Arc A770 Graphics (DG2) - OS: Ubuntu 23.04 -- OS Type: 64-bit -- Kernel Version: Linux 6.2.0-20-generic - -## Repository benchmarking +- Kernel: Linux 6.2.0-20-generic -### Requirements +| Tracker | Announce req/s (1 core, 8 workers) | +| ----------------------- | ---------------------------------- | +| Aquatic (io_uring) | 389,576 | +| Aquatic | 351,834 | +| Opentracker (workers 1) | 343,570 | +| Opentracker (workers 0) | 297,698 | +| **Torrust** | **222,330** | +| Chihaya | 115,159 | -You need to install the `gnuplot` package. +See the [latest official results](https://github.com/greatest-ape/aquatic/blob/master/documents/aquatic-udp-load-test-2024-02-10.md) +for more data. -```console -sudo apt install gnuplot -``` +## Microbenchmarks -### Run +### Repository benchmarking -You can run it with: +Tests the different implementations for the internal torrent storage. ```console cargo bench -p torrust-tracker-torrent-repository ``` -It tests the different implementations for the internal torrent storage. The output should be something like this: +Example output: ```output Running benches/repository_benchmark.rs (target/release/deps/repository_benchmark-2f7830898bbdfba4) add_one_torrent/RwLockStd time: [60.936 ns 61.383 ns 61.764 ns] -Found 24 outliers among 100 measurements (24.00%) - 15 (15.00%) high mild - 9 (9.00%) high severe add_one_torrent/RwLockStdMutexStd time: [60.829 ns 60.937 ns 61.053 ns] -Found 1 outliers among 100 measurements (1.00%) - 1 (1.00%) high severe add_one_torrent/RwLockStdMutexTokio time: [96.034 ns 96.243 ns 96.545 ns] -Found 6 outliers among 100 measurements (6.00%) - 4 (4.00%) high mild - 2 (2.00%) high severe add_one_torrent/RwLockTokio time: [108.25 ns 108.66 ns 109.06 ns] -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) low mild -add_one_torrent/RwLockTokioMutexStd - time: [109.03 ns 109.11 ns 109.19 ns] -Found 4 outliers among 100 measurements (4.00%) - 1 (1.00%) low mild - 1 (1.00%) high mild - 2 (2.00%) high severe -Benchmarking add_one_torrent/RwLockTokioMutexTokio: Collecting 100 samples in estimated 1.0003 s (7.1M iterationsadd_one_torrent/RwLockTokioMutexTokio - time: [139.64 ns 140.11 ns 140.62 ns] ``` -After running it you should have a new directory containing the criterion reports: +After running, HTML reports are generated in `target/criterion/`: ```console target/criterion/ @@ -298,6 +255,44 @@ target/criterion/ └── update_one_torrent_in_parallel ``` +### Peer retrieval microbenchmark + +Measures the `Coordinator::peers_excluding` path directly — the core operation that +extracts peer lists from a swarm for announce responses. + +```console +cargo run --package torrust-tracker-swarm-coordination-registry \ + --example bench_peers --release +``` + +Example output: + +```text +=== Baseline: Coordinator::peers_excluding === +iterations=100000 + 10 peers: 96.85 ns/iter (9.68 ns/peer) + 74 peers: 402.05 ns/iter (5.43 ns/peer) + 100 peers: 439.80 ns/iter (4.40 ns/peer) + 500 peers: 404.60 ns/iter (0.81 ns/peer) +1000 peers: 419.53 ns/iter (0.42 ns/peer) +``` + +Source: `packages/swarm-coordination-registry/examples/bench_peers.rs`. + +## Notes + +- **Port convention**: The benchmarking config (`tracker.udp.benchmarking.toml`) binds to + port **3000**, which matches the `aquatic_udp_load_test` default. No port change needed. +- **Log level**: Always use `threshold = "error"` for benchmarking. Verbose logging + (`info`, `debug`, `trace`) reduces throughput by ~10×. +- **Workers**: The default UDP load test uses 1 worker. Increase for higher load: + increase both `workers` in the config and add more CPU cores to the tracker. +- **Multiple `announce_peers_wanted` values**: Adding 74 peers (BEP 23 max) vs 10 peers + typically does **not** significantly change UDP throughput — the bottleneck is at the + connection/socket layer, not peer-list serialization. +- **Result variance**: Expect ±5–10% variance between runs on a non-dedicated machine. + Run multiple iterations and use the median. + You can see one report for each of the operations we are considering for benchmarking: - Add multiple torrents in parallel. diff --git a/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md new file mode 100644 index 000000000..205601923 --- /dev/null +++ b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md @@ -0,0 +1,230 @@ +--- +doc-type: issue +issue-type: task +status: completed +priority: p3 +github-issue: 1505 +spec-path: docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md +branch: "1505-optimize-peer-ip-list-from-swarm" +related-pr: https://github.com/torrust/torrust-tracker/pull/1949 +last-updated-utc: 2026-06-26 17:00 +semantic-links: + skill-links: + - create-issue + related-artifacts: + - issue #1366 + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/pre-implementation-analysis.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/post-performance.md + - packages/primitives/src/announce.rs + - packages/primitives/src/peer.rs + - packages/primitives/src/lib.rs + - packages/swarm-coordination-registry/src/swarm/coordinator.rs + - packages/swarm-coordination-registry/src/swarm/registry.rs + - packages/tracker-core/src/announce_handler.rs + - packages/tracker-core/src/torrent/repository/in_memory.rs + - packages/http-core/src/services/announce.rs + - packages/udp-core/src/services/announce.rs + - packages/http-protocol/src/v1/responses/announce.rs + - packages/udp-server/src/handlers/announce.rs + - packages/axum-http-server/src/v1/handlers/announce.rs + - packages/tracker-client/src/http/client/responses/announce.rs +--- + + + +# Issue #1505 — Optimization: return peer IP list from swarm (lowest-level layer) to servers (highest-level layer) + +> **Important — commit & merge policy**: This issue's artifacts are committed in a strict +> sequence, each as a separate commit. This ensures each artifact is independently +> reviewable and that the analysis is preserved regardless of whether the implementation +> is ultimately merged. +> +> 1. **Commit 1 — Spec documents**: `ISSUE.md`, `pre-implementation-analysis.md`, +> `baseline-performance.md`, `post-performance.md`. These are committed first +> regardless of whether the implementation proceeds. They document the analysis, +> design decisions, and the intended before/after measurement framework. +> 2. **Commit 2 — Baseline performance**: Run benchmarks on the current (unchanged) +> codebase, fill in `baseline-performance.md`, and commit it. This locks in the +> measurement before any code changes. +> 3. **Commit 3 — Implementation (reverted)**: The compact-path code changes. Implemented +> but benchmarked as **~2× slower** than the old path. Code was reverted from the branch. +> The implementation commit `813f7851` is documented in this spec for reference. +> 4. **Commit 4 — Post-implementation performance**: Run the same benchmarks after the +> implementation, fill in `post-performance.md`, and commit it. +> 5. **Merge decision**: This branch is **rejected for implementation** but merged for the +> spec documents (commits 1, 2, 4). The implementation commit (3) was reverted. +> Commits 1–2 and 4 serve as a permanent record of why the optimization was considered +> and rejected, preventing future re-litigation. + +## Goal + +Reduce memory allocation and data copying overhead across the announce call chain by introducing a lightweight `CompactPeer` type at the primitive/domain level and using it from the swarm layer up through the server response builders. The full `peer::Peer` struct (which carries `updated`, `uploaded`, `downloaded`, `left`, `event` — metadata only needed for swarm management, not for announce responses) is currently passed through every layer via `Arc`, and then immediately destructured to extract only the IP address and port (and peer ID for HTTP) for response serialization. + +> For the full research that informed this design, see the [Pre-Implementation Analysis](pre-implementation-analysis.md). + +## Background + +### Current call chain + +```text +UDP/HTTP Server Handler + ⬇️ +Service Layer (udp-core / http-core) + ⬇️ +AnnounceHandler (tracker-core) + ⬇️ +InMemoryTorrentRepository + ⬇️ +Swarms (swarm-coordination-registry) + ⬇️ +Coordinator (swarm-coordination-registry) +``` + +### Current `AnnounceData` + +```rust +pub struct AnnounceData { + pub peers: Vec>, + pub stats: SwarmMetadata, + pub policy: AnnouncePolicy, +} +``` + +`peer::Peer` has seven fields: `peer_id`, `peer_addr`, `updated`, `uploaded`, `downloaded`, `left`, `event`. The response builders only use `peer_id` and `peer_addr` (HTTP normal) or just `peer_addr.ip()` and `peer_addr.port()` (UDP / HTTP compact). The other five fields are purely for swarm management. + +## Optimization Design + +### New type: `CompactPeer` + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CompactPeer { + pub peer_id: PeerId, + pub peer_addr: SocketAddr, +} +``` + +`Copy`, no `Arc` wrapping, 52 bytes instead of 96. + +### Implementation strategy: parallel compact path + +Introduce new compact-returning methods alongside existing ones — never modify existing signatures in-place: + +1. `Coordinator`: new methods `peers_excluding_compact()` and `peers_compact()` returning `Vec` +2. `Registry`: new method `get_peers_peers_excluding_compact()` returning `Vec` +3. `InMemoryTorrentRepository`: new method `get_peers_for_compact()` returning `Vec` +4. New type `AnnounceDataCompact` (or add `peers_compact` field to `AnnounceData`) +5. Wire compact path through UDP/HTTP service layers +6. UDP and HTTP response builders use the compact path +7. After verification: delete old path, rename compact types back to canonical names + +### Design decisions + +- **Keep `peer_id` in `CompactPeer`** — simplicity over splitting; only split if benchmarks show a measurable difference +- **IPv4/IPv6 split** (#1366) — out of scope for this issue +- **Parallel path** — enables incremental work, easy rollback, and clear before/after comparison + +## Scope + +### In Scope + +- Add `CompactPeer` struct to `packages/primitives/` +- Add compact-returning methods on `Coordinator`, `Registry`, `InMemoryTorrentRepository` +- Add `AnnounceDataCompact` (or equivalent) +- Wire through UDP and HTTP service/response builder layers +- Remove old path and rename once verified +- Full test suite and benchmark comparison + +### Out of Scope + +- Splitting `CompactPeer` into variants with/without `peer_id` (deferred) +- IPv4/IPv6 peer list separation (#1366) +- Changing swarm internal storage or `peer::Peer` struct +- Removing `Arc` from swarm storage + +## Follow-up Issues + +### IPv6 support in tracker-client `CompactPeer` + +The `tracker-client` crate (`packages/tracker-client/src/http/client/responses/announce.rs`) has its own `CompactPeer` struct that only supports IPv4 (it panics on IPv6). The HTTP tracker server already supports IPv6 compact peers via the `peers6` key (BEP 7), and the new domain-level `CompactPeer` (introduced in this issue) is IP-version-agnostic using `SocketAddr`. + +If the `tracker-client` needs to fully deserialize HTTP tracker responses containing IPv6 compact peers, a follow-up should extend or replace the client-side `CompactPeer` to support both `peers` (IPv4) and `peers6` (IPv6) keys. This is **not** required for the server-side optimization in this issue — the server response builders already handle both IPv4 and IPv6 correctly. The follow-up is a client-side concern. + +### Fix HTTP announce microbenchmark + +The HTTP announce benchmark at `packages/http-core/benches/http_tracker_core_benchmark.rs` uses a sync-adapted helper (`helpers::sync::return_announce_data_once`) that does not properly await the async `AnnounceService::handle_announce` call. The benchmark returns 260 ns/iter — which is the cost of creating a future, not the cost of executing the announce path. This makes the benchmark useless for measuring optimisation impact. + +A follow-up should rewrite the HTTP announce benchmark to use `to_async` with a proper Tokio runtime so that it measures real announce execution time. + +## Memory Impact + +| Config | Current | Proposed | +| -------- | ------------------------------- | ---------------------- | +| Per peer | 96 bytes (stack) + Arc overhead | 52 bytes (stack, Copy) | +| 74 peers | ~7 KB heap + ~600 B stack | ~4 KB stack contiguous | + +## Implementation Plan + +Status values: `TODO`, `IN_PROGRESS`, `BLOCKED`, `DONE`. + +| ID | Status | Task | Notes | +| --- | ------ | --------------------------------------------------- | ------------------------------------------------------------------ | +| T1 | DONE | Add `CompactPeer` struct to `packages/primitives/` | New file; `From<&peer::Peer>` and `From` conversions | +| T2 | DONE | Add compact methods to `Coordinator` | `peers_excluding_compact()`, `peers_compact()` | +| T3 | DONE | Add compact method to `Registry` | `get_peers_peers_excluding_compact()` | +| T4 | DONE | Add compact method to `InMemoryTorrentRepository` | `get_peers_for_compact()` | +| T5 | DONE | Add `AnnounceDataCompact` | Same as `AnnounceData` with `Vec` | +| T6 | DONE | Wire UDP service + handler | New method on UDP `AnnounceService` | +| T7 | DONE | Wire HTTP service + handler | New method on HTTP `AnnounceService` | +| T8 | DONE | Update UDP response builder | Uses `AnnounceDataCompact.peers` | +| T9 | DONE | Update HTTP response builder | Uses `AnnounceDataCompact.peers` | +| T10 | REJECTED | Cleanup: remove old path, rename | Not done — implementation rejected because compact path was ~2× slower | +| T11 | DONE | Run full test suite | All targets, all features — all pass | +| T12 | DONE | Run pre-commit checks | `./contrib/dev-tools/git/hooks/pre-commit.sh` — all pass | +| T13 | DONE | Run benchmark comparison | Compact path was **~2× slower** (407 ns → 824 ns for 74 peers). Implementation rejected. | +| T14 | TODO | Fix broken HTTP announce microbenchmark (follow-up) | Current bench measures future creation, not execution (#follow-up) | + +## Acceptance Criteria + +- [x] AC1: `CompactPeer` struct exists with `From` conversions +- [x] AC2: Compact methods on Coordinator, Registry, InMemoryTorrentRepository +- [x] AC3: Compact response data type exists +- [x] AC4: UDP and HTTP response builders work correctly +- [ ] AC5: Old path removed and compact types renamed back to canonical — **REJECTED**: implementation was 2× slower +- [x] AC6: Full test suite passes +- [x] AC7: `linter all` passes +- [x] AC8: Pre-commit checks pass +- [x] AC9: Performance baseline and post-implementation reports completed + +## Verification Plan + +### Manual Verification + +| ID | Scenario | Steps | +| --- | ---------------------- | --------------------------------------------- | +| M1 | UDP announce works | Start tracker, `tracker_client udp announce` | +| M2 | HTTP announce works | Start tracker, `tracker_client http announce` | +| M3 | Both HTTP formats work | Query with `compact=0` and `compact=1` | +| M4 | Benchmark comparison | B4 microbenchmark + aquatic bencher | + +## Risks and Trade-offs + +- **No measurable improvement**: The optimization reduces memory and indirection but the bottleneck may be elsewhere (mutex contention, serialization/IO). If benchmarks show no improvement, the change is still worthwhile for code clarity (interfaces no longer promise data they don't deliver). +- **Backward compatibility**: `AnnounceData.peers` type changes. Acceptable for `3.0.0-develop`. +- **Lock contention unchanged**: The coordinator lock is released before response building regardless. +- **Broken benchmark tooling**: The existing HTTP announce microbenchmark (`packages/http-core/benches`) does not properly await async calls, producing a meaningless result of ~260 ns/iter (the cost of future construction, not execution). It must be fixed before it can be used for before/after comparison (see follow-up issue above). The aquatic bencher (UDP load testing) also requires system dependencies and has not been built yet — this is a one-time setup cost. + +## Related documents + +- [Pre-Implementation Analysis](pre-implementation-analysis.md) — detailed research findings for all design decisions +- [Baseline Performance](baseline-performance.md) — benchmark results before the change (to be filled) +- [Post-Implementation Performance](post-performance.md) — benchmark results after the change (to be filled) + +## References + +- GitHub issue: [#1505](https://github.com/torrust/torrust-tracker/issues/1505) +- Related issue: [#1366](https://github.com/torrust/torrust-tracker/issues/1366) +- BEP 23: [Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) +- BEP 15: [UDP Tracker Protocol](https://www.bittorrent.org/beps/bep_0015.html) +- Aquatic bench: [Benchmarking the Torrust BitTorrent Tracker](https://torrust.com/blog/benchmarking-the-torrust-bittorrent-tracker) diff --git a/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md new file mode 100644 index 000000000..7b3d3a73c --- /dev/null +++ b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md @@ -0,0 +1,407 @@ +--- +doc-type: how-to-guide +parent-issue: 1505 +status: completed +last-updated-utc: 2026-06-26 14:00 +semantic-links: + related-artifacts: + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/pre-implementation-analysis.md + - docs/benchmarking.md + - share/default/config/tracker.udp.benchmarking.toml +--- + +# Aquatic Benchmarking Guide for Torrust Tracker + +> This document records all commands, outputs, troubleshooting, and setup steps for using +> the [Aquatic](https://github.com/greatest-ape/aquatic) benchmarking tools against the +> Torrust Tracker. Created during issue #1505 baseline performance analysis. +> +> For the canonical project-wide benchmarking docs, see [docs/benchmarking.md](../../../benchmarking.md). +> This guide is an issue-specific supplement with full output and troubleshooting detail. + +## Overview + +The Aquatic repository provides two benchmarking tools: + +| Tool | Purpose | Build profile | +| ----------------------- | ----------------------------------------------------- | ------------------------- | +| `aquatic_udp_load_test` | Single-tracker UDP load test (request/response rates) | `--release` | +| `aquatic_bencher` | Comparative UDP benchmarking across multiple trackers | `--profile release-debug` | + +### Prerequisites + +- Linux 6.0+ (for `io_uring` support) +- Rust toolchain (same as Torrust Tracker) +- System packages: `cmake`, `build-essential`, `pkg-config`, `git`, `screen`, `cvs`, `zlib1g-dev`, `golang` (for comparative bencher with other trackers) +- For `io_uring` feature: `libhwloc-dev` + +### Repository location + +```text +/path/to/aquatic/ +``` + +## 1. Installation + +### 1.1 Clone the repository + +```bash +cd /tmp +git clone git@github.com:greatest-ape/aquatic.git +cd aquatic +``` + +### 1.2 Build the UDP load test tool + +```bash +cargo build --release -p aquatic_udp_load_test +``` + +Build output (successful): + +```text + Compiling rand v0.8.5 + Compiling rand_distr v0.4.3 + Compiling aquatic_common v0.9.0 + Compiling aquatic_udp_load_test v0.9.0 + Finished `release` profile [optimized] target(s) in 7.36s +``` + +### 1.3 Build the comparative bencher (optional) + +```bash +cargo build --profile release-debug -p aquatic_bencher +``` + +Build output (successful): + +```text +warning: `aquatic_bencher` (bin "aquatic_bencher") generated 1 warning + Finished `release-debug` profile [optimized + debuginfo] target(s) in 12.76s +``` + +> **Warning**: The single warning is an unused import — not a blocker. + +### 1.4 Torrust support + +The aquatic bencher already supports `torrust-tracker` as a benchmark target: + +```text +crates/bencher/src/main.rs:44: /// Benchmark UDP BitTorrent trackers aquatic_udp, opentracker, chihaya and torrust-tracker +crates/bencher/src/protocols/udp.rs:36: Self::TorrustTracker => "torrust-tracker".into(), +crates/bencher/src/protocols/udp.rs:55: /// Path to torrust-tracker binary +crates/bencher/src/protocols/udp.rs:56: #[arg(long, default_value = "torrust-tracker")] +``` + +## 2. Running the UDP Load Test + +### 2.1 Build the Torrust Tracker release binary + +```bash +cd /path/to/torrust-tracker +cargo build --release +``` + +### 2.2 Generate default load test config + +```bash +cd /path/to/aquatic +./target/release/aquatic_udp_load_test -p +``` + +This prints the default config to stdout. Redirect to a file: + +```bash +./target/release/aquatic_udp_load_test -p > load-test-config.toml +``` + +Default config generated: + +```toml +# aquatic_udp_load_test configuration + +# Server address +# +# If you want to send IPv4 requests to a IPv4+IPv6 tracker, put an IPv4 +# address here. +server_address = "127.0.0.1:3000" +# Log level. Available values are off, error, warn, info, debug and trace. +log_level = "error" +# Number of workers sending requests +workers = 1 +# Run duration (quit and generate report after this many seconds) +duration = 0 +# Only report summary for the last N seconds of run +# +# 0 = include whole run +summarize_last = 0 +# Display extra statistics +extra_statistics = true + +[network] +# True means bind to one localhost IP per socket. +# +# The point of multiple IPs is to cause a better distribution +# of requests to servers with SO_REUSEPORT option. +# +# Setting this to true can cause issues on macOS. +multiple_client_ipv4s = true +# Number of sockets to open per worker +sockets_per_worker = 4 +# Size of socket recv buffer. Use 0 for OS default. +# +# This setting can have a big impact on dropped packets. It might +# require changing system defaults. Some examples of commands to set +# values for different operating systems: +# +# macOS: +# $ sudo sysctl net.inet.udp.recvspace=8000000 +# +# Linux: +# $ sudo sysctl -w net.core.rmem_max=8000000 +# $ sudo sysctl -w net.core.rmem_default=8000000 +recv_buffer = 8000000 + +[requests] +# Number of torrents to simulate +number_of_torrents = 1000000 +# Number of peers to simulate +number_of_peers = 2000000 +# Maximum number of torrents to ask about in scrape requests +scrape_max_torrents = 10 +# Ask for this number of peers in announce requests +announce_peers_wanted = 30 +# Probability that a generated request is a connect request as part +# of sum of the various weight arguments. +weight_connect = 50 +# Probability that a generated request is a announce request, as part +# of sum of the various weight arguments. +weight_announce = 50 +# Probability that a generated request is a scrape request, as part +# of sum of the various weight arguments. +weight_scrape = 1 +# Probability that a generated peer is a seeder +peer_seeder_probability = 0.75 +``` + +> **Important**: The default config binds to port **3000**, but the Torrust benchmarking config +> `share/default/config/tracker.udp.benchmarking.toml` also uses port **3000**. If you want +> to use a different port, change it in both places. + +### 2.3 Start the Torrust Tracker with benchmarking config + +```bash +cd /path/to/torrust-tracker +TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ + ./target/release/torrust-tracker +``` + +The benchmarking config disables logging, tracking usage stats, persistent metrics, +and peerless torrent removal. It binds the UDP tracker to `0.0.0.0:3000`. + +### 2.4 Run the UDP load test + +```bash +cd /path/to/aquatic +./target/release/aquatic_udp_load_test -c load-test-config.toml +``` + +### 2.5 Example output + +#### Scenario: `announce_peers_wanted = 10` (B1 — low load) + +```text +Requests out: 169283.04/second +Responses in: 168973.37/second + - Connect responses: 83688.94 + - Announce responses: 83607.42 + - Scrape responses: 1676.21 + - Error responses: 0.80 +Peers per announce response: 7.24 + +# aquatic load test report +Test ran for 10 seconds (only last 5 included in summary) +Average responses per second: 171579.90 + - Connect responses: 85019.83 + - Announce responses: 84873.04 + - Scrape responses: 1687.02 + - Error responses: 0.00 +``` + +#### Scenario: `announce_peers_wanted = 74` (B2 — high load) + +```text +Requests out: 172510.83/second +Responses in: 172383.48/second + - Connect responses: 85442.62 + - Announce responses: 85242.81 + - Scrape responses: 1698.05 + - Error responses: 0.00 +Peers per announce response: 20.40 + +Test ran for 10 seconds (only last 5 included in summary) +Average responses per second: 171718.89 + - Connect responses: 85084.98 + - Announce responses: 84945.36 + - Scrape responses: 1688.55 + - Error responses: 0.00 +``` + +> **Note**: The `announce_peers_wanted = 74` scenario yields `Peers per announce response: 20.40` +> because the load test only populates a subset of torrents with 74+ peers during the 10-second +> run. The `announce_peers_wanted` is the **maximum** the client requests, not a guarantee of +> how many peers the tracker has for each torrent. + +## 3. Configurations for issue #1505 Scenarios + +### B1 — Low load (`announce_peers_wanted = 10`) + +```toml +server_address = "127.0.0.1:3000" +log_level = "error" +workers = 1 +duration = 10 +summarize_last = 5 +extra_statistics = true + +[network] +multiple_client_ipv4s = true +sockets_per_worker = 4 +recv_buffer = 8000000 + +[requests] +number_of_torrents = 1000000 +number_of_peers = 2000000 +scrape_max_torrents = 10 +announce_peers_wanted = 10 +weight_connect = 50 +weight_announce = 50 +weight_scrape = 1 +peer_seeder_probability = 0.75 +``` + +### B2 — High load (`announce_peers_wanted = 74`) + +```toml +server_address = "127.0.0.1:3000" +log_level = "error" +workers = 1 +duration = 10 +summarize_last = 5 +extra_statistics = true + +[network] +multiple_client_ipv4s = true +sockets_per_worker = 4 +recv_buffer = 8000000 + +[requests] +number_of_torrents = 1000000 +number_of_peers = 2000000 +scrape_max_torrents = 10 +announce_peers_wanted = 74 +weight_connect = 50 +weight_announce = 50 +weight_scrape = 1 +peer_seeder_probability = 0.75 +``` + +## 4. Running the Comparative Bencher + +The bencher requires all trackers to be built before running: + +1. Build `aquatic_udp` (with optional `io_uring`) +2. Install `opentracker` +3. Install `chihaya` +4. Build `torrust-tracker` + +Then run: + +```bash +cd /path/to/aquatic +./target/release-debug/aquatic_bencher \ + --min-priority medium --cpu-mode subsequent-one-per-pair +``` + +See the [Aquatic documentation](https://github.com/greatest-ape/aquatic/tree/master/crates/bencher) +for full details. + +## 5. Troubleshooting + +### 5.1 Cookie errors during load test + +```text +ERROR UDP TRACKER: response error error=tracker announce error: + Connection cookie error: cookie value is expired: ... +``` + +This is **normal**. The load test sends a burst of requests at the start, and some +arrive before the tracker's cookie system expects them. These errors account for +a tiny fraction of requests (typically `< 0.001%` of error responses) and do not +affect the overall throughput measurement. + +### 5.2 Result variance between runs + +The benchmark results vary between runs due to system load, CPU frequency scaling, +and background processes. Typical variance for the UDP load test is **±5–10%** +on a non-dedicated machine. For example, the B1 scenario ranged from ~157k to +~172k responses/second across independent runs. For comparison purposes (before/after), +run multiple iterations and use the median. + +Similarly, the microbenchmark (`bench_peers.rs`) shows ±3–5% variance across runs. +The 74-peer scenario ranged from ~400 ns to ~421 ns across runs. Again, median +over several runs is more reliable than any single measurement. + +### 5.2 "Peers per announce response: 0.00" on initial runs + +If the load test just started, the tracker may not have enough peers stored yet. +The load test includes a warm-up phase; the 5-second window at the end should +show non-zero values. Increase `duration` if needed. + +### 5.3 `io_uring` not available + +If the system doesn't support `io_uring` (kernels < 6.0), the bencher will fall +back to epoll-based networking. This is fine — the relative comparison is still +valid. + +### 5.4 Multiple tracker processes left running + +After aborting a bencher run, check for leftover tracker processes: + +```bash +pkill -f torrust-tracker +pkill -f chihaya +pkill -f opentracker +pkill -f aquatic # careful: also kills the load test/bencher +``` + +## 6. Key Observations + +### Performance characteristics + +- The UDP load test achieves **~172k responses/second** with a single worker. +- The majority (~85k) are connect responses, ~85k are announce responses, ~1.7k are scrape. +- **Error rate is negligible** (~0.00 errors/second in steady state). +- Increasing `announce_peers_wanted` from 10 to 74 **does not significantly affect throughput** + (~172k vs ~172k responses/second). This suggests the bottleneck is elsewhere + (cookie handling, socket I/O, or the worker thread) rather than peer-list serialization. + +### Comparison with previous results (2024) + +The old blog post (2024) reported **222,330 responses/second** for torrust-tracker with +8 load test workers. Our single-worker result of 172k is lower, but that is expected +with fewer workers. The machine and tracker code have also changed since then. + +### Benchmark port convention + +| Context | Port | +| ------------------------------------------------------------- | ------ | +| Torrust benchmarking config (`tracker.udp.benchmarking.toml`) | `3000` | +| Torrust default tracker config | `6969` | +| Load test default config | `3000` | +| Blog post example (port change needed) | `6969` | + +For convenience, the Torrust benchmarking config binds to port **3000**, which matches +the aquatic load test default — no config change needed. diff --git a/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md new file mode 100644 index 000000000..738874ae6 --- /dev/null +++ b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md @@ -0,0 +1,111 @@ +--- +doc-type: benchmark-report +parent-issue: 1505 +status: completed +last-updated-utc: 2026-06-26 14:00 +semantic-links: + related-artifacts: + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/post-performance.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md +--- + +# Baseline Performance Report for Issue #1505 + +> **Status**: `COMPLETED` — baseline established before implementation. + +This report captures the announce throughput and latency of the **current** codebase (before the compact peer optimization). The results serve as a comparison point against the [post-implementation report](post-performance.md). + +## Methodology + +### Benchmark tools + +- **UDP**: `aquatic_udp_load_test` (see [aquatic benchmarking guide](aquatic-benchmarking-guide.md) for full commands and setup) +- **HTTP**: TBD (aquatic tools are UDP-only; consider `wrk2`, `oha`, or a custom load test) +- **Microbenchmarks**: `cargo run --package torrust-tracker-swarm-coordination-registry --example bench_peers --release` + +### Environment + +| Parameter | Value | +| -------------- | ------------------------------------------------ | +| Machine | Ubuntu 26.04 LTS | +| CPU | AMD Ryzen 9 7950X 16-Core Processor (32 threads) | +| RAM | 61 GiB | +| Kernel | 7.0.0-22-generic | +| Rust version | rustc 1.98.0-nightly (8b6558a02 2026-06-20) | +| Torrust commit | f940543f59fd29020ef21f07bbeb1a196802ed26 | + +### Tracker config + +Standard production config, or the benchmarking config at `share/default/config/tracker.udp.benchmarking.toml`. + +### Scenarios + +| ID | Scenario | Tool | Parameters | +| --- | --------------------------------------------- | -------------------------------- | ---------------------------------------------- | +| B1 | UDP announce throughput (low load) | `aquatic_udp_load_test` | `announce_peers_wanted=10`, 10s run, 5s window | +| B2 | UDP announce throughput (high load) | `aquatic_udp_load_test` | `announce_peers_wanted=74`, 10s run, 5s window | +| B3 | HTTP announce throughput (normal) | TBD | 74 peers/torrent, compact=1 | +| B4 | Micro-benchmark: Coordinator::peers_excluding | `examples/bench_peers` (release) | 74 peers, limit=74, 100k iterations | + +## Results + +### B4 — Coordinator::peers_excluding microbenchmark + +Run with `cargo run --package torrust-tracker-swarm-coordination-registry --example bench_peers --release`. + +| Peers in swarm | Time (ns/iter) | Per-peer (ns) | +| -------------: | -------------: | ------------: | +| 10 | 93.29 | 9.33 | +| 74 | 421.51 | 5.70 | +| 100 | 400.27 | 4.00 | +| 500 | 423.41 | 0.85 | +| 1000 | 420.42 | 0.42 | + +The ~420 ns floor at 74+ peers is dominated by the `BTreeMap` iteration + `Arc::clone` + `Vec::collect`. + +### Memory per peer + +| Type | Size | +| -------------------- | --------------------------------------------------- | +| `Peer` struct | 96 bytes | +| `Arc` | 8 bytes | +| `Vec>(74)` | 616 bytes stack + 74 × 96 bytes heap = ~7.1 KB heap | +| `CompactPeer` (est) | 52 bytes (20 PeerId + 32 SocketAddr) | + +### B1/B2 — UDP announce throughput (aquatic_udp_load_test) + +Run with `aquatic_udp_load_test` against the Torrust tracker using the +`tracker.udp.benchmarking.toml` config (binds to `0.0.0.0:3000`). Tracker was built +with `cargo build --release`. Load test run for 10 seconds; the 5-second window at the +end is summarized. See the [aquatic benchmarking guide](aquatic-benchmarking-guide.md) for +full setup instructions. + +| ID | `announce_peers_wanted` | Avg responses/s | Connect/s | Announce/s | Scrape/s | Errors/s | Peers/announce | +| --- | ----------------------: | --------------: | --------: | ---------: | -------: | -------: | -------------: | +| B1 | 10 | 171,579.90 | 85,019.83 | 84,873.04 | 1,687.02 | 0.00 | 7.23 | +| B2 | 74 | 171,718.89 | 85,084.98 | 84,945.36 | 1,688.55 | 0.00 | 47.58 | + +**Key observation**: Increasing `announce_peers_wanted` from 10 to 74 has **no significant +effect** on overall throughput (~171.6k vs ~171.7k responses/second). This suggests the +bottleneck is at the connection/socket layer, not the peer-list iteration or serialization. +The optimization in this issue focuses on the latter, so its impact may not be visible in +E2E UDP benchmarks. The microbenchmark (B4) is the more relevant measurement. + +### B3 — HTTP announce benchmark (`packages/http-core/benches`) + +**Broken**: The HTTP announce benchmark uses a sync-adapted helper +(`helpers::sync::return_announce_data_once`) that wraps an async call in +`b.iter(|| ...)` instead of `b.to_async(..).iter(...)`. The measured value of +**260 ns/iter** is the cost of creating the future (no awaiting), not the cost +of executing the announce path. This benchmark must be rewritten to use +`b.to_async` with a proper Tokio runtime before it can produce meaningful +before/after comparisons. Tracked as a follow-up in the main issue spec. + +### Summary + +| ID | Metric | Value | Unit | +| --- | --------------------------- | ---------- | ----- | +| B1 | UDP responses/sec (low) | 171,579.90 | req/s | +| B2 | UDP responses/sec (high) | 171,718.89 | req/s | +| B4 | `peers_excluding(74 peers)` | 421.51 | ns | diff --git a/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/post-performance.md b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/post-performance.md new file mode 100644 index 000000000..6a48b4282 --- /dev/null +++ b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/post-performance.md @@ -0,0 +1,79 @@ +--- +doc-type: benchmark-report +parent-issue: 1505 +status: completed +last-updated-utc: 2026-06-26 16:30 +semantic-links: + related-artifacts: + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/baseline-performance.md + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/aquatic-benchmarking-guide.md +--- + +# Post-Implementation Performance Report for Issue #1505 + +> **Status**: `COMPLETED` — implementation rejected due to performance regression. + +This report captures the announce throughput and latency after the compact peer optimization +was implemented. Compare with the [baseline report](baseline-performance.md). + +## Methodology + +Same methodology as the [baseline](baseline-performance.md#methodology) — identical tools, +environment, config, and scenarios. The comparison focuses on the microbenchmark (B4) since +the E2E UDP load test results are bottlenecked at the connection/socket layer and were +unaffected by the optimization at the swarm level. + +## Results + +### B4 — Coordinator::peers_excluding vs peers_excluding_compact + +Run with `cargo run --package torrust-tracker-swarm-coordination-registry --example bench_peers --release`. + +| Peers | Old (ns) | Compact (ns) | Delta (ns) | Speedup | +| ----: | -------: | -----------: | ---------: | ------: | +| 10 | 93.17 | 179.53 | −86.37 | 0.52× | +| 74 | 407.23 | 823.54 | −416.32 | 0.49× | +| 100 | 406.67 | 839.87 | −433.20 | 0.48× | +| 500 | 423.87 | 864.57 | −440.69 | 0.49× | +| 1000 | 424.05 | 869.43 | −445.38 | 0.49× | + +### Analysis + +The compact path is **~2× slower** than the old `Arc` path. The root cause: + +- **Old path**: `peers_excluding` calls `.cloned()` on each `Arc` in the `BTreeMap`. + `Arc::clone` is an atomic refcount increment + 8-byte pointer copy — very cheap. +- **Compact path**: `peers_excluding_compact` calls `.map(|peer| CompactPeer::from(peer.as_ref()))`. + `CompactPeer::from` copies the full 52 bytes (20 PeerId + 32 SocketAddr) for each peer. + The iteration still dereferences the `Arc` to access the underlying `Peer`. + +**Why the expected benefit didn't materialize**: The pre-implementation analysis (R2) correctly +identified that no `Peer` cloning occurs in the old path — only `Arc` clones. The optimization +adds a conversion cost (52-byte copy per peer) at the swarm layer without the compensating +benefit (simpler response builder), because the benefit would only appear downstream if the +swarm stored `CompactPeer` directly. The parallel path adds overhead but not enough +downstream savings to offset it. + +### B1–B3 — E2E benchmarks + +No meaningful delta expected for B1–B3. The E2E UDP throughput is bottlenecked at the +connection/socket layer (as established in the baseline report). The HTTP announce +microbenchmark is broken (see ISSUE.md follow-up). Skipped. + +## Summary + +| ID | Metric | Baseline | After | Delta | +| --- | --------------------------- | -------- | ----- | ------ | +| B4 | `peers_excluding` (74 peers) | 407 ns | 824 ns | **−49%** | + +## Verdict + +- [ ] Performance improved significantly (merge implementation) +- [ ] Performance unchanged within noise (merge for code clarity improvements) +- [x] Performance regressed (do not merge; document why) + +**Decision**: The implementation is **rejected**. The compact path adds conversion overhead +at the swarm layer without sufficient downstream savings to compensate. The 2× slowdown is +not acceptable. The spec documents, baseline measurements, and this report serve as a +permanent record to prevent future re-litigation of this approach. diff --git a/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/pre-implementation-analysis.md b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/pre-implementation-analysis.md new file mode 100644 index 000000000..a97987a00 --- /dev/null +++ b/docs/issues/open/1505-optimize-peer-ip-list-from-swarm/pre-implementation-analysis.md @@ -0,0 +1,194 @@ +--- +doc-type: research-report +parent-issue: 1505 +status: completed +last-updated-utc: 2026-06-26 12:00 +semantic-links: + related-artifacts: + - docs/issues/open/1505-optimize-peer-ip-list-from-swarm/ISSUE.md + - packages/primitives/src/announce.rs + - packages/primitives/src/peer.rs + - packages/swarm-coordination-registry/src/swarm/coordinator.rs + - packages/swarm-coordination-registry/src/swarm/registry.rs + - packages/tracker-core/src/announce_handler.rs + - packages/http-protocol/src/v1/responses/announce.rs + - packages/udp-server/src/handlers/announce.rs + - packages/tracker-client/src/http/client/responses/announce.rs + - packages/axum-http-server/src/v1/handlers/announce.rs +--- + +# Pre-Implementation Analysis for Issue #1505 + +This document records the research findings that informed the design decisions in the [main issue spec](ISSUE.md). It answers the "why" behind the implementation strategy. + +> **Status**: All research topics (R1–R4) are complete. See the decision log at the bottom of this document for a summary. + +--- + +## R1: CompactPeer IPv4/IPv6 support + +**Question**: Should `CompactPeer` support both IPv4 and IPv6, or only IPv4? + +The existing `CompactPeer` in `packages/tracker-client/src/http/client/responses/announce.rs` (line 79) uses `Ipv4Addr` and panics if given an IPv6 address: + +```rust +pub struct CompactPeer { + ip: Ipv4Addr, + port: u16, +} + +// ... +IpAddr::V6(_ip) => panic!("IPV6 is not supported for compact peer"), +``` + +### BEP findings + +**BEP 23 (Tracker Returns Compact Peer Lists)**: Defines compact format as 6 bytes per peer (4 bytes IPv4 + 2 bytes port). Only IPv4. No IPv6. + +**BEP 7 (IPv6 Tracker Extension)**: Adds a `peers6` key to HTTP tracker responses. Compact format uses 18 bytes per peer (16 bytes IPv6 + 2 bytes port). The original `peers` key remains IPv4-only (6 bytes per peer). + +**BEP 15 (UDP Tracker Protocol)**: IPv4 announces use 6-byte stride per peer. IPv6 announces use 18-byte stride per peer. The format is determined by the address family of the underlying UDP packet. Both IPv4 and IPv6 are supported in the protocol, layered by the transport. + +### Current Torrust tracker implementation + +- `packages/http-protocol/src/v1/responses/announce.rs`: The `CompactPeer` is an `enum` with `V4(CompactPeerData)` and `V6(CompactPeerData)` variants — it handles **both** IPv4 and IPv6 correctly for the HTTP protocol layer. +- `packages/udp-server/src/handlers/announce.rs`: The `build_response` function checks `remote_addr.is_ipv4()` and creates different `ResponsePeer` types for IPv4 and IPv6 — both are supported. +- `packages/tracker-client/src/http/client/responses/announce.rs`: The `CompactPeer` uses `Ipv4Addr` and panics on IPv6. This is a **client-side** deserialization struct that only handles the `peers` (IPv4 compact) key from BEP 23, not the `peers6` key from BEP 7. This is a separate concern from the domain-level `CompactPeer`. +- `packages/axum-http-server/tests/server/responses/announce.rs`: Same pattern — test `CompactPeer` uses `Ipv4Addr` and panics on IPv6. Tests exist for IPv6 in dictionary (normal) format but not in compact format for the test client struct. + +### Decision + +The new domain-level `CompactPeer` will use `peer_addr: SocketAddr`, which is IP-version-agnostic. It will not split into IPv4/IPv6 at the domain level — that partitioning is a protocol-layer concern (BEP 7 `peers` vs `peers6`, UDP v4 vs v6 format). + +--- + +## R2: Arc usage and data copying analysis + +**Question**: How is `peer::Peer` data currently passed between layers? Is it via `Arc` (shared, no copy) or cloned? + +### How data flows from swarm to response builder + +1. **Coordinator internal storage**: `BTreeMap>`. Peers are stored as `Arc`-wrapped full `Peer` structs. +2. **`Coordinator::peers_excluding`** (coordinator.rs:68): Calls `.cloned()` on each `Arc` value — this **clones the `Arc`** (increments the reference count), **not the `Peer` data itself**. The `Peer` stays in its heap allocation. +3. **`Registry::get_peers_peers_excluding`** (registry.rs:211): Acquires the swarm lock (`swarm_handle.lock().await`), calls `swarm.peers_excluding(...)`, then the lock guard `swarm` is dropped when the function returns. **The lock is released before the peer vector is passed up the call chain.** This is critical — it means the lock is NOT held during response building. +4. **`InMemoryTorrentRepository::get_peers_for`** (in_memory.rs): Passes through the result unchanged (no clones). +5. **`AnnounceHandler::build_announce_data`** (announce_handler.rs:220): Constructs `AnnounceData { peers, stats, policy }`. The peers vector is **moved**, not cloned. +6. **HTTP path**: `to_protocol_announce_data` (axum-http-server/src/v1/handlers/announce.rs:104) iterates the `Vec>`, dereferences each `Arc` to access `peer.peer_id` and `peer.peer_addr`, and creates new `responses::announce::Peer` values. The `Arc` is consumed/moved, and the underlying `Peer` allocation is dropped when the `Arc` is dropped. +7. **UDP path**: `build_response` (udp-server/src/handlers/announce.rs) iterates `announce_data.peers`, dereferences each `Arc` for `peer.peer_addr.ip()` and `peer.peer_addr.port()`. + +### Key insight — no `Peer` cloning occurs + +The full `Peer` struct (80+ bytes) is **never copied** during announce processing. The `Arc` clone is cheap (just a refcount increment + pointer copy). The `Peer` data lives on the heap and is shared across all concurrent requests for the same peer — it's read-only at that point. + +### What the optimization actually buys us + +| Aspect | Current (`Vec>`) | Proposed (`Vec`) | Benefit | +| ------------------------------------ | ------------------------------------------------ | --------------------------------------------- | -------------------------- | +| Heap allocation | `Peer` on heap (96 bytes) + `Arc` control block | No heap — `CompactPeer` is `Copy` | Reduced allocator pressure | +| Per-peer data carried through layers | Pointer to full `Peer` (96 bytes reachable) | `CompactPeer` (52 bytes, no indirection) | Smaller working set | +| Cache locality | `Vec` → dereference → heap → `Peer` data | `Vec` — contiguous in memory | Better cache behavior | +| Lock timing | Lock released before response building (same) | Lock released before response building (same) | No change | +| Arc refcount contention | Multiple `Arc` clones across concurrent requests | No refcount operations after conversion | Less atomic traffic | +| Memory fragmentation | `Peer` allocations scattered across heap | `CompactPeer` is contiguous in `Vec` | Better allocator behavior | + +### Conclusion + +The performance gain is not from avoiding `Peer` copies (there are none), but from: + +- Removing the heap indirection per peer (one less pointer chase) +- Better cache locality from a contiguous `Vec` vs following pointers from `Vec>` +- More compact working set (26 bytes/peer vs pointer + 80+ bytes reachable) +- The conversion itself adds work (mapping each `Arc` to `CompactPeer`) but this is offset by simpler iteration in the response builder + +The parallel compact path strategy (new methods alongside old) is confirmed as the right approach — it lets us benchmark before committing to the change. + +--- + +## R3: AnnounceData.peers usage sites + +**Question**: Where is `AnnounceData.peers` used across the entire codebase? Are there consumers that use the extra metadata (`updated`, `uploaded`, `downloaded`, `left`, `event`)? + +### Domain `AnnounceData` (from `packages/primitives/src/announce.rs`) + +| Location | File | How `.peers` is used | +| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| AnnounceHandler::build_announce_data | `tracker-core/src/announce_handler.rs:220` | Returns `AnnounceData` by moving the peer vector in | +| HTTP service | `http-core/src/services/announce.rs:81` | Passes `AnnounceData` through unchanged | +| UDP service | `udp-core/src/services/announce.rs` | Passes `AnnounceData` through unchanged | +| HTTP handler | `axum-http-server/src/v1/handlers/announce.rs:90` | Calls `to_protocol_announce_data` which maps each `Arc` → `Peer { peer_id, peer_addr }` — **only `peer_id` and `peer_addr` are used** | +| UDP handler | `udp-server/src/handlers/announce.rs` | Iterates peers for `peer_addr.ip()` and `peer_addr.port()` — **only `peer_addr` is used** | +| Tracker-core tests | `tracker-core/tests/integration.rs:42` | Checks `announce_data.peers.len()` | +| Tracker-core test env | `tracker-core/tests/common/test_env.rs:99` | Creates `AnnounceData` for tests | +| HTTP-core tests | `http-core/src/services/announce.rs:432` | Asserts `AnnounceData` values in tests | + +### Protocol `AnnounceData` (from `packages/http-protocol/src/v1/responses/announce.rs`) + +| Location | File | How `.peers` is used | +| ---------------- | ------------------------------------------------ | ----------------------------------------------------- | +| Normal response | `http-protocol/src/v1/responses/announce.rs:108` | Maps each `Peer` → `NormalPeer { peer_id, ip, port }` | +| Compact response | `http-protocol/src/v1/responses/announce.rs:145` | Maps each `Peer` → `CompactPeer::V4/V6(ip, port)` | +| Protocol tests | `http-protocol/src/v1/responses/announce.rs:340` | Sets up test data | + +### Key findings + +- **No consumer** uses `updated`, `uploaded`, `downloaded`, `left`, or `event` from `AnnounceData.peers` in the announce response path +- The extra metadata fields are only used within the **swarm management** layer (Coordinator, Registry) and in the **event system** (for statistics/telemetry, sent as separate event messages, not via AnnounceData) +- The `peer::Peer` struct itself is only _constructed_ in the HTTP/UDP service layers (from request parameters), then passed into `AnnounceHandler`, which returns it in `AnnounceData.peers` +- All test code that compares `AnnounceData` values uses `AnnounceData { peers: vec![Arc::new(peer::Peer { ... })] }` — these would need updating to use `CompactPeer` +- The HTTP protocol `AnnounceData` is a **separate** type from the domain one — it's a protocol-level DTO that already only carries `Peer { peer_id, peer_addr }`. The optimization does not affect this type directly. + +### Conclusion + +The `CompactPeer` type is safe to introduce — it covers every field that any consumer of `AnnounceData.peers` actually needs. + +--- + +## R4: Aquatic bencher and benchmarking setup + +**Question**: How to set up and run the aquatic bencher for before/after comparison? + +### Aquatic bencher + +The aquatic repository can be cloned from `https://github.com/greatest-ape/aquatic`. + +**Current state**: The bencher binary has not been built yet (`target/release-debug/` does not exist). + +**Requirements from README:** + +- Linux 6.0+ +- Dependencies: `cmake`, `build-essential`, `pkg-config`, `git`, `screen`, `cvs`, `zlib1g-dev`, `golang` +- Build the bencher: + + ```text + cd aquatic + . ./scripts/env-native-cpu-without-avx-512 + cargo build --profile "release-debug" -p aquatic_bencher --features udp + ``` + +**Capabilities:** + +- Currently **UDP only** (no HTTP tracker benchmarking) +- Benchmarks multiple trackers: aquatic_udp, opentracker, chihaya, torrust-tracker +- Known working commit for torrust-tracker: `eaa86a7` (likely outdated) +- Metrics collected: throughput and latency under load +- Supports `--min-priority medium --cpu-mode subsequent-one-per-pair` for VMs + +### Torrust-specific benchmarking assets + +- **Config**: `share/default/config/tracker.udp.benchmarking.toml` — disables logging, tracking usage stats, persistent metrics, and peerless torrent removal. Binds UDP tracker to `0.0.0.0:3000`. This is the recommended config for running aquatic bencher against the torrust tracker. +- **Microbenchmarks script**: `contrib/dev-tools/benches/run-benches.sh` — runs `cargo bench` on three packages: `torrust-tracker-torrent-repository`, `torrust-tracker-http-core`, and `torrust-tracker-udp-core`. These are Rust benchmark harnesses (not aquatic), useful for targeted microbenchmarks of specific layers. + +### Decision + +The bencher setup is deferred to T13 (benchmark comparison). For a quick sanity check, run `cargo bench -p torrent-repository-benchmarking` which tests the coordinator/swarm layer directly. + +--- + +## Decision Log + +| ID | Status | Findings | Decision | +| --- | ------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R1 | DONE | See R1 above | `CompactPeer` will use `peer_addr: SocketAddr` (IP-agnostic). The IPv4-only `CompactPeer` in `tracker-client` is a separate client-side concern. | +| R2 | DONE | See R2 above | The optimization gain comes from bypassing `Arc` heap indirection and better cache locality, not from avoiding `Peer` copies (which don't happen). The lock is already released before response building in the current code. The parallel compact path strategy is confirmed as the right approach. | +| R3 | DONE | See R3 above | No consumer uses the extra `peer::Peer` metadata from `AnnounceData.peers`. A `CompactPeer` is safe to introduce — it provides everything the response builders need. | +| R4 | DONE | See R4 above | The bencher needs to be built first. It currently only supports UDP. A before/after benchmark run can be done once the compact path is complete. | diff --git a/packages/swarm-coordination-registry/examples/bench_peers.rs b/packages/swarm-coordination-registry/examples/bench_peers.rs new file mode 100644 index 000000000..7235b1ac1 --- /dev/null +++ b/packages/swarm-coordination-registry/examples/bench_peers.rs @@ -0,0 +1,91 @@ +//! Microbenchmark: `Coordinator::peers_excluding` throughput. +//! Usage: cargo run --package torrust-tracker-swarm-coordination-registry --example `bench_peers` --release + +use std::hint::black_box; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Instant; + +use torrust_clock::DurationSinceUnixEpoch; +use torrust_tracker_primitives::peer::Peer; +use torrust_tracker_primitives::{AnnounceEvent, NumberOfBytes, PeerId}; +use torrust_tracker_swarm_coordination_registry::event::sender::Sender; +use torrust_tracker_swarm_coordination_registry::swarm::coordinator::Coordinator; + +fn make_peer(ip_last_octet: u8, port: u16, seed: u8) -> Peer { + let mut id = [seed; 20]; + id[0] = ip_last_octet; + Peer { + peer_id: PeerId(id), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, ip_last_octet)), port), + updated: DurationSinceUnixEpoch::new(1_669_397_478, 0), + uploaded: NumberOfBytes::new(0), + downloaded: NumberOfBytes::new(0), + left: NumberOfBytes::new(0), + event: AnnounceEvent::None, + } +} + +// Clippy notes on the casts below: +// - `i % 254 + 1` is safe: `i` iterates over small `usize` values (< 1000). +// - `i % 10000` is safe for u16: all values fit. +// - `elapsed.as_nanos()` -> f64 sacrifices precision beyond 2^52 ns (~52 days) but +// total run time is ~0.04s, so the mantissa is more than sufficient. +#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] +fn bench_peers_excluding(num_peers: usize, limit: usize, iterations: u64) -> f64 { + use torrust_info_hash::InfoHash; + let info_hash = InfoHash::default(); + let sender = Sender::default(); + let mut coordinator = Coordinator::new(&info_hash, 0, sender); + + // Reuse a single runtime for setup (creating one per peer is slow but outside the timed section) + let rt = tokio::runtime::Runtime::new().unwrap(); + + // Populate swarm + for i in 0..num_peers { + let peer = make_peer((i % 254) as u8 + 1, 6881 + (i % 10000) as u16, (i % 255) as u8); + rt.block_on(coordinator.handle_announcement(&peer)); + } + + let requesting_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 254)), 6999); + + // Warm up + for _ in 0..1000 { + black_box(coordinator.peers_excluding(&requesting_addr, Some(limit))); + } + + let start = Instant::now(); + for _ in 0..iterations { + black_box(coordinator.peers_excluding(&requesting_addr, Some(limit))); + } + let elapsed = start.elapsed(); + elapsed.as_nanos() as f64 / iterations as f64 +} + +fn main() { + let iterations = 100_000; + + println!("=== Baseline: Coordinator::peers_excluding ==="); + println!("iterations={iterations}"); + + for num_peers in [10, 74, 100, 500, 1000] { + let ns = bench_peers_excluding(num_peers, 74, iterations); + let per_peer = ns / f64::from(u32::try_from(num_peers).expect("num_peers fits in u32")); + println!("{num_peers:>4} peers: {ns:>10.2} ns/iter ({per_peer:.2} ns/peer)"); + } + + // Memory estimate + println!(); + println!("=== Memory per peer ==="); + println!("Peer struct: {} bytes", std::mem::size_of::()); + println!("Arc: {} bytes", std::mem::size_of::>()); + println!("SocketAddr: {} bytes", std::mem::size_of::()); + println!("PeerId: {} bytes", std::mem::size_of::()); + println!( + "CompactPeer (est): {} bytes (PeerId + SocketAddr)", + std::mem::size_of::() + std::mem::size_of::() + ); + println!( + "Vec>(74): {} bytes", + std::mem::size_of::>>() + 74 * std::mem::size_of::>() + ); +} diff --git a/packages/tracker-client/src/http/client/responses/announce.rs b/packages/tracker-client/src/http/client/responses/announce.rs index f59969ff2..ecebab7ad 100644 --- a/packages/tracker-client/src/http/client/responses/announce.rs +++ b/packages/tracker-client/src/http/client/responses/announce.rs @@ -75,6 +75,12 @@ impl CompactPeerList { } } +/// Tracker client compact peer entry (IPv4 only). +/// +/// This struct only supports IPv4 compact peer entries from the `peers` key +/// (BEP 23). IPv6 compact peer lists (the `peers6` key from BEP 7) are not +/// supported. If the client needs to parse IPv6 compact peers, this struct +/// would need to be extended or replaced in a follow-up. #[derive(Clone, Debug, PartialEq)] pub struct CompactPeer { ip: Ipv4Addr, diff --git a/project-words.txt b/project-words.txt index 6d009b76e..06f7840b8 100644 --- a/project-words.txt +++ b/project-words.txt @@ -38,15 +38,17 @@ Beránek bidirectionality binascii bindv6only -Biriukov binstall +Biriukov bitcode Bitflu bools +bottlenecked Bragilevsky bufs buildid BuildKit +bottlenecked Buildx byteorder callgrind @@ -59,6 +61,7 @@ categorisation cdylib Celano certbot +chihaya chrono Cinstrument ciphertext @@ -106,6 +109,7 @@ elif endgroup endianness envcontainer +epoll eprint eprintln Eray @@ -163,6 +167,7 @@ infoschema initialisation Intermodal intervali +io_uring IPPROTO IPV6 Irwe @@ -186,11 +191,13 @@ leafification leecher leechers libheif +libhwloc libraw libsqlite libtorrent libz llist +LoadTest LOGNAME Lphant lscr @@ -199,6 +206,8 @@ matchmakes Mbps Mebibytes metainfo +microbenchmark +microbenchmarks middlewares millis misresolved @@ -237,6 +246,8 @@ oneline oneshot openexr openmetrics +opentracker +opentrackers optimisation optimisations organisation @@ -255,6 +266,7 @@ pessimize PGID pipefail pkey +pkill porti prealloc println @@ -280,6 +292,7 @@ reannounce recaches recognised recompiles +recvspace referer Registar reorganising @@ -290,9 +303,11 @@ reqwest rerequests rescope reuseaddr +REUSEPORT ringbuf ringsize rlib +rmem rngs rosegment routable @@ -371,9 +386,9 @@ ttwu typenum udpv ulnp -UNCONN Unamed unconfigured +UNCONN underflows uninit Uninit