From 694dc89f037ca2828aa959ac36be26b7ce9eec26 Mon Sep 17 00:00:00 2001 From: CJ_Clippy Date: Fri, 21 Feb 2025 19:47:41 -0800 Subject: [PATCH] git subrepo clone https://github.com/greatest-ape/aquatic ./apps/aquatic subrepo: subdir: "apps/aquatic" merged: "34b45e92" upstream: origin: "https://github.com/greatest-ape/aquatic" branch: "master" commit: "34b45e92" git-subrepo: version: "0.4.9" origin: "???" commit: "???" --- apps/aquatic/.dockerignore | 5 + .../actions/test-file-transfers/action.yml | 16 + .../actions/test-file-transfers/entrypoint.sh | 309 ++ apps/aquatic/.github/workflows/ci.yml | 66 + apps/aquatic/.gitignore | 8 + apps/aquatic/.gitrepo | 12 + apps/aquatic/CHANGELOG.md | 206 + apps/aquatic/Cargo.lock | 3455 +++++++++++++++++ apps/aquatic/Cargo.toml | 60 + apps/aquatic/LICENSE | 202 + apps/aquatic/README.md | 79 + apps/aquatic/TODO.md | 58 + apps/aquatic/crates/bencher/Cargo.toml | 39 + apps/aquatic/crates/bencher/README.md | 112 + apps/aquatic/crates/bencher/src/common.rs | 336 ++ apps/aquatic/crates/bencher/src/html.rs | 230 ++ apps/aquatic/crates/bencher/src/main.rs | 71 + .../crates/bencher/src/protocols/mod.rs | 2 + .../crates/bencher/src/protocols/udp.rs | 558 +++ apps/aquatic/crates/bencher/src/run.rs | 374 ++ apps/aquatic/crates/bencher/src/set.rs | 290 ++ .../aquatic/crates/combined_binary/Cargo.toml | 21 + .../crates/combined_binary/src/main.rs | 91 + apps/aquatic/crates/common/Cargo.toml | 51 + apps/aquatic/crates/common/src/access_list.rs | 197 + apps/aquatic/crates/common/src/cli.rs | 255 ++ apps/aquatic/crates/common/src/cpu_pinning.rs | 240 ++ apps/aquatic/crates/common/src/lib.rs | 185 + apps/aquatic/crates/common/src/privileges.rs | 62 + .../crates/common/src/rustls_config.rs | 59 + apps/aquatic/crates/http/Cargo.toml | 67 + apps/aquatic/crates/http/README.md | 121 + apps/aquatic/crates/http/src/common.rs | 40 + apps/aquatic/crates/http/src/config.rs | 231 ++ apps/aquatic/crates/http/src/lib.rs | 199 + apps/aquatic/crates/http/src/main.rs | 15 + apps/aquatic/crates/http/src/workers/mod.rs | 2 + .../http/src/workers/socket/connection.rs | 466 +++ .../crates/http/src/workers/socket/mod.rs | 302 ++ .../crates/http/src/workers/socket/request.rs | 147 + .../crates/http/src/workers/swarm/mod.rs | 135 + .../crates/http/src/workers/swarm/storage.rs | 543 +++ apps/aquatic/crates/http_load_test/Cargo.toml | 37 + apps/aquatic/crates/http_load_test/README.md | 55 + .../crates/http_load_test/src/common.rs | 38 + .../crates/http_load_test/src/config.rs | 88 + .../aquatic/crates/http_load_test/src/main.rs | 225 ++ .../crates/http_load_test/src/network.rs | 277 ++ .../crates/http_load_test/src/utils.rs | 81 + apps/aquatic/crates/http_protocol/Cargo.toml | 43 + apps/aquatic/crates/http_protocol/README.md | 15 + .../bench_announce_response_to_bytes.rs | 49 + .../benches/bench_request_from_bytes.rs | 22 + .../crates/http_protocol/src/common.rs | 102 + apps/aquatic/crates/http_protocol/src/lib.rs | 4 + .../crates/http_protocol/src/request.rs | 450 +++ .../crates/http_protocol/src/response.rs | 335 ++ .../aquatic/crates/http_protocol/src/utils.rs | 335 ++ .../bendy/benchmark.json | 1 + .../bendy/estimates.json | 1 + .../announce-response-to-bytes/bendy/raw.csv | 1001 +++++ .../bendy/sample.json | 1 + .../bendy/tukey.json | 1 + .../latest/benchmark.json | 1 + .../latest/estimates.json | 1 + .../announce-response-to-bytes/latest/raw.csv | 1001 +++++ .../latest/sample.json | 1 + .../latest/tukey.json | 1 + .../request-from-bytes/latest/benchmark.json | 1 + .../request-from-bytes/latest/estimates.json | 1 + .../request-from-bytes/latest/raw.csv | 1001 +++++ .../request-from-bytes/latest/sample.json | 1 + .../request-from-bytes/latest/tukey.json | 1 + apps/aquatic/crates/peer_id/Cargo.toml | 25 + apps/aquatic/crates/peer_id/README.md | 3 + apps/aquatic/crates/peer_id/src/lib.rs | 293 ++ apps/aquatic/crates/toml_config/Cargo.toml | 23 + apps/aquatic/crates/toml_config/src/lib.rs | 128 + apps/aquatic/crates/toml_config/tests/test.rs | 46 + .../crates/toml_config_derive/Cargo.toml | 20 + .../crates/toml_config_derive/src/lib.rs | 174 + apps/aquatic/crates/udp/Cargo.toml | 73 + apps/aquatic/crates/udp/README.md | 94 + apps/aquatic/crates/udp/src/common.rs | 148 + apps/aquatic/crates/udp/src/config.rs | 262 ++ apps/aquatic/crates/udp/src/lib.rs | 188 + apps/aquatic/crates/udp/src/main.rs | 12 + apps/aquatic/crates/udp/src/swarm.rs | 706 ++++ apps/aquatic/crates/udp/src/workers/mod.rs | 2 + .../crates/udp/src/workers/socket/mio/mod.rs | 194 + .../udp/src/workers/socket/mio/socket.rs | 323 ++ .../crates/udp/src/workers/socket/mod.rs | 71 + .../udp/src/workers/socket/uring/buf_ring.rs | 947 +++++ .../udp/src/workers/socket/uring/mod.rs | 618 +++ .../src/workers/socket/uring/recv_helper.rs | 169 + .../src/workers/socket/uring/send_buffers.rs | 242 ++ .../udp/src/workers/socket/validator.rs | 165 + .../udp/src/workers/statistics/collector.rs | 331 ++ .../crates/udp/src/workers/statistics/mod.rs | 298 ++ .../crates/udp/templates/statistics.css | 22 + .../crates/udp/templates/statistics.html | 278 ++ apps/aquatic/crates/udp/tests/access_list.rs | 110 + apps/aquatic/crates/udp/tests/common/mod.rs | 125 + .../crates/udp/tests/invalid_connection_id.rs | 97 + .../crates/udp/tests/requests_responses.rs | 108 + apps/aquatic/crates/udp_load_test/Cargo.toml | 39 + apps/aquatic/crates/udp_load_test/README.md | 49 + .../crates/udp_load_test/src/common.rs | 32 + .../crates/udp_load_test/src/config.rs | 140 + apps/aquatic/crates/udp_load_test/src/lib.rs | 336 ++ apps/aquatic/crates/udp_load_test/src/main.rs | 13 + .../crates/udp_load_test/src/worker.rs | 439 +++ apps/aquatic/crates/udp_protocol/Cargo.toml | 24 + apps/aquatic/crates/udp_protocol/README.md | 5 + .../aquatic/crates/udp_protocol/src/common.rs | 299 ++ apps/aquatic/crates/udp_protocol/src/lib.rs | 7 + .../crates/udp_protocol/src/request.rs | 414 ++ .../crates/udp_protocol/src/response.rs | 342 ++ apps/aquatic/crates/ws/Cargo.toml | 67 + apps/aquatic/crates/ws/README.md | 121 + apps/aquatic/crates/ws/src/common.rs | 76 + apps/aquatic/crates/ws/src/config.rs | 235 ++ apps/aquatic/crates/ws/src/lib.rs | 215 + apps/aquatic/crates/ws/src/main.rs | 15 + apps/aquatic/crates/ws/src/workers/mod.rs | 2 + .../ws/src/workers/socket/connection.rs | 658 ++++ .../crates/ws/src/workers/socket/mod.rs | 372 ++ .../crates/ws/src/workers/swarm/mod.rs | 167 + .../crates/ws/src/workers/swarm/storage.rs | 726 ++++ apps/aquatic/crates/ws_load_test/Cargo.toml | 38 + apps/aquatic/crates/ws_load_test/README.md | 55 + .../aquatic/crates/ws_load_test/src/common.rs | 29 + .../aquatic/crates/ws_load_test/src/config.rs | 80 + apps/aquatic/crates/ws_load_test/src/main.rs | 237 ++ .../crates/ws_load_test/src/network.rs | 322 ++ apps/aquatic/crates/ws_load_test/src/utils.rs | 20 + apps/aquatic/crates/ws_protocol/Cargo.toml | 38 + apps/aquatic/crates/ws_protocol/README.md | 4 + .../bench_deserialize_announce_request.rs | 62 + apps/aquatic/crates/ws_protocol/src/common.rs | 223 ++ .../ws_protocol/src/incoming/announce.rs | 80 + .../crates/ws_protocol/src/incoming/mod.rs | 43 + .../crates/ws_protocol/src/incoming/scrape.rs | 32 + apps/aquatic/crates/ws_protocol/src/lib.rs | 377 ++ .../ws_protocol/src/outgoing/announce.rs | 15 + .../crates/ws_protocol/src/outgoing/answer.rs | 17 + .../crates/ws_protocol/src/outgoing/error.rs | 24 + .../crates/ws_protocol/src/outgoing/mod.rs | 45 + .../crates/ws_protocol/src/outgoing/offer.rs | 22 + .../crates/ws_protocol/src/outgoing/scrape.rs | 19 + .../latest/benchmark.json | 1 + .../latest/estimates.json | 1 + .../latest/raw.csv | 1001 +++++ .../latest/sample.json | 1 + .../latest/tukey.json | 1 + apps/aquatic/deny.toml | 194 + apps/aquatic/docker/aquatic_udp.Dockerfile | 48 + apps/aquatic/docker/ci.Dockerfile | 18 + .../documents/aquatic-architecture-2024.svg | 3 + .../aquatic-http-load-test-2023-01-25.pdf | Bin 0 -> 152145 bytes ...http-load-test-illustration-2023-01-25.png | Bin 0 -> 50322 bytes .../aquatic-udp-load-test-2023-01-11.pdf | Bin 0 -> 140778 bytes .../aquatic-udp-load-test-2024-02-10.md | 106 + .../aquatic-udp-load-test-2024-02-10.png | Bin 0 -> 59692 bytes ...-udp-load-test-illustration-2023-01-11.png | Bin 0 -> 45761 bytes .../aquatic-ws-load-test-2023-01-25.pdf | Bin 0 -> 169901 bytes ...c-ws-load-test-illustration-2023-01-25.png | Bin 0 -> 65302 bytes .../scripts/bench/setup-udp-bookworm.sh | 50 + apps/aquatic/scripts/ci-file-transfers.sh | 8 + ...aquatic-http-announce-response-to-bytes.sh | 19 + .../aquatic-http-request-from-bytes.sh | 19 + ...aquatic-ws-deserialize-announce-request.sh | 19 + .../scripts/env-native-cpu-without-avx-512 | 9 + apps/aquatic/scripts/gen-tls.sh | 17 + apps/aquatic/scripts/heaptrack.sh | 3 + apps/aquatic/scripts/run-aquatic-http.sh | 5 + .../scripts/run-aquatic-udp-io-uring.sh | 5 + apps/aquatic/scripts/run-aquatic-udp.sh | 5 + apps/aquatic/scripts/run-aquatic-ws.sh | 5 + apps/aquatic/scripts/run-load-test-http.sh | 5 + apps/aquatic/scripts/run-load-test-udp.sh | 5 + apps/aquatic/scripts/run-load-test-ws.sh | 5 + apps/aquatic/scripts/watch-threads.sh | 3 + 183 files changed, 29514 insertions(+) create mode 100644 apps/aquatic/.dockerignore create mode 100644 apps/aquatic/.github/actions/test-file-transfers/action.yml create mode 100755 apps/aquatic/.github/actions/test-file-transfers/entrypoint.sh create mode 100644 apps/aquatic/.github/workflows/ci.yml create mode 100644 apps/aquatic/.gitignore create mode 100644 apps/aquatic/.gitrepo create mode 100644 apps/aquatic/CHANGELOG.md create mode 100644 apps/aquatic/Cargo.lock create mode 100644 apps/aquatic/Cargo.toml create mode 100644 apps/aquatic/LICENSE create mode 100644 apps/aquatic/README.md create mode 100644 apps/aquatic/TODO.md create mode 100644 apps/aquatic/crates/bencher/Cargo.toml create mode 100644 apps/aquatic/crates/bencher/README.md create mode 100644 apps/aquatic/crates/bencher/src/common.rs create mode 100644 apps/aquatic/crates/bencher/src/html.rs create mode 100644 apps/aquatic/crates/bencher/src/main.rs create mode 100644 apps/aquatic/crates/bencher/src/protocols/mod.rs create mode 100644 apps/aquatic/crates/bencher/src/protocols/udp.rs create mode 100644 apps/aquatic/crates/bencher/src/run.rs create mode 100644 apps/aquatic/crates/bencher/src/set.rs create mode 100644 apps/aquatic/crates/combined_binary/Cargo.toml create mode 100644 apps/aquatic/crates/combined_binary/src/main.rs create mode 100644 apps/aquatic/crates/common/Cargo.toml create mode 100644 apps/aquatic/crates/common/src/access_list.rs create mode 100644 apps/aquatic/crates/common/src/cli.rs create mode 100644 apps/aquatic/crates/common/src/cpu_pinning.rs create mode 100644 apps/aquatic/crates/common/src/lib.rs create mode 100644 apps/aquatic/crates/common/src/privileges.rs create mode 100644 apps/aquatic/crates/common/src/rustls_config.rs create mode 100644 apps/aquatic/crates/http/Cargo.toml create mode 100644 apps/aquatic/crates/http/README.md create mode 100644 apps/aquatic/crates/http/src/common.rs create mode 100644 apps/aquatic/crates/http/src/config.rs create mode 100644 apps/aquatic/crates/http/src/lib.rs create mode 100644 apps/aquatic/crates/http/src/main.rs create mode 100644 apps/aquatic/crates/http/src/workers/mod.rs create mode 100644 apps/aquatic/crates/http/src/workers/socket/connection.rs create mode 100644 apps/aquatic/crates/http/src/workers/socket/mod.rs create mode 100644 apps/aquatic/crates/http/src/workers/socket/request.rs create mode 100644 apps/aquatic/crates/http/src/workers/swarm/mod.rs create mode 100644 apps/aquatic/crates/http/src/workers/swarm/storage.rs create mode 100644 apps/aquatic/crates/http_load_test/Cargo.toml create mode 100644 apps/aquatic/crates/http_load_test/README.md create mode 100644 apps/aquatic/crates/http_load_test/src/common.rs create mode 100644 apps/aquatic/crates/http_load_test/src/config.rs create mode 100644 apps/aquatic/crates/http_load_test/src/main.rs create mode 100644 apps/aquatic/crates/http_load_test/src/network.rs create mode 100644 apps/aquatic/crates/http_load_test/src/utils.rs create mode 100644 apps/aquatic/crates/http_protocol/Cargo.toml create mode 100644 apps/aquatic/crates/http_protocol/README.md create mode 100644 apps/aquatic/crates/http_protocol/benches/bench_announce_response_to_bytes.rs create mode 100644 apps/aquatic/crates/http_protocol/benches/bench_request_from_bytes.rs create mode 100644 apps/aquatic/crates/http_protocol/src/common.rs create mode 100644 apps/aquatic/crates/http_protocol/src/lib.rs create mode 100644 apps/aquatic/crates/http_protocol/src/request.rs create mode 100644 apps/aquatic/crates/http_protocol/src/response.rs create mode 100644 apps/aquatic/crates/http_protocol/src/utils.rs create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/benchmark.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/estimates.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/raw.csv create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/sample.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/tukey.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/benchmark.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/estimates.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/raw.csv create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/sample.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/tukey.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/benchmark.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/estimates.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/raw.csv create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/sample.json create mode 100644 apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/tukey.json create mode 100644 apps/aquatic/crates/peer_id/Cargo.toml create mode 100644 apps/aquatic/crates/peer_id/README.md create mode 100644 apps/aquatic/crates/peer_id/src/lib.rs create mode 100644 apps/aquatic/crates/toml_config/Cargo.toml create mode 100644 apps/aquatic/crates/toml_config/src/lib.rs create mode 100644 apps/aquatic/crates/toml_config/tests/test.rs create mode 100644 apps/aquatic/crates/toml_config_derive/Cargo.toml create mode 100644 apps/aquatic/crates/toml_config_derive/src/lib.rs create mode 100644 apps/aquatic/crates/udp/Cargo.toml create mode 100644 apps/aquatic/crates/udp/README.md create mode 100644 apps/aquatic/crates/udp/src/common.rs create mode 100644 apps/aquatic/crates/udp/src/config.rs create mode 100644 apps/aquatic/crates/udp/src/lib.rs create mode 100644 apps/aquatic/crates/udp/src/main.rs create mode 100644 apps/aquatic/crates/udp/src/swarm.rs create mode 100644 apps/aquatic/crates/udp/src/workers/mod.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/mio/mod.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/mio/socket.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/mod.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/uring/buf_ring.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/uring/mod.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/uring/recv_helper.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/uring/send_buffers.rs create mode 100644 apps/aquatic/crates/udp/src/workers/socket/validator.rs create mode 100644 apps/aquatic/crates/udp/src/workers/statistics/collector.rs create mode 100644 apps/aquatic/crates/udp/src/workers/statistics/mod.rs create mode 100644 apps/aquatic/crates/udp/templates/statistics.css create mode 100644 apps/aquatic/crates/udp/templates/statistics.html create mode 100644 apps/aquatic/crates/udp/tests/access_list.rs create mode 100644 apps/aquatic/crates/udp/tests/common/mod.rs create mode 100644 apps/aquatic/crates/udp/tests/invalid_connection_id.rs create mode 100644 apps/aquatic/crates/udp/tests/requests_responses.rs create mode 100644 apps/aquatic/crates/udp_load_test/Cargo.toml create mode 100644 apps/aquatic/crates/udp_load_test/README.md create mode 100644 apps/aquatic/crates/udp_load_test/src/common.rs create mode 100644 apps/aquatic/crates/udp_load_test/src/config.rs create mode 100644 apps/aquatic/crates/udp_load_test/src/lib.rs create mode 100644 apps/aquatic/crates/udp_load_test/src/main.rs create mode 100644 apps/aquatic/crates/udp_load_test/src/worker.rs create mode 100644 apps/aquatic/crates/udp_protocol/Cargo.toml create mode 100644 apps/aquatic/crates/udp_protocol/README.md create mode 100644 apps/aquatic/crates/udp_protocol/src/common.rs create mode 100644 apps/aquatic/crates/udp_protocol/src/lib.rs create mode 100644 apps/aquatic/crates/udp_protocol/src/request.rs create mode 100644 apps/aquatic/crates/udp_protocol/src/response.rs create mode 100644 apps/aquatic/crates/ws/Cargo.toml create mode 100644 apps/aquatic/crates/ws/README.md create mode 100644 apps/aquatic/crates/ws/src/common.rs create mode 100644 apps/aquatic/crates/ws/src/config.rs create mode 100644 apps/aquatic/crates/ws/src/lib.rs create mode 100644 apps/aquatic/crates/ws/src/main.rs create mode 100644 apps/aquatic/crates/ws/src/workers/mod.rs create mode 100644 apps/aquatic/crates/ws/src/workers/socket/connection.rs create mode 100644 apps/aquatic/crates/ws/src/workers/socket/mod.rs create mode 100644 apps/aquatic/crates/ws/src/workers/swarm/mod.rs create mode 100644 apps/aquatic/crates/ws/src/workers/swarm/storage.rs create mode 100644 apps/aquatic/crates/ws_load_test/Cargo.toml create mode 100644 apps/aquatic/crates/ws_load_test/README.md create mode 100644 apps/aquatic/crates/ws_load_test/src/common.rs create mode 100644 apps/aquatic/crates/ws_load_test/src/config.rs create mode 100644 apps/aquatic/crates/ws_load_test/src/main.rs create mode 100644 apps/aquatic/crates/ws_load_test/src/network.rs create mode 100644 apps/aquatic/crates/ws_load_test/src/utils.rs create mode 100644 apps/aquatic/crates/ws_protocol/Cargo.toml create mode 100644 apps/aquatic/crates/ws_protocol/README.md create mode 100644 apps/aquatic/crates/ws_protocol/benches/bench_deserialize_announce_request.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/common.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/incoming/announce.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/incoming/mod.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/incoming/scrape.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/lib.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/announce.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/answer.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/error.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/mod.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/offer.rs create mode 100644 apps/aquatic/crates/ws_protocol/src/outgoing/scrape.rs create mode 100644 apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/benchmark.json create mode 100644 apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/estimates.json create mode 100644 apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/raw.csv create mode 100644 apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/sample.json create mode 100644 apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/tukey.json create mode 100644 apps/aquatic/deny.toml create mode 100644 apps/aquatic/docker/aquatic_udp.Dockerfile create mode 100644 apps/aquatic/docker/ci.Dockerfile create mode 100644 apps/aquatic/documents/aquatic-architecture-2024.svg create mode 100644 apps/aquatic/documents/aquatic-http-load-test-2023-01-25.pdf create mode 100644 apps/aquatic/documents/aquatic-http-load-test-illustration-2023-01-25.png create mode 100644 apps/aquatic/documents/aquatic-udp-load-test-2023-01-11.pdf create mode 100644 apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.md create mode 100644 apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.png create mode 100644 apps/aquatic/documents/aquatic-udp-load-test-illustration-2023-01-11.png create mode 100644 apps/aquatic/documents/aquatic-ws-load-test-2023-01-25.pdf create mode 100644 apps/aquatic/documents/aquatic-ws-load-test-illustration-2023-01-25.png create mode 100755 apps/aquatic/scripts/bench/setup-udp-bookworm.sh create mode 100644 apps/aquatic/scripts/ci-file-transfers.sh create mode 100755 apps/aquatic/scripts/criterion/aquatic-http-announce-response-to-bytes.sh create mode 100755 apps/aquatic/scripts/criterion/aquatic-http-request-from-bytes.sh create mode 100755 apps/aquatic/scripts/criterion/aquatic-ws-deserialize-announce-request.sh create mode 100644 apps/aquatic/scripts/env-native-cpu-without-avx-512 create mode 100755 apps/aquatic/scripts/gen-tls.sh create mode 100755 apps/aquatic/scripts/heaptrack.sh create mode 100755 apps/aquatic/scripts/run-aquatic-http.sh create mode 100755 apps/aquatic/scripts/run-aquatic-udp-io-uring.sh create mode 100755 apps/aquatic/scripts/run-aquatic-udp.sh create mode 100755 apps/aquatic/scripts/run-aquatic-ws.sh create mode 100755 apps/aquatic/scripts/run-load-test-http.sh create mode 100755 apps/aquatic/scripts/run-load-test-udp.sh create mode 100755 apps/aquatic/scripts/run-load-test-ws.sh create mode 100755 apps/aquatic/scripts/watch-threads.sh diff --git a/apps/aquatic/.dockerignore b/apps/aquatic/.dockerignore new file mode 100644 index 0000000..4636ce6 --- /dev/null +++ b/apps/aquatic/.dockerignore @@ -0,0 +1,5 @@ +target +docker +.git +tmp +documents diff --git a/apps/aquatic/.github/actions/test-file-transfers/action.yml b/apps/aquatic/.github/actions/test-file-transfers/action.yml new file mode 100644 index 0000000..67c1026 --- /dev/null +++ b/apps/aquatic/.github/actions/test-file-transfers/action.yml @@ -0,0 +1,16 @@ +name: 'test-file-transfers' +description: 'test aquatic file transfers' +outputs: + # http_ipv4: + # description: 'HTTP IPv4 status' + http_tls_ipv4: + description: 'HTTP IPv4 over TLS status' + udp_ipv4: + description: 'UDP IPv4 status' + wss_ipv4: + description: 'WSS IPv4 status' +runs: + using: 'composite' + steps: + - run: $GITHUB_ACTION_PATH/entrypoint.sh + shell: bash \ No newline at end of file diff --git a/apps/aquatic/.github/actions/test-file-transfers/entrypoint.sh b/apps/aquatic/.github/actions/test-file-transfers/entrypoint.sh new file mode 100755 index 0000000..7e97f39 --- /dev/null +++ b/apps/aquatic/.github/actions/test-file-transfers/entrypoint.sh @@ -0,0 +1,309 @@ +#!/bin/bash +# +# Test that file transfers work over all protocols. +# +# IPv6 is unfortunately disabled by default in Docker +# (see sysctl net.ipv6.conf.lo.disable_ipv6) + +set -e + +# Install programs and build dependencies + +if command -v sudo; then + SUDO="sudo " +else + SUDO="" +fi + +ulimit -a + +$SUDO apt-get update +$SUDO apt-get install -y cmake libssl-dev screen rtorrent mktorrent ssl-cert ca-certificates curl golang libhwloc-dev + +git clone https://github.com/anacrolix/torrent.git gotorrent +cd gotorrent +# Use commit known to work +git checkout 16176b762e4a840fc5dfe3b1dfd2d6fa853b68d7 +go build -o $HOME/gotorrent ./cmd/torrent +cd .. +file $HOME/gotorrent + +# Go to repository directory + +if [[ -z "${GITHUB_WORKSPACE}" ]]; then + exit 1 +else + cd "$GITHUB_WORKSPACE" +fi + +# Setup bogus TLS certificate + +$SUDO echo "127.0.0.1 example.com" >> /etc/hosts + +openssl genrsa -out ca.key 2048 +openssl req -new -x509 -days 365 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" -out ca.crt +openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=*.example.com" -out server.csr +openssl x509 -req -extfile <(printf "subjectAltName=DNS:example.com,DNS:www.example.com") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt +openssl pkcs8 -in server.key -topk8 -nocrypt -out key.pk8 + +$SUDO cp ca.crt /usr/local/share/ca-certificates/snakeoil-ca.crt +$SUDO cp server.crt /usr/local/share/ca-certificates/snakeoil-server.crt +$SUDO update-ca-certificates + +# Build and start tracker + +cargo build --bin aquatic + +# UDP +echo " +log_level = 'debug' + +[network] +address_ipv4 = '127.0.0.1:3000'" > udp.toml +./target/debug/aquatic udp -c udp.toml > "$HOME/udp.log" 2>&1 & + +# HTTP +echo "log_level = 'debug' + +[network] +address_ipv4 = '127.0.0.1:3004'" > http.toml +./target/debug/aquatic http -c http.toml > "$HOME/http.log" 2>&1 & + +# HTTP with TLS +echo "log_level = 'debug' + +[network] +address_ipv4 = '127.0.0.1:3001' +enable_tls = true +tls_certificate_path = './server.crt' +tls_private_key_path = './key.pk8' +" > tls.toml +./target/debug/aquatic http -c tls.toml > "$HOME/tls.log" 2>&1 & + +# WebTorrent +echo "log_level = 'debug' + +[network] +address = '127.0.0.1:3003' +enable_http_health_checks = true +" > ws.toml +./target/debug/aquatic ws -c ws.toml > "$HOME/ws.log" 2>&1 & + +# WebTorrent with TLS +echo "log_level = 'debug' + +[network] +address = '127.0.0.1:3002' +enable_tls = true +tls_certificate_path = './server.crt' +tls_private_key_path = './key.pk8' +" > ws-tls.toml +./target/debug/aquatic ws -c ws-tls.toml > "$HOME/ws-tls.log" 2>&1 & + +# Setup directories + +cd "$HOME" + +mkdir seed +mkdir leech +mkdir torrents + +# Create torrents + +echo "udp-test-ipv4" > seed/udp-test-ipv4 +echo "http-test-ipv4" > seed/http-test-ipv4 +echo "tls-test-ipv4" > seed/tls-test-ipv4 +echo "ws-test-ipv4" > seed/ws-test-ipv4 +echo "ws-tls-test-ipv4" > seed/ws-tls-test-ipv4 + +mktorrent -p -o "torrents/udp-ipv4.torrent" -a "udp://127.0.0.1:3000" "seed/udp-test-ipv4" +mktorrent -p -o "torrents/http-ipv4.torrent" -a "http://127.0.0.1:3004/announce" "seed/http-test-ipv4" +mktorrent -p -o "torrents/tls-ipv4.torrent" -a "https://example.com:3001/announce" "seed/tls-test-ipv4" +mktorrent -p -o "torrents/ws-ipv4.torrent" -a "ws://example.com:3003" "seed/ws-test-ipv4" +mktorrent -p -o "torrents/ws-tls-ipv4.torrent" -a "wss://example.com:3002" "seed/ws-tls-test-ipv4" + +cp -r torrents torrents-seed +cp -r torrents torrents-leech + +# Setup ws-tls seeding client + +echo "Starting seeding ws-tls (wss) client" +cd seed +GOPPROF=http $HOME/gotorrent download --dht=false --tcppeers=false --utppeers=false --pex=false --stats --seed ../torrents/ws-tls-ipv4.torrent > "$HOME/ws-tls-seed.log" 2>&1 & +cd .. + +# Setup ws seeding client + +echo "Starting seeding ws client" +cd seed +GOPPROF=http $HOME/gotorrent download --dht=false --tcppeers=false --utppeers=false --pex=false --stats --seed ../torrents/ws-ipv4.torrent > "$HOME/ws-seed.log" 2>&1 & +cd .. + +# Start seeding rtorrent client + +echo "directory.default.set = $HOME/seed +schedule2 = watch_directory,5,5,load.start=$HOME/torrents-seed/*.torrent" > ~/.rtorrent.rc + +echo "Starting seeding rtorrent client" +screen -dmS rtorrent-seed rtorrent + +# Give seeding clients time to load config files etc + +echo "Waiting for a while" +sleep 30 + +# Start leeching clients + +echo "directory.default.set = $HOME/leech +schedule2 = watch_directory,5,5,load.start=$HOME/torrents-leech/*.torrent" > ~/.rtorrent.rc + +echo "Starting leeching client.." +screen -dmS rtorrent-leech rtorrent + +echo "Starting leeching ws-tls (wss) client" +cd leech +GOPPROF=http $HOME/gotorrent download --dht=false --tcppeers=false --utppeers=false --pex=false --stats --addr ":43000" ../torrents/ws-tls-ipv4.torrent > "$HOME/ws-tls-leech.log" 2>&1 & +cd .. + +echo "Starting leeching ws client" +cd leech +GOPPROF=http $HOME/gotorrent download --dht=false --tcppeers=false --utppeers=false --pex=false --stats --addr ":43001" ../torrents/ws-ipv4.torrent > "$HOME/ws-leech.log" 2>&1 & +cd .. + +# Check for completion + +HTTP_IPv4="Failed" +TLS_IPv4="Failed" +UDP_IPv4="Failed" +WS_TLS_IPv4="Failed" +WS_IPv4="Failed" + +i="0" + +echo "Watching for finished files.." + +while [ $i -lt 60 ] +do + if test -f "leech/http-test-ipv4"; then + if grep -q "http-test-ipv4" "leech/http-test-ipv4"; then + if [ "$HTTP_IPv4" != "Ok" ]; then + HTTP_IPv4="Ok" + echo "HTTP_IPv4 is Ok" + fi + fi + fi + if test -f "leech/tls-test-ipv4"; then + if grep -q "tls-test-ipv4" "leech/tls-test-ipv4"; then + if [ "$TLS_IPv4" != "Ok" ]; then + TLS_IPv4="Ok" + echo "TLS_IPv4 is Ok" + fi + fi + fi + if test -f "leech/udp-test-ipv4"; then + if grep -q "udp-test-ipv4" "leech/udp-test-ipv4"; then + if [ "$UDP_IPv4" != "Ok" ]; then + UDP_IPv4="Ok" + echo "UDP_IPv4 is Ok" + fi + fi + fi + if test -f "leech/ws-tls-test-ipv4"; then + if grep -q "ws-tls-test-ipv4" "leech/ws-tls-test-ipv4"; then + if [ "$WS_TLS_IPv4" != "Ok" ]; then + WS_TLS_IPv4="Ok" + echo "WS_TLS_IPv4 is Ok" + fi + fi + fi + if test -f "leech/ws-test-ipv4"; then + if grep -q "ws-test-ipv4" "leech/ws-test-ipv4"; then + if [ "$WS_IPv4" != "Ok" ]; then + WS_IPv4="Ok" + echo "WS_IPv4 is Ok" + fi + fi + fi + + if [ "$HTTP_IPv4" = "Ok" ] && [ "$TLS_IPv4" = "Ok" ] && [ "$UDP_IPv4" = "Ok" ] && [ "$WS_TLS_IPv4" = "Ok" ] && [ "$WS_IPv4" = "Ok" ]; then + break + fi + + sleep 1 + + i=$[$i+1] +done + +echo "Waited for $i seconds" + +echo "::set-output name=http_ipv4::$HTTP_IPv4" +echo "::set-output name=http_tls_ipv4::$TLS_IPv4" +echo "::set-output name=udp_ipv4::$UDP_IPv4" +echo "::set-output name=ws_tls_ipv4::$WS_TLS_IPv4" +echo "::set-output name=ws_ipv4::$WS_IPv4" + +echo "" +echo "# --- HTTP log --- #" +cat "http.log" + +sleep 1 + +echo "" +echo "# --- HTTP over TLS log --- #" +cat "tls.log" + +sleep 1 + +echo "" +echo "# --- UDP log --- #" +cat "udp.log" + +sleep 1 + +echo "" +echo "# --- WS over TLS tracker log --- #" +cat "ws-tls.log" + +sleep 1 + +echo "" +echo "# --- WS tracker log --- #" +cat "ws.log" + +sleep 1 + +echo "" +echo "# --- WS over TLS seed log --- #" +cat "ws-tls-seed.log" + +sleep 1 + +echo "" +echo "# --- WS over TLS leech log --- #" +cat "ws-tls-leech.log" + +sleep 1 + +echo "" +echo "# --- WS seed log --- #" +cat "ws-seed.log" + +sleep 1 + +echo "" +echo "# --- WS leech log --- #" +cat "ws-leech.log" + +sleep 1 + +echo "" +echo "# --- Test results --- #" +echo "HTTP: $HTTP_IPv4" +echo "HTTP (TLS): $TLS_IPv4" +echo "UDP: $UDP_IPv4" +echo "WebTorrent (TLS): $WS_TLS_IPv4" +echo "WebTorrent: $WS_IPv4" + +if [ "$HTTP_IPv4" != "Ok" ] || [ "$TLS_IPv4" != "Ok" ] || [ "$UDP_IPv4" != "Ok" ] || [ "$WS_TLS_IPv4" != "Ok" ] || [ "$WS_IPv4" != "Ok" ]; then + exit 1 +fi diff --git a/apps/aquatic/.github/workflows/ci.yml b/apps/aquatic/.github/workflows/ci.yml new file mode 100644 index 0000000..e32e107 --- /dev/null +++ b/apps/aquatic/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build-linux: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - name: Install latest stable Rust + uses: dtolnay/rust-toolchain@stable + - name: Setup Rust dependency caching + uses: Swatinem/rust-cache@v2 + - name: Build + run: | + cargo build --verbose -p aquatic_udp + cargo build --verbose -p aquatic_http + cargo build --verbose -p aquatic_ws + + build-macos: + runs-on: macos-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + - name: Install latest stable Rust + uses: dtolnay/rust-toolchain@stable + - name: Setup Rust dependency caching + uses: Swatinem/rust-cache@v2 + - name: Build + run: cargo build --verbose -p aquatic_udp + + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - name: Install latest stable Rust + uses: dtolnay/rust-toolchain@stable + - name: Setup Rust dependency caching + uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --verbose --profile "test-fast" --workspace + - name: Run tests (aquatic_udp with io_uring) + run: cargo test --verbose --profile "test-fast" -p aquatic_udp --features "io-uring" + + test-file-transfers: + runs-on: ubuntu-latest + name: "Test BitTorrent file transfers (UDP, HTTP, WebTorrent)" + timeout-minutes: 20 + container: + image: rust:1-bookworm + options: --ulimit memlock=524288:524288 --privileged --security-opt="seccomp=unconfined" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Test file transfers + uses: ./.github/actions/test-file-transfers + id: test_file_transfers diff --git a/apps/aquatic/.gitignore b/apps/aquatic/.gitignore new file mode 100644 index 0000000..0446378 --- /dev/null +++ b/apps/aquatic/.gitignore @@ -0,0 +1,8 @@ +/target +/tmp + +**/criterion/*/change +**/criterion/*/new + +.DS_Store +.env \ No newline at end of file diff --git a/apps/aquatic/.gitrepo b/apps/aquatic/.gitrepo new file mode 100644 index 0000000..d94dbfd --- /dev/null +++ b/apps/aquatic/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = https://github.com/greatest-ape/aquatic + branch = master + commit = 34b45e923f84421181fc43cf5e20709e69ce0dfd + parent = fe0291091c67c0a832eeaa432fed52d61f1a1bb1 + method = merge + cmdver = 0.4.9 diff --git a/apps/aquatic/CHANGELOG.md b/apps/aquatic/CHANGELOG.md new file mode 100644 index 0000000..813b8d0 --- /dev/null +++ b/apps/aquatic/CHANGELOG.md @@ -0,0 +1,206 @@ +# Changelog + +## Unreleased + +### aquatic_udp + +#### Changed + +* (Breaking) Open one socket each for IPv4 and IPv6. The config file now has + one setting for each. + +### aquatic_http + +#### Changed + +* (Breaking) Open one socket each for IPv4 and IPv6. The config file now has + one setting for each. + +## 0.9.0 - 2024-04-03 + +### General + +#### Added + +* Add `aquatic_peer_id` crate with peer client information logic +* Add `aquatic_bencher` crate for automated benchmarking of aquatic and other + BitTorrent trackers + +### aquatic_udp + +#### Added + +* Add support for reporting peer client information + +#### Changed + +* Switch from socket worker/swarm worker division to a single type of worker, + for performance reasons. Several config file keys were removed since they + are no longer needed. +* Index peers by packet source IP and provided port, instead of by peer_id. + This prevents users from impersonating others and is likely also slightly + faster for IPv4 peers. +* Avoid a heap allocation for torrents with two or less peers. This can save + a lot of memory if many torrents are tracked +* Improve announce performance by avoiding having to filter response peers +* In announce response statistics, don't include announcing peer +* Harden ConnectionValidator to make IP spoofing even more costly +* Remove config key `network.poll_event_capacity` (always use 1) +* Speed up parsing and serialization of requests and responses by using + [zerocopy](https://crates.io/crates/zerocopy) +* Report socket worker related prometheus stats per worker +* Remove CPU pinning support + +#### Fixed + +* Quit whole application if any worker thread quits +* Disallow announce requests with port value of 0 +* Fix io_uring UB issues + +### aquatic_http + +#### Added + +* Reload TLS certificate (and key) on SIGUSR1 +* Support running without TLS +* Support running behind reverse proxy + +#### Changed + +* Index peers by packet source IP and provided port instead of by source ip + and peer id. This is likely slightly faster. +* Avoid a heap allocation for torrents with four or less peers. This can save + a lot of memory if many torrents are tracked +* Improve announce performance by avoiding having to filter response peers +* In announce response statistics, don't include announcing peer +* Remove CPU pinning support + +#### Fixed + +* Fix bug where clean up after closing connections wasn't always done +* Quit whole application if any worker thread quits +* Fix panic when sending failure response when running with metrics behind + reverse proxy +* Don't always close connections after sending failure response + +### aquatic_ws + +#### Added + +* Add support for reporting peer client information +* Reload TLS certificate (and key) on SIGUSR1 +* Keep track of which offers peers have sent and only allow matching answers + +#### Changed + +* A response is no longer generated when peers announce with AnnounceEvent::Stopped +* Compiling with SIMD extensions enabled is no longer required, due to the + addition of runtime detection to simd-json +* Only consider announce and scrape responses as signs of connection still + being alive. Previously, all messages sent to peer were considered. +* Decrease default max_peer_age and max_connection_idle config values +* Remove CPU pinning support + +#### Fixed + +* Fix memory leak +* Fix bug where clean up after closing connections wasn't always done +* Fix double counting of error responses +* Actually close connections that are too slow to send responses to +* If peers announce with AnnounceEvent::Stopped, allow them to later announce on + same torrent with different peer_id +* Quit whole application if any worker thread quits + +## 0.8.0 - 2023-03-17 + +### General + +#### Added + +* Support exposing a Prometheus endpoint for metrics +* Add cli flag for printing parsed config +* Add `aquatic_http_private`, an experiment for integrating with private trackers + +#### Changed + +* Rename request workers to swarm workers +* Switch to thin LTO for faster compile times +* Use proper workspace path declarations instead of workspace patch section +* Use [Rust 1.64 workspace inheritance](https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html) +* Reduce space taken by ValidUntil struct from 128 to 32 bits, reducing memory + consumption for each stored peer by same amount +* Use regular indexmap instead of amortized-indexmap. This goes for torrent, + peer and pending scrape response maps +* Improve privilege dropping +* Quit whole program if any thread panics +* Update dependencies + +#### Fixed + +* Forbid unrecognized keys when parsing config files +* Stop including invalid avx512 key in `./scripts/env-native-cpu-without-avx-512` + +### aquatic_udp + +#### Added + +* Add experimental io_uring backend with higher throughput +* Add optional response resend buffer for use on on operating systems that + don't buffer outgoing UDP traffic +* Add optional extended statistics (peers per torrent histogram) +* Add Dockerfile to make it easier to get started + +#### Changed + +* Replace ConnectionMap with BLAKE3-based connection validator, greatly + decreasing memory consumtion +* Don't return any response peers if announce event is stopped +* Ignore requests with source port value of zero + +#### Fixed + +* When calculating bandwidth statistics, include size of protocol headers + +### aquatic_http + +#### Changed + +* Don't return any response peers if announce event is stopped + +### aquatic_http_protocol + +#### Fixed + +* Explicity check for /scrape path +* Return NeedMoreData until headers are fully parsed +* Fix issues with ScrapeRequest::write and AnnounceRequest::write +* Expose write and parse methods for subtypes + +### aquatic_http_load_test + +#### Changed + +* Exclusively use TLS 1.3 + +### aquatic_ws + +#### Added + +* Add HTTP health check route when running without TLS + +#### Changed + +* Make TLS optional +* Support reverse proxies +* Reduce size of various structs + +#### Fixed + +* Remove peer from swarms immediately when connection is closed +* Allow peers to use multiple peer IDs, as long as they only use one per info hash + +### aquatic_ws_load_test + +#### Changed + +* Exclusively use TLS 1.3 diff --git a/apps/aquatic/Cargo.lock b/apps/aquatic/Cargo.lock new file mode 100644 index 0000000..40ac000 --- /dev/null +++ b/apps/aquatic/Cargo.lock @@ -0,0 +1,3455 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "aquatic" +version = "0.9.0" +dependencies = [ + "aquatic_common", + "aquatic_http", + "aquatic_udp", + "aquatic_ws", + "mimalloc", +] + +[[package]] +name = "aquatic_bencher" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_udp", + "aquatic_udp_load_test", + "clap 4.5.26", + "humanize-bytes", + "indexmap 2.7.0", + "indoc", + "itertools 0.14.0", + "nonblock", + "num-format", + "once_cell", + "regex", + "serde", + "tempfile", + "toml 0.8.19", +] + +[[package]] +name = "aquatic_common" +version = "0.9.0" +dependencies = [ + "ahash 0.8.11", + "anyhow", + "aquatic_toml_config", + "arc-swap", + "duplicate", + "git-testament", + "hashbrown 0.15.2", + "hex", + "hwloc", + "indexmap 2.7.0", + "libc", + "log", + "metrics", + "metrics-exporter-prometheus", + "metrics-util", + "privdrop", + "rand", + "rustls", + "rustls-pemfile", + "serde", + "simplelog", + "tokio", + "toml 0.5.11", +] + +[[package]] +name = "aquatic_http" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_http_protocol", + "aquatic_toml_config", + "arc-swap", + "arrayvec", + "cfg-if", + "either", + "futures", + "futures-lite", + "futures-rustls", + "glommio", + "httparse", + "itoa", + "libc", + "log", + "memchr", + "metrics", + "metrics-util", + "mimalloc", + "once_cell", + "privdrop", + "quickcheck", + "quickcheck_macros", + "rand", + "rustls-pemfile", + "serde", + "signal-hook", + "slotmap", + "socket2 0.5.8", + "thiserror 2.0.11", +] + +[[package]] +name = "aquatic_http_load_test" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_http_protocol", + "aquatic_toml_config", + "futures", + "futures-lite", + "futures-rustls", + "glommio", + "hashbrown 0.15.2", + "log", + "mimalloc", + "quickcheck", + "quickcheck_macros", + "rand", + "rand_distr", + "rustls", + "serde", +] + +[[package]] +name = "aquatic_http_protocol" +version = "0.9.0" +dependencies = [ + "anyhow", + "bendy", + "compact_str 0.7.1", + "criterion 0.4.0", + "hex", + "httparse", + "itoa", + "log", + "memchr", + "quickcheck", + "quickcheck_macros", + "serde", + "serde_bencode", + "urlencoding", +] + +[[package]] +name = "aquatic_peer_id" +version = "0.9.0" +dependencies = [ + "compact_str 0.8.1", + "hex", + "quickcheck", + "regex", + "serde", + "zerocopy", +] + +[[package]] +name = "aquatic_toml_config" +version = "0.9.0" +dependencies = [ + "aquatic_toml_config_derive", + "quickcheck", + "quickcheck_macros", + "serde", + "toml 0.5.11", +] + +[[package]] +name = "aquatic_toml_config_derive" +version = "0.9.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "aquatic_udp" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_toml_config", + "aquatic_udp_protocol", + "arrayvec", + "blake3", + "cfg-if", + "compact_str 0.8.1", + "constant_time_eq", + "crossbeam-channel", + "crossbeam-utils", + "getrandom", + "hashbrown 0.15.2", + "hdrhistogram", + "hex", + "io-uring", + "libc", + "log", + "metrics", + "mimalloc", + "mio", + "num-format", + "parking_lot", + "quickcheck", + "quickcheck_macros", + "rand", + "serde", + "signal-hook", + "slab", + "socket2 0.5.8", + "tempfile", + "time", + "tinytemplate", +] + +[[package]] +name = "aquatic_udp_load_test" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_toml_config", + "aquatic_udp_protocol", + "crossbeam-channel", + "hdrhistogram", + "mimalloc", + "quickcheck", + "quickcheck_macros", + "rand", + "rand_distr", + "serde", + "socket2 0.5.8", +] + +[[package]] +name = "aquatic_udp_protocol" +version = "0.9.0" +dependencies = [ + "aquatic_peer_id", + "byteorder", + "either", + "pretty_assertions", + "quickcheck", + "quickcheck_macros", + "zerocopy", +] + +[[package]] +name = "aquatic_ws" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_peer_id", + "aquatic_toml_config", + "aquatic_ws_protocol", + "arc-swap", + "async-tungstenite", + "cfg-if", + "futures", + "futures-lite", + "futures-rustls", + "glommio", + "hashbrown 0.15.2", + "httparse", + "indexmap 2.7.0", + "log", + "metrics", + "metrics-util", + "mimalloc", + "privdrop", + "quickcheck", + "quickcheck_macros", + "rand", + "rustls", + "rustls-pemfile", + "serde", + "signal-hook", + "slab", + "slotmap", + "socket2 0.5.8", + "tungstenite", +] + +[[package]] +name = "aquatic_ws_load_test" +version = "0.9.0" +dependencies = [ + "anyhow", + "aquatic_common", + "aquatic_toml_config", + "aquatic_ws_protocol", + "async-tungstenite", + "futures", + "futures-rustls", + "glommio", + "log", + "mimalloc", + "quickcheck", + "quickcheck_macros", + "rand", + "rand_distr", + "rustls", + "serde", + "serde_json", + "tungstenite", +] + +[[package]] +name = "aquatic_ws_protocol" +version = "0.9.0" +dependencies = [ + "anyhow", + "criterion 0.5.1", + "hashbrown 0.15.2", + "quickcheck", + "quickcheck_macros", + "serde", + "serde_json", + "simd-json", + "tungstenite", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-tungstenite" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c348fb0b6d132c596eca3dcd941df48fb597aafcb07a738ec41c004b087dc99" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" +dependencies = [ + "aws-lc-sys", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bendy" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a77a3f8614953e04ffd9901b9420a324125c3a21bfa485c5161936c378b67b3" +dependencies = [ + "rustversion", + "serde", + "serde_bytes", + "snafu", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.7.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.96", + "which", +] + +[[package]] +name = "bitflags" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "blake3" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "buddy-alloc" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3240a4cb09cf0da6a51641bd40ce90e96ea6065e3a1adc46434029254bcc2d09" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.4", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "cmake" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap 3.2.25", + "criterion-plot", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.26", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "duplicate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "proc-macro2-diagnostics", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enclose" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef4f6f904480430009ad8f22edc9573e26e4f137365f014d7ea998d5341639a" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "git-testament" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a74999c921479f919c87a9d2e6922a79a18683f18105344df8e067149232e51" +dependencies = [ + "git-testament-derive", +] + +[[package]] +name = "git-testament-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeac967e71eb3dc1656742fc7521ec7cd3b6b88738face65bf1fddf702bc4c0" +dependencies = [ + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "time", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "glommio" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1f8bc1fce949d18098dc0a4e861314e40351a0144ebf61e59bdb5254a2273b2" +dependencies = [ + "ahash 0.7.8", + "backtrace", + "bitflags 2.7.0", + "bitmaps", + "buddy-alloc", + "cc", + "concurrent-queue", + "crossbeam", + "enclose", + "flume", + "futures-lite", + "intrusive-collections", + "lazy_static", + "libc", + "lockfree", + "log", + "nix 0.27.1", + "pin-project-lite", + "rlimit", + "scoped-tls", + "scopeguard", + "signal-hook", + "sketches-ddsketch 0.1.3", + "smallvec", + "socket2 0.4.10", + "tracing", + "typenum", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "halfbrown" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" +dependencies = [ + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "crossbeam-channel", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humanize-bytes" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a92093c50b761e6ba595c797365301d3aaae67db1382ddf9d5d0092d98df799" +dependencies = [ + "smartstring", +] + +[[package]] +name = "hwloc" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2934f84993b8b4bcae9b6a4e5f0aca638462dda9c7b4f26a570241494f21e0f4" +dependencies = [ + "bitflags 0.7.0", + "errno 0.2.8", + "kernel32-sys", + "libc", + "num", + "pkg-config", + "winapi 0.2.8", +] + +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset 0.9.1", +] + +[[package]] +name = "io-uring" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d5b4a5e02a58296749114728ea3644f9a4cd5669c243896e445b90bd299ad6" +dependencies = [ + "bitflags 2.7.0", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "lockfree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ee94b5ad113c7cb98c5a040f783d0952ee4fe100993881d1673c2cb002dd23" +dependencies = [ + "owned-alloc", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metrics" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" +dependencies = [ + "ahash 0.8.11", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12779523996a67c13c84906a876ac6fe4d07a6e1adb54978378e13f199251a62" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper", + "hyper-util", + "indexmap 2.7.0", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd4884b1dd24f7d6628274a2f5ae22465c337c5ba065ec9b6edccddf8acc673" +dependencies = [ + "aho-corasick", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.2", + "indexmap 2.7.0", + "metrics", + "ordered-float", + "quanta", + "radix_trie", + "rand", + "rand_xoshiro", + "sketches-ddsketch 0.3.0", +] + +[[package]] +name = "mimalloc" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.7.0", + "cfg-if", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonblock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c7a4f22e5f2e2bd805d6ab56f1ae87eb1815673e1b452048896fb687a8a3d4" +dependencies = [ + "libc", +] + +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-integer", + "num-iter", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "owned-alloc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30fceb411f9a12ff9222c5f824026be368ff15dc2f13468d850c7d3f502205d6" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" +dependencies = [ + "proc-macro2", + "syn 2.0.96", +] + +[[package]] +name = "privdrop" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bc12de3935536ed9b69488faea4450a298dac44179b54f71806e63f55034bf9" +dependencies = [ + "libc", + "nix 0.26.4", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "version_check", + "yansi", +] + +[[package]] +name = "quanta" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + +[[package]] +name = "quickcheck_macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.7.0", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.7.0", +] + +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rlimit" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc0bf25554376fd362f54332b8410a625c71f15445bca32ffdfdf4ec9ac91726" +dependencies = [ + "libc", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags 2.7.0", + "errno 0.3.10", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "serde_json" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-json" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" +dependencies = [ + "getrandom", + "halfbrown", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d2ecae5fcf33b122e2e6bd520a57ccf152d2dde3b38c71039df1a6867264ee" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand 2.3.0", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.8", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.7.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "value-trait" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/apps/aquatic/Cargo.toml b/apps/aquatic/Cargo.toml new file mode 100644 index 0000000..20c1bd6 --- /dev/null +++ b/apps/aquatic/Cargo.toml @@ -0,0 +1,60 @@ +[workspace] +members = [ + "crates/bencher", + "crates/combined_binary", + "crates/common", + "crates/http", + "crates/http_load_test", + "crates/http_protocol", + "crates/peer_id", + "crates/toml_config", + "crates/toml_config_derive", + "crates/udp", + "crates/udp_load_test", + "crates/udp_protocol", + "crates/ws", + "crates/ws_load_test", + "crates/ws_protocol", +] +resolver = "2" + +[workspace.package] +version = "0.9.0" +authors = ["Joakim Frostegård "] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/greatest-ape/aquatic" +readme = "./README.md" +rust-version = "1.64" + +[workspace.dependencies] +aquatic_common = { version = "0.9.0", path = "./crates/common" } +aquatic_http_protocol = { version = "0.9.0", path = "./crates/http_protocol" } +aquatic_http = { version = "0.9.0", path = "./crates/http" } +aquatic_peer_id = { version = "0.9.0", path = "./crates/peer_id" } +aquatic_toml_config = { version = "0.9.0", path = "./crates/toml_config" } +aquatic_toml_config_derive = { version = "0.9.0", path = "./crates/toml_config_derive" } +aquatic_udp_protocol = { version = "0.9.0", path = "./crates/udp_protocol" } +aquatic_udp = { version = "0.9.0", path = "./crates/udp" } +aquatic_udp_load_test = { version = "0.9.0", path = "./crates/udp_load_test" } +aquatic_ws_protocol = { version = "0.9.0", path = "./crates/ws_protocol" } +aquatic_ws = { version = "0.9.0", path = "./crates/ws" } + +[profile.release] +debug = false +lto = "thin" +opt-level = 3 + +[profile.test] +inherits = "release-debug" + +[profile.bench] +inherits = "release-debug" + +[profile.release-debug] +inherits = "release" +debug = true + +[profile.test-fast] +inherits = "release" +lto = false diff --git a/apps/aquatic/LICENSE b/apps/aquatic/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/apps/aquatic/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/aquatic/README.md b/apps/aquatic/README.md new file mode 100644 index 0000000..d0c8136 --- /dev/null +++ b/apps/aquatic/README.md @@ -0,0 +1,79 @@ +# aquatic: high-performance open BitTorrent tracker + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +High-performance open BitTorrent tracker, consisting +of sub-implementations for different protocols: + +[aquatic_udp]: ./crates/udp +[aquatic_http]: ./crates/http +[aquatic_ws]: ./crates/ws + +| Name | Protocol | OS requirements | +|----------------|-------------------------------------------|--------------------| +| [aquatic_udp] | BitTorrent over UDP | Unix-like | +| [aquatic_http] | BitTorrent over HTTP, optionally over TLS | Linux 5.8 or later | +| [aquatic_ws] | WebTorrent, optionally over TLS | Linux 5.8 or later | + +Features at a glance: + +- Multithreaded design for handling large amounts of traffic +- All data is stored in-memory (no database needed) +- IPv4 and IPv6 support +- Supports forbidding/allowing info hashes +- Prometheus metrics +- Automated CI testing of full file transfers + +Known users: + +- [explodie.org public tracker](https://explodie.org/opentracker.html) (`udp://explodie.org:6969`), typically [serving ~100,000 requests per second](https://explodie.org/tracker-stats.html) +- [tracker.webtorrent.dev](https://tracker.webtorrent.dev) (`wss://tracker.webtorrent.dev`) + +## Performance of the UDP implementation + +![UDP BitTorrent tracker throughput](./documents/aquatic-udp-load-test-2024-02-10.png) + +More benchmark details are available [here](./documents/aquatic-udp-load-test-2024-02-10.md). + +## Usage + +Please refer to the README pages for the respective implementations listed in +the table above. + +## Auxiliary software + +There are also some auxiliary applications and libraries. + +### Tracker load testing + +Load test applications for aquatic and other trackers, useful for profiling: + +- [aquatic_udp_load_test](./crates/udp_load_test/) - BitTorrent over UDP +- [aquatic_http_load_test](./crates/http_load_test/) - BitTorrent over HTTP +- [aquatic_ws_load_test](./crates/ws_load_test/) - WebTorrent + +Automated benchmarking of aquatic and other trackers: [aquatic_bencher](./crates/bencher/) + +### Client ⇄ tracker communication + +Libraries for communication between clients and trackers: + +- [aquatic_udp_protocol](./crates/udp_protocol/) - BitTorrent over UDP +- [aquatic_http_protocol](./crates/http_protocol/) - BitTorrent over HTTP +- [aquatic_ws_protocol](./crates/ws_protocol/) - WebTorrent + +### Other + +- [aquatic_peer_id](./crates/peer_id/) - extract BitTorrent client information + from peer identifiers + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. + +## Trivia + +The tracker is called aquatic because it thrives under a torrent of bits ;-) diff --git a/apps/aquatic/TODO.md b/apps/aquatic/TODO.md new file mode 100644 index 0000000..1917491 --- /dev/null +++ b/apps/aquatic/TODO.md @@ -0,0 +1,58 @@ +# TODO + +## High priority + +* Change network address handling to accept separate IPv4 and IPv6 + addresses. Open a socket for each one, setting ipv6_only flag on + the IPv6 one (unless user opts out). +* update zerocopy version (will likely require minor rewrite) + +* udp (uring) + * run tests under valgrind + * hangs for integration tests, possibly related to https://bugs.kde.org/show_bug.cgi?id=463859 + * run tests with AddressSanitizer + * `RUSTFLAGS=-Zsanitizer=address cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu --verbose --profile "test-fast" -p aquatic_udp --features "io-uring"` + * build fails with `undefined reference to __asan_init`, currently unclear why + +## Medium priority + +* stagger cleaning tasks? +* Run cargo-fuzz on protocol crates + +* udp + * support link to arbitrary homepage as well as embedded tracker URL in statistics page + * Non-trivial dependency updates + * toml v0.7 + * syn v2.0 + +* Run cargo-deny in CI + +* aquatic_ws + * Add cleaning task for ConnectionHandle.announced_info_hashes? + +## Low priority + +* aquatic_udp + * udp uring + * miri + * thiserror? + * CI + * uring load test? + +* Performance hyperoptimization (receive interrupts on correct core) + * If there is no network card RSS support, do eBPF XDP CpuMap redirect based on packet info, to + cpus where socket workers run. Support is work in progress in the larger Rust eBPF + implementations, but exists in rebpf + * Pin socket workers + * Set SO_INCOMING_CPU (which should be fixed in very recent Linux?) to currently pinned thread + * How does this relate to (currently unused) so_attach_reuseport_cbpf code? + +# Not important + +* aquatic_http: + * consider better error type for request parsing, so that better error + messages can be sent back (e.g., "full scrapes are not supported") + * test torrent transfer with real clients + * scrape: does it work (serialization etc), and with multiple hashes? + * 'left' optional in magnet requests? Probably not. Transmission sends huge + positive number. diff --git a/apps/aquatic/crates/bencher/Cargo.toml b/apps/aquatic/crates/bencher/Cargo.toml new file mode 100644 index 0000000..8442de9 --- /dev/null +++ b/apps/aquatic/crates/bencher/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "aquatic_bencher" +description = "Automated benchmarking of aquatic and other BitTorrent trackers (Linux only)" +keywords = ["peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[[bin]] +name = "aquatic_bencher" + +[features] +default = ["udp"] +udp = ["aquatic_udp", "aquatic_udp_load_test"] + +[dependencies] +aquatic_udp = { optional = true, workspace = true, features = ["io-uring"] } +aquatic_udp_load_test = { optional = true, workspace = true } + +anyhow = "1" +clap = { version = "4", features = ["derive"] } +humanize-bytes = "1" +indexmap = "2" +indoc = "2" +itertools = "0.14" +num-format = "0.4" +nonblock = "0.2" +once_cell = "1" +regex = "1" +serde = "1" +tempfile = "3" +toml = "0.8" + +[dev-dependencies] \ No newline at end of file diff --git a/apps/aquatic/crates/bencher/README.md b/apps/aquatic/crates/bencher/README.md new file mode 100644 index 0000000..5e43226 --- /dev/null +++ b/apps/aquatic/crates/bencher/README.md @@ -0,0 +1,112 @@ +# aquatic_bencher + +Automated benchmarking of aquatic and other BitTorrent trackers. + +Requires Linux 6.0 or later. + +Currently, only UDP BitTorrent tracker support is implemented. + +## UDP + +| Name | Commit | +|-------------------|-----------------------| +| [aquatic_udp] | (use same as bencher) | +| [opentracker] | 110868e | +| [chihaya] | 2f79440 | +| [torrust-tracker] | eaa86a7 | + +The commits listed are ones known to work. It might be a good idea to first +test with the latest commits for each project, and if they don't seem to work, +revert to the listed commits. + +Chihaya is known to crash under high load. + +[aquatic_udp]: https://github.com/greatest-ape/aquatic/ +[opentracker]: http://erdgeist.org/arts/software/opentracker/ +[chihaya]: https://github.com/chihaya/chihaya +[torrust-tracker]: https://github.com/torrust/torrust-tracker + +### Usage + +Install dependencies. This is done differently for different Linux +distributions. On Debian 12, run: + +```sh +sudo apt-get update +sudo apt-get install -y curl cmake build-essential pkg-config git screen cvs zlib1g zlib1g-dev golang +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +``` + +Optionally install latest Linux kernel. On Debian 12, you can do so from backports: + +```sh +sudo echo "deb http://deb.debian.org/debian bookworm-backports main contrib" >> /etc/apt/sources.list +sudo apt-get update && sudo apt-get install -y linux-image-amd64/bookworm-backports +# You will have to restart to boot into the new kernel +``` + +Compile aquatic_udp, aquatic_udp_load_test and aquatic_udp_bencher: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic +# Optionally enable certain native platform optimizations +. ./scripts/env-native-cpu-without-avx-512 +cargo build --profile "release-debug" -p aquatic_udp --features "io-uring" +cargo build --profile "release-debug" -p aquatic_udp_load_test +cargo build --profile "release-debug" -p aquatic_bencher --features udp +cd .. +``` + +Compile and install opentracker: + +```sh +cvs -d :pserver:cvs@cvs.fefe.de:/cvs -z9 co libowfat +cd libowfat +make +cd .. +git clone git://erdgeist.org/opentracker +cd opentracker +# Optionally enable native platform optimizations +sed -i "s/^OPTS_production=-O3/OPTS_production=-O3 -march=native -mtune=native/g" Makefile +make +sudo cp ./opentracker /usr/local/bin/ +cd .. +``` + +Compile and install chihaya: + +```sh +git clone https://github.com/chihaya/chihaya.git +cd chihaya +go build ./cmd/chihaya +sudo cp ./chihaya /usr/local/bin/ +``` + +Compile and install torrust-tracker: + +```sh +git clone git@github.com:torrust/torrust-tracker.git +cd torrust-tracker +cargo build --release +cp ./target/release/torrust-tracker /usr/local/bin/ +``` + +You might need to raise locked memory limits: + +```sh +ulimit -l 65536 +``` + +Run the bencher: + +```sh +cd aquatic +./target/release-debug/aquatic_bencher udp +# or print info on command line arguments +./target/release-debug/aquatic_bencher udp --help +``` + +If you're running the load test on a virtual machine / virtual server, consider +passing `--min-priority medium --cpu-mode subsequent-one-per-pair` for fairer +results. diff --git a/apps/aquatic/crates/bencher/src/common.rs b/apps/aquatic/crates/bencher/src/common.rs new file mode 100644 index 0000000..a26862f --- /dev/null +++ b/apps/aquatic/crates/bencher/src/common.rs @@ -0,0 +1,336 @@ +use std::{fmt::Display, ops::Range, thread::available_parallelism}; + +use itertools::Itertools; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum Priority { + Low, + Medium, + High, +} + +impl Display for Priority { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Low => f.write_str("low"), + Self::Medium => f.write_str("medium"), + Self::High => f.write_str("high"), + } + } +} + +#[derive(Debug, Clone)] +pub struct TaskSetCpuList(pub Vec); + +impl TaskSetCpuList { + pub fn as_cpu_list(&self) -> String { + let indicator = self.0.iter().map(|indicator| match indicator { + TaskSetCpuIndicator::Single(i) => i.to_string(), + TaskSetCpuIndicator::Range(range) => { + format!("{}-{}", range.start, range.clone().last().unwrap()) + } + }); + + Itertools::intersperse_with(indicator, || ",".to_string()).collect() + } + + pub fn new( + mode: CpuMode, + direction: CpuDirection, + requested_cpus: usize, + ) -> anyhow::Result { + let available_parallelism: usize = available_parallelism()?.into(); + + Ok(Self::new_with_available_parallelism( + available_parallelism, + mode, + direction, + requested_cpus, + )) + } + + fn new_with_available_parallelism( + available_parallelism: usize, + mode: CpuMode, + direction: CpuDirection, + requested_cpus: usize, + ) -> Self { + match direction { + CpuDirection::Asc => match mode { + CpuMode::Subsequent => { + let range = 0..(available_parallelism.min(requested_cpus)); + + Self(vec![range.try_into().unwrap()]) + } + CpuMode::SplitPairs => { + let middle = available_parallelism / 2; + + let range_a = 0..(middle.min(requested_cpus)); + let range_b = middle..(available_parallelism.min(middle + requested_cpus)); + + Self(vec![ + range_a.try_into().unwrap(), + range_b.try_into().unwrap(), + ]) + } + CpuMode::SubsequentPairs => { + let range = 0..(available_parallelism.min(requested_cpus * 2)); + + Self(vec![range.try_into().unwrap()]) + } + CpuMode::SubsequentOnePerPair => { + let range = 0..(available_parallelism.min(requested_cpus * 2)); + + Self( + range + .chunks(2) + .into_iter() + .map(|mut chunk| TaskSetCpuIndicator::Single(chunk.next().unwrap())) + .collect(), + ) + } + }, + CpuDirection::Desc => match mode { + CpuMode::Subsequent => { + let range = + available_parallelism.saturating_sub(requested_cpus)..available_parallelism; + + Self(vec![range.try_into().unwrap()]) + } + CpuMode::SplitPairs => { + let middle = available_parallelism / 2; + + let range_a = middle.saturating_sub(requested_cpus)..middle; + let range_b = available_parallelism + .saturating_sub(requested_cpus) + .max(middle)..available_parallelism; + + Self(vec![ + range_a.try_into().unwrap(), + range_b.try_into().unwrap(), + ]) + } + CpuMode::SubsequentPairs => { + let range = available_parallelism.saturating_sub(requested_cpus * 2) + ..available_parallelism; + + Self(vec![range.try_into().unwrap()]) + } + CpuMode::SubsequentOnePerPair => { + let range = available_parallelism.saturating_sub(requested_cpus * 2) + ..available_parallelism; + + Self( + range + .chunks(2) + .into_iter() + .map(|mut chunk| TaskSetCpuIndicator::Single(chunk.next().unwrap())) + .collect(), + ) + } + }, + } + } +} + +impl TryFrom>> for TaskSetCpuList { + type Error = String; + + fn try_from(value: Vec>) -> Result { + let mut output = Vec::new(); + + for range in value { + output.push(range.try_into()?); + } + + Ok(Self(output)) + } +} + +#[derive(Debug, Clone)] +pub enum TaskSetCpuIndicator { + Single(usize), + Range(Range), +} + +impl TryFrom> for TaskSetCpuIndicator { + type Error = String; + + fn try_from(value: Range) -> Result { + match value.len() { + 0 => Err("Empty ranges not supported".into()), + 1 => Ok(TaskSetCpuIndicator::Single(value.start)), + _ => Ok(TaskSetCpuIndicator::Range(value)), + } + } +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +pub enum CpuMode { + /// Suitable for bare-metal machines without hyperthreads/SMT. + /// + /// For 8 vCPU processor, uses vCPU groups 0, 1, 2, 3, 4, 5, 6 and 7 + Subsequent, + /// Suitable for bare-metal machines with hyperthreads/SMT. + /// + /// For 8 vCPU processor, uses vCPU groups 0 & 4, 1 & 5, 2 & 6 and 3 & 7 + SplitPairs, + /// For 8 vCPU processor, uses vCPU groups 0 & 1, 2 & 3, 4 & 5 and 6 & 7 + SubsequentPairs, + /// Suitable for somewhat fairly comparing trackers on Hetzner virtual + /// machines. Since in-VM hyperthreads aren't really hyperthreads, + /// enabling them causes unpredictable performance. + /// + /// For 8 vCPU processor, uses vCPU groups 0, 2, 4 and 6 + SubsequentOnePerPair, +} + +impl Display for CpuMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Subsequent => f.write_str("subsequent"), + Self::SplitPairs => f.write_str("split-pairs"), + Self::SubsequentPairs => f.write_str("subsequent-pairs"), + Self::SubsequentOnePerPair => f.write_str("subsequent-one-per-pair"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum CpuDirection { + Asc, + Desc, +} + +pub fn simple_load_test_runs( + cpu_mode: CpuMode, + workers: &[(usize, Priority)], +) -> Vec<(usize, Priority, TaskSetCpuList)> { + workers + .iter() + .copied() + .map(|(workers, priority)| { + ( + workers, + priority, + TaskSetCpuList::new(cpu_mode, CpuDirection::Desc, workers).unwrap(), + ) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_task_set_cpu_list_split_pairs_asc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::SplitPairs; + let direction = CpuDirection::Asc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "0,4"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "0-1,4-5"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0-3,4-7"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-3,4-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-3,4-7"); + } + + #[test] + fn test_task_set_cpu_list_split_pairs_desc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::SplitPairs; + let direction = CpuDirection::Desc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "3,7"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "2-3,6-7"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0-3,4-7"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-3,4-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-3,4-7"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_asc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::Subsequent; + let direction = CpuDirection::Asc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "0"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "0-1"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0-3"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-7"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_desc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::Subsequent; + let direction = CpuDirection::Desc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "7"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "6-7"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "4-7"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-7"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_pairs_asc() { + let f = TaskSetCpuList::new_with_available_parallelism; + let mode = CpuMode::SubsequentPairs; + let direction = CpuDirection::Asc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "0-1"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "0-3"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-7"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_pairs_desc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::SubsequentPairs; + let direction = CpuDirection::Desc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "6-7"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "4-7"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0-7"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0-7"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_one_per_pair_asc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::SubsequentOnePerPair; + let direction = CpuDirection::Asc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "0"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "0,2"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0,2,4,6"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0,2,4,6"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0,2,4,6"); + } + + #[test] + fn test_task_set_cpu_list_subsequent_one_per_pair_desc() { + let f = TaskSetCpuList::new_with_available_parallelism; + + let mode = CpuMode::SubsequentOnePerPair; + let direction = CpuDirection::Desc; + + assert_eq!(f(8, mode, direction, 1).as_cpu_list(), "6"); + assert_eq!(f(8, mode, direction, 2).as_cpu_list(), "4,6"); + assert_eq!(f(8, mode, direction, 4).as_cpu_list(), "0,2,4,6"); + assert_eq!(f(8, mode, direction, 8).as_cpu_list(), "0,2,4,6"); + assert_eq!(f(8, mode, direction, 9).as_cpu_list(), "0,2,4,6"); + } +} diff --git a/apps/aquatic/crates/bencher/src/html.rs b/apps/aquatic/crates/bencher/src/html.rs new file mode 100644 index 0000000..03278f3 --- /dev/null +++ b/apps/aquatic/crates/bencher/src/html.rs @@ -0,0 +1,230 @@ +use humanize_bytes::humanize_bytes_binary; +use indexmap::{IndexMap, IndexSet}; +use indoc::formatdoc; +use itertools::Itertools; +use num_format::{Locale, ToFormattedString}; + +use crate::{ + run::ProcessStats, + set::{LoadTestRunResults, TrackerCoreCountResults}, +}; + +pub fn html_best_results(results: &[TrackerCoreCountResults]) -> String { + let mut all_implementation_names = IndexSet::new(); + + for core_count_results in results { + all_implementation_names.extend( + core_count_results + .implementations + .iter() + .map(|r| r.name.clone()), + ); + } + + let mut data_rows = Vec::new(); + + for core_count_results in results { + let best_results = core_count_results + .implementations + .iter() + .map(|implementation| (implementation.name.clone(), implementation.best_result())) + .collect::>(); + + let best_results_for_all_implementations = all_implementation_names + .iter() + .map(|name| best_results.get(name).cloned().flatten()) + .collect::>(); + + let data_row = format!( + " + + {} + {} + + ", + core_count_results.core_count, + best_results_for_all_implementations + .into_iter() + .map(|result| { + if let Some(r) = result { + format!( + r#"{}"#, + r.tracker_info, + r.tracker_process_stats.avg_cpu_utilization, + r.average_responses.to_formatted_string(&Locale::en), + ) + } else { + "-".to_string() + } + }) + .join("\n"), + ); + + data_rows.push(data_row); + } + + format!( + " +

Best results

+ + + + + {} + + + + {} + +
CPU cores
+ ", + all_implementation_names + .iter() + .map(|name| format!("{name}")) + .join("\n"), + data_rows.join("\n") + ) +} + +pub fn html_all_runs(all_results: &[TrackerCoreCountResults]) -> String { + let mut all_implementation_names = IndexSet::new(); + + for core_count_results in all_results { + all_implementation_names.extend( + core_count_results + .implementations + .iter() + .map(|r| r.name.clone()), + ); + } + + struct R { + core_count: usize, + avg_responses: Option, + tracker_keys: IndexMap, + tracker_vcpus: String, + tracker_stats: Option, + load_test_keys: IndexMap, + load_test_vcpus: String, + } + + let mut output = String::new(); + + let mut results_by_implementation: IndexMap> = Default::default(); + + for implementation_name in all_implementation_names { + let results = results_by_implementation + .entry(implementation_name.clone()) + .or_default(); + + let mut tracker_key_names: IndexSet = Default::default(); + let mut load_test_key_names: IndexSet = Default::default(); + + for r in all_results { + for i in r + .implementations + .iter() + .filter(|i| i.name == implementation_name) + { + for c in i.configurations.iter() { + for l in c.load_tests.iter() { + match l { + LoadTestRunResults::Success(l) => { + tracker_key_names.extend(l.tracker_keys.keys().cloned()); + load_test_key_names.extend(l.load_test_keys.keys().cloned()); + + results.push(R { + core_count: r.core_count, + avg_responses: Some(l.average_responses), + tracker_keys: l.tracker_keys.clone(), + tracker_vcpus: l.tracker_vcpus.as_cpu_list(), + tracker_stats: Some(l.tracker_process_stats), + load_test_keys: l.load_test_keys.clone(), + load_test_vcpus: l.load_test_vcpus.as_cpu_list(), + }) + } + LoadTestRunResults::Failure(l) => { + tracker_key_names.extend(l.tracker_keys.keys().cloned()); + load_test_key_names.extend(l.load_test_keys.keys().cloned()); + + results.push(R { + core_count: r.core_count, + avg_responses: None, + tracker_keys: l.tracker_keys.clone(), + tracker_vcpus: l.tracker_vcpus.as_cpu_list(), + tracker_stats: None, + load_test_keys: l.load_test_keys.clone(), + load_test_vcpus: l.load_test_vcpus.as_cpu_list(), + }) + } + } + } + } + } + } + + output.push_str(&formatdoc! { + " +

Results for {implementation}

+ + + + + + {tracker_key_names} + + + + {load_test_key_names} + + + + + {body} + +
CoresResponsesTracker avg CPUTracker peak RSSTracker vCPUsLoad test vCPUs
+ ", + implementation = implementation_name, + tracker_key_names = tracker_key_names.iter() + .map(|name| format!("{}", name)) + .join("\n"), + load_test_key_names = load_test_key_names.iter() + .map(|name| format!("Load test {}", name)) + .join("\n"), + body = results.iter_mut().map(|r| { + formatdoc! { + " + + {cores} + {avg_responses} + {tracker_key_values} + {cpu}% + {mem} + {tracker_vcpus} + {load_test_key_values} + {load_test_vcpus} + + ", + cores = r.core_count, + avg_responses = r.avg_responses.map(|v| v.to_formatted_string(&Locale::en)) + .unwrap_or_else(|| "-".to_string()), + tracker_key_values = tracker_key_names.iter().map(|name| { + format!("{}", r.tracker_keys.get(name).cloned().unwrap_or_else(|| "-".to_string())) + }).join("\n"), + cpu = r.tracker_stats.map(|stats| stats.avg_cpu_utilization.to_string()) + .unwrap_or_else(|| "-".to_string()), + mem = r.tracker_stats + .map(|stats| humanize_bytes_binary!(stats.peak_rss_bytes).to_string()) + .unwrap_or_else(|| "-".to_string()), + tracker_vcpus = r.tracker_vcpus, + load_test_key_values = load_test_key_names.iter().map(|name| { + format!("{}", r.load_test_keys.get(name).cloned().unwrap_or_else(|| "-".to_string())) + }).join("\n"), + load_test_vcpus = r.load_test_vcpus, + } + }).join("\n") + }); + } + + output +} diff --git a/apps/aquatic/crates/bencher/src/main.rs b/apps/aquatic/crates/bencher/src/main.rs new file mode 100644 index 0000000..9c05344 --- /dev/null +++ b/apps/aquatic/crates/bencher/src/main.rs @@ -0,0 +1,71 @@ +pub mod common; +pub mod html; +pub mod protocols; +pub mod run; +pub mod set; + +use clap::{Parser, Subcommand}; +use common::{CpuMode, Priority}; +use set::run_sets; + +#[derive(Parser)] +#[command(author, version, about)] +struct Args { + /// How to choose which virtual CPUs to allow trackers and load test + /// executables on + #[arg(long, default_value_t = CpuMode::SplitPairs)] + cpu_mode: CpuMode, + /// Minimum number of tracker cpu cores to run benchmarks for + #[arg(long)] + min_cores: Option, + /// Maximum number of tracker cpu cores to run benchmarks for + #[arg(long)] + max_cores: Option, + /// Minimum benchmark priority + #[arg(long, default_value_t = Priority::Medium)] + min_priority: Priority, + /// How long to run each load test for + #[arg(long, default_value_t = 30)] + duration: usize, + /// Only include data for last N seconds of load test runs. + /// + /// Useful if the tracker/load tester combination is slow at reaching + /// maximum throughput + /// + /// 0 = use data for whole run + #[arg(long, default_value_t = 0)] + summarize_last: usize, + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Benchmark UDP BitTorrent trackers aquatic_udp, opentracker, chihaya and torrust-tracker + #[cfg(feature = "udp")] + Udp(protocols::udp::UdpCommand), +} + +fn main() { + let args = Args::parse(); + + match args.command { + #[cfg(feature = "udp")] + Command::Udp(command) => { + let sets = command.sets(args.cpu_mode); + let load_test_gen = protocols::udp::UdpCommand::load_test_gen; + + run_sets( + &command, + args.cpu_mode, + args.min_cores, + args.max_cores, + args.min_priority, + args.duration, + args.summarize_last, + sets, + load_test_gen, + ); + } + } +} diff --git a/apps/aquatic/crates/bencher/src/protocols/mod.rs b/apps/aquatic/crates/bencher/src/protocols/mod.rs new file mode 100644 index 0000000..1fa6e12 --- /dev/null +++ b/apps/aquatic/crates/bencher/src/protocols/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "udp")] +pub mod udp; diff --git a/apps/aquatic/crates/bencher/src/protocols/udp.rs b/apps/aquatic/crates/bencher/src/protocols/udp.rs new file mode 100644 index 0000000..62058ef --- /dev/null +++ b/apps/aquatic/crates/bencher/src/protocols/udp.rs @@ -0,0 +1,558 @@ +use std::{ + io::Write, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + path::PathBuf, + process::{Child, Command, Stdio}, + rc::Rc, +}; + +use clap::Parser; +use indexmap::{indexmap, IndexMap}; +use indoc::writedoc; +use tempfile::NamedTempFile; + +use crate::{ + common::{simple_load_test_runs, CpuMode, Priority, TaskSetCpuList}, + run::ProcessRunner, + set::{LoadTestRunnerParameters, SetConfig, Tracker}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum UdpTracker { + Aquatic, + AquaticIoUring, + OpenTracker, + Chihaya, + TorrustTracker, +} + +impl Tracker for UdpTracker { + fn name(&self) -> String { + match self { + Self::Aquatic => "aquatic_udp".into(), + Self::AquaticIoUring => "aquatic_udp (io_uring)".into(), + Self::OpenTracker => "opentracker".into(), + Self::Chihaya => "chihaya".into(), + Self::TorrustTracker => "torrust-tracker".into(), + } + } +} + +#[derive(Parser, Debug)] +pub struct UdpCommand { + /// Path to aquatic_udp_load_test binary + #[arg(long, default_value = "./target/release-debug/aquatic_udp_load_test")] + load_test: PathBuf, + /// Path to aquatic_udp binary + #[arg(long, default_value = "./target/release-debug/aquatic_udp")] + aquatic: PathBuf, + /// Path to opentracker binary + #[arg(long, default_value = "opentracker")] + opentracker: PathBuf, + /// Path to chihaya binary + #[arg(long, default_value = "chihaya")] + chihaya: PathBuf, + /// Path to torrust-tracker binary + #[arg(long, default_value = "torrust-tracker")] + torrust_tracker: PathBuf, +} + +impl UdpCommand { + pub fn sets(&self, cpu_mode: CpuMode) -> IndexMap> { + // Priorities are based on what has previously produced the best results + indexmap::indexmap! { + 1 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(1, Priority::High), + // Allow running two workers per core for aquatic and + // opentracker. Skip this priority if testing on a + // virtual machine + AquaticUdpRunner::with_mio(2, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(1, Priority::High), + AquaticUdpRunner::with_io_uring(2, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(0, Priority::Medium), // Handle requests within event loop + OpenTrackerUdpRunner::new(1, Priority::High), + OpenTrackerUdpRunner::new(2, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High) + ]), + }, + 2 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(2, Priority::High), + AquaticUdpRunner::with_mio(4, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(2, Priority::High), + AquaticUdpRunner::with_io_uring(4, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(2, Priority::High), + OpenTrackerUdpRunner::new(4, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High), + ]), + }, + 4 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(4, Priority::High), + AquaticUdpRunner::with_mio(8, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(4, Priority::High), + AquaticUdpRunner::with_io_uring(8, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(4, Priority::High), + OpenTrackerUdpRunner::new(8, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High), + ]), + }, + 6 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(6, Priority::High), + AquaticUdpRunner::with_mio(12, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(6, Priority::High), + AquaticUdpRunner::with_io_uring(12, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(6, Priority::High), + OpenTrackerUdpRunner::new(12, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High), + ]), + }, + 8 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(8, Priority::High), + AquaticUdpRunner::with_mio(16, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(8, Priority::High), + AquaticUdpRunner::with_io_uring(16, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(8, Priority::High), + OpenTrackerUdpRunner::new(16, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High), + ]), + }, + 12 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(12, Priority::High), + AquaticUdpRunner::with_mio(24, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(12, Priority::High), + AquaticUdpRunner::with_io_uring(24, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(12, Priority::High), + OpenTrackerUdpRunner::new(24, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::Medium), + (12, Priority::High), + ]), + }, + 16 => SetConfig { + implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(16, Priority::High), + AquaticUdpRunner::with_mio(32, Priority::Low), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(16, Priority::High), + AquaticUdpRunner::with_io_uring(32, Priority::Low), + ], + UdpTracker::OpenTracker => vec![ + OpenTrackerUdpRunner::new(16, Priority::High), + OpenTrackerUdpRunner::new(32, Priority::Low), + ], + UdpTracker::Chihaya => vec![ + ChihayaUdpRunner::new(), + ], + UdpTracker::TorrustTracker => vec![ + TorrustTrackerUdpRunner::new(), + ], + }, + load_test_runs: simple_load_test_runs(cpu_mode, &[ + (8, Priority::High), + (12, Priority::High), + ]), + }, + } + } + + pub fn load_test_gen( + parameters: LoadTestRunnerParameters, + ) -> Box> { + Box::new(AquaticUdpLoadTestRunner { parameters }) + } +} + +#[derive(Debug, Clone)] +struct AquaticUdpRunner { + socket_workers: usize, + use_io_uring: bool, + priority: Priority, +} + +impl AquaticUdpRunner { + fn with_mio( + socket_workers: usize, + priority: Priority, + ) -> Rc> { + Rc::new(Self { + socket_workers, + use_io_uring: false, + priority, + }) + } + fn with_io_uring( + socket_workers: usize, + priority: Priority, + ) -> Rc> { + Rc::new(Self { + socket_workers, + use_io_uring: true, + priority, + }) + } +} + +impl ProcessRunner for AquaticUdpRunner { + type Command = UdpCommand; + + #[allow(clippy::field_reassign_with_default)] + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result { + let mut c = aquatic_udp::config::Config::default(); + + c.socket_workers = self.socket_workers; + c.network.address_ipv4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3000); + c.network.use_ipv6 = false; + c.network.use_io_uring = self.use_io_uring; + c.protocol.max_response_peers = 30; + + let c = toml::to_string_pretty(&c)?; + + tmp_file.write_all(c.as_bytes())?; + + Ok(Command::new("taskset") + .arg("--cpu-list") + .arg(vcpus.as_cpu_list()) + .arg(&command.aquatic) + .arg("-c") + .arg(tmp_file.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?) + } + + fn priority(&self) -> crate::common::Priority { + self.priority + } + + fn keys(&self) -> IndexMap { + indexmap! { + "socket workers".to_string() => self.socket_workers.to_string(), + } + } +} + +#[derive(Debug, Clone)] +struct OpenTrackerUdpRunner { + workers: usize, + priority: Priority, +} + +impl OpenTrackerUdpRunner { + #[allow(clippy::new_ret_no_self)] + fn new(workers: usize, priority: Priority) -> Rc> { + Rc::new(Self { workers, priority }) + } +} + +impl ProcessRunner for OpenTrackerUdpRunner { + type Command = UdpCommand; + + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result { + writeln!( + tmp_file, + "listen.udp.workers {}\nlisten.udp 127.0.0.1:3000", + self.workers + )?; + + Ok(Command::new("taskset") + .arg("--cpu-list") + .arg(vcpus.as_cpu_list()) + .arg(&command.opentracker) + .arg("-f") + .arg(tmp_file.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?) + } + + fn priority(&self) -> crate::common::Priority { + self.priority + } + + fn keys(&self) -> IndexMap { + indexmap! { + "workers".to_string() => self.workers.to_string(), + } + } +} + +#[derive(Debug, Clone)] +struct ChihayaUdpRunner; + +impl ChihayaUdpRunner { + #[allow(clippy::new_ret_no_self)] + fn new() -> Rc> { + Rc::new(Self {}) + } +} + +impl ProcessRunner for ChihayaUdpRunner { + type Command = UdpCommand; + + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result { + writedoc!( + tmp_file, + r#" + --- + chihaya: + metrics_addr: "127.0.0.1:0" + udp: + addr: "127.0.0.1:3000" + private_key: "abcdefghijklmnopqrst" + max_numwant: 30 + default_numwant: 30 + storage: + name: "memory" + "#, + )?; + + Ok(Command::new("taskset") + .arg("--cpu-list") + .arg(vcpus.as_cpu_list()) + .arg(&command.chihaya) + .arg("--config") + .arg(tmp_file.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?) + } + + fn priority(&self) -> crate::common::Priority { + Priority::High + } + + fn keys(&self) -> IndexMap { + Default::default() + } +} + +#[derive(Debug, Clone)] +struct TorrustTrackerUdpRunner; + +impl TorrustTrackerUdpRunner { + #[allow(clippy::new_ret_no_self)] + fn new() -> Rc> { + Rc::new(Self {}) + } +} + +impl ProcessRunner for TorrustTrackerUdpRunner { + type Command = UdpCommand; + + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result { + writedoc!( + tmp_file, + r#" + [metadata] + schema_version = "2.0.0" + + [logging] + threshold = "error" + + [core] + listed = false + private = false + tracker_usage_statistics = false + + [core.database] + driver = "sqlite3" + path = "./sqlite3.db" + + [core.tracker_policy] + persistent_torrent_completed_stat = false + remove_peerless_torrents = false + + [[udp_trackers]] + bind_address = "0.0.0.0:3000" + "#, + )?; + + Ok(Command::new("taskset") + .arg("--cpu-list") + .arg(vcpus.as_cpu_list()) + .env("TORRUST_TRACKER_CONFIG_TOML_PATH", tmp_file.path()) + .arg(&command.torrust_tracker) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?) + } + + fn priority(&self) -> crate::common::Priority { + Priority::High + } + + fn keys(&self) -> IndexMap { + Default::default() + } +} + +#[derive(Debug, Clone)] +struct AquaticUdpLoadTestRunner { + parameters: LoadTestRunnerParameters, +} + +impl ProcessRunner for AquaticUdpLoadTestRunner { + type Command = UdpCommand; + + #[allow(clippy::field_reassign_with_default)] + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result { + let mut c = aquatic_udp_load_test::config::Config::default(); + + c.workers = self.parameters.workers as u8; + c.duration = self.parameters.duration; + c.summarize_last = self.parameters.summarize_last; + + c.extra_statistics = false; + + c.requests.announce_peers_wanted = 30; + c.requests.weight_connect = 0; + c.requests.weight_announce = 100; + c.requests.weight_scrape = 1; + + let c = toml::to_string_pretty(&c)?; + + tmp_file.write_all(c.as_bytes())?; + + Ok(Command::new("taskset") + .arg("--cpu-list") + .arg(vcpus.as_cpu_list()) + .arg(&command.load_test) + .arg("-c") + .arg(tmp_file.path()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?) + } + + fn priority(&self) -> crate::common::Priority { + eprintln!("load test runner priority method called"); + + Priority::High + } + + fn keys(&self) -> IndexMap { + indexmap! { + "workers".to_string() => self.parameters.workers.to_string(), + } + } +} diff --git a/apps/aquatic/crates/bencher/src/run.rs b/apps/aquatic/crates/bencher/src/run.rs new file mode 100644 index 0000000..a28e62f --- /dev/null +++ b/apps/aquatic/crates/bencher/src/run.rs @@ -0,0 +1,374 @@ +use std::{ + process::{Child, Command}, + rc::Rc, + str::FromStr, + time::Duration, +}; + +use indexmap::IndexMap; +use itertools::Itertools; +use nonblock::NonBlockingReader; +use once_cell::sync::Lazy; +use regex::Regex; +use tempfile::NamedTempFile; + +use crate::common::{Priority, TaskSetCpuList}; + +pub trait ProcessRunner: ::std::fmt::Debug { + type Command; + + fn run( + &self, + command: &Self::Command, + vcpus: &TaskSetCpuList, + tmp_file: &mut NamedTempFile, + ) -> anyhow::Result; + + fn keys(&self) -> IndexMap; + + fn priority(&self) -> Priority; + + fn info(&self) -> String { + self.keys() + .into_iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .join(", ") + } +} + +#[derive(Debug)] +pub struct RunConfig { + pub tracker_runner: Rc>, + pub tracker_vcpus: TaskSetCpuList, + pub load_test_runner: Box>, + pub load_test_vcpus: TaskSetCpuList, +} + +impl RunConfig { + pub fn run( + self, + command: &C, + duration: usize, + ) -> Result> { + let mut tracker_config_file = NamedTempFile::new().unwrap(); + let mut load_test_config_file = NamedTempFile::new().unwrap(); + + let mut tracker = + match self + .tracker_runner + .run(command, &self.tracker_vcpus, &mut tracker_config_file) + { + Ok(handle) => ChildWrapper(handle), + Err(err) => return Err(RunErrorResults::new(self).set_error(err, "run tracker")), + }; + + ::std::thread::sleep(Duration::from_secs(1)); + + let mut load_tester = match self.load_test_runner.run( + command, + &self.load_test_vcpus, + &mut load_test_config_file, + ) { + Ok(handle) => ChildWrapper(handle), + Err(err) => { + return Err(RunErrorResults::new(self) + .set_error(err, "run load test") + .set_tracker_outputs(tracker)) + } + }; + + for _ in 0..(duration - 1) { + if let Ok(Some(status)) = tracker.0.try_wait() { + return Err(RunErrorResults::new(self) + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester) + .set_error_context(&format!("tracker exited with {}", status))); + } + + ::std::thread::sleep(Duration::from_secs(1)); + } + + // Note: a more advanced version tracking threads too would add argument + // "-L" and add "comm" to output format list + let tracker_process_stats_res = Command::new("ps") + .arg("-p") + .arg(tracker.0.id().to_string()) + .arg("-o") + .arg("%cpu,rss") + .arg("--noheader") + .output(); + + let tracker_process_stats = match tracker_process_stats_res { + Ok(output) if output.status.success() => { + ProcessStats::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap() + } + Ok(_) => { + return Err(RunErrorResults::new(self) + .set_error_context("run ps") + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester)); + } + Err(err) => { + return Err(RunErrorResults::new(self) + .set_error(err.into(), "run ps") + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester)); + } + }; + + ::std::thread::sleep(Duration::from_secs(5)); + + let (load_test_stdout, load_test_stderr) = match load_tester.0.try_wait() { + Ok(Some(status)) if status.success() => read_child_outputs(load_tester), + Ok(Some(_)) => { + return Err(RunErrorResults::new(self) + .set_error_context("wait for load tester") + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester)) + } + Ok(None) => { + if let Err(err) = load_tester.0.kill() { + return Err(RunErrorResults::new(self) + .set_error(err.into(), "kill load tester") + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester)); + } + + ::std::thread::sleep(Duration::from_secs(1)); + + match load_tester.0.try_wait() { + Ok(_) => { + return Err(RunErrorResults::new(self) + .set_error_context("load tester didn't finish in time") + .set_load_test_outputs(load_tester)) + } + Err(err) => { + return Err(RunErrorResults::new(self) + .set_error(err.into(), "wait for load tester after kill") + .set_tracker_outputs(tracker)); + } + } + } + Err(err) => { + return Err(RunErrorResults::new(self) + .set_error(err.into(), "wait for load tester") + .set_tracker_outputs(tracker) + .set_load_test_outputs(load_tester)) + } + }; + + let load_test_stdout = if let Some(load_test_stdout) = load_test_stdout { + load_test_stdout + } else { + return Err(RunErrorResults::new(self) + .set_error_context("couldn't read load tester stdout") + .set_tracker_outputs(tracker) + .set_load_test_stderr(load_test_stderr)); + }; + + let avg_responses = { + static RE: Lazy = + Lazy::new(|| Regex::new(r"Average responses per second: ([0-9]+)").unwrap()); + + let opt_avg_responses = RE + .captures_iter(&load_test_stdout) + .next() + .map(|c| { + let (_, [avg_responses]) = c.extract(); + + avg_responses.to_string() + }) + .and_then(|v| v.parse::().ok()); + + if let Some(avg_responses) = opt_avg_responses { + avg_responses + } else { + return Err(RunErrorResults::new(self) + .set_error_context("couldn't extract avg_responses") + .set_tracker_outputs(tracker) + .set_load_test_stdout(Some(load_test_stdout)) + .set_load_test_stderr(load_test_stderr)); + } + }; + + let results = RunSuccessResults { + tracker_process_stats, + avg_responses, + }; + + Ok(results) + } +} + +pub struct RunSuccessResults { + pub tracker_process_stats: ProcessStats, + pub avg_responses: u64, +} + +#[derive(Debug)] +pub struct RunErrorResults { + pub run_config: RunConfig, + pub tracker_stdout: Option, + pub tracker_stderr: Option, + pub load_test_stdout: Option, + pub load_test_stderr: Option, + pub error: Option, + pub error_context: Option, +} + +impl RunErrorResults { + fn new(run_config: RunConfig) -> Self { + Self { + run_config, + tracker_stdout: Default::default(), + tracker_stderr: Default::default(), + load_test_stdout: Default::default(), + load_test_stderr: Default::default(), + error: Default::default(), + error_context: Default::default(), + } + } + + fn set_tracker_outputs(mut self, tracker: ChildWrapper) -> Self { + let (stdout, stderr) = read_child_outputs(tracker); + + self.tracker_stdout = stdout; + self.tracker_stderr = stderr; + + self + } + + fn set_load_test_outputs(mut self, load_test: ChildWrapper) -> Self { + let (stdout, stderr) = read_child_outputs(load_test); + + self.load_test_stdout = stdout; + self.load_test_stderr = stderr; + + self + } + + fn set_load_test_stdout(mut self, stdout: Option) -> Self { + self.load_test_stdout = stdout; + + self + } + + fn set_load_test_stderr(mut self, stderr: Option) -> Self { + self.load_test_stderr = stderr; + + self + } + + fn set_error(mut self, error: anyhow::Error, context: &str) -> Self { + self.error = Some(error); + self.error_context = Some(context.to_string()); + + self + } + + fn set_error_context(mut self, context: &str) -> Self { + self.error_context = Some(context.to_string()); + + self + } +} + +impl std::fmt::Display for RunErrorResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(t) = self.error_context.as_ref() { + writeln!(f, "- {}", t)?; + } + if let Some(err) = self.error.as_ref() { + writeln!(f, "- {:#}", err)?; + } + + writeln!(f, "- tracker_runner: {:?}", self.run_config.tracker_runner)?; + writeln!( + f, + "- load_test_runner: {:?}", + self.run_config.load_test_runner + )?; + writeln!( + f, + "- tracker_vcpus: {}", + self.run_config.tracker_vcpus.as_cpu_list() + )?; + writeln!( + f, + "- load_test_vcpus: {}", + self.run_config.load_test_vcpus.as_cpu_list() + )?; + + if let Some(t) = self.tracker_stdout.as_ref() { + writeln!(f, "- tracker stdout:\n```\n{}\n```", t)?; + } + if let Some(t) = self.tracker_stderr.as_ref() { + writeln!(f, "- tracker stderr:\n```\n{}\n```", t)?; + } + if let Some(t) = self.load_test_stdout.as_ref() { + writeln!(f, "- load test stdout:\n```\n{}\n```", t)?; + } + if let Some(t) = self.load_test_stderr.as_ref() { + writeln!(f, "- load test stderr:\n```\n{}\n```", t)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ProcessStats { + pub avg_cpu_utilization: f32, + pub peak_rss_bytes: u64, +} + +impl FromStr for ProcessStats { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut parts = s.split_whitespace(); + + let avg_cpu_utilization = parts.next().ok_or(())?.parse().map_err(|_| ())?; + let peak_rss_kb: f32 = parts.next().ok_or(())?.parse().map_err(|_| ())?; + + Ok(Self { + avg_cpu_utilization, + peak_rss_bytes: (peak_rss_kb * 1000.0) as u64, + }) + } +} + +struct ChildWrapper(Child); + +impl Drop for ChildWrapper { + fn drop(&mut self) { + let _ = self.0.kill(); + + ::std::thread::sleep(Duration::from_secs(1)); + + let _ = self.0.try_wait(); + } +} + +fn read_child_outputs(mut child: ChildWrapper) -> (Option, Option) { + let stdout = child.0.stdout.take().and_then(|stdout| { + let mut buf = String::new(); + + let mut reader = NonBlockingReader::from_fd(stdout).unwrap(); + + reader.read_available_to_string(&mut buf).unwrap(); + + (!buf.is_empty()).then_some(buf) + }); + let stderr = child.0.stderr.take().and_then(|stderr| { + let mut buf = String::new(); + + let mut reader = NonBlockingReader::from_fd(stderr).unwrap(); + + reader.read_available_to_string(&mut buf).unwrap(); + + (!buf.is_empty()).then_some(buf) + }); + + (stdout, stderr) +} diff --git a/apps/aquatic/crates/bencher/src/set.rs b/apps/aquatic/crates/bencher/src/set.rs new file mode 100644 index 0000000..cae39df --- /dev/null +++ b/apps/aquatic/crates/bencher/src/set.rs @@ -0,0 +1,290 @@ +use std::rc::Rc; + +use humanize_bytes::humanize_bytes_binary; +use indexmap::IndexMap; +use num_format::{Locale, ToFormattedString}; + +use crate::{ + common::{CpuDirection, CpuMode, Priority, TaskSetCpuList}, + html::{html_all_runs, html_best_results}, + run::{ProcessRunner, ProcessStats, RunConfig}, +}; + +#[derive(Debug, Clone, Copy)] +pub struct LoadTestRunnerParameters { + pub workers: usize, + pub duration: usize, + pub summarize_last: usize, +} + +pub trait Tracker: ::std::fmt::Debug + Copy + Clone + ::std::hash::Hash { + fn name(&self) -> String; +} + +pub struct SetConfig { + pub implementations: IndexMap>>>, + pub load_test_runs: Vec<(usize, Priority, TaskSetCpuList)>, +} + +#[allow(clippy::too_many_arguments)] +pub fn run_sets( + command: &C, + cpu_mode: CpuMode, + min_cores: Option, + max_cores: Option, + min_priority: Priority, + duration: usize, + summarize_last: usize, + mut set_configs: IndexMap>, + load_test_gen: F, +) where + C: ::std::fmt::Debug, + I: Tracker, + F: Fn(LoadTestRunnerParameters) -> Box>, +{ + if let Some(min_cores) = min_cores { + set_configs.retain(|cores, _| *cores >= min_cores); + } + if let Some(max_cores) = max_cores { + set_configs.retain(|cores, _| *cores <= max_cores); + } + + for set_config in set_configs.values_mut() { + for runners in set_config.implementations.values_mut() { + runners.retain(|r| r.priority() >= min_priority); + } + + set_config + .load_test_runs + .retain(|(_, priority, _)| *priority >= min_priority); + } + + println!("# Benchmark report"); + + let total_num_runs = set_configs + .values() + .map(|set| { + set.implementations.values().map(Vec::len).sum::() * set.load_test_runs.len() + }) + .sum::(); + + let (estimated_hours, estimated_minutes) = { + let minutes = (total_num_runs * (duration + 7)) / 60; + + (minutes / 60, minutes % 60) + }; + + println!(); + println!("Total number of load test runs: {}", total_num_runs); + println!( + "Estimated duration: {} hours, {} minutes", + estimated_hours, estimated_minutes + ); + println!(); + + let results = set_configs + .into_iter() + .map(|(tracker_core_count, set_config)| { + let tracker_vcpus = + TaskSetCpuList::new(cpu_mode, CpuDirection::Asc, tracker_core_count).unwrap(); + + println!( + "## Tracker cores: {} (cpus: {})", + tracker_core_count, + tracker_vcpus.as_cpu_list() + ); + + let tracker_results = set_config + .implementations + .into_iter() + .map(|(implementation, tracker_runs)| { + let tracker_run_results = tracker_runs + .iter() + .map(|tracker_run| { + let load_test_run_results = set_config + .load_test_runs + .clone() + .into_iter() + .map(|(workers, _, load_test_vcpus)| { + let load_test_parameters = LoadTestRunnerParameters { + workers, + duration, + summarize_last, + }; + LoadTestRunResults::produce( + command, + &load_test_gen, + load_test_parameters, + implementation, + tracker_run, + tracker_vcpus.clone(), + load_test_vcpus, + ) + }) + .collect(); + + TrackerConfigurationResults { + load_tests: load_test_run_results, + } + }) + .collect(); + + ImplementationResults { + name: implementation.name(), + configurations: tracker_run_results, + } + }) + .collect(); + + TrackerCoreCountResults { + core_count: tracker_core_count, + implementations: tracker_results, + } + }) + .collect::>(); + + println!("{}", html_all_runs(&results)); + println!("{}", html_best_results(&results)); +} + +pub struct TrackerCoreCountResults { + pub core_count: usize, + pub implementations: Vec, +} + +pub struct ImplementationResults { + pub name: String, + pub configurations: Vec, +} + +impl ImplementationResults { + pub fn best_result(&self) -> Option { + self.configurations + .iter() + .filter_map(|c| c.best_result()) + .reduce(|acc, r| { + if r.average_responses > acc.average_responses { + r + } else { + acc + } + }) + } +} + +pub struct TrackerConfigurationResults { + pub load_tests: Vec, +} + +impl TrackerConfigurationResults { + fn best_result(&self) -> Option { + self.load_tests + .iter() + .filter_map(|r| match r { + LoadTestRunResults::Success(r) => Some(r.clone()), + LoadTestRunResults::Failure(_) => None, + }) + .reduce(|acc, r| { + if r.average_responses > acc.average_responses { + r + } else { + acc + } + }) + } +} + +pub enum LoadTestRunResults { + Success(LoadTestRunResultsSuccess), + Failure(LoadTestRunResultsFailure), +} + +impl LoadTestRunResults { + pub fn produce( + command: &C, + load_test_gen: &F, + load_test_parameters: LoadTestRunnerParameters, + implementation: I, + tracker_process: &Rc>, + tracker_vcpus: TaskSetCpuList, + load_test_vcpus: TaskSetCpuList, + ) -> Self + where + C: ::std::fmt::Debug, + I: Tracker, + F: Fn(LoadTestRunnerParameters) -> Box>, + { + println!( + "### {} run ({}) (load test workers: {}, cpus: {})", + implementation.name(), + tracker_process.info(), + load_test_parameters.workers, + load_test_vcpus.as_cpu_list() + ); + + let load_test_runner = load_test_gen(load_test_parameters); + let load_test_keys = load_test_runner.keys(); + + let run_config = RunConfig { + tracker_runner: tracker_process.clone(), + tracker_vcpus: tracker_vcpus.clone(), + load_test_runner, + load_test_vcpus: load_test_vcpus.clone(), + }; + + match run_config.run(command, load_test_parameters.duration) { + Ok(r) => { + println!( + "- Average responses per second: {}", + r.avg_responses.to_formatted_string(&Locale::en) + ); + println!( + "- Average tracker CPU utilization: {}%", + r.tracker_process_stats.avg_cpu_utilization, + ); + println!( + "- Peak tracker RSS: {}", + humanize_bytes_binary!(r.tracker_process_stats.peak_rss_bytes) + ); + + LoadTestRunResults::Success(LoadTestRunResultsSuccess { + average_responses: r.avg_responses, + tracker_keys: tracker_process.keys(), + tracker_info: tracker_process.info(), + tracker_process_stats: r.tracker_process_stats, + tracker_vcpus, + load_test_keys, + load_test_vcpus, + }) + } + Err(results) => { + println!("\nRun failed:\n{:#}\n", results); + + LoadTestRunResults::Failure(LoadTestRunResultsFailure { + tracker_keys: tracker_process.keys(), + tracker_vcpus, + load_test_keys, + load_test_vcpus, + }) + } + } + } +} + +#[derive(Clone)] +pub struct LoadTestRunResultsSuccess { + pub average_responses: u64, + pub tracker_keys: IndexMap, + pub tracker_info: String, + pub tracker_process_stats: ProcessStats, + pub tracker_vcpus: TaskSetCpuList, + pub load_test_keys: IndexMap, + pub load_test_vcpus: TaskSetCpuList, +} + +pub struct LoadTestRunResultsFailure { + pub tracker_keys: IndexMap, + pub tracker_vcpus: TaskSetCpuList, + pub load_test_keys: IndexMap, + pub load_test_vcpus: TaskSetCpuList, +} diff --git a/apps/aquatic/crates/combined_binary/Cargo.toml b/apps/aquatic/crates/combined_binary/Cargo.toml new file mode 100644 index 0000000..e13d3e5 --- /dev/null +++ b/apps/aquatic/crates/combined_binary/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "aquatic" +description = "High-performance open BitTorrent tracker (UDP, HTTP, WebTorrent)" +keywords = ["bittorrent", "torrent", "webtorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +rust-version.workspace = true + +[[bin]] +name = "aquatic" + +[dependencies] +aquatic_common.workspace = true +aquatic_http.workspace = true +aquatic_udp.workspace = true +aquatic_ws.workspace = true +mimalloc = { version = "0.1", default-features = false } diff --git a/apps/aquatic/crates/combined_binary/src/main.rs b/apps/aquatic/crates/combined_binary/src/main.rs new file mode 100644 index 0000000..56eeddd --- /dev/null +++ b/apps/aquatic/crates/combined_binary/src/main.rs @@ -0,0 +1,91 @@ +use aquatic_common::cli::{print_help, run_app_with_cli_and_config, Options}; +use aquatic_http::config::Config as HttpConfig; +use aquatic_udp::config::Config as UdpConfig; +use aquatic_ws::config::Config as WsConfig; + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +const APP_NAME: &str = "aquatic: BitTorrent tracker"; + +fn main() { + ::std::process::exit(match run() { + Ok(()) => 0, + Err(None) => { + print_help(gen_info, None); + + 0 + } + Err(opt_err @ Some(_)) => { + print_help(gen_info, opt_err); + + 1 + } + }) +} + +fn run() -> Result<(), Option> { + let mut arg_iter = ::std::env::args().skip(1); + + let protocol = if let Some(protocol) = arg_iter.next() { + protocol + } else { + return Err(None); + }; + + let options = match Options::parse_args(arg_iter) { + Ok(options) => options, + Err(opt_err) => { + return Err(opt_err); + } + }; + + match protocol.as_str() { + "udp" => run_app_with_cli_and_config::( + aquatic_udp::APP_NAME, + aquatic_udp::APP_VERSION, + aquatic_udp::run, + Some(options), + ), + "http" => run_app_with_cli_and_config::( + aquatic_http::APP_NAME, + aquatic_http::APP_VERSION, + aquatic_http::run, + Some(options), + ), + "ws" => run_app_with_cli_and_config::( + aquatic_ws::APP_NAME, + aquatic_ws::APP_VERSION, + aquatic_ws::run, + Some(options), + ), + arg => { + let opt_err = if arg == "-h" || arg == "--help" { + None + } else if arg.starts_with('-') { + Some("First argument must be protocol".to_string()) + } else { + Some("Invalid protocol".to_string()) + }; + + return Err(opt_err); + } + } + + Ok(()) +} + +fn gen_info() -> String { + let mut info = String::new(); + + info.push_str(APP_NAME); + + let app_path = ::std::env::args().next().unwrap(); + info.push_str(&format!("\n\nUsage: {} PROTOCOL [OPTIONS]", app_path)); + info.push_str("\n\nAvailable protocols:"); + info.push_str("\n udp BitTorrent over UDP"); + info.push_str("\n http BitTorrent over HTTP"); + info.push_str("\n ws WebTorrent"); + + info +} diff --git a/apps/aquatic/crates/common/Cargo.toml b/apps/aquatic/crates/common/Cargo.toml new file mode 100644 index 0000000..80505b2 --- /dev/null +++ b/apps/aquatic/crates/common/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "aquatic_common" +description = "aquatic BitTorrent tracker common code" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +rust-version.workspace = true + +[lib] +name = "aquatic_common" + +[features] +rustls = ["dep:rustls", "rustls-pemfile"] +prometheus = ["dep:metrics", "dep:metrics-util", "dep:metrics-exporter-prometheus", "dep:tokio"] +# Experimental CPU pinning support. Requires hwloc (apt-get install libhwloc-dev) +cpu-pinning = ["dep:hwloc"] + +[dependencies] +aquatic_toml_config.workspace = true + +ahash = "0.8" +anyhow = "1" +arc-swap = "1" +duplicate = "2" +git-testament = "0.2" +hashbrown = "0.15" +hex = "0.4" +indexmap = "2" +libc = "0.2" +log = "0.4" +privdrop = "0.5" +rand = { version = "0.8", features = ["small_rng"] } +serde = { version = "1", features = ["derive"] } +simplelog = { version = "0.12" } +toml = "0.5" + +# rustls feature +rustls = { version = "0.23", optional = true } +rustls-pemfile = { version = "2", optional = true } + +# prometheus feature +metrics = { version = "0.24", optional = true } +metrics-util = { version = "0.19", optional = true } +metrics-exporter-prometheus = { version = "0.16", optional = true, default-features = false, features = ["http-listener"] } +tokio = { version = "1", optional = true, features = ["rt", "net", "time"] } + +# cpu pinning feature +hwloc = { version = "0.5", optional = true } \ No newline at end of file diff --git a/apps/aquatic/crates/common/src/access_list.rs b/apps/aquatic/crates/common/src/access_list.rs new file mode 100644 index 0000000..5ba7ea8 --- /dev/null +++ b/apps/aquatic/crates/common/src/access_list.rs @@ -0,0 +1,197 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Context; +use aquatic_toml_config::TomlConfig; +use arc_swap::{ArcSwap, Cache}; +use hashbrown::HashSet; +use serde::{Deserialize, Serialize}; + +/// Access list mode. Available modes are allow, deny and off. +#[derive(Clone, Copy, Debug, PartialEq, TomlConfig, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AccessListMode { + /// Only serve torrents with info hash present in file + Allow, + /// Do not serve torrents if info hash present in file + Deny, + /// Turn off access list functionality + Off, +} + +impl AccessListMode { + pub fn is_on(&self) -> bool { + !matches!(self, Self::Off) + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct AccessListConfig { + pub mode: AccessListMode, + /// Path to access list file consisting of newline-separated hex-encoded info hashes. + /// + /// If using chroot mode, path must be relative to new root. + pub path: PathBuf, +} + +impl Default for AccessListConfig { + fn default() -> Self { + Self { + path: "./access-list.txt".into(), + mode: AccessListMode::Off, + } + } +} + +#[derive(Default, Clone)] +pub struct AccessList(HashSet<[u8; 20]>); + +impl AccessList { + pub fn insert_from_line(&mut self, line: &str) -> anyhow::Result<()> { + self.0.insert(parse_info_hash(line)?); + + Ok(()) + } + + pub fn create_from_path(path: &PathBuf) -> anyhow::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + + let mut new_list = Self::default(); + + for line in reader.lines() { + let line = line?; + let line = line.trim(); + + if line.is_empty() { + continue; + } + + new_list + .insert_from_line(line) + .with_context(|| format!("Invalid line in access list: {}", line))?; + } + + Ok(new_list) + } + + pub fn allows(&self, mode: AccessListMode, info_hash: &[u8; 20]) -> bool { + match mode { + AccessListMode::Allow => self.0.contains(info_hash), + AccessListMode::Deny => !self.0.contains(info_hash), + AccessListMode::Off => true, + } + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } +} + +pub trait AccessListQuery { + fn update(&self, config: &AccessListConfig) -> anyhow::Result<()>; + fn allows(&self, list_mode: AccessListMode, info_hash_bytes: &[u8; 20]) -> bool; +} + +pub type AccessListArcSwap = ArcSwap; +pub type AccessListCache = Cache, Arc>; + +impl AccessListQuery for AccessListArcSwap { + fn update(&self, config: &AccessListConfig) -> anyhow::Result<()> { + self.store(Arc::new(AccessList::create_from_path(&config.path)?)); + + Ok(()) + } + + fn allows(&self, mode: AccessListMode, info_hash_bytes: &[u8; 20]) -> bool { + match mode { + AccessListMode::Allow => self.load().0.contains(info_hash_bytes), + AccessListMode::Deny => !self.load().0.contains(info_hash_bytes), + AccessListMode::Off => true, + } + } +} + +pub fn create_access_list_cache(arc_swap: &Arc) -> AccessListCache { + Cache::from(Arc::clone(arc_swap)) +} + +pub fn update_access_list( + config: &AccessListConfig, + access_list: &Arc, +) -> anyhow::Result<()> { + if config.mode.is_on() { + match access_list.update(config) { + Ok(()) => { + ::log::info!("Access list updated") + } + Err(err) => { + ::log::error!("Updating access list failed: {:#}", err); + + return Err(err); + } + } + } + + Ok(()) +} + +fn parse_info_hash(line: &str) -> anyhow::Result<[u8; 20]> { + let mut bytes = [0u8; 20]; + + hex::decode_to_slice(line, &mut bytes)?; + + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_info_hash() { + let f = parse_info_hash; + + assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeee").is_ok()); + assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeeef").is_err()); + assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeee").is_err()); + assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeeö").is_err()); + } + + #[test] + fn test_cache_allows() { + let mut access_list = AccessList::default(); + + let a = parse_info_hash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(); + let b = parse_info_hash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); + let c = parse_info_hash("cccccccccccccccccccccccccccccccccccccccc").unwrap(); + + access_list.0.insert(a); + access_list.0.insert(b); + + let access_list = Arc::new(ArcSwap::new(Arc::new(access_list))); + + let mut access_list_cache = Cache::new(Arc::clone(&access_list)); + + assert!(access_list_cache.load().allows(AccessListMode::Allow, &a)); + assert!(access_list_cache.load().allows(AccessListMode::Allow, &b)); + assert!(!access_list_cache.load().allows(AccessListMode::Allow, &c)); + + assert!(!access_list_cache.load().allows(AccessListMode::Deny, &a)); + assert!(!access_list_cache.load().allows(AccessListMode::Deny, &b)); + assert!(access_list_cache.load().allows(AccessListMode::Deny, &c)); + + assert!(access_list_cache.load().allows(AccessListMode::Off, &a)); + assert!(access_list_cache.load().allows(AccessListMode::Off, &b)); + assert!(access_list_cache.load().allows(AccessListMode::Off, &c)); + + access_list.store(Arc::new(AccessList::default())); + + assert!(access_list_cache.load().allows(AccessListMode::Deny, &a)); + assert!(access_list_cache.load().allows(AccessListMode::Deny, &b)); + } +} diff --git a/apps/aquatic/crates/common/src/cli.rs b/apps/aquatic/crates/common/src/cli.rs new file mode 100644 index 0000000..16521d3 --- /dev/null +++ b/apps/aquatic/crates/common/src/cli.rs @@ -0,0 +1,255 @@ +use std::fs::File; +use std::io::Read; + +use anyhow::Context; +use aquatic_toml_config::TomlConfig; +use git_testament::{git_testament, CommitKind}; +use log::LevelFilter; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use simplelog::{ColorChoice, TermLogger, TerminalMode, ThreadLogMode}; + +/// Log level. Available values are off, error, warn, info, debug and trace. +#[derive(Debug, Clone, Copy, PartialEq, TomlConfig, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl Default for LogLevel { + fn default() -> Self { + Self::Warn + } +} + +pub trait Config: Default + TomlConfig + DeserializeOwned + std::fmt::Debug { + fn get_log_level(&self) -> Option { + None + } +} + +#[derive(Debug, Default)] +pub struct Options { + config_file: Option, + print_config: bool, + print_parsed_config: bool, + print_version: bool, +} + +impl Options { + pub fn parse_args(mut arg_iter: I) -> Result> + where + I: Iterator, + { + let mut options = Options::default(); + + #[allow(clippy::while_let_loop)] // False positive + loop { + if let Some(arg) = arg_iter.next() { + match arg.as_str() { + "-c" | "--config-file" => { + if let Some(path) = arg_iter.next() { + options.config_file = Some(path); + } else { + return Err(Some("No config file path given".to_string())); + } + } + "-p" | "--print-config" => { + options.print_config = true; + } + "-P" => { + options.print_parsed_config = true; + } + "-v" | "--version" => { + options.print_version = true; + } + "-h" | "--help" => { + return Err(None); + } + "" => (), + _ => { + return Err(Some("Unrecognized argument".to_string())); + } + } + } else { + break; + } + } + + Ok(options) + } +} + +pub fn run_app_with_cli_and_config( + app_title: &str, + crate_version: &str, + // Function that takes config file and runs application + app_fn: fn(T) -> anyhow::Result<()>, + opts: Option, +) where + T: Config, +{ + ::std::process::exit(match run_inner(app_title, crate_version, app_fn, opts) { + Ok(()) => 0, + Err(err) => { + eprintln!("Error: {:#}", err); + + 1 + } + }) +} + +fn run_inner( + app_title: &str, + crate_version: &str, + // Function that takes config file and runs application + app_fn: fn(T) -> anyhow::Result<()>, + // Possibly preparsed options + options: Option, +) -> anyhow::Result<()> +where + T: Config, +{ + let options = if let Some(options) = options { + options + } else { + let mut arg_iter = ::std::env::args(); + + let app_path = arg_iter.next().unwrap(); + + match Options::parse_args(arg_iter) { + Ok(options) => options, + Err(opt_err) => { + let gen_info = || format!("{}\n\nUsage: {} [OPTIONS]", app_title, app_path); + + print_help(gen_info, opt_err); + + return Ok(()); + } + } + }; + + if options.print_version { + let commit_info = get_commit_info(); + + println!("{}{}", crate_version, commit_info); + + Ok(()) + } else if options.print_config { + print!("{}", default_config_as_toml::()); + + Ok(()) + } else { + let config = if let Some(path) = options.config_file { + config_from_toml_file(path)? + } else { + T::default() + }; + + if let Some(log_level) = config.get_log_level() { + start_logger(log_level)?; + } + + if options.print_parsed_config { + println!("Running with configuration: {:#?}", config); + } + + app_fn(config) + } +} + +pub fn print_help(info_generator: F, opt_error: Option) +where + F: FnOnce() -> String, +{ + println!("{}", info_generator()); + + println!("\nOptions:"); + println!(" -c, --config-file Load config from this path"); + println!(" -h, --help Print this help message"); + println!(" -p, --print-config Print default config"); + println!(" -P Print parsed config"); + println!(" -v, --version Print version information"); + + if let Some(error) = opt_error { + println!("\nError: {}.", error); + } +} + +fn config_from_toml_file(path: String) -> anyhow::Result +where + T: DeserializeOwned, +{ + let mut file = File::open(path.clone()) + .with_context(|| format!("Couldn't open config file {}", path.clone()))?; + + let mut data = String::new(); + + file.read_to_string(&mut data) + .with_context(|| format!("Couldn't read config file {}", path.clone()))?; + + toml::from_str(&data).with_context(|| format!("Couldn't parse config file {}", path.clone())) +} + +fn default_config_as_toml() -> String +where + T: Default + TomlConfig, +{ + ::default_to_string() +} + +fn start_logger(log_level: LogLevel) -> ::anyhow::Result<()> { + let mut builder = simplelog::ConfigBuilder::new(); + + builder + .set_thread_mode(ThreadLogMode::Both) + .set_thread_level(LevelFilter::Error) + .set_target_level(LevelFilter::Error) + .set_location_level(LevelFilter::Off); + + let config = match builder.set_time_offset_to_local() { + Ok(builder) => builder.build(), + Err(builder) => builder.build(), + }; + + let level_filter = match log_level { + LogLevel::Off => LevelFilter::Off, + LogLevel::Error => LevelFilter::Error, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Info => LevelFilter::Info, + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Trace => LevelFilter::Trace, + }; + + TermLogger::init( + level_filter, + config, + TerminalMode::Stderr, + ColorChoice::Auto, + ) + .context("Couldn't initialize logger")?; + + Ok(()) +} + +fn get_commit_info() -> String { + git_testament!(TESTAMENT); + + match TESTAMENT.commit { + CommitKind::NoTags(hash, date) => { + format!(" ({} - {})", first_8_chars(hash), date) + } + CommitKind::FromTag(_tag, hash, date, _tag_distance) => { + format!(" ({} - {})", first_8_chars(hash), date) + } + _ => String::new(), + } +} + +fn first_8_chars(input: &str) -> String { + input.chars().take(8).collect() +} diff --git a/apps/aquatic/crates/common/src/cpu_pinning.rs b/apps/aquatic/crates/common/src/cpu_pinning.rs new file mode 100644 index 0000000..30c9918 --- /dev/null +++ b/apps/aquatic/crates/common/src/cpu_pinning.rs @@ -0,0 +1,240 @@ +//! Experimental CPU pinning + +use aquatic_toml_config::TomlConfig; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, TomlConfig, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CpuPinningDirection { + Ascending, + Descending, +} + +impl Default for CpuPinningDirection { + fn default() -> Self { + Self::Ascending + } +} + +pub trait CpuPinningConfig { + fn active(&self) -> bool; + fn direction(&self) -> CpuPinningDirection; + fn core_offset(&self) -> usize; +} + +// Do these shenanigans for compatibility with aquatic_toml_config +#[duplicate::duplicate_item( + mod_name struct_name cpu_pinning_direction; + [asc] [CpuPinningConfigAsc] [CpuPinningDirection::Ascending]; + [desc] [CpuPinningConfigDesc] [CpuPinningDirection::Descending]; +)] +pub mod mod_name { + use super::*; + + /// Experimental cpu pinning + #[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] + pub struct struct_name { + pub active: bool, + pub direction: CpuPinningDirection, + pub core_offset: usize, + } + + impl Default for struct_name { + fn default() -> Self { + Self { + active: false, + direction: cpu_pinning_direction, + core_offset: 0, + } + } + } + impl CpuPinningConfig for struct_name { + fn active(&self) -> bool { + self.active + } + fn direction(&self) -> CpuPinningDirection { + self.direction + } + fn core_offset(&self) -> usize { + self.core_offset + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum WorkerIndex { + SocketWorker(usize), + SwarmWorker(usize), + Util, +} + +impl WorkerIndex { + pub fn get_core_index( + &self, + config: &C, + socket_workers: usize, + swarm_workers: usize, + num_cores: usize, + ) -> usize { + let ascending_index = match self { + Self::SocketWorker(index) => config.core_offset() + index, + Self::SwarmWorker(index) => config.core_offset() + socket_workers + index, + Self::Util => config.core_offset() + socket_workers + swarm_workers, + }; + + let max_core_index = num_cores - 1; + + let ascending_index = ascending_index.min(max_core_index); + + match config.direction() { + CpuPinningDirection::Ascending => ascending_index, + CpuPinningDirection::Descending => max_core_index - ascending_index, + } + } +} + +/// Pin current thread to a suitable core +/// +/// Requires hwloc (`apt-get install libhwloc-dev`) +pub fn pin_current_if_configured_to( + config: &C, + socket_workers: usize, + swarm_workers: usize, + worker_index: WorkerIndex, +) { + use hwloc::{CpuSet, ObjectType, Topology, CPUBIND_THREAD}; + + if config.active() { + let mut topology = Topology::new(); + + let core_cpu_sets: Vec = topology + .objects_with_type(&ObjectType::Core) + .expect("hwloc: list cores") + .into_iter() + .map(|core| core.allowed_cpuset().expect("hwloc: get core cpu set")) + .collect(); + + let num_cores = core_cpu_sets.len(); + + let core_index = + worker_index.get_core_index(config, socket_workers, swarm_workers, num_cores); + + let cpu_set = core_cpu_sets + .get(core_index) + .unwrap_or_else(|| panic!("get cpu set for core {}", core_index)) + .to_owned(); + + topology + .set_cpubind(cpu_set, CPUBIND_THREAD) + .unwrap_or_else(|err| panic!("bind thread to core {}: {:?}", core_index, err)); + + ::log::info!( + "Pinned worker {:?} to cpu core {}", + worker_index, + core_index + ); + } +} + +/// Tell Linux that incoming messages should be handled by the socket worker +/// with the same index as the CPU core receiving the interrupt. +/// +/// Requires that sockets are actually bound in order, so waiting has to be done +/// in socket workers. +/// +/// It might make sense to first enable RSS or RPS (if hardware doesn't support +/// RSS) and enable sending interrupts to all CPUs that have socket workers +/// running on them. Possibly, CPU 0 should be excluded. +/// +/// More Information: +/// - https://talawah.io/blog/extreme-http-performance-tuning-one-point-two-million/ +/// - https://www.kernel.org/doc/Documentation/networking/scaling.txt +/// - https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/performance_tuning_guide/network-rps +#[cfg(target_os = "linux")] +pub fn socket_attach_cbpf( + socket: &S, + _num_sockets: usize, +) -> ::std::io::Result<()> { + use std::mem::size_of; + use std::os::raw::c_void; + + use libc::{setsockopt, sock_filter, sock_fprog, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF}; + + // Good BPF documentation: https://man.openbsd.org/bpf.4 + + // Values of constants were copied from the following Linux source files: + // - include/uapi/linux/bpf_common.h + // - include/uapi/linux/filter.h + + // Instruction + const BPF_LD: u16 = 0x00; // Load into A + // const BPF_LDX: u16 = 0x01; // Load into X + // const BPF_ALU: u16 = 0x04; // Load into X + const BPF_RET: u16 = 0x06; // Return value + // const BPF_MOD: u16 = 0x90; // Run modulo on A + + // Size + const BPF_W: u16 = 0x00; // 32-bit width + + // Source + // const BPF_IMM: u16 = 0x00; // Use constant (k) + const BPF_ABS: u16 = 0x20; + + // Registers + // const BPF_K: u16 = 0x00; + const BPF_A: u16 = 0x10; + + // k + const SKF_AD_OFF: i32 = -0x1000; // Activate extensions + const SKF_AD_CPU: i32 = 36; // Extension for getting CPU + + // Return index of socket that should receive packet + let mut filter = [ + // Store index of CPU receiving packet in register A + sock_filter { + code: BPF_LD | BPF_W | BPF_ABS, + jt: 0, + jf: 0, + k: u32::from_ne_bytes((SKF_AD_OFF + SKF_AD_CPU).to_ne_bytes()), + }, + /* Disabled, because it doesn't make a lot of sense + // Run A = A % socket_workers + sock_filter { + code: BPF_ALU | BPF_MOD, + jt: 0, + jf: 0, + k: num_sockets as u32, + }, + */ + // Return A + sock_filter { + code: BPF_RET | BPF_A, + jt: 0, + jf: 0, + k: 0, + }, + ]; + + let program = sock_fprog { + filter: filter.as_mut_ptr(), + len: filter.len() as u16, + }; + + let program_ptr: *const sock_fprog = &program; + + unsafe { + let result = setsockopt( + socket.as_raw_fd(), + SOL_SOCKET, + SO_ATTACH_REUSEPORT_CBPF, + program_ptr as *const c_void, + size_of::() as u32, + ); + + if result != 0 { + Err(::std::io::Error::last_os_error()) + } else { + Ok(()) + } + } +} diff --git a/apps/aquatic/crates/common/src/lib.rs b/apps/aquatic/crates/common/src/lib.rs new file mode 100644 index 0000000..19e32cb --- /dev/null +++ b/apps/aquatic/crates/common/src/lib.rs @@ -0,0 +1,185 @@ +use std::fmt::Display; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; +use std::time::Instant; + +use ahash::RandomState; + +pub mod access_list; +pub mod cli; +#[cfg(feature = "cpu-pinning")] +pub mod cpu_pinning; +pub mod privileges; +#[cfg(feature = "rustls")] +pub mod rustls_config; + +/// IndexMap using AHash hasher +pub type IndexMap = indexmap::IndexMap; + +/// Peer, connection or similar valid until this instant +#[derive(Debug, Clone, Copy)] +pub struct ValidUntil(SecondsSinceServerStart); + +impl ValidUntil { + #[inline] + pub fn new(start_instant: ServerStartInstant, offset_seconds: u32) -> Self { + Self(SecondsSinceServerStart( + start_instant.seconds_elapsed().0 + offset_seconds, + )) + } + pub fn new_with_now(now: SecondsSinceServerStart, offset_seconds: u32) -> Self { + Self(SecondsSinceServerStart(now.0 + offset_seconds)) + } + pub fn valid(&self, now: SecondsSinceServerStart) -> bool { + self.0 .0 > now.0 + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ServerStartInstant(Instant); + +impl ServerStartInstant { + #[allow(clippy::new_without_default)] // I prefer ::new here + pub fn new() -> Self { + Self(Instant::now()) + } + pub fn seconds_elapsed(&self) -> SecondsSinceServerStart { + SecondsSinceServerStart( + self.0 + .elapsed() + .as_secs() + .try_into() + .expect("server ran for more seconds than what fits in a u32"), + ) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct SecondsSinceServerStart(u32); + +/// SocketAddr that is not an IPv6-mapped IPv4 address +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct CanonicalSocketAddr(SocketAddr); + +impl CanonicalSocketAddr { + pub fn new(addr: SocketAddr) -> Self { + match addr { + addr @ SocketAddr::V4(_) => Self(addr), + SocketAddr::V6(addr) => { + match addr.ip().octets() { + // Convert IPv4-mapped address (available in std but nightly-only) + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a, b, c, d] => Self(SocketAddr::V4( + SocketAddrV4::new(Ipv4Addr::new(a, b, c, d), addr.port()), + )), + _ => Self(addr.into()), + } + } + } + } + + pub fn get_ipv6_mapped(self) -> SocketAddr { + match self.0 { + SocketAddr::V4(addr) => { + let ip = addr.ip().to_ipv6_mapped(); + + SocketAddr::V6(SocketAddrV6::new(ip, addr.port(), 0, 0)) + } + addr => addr, + } + } + + pub fn get(self) -> SocketAddr { + self.0 + } + + pub fn get_ipv4(self) -> Option { + match self.0 { + addr @ SocketAddr::V4(_) => Some(addr), + _ => None, + } + } + + pub fn is_ipv4(&self) -> bool { + self.0.is_ipv4() + } +} + +#[cfg(feature = "prometheus")] +pub fn spawn_prometheus_endpoint( + addr: SocketAddr, + timeout: Option<::std::time::Duration>, + timeout_mask: Option, +) -> anyhow::Result<::std::thread::JoinHandle>> { + use std::thread::Builder; + use std::time::Duration; + + use anyhow::Context; + + let handle = Builder::new() + .name("prometheus".into()) + .spawn(move || { + use metrics_exporter_prometheus::PrometheusBuilder; + use metrics_util::MetricKindMask; + + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("build prometheus tokio runtime")?; + + rt.block_on(async { + let mask = timeout_mask.unwrap_or(MetricKindMask::ALL); + + let (recorder, exporter) = PrometheusBuilder::new() + .idle_timeout(mask, timeout) + .with_http_listener(addr) + .build() + .context("build prometheus recorder and exporter")?; + + let recorder_handle = recorder.handle(); + + ::metrics::set_global_recorder(recorder).context("set global metrics recorder")?; + + ::tokio::spawn(async move { + let mut interval = ::tokio::time::interval(Duration::from_secs(5)); + + loop { + interval.tick().await; + + // Periodically render metrics to make sure + // idles are cleaned up + recorder_handle.render(); + } + }); + + exporter + .await + .map_err(|err| anyhow::anyhow!("run prometheus exporter: :{:#?}", err)) + }) + }) + .context("spawn prometheus endpoint")?; + + Ok(handle) +} + +pub enum WorkerType { + Swarm(usize), + Socket(usize), + Statistics, + Signals, + Cleaning, + #[cfg(feature = "prometheus")] + Prometheus, +} + +impl Display for WorkerType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Swarm(index) => f.write_fmt(format_args!("Swarm worker {}", index + 1)), + Self::Socket(index) => f.write_fmt(format_args!("Socket worker {}", index + 1)), + Self::Statistics => f.write_str("Statistics worker"), + Self::Signals => f.write_str("Signals worker"), + Self::Cleaning => f.write_str("Cleaning worker"), + #[cfg(feature = "prometheus")] + Self::Prometheus => f.write_str("Prometheus worker"), + } + } +} diff --git a/apps/aquatic/crates/common/src/privileges.rs b/apps/aquatic/crates/common/src/privileges.rs new file mode 100644 index 0000000..10731ca --- /dev/null +++ b/apps/aquatic/crates/common/src/privileges.rs @@ -0,0 +1,62 @@ +use std::{ + path::PathBuf, + sync::{Arc, Barrier}, +}; + +use anyhow::Context; +use privdrop::PrivDrop; +use serde::{Deserialize, Serialize}; + +use aquatic_toml_config::TomlConfig; + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct PrivilegeConfig { + /// Chroot and switch group and user after binding to sockets + pub drop_privileges: bool, + /// Chroot to this path + pub chroot_path: PathBuf, + /// Group to switch to after chrooting + pub group: String, + /// User to switch to after chrooting + pub user: String, +} + +impl Default for PrivilegeConfig { + fn default() -> Self { + Self { + drop_privileges: false, + chroot_path: ".".into(), + user: "nobody".to_string(), + group: "nogroup".to_string(), + } + } +} + +#[derive(Clone)] +pub struct PrivilegeDropper { + barrier: Arc, + config: Arc, +} + +impl PrivilegeDropper { + pub fn new(config: PrivilegeConfig, num_sockets: usize) -> Self { + Self { + barrier: Arc::new(Barrier::new(num_sockets)), + config: Arc::new(config), + } + } + + pub fn after_socket_creation(self) -> anyhow::Result<()> { + if self.config.drop_privileges && self.barrier.wait().is_leader() { + PrivDrop::default() + .chroot(self.config.chroot_path.clone()) + .group(self.config.group.clone()) + .user(self.config.user.clone()) + .apply() + .with_context(|| "couldn't drop privileges after socket creation")?; + } + + Ok(()) + } +} diff --git a/apps/aquatic/crates/common/src/rustls_config.rs b/apps/aquatic/crates/common/src/rustls_config.rs new file mode 100644 index 0000000..86d2bd0 --- /dev/null +++ b/apps/aquatic/crates/common/src/rustls_config.rs @@ -0,0 +1,59 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use anyhow::Context; + +pub type RustlsConfig = rustls::ServerConfig; + +pub fn create_rustls_config( + tls_certificate_path: &Path, + tls_private_key_path: &Path, +) -> anyhow::Result { + let certs = { + let f = File::open(tls_certificate_path).with_context(|| { + format!( + "open tls certificate file at {}", + tls_certificate_path.to_string_lossy() + ) + })?; + let mut f = BufReader::new(f); + + let mut certs = Vec::new(); + + for cert in rustls_pemfile::certs(&mut f) { + match cert { + Ok(cert) => { + certs.push(cert); + } + Err(err) => { + ::log::error!("error parsing certificate: {:#?}", err) + } + } + } + + certs + }; + + let private_key = { + let f = File::open(tls_private_key_path).with_context(|| { + format!( + "open tls private key file at {}", + tls_private_key_path.to_string_lossy() + ) + })?; + let mut f = BufReader::new(f); + + let key = rustls_pemfile::pkcs8_private_keys(&mut f) + .next() + .ok_or(anyhow::anyhow!("No private keys in file"))??; + + #[allow(clippy::let_and_return)] // Using temporary variable fixes lifetime issue + key + }; + + let tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, rustls::pki_types::PrivateKeyDer::Pkcs8(private_key)) + .with_context(|| "create rustls config")?; + + Ok(tls_config) +} diff --git a/apps/aquatic/crates/http/Cargo.toml b/apps/aquatic/crates/http/Cargo.toml new file mode 100644 index 0000000..7d1ff77 --- /dev/null +++ b/apps/aquatic/crates/http/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "aquatic_http" +description = "High-performance open HTTP BitTorrent tracker (with optional TLS)" +keywords = ["http", "server", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[lib] +name = "aquatic_http" + +[[bin]] +name = "aquatic_http" + +[features] +default = ["prometheus", "mimalloc"] +prometheus = ["aquatic_common/prometheus", "metrics", "dep:metrics-util"] +metrics = ["dep:metrics"] +# Use mimalloc allocator for much better performance. +# +# Requires cmake and a C compiler +mimalloc = ["dep:mimalloc"] + +[dependencies] +aquatic_common = { workspace = true, features = ["rustls"] } +aquatic_http_protocol.workspace = true +aquatic_toml_config.workspace = true + +anyhow = "1" +arrayvec = "0.7" +arc-swap = "1" +cfg-if = "1" +either = "1" +futures = "0.3" +futures-lite = "1" +futures-rustls = "0.26" +glommio = "0.9" +httparse = "1" +itoa = "1" +libc = "0.2" +log = "0.4" +memchr = "2" +privdrop = "0.5" +once_cell = "1" +rand = { version = "0.8", features = ["small_rng"] } +rustls-pemfile = "2" +serde = { version = "1", features = ["derive"] } +signal-hook = { version = "0.3" } +slotmap = "1" +socket2 = { version = "0.5", features = ["all"] } +thiserror = "2" + +# metrics feature +metrics = { version = "0.24", optional = true } +metrics-util = { version = "0.19", optional = true } + +# mimalloc feature +mimalloc = { version = "0.1", default-features = false, optional = true } + +[dev-dependencies] +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/http/README.md b/apps/aquatic/crates/http/README.md new file mode 100644 index 0000000..2f17319 --- /dev/null +++ b/apps/aquatic/crates/http/README.md @@ -0,0 +1,121 @@ +# aquatic_http: high-performance open HTTP BitTorrent tracker + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +High-performance open HTTP BitTorrent tracker for Linux 5.8 or later. + +Features at a glance: + +- Multithreaded design for handling large amounts of traffic +- All data is stored in-memory (no database needed) +- IPv4 and IPv6 support +- Supports forbidding/allowing info hashes +- Prometheus metrics +- Automated CI testing of full file transfers + +## Performance + +![HTTP BitTorrent tracker throughput comparison](../../documents/aquatic-http-load-test-illustration-2023-01-25.png) + +More benchmark details are available [here](../../documents/aquatic-http-load-test-2023-01-25.pdf). + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_http +``` + +### Configuring + +Generate the configuration file: + +```sh +./target/release/aquatic_http -p > "aquatic-http-config.toml" +``` + +Make necessary adjustments to the file. You will likely want to adjust +listening addresses under the `network` section. + +To run over TLS, configure certificate and private key files. + +Running behind a reverse proxy is supported. Please refer to the config file +for details. + +### Running + +Make sure locked memory limits are sufficient: +- If you're using a systemd service file, add `LimitMEMLOCK=65536000` to it +- Otherwise, add the following lines to +`/etc/security/limits.conf`, and then log out and back in: + +``` +* hard memlock 65536 +* soft memlock 65536 +``` + +Once done, start the application: + +```sh +./target/release/aquatic_http -c "aquatic-http-config.toml" +``` + +If your server is pointed to by domain `example.com` and you configured the +tracker to run on port 3000, people can now use it by adding the URL +`https://example.com:3000/announce` to their torrent files or magnet links. + +### Load testing + +A load test application is available. It supports generation and loading of +configuration files in a similar manner to the tracker application. + +After starting the tracker, run the load tester: + +```sh +. ./scripts/env-native-cpu-without-avx-512 # Optional + +cargo run --release -p aquatic_http_load_test -- --help +``` + +## Details + +[BEP 003]: https://www.bittorrent.org/beps/bep_0003.html +[BEP 007]: https://www.bittorrent.org/beps/bep_0007.html +[BEP 023]: https://www.bittorrent.org/beps/bep_0023.html +[BEP 048]: https://www.bittorrent.org/beps/bep_0048.html + +Implements: + * [BEP 003]: HTTP BitTorrent protocol ([more details](https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol)). Exceptions: + * Doesn't track the number of torrent downloads (0 is always sent) + * Only compact responses are supported + * [BEP 023]: Compact HTTP responses + * [BEP 007]: IPv6 support + * [BEP 048]: HTTP scrape support. Notes: + * Doesn't allow full scrapes, i.e. of all registered info hashes + +`aquatic_http` has not been tested as much as `aquatic_udp`, but likely works +fine in production. + +## Architectural overview + +![Architectural overview of aquatic](../../documents/aquatic-architecture-2024.svg) + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. diff --git a/apps/aquatic/crates/http/src/common.rs b/apps/aquatic/crates/http/src/common.rs new file mode 100644 index 0000000..01b851a --- /dev/null +++ b/apps/aquatic/crates/http/src/common.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use aquatic_common::access_list::AccessListArcSwap; +use aquatic_common::CanonicalSocketAddr; + +pub use aquatic_common::ValidUntil; + +use aquatic_http_protocol::{ + request::{AnnounceRequest, ScrapeRequest}, + response::{AnnounceResponse, ScrapeResponse}, +}; +use glommio::channels::shared_channel::SharedSender; +use slotmap::new_key_type; + +#[allow(dead_code)] +#[derive(Copy, Clone, Debug)] +pub struct ConsumerId(pub usize); + +new_key_type! { + pub struct ConnectionId; +} + +#[derive(Debug)] +pub enum ChannelRequest { + Announce { + request: AnnounceRequest, + peer_addr: CanonicalSocketAddr, + response_sender: SharedSender, + }, + Scrape { + request: ScrapeRequest, + peer_addr: CanonicalSocketAddr, + response_sender: SharedSender, + }, +} + +#[derive(Default, Clone)] +pub struct State { + pub access_list: Arc, +} diff --git a/apps/aquatic/crates/http/src/config.rs b/apps/aquatic/crates/http/src/config.rs new file mode 100644 index 0000000..cad61e3 --- /dev/null +++ b/apps/aquatic/crates/http/src/config.rs @@ -0,0 +1,231 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + path::PathBuf, +}; + +use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; +use aquatic_toml_config::TomlConfig; +use serde::{Deserialize, Serialize}; + +use aquatic_common::cli::LogLevel; + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, TomlConfig, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum ReverseProxyPeerIpHeaderFormat { + #[default] + LastAddress, +} + +/// aquatic_http configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + /// Number of socket worker. One per physical core is recommended. + /// + /// Socket workers receive requests from the socket, parse them and send + /// them on to the swarm workers. They then receive responses from the + /// swarm workers, encode them and send them back over the socket. + pub socket_workers: usize, + /// Number of swarm workers. One is enough in almost all cases + /// + /// Swarm workers receive a number of requests from socket workers, + /// generate responses and send them back to the socket workers. + pub swarm_workers: usize, + pub log_level: LogLevel, + pub network: NetworkConfig, + pub protocol: ProtocolConfig, + pub cleaning: CleaningConfig, + pub privileges: PrivilegeConfig, + /// Access list configuration + /// + /// The file is read on start and when the program receives `SIGUSR1`. If + /// initial parsing fails, the program exits. Later failures result in in + /// emitting of an error-level log message, while successful updates of the + /// access list result in emitting of an info-level log message. + pub access_list: AccessListConfig, + #[cfg(feature = "metrics")] + pub metrics: MetricsConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + socket_workers: 1, + swarm_workers: 1, + log_level: LogLevel::default(), + network: NetworkConfig::default(), + protocol: ProtocolConfig::default(), + cleaning: CleaningConfig::default(), + privileges: PrivilegeConfig::default(), + access_list: AccessListConfig::default(), + #[cfg(feature = "metrics")] + metrics: Default::default(), + } + } +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct NetworkConfig { + /// Use IPv4 + pub use_ipv4: bool, + /// Use IPv6 + pub use_ipv6: bool, + /// IPv4 address and port + /// + /// Examples: + /// - Use 0.0.0.0:3000 to bind to all interfaces on port 3000 + /// - Use 127.0.0.1:3000 to bind to the loopback interface (localhost) on + /// port 3000 + pub address_ipv4: SocketAddrV4, + /// IPv6 address and port + /// + /// Examples: + /// - Use [::]:3000 to bind to all interfaces on port 3000 + /// - Use [::1]:3000 to bind to the loopback interface (localhost) on + /// port 3000 + pub address_ipv6: SocketAddrV6, + /// Maximum number of pending TCP connections + pub tcp_backlog: i32, + /// Enable TLS + /// + /// The TLS files are read on start and when the program receives `SIGUSR1`. + /// If initial parsing fails, the program exits. Later failures result in + /// in emitting of an error-level log message, while successful updates + /// result in emitting of an info-level log message. Updates only affect + /// new connections. + pub enable_tls: bool, + /// Path to TLS certificate (DER-encoded X.509) + pub tls_certificate_path: PathBuf, + /// Path to TLS private key (DER-encoded ASN.1 in PKCS#8 or PKCS#1 format) + pub tls_private_key_path: PathBuf, + /// Keep connections alive after sending a response + pub keep_alive: bool, + /// Does tracker run behind reverse proxy? + /// + /// MUST be set to false if not running behind reverse proxy. + /// + /// If set to true, make sure that reverse_proxy_ip_header_name and + /// reverse_proxy_ip_header_format are set to match your reverse proxy + /// setup. + /// + /// More info on what can go wrong when running behind reverse proxies: + /// https://adam-p.ca/blog/2022/03/x-forwarded-for/ + pub runs_behind_reverse_proxy: bool, + /// Name of header set by reverse proxy to indicate peer ip + pub reverse_proxy_ip_header_name: String, + /// How to extract peer IP from header field + /// + /// Options: + /// - last_address: use the last address in the last instance of the + /// header. Works with typical multi-IP setups (e.g., "X-Forwarded-For") + /// as well as for single-IP setups (e.g., nginx "X-Real-IP") + pub reverse_proxy_ip_header_format: ReverseProxyPeerIpHeaderFormat, + /// Set flag on IPv6 socket to only accept IPv6 traffic. + /// + /// This should typically be set to true unless your OS does not support + /// double-stack sockets (that is, sockets that receive both IPv4 and IPv6 + /// packets). + pub set_only_ipv6: bool, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + use_ipv4: true, + use_ipv6: true, + address_ipv4: SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 3000), + address_ipv6: SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 3000, 0, 0), + enable_tls: false, + tls_certificate_path: "".into(), + tls_private_key_path: "".into(), + tcp_backlog: 1024, + keep_alive: true, + runs_behind_reverse_proxy: false, + reverse_proxy_ip_header_name: "X-Forwarded-For".into(), + reverse_proxy_ip_header_format: Default::default(), + set_only_ipv6: true, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct ProtocolConfig { + /// Maximum number of torrents to accept in scrape request + pub max_scrape_torrents: usize, + /// Maximum number of requested peers to accept in announce request + pub max_peers: usize, + /// Ask peers to announce this often (seconds) + pub peer_announce_interval: usize, +} + +impl Default for ProtocolConfig { + fn default() -> Self { + Self { + max_scrape_torrents: 100, + max_peers: 50, + peer_announce_interval: 120, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CleaningConfig { + /// Clean peers this often (seconds) + pub torrent_cleaning_interval: u64, + /// Clean connections this often (seconds) + pub connection_cleaning_interval: u64, + /// Remove peers that have not announced for this long (seconds) + pub max_peer_age: u32, + /// Remove connections that haven't seen valid requests for this long (seconds) + pub max_connection_idle: u32, +} + +impl Default for CleaningConfig { + fn default() -> Self { + Self { + torrent_cleaning_interval: 30, + connection_cleaning_interval: 60, + max_peer_age: 1800, + max_connection_idle: 180, + } + } +} + +#[cfg(feature = "metrics")] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MetricsConfig { + /// Run a prometheus endpoint + pub run_prometheus_endpoint: bool, + /// Address to run prometheus endpoint on + pub prometheus_endpoint_address: SocketAddr, + /// Update metrics for torrent count this often (seconds) + pub torrent_count_update_interval: u64, +} + +#[cfg(feature = "metrics")] +impl Default for MetricsConfig { + fn default() -> Self { + Self { + run_prometheus_endpoint: false, + prometheus_endpoint_address: SocketAddr::from(([0, 0, 0, 0], 9000)), + torrent_count_update_interval: 10, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/http/src/lib.rs b/apps/aquatic/crates/http/src/lib.rs new file mode 100644 index 0000000..f376c9c --- /dev/null +++ b/apps/aquatic/crates/http/src/lib.rs @@ -0,0 +1,199 @@ +use anyhow::Context; +use aquatic_common::{ + access_list::update_access_list, privileges::PrivilegeDropper, + rustls_config::create_rustls_config, ServerStartInstant, WorkerType, +}; +use arc_swap::ArcSwap; +use common::State; +use glommio::{channels::channel_mesh::MeshBuilder, prelude::*}; +use signal_hook::{consts::SIGUSR1, iterator::Signals}; +use std::{ + sync::Arc, + thread::{sleep, Builder, JoinHandle}, + time::Duration, +}; + +use crate::config::Config; + +mod common; +pub mod config; +mod workers; + +pub const APP_NAME: &str = "aquatic_http: HTTP BitTorrent tracker"; +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const SHARED_CHANNEL_SIZE: usize = 1024; + +pub fn run(config: Config) -> ::anyhow::Result<()> { + let mut signals = Signals::new([SIGUSR1])?; + + if !(config.network.use_ipv4 || config.network.use_ipv6) { + return Result::Err(anyhow::anyhow!( + "Both use_ipv4 and use_ipv6 can not be set to false" + )); + } + + let state = State::default(); + + update_access_list(&config.access_list, &state.access_list)?; + + let request_mesh_builder = MeshBuilder::partial( + config.socket_workers + config.swarm_workers, + SHARED_CHANNEL_SIZE, + ); + + let num_sockets_per_worker = + if config.network.use_ipv4 { 1 } else { 0 } + if config.network.use_ipv6 { 1 } else { 0 }; + + let priv_dropper = PrivilegeDropper::new( + config.privileges.clone(), + config.socket_workers * num_sockets_per_worker, + ); + + let opt_tls_config = if config.network.enable_tls { + Some(Arc::new(ArcSwap::from_pointee(create_rustls_config( + &config.network.tls_certificate_path, + &config.network.tls_private_key_path, + )?))) + } else { + None + }; + + let server_start_instant = ServerStartInstant::new(); + + let mut join_handles = Vec::new(); + + for i in 0..(config.socket_workers) { + let config = config.clone(); + let state = state.clone(); + let opt_tls_config = opt_tls_config.clone(); + let request_mesh_builder = request_mesh_builder.clone(); + + let mut priv_droppers = Vec::new(); + + for _ in 0..num_sockets_per_worker { + priv_droppers.push(priv_dropper.clone()); + } + + let handle = Builder::new() + .name(format!("socket-{:02}", i + 1)) + .spawn(move || { + LocalExecutorBuilder::default() + .make() + .map_err(|err| anyhow::anyhow!("Spawning executor failed: {:#}", err))? + .run(workers::socket::run_socket_worker( + config, + state, + opt_tls_config, + request_mesh_builder, + priv_droppers, + server_start_instant, + i, + )) + }) + .context("spawn socket worker")?; + + join_handles.push((WorkerType::Socket(i), handle)); + } + + for i in 0..(config.swarm_workers) { + let config = config.clone(); + let state = state.clone(); + let request_mesh_builder = request_mesh_builder.clone(); + + let handle = Builder::new() + .name(format!("swarm-{:02}", i + 1)) + .spawn(move || { + LocalExecutorBuilder::default() + .make() + .map_err(|err| anyhow::anyhow!("Spawning executor failed: {:#}", err))? + .run(workers::swarm::run_swarm_worker( + config, + state, + request_mesh_builder, + server_start_instant, + i, + )) + }) + .context("spawn swarm worker")?; + + join_handles.push((WorkerType::Swarm(i), handle)); + } + + #[cfg(feature = "prometheus")] + if config.metrics.run_prometheus_endpoint { + let idle_timeout = config + .cleaning + .connection_cleaning_interval + .max(config.cleaning.torrent_cleaning_interval) + .max(config.metrics.torrent_count_update_interval) + * 2; + + let handle = aquatic_common::spawn_prometheus_endpoint( + config.metrics.prometheus_endpoint_address, + Some(Duration::from_secs(idle_timeout)), + Some(metrics_util::MetricKindMask::GAUGE), + )?; + + join_handles.push((WorkerType::Prometheus, handle)); + } + + // Spawn signal handler thread + { + let handle: JoinHandle> = Builder::new() + .name("signals".into()) + .spawn(move || { + for signal in &mut signals { + match signal { + SIGUSR1 => { + let _ = update_access_list(&config.access_list, &state.access_list); + + if let Some(tls_config) = opt_tls_config.as_ref() { + match create_rustls_config( + &config.network.tls_certificate_path, + &config.network.tls_private_key_path, + ) { + Ok(config) => { + tls_config.store(Arc::new(config)); + + ::log::info!("successfully updated tls config"); + } + Err(err) => { + ::log::error!("could not update tls config: {:#}", err) + } + } + } + } + _ => unreachable!(), + } + } + + Ok(()) + }) + .context("spawn signal worker")?; + + join_handles.push((WorkerType::Signals, handle)); + } + + loop { + for (i, (_, handle)) in join_handles.iter().enumerate() { + if handle.is_finished() { + let (worker_type, handle) = join_handles.remove(i); + + match handle.join() { + Ok(Ok(())) => { + return Err(anyhow::anyhow!("{} stopped", worker_type)); + } + Ok(Err(err)) => { + return Err(err.context(format!("{} stopped", worker_type))); + } + Err(_) => { + return Err(anyhow::anyhow!("{} panicked", worker_type)); + } + } + } + } + + sleep(Duration::from_secs(5)); + } +} diff --git a/apps/aquatic/crates/http/src/main.rs b/apps/aquatic/crates/http/src/main.rs new file mode 100644 index 0000000..bbea551 --- /dev/null +++ b/apps/aquatic/crates/http/src/main.rs @@ -0,0 +1,15 @@ +use aquatic_common::cli::run_app_with_cli_and_config; +use aquatic_http::config::Config; + +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() { + run_app_with_cli_and_config::( + aquatic_http::APP_NAME, + aquatic_http::APP_VERSION, + aquatic_http::run, + None, + ) +} diff --git a/apps/aquatic/crates/http/src/workers/mod.rs b/apps/aquatic/crates/http/src/workers/mod.rs new file mode 100644 index 0000000..28ef095 --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/mod.rs @@ -0,0 +1,2 @@ +pub mod socket; +pub mod swarm; diff --git a/apps/aquatic/crates/http/src/workers/socket/connection.rs b/apps/aquatic/crates/http/src/workers/socket/connection.rs new file mode 100644 index 0000000..c277ffc --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/socket/connection.rs @@ -0,0 +1,466 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::rc::Rc; +use std::sync::Arc; + +use anyhow::Context; +use aquatic_common::access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache}; +use aquatic_common::rustls_config::RustlsConfig; +use aquatic_common::{CanonicalSocketAddr, ServerStartInstant}; +use aquatic_http_protocol::common::InfoHash; +use aquatic_http_protocol::request::{Request, ScrapeRequest}; +use aquatic_http_protocol::response::{ + FailureResponse, Response, ScrapeResponse, ScrapeStatistics, +}; +use arc_swap::ArcSwap; +use futures::stream::FuturesUnordered; +use futures_lite::{AsyncReadExt, AsyncWriteExt, StreamExt}; +use futures_rustls::TlsAcceptor; +use glommio::channels::channel_mesh::Senders; +use glommio::channels::shared_channel::{self, SharedReceiver}; +use glommio::net::TcpStream; +use once_cell::sync::Lazy; + +use crate::common::*; +use crate::config::Config; + +#[cfg(feature = "metrics")] +use super::peer_addr_to_ip_version_str; +use super::request::{parse_request, RequestParseError}; + +const REQUEST_BUFFER_SIZE: usize = 2048; +const RESPONSE_BUFFER_SIZE: usize = 4096; + +const RESPONSE_HEADER_A: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: "; +const RESPONSE_HEADER_B: &[u8] = b" "; +const RESPONSE_HEADER_C: &[u8] = b"\r\n\r\n"; + +static RESPONSE_HEADER: Lazy> = + Lazy::new(|| [RESPONSE_HEADER_A, RESPONSE_HEADER_B, RESPONSE_HEADER_C].concat()); + +struct PendingScrapeResponse { + pending_worker_responses: usize, + stats: BTreeMap, +} + +#[derive(Debug, thiserror::Error)] +pub enum ConnectionError { + #[error("inactive")] + Inactive, + #[error("socket peer addr extraction failed")] + NoSocketPeerAddr(String), + #[error("request buffer full")] + RequestBufferFull, + #[error("response buffer full")] + ResponseBufferFull, + #[error("response buffer write error: {0}")] + ResponseBufferWrite(::std::io::Error), + #[error("peer closed")] + PeerClosed, + #[error("response sender closed")] + ResponseSenderClosed, + #[error("scrape channel error: {0}")] + ScrapeChannelError(&'static str), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +#[allow(clippy::too_many_arguments)] +pub(super) async fn run_connection( + config: Rc, + access_list: Arc, + request_senders: Rc>, + server_start_instant: ServerStartInstant, + opt_tls_config: Option>>, + valid_until: Rc>, + stream: TcpStream, + worker_index: usize, +) -> Result<(), ConnectionError> { + let access_list_cache = create_access_list_cache(&access_list); + let request_buffer = Box::new([0u8; REQUEST_BUFFER_SIZE]); + + let mut response_buffer = Box::new([0; RESPONSE_BUFFER_SIZE]); + + response_buffer[..RESPONSE_HEADER.len()].copy_from_slice(&RESPONSE_HEADER); + + let remote_addr = stream + .peer_addr() + .map_err(|err| ConnectionError::NoSocketPeerAddr(err.to_string()))?; + + let opt_peer_addr = if config.network.runs_behind_reverse_proxy { + None + } else { + Some(CanonicalSocketAddr::new(remote_addr)) + }; + + let peer_port = remote_addr.port(); + + if let Some(tls_config) = opt_tls_config { + let tls_acceptor: TlsAcceptor = tls_config.load_full().into(); + let stream = tls_acceptor + .accept(stream) + .await + .with_context(|| "tls accept")?; + + let mut conn = Connection { + config, + access_list_cache, + request_senders, + valid_until, + server_start_instant, + peer_port, + request_buffer, + request_buffer_position: 0, + response_buffer, + stream, + worker_index_string: worker_index.to_string(), + }; + + conn.run(opt_peer_addr).await + } else { + let mut conn = Connection { + config, + access_list_cache, + request_senders, + valid_until, + server_start_instant, + peer_port, + request_buffer, + request_buffer_position: 0, + response_buffer, + stream, + worker_index_string: worker_index.to_string(), + }; + + conn.run(opt_peer_addr).await + } +} + +struct Connection { + config: Rc, + access_list_cache: AccessListCache, + request_senders: Rc>, + valid_until: Rc>, + server_start_instant: ServerStartInstant, + peer_port: u16, + request_buffer: Box<[u8; REQUEST_BUFFER_SIZE]>, + request_buffer_position: usize, + response_buffer: Box<[u8; RESPONSE_BUFFER_SIZE]>, + stream: S, + worker_index_string: String, +} + +impl Connection +where + S: futures::AsyncRead + futures::AsyncWrite + Unpin + 'static, +{ + async fn run( + &mut self, + // Set unless running behind reverse proxy + opt_stable_peer_addr: Option, + ) -> Result<(), ConnectionError> { + loop { + let (request, opt_peer_addr) = self.read_request().await?; + + let peer_addr = opt_stable_peer_addr + .or(opt_peer_addr) + .ok_or(anyhow::anyhow!("Could not extract peer addr"))?; + + let response = self.handle_request(request, peer_addr).await?; + + self.write_response(&response, peer_addr).await?; + + if !self.config.network.keep_alive { + break; + } + } + + Ok(()) + } + + async fn read_request( + &mut self, + ) -> Result<(Request, Option), ConnectionError> { + self.request_buffer_position = 0; + + loop { + if self.request_buffer_position == self.request_buffer.len() { + return Err(ConnectionError::RequestBufferFull); + } + + let bytes_read = self + .stream + .read(&mut self.request_buffer[self.request_buffer_position..]) + .await + .with_context(|| "read")?; + + if bytes_read == 0 { + return Err(ConnectionError::PeerClosed); + } + + self.request_buffer_position += bytes_read; + + let buffer_slice = &self.request_buffer[..self.request_buffer_position]; + + match parse_request(&self.config, buffer_slice) { + Ok((request, opt_peer_ip)) => { + let opt_peer_addr = if self.config.network.runs_behind_reverse_proxy { + let peer_ip = opt_peer_ip + .expect("logic error: peer ip must have been extracted at this point"); + + Some(CanonicalSocketAddr::new(SocketAddr::new( + peer_ip, + self.peer_port, + ))) + } else { + None + }; + + return Ok((request, opt_peer_addr)); + } + Err(RequestParseError::MoreDataNeeded) => continue, + Err(RequestParseError::RequiredPeerIpHeaderMissing(err)) => { + panic!("Tracker configured as running behind reverse proxy, but no corresponding IP header set in request. Please check your reverse proxy setup as well as your aquatic configuration. Error: {:#}", err); + } + Err(RequestParseError::Other(err)) => { + ::log::debug!("Failed parsing request: {:#}", err); + } + } + } + } + + /// Take a request and: + /// - Update connection ValidUntil + /// - Return error response if request is not allowed + /// - If it is an announce request, send it to swarm workers an await a + /// response + /// - If it is a scrape requests, split it up, pass on the parts to + /// relevant swarm workers and await a response + async fn handle_request( + &mut self, + request: Request, + peer_addr: CanonicalSocketAddr, + ) -> Result { + *self.valid_until.borrow_mut() = ValidUntil::new( + self.server_start_instant, + self.config.cleaning.max_connection_idle, + ); + + match request { + Request::Announce(request) => { + #[cfg(feature = "metrics")] + ::metrics::counter!( + "aquatic_requests_total", + "type" => "announce", + "ip_version" => peer_addr_to_ip_version_str(&peer_addr), + "worker_index" => self.worker_index_string.clone(), + ) + .increment(1); + + let info_hash = request.info_hash; + + if self + .access_list_cache + .load() + .allows(self.config.access_list.mode, &info_hash.0) + { + let (response_sender, response_receiver) = shared_channel::new_bounded(1); + + let request = ChannelRequest::Announce { + request, + peer_addr, + response_sender, + }; + + let consumer_index = calculate_request_consumer_index(&self.config, info_hash); + + // Only fails when receiver is closed + self.request_senders + .send_to(consumer_index, request) + .await + .unwrap(); + + response_receiver + .connect() + .await + .recv() + .await + .ok_or(ConnectionError::ResponseSenderClosed) + .map(Response::Announce) + } else { + let response = Response::Failure(FailureResponse { + failure_reason: "Info hash not allowed".into(), + }); + + Ok(response) + } + } + Request::Scrape(ScrapeRequest { info_hashes }) => { + #[cfg(feature = "metrics")] + ::metrics::counter!( + "aquatic_requests_total", + "type" => "scrape", + "ip_version" => peer_addr_to_ip_version_str(&peer_addr), + "worker_index" => self.worker_index_string.clone(), + ) + .increment(1); + + let mut info_hashes_by_worker: BTreeMap> = BTreeMap::new(); + + for info_hash in info_hashes.into_iter() { + let info_hashes = info_hashes_by_worker + .entry(calculate_request_consumer_index(&self.config, info_hash)) + .or_default(); + + info_hashes.push(info_hash); + } + + let pending_worker_responses = info_hashes_by_worker.len(); + let mut response_receivers = Vec::with_capacity(pending_worker_responses); + + for (consumer_index, info_hashes) in info_hashes_by_worker { + let (response_sender, response_receiver) = shared_channel::new_bounded(1); + + response_receivers.push(response_receiver); + + let request = ChannelRequest::Scrape { + request: ScrapeRequest { info_hashes }, + peer_addr, + response_sender, + }; + + // Only fails when receiver is closed + self.request_senders + .send_to(consumer_index, request) + .await + .unwrap(); + } + + let pending_scrape_response = PendingScrapeResponse { + pending_worker_responses, + stats: Default::default(), + }; + + self.wait_for_scrape_responses(response_receivers, pending_scrape_response) + .await + } + } + } + + /// Wait for partial scrape responses to arrive, + /// return full response + async fn wait_for_scrape_responses( + &self, + response_receivers: Vec>, + mut pending: PendingScrapeResponse, + ) -> Result { + let mut responses = response_receivers + .into_iter() + .map(|receiver| async { receiver.connect().await.recv().await }) + .collect::>(); + + loop { + let response = responses + .next() + .await + .ok_or_else(|| { + ConnectionError::ScrapeChannelError( + "stream ended before all partial scrape responses received", + ) + })? + .ok_or_else(|| ConnectionError::ScrapeChannelError("sender is closed"))?; + + pending.stats.extend(response.files); + pending.pending_worker_responses -= 1; + + if pending.pending_worker_responses == 0 { + let response = Response::Scrape(ScrapeResponse { + files: pending.stats, + }); + + break Ok(response); + } + } + } + + async fn write_response( + &mut self, + response: &Response, + peer_addr: CanonicalSocketAddr, + ) -> Result<(), ConnectionError> { + // Write body and final newline to response buffer + + let mut position = RESPONSE_HEADER.len(); + + let body_len = response + .write_bytes(&mut &mut self.response_buffer[position..]) + .map_err(ConnectionError::ResponseBufferWrite)?; + + position += body_len; + + if position + 2 > self.response_buffer.len() { + return Err(ConnectionError::ResponseBufferFull); + } + + self.response_buffer[position..position + 2].copy_from_slice(b"\r\n"); + + position += 2; + + let content_len = body_len + 2; + + // Clear content-len header value + + { + let start = RESPONSE_HEADER_A.len(); + let end = start + RESPONSE_HEADER_B.len(); + + self.response_buffer[start..end].copy_from_slice(RESPONSE_HEADER_B); + } + + // Set content-len header value + + { + let mut buf = ::itoa::Buffer::new(); + let content_len_bytes = buf.format(content_len).as_bytes(); + + let start = RESPONSE_HEADER_A.len(); + let end = start + content_len_bytes.len(); + + self.response_buffer[start..end].copy_from_slice(content_len_bytes); + } + + // Write buffer to stream + + self.stream + .write(&self.response_buffer[..position]) + .await + .with_context(|| "write")?; + self.stream.flush().await.with_context(|| "flush")?; + + #[cfg(feature = "metrics")] + { + let response_type = match response { + Response::Announce(_) => "announce", + Response::Scrape(_) => "scrape", + Response::Failure(_) => "error", + }; + + let ip_version_str = peer_addr_to_ip_version_str(&peer_addr); + + ::metrics::counter!( + "aquatic_responses_total", + "type" => response_type, + "ip_version" => ip_version_str, + "worker_index" => self.worker_index_string.clone(), + ) + .increment(1); + } + + Ok(()) + } +} + +fn calculate_request_consumer_index(config: &Config, info_hash: InfoHash) -> usize { + (info_hash.0[0] as usize) % config.swarm_workers +} diff --git a/apps/aquatic/crates/http/src/workers/socket/mod.rs b/apps/aquatic/crates/http/src/workers/socket/mod.rs new file mode 100644 index 0000000..9ac6ac3 --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/socket/mod.rs @@ -0,0 +1,302 @@ +mod connection; +mod request; + +use std::cell::RefCell; +use std::net::SocketAddr; +use std::os::unix::prelude::{FromRawFd, IntoRawFd}; +use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::access_list::AccessList; +use aquatic_common::privileges::PrivilegeDropper; +use aquatic_common::rustls_config::RustlsConfig; +use aquatic_common::{CanonicalSocketAddr, ServerStartInstant}; +use arc_swap::{ArcSwap, ArcSwapAny}; +use futures_lite::future::race; +use futures_lite::StreamExt; +use glommio::channels::channel_mesh::{MeshBuilder, Partial, Role, Senders}; +use glommio::channels::local_channel::{new_bounded, LocalReceiver, LocalSender}; +use glommio::net::{TcpListener, TcpStream}; +use glommio::timer::TimerActionRepeat; +use glommio::{enclose, prelude::*}; +use slotmap::HopSlotMap; + +use crate::common::*; +use crate::config::Config; +use crate::workers::socket::connection::{run_connection, ConnectionError}; + +struct ConnectionHandle { + close_conn_sender: LocalSender<()>, + valid_until: Rc>, +} + +#[allow(clippy::too_many_arguments)] +pub async fn run_socket_worker( + config: Config, + state: State, + opt_tls_config: Option>>, + request_mesh_builder: MeshBuilder, + mut priv_droppers: Vec, + server_start_instant: ServerStartInstant, + worker_index: usize, +) -> anyhow::Result<()> { + let config = Rc::new(config); + + let tcp_listeners = { + let opt_listener_ipv4 = if config.network.use_ipv4 { + let priv_dropper = priv_droppers + .pop() + .ok_or(anyhow::anyhow!("no enough priv droppers"))?; + let socket = + create_tcp_listener(&config, priv_dropper, config.network.address_ipv4.into()) + .context("create tcp listener")?; + + Some(socket) + } else { + None + }; + let opt_listener_ipv6 = if config.network.use_ipv6 { + let priv_dropper = priv_droppers + .pop() + .ok_or(anyhow::anyhow!("no enough priv droppers"))?; + let socket = + create_tcp_listener(&config, priv_dropper, config.network.address_ipv6.into()) + .context("create tcp listener")?; + + Some(socket) + } else { + None + }; + + [opt_listener_ipv4, opt_listener_ipv6] + .into_iter() + .flatten() + .collect::>() + }; + + let (request_senders, _) = request_mesh_builder + .join(Role::Producer) + .await + .map_err(|err| anyhow::anyhow!("join request mesh: {:#}", err))?; + let request_senders = Rc::new(request_senders); + + let connection_handles = Rc::new(RefCell::new(HopSlotMap::with_key())); + + TimerActionRepeat::repeat(enclose!((config, connection_handles) move || { + clean_connections( + config.clone(), + connection_handles.clone(), + server_start_instant, + ) + })); + + let tasks = tcp_listeners + .into_iter() + .map(|tcp_listener| { + let listener_state = ListenerState { + config: config.clone(), + access_list: state.access_list.clone(), + opt_tls_config: opt_tls_config.clone(), + server_start_instant, + connection_handles: connection_handles.clone(), + request_senders: request_senders.clone(), + worker_index, + }; + + spawn_local(listener_state.accept_connections(tcp_listener)) + }) + .collect::>(); + + for task in tasks { + task.await; + } + + Ok(()) +} + +#[derive(Clone)] +struct ListenerState { + config: Rc, + access_list: Arc>>, + opt_tls_config: Option>>, + server_start_instant: ServerStartInstant, + connection_handles: Rc>>, + request_senders: Rc>, + worker_index: usize, +} + +impl ListenerState { + async fn accept_connections(self, listener: TcpListener) { + let mut incoming = listener.incoming(); + + while let Some(stream) = incoming.next().await { + match stream { + Ok(stream) => { + let (close_conn_sender, close_conn_receiver) = new_bounded(1); + + let valid_until = Rc::new(RefCell::new(ValidUntil::new( + self.server_start_instant, + self.config.cleaning.max_connection_idle, + ))); + + let connection_id = + self.connection_handles + .borrow_mut() + .insert(ConnectionHandle { + close_conn_sender, + valid_until: valid_until.clone(), + }); + + spawn_local(self.clone().handle_connection( + close_conn_receiver, + valid_until, + connection_id, + stream, + )) + .detach(); + } + Err(err) => { + ::log::error!("accept connection: {:?}", err); + } + } + } + } + + async fn handle_connection( + self, + close_conn_receiver: LocalReceiver<()>, + valid_until: Rc>, + connection_id: ConnectionId, + stream: TcpStream, + ) { + #[cfg(feature = "metrics")] + let active_connections_gauge = ::metrics::gauge!( + "aquatic_active_connections", + "worker_index" => self.worker_index.to_string(), + ); + + #[cfg(feature = "metrics")] + active_connections_gauge.increment(1.0); + + let f1 = async { + run_connection( + self.config, + self.access_list, + self.request_senders, + self.server_start_instant, + self.opt_tls_config, + valid_until.clone(), + stream, + self.worker_index, + ) + .await + }; + let f2 = async { + close_conn_receiver.recv().await; + + Err(ConnectionError::Inactive) + }; + + let result = race(f1, f2).await; + + #[cfg(feature = "metrics")] + active_connections_gauge.decrement(1.0); + + match result { + Ok(()) => (), + Err( + err @ (ConnectionError::ResponseBufferWrite(_) + | ConnectionError::ResponseBufferFull + | ConnectionError::ScrapeChannelError(_) + | ConnectionError::ResponseSenderClosed), + ) => { + ::log::error!("connection closed: {:#}", err); + } + Err(err @ ConnectionError::RequestBufferFull) => { + ::log::info!("connection closed: {:#}", err); + } + Err(err) => { + ::log::debug!("connection closed: {:#}", err); + } + } + + self.connection_handles.borrow_mut().remove(connection_id); + } +} + +async fn clean_connections( + config: Rc, + connection_slab: Rc>>, + server_start_instant: ServerStartInstant, +) -> Option { + let now = server_start_instant.seconds_elapsed(); + + connection_slab.borrow_mut().retain(|_, handle| { + if handle.valid_until.borrow().valid(now) { + true + } else { + let _ = handle.close_conn_sender.try_send(()); + + false + } + }); + + Some(Duration::from_secs( + config.cleaning.connection_cleaning_interval, + )) +} + +fn create_tcp_listener( + config: &Config, + priv_dropper: PrivilegeDropper, + address: SocketAddr, +) -> anyhow::Result { + let socket = if address.is_ipv4() { + socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )? + } else { + let socket = socket2::Socket::new( + socket2::Domain::IPV6, + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + if config.network.set_only_ipv6 { + socket + .set_only_v6(true) + .with_context(|| "socket: set only ipv6")?; + } + + socket + }; + + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + + socket + .bind(&address.into()) + .with_context(|| format!("socket: bind to {}", address))?; + + socket + .listen(config.network.tcp_backlog) + .with_context(|| format!("socket: listen on {}", address))?; + + priv_dropper.after_socket_creation()?; + + Ok(unsafe { TcpListener::from_raw_fd(socket.into_raw_fd()) }) +} + +#[cfg(feature = "metrics")] +fn peer_addr_to_ip_version_str(addr: &CanonicalSocketAddr) -> &'static str { + if addr.is_ipv4() { + "4" + } else { + "6" + } +} diff --git a/apps/aquatic/crates/http/src/workers/socket/request.rs b/apps/aquatic/crates/http/src/workers/socket/request.rs new file mode 100644 index 0000000..f10950a --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/socket/request.rs @@ -0,0 +1,147 @@ +use std::net::IpAddr; + +use anyhow::Context; +use aquatic_http_protocol::request::Request; + +use crate::config::{Config, ReverseProxyPeerIpHeaderFormat}; + +#[derive(Debug, thiserror::Error)] +pub enum RequestParseError { + #[error("required peer ip header missing or invalid")] + RequiredPeerIpHeaderMissing(anyhow::Error), + #[error("more data needed")] + MoreDataNeeded, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub fn parse_request( + config: &Config, + buffer: &[u8], +) -> Result<(Request, Option), RequestParseError> { + let mut headers = [httparse::EMPTY_HEADER; 16]; + let mut http_request = httparse::Request::new(&mut headers); + + match http_request.parse(buffer).with_context(|| "httparse")? { + httparse::Status::Complete(_) => { + let path = http_request.path.ok_or(anyhow::anyhow!("no http path"))?; + let request = Request::parse_http_get_path(path)?; + + let opt_peer_ip = if config.network.runs_behind_reverse_proxy { + let header_name = &config.network.reverse_proxy_ip_header_name; + let header_format = config.network.reverse_proxy_ip_header_format; + + match parse_forwarded_header(header_name, header_format, http_request.headers) { + Ok(peer_ip) => Some(peer_ip), + Err(err) => { + return Err(RequestParseError::RequiredPeerIpHeaderMissing(err)); + } + } + } else { + None + }; + + Ok((request, opt_peer_ip)) + } + httparse::Status::Partial => Err(RequestParseError::MoreDataNeeded), + } +} + +fn parse_forwarded_header( + header_name: &str, + header_format: ReverseProxyPeerIpHeaderFormat, + headers: &[httparse::Header<'_>], +) -> anyhow::Result { + for header in headers.iter().rev() { + if header.name == header_name { + match header_format { + ReverseProxyPeerIpHeaderFormat::LastAddress => { + return ::std::str::from_utf8(header.value)? + .split(',') + .last() + .ok_or(anyhow::anyhow!("no header value"))? + .trim() + .parse::() + .with_context(|| "parse ip"); + } + } + } + } + + Err(anyhow::anyhow!("header not present")) +} + +#[cfg(test)] +mod tests { + use super::*; + + const REQUEST_START: &str = "GET /announce?info_hash=%04%0bkV%3f%5cr%14%a6%b7%98%adC%c3%c9.%40%24%00%b9&peer_id=-ABC940-5ert69muw5t8&port=12345&uploaded=1&downloaded=2&left=3&numwant=0&key=4ab4b877&compact=1&supportcrypto=1&event=started HTTP/1.1\r\nHost: example.com\r\n"; + + #[test] + fn test_parse_peer_ip_header_multiple() { + let mut config = Config::default(); + + config.network.runs_behind_reverse_proxy = true; + config.network.reverse_proxy_ip_header_name = "X-Forwarded-For".into(); + config.network.reverse_proxy_ip_header_format = ReverseProxyPeerIpHeaderFormat::LastAddress; + + let mut request = REQUEST_START.to_string(); + + request.push_str("X-Forwarded-For: 200.0.0.1\r\n"); + request.push_str("X-Forwarded-For: 1.2.3.4, 5.6.7.8,9.10.11.12\r\n"); + request.push_str("\r\n"); + + let expected_ip = IpAddr::from([9, 10, 11, 12]); + + assert_eq!( + parse_request(&config, request.as_bytes()) + .unwrap() + .1 + .unwrap(), + expected_ip + ) + } + + #[test] + fn test_parse_peer_ip_header_single() { + let mut config = Config::default(); + + config.network.runs_behind_reverse_proxy = true; + config.network.reverse_proxy_ip_header_name = "X-Forwarded-For".into(); + config.network.reverse_proxy_ip_header_format = ReverseProxyPeerIpHeaderFormat::LastAddress; + + let mut request = REQUEST_START.to_string(); + + request.push_str("X-Forwarded-For: 1.2.3.4, 5.6.7.8,9.10.11.12\r\n"); + request.push_str("X-Forwarded-For: 200.0.0.1\r\n"); + request.push_str("\r\n"); + + let expected_ip = IpAddr::from([200, 0, 0, 1]); + + assert_eq!( + parse_request(&config, request.as_bytes()) + .unwrap() + .1 + .unwrap(), + expected_ip + ) + } + + #[test] + fn test_parse_peer_ip_header_no_header() { + let mut config = Config::default(); + + config.network.runs_behind_reverse_proxy = true; + + let mut request = REQUEST_START.to_string(); + + request.push_str("\r\n"); + + let res = parse_request(&config, request.as_bytes()); + + assert!(matches!( + res, + Err(RequestParseError::RequiredPeerIpHeaderMissing(_)) + )); + } +} diff --git a/apps/aquatic/crates/http/src/workers/swarm/mod.rs b/apps/aquatic/crates/http/src/workers/swarm/mod.rs new file mode 100644 index 0000000..f8d7f34 --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/swarm/mod.rs @@ -0,0 +1,135 @@ +mod storage; + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; + +use futures_lite::{Stream, StreamExt}; +use glommio::channels::channel_mesh::{MeshBuilder, Partial, Role}; +use glommio::timer::TimerActionRepeat; +use glommio::{enclose, prelude::*}; +use rand::prelude::SmallRng; +use rand::SeedableRng; + +use aquatic_common::{ServerStartInstant, ValidUntil}; + +use crate::common::*; +use crate::config::Config; + +use self::storage::TorrentMaps; + +pub async fn run_swarm_worker( + config: Config, + state: State, + request_mesh_builder: MeshBuilder, + server_start_instant: ServerStartInstant, + worker_index: usize, +) -> anyhow::Result<()> { + let (_, mut request_receivers) = request_mesh_builder + .join(Role::Consumer) + .await + .map_err(|err| anyhow::anyhow!("join request mesh: {:#}", err))?; + + let torrents = Rc::new(RefCell::new(TorrentMaps::new(worker_index))); + let access_list = state.access_list; + + // Periodically clean torrents + TimerActionRepeat::repeat(enclose!((config, torrents, access_list) move || { + enclose!((config, torrents, access_list) move || async move { + torrents.borrow_mut().clean(&config, &access_list, server_start_instant); + + Some(Duration::from_secs(config.cleaning.torrent_cleaning_interval)) + })() + })); + + let max_peer_age = config.cleaning.max_peer_age; + let peer_valid_until = Rc::new(RefCell::new(ValidUntil::new( + server_start_instant, + max_peer_age, + ))); + + // Periodically update peer_valid_until + TimerActionRepeat::repeat(enclose!((peer_valid_until) move || { + enclose!((peer_valid_until) move || async move { + *peer_valid_until.borrow_mut() = ValidUntil::new(server_start_instant, max_peer_age); + + Some(Duration::from_secs(1)) + })() + })); + + // Periodically update torrent count metrics + #[cfg(feature = "metrics")] + TimerActionRepeat::repeat(enclose!((config, torrents) move || { + enclose!((config, torrents) move || async move { + torrents.borrow_mut().update_torrent_metrics(); + + Some(Duration::from_secs(config.metrics.torrent_count_update_interval)) + })() + })); + + let mut handles = Vec::new(); + + for (_, receiver) in request_receivers.streams() { + let handle = spawn_local(handle_request_stream( + config.clone(), + torrents.clone(), + peer_valid_until.clone(), + receiver, + )) + .detach(); + + handles.push(handle); + } + + for handle in handles { + handle.await; + } + + Ok(()) +} + +async fn handle_request_stream( + config: Config, + torrents: Rc>, + peer_valid_until: Rc>, + mut stream: S, +) where + S: Stream + ::std::marker::Unpin, +{ + let mut rng = SmallRng::from_entropy(); + + while let Some(channel_request) = stream.next().await { + match channel_request { + ChannelRequest::Announce { + request, + peer_addr, + response_sender, + } => { + let response = torrents.borrow_mut().handle_announce_request( + &config, + &mut rng, + peer_valid_until.borrow().to_owned(), + peer_addr, + request, + ); + + if let Err(err) = response_sender.connect().await.send(response).await { + ::log::error!("swarm worker could not send announce response: {:#}", err); + } + } + ChannelRequest::Scrape { + request, + peer_addr, + response_sender, + } => { + let response = torrents + .borrow_mut() + .handle_scrape_request(&config, peer_addr, request); + + if let Err(err) = response_sender.connect().await.send(response).await { + ::log::error!("swarm worker could not send scrape response: {:#}", err); + } + } + }; + } +} diff --git a/apps/aquatic/crates/http/src/workers/swarm/storage.rs b/apps/aquatic/crates/http/src/workers/swarm/storage.rs new file mode 100644 index 0000000..bc1246b --- /dev/null +++ b/apps/aquatic/crates/http/src/workers/swarm/storage.rs @@ -0,0 +1,543 @@ +use std::collections::BTreeMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::Arc; + +use arrayvec::ArrayVec; +use rand::Rng; + +use aquatic_common::access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache}; +use aquatic_common::{ + CanonicalSocketAddr, IndexMap, SecondsSinceServerStart, ServerStartInstant, ValidUntil, +}; +use aquatic_http_protocol::common::*; +use aquatic_http_protocol::request::*; +use aquatic_http_protocol::response::ResponsePeer; +use aquatic_http_protocol::response::*; + +use crate::config::Config; + +const SMALL_PEER_MAP_CAPACITY: usize = 4; + +pub trait Ip: ::std::fmt::Debug + Copy + Eq + ::std::hash::Hash {} + +impl Ip for Ipv4Addr {} +impl Ip for Ipv6Addr {} + +pub struct TorrentMaps { + pub ipv4: TorrentMap, + pub ipv6: TorrentMap, +} + +impl TorrentMaps { + pub fn new(worker_index: usize) -> Self { + Self { + ipv4: TorrentMap::new(worker_index, true), + ipv6: TorrentMap::new(worker_index, false), + } + } + + pub fn handle_announce_request( + &mut self, + config: &Config, + rng: &mut impl Rng, + valid_until: ValidUntil, + peer_addr: CanonicalSocketAddr, + request: AnnounceRequest, + ) -> AnnounceResponse { + match peer_addr.get().ip() { + IpAddr::V4(peer_ip_address) => { + let (seeders, leechers, response_peers) = + self.ipv4.upsert_peer_and_get_response_peers( + config, + rng, + valid_until, + peer_ip_address, + request, + ); + + AnnounceResponse { + complete: seeders, + incomplete: leechers, + announce_interval: config.protocol.peer_announce_interval, + peers: ResponsePeerListV4(response_peers), + peers6: ResponsePeerListV6(vec![]), + warning_message: None, + } + } + IpAddr::V6(peer_ip_address) => { + let (seeders, leechers, response_peers) = + self.ipv6.upsert_peer_and_get_response_peers( + config, + rng, + valid_until, + peer_ip_address, + request, + ); + + AnnounceResponse { + complete: seeders, + incomplete: leechers, + announce_interval: config.protocol.peer_announce_interval, + peers: ResponsePeerListV4(vec![]), + peers6: ResponsePeerListV6(response_peers), + warning_message: None, + } + } + } + } + + pub fn handle_scrape_request( + &mut self, + config: &Config, + peer_addr: CanonicalSocketAddr, + request: ScrapeRequest, + ) -> ScrapeResponse { + if peer_addr.get().ip().is_ipv4() { + self.ipv4.handle_scrape_request(config, request) + } else { + self.ipv6.handle_scrape_request(config, request) + } + } + + #[cfg(feature = "metrics")] + pub fn update_torrent_metrics(&self) { + self.ipv4.torrent_gauge.set(self.ipv4.torrents.len() as f64); + self.ipv6.torrent_gauge.set(self.ipv6.torrents.len() as f64); + } + + pub fn clean( + &mut self, + config: &Config, + access_list: &Arc, + server_start_instant: ServerStartInstant, + ) { + let mut access_list_cache = create_access_list_cache(access_list); + + let now = server_start_instant.seconds_elapsed(); + + self.ipv4.clean(config, &mut access_list_cache, now); + self.ipv6.clean(config, &mut access_list_cache, now); + } +} + +pub struct TorrentMap { + torrents: IndexMap>, + #[cfg(feature = "metrics")] + peer_gauge: ::metrics::Gauge, + #[cfg(feature = "metrics")] + torrent_gauge: ::metrics::Gauge, +} + +impl TorrentMap { + fn new(worker_index: usize, ipv4: bool) -> Self { + #[cfg(feature = "metrics")] + let peer_gauge = if ipv4 { + ::metrics::gauge!( + "aquatic_peers", + "ip_version" => "4", + "worker_index" => worker_index.to_string(), + ) + } else { + ::metrics::gauge!( + "aquatic_peers", + "ip_version" => "6", + "worker_index" => worker_index.to_string(), + ) + }; + #[cfg(feature = "metrics")] + let torrent_gauge = if ipv4 { + ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => "4", + "worker_index" => worker_index.to_string(), + ) + } else { + ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => "6", + "worker_index" => worker_index.to_string(), + ) + }; + + Self { + torrents: Default::default(), + #[cfg(feature = "metrics")] + peer_gauge, + #[cfg(feature = "metrics")] + torrent_gauge, + } + } + + fn upsert_peer_and_get_response_peers( + &mut self, + config: &Config, + rng: &mut impl Rng, + valid_until: ValidUntil, + peer_ip_address: I, + request: AnnounceRequest, + ) -> (usize, usize, Vec>) { + self.torrents + .entry(request.info_hash) + .or_default() + .upsert_peer_and_get_response_peers( + config, + rng, + request, + peer_ip_address, + valid_until, + #[cfg(feature = "metrics")] + &self.peer_gauge, + ) + } + + fn handle_scrape_request(&mut self, config: &Config, request: ScrapeRequest) -> ScrapeResponse { + let num_to_take = request + .info_hashes + .len() + .min(config.protocol.max_scrape_torrents); + + let mut response = ScrapeResponse { + files: BTreeMap::new(), + }; + + for info_hash in request.info_hashes.into_iter().take(num_to_take) { + let stats = self + .torrents + .get(&info_hash) + .map(|torrent_data| torrent_data.scrape_statistics()) + .unwrap_or(ScrapeStatistics { + complete: 0, + incomplete: 0, + downloaded: 0, + }); + + response.files.insert(info_hash, stats); + } + + response + } + + fn clean( + &mut self, + config: &Config, + access_list_cache: &mut AccessListCache, + now: SecondsSinceServerStart, + ) { + let mut total_num_peers = 0; + + self.torrents.retain(|info_hash, torrent_data| { + if !access_list_cache + .load() + .allows(config.access_list.mode, &info_hash.0) + { + return false; + } + + let num_peers = match torrent_data { + TorrentData::Small(t) => t.clean_and_get_num_peers(now), + TorrentData::Large(t) => t.clean_and_get_num_peers(now), + }; + + total_num_peers += num_peers as u64; + + num_peers > 0 + }); + + self.torrents.shrink_to_fit(); + + #[cfg(feature = "metrics")] + self.peer_gauge.set(total_num_peers as f64); + } +} + +pub enum TorrentData { + Small(SmallPeerMap), + Large(LargePeerMap), +} + +impl TorrentData { + fn upsert_peer_and_get_response_peers( + &mut self, + config: &Config, + rng: &mut impl Rng, + request: AnnounceRequest, + ip_address: I, + valid_until: ValidUntil, + #[cfg(feature = "metrics")] peer_gauge: &::metrics::Gauge, + ) -> (usize, usize, Vec>) { + let max_num_peers_to_take = match request.numwant { + Some(0) | None => config.protocol.max_peers, + Some(numwant) => numwant.min(config.protocol.max_peers), + }; + + let status = PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left); + + let peer_map_key = ResponsePeer { + ip_address, + port: request.port, + }; + + // Create the response before inserting the peer. This means that we + // don't have to filter it out from the response peers, and that the + // reported number of seeders/leechers will not include it + let (response_data, opt_removed_peer) = match self { + Self::Small(peer_map) => { + let opt_removed_peer = peer_map.remove(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + let response_peers = peer_map.extract_response_peers(max_num_peers_to_take); + + // Convert peer map to large variant if it is full and + // announcing peer is not stopped and will therefore be + // inserted + if peer_map.is_full() && status != PeerStatus::Stopped { + *self = Self::Large(peer_map.to_large()); + } + + ((seeders, leechers, response_peers), opt_removed_peer) + } + Self::Large(peer_map) => { + let opt_removed_peer = peer_map.remove_peer(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + let response_peers = peer_map.extract_response_peers(rng, max_num_peers_to_take); + + // Try shrinking the map if announcing peer is stopped and + // will therefore not be inserted + if status == PeerStatus::Stopped { + if let Some(peer_map) = peer_map.try_shrink() { + *self = Self::Small(peer_map); + } + } + + ((seeders, leechers, response_peers), opt_removed_peer) + } + }; + + match status { + PeerStatus::Leeching | PeerStatus::Seeding => { + #[cfg(feature = "metrics")] + if opt_removed_peer.is_none() { + peer_gauge.increment(1.0); + } + + let peer = Peer { + is_seeder: status == PeerStatus::Seeding, + valid_until, + }; + + match self { + Self::Small(peer_map) => peer_map.insert(peer_map_key, peer), + Self::Large(peer_map) => peer_map.insert(peer_map_key, peer), + } + } + PeerStatus::Stopped => + { + #[cfg(feature = "metrics")] + if opt_removed_peer.is_some() { + peer_gauge.decrement(1.0); + } + } + }; + + response_data + } + + fn scrape_statistics(&self) -> ScrapeStatistics { + let (seeders, leechers) = match self { + Self::Small(peer_map) => peer_map.num_seeders_leechers(), + Self::Large(peer_map) => peer_map.num_seeders_leechers(), + }; + + ScrapeStatistics { + complete: seeders, + incomplete: leechers, + downloaded: 0, + } + } +} + +impl Default for TorrentData { + fn default() -> Self { + Self::Small(SmallPeerMap(ArrayVec::default())) + } +} + +/// Store torrents with very few peers without an extra heap allocation +/// +/// On public open trackers, this is likely to be the majority of torrents. +#[derive(Default, Debug)] +pub struct SmallPeerMap(ArrayVec<(ResponsePeer, Peer), SMALL_PEER_MAP_CAPACITY>); + +impl SmallPeerMap { + fn is_full(&self) -> bool { + self.0.is_full() + } + + fn num_seeders_leechers(&self) -> (usize, usize) { + let seeders = self.0.iter().filter(|(_, p)| p.is_seeder).count(); + let leechers = self.0.len() - seeders; + + (seeders, leechers) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + self.0.push((key, peer)); + } + + fn remove(&mut self, key: &ResponsePeer) -> Option { + for (i, (k, _)) in self.0.iter().enumerate() { + if k == key { + return Some(self.0.remove(i).1); + } + } + + None + } + + fn extract_response_peers(&self, max_num_peers_to_take: usize) -> Vec> { + Vec::from_iter(self.0.iter().take(max_num_peers_to_take).map(|(k, _)| *k)) + } + + fn clean_and_get_num_peers(&mut self, now: SecondsSinceServerStart) -> usize { + self.0.retain(|(_, peer)| peer.valid_until.valid(now)); + + self.0.len() + } + + fn to_large(&self) -> LargePeerMap { + let (num_seeders, _) = self.num_seeders_leechers(); + let peers = self.0.iter().copied().collect(); + + LargePeerMap { peers, num_seeders } + } +} + +#[derive(Default)] +pub struct LargePeerMap { + peers: IndexMap, Peer>, + num_seeders: usize, +} + +impl LargePeerMap { + fn num_seeders_leechers(&self) -> (usize, usize) { + (self.num_seeders, self.peers.len() - self.num_seeders) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + if peer.is_seeder { + self.num_seeders += 1; + } + + self.peers.insert(key, peer); + } + + fn remove_peer(&mut self, key: &ResponsePeer) -> Option { + let opt_removed_peer = self.peers.swap_remove(key); + + if let Some(Peer { + is_seeder: true, .. + }) = opt_removed_peer + { + self.num_seeders -= 1; + } + + opt_removed_peer + } + + /// Extract response peers + /// + /// If there are more peers in map than `max_num_peers_to_take`, do a random + /// selection of peers from first and second halves of map in order to avoid + /// returning too homogeneous peers. + /// + /// Does NOT filter out announcing peer. + pub fn extract_response_peers( + &self, + rng: &mut impl Rng, + max_num_peers_to_take: usize, + ) -> Vec> { + if self.peers.len() <= max_num_peers_to_take { + self.peers.keys().copied().collect() + } else { + let middle_index = self.peers.len() / 2; + let num_to_take_per_half = max_num_peers_to_take / 2; + + let offset_half_one = { + let from = 0; + let to = usize::max(1, middle_index - num_to_take_per_half); + + rng.gen_range(from..to) + }; + let offset_half_two = { + let from = middle_index; + let to = usize::max(middle_index + 1, self.peers.len() - num_to_take_per_half); + + rng.gen_range(from..to) + }; + + let end_half_one = offset_half_one + num_to_take_per_half; + let end_half_two = offset_half_two + num_to_take_per_half; + + let mut peers = Vec::with_capacity(max_num_peers_to_take); + + if let Some(slice) = self.peers.get_range(offset_half_one..end_half_one) { + peers.extend(slice.keys()); + } + if let Some(slice) = self.peers.get_range(offset_half_two..end_half_two) { + peers.extend(slice.keys()); + } + + peers + } + } + + fn clean_and_get_num_peers(&mut self, now: SecondsSinceServerStart) -> usize { + self.peers.retain(|_, peer| { + let keep = peer.valid_until.valid(now); + + if (!keep) & peer.is_seeder { + self.num_seeders -= 1; + } + + keep + }); + + self.peers.shrink_to_fit(); + + self.peers.len() + } + + fn try_shrink(&mut self) -> Option> { + (self.peers.len() <= SMALL_PEER_MAP_CAPACITY).then(|| { + SmallPeerMap(ArrayVec::from_iter( + self.peers.iter().map(|(k, v)| (*k, *v)), + )) + }) + } +} + +#[derive(Debug, Clone, Copy)] +struct Peer { + pub valid_until: ValidUntil, + pub is_seeder: bool, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum PeerStatus { + Seeding, + Leeching, + Stopped, +} + +impl PeerStatus { + fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: usize) -> Self { + if let AnnounceEvent::Stopped = event { + Self::Stopped + } else if bytes_left == 0 { + Self::Seeding + } else { + Self::Leeching + } + } +} diff --git a/apps/aquatic/crates/http_load_test/Cargo.toml b/apps/aquatic/crates/http_load_test/Cargo.toml new file mode 100644 index 0000000..a05d999 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "aquatic_http_load_test" +description = "BitTorrent (HTTP over TLS) load tester" +keywords = ["http", "benchmark", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "README.md" + +[[bin]] +name = "aquatic_http_load_test" + +[dependencies] +aquatic_common.workspace = true +aquatic_http_protocol.workspace = true +aquatic_toml_config.workspace = true + +anyhow = "1" +futures = "0.3" +futures-lite = "1" +futures-rustls = "0.26" +hashbrown = "0.15" +glommio = "0.9" +log = "0.4" +mimalloc = { version = "0.1", default-features = false } +rand = { version = "0.8", features = ["small_rng"] } +rand_distr = "0.4" +rustls = { version = "0.23", default-features = false, features = ["logging"] } # TLS 1.2 disabled +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/http_load_test/README.md b/apps/aquatic/crates/http_load_test/README.md new file mode 100644 index 0000000..be0bf61 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/README.md @@ -0,0 +1,55 @@ +# aquatic_http_load_test: HTTP BitTorrent tracker load tester + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +Load tester for HTTP BitTorrent trackers. Requires Linux 5.8 or later. + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_http_load_test +``` + +### Configuring and running + +Generate the configuration file: + +```sh +./target/release/aquatic_http_load_test -p > "load-test-config.toml" +``` + +Make necessary adjustments to the file. + +Make sure locked memory limits are sufficient: + +```sh +ulimit -l 65536 +``` + +First, start the tracker application that you want to test. Then +start the load tester: + +```sh +./target/release/aquatic_http_load_test -c "load-test-config.toml" +``` + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. diff --git a/apps/aquatic/crates/http_load_test/src/common.rs b/apps/aquatic/crates/http_load_test/src/common.rs new file mode 100644 index 0000000..6d44b12 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/src/common.rs @@ -0,0 +1,38 @@ +use std::sync::{atomic::AtomicUsize, Arc}; + +use rand_distr::Gamma; + +pub use aquatic_http_protocol::common::*; +pub use aquatic_http_protocol::request::*; + +#[derive(PartialEq, Eq, Clone)] +pub struct TorrentPeer { + pub info_hash: InfoHash, + pub scrape_hash_indeces: Vec, + pub peer_id: PeerId, + pub port: u16, +} + +#[derive(Default)] +pub struct Statistics { + pub requests: AtomicUsize, + pub response_peers: AtomicUsize, + pub responses_announce: AtomicUsize, + pub responses_scrape: AtomicUsize, + pub responses_failure: AtomicUsize, + pub bytes_sent: AtomicUsize, + pub bytes_received: AtomicUsize, +} + +#[derive(Clone)] +pub struct LoadTestState { + pub info_hashes: Arc>, + pub statistics: Arc, + pub gamma: Arc>, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum RequestType { + Announce, + Scrape, +} diff --git a/apps/aquatic/crates/http_load_test/src/config.rs b/apps/aquatic/crates/http_load_test/src/config.rs new file mode 100644 index 0000000..a372f68 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/src/config.rs @@ -0,0 +1,88 @@ +use std::net::SocketAddr; + +use aquatic_common::cli::LogLevel; +use aquatic_toml_config::TomlConfig; +use serde::Deserialize; + +/// aquatic_http_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + pub server_address: SocketAddr, + pub log_level: LogLevel, + pub num_workers: usize, + /// Maximum number of connections to keep open + pub num_connections: usize, + /// How often to check if num_connections connections are open, and + /// open a new one otherwise. A value of 0 means that connections are + /// opened as quickly as possible, which is useful when the tracker + /// does not keep connections alive. + pub connection_creation_interval_ms: u64, + /// Announce/scrape url suffix. Use `/my_token/` to get `/announce/my_token/` + pub url_suffix: String, + pub duration: usize, + pub keep_alive: bool, + pub enable_tls: bool, + pub torrents: TorrentConfig, +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + server_address: "127.0.0.1:3000".parse().unwrap(), + log_level: LogLevel::Error, + num_workers: 1, + num_connections: 128, + connection_creation_interval_ms: 10, + url_suffix: "".into(), + duration: 0, + keep_alive: true, + enable_tls: true, + torrents: TorrentConfig::default(), + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct TorrentConfig { + pub number_of_torrents: usize, + /// Probability that a generated peer is a seeder + pub peer_seeder_probability: f64, + /// Probability that a generated request is a announce request, as part + /// of sum of the various weight arguments. + pub weight_announce: usize, + /// Probability that a generated request is a scrape request, as part + /// of sum of the various weight arguments. + pub weight_scrape: usize, + /// Peers choose torrents according to this Gamma distribution shape + pub torrent_gamma_shape: f64, + /// Peers choose torrents according to this Gamma distribution scale + pub torrent_gamma_scale: f64, +} + +impl Default for TorrentConfig { + fn default() -> Self { + Self { + number_of_torrents: 10_000, + peer_seeder_probability: 0.25, + weight_announce: 5, + weight_scrape: 0, + torrent_gamma_shape: 0.2, + torrent_gamma_scale: 100.0, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/http_load_test/src/main.rs b/apps/aquatic/crates/http_load_test/src/main.rs new file mode 100644 index 0000000..176fe66 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/src/main.rs @@ -0,0 +1,225 @@ +use std::sync::{atomic::Ordering, Arc}; +use std::thread; +use std::time::{Duration, Instant}; + +use ::glommio::LocalExecutorBuilder; +use rand::prelude::*; +use rand_distr::Gamma; + +mod common; +mod config; +mod network; +mod utils; + +use common::*; +use config::*; +use network::*; + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +/// Multiply bytes during a second with this to get Mbit/s +const MBITS_FACTOR: f64 = 1.0 / ((1024.0 * 1024.0) / 8.0); + +pub fn main() { + aquatic_common::cli::run_app_with_cli_and_config::( + "aquatic_http_load_test: BitTorrent load tester", + env!("CARGO_PKG_VERSION"), + run, + None, + ) +} + +fn run(config: Config) -> ::anyhow::Result<()> { + if config.torrents.weight_announce + config.torrents.weight_scrape == 0 { + panic!("Error: at least one weight must be larger than zero."); + } + + println!("Starting client with config: {:#?}", config); + + let mut info_hashes = Vec::with_capacity(config.torrents.number_of_torrents); + + let mut rng = SmallRng::from_entropy(); + + for _ in 0..config.torrents.number_of_torrents { + info_hashes.push(InfoHash(rng.gen())); + } + + let gamma = Gamma::new( + config.torrents.torrent_gamma_shape, + config.torrents.torrent_gamma_scale, + ) + .unwrap(); + + let state = LoadTestState { + info_hashes: Arc::new(info_hashes), + statistics: Arc::new(Statistics::default()), + gamma: Arc::new(gamma), + }; + + let opt_tls_config = if config.enable_tls { + Some(create_tls_config().unwrap()) + } else { + None + }; + + for _ in 0..config.num_workers { + let config = config.clone(); + let opt_tls_config = opt_tls_config.clone(); + let state = state.clone(); + + LocalExecutorBuilder::default() + .name("load-test") + .spawn(move || async move { + run_socket_thread(config, opt_tls_config, state) + .await + .unwrap(); + }) + .unwrap(); + } + + monitor_statistics(state, &config); + + Ok(()) +} + +fn monitor_statistics(state: LoadTestState, config: &Config) { + let start_time = Instant::now(); + let mut report_avg_response_vec: Vec = Vec::new(); + + let interval = 5; + let interval_f64 = interval as f64; + + loop { + thread::sleep(Duration::from_secs(interval)); + + let statistics = state.statistics.as_ref(); + + let responses_announce = statistics + .responses_announce + .fetch_and(0, Ordering::Relaxed) as f64; + // let response_peers = statistics.response_peers + // .fetch_and(0, Ordering::SeqCst) as f64; + + let requests_per_second = + statistics.requests.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_scrape_per_second = + statistics.responses_scrape.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_failure_per_second = + statistics.responses_failure.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + + let bytes_sent_per_second = + statistics.bytes_sent.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let bytes_received_per_second = + statistics.bytes_received.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + + let responses_announce_per_second = responses_announce / interval_f64; + + let responses_per_second = responses_announce_per_second + + responses_scrape_per_second + + responses_failure_per_second; + + report_avg_response_vec.push(responses_per_second); + + println!(); + println!("Requests out: {:.2}/second", requests_per_second); + println!("Responses in: {:.2}/second", responses_per_second); + println!( + " - Announce responses: {:.2}", + responses_announce_per_second + ); + println!(" - Scrape responses: {:.2}", responses_scrape_per_second); + println!( + " - Failure responses: {:.2}", + responses_failure_per_second + ); + //println!("Peers per announce response: {:.2}", response_peers / responses_announce); + println!( + "Bandwidth out: {:.2}Mbit/s", + bytes_sent_per_second * MBITS_FACTOR + ); + println!( + "Bandwidth in: {:.2}Mbit/s", + bytes_received_per_second * MBITS_FACTOR + ); + + let time_elapsed = start_time.elapsed(); + let duration = Duration::from_secs(config.duration as u64); + + if config.duration != 0 && time_elapsed >= duration { + let report_len = report_avg_response_vec.len() as f64; + let report_sum: f64 = report_avg_response_vec.into_iter().sum(); + let report_avg: f64 = report_sum / report_len; + + println!( + concat!( + "\n# aquatic load test report\n\n", + "Test ran for {} seconds.\n", + "Average responses per second: {:.2}\n\nConfig: {:#?}\n" + ), + time_elapsed.as_secs(), + report_avg, + config + ); + + break; + } + } +} + +#[derive(Debug)] +struct FakeCertificateVerifier; + +impl rustls::client::danger::ServerCertVerifier for FakeCertificateVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::ED25519, + ] + } +} + +fn create_tls_config() -> anyhow::Result> { + let mut config = rustls::ClientConfig::builder() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + + config + .dangerous() + .set_certificate_verifier(Arc::new(FakeCertificateVerifier)); + + Ok(Arc::new(config)) +} diff --git a/apps/aquatic/crates/http_load_test/src/network.rs b/apps/aquatic/crates/http_load_test/src/network.rs new file mode 100644 index 0000000..f739a97 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/src/network.rs @@ -0,0 +1,277 @@ +use std::{ + cell::RefCell, + convert::TryInto, + io::Cursor, + rc::Rc, + sync::{atomic::Ordering, Arc}, + time::Duration, +}; + +use aquatic_http_protocol::response::Response; +use futures_lite::{AsyncReadExt, AsyncWriteExt}; +use futures_rustls::TlsConnector; +use glommio::net::TcpStream; +use glommio::{prelude::*, timer::TimerActionRepeat}; +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{common::LoadTestState, config::Config, utils::create_random_request}; + +pub async fn run_socket_thread( + config: Config, + opt_tls_config: Option>, + load_test_state: LoadTestState, +) -> anyhow::Result<()> { + let config = Rc::new(config); + let num_active_connections = Rc::new(RefCell::new(0usize)); + let rng = Rc::new(RefCell::new(SmallRng::from_entropy())); + + let interval = config.connection_creation_interval_ms; + + if interval == 0 { + loop { + if *num_active_connections.borrow() < config.num_connections { + if let Err(err) = run_connection( + config.clone(), + opt_tls_config.clone(), + load_test_state.clone(), + num_active_connections.clone(), + rng.clone(), + ) + .await + { + ::log::error!("connection creation error: {:?}", err); + } + } + } + } else { + let interval = Duration::from_millis(interval); + + TimerActionRepeat::repeat(move || { + periodically_open_connections( + config.clone(), + interval, + opt_tls_config.clone(), + load_test_state.clone(), + num_active_connections.clone(), + rng.clone(), + ) + }); + } + + futures_lite::future::pending::().await; + + Ok(()) +} + +async fn periodically_open_connections( + config: Rc, + interval: Duration, + opt_tls_config: Option>, + load_test_state: LoadTestState, + num_active_connections: Rc>, + rng: Rc>, +) -> Option { + if *num_active_connections.borrow() < config.num_connections { + spawn_local(async move { + if let Err(err) = run_connection( + config, + opt_tls_config, + load_test_state, + num_active_connections, + rng.clone(), + ) + .await + { + ::log::error!("connection creation error: {:?}", err); + } + }) + .detach(); + } + + Some(interval) +} + +async fn run_connection( + config: Rc, + opt_tls_config: Option>, + load_test_state: LoadTestState, + num_active_connections: Rc>, + rng: Rc>, +) -> anyhow::Result<()> { + let stream = TcpStream::connect(config.server_address) + .await + .map_err(|err| anyhow::anyhow!("connect: {:?}", err))?; + + if let Some(tls_config) = opt_tls_config { + let stream = TlsConnector::from(tls_config) + .connect("example.com".try_into().unwrap(), stream) + .await?; + + let mut connection = Connection { + config, + load_test_state, + rng, + stream, + buffer: Box::new([0; 2048]), + }; + + connection.run(num_active_connections).await?; + } else { + let mut connection = Connection { + config, + load_test_state, + rng, + stream, + buffer: Box::new([0; 2048]), + }; + + connection.run(num_active_connections).await?; + } + + Ok(()) +} + +struct Connection { + config: Rc, + load_test_state: LoadTestState, + rng: Rc>, + stream: S, + buffer: Box<[u8; 2048]>, +} + +impl Connection +where + S: futures::AsyncRead + futures::AsyncWrite + Unpin + 'static, +{ + async fn run(&mut self, num_active_connections: Rc>) -> anyhow::Result<()> { + *num_active_connections.borrow_mut() += 1; + + let result = self.run_connection_loop().await; + + if let Err(err) = &result { + ::log::info!("connection error: {:?}", err); + } + + *num_active_connections.borrow_mut() -= 1; + + result + } + + async fn run_connection_loop(&mut self) -> anyhow::Result<()> { + loop { + self.send_request().await?; + self.read_response().await?; + + if !self.config.keep_alive { + break Ok(()); + } + } + } + + async fn send_request(&mut self) -> anyhow::Result<()> { + let request = create_random_request( + &self.config, + &self.load_test_state, + &mut self.rng.borrow_mut(), + ); + + let mut cursor = Cursor::new(&mut self.buffer[..]); + + request.write(&mut cursor, self.config.url_suffix.as_bytes())?; + + let cursor_position = cursor.position() as usize; + + let bytes_sent = self + .stream + .write(&cursor.into_inner()[..cursor_position]) + .await?; + + self.stream.flush().await?; + + self.load_test_state + .statistics + .bytes_sent + .fetch_add(bytes_sent, Ordering::Relaxed); + + self.load_test_state + .statistics + .requests + .fetch_add(1, Ordering::Relaxed); + + Ok(()) + } + + async fn read_response(&mut self) -> anyhow::Result<()> { + let mut buffer_position = 0; + + loop { + let bytes_read = self + .stream + .read(&mut self.buffer[buffer_position..]) + .await?; + + if bytes_read == 0 { + break; + } + + buffer_position += bytes_read; + + let interesting_bytes = &self.buffer[..buffer_position]; + + let mut opt_body_start_index = None; + + for (i, chunk) in interesting_bytes.windows(4).enumerate() { + if chunk == b"\r\n\r\n" { + opt_body_start_index = Some(i + 4); + + break; + } + } + + if let Some(body_start_index) = opt_body_start_index { + match Response::parse_bytes(&interesting_bytes[body_start_index..]) { + Ok(response) => { + match response { + Response::Announce(_) => { + self.load_test_state + .statistics + .responses_announce + .fetch_add(1, Ordering::Relaxed); + } + Response::Scrape(_) => { + self.load_test_state + .statistics + .responses_scrape + .fetch_add(1, Ordering::Relaxed); + } + Response::Failure(response) => { + self.load_test_state + .statistics + .responses_failure + .fetch_add(1, Ordering::Relaxed); + println!("failure response: reason: {}", response.failure_reason); + } + } + + self.load_test_state + .statistics + .bytes_received + .fetch_add(interesting_bytes.len(), Ordering::Relaxed); + + break; + } + Err(err) => { + ::log::warn!( + "deserialize response error with {} bytes read: {:?}, text: {}", + buffer_position, + err, + interesting_bytes.escape_ascii() + ); + } + } + } + } + + Ok(()) + } +} diff --git a/apps/aquatic/crates/http_load_test/src/utils.rs b/apps/aquatic/crates/http_load_test/src/utils.rs new file mode 100644 index 0000000..d22c2c1 --- /dev/null +++ b/apps/aquatic/crates/http_load_test/src/utils.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use rand::distributions::WeightedIndex; +use rand::prelude::*; +use rand_distr::Gamma; + +use crate::common::*; +use crate::config::*; + +pub fn create_random_request( + config: &Config, + state: &LoadTestState, + rng: &mut SmallRng, +) -> Request { + let weights = [ + config.torrents.weight_announce as u32, + config.torrents.weight_scrape as u32, + ]; + + let items = [RequestType::Announce, RequestType::Scrape]; + + let dist = WeightedIndex::new(weights).expect("random request weighted index"); + + match items[dist.sample(rng)] { + RequestType::Announce => create_announce_request(config, state, rng), + RequestType::Scrape => create_scrape_request(config, state, rng), + } +} + +#[inline] +fn create_announce_request(config: &Config, state: &LoadTestState, rng: &mut impl Rng) -> Request { + let (event, bytes_left) = { + if rng.gen_bool(config.torrents.peer_seeder_probability) { + (AnnounceEvent::Completed, 0) + } else { + (AnnounceEvent::Started, 50) + } + }; + + let info_hash_index = select_info_hash_index(config, state, rng); + + Request::Announce(AnnounceRequest { + info_hash: state.info_hashes[info_hash_index], + peer_id: PeerId(rng.gen()), + bytes_left, + event, + key: None, + numwant: None, + port: rng.gen(), + bytes_uploaded: 0, + bytes_downloaded: 0, + }) +} + +#[inline] +fn create_scrape_request(config: &Config, state: &LoadTestState, rng: &mut impl Rng) -> Request { + let mut scrape_hashes = Vec::with_capacity(5); + + for _ in 0..5 { + let info_hash_index = select_info_hash_index(config, state, rng); + + scrape_hashes.push(state.info_hashes[info_hash_index]); + } + + Request::Scrape(ScrapeRequest { + info_hashes: scrape_hashes, + }) +} + +#[inline] +fn select_info_hash_index(config: &Config, state: &LoadTestState, rng: &mut impl Rng) -> usize { + gamma_usize(rng, &state.gamma, config.torrents.number_of_torrents - 1) +} + +#[inline] +fn gamma_usize(rng: &mut impl Rng, gamma: &Arc>, max: usize) -> usize { + let p: f64 = gamma.sample(rng); + let p = (p.min(101.0f64) - 1.0) / 100.0; + + (p * max as f64) as usize +} diff --git a/apps/aquatic/crates/http_protocol/Cargo.toml b/apps/aquatic/crates/http_protocol/Cargo.toml new file mode 100644 index 0000000..b373072 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "aquatic_http_protocol" +description = "HTTP BitTorrent tracker protocol" +keywords = ["http", "protocol", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[lib] +name = "aquatic_http_protocol" + +[[bench]] +name = "bench_request_from_bytes" +path = "benches/bench_request_from_bytes.rs" +harness = false + +[[bench]] +name = "bench_announce_response_to_bytes" +path = "benches/bench_announce_response_to_bytes.rs" +harness = false + +[dependencies] +anyhow = "1" +compact_str = { version = "0.7", features = ["serde"] } +hex = { version = "0.4", default-features = false } +httparse = "1" +itoa = "1" +log = "0.4" +memchr = "2" +serde = { version = "1", features = ["derive"] } +serde_bencode = "0.2" +urlencoding = "2" + +[dev-dependencies] +bendy = { version = "0.4.0-beta.2", features = ["std", "serde"] } +criterion = "0.4" +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/http_protocol/README.md b/apps/aquatic/crates/http_protocol/README.md new file mode 100644 index 0000000..f618dae --- /dev/null +++ b/apps/aquatic/crates/http_protocol/README.md @@ -0,0 +1,15 @@ +# aquatic_http_protocol: HTTP BitTorrent tracker protocol + +HTTP BitTorrent tracker message parsing and serialization. + +[BEP 003]: https://www.bittorrent.org/beps/bep_0003.html +[BEP 007]: https://www.bittorrent.org/beps/bep_0007.html +[BEP 023]: https://www.bittorrent.org/beps/bep_0023.html +[BEP 048]: https://www.bittorrent.org/beps/bep_0048.html + +Implements: + * [BEP 003]: HTTP BitTorrent protocol ([more details](https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol)). Exceptions: + * Only compact responses are supported + * [BEP 023]: Compact HTTP responses + * [BEP 007]: IPv6 support + * [BEP 048]: HTTP scrape support \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/benches/bench_announce_response_to_bytes.rs b/apps/aquatic/crates/http_protocol/benches/bench_announce_response_to_bytes.rs new file mode 100644 index 0000000..146e168 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/benches/bench_announce_response_to_bytes.rs @@ -0,0 +1,49 @@ +use std::net::Ipv4Addr; +use std::time::Duration; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use aquatic_http_protocol::response::*; + +pub fn bench(c: &mut Criterion) { + let mut peers = Vec::new(); + + for i in 0..100 { + peers.push(ResponsePeer { + ip_address: Ipv4Addr::new(127, 0, 0, i), + port: i as u16, + }) + } + + let announce_response = AnnounceResponse { + announce_interval: 120, + complete: 100, + incomplete: 500, + peers: ResponsePeerListV4(peers), + peers6: ResponsePeerListV6(Vec::new()), + warning_message: None, + }; + + let response = Response::Announce(announce_response); + + let mut buffer = [0u8; 4096]; + let mut buffer = ::std::io::Cursor::new(&mut buffer[..]); + + c.bench_function("announce-response-to-bytes", |b| { + b.iter(|| { + buffer.set_position(0); + + Response::write_bytes(black_box(&response), black_box(&mut buffer)).unwrap(); + }) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(1000) + .measurement_time(Duration::from_secs(180)) + .significance_level(0.01); + targets = bench +} +criterion_main!(benches); diff --git a/apps/aquatic/crates/http_protocol/benches/bench_request_from_bytes.rs b/apps/aquatic/crates/http_protocol/benches/bench_request_from_bytes.rs new file mode 100644 index 0000000..b58c586 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/benches/bench_request_from_bytes.rs @@ -0,0 +1,22 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::time::Duration; + +use aquatic_http_protocol::request::Request; + +static INPUT: &[u8] = b"GET /announce?info_hash=%04%0bkV%3f%5cr%14%a6%b7%98%adC%c3%c9.%40%24%00%b9&peer_id=-TR2940-5ert69muw5t8&port=11000&uploaded=0&downloaded=0&left=0&numwant=0&key=3ab4b977&compact=1&supportcrypto=1&event=stopped HTTP/1.1\r\n\r\n"; + +pub fn bench(c: &mut Criterion) { + c.bench_function("request-from-bytes", |b| { + b.iter(|| Request::parse_bytes(black_box(INPUT))) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(1000) + .measurement_time(Duration::from_secs(180)) + .significance_level(0.01); + targets = bench +} +criterion_main!(benches); diff --git a/apps/aquatic/crates/http_protocol/src/common.rs b/apps/aquatic/crates/http_protocol/src/common.rs new file mode 100644 index 0000000..bfb48b5 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/src/common.rs @@ -0,0 +1,102 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use super::utils::*; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PeerId( + #[serde( + serialize_with = "serialize_20_bytes", + deserialize_with = "deserialize_20_bytes" + )] + pub [u8; 20], +); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InfoHash( + #[serde( + serialize_with = "serialize_20_bytes", + deserialize_with = "deserialize_20_bytes" + )] + pub [u8; 20], +); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnnounceEvent { + Started, + Stopped, + Completed, + Empty, +} + +impl Default for AnnounceEvent { + fn default() -> Self { + Self::Empty + } +} + +impl FromStr for AnnounceEvent { + type Err = String; + + fn from_str(value: &str) -> std::result::Result { + match value { + "started" => Ok(Self::Started), + "stopped" => Ok(Self::Stopped), + "completed" => Ok(Self::Completed), + "empty" => Ok(Self::Empty), + value => Err(format!("Unknown value: {}", value)), + } + } +} + +impl AnnounceEvent { + pub fn as_str(&self) -> Option<&str> { + match self { + Self::Started => Some("started"), + Self::Stopped => Some("stopped"), + Self::Completed => Some("completed"), + Self::Empty => None, + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for InfoHash { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut arr = [b'0'; 20]; + + for byte in arr.iter_mut() { + *byte = u8::arbitrary(g); + } + + Self(arr) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for PeerId { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut arr = [b'0'; 20]; + + for byte in arr.iter_mut() { + *byte = u8::arbitrary(g); + } + + Self(arr) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for AnnounceEvent { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + match (bool::arbitrary(g), bool::arbitrary(g)) { + (false, false) => Self::Started, + (true, false) => Self::Started, + (false, true) => Self::Completed, + (true, true) => Self::Empty, + } + } +} diff --git a/apps/aquatic/crates/http_protocol/src/lib.rs b/apps/aquatic/crates/http_protocol/src/lib.rs new file mode 100644 index 0000000..a660008 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/src/lib.rs @@ -0,0 +1,4 @@ +pub mod common; +pub mod request; +pub mod response; +mod utils; diff --git a/apps/aquatic/crates/http_protocol/src/request.rs b/apps/aquatic/crates/http_protocol/src/request.rs new file mode 100644 index 0000000..a462de7 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/src/request.rs @@ -0,0 +1,450 @@ +use std::io::Write; + +use anyhow::Context; +use compact_str::CompactString; + +use super::common::*; +use super::utils::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AnnounceRequest { + pub info_hash: InfoHash, + pub peer_id: PeerId, + pub port: u16, + pub bytes_uploaded: usize, + pub bytes_downloaded: usize, + pub bytes_left: usize, + pub event: AnnounceEvent, + /// Number of response peers wanted + pub numwant: Option, + pub key: Option, +} + +impl AnnounceRequest { + fn write_bytes(&self, output: &mut W, url_suffix: &[u8]) -> ::std::io::Result<()> { + output.write_all(b"GET /announce")?; + output.write_all(url_suffix)?; + output.write_all(b"?info_hash=")?; + urlencode_20_bytes(self.info_hash.0, output)?; + + output.write_all(b"&peer_id=")?; + urlencode_20_bytes(self.peer_id.0, output)?; + + output.write_all(b"&port=")?; + output.write_all(itoa::Buffer::new().format(self.port).as_bytes())?; + + output.write_all(b"&uploaded=")?; + output.write_all(itoa::Buffer::new().format(self.bytes_uploaded).as_bytes())?; + + output.write_all(b"&downloaded=")?; + output.write_all(itoa::Buffer::new().format(self.bytes_downloaded).as_bytes())?; + + output.write_all(b"&left=")?; + output.write_all(itoa::Buffer::new().format(self.bytes_left).as_bytes())?; + + match self.event { + AnnounceEvent::Started => output.write_all(b"&event=started")?, + AnnounceEvent::Stopped => output.write_all(b"&event=stopped")?, + AnnounceEvent::Completed => output.write_all(b"&event=completed")?, + AnnounceEvent::Empty => (), + }; + + if let Some(numwant) = self.numwant { + output.write_all(b"&numwant=")?; + output.write_all(itoa::Buffer::new().format(numwant).as_bytes())?; + } + + if let Some(ref key) = self.key { + output.write_all(b"&key=")?; + output.write_all(::urlencoding::encode(key.as_str()).as_bytes())?; + } + + // Always ask for compact responses to ease load testing of non-aquatic trackers + output.write_all(b"&compact=1")?; + + output.write_all(b" HTTP/1.1\r\nHost: localhost\r\n\r\n")?; + + Ok(()) + } + + pub fn parse_query_string(query_string: &str) -> anyhow::Result { + // -- Parse key-value pairs + + let mut opt_info_hash = None; + let mut opt_peer_id = None; + let mut opt_port = None; + let mut opt_bytes_left = None; + let mut opt_bytes_uploaded = None; + let mut opt_bytes_downloaded = None; + let mut event = AnnounceEvent::default(); + let mut opt_numwant = None; + let mut opt_key = None; + + let query_string_bytes = query_string.as_bytes(); + + let mut ampersand_iter = ::memchr::memchr_iter(b'&', query_string_bytes); + let mut position = 0usize; + + for equal_sign_index in ::memchr::memchr_iter(b'=', query_string_bytes) { + let segment_end = ampersand_iter.next().unwrap_or(query_string.len()); + + let key = query_string + .get(position..equal_sign_index) + .with_context(|| format!("no key at {}..{}", position, equal_sign_index))?; + let value = query_string + .get(equal_sign_index + 1..segment_end) + .with_context(|| { + format!("no value at {}..{}", equal_sign_index + 1, segment_end) + })?; + + match key { + "info_hash" => { + let value = urldecode_20_bytes(value)?; + + opt_info_hash = Some(InfoHash(value)); + } + "peer_id" => { + let value = urldecode_20_bytes(value)?; + + opt_peer_id = Some(PeerId(value)); + } + "port" => { + opt_port = Some(value.parse::().with_context(|| "parse port")?); + } + "left" => { + opt_bytes_left = Some(value.parse::().with_context(|| "parse left")?); + } + "uploaded" => { + opt_bytes_uploaded = + Some(value.parse::().with_context(|| "parse uploaded")?); + } + "downloaded" => { + opt_bytes_downloaded = + Some(value.parse::().with_context(|| "parse downloaded")?); + } + "event" => { + event = value + .parse::() + .map_err(|err| anyhow::anyhow!("invalid event: {}", err))?; + } + "compact" => { + if value != "1" { + return Err(anyhow::anyhow!("compact set, but not to 1")); + } + } + "numwant" => { + opt_numwant = Some(value.parse::().with_context(|| "parse numwant")?); + } + "key" => { + if value.len() > 100 { + return Err(anyhow::anyhow!("'key' is too long")); + } + opt_key = Some(::urlencoding::decode(value)?.into()); + } + k => { + ::log::debug!("ignored unrecognized key: {}", k) + } + } + + if segment_end == query_string.len() { + break; + } else { + position = segment_end + 1; + } + } + + Ok(AnnounceRequest { + info_hash: opt_info_hash.with_context(|| "no info_hash")?, + peer_id: opt_peer_id.with_context(|| "no peer_id")?, + port: opt_port.with_context(|| "no port")?, + bytes_uploaded: opt_bytes_uploaded.with_context(|| "no uploaded")?, + bytes_downloaded: opt_bytes_downloaded.with_context(|| "no downloaded")?, + bytes_left: opt_bytes_left.with_context(|| "no left")?, + event, + numwant: opt_numwant, + key: opt_key, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScrapeRequest { + pub info_hashes: Vec, +} + +impl ScrapeRequest { + fn write_bytes(&self, output: &mut W, url_suffix: &[u8]) -> ::std::io::Result<()> { + output.write_all(b"GET /scrape")?; + output.write_all(url_suffix)?; + output.write_all(b"?")?; + + let mut first = true; + + for info_hash in self.info_hashes.iter() { + if !first { + output.write_all(b"&")?; + } + + output.write_all(b"info_hash=")?; + urlencode_20_bytes(info_hash.0, output)?; + + first = false; + } + + output.write_all(b" HTTP/1.1\r\nHost: localhost\r\n\r\n")?; + + Ok(()) + } + + pub fn parse_query_string(query_string: &str) -> anyhow::Result { + // -- Parse key-value pairs + + let mut info_hashes = Vec::new(); + + let query_string_bytes = query_string.as_bytes(); + + let mut ampersand_iter = ::memchr::memchr_iter(b'&', query_string_bytes); + let mut position = 0usize; + + for equal_sign_index in ::memchr::memchr_iter(b'=', query_string_bytes) { + let segment_end = ampersand_iter.next().unwrap_or(query_string.len()); + + let key = query_string + .get(position..equal_sign_index) + .with_context(|| format!("no key at {}..{}", position, equal_sign_index))?; + let value = query_string + .get(equal_sign_index + 1..segment_end) + .with_context(|| { + format!("no value at {}..{}", equal_sign_index + 1, segment_end) + })?; + + match key { + "info_hash" => { + let value = urldecode_20_bytes(value)?; + + info_hashes.push(InfoHash(value)); + } + k => { + ::log::debug!("ignored unrecognized key: {}", k) + } + } + + if segment_end == query_string.len() { + break; + } else { + position = segment_end + 1; + } + } + + if info_hashes.is_empty() { + return Err(anyhow::anyhow!("No info hashes sent")); + } + + Ok(ScrapeRequest { info_hashes }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Request { + Announce(AnnounceRequest), + Scrape(ScrapeRequest), +} + +impl Request { + /// Parse Request from HTTP request bytes + pub fn parse_bytes(bytes: &[u8]) -> anyhow::Result> { + let mut headers = [httparse::EMPTY_HEADER; 16]; + let mut http_request = httparse::Request::new(&mut headers); + + match http_request.parse(bytes) { + Ok(httparse::Status::Complete(_)) => { + if let Some(path) = http_request.path { + Self::parse_http_get_path(path).map(Some) + } else { + Err(anyhow::anyhow!("no http path")) + } + } + Ok(httparse::Status::Partial) => Ok(None), + Err(err) => Err(anyhow::Error::from(err)), + } + } + + /// Parse Request from http GET path (`/announce?info_hash=...`) + /// + /// Existing serde-url decode crates were insufficient, so the decision was + /// made to create a custom parser. serde_urlencoded doesn't support multiple + /// values with same key, and serde_qs pulls in lots of dependencies. Both + /// would need preprocessing for the binary format used for info_hash and + /// peer_id. + /// + /// The info hashes and peer id's that are received are url-encoded byte + /// by byte, e.g., %fa for byte 0xfa. However, they need to be parsed as + /// UTF-8 string, meaning that non-ascii bytes are invalid characters. + /// Therefore, these bytes must be converted to their equivalent multi-byte + /// UTF-8 encodings. + pub fn parse_http_get_path(path: &str) -> anyhow::Result { + ::log::debug!("request GET path: {}", path); + + let mut split_parts = path.splitn(2, '?'); + + let location = split_parts.next().with_context(|| "no location")?; + let query_string = split_parts.next().with_context(|| "no query string")?; + + if location == "/announce" { + Ok(Request::Announce(AnnounceRequest::parse_query_string( + query_string, + )?)) + } else if location == "/scrape" { + Ok(Request::Scrape(ScrapeRequest::parse_query_string( + query_string, + )?)) + } else { + Err(anyhow::anyhow!("Path must be /announce or /scrape")) + } + } + + pub fn write(&self, output: &mut W, url_suffix: &[u8]) -> ::std::io::Result<()> { + match self { + Self::Announce(r) => r.write_bytes(output, url_suffix), + Self::Scrape(r) => r.write_bytes(output, url_suffix), + } + } +} + +#[cfg(test)] +mod tests { + use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; + + use super::*; + + static ANNOUNCE_REQUEST_PATH: &str = "/announce?info_hash=%04%0bkV%3f%5cr%14%a6%b7%98%adC%c3%c9.%40%24%00%b9&peer_id=-ABC940-5ert69muw5t8&port=12345&uploaded=1&downloaded=2&left=3&numwant=0&key=4ab4b877&compact=1&supportcrypto=1&event=started"; + static SCRAPE_REQUEST_PATH: &str = + "/scrape?info_hash=%04%0bkV%3f%5cr%14%a6%b7%98%adC%c3%c9.%40%24%00%b9"; + static REFERENCE_INFO_HASH: [u8; 20] = [ + 0x04, 0x0b, b'k', b'V', 0x3f, 0x5c, b'r', 0x14, 0xa6, 0xb7, 0x98, 0xad, b'C', 0xc3, 0xc9, + b'.', 0x40, 0x24, 0x00, 0xb9, + ]; + static REFERENCE_PEER_ID: [u8; 20] = [ + b'-', b'A', b'B', b'C', b'9', b'4', b'0', b'-', b'5', b'e', b'r', b't', b'6', b'9', b'm', + b'u', b'w', b'5', b't', b'8', + ]; + + fn get_reference_announce_request() -> Request { + Request::Announce(AnnounceRequest { + info_hash: InfoHash(REFERENCE_INFO_HASH), + peer_id: PeerId(REFERENCE_PEER_ID), + port: 12345, + bytes_uploaded: 1, + bytes_downloaded: 2, + bytes_left: 3, + event: AnnounceEvent::Started, + numwant: Some(0), + key: Some("4ab4b877".into()), + }) + } + + #[test] + fn test_announce_request_from_bytes() { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(b"GET "); + bytes.extend_from_slice(ANNOUNCE_REQUEST_PATH.as_bytes()); + bytes.extend_from_slice(b" HTTP/1.1\r\n\r\n"); + + let parsed_request = Request::parse_bytes(&bytes[..]).unwrap().unwrap(); + let reference_request = get_reference_announce_request(); + + assert_eq!(parsed_request, reference_request); + } + + #[test] + fn test_scrape_request_from_bytes() { + let mut bytes = Vec::new(); + + bytes.extend_from_slice(b"GET "); + bytes.extend_from_slice(SCRAPE_REQUEST_PATH.as_bytes()); + bytes.extend_from_slice(b" HTTP/1.1\r\n\r\n"); + + let parsed_request = Request::parse_bytes(&bytes[..]).unwrap().unwrap(); + let reference_request = Request::Scrape(ScrapeRequest { + info_hashes: vec![InfoHash(REFERENCE_INFO_HASH)], + }); + + assert_eq!(parsed_request, reference_request); + } + + impl Arbitrary for AnnounceRequest { + fn arbitrary(g: &mut Gen) -> Self { + let key: Option = Arbitrary::arbitrary(g); + + AnnounceRequest { + info_hash: Arbitrary::arbitrary(g), + peer_id: Arbitrary::arbitrary(g), + port: Arbitrary::arbitrary(g), + bytes_uploaded: Arbitrary::arbitrary(g), + bytes_downloaded: Arbitrary::arbitrary(g), + bytes_left: Arbitrary::arbitrary(g), + event: Arbitrary::arbitrary(g), + numwant: Arbitrary::arbitrary(g), + key: key.map(|key| key.into()), + } + } + } + + impl Arbitrary for ScrapeRequest { + fn arbitrary(g: &mut Gen) -> Self { + ScrapeRequest { + info_hashes: Arbitrary::arbitrary(g), + } + } + } + + impl Arbitrary for Request { + fn arbitrary(g: &mut Gen) -> Self { + if Arbitrary::arbitrary(g) { + Self::Announce(Arbitrary::arbitrary(g)) + } else { + Self::Scrape(Arbitrary::arbitrary(g)) + } + } + } + + #[test] + fn quickcheck_serde_identity_request() { + fn prop(request: Request) -> TestResult { + match request { + Request::Announce(AnnounceRequest { + key: Some(ref key), .. + }) => { + if key.len() > 30 { + return TestResult::discard(); + } + } + Request::Scrape(ScrapeRequest { ref info_hashes }) => { + if info_hashes.is_empty() { + return TestResult::discard(); + } + } + _ => {} + } + + let mut bytes = Vec::new(); + + request.write(&mut bytes, &[]).unwrap(); + + let parsed_request = Request::parse_bytes(&bytes[..]).unwrap().unwrap(); + + let success = request == parsed_request; + + if !success { + println!("request: {:?}", request); + println!("parsed request: {:?}", parsed_request); + println!("bytes as str: {}", String::from_utf8_lossy(&bytes)); + } + + TestResult::from_bool(success) + } + + quickcheck(prop as fn(Request) -> TestResult); + } +} diff --git a/apps/aquatic/crates/http_protocol/src/response.rs b/apps/aquatic/crates/http_protocol/src/response.rs new file mode 100644 index 0000000..a3b0d17 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/src/response.rs @@ -0,0 +1,335 @@ +use std::borrow::Cow; +use std::io::Write; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use super::common::*; +use super::utils::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct ResponsePeer { + pub ip_address: I, + pub port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct ResponsePeerListV4( + #[serde( + serialize_with = "serialize_response_peers_ipv4", + deserialize_with = "deserialize_response_peers_ipv4" + )] + pub Vec>, +); + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct ResponsePeerListV6( + #[serde( + serialize_with = "serialize_response_peers_ipv6", + deserialize_with = "deserialize_response_peers_ipv6" + )] + pub Vec>, +); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScrapeStatistics { + pub complete: usize, + pub incomplete: usize, + pub downloaded: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AnnounceResponse { + #[serde(rename = "interval")] + pub announce_interval: usize, + pub complete: usize, + pub incomplete: usize, + #[serde(default)] + pub peers: ResponsePeerListV4, + #[serde(default)] + pub peers6: ResponsePeerListV6, + // Serialize as string if Some, otherwise skip + #[serde( + rename = "warning message", + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_optional_string" + )] + pub warning_message: Option, +} + +impl AnnounceResponse { + pub fn write_bytes(&self, output: &mut W) -> ::std::io::Result { + let mut bytes_written = 0usize; + + bytes_written += output.write(b"d8:completei")?; + bytes_written += output.write(itoa::Buffer::new().format(self.complete).as_bytes())?; + + bytes_written += output.write(b"e10:incompletei")?; + bytes_written += output.write(itoa::Buffer::new().format(self.incomplete).as_bytes())?; + + bytes_written += output.write(b"e8:intervali")?; + bytes_written += output.write( + itoa::Buffer::new() + .format(self.announce_interval) + .as_bytes(), + )?; + + bytes_written += output.write(b"e5:peers")?; + bytes_written += output.write( + itoa::Buffer::new() + .format(self.peers.0.len() * 6) + .as_bytes(), + )?; + bytes_written += output.write(b":")?; + for peer in self.peers.0.iter() { + bytes_written += output.write(&u32::from(peer.ip_address).to_be_bytes())?; + bytes_written += output.write(&peer.port.to_be_bytes())?; + } + + bytes_written += output.write(b"6:peers6")?; + bytes_written += output.write( + itoa::Buffer::new() + .format(self.peers6.0.len() * 18) + .as_bytes(), + )?; + bytes_written += output.write(b":")?; + for peer in self.peers6.0.iter() { + bytes_written += output.write(&u128::from(peer.ip_address).to_be_bytes())?; + bytes_written += output.write(&peer.port.to_be_bytes())?; + } + + if let Some(ref warning_message) = self.warning_message { + let message_bytes = warning_message.as_bytes(); + + bytes_written += output.write(b"15:warning message")?; + bytes_written += + output.write(itoa::Buffer::new().format(message_bytes.len()).as_bytes())?; + bytes_written += output.write(b":")?; + bytes_written += output.write(message_bytes)?; + } + + bytes_written += output.write(b"e")?; + + Ok(bytes_written) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScrapeResponse { + /// BTreeMap instead of HashMap since keys need to be serialized in order + pub files: BTreeMap, +} + +impl ScrapeResponse { + pub fn write_bytes(&self, output: &mut W) -> ::std::io::Result { + let mut bytes_written = 0usize; + + bytes_written += output.write(b"d5:filesd")?; + + for (info_hash, statistics) in self.files.iter() { + bytes_written += output.write(b"20:")?; + bytes_written += output.write(&info_hash.0)?; + bytes_written += output.write(b"d8:completei")?; + bytes_written += + output.write(itoa::Buffer::new().format(statistics.complete).as_bytes())?; + bytes_written += output.write(b"e10:downloadedi0e10:incompletei")?; + bytes_written += + output.write(itoa::Buffer::new().format(statistics.incomplete).as_bytes())?; + bytes_written += output.write(b"ee")?; + } + + bytes_written += output.write(b"ee")?; + + Ok(bytes_written) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FailureResponse { + #[serde(rename = "failure reason")] + pub failure_reason: Cow<'static, str>, +} + +impl FailureResponse { + pub fn new>>(reason: S) -> Self { + Self { + failure_reason: reason.into(), + } + } + + pub fn write_bytes(&self, output: &mut W) -> ::std::io::Result { + let mut bytes_written = 0usize; + + let reason_bytes = self.failure_reason.as_bytes(); + + bytes_written += output.write(b"d14:failure reason")?; + bytes_written += output.write(itoa::Buffer::new().format(reason_bytes.len()).as_bytes())?; + bytes_written += output.write(b":")?; + bytes_written += output.write(reason_bytes)?; + bytes_written += output.write(b"e")?; + + Ok(bytes_written) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Response { + Announce(AnnounceResponse), + Scrape(ScrapeResponse), + Failure(FailureResponse), +} + +impl Response { + pub fn write_bytes(&self, output: &mut W) -> ::std::io::Result { + match self { + Response::Announce(r) => r.write_bytes(output), + Response::Failure(r) => r.write_bytes(output), + Response::Scrape(r) => r.write_bytes(output), + } + } + pub fn parse_bytes(bytes: &[u8]) -> Result { + ::serde_bencode::from_bytes(bytes) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ResponsePeer { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + ip_address: Ipv4Addr::arbitrary(g), + port: u16::arbitrary(g), + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ResponsePeer { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + ip_address: Ipv6Addr::arbitrary(g), + port: u16::arbitrary(g), + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ResponsePeerListV4 { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(Vec::arbitrary(g)) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ResponsePeerListV6 { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(Vec::arbitrary(g)) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ScrapeStatistics { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + complete: usize::arbitrary(g), + incomplete: usize::arbitrary(g), + downloaded: 0, + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for AnnounceResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + announce_interval: usize::arbitrary(g), + complete: usize::arbitrary(g), + incomplete: usize::arbitrary(g), + peers: ResponsePeerListV4::arbitrary(g), + peers6: ResponsePeerListV6::arbitrary(g), + warning_message: quickcheck::Arbitrary::arbitrary(g), + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ScrapeResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + files: BTreeMap::arbitrary(g), + } + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for FailureResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + failure_reason: String::arbitrary(g).into(), + } + } +} + +#[cfg(test)] +mod tests { + use quickcheck_macros::*; + + use super::*; + + #[quickcheck] + fn test_announce_response_to_bytes(response: AnnounceResponse) -> bool { + let reference = bendy::serde::to_bytes(&Response::Announce(response.clone())).unwrap(); + + let mut hand_written = Vec::new(); + + response.write_bytes(&mut hand_written).unwrap(); + + let success = hand_written == reference; + + if !success { + println!("reference: {}", String::from_utf8_lossy(&reference)); + println!("hand_written: {}", String::from_utf8_lossy(&hand_written)); + } + + success + } + + #[quickcheck] + fn test_scrape_response_to_bytes(response: ScrapeResponse) -> bool { + let reference = bendy::serde::to_bytes(&Response::Scrape(response.clone())).unwrap(); + + let mut hand_written = Vec::new(); + + response.write_bytes(&mut hand_written).unwrap(); + + let success = hand_written == reference; + + if !success { + println!("reference: {}", String::from_utf8_lossy(&reference)); + println!("hand_written: {}", String::from_utf8_lossy(&hand_written)); + } + + success + } + + #[quickcheck] + fn test_failure_response_to_bytes(response: FailureResponse) -> bool { + let reference = bendy::serde::to_bytes(&Response::Failure(response.clone())).unwrap(); + + let mut hand_written = Vec::new(); + + response.write_bytes(&mut hand_written).unwrap(); + + let success = hand_written == reference; + + if !success { + println!("reference: {}", String::from_utf8_lossy(&reference)); + println!("hand_written: {}", String::from_utf8_lossy(&hand_written)); + } + + success + } +} diff --git a/apps/aquatic/crates/http_protocol/src/utils.rs b/apps/aquatic/crates/http_protocol/src/utils.rs new file mode 100644 index 0000000..1668acc --- /dev/null +++ b/apps/aquatic/crates/http_protocol/src/utils.rs @@ -0,0 +1,335 @@ +use std::io::Write; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use anyhow::Context; +use serde::{de::Visitor, Deserializer, Serializer}; + +use super::response::ResponsePeer; + +pub fn urlencode_20_bytes(input: [u8; 20], output: &mut impl Write) -> ::std::io::Result<()> { + let mut tmp = [b'%'; 60]; + + for i in 0..input.len() { + hex::encode_to_slice(&input[i..i + 1], &mut tmp[i * 3 + 1..i * 3 + 3]).unwrap(); + } + + output.write_all(&tmp)?; + + Ok(()) +} + +pub fn urldecode_20_bytes(value: &str) -> anyhow::Result<[u8; 20]> { + let mut out_arr = [0u8; 20]; + + let mut chars = value.chars(); + + for i in 0..20 { + let c = chars.next().with_context(|| "less than 20 chars")?; + + if c as u32 > 255 { + return Err(anyhow::anyhow!( + "character not in single byte range: {:#?}", + c + )); + } + + if c == '%' { + let first = chars + .next() + .with_context(|| "missing first urldecode char in pair")?; + let second = chars + .next() + .with_context(|| "missing second urldecode char in pair")?; + + let hex = [first as u8, second as u8]; + + hex::decode_to_slice(hex, &mut out_arr[i..i + 1]) + .map_err(|err| anyhow::anyhow!("hex decode error: {:?}", err))?; + } else { + out_arr[i] = c as u8; + } + } + + if chars.next().is_some() { + return Err(anyhow::anyhow!("more than 20 chars")); + } + + Ok(out_arr) +} + +#[inline] +pub fn serialize_optional_string(v: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match v { + Some(s) => serializer.serialize_str(s.as_str()), + None => Err(serde::ser::Error::custom("use skip_serializing_if")), + } +} + +#[inline] +pub fn serialize_20_bytes(bytes: &[u8; 20], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_bytes(bytes) +} + +struct TwentyByteVisitor; + +impl<'de> Visitor<'de> for TwentyByteVisitor { + type Value = [u8; 20]; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("20 bytes") + } + + #[inline] + fn visit_bytes(self, value: &[u8]) -> Result + where + E: ::serde::de::Error, + { + if value.len() != 20 { + return Err(::serde::de::Error::custom("not 20 bytes")); + } + + let mut arr = [0u8; 20]; + + arr.copy_from_slice(value); + + Ok(arr) + } +} + +#[inline] +pub fn deserialize_20_bytes<'de, D>(deserializer: D) -> Result<[u8; 20], D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(TwentyByteVisitor) +} + +pub fn serialize_response_peers_ipv4( + response_peers: &[ResponsePeer], + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut bytes = Vec::with_capacity(response_peers.len() * 6); + + for peer in response_peers { + bytes.extend_from_slice(&u32::from(peer.ip_address).to_be_bytes()); + bytes.extend_from_slice(&peer.port.to_be_bytes()) + } + + serializer.serialize_bytes(&bytes) +} + +pub fn serialize_response_peers_ipv6( + response_peers: &[ResponsePeer], + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut bytes = Vec::with_capacity(response_peers.len() * 6); + + for peer in response_peers { + bytes.extend_from_slice(&u128::from(peer.ip_address).to_be_bytes()); + bytes.extend_from_slice(&peer.port.to_be_bytes()) + } + + serializer.serialize_bytes(&bytes) +} + +struct ResponsePeersIpv4Visitor; + +impl<'de> Visitor<'de> for ResponsePeersIpv4Visitor { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("byte-encoded ipv4 address-port pairs") + } + + #[inline] + fn visit_bytes(self, value: &[u8]) -> Result + where + E: ::serde::de::Error, + { + let chunks = value.chunks_exact(6); + + if !chunks.remainder().is_empty() { + return Err(::serde::de::Error::custom("trailing bytes")); + } + + let mut ip_bytes = [0u8; 4]; + let mut port_bytes = [0u8; 2]; + + let peers = chunks + .into_iter() + .map(|chunk| { + ip_bytes.copy_from_slice(&chunk[0..4]); + port_bytes.copy_from_slice(&chunk[4..6]); + + ResponsePeer { + ip_address: Ipv4Addr::from(u32::from_be_bytes(ip_bytes)), + port: u16::from_be_bytes(port_bytes), + } + }) + .collect(); + + Ok(peers) + } +} + +#[inline] +pub fn deserialize_response_peers_ipv4<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(ResponsePeersIpv4Visitor) +} + +struct ResponsePeersIpv6Visitor; + +impl<'de> Visitor<'de> for ResponsePeersIpv6Visitor { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("byte-encoded ipv6 address-port pairs") + } + + #[inline] + fn visit_bytes(self, value: &[u8]) -> Result + where + E: ::serde::de::Error, + { + let chunks = value.chunks_exact(18); + + if !chunks.remainder().is_empty() { + return Err(::serde::de::Error::custom("trailing bytes")); + } + + let mut ip_bytes = [0u8; 16]; + let mut port_bytes = [0u8; 2]; + + let peers = chunks + .into_iter() + .map(|chunk| { + ip_bytes.copy_from_slice(&chunk[0..16]); + port_bytes.copy_from_slice(&chunk[16..18]); + + ResponsePeer { + ip_address: Ipv6Addr::from(u128::from_be_bytes(ip_bytes)), + port: u16::from_be_bytes(port_bytes), + } + }) + .collect(); + + Ok(peers) + } +} + +#[inline] +pub fn deserialize_response_peers_ipv6<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(ResponsePeersIpv6Visitor) +} + +#[cfg(test)] +mod tests { + use quickcheck_macros::*; + + use crate::common::InfoHash; + + use super::*; + + #[test] + fn test_urlencode_20_bytes() { + let mut input = [0u8; 20]; + + for (i, b) in input.iter_mut().enumerate() { + *b = i as u8 % 10; + } + + let mut output = Vec::new(); + + urlencode_20_bytes(input, &mut output).unwrap(); + + assert_eq!(output.len(), 60); + + for (i, chunk) in output.chunks_exact(3).enumerate() { + // Not perfect but should do the job + let reference = [b'%', b'0', input[i] + 48]; + + let success = chunk == reference; + + if !success { + println!("failing index: {}", i); + } + + assert_eq!(chunk, reference); + } + } + + #[allow(clippy::too_many_arguments)] + #[quickcheck] + fn test_urlencode_urldecode_20_bytes( + a: u8, + b: u8, + c: u8, + d: u8, + e: u8, + f: u8, + g: u8, + h: u8, + ) -> bool { + let input: [u8; 20] = [a, b, c, d, e, f, g, h, b, c, d, a, e, f, g, h, a, b, d, c]; + + let mut output = Vec::new(); + + urlencode_20_bytes(input, &mut output).unwrap(); + + let s = ::std::str::from_utf8(&output).unwrap(); + + let decoded = urldecode_20_bytes(s).unwrap(); + + assert_eq!(input, decoded); + + input == decoded + } + + #[quickcheck] + fn test_serde_response_peers_ipv4(peers: Vec>) -> bool { + let serialized = bendy::serde::to_bytes(&peers).unwrap(); + let deserialized: Vec> = + ::bendy::serde::from_bytes(&serialized).unwrap(); + + peers == deserialized + } + + #[quickcheck] + fn test_serde_response_peers_ipv6(peers: Vec>) -> bool { + let serialized = bendy::serde::to_bytes(&peers).unwrap(); + let deserialized: Vec> = + ::bendy::serde::from_bytes(&serialized).unwrap(); + + peers == deserialized + } + + #[quickcheck] + fn test_serde_info_hash(info_hash: InfoHash) -> bool { + let serialized = bendy::serde::to_bytes(&info_hash).unwrap(); + let deserialized: InfoHash = ::bendy::serde::from_bytes(&serialized).unwrap(); + + info_hash == deserialized + } +} diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/benchmark.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/benchmark.json new file mode 100644 index 0000000..1c97482 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/benchmark.json @@ -0,0 +1 @@ +{"group_id":"announce-response-to-bytes","function_id":null,"value_str":null,"throughput":null,"full_id":"announce-response-to-bytes","directory_name":"announce-response-to-bytes","title":"announce-response-to-bytes"} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/estimates.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/estimates.json new file mode 100644 index 0000000..450cd39 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/estimates.json @@ -0,0 +1 @@ +{"mean":{"confidence_interval":{"confidence_level":0.95,"lower_bound":6033.211414448978,"upper_bound":6077.812796004471},"point_estimate":6054.625623439862,"standard_error":11.387162302248655},"median":{"confidence_interval":{"confidence_level":0.95,"lower_bound":5978.799232230455,"upper_bound":6005.189535363421},"point_estimate":5992.745967541798,"standard_error":6.185398365563177},"median_abs_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":157.08470879401094,"upper_bound":190.1634482791119},"point_estimate":175.51713287349847,"standard_error":8.3821979113297},"slope":{"confidence_interval":{"confidence_level":0.95,"lower_bound":6052.909623777413,"upper_bound":6106.324900686703},"point_estimate":6078.257114077077,"standard_error":13.648790489926581},"std_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":285.8045348063516,"upper_bound":442.7497149360172},"point_estimate":363.44843558752416,"standard_error":40.16921333191484}} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/raw.csv b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/raw.csv new file mode 100644 index 0000000..ccff025 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/raw.csv @@ -0,0 +1,1001 @@ +group,function,value,throughput_num,throughput_type,sample_measured_value,unit,iteration_count +announce-response-to-bytes,,,,,353477.0,ns,60 +announce-response-to-bytes,,,,,671839.0,ns,120 +announce-response-to-bytes,,,,,1043769.0,ns,180 +announce-response-to-bytes,,,,,1387044.0,ns,240 +announce-response-to-bytes,,,,,1817564.0,ns,300 +announce-response-to-bytes,,,,,2091668.0,ns,360 +announce-response-to-bytes,,,,,2412910.0,ns,420 +announce-response-to-bytes,,,,,2765877.0,ns,480 +announce-response-to-bytes,,,,,3138818.0,ns,540 +announce-response-to-bytes,,,,,3405635.0,ns,600 +announce-response-to-bytes,,,,,3762510.0,ns,660 +announce-response-to-bytes,,,,,4223891.0,ns,720 +announce-response-to-bytes,,,,,4500944.0,ns,780 +announce-response-to-bytes,,,,,4843678.0,ns,840 +announce-response-to-bytes,,,,,5250885.0,ns,900 +announce-response-to-bytes,,,,,5554563.0,ns,960 +announce-response-to-bytes,,,,,5991116.0,ns,1020 +announce-response-to-bytes,,,,,6222664.0,ns,1080 +announce-response-to-bytes,,,,,6607356.0,ns,1140 +announce-response-to-bytes,,,,,6983522.0,ns,1200 +announce-response-to-bytes,,,,,8637887.0,ns,1260 +announce-response-to-bytes,,,,,8257978.0,ns,1320 +announce-response-to-bytes,,,,,7885038.0,ns,1380 +announce-response-to-bytes,,,,,8335095.0,ns,1440 +announce-response-to-bytes,,,,,8453124.0,ns,1500 +announce-response-to-bytes,,,,,8877779.0,ns,1560 +announce-response-to-bytes,,,,,9232114.0,ns,1620 +announce-response-to-bytes,,,,,9442849.0,ns,1680 +announce-response-to-bytes,,,,,10166530.0,ns,1740 +announce-response-to-bytes,,,,,10433222.0,ns,1800 +announce-response-to-bytes,,,,,10610175.0,ns,1860 +announce-response-to-bytes,,,,,11098298.0,ns,1920 +announce-response-to-bytes,,,,,14430476.0,ns,1980 +announce-response-to-bytes,,,,,12476277.0,ns,2040 +announce-response-to-bytes,,,,,12170643.0,ns,2100 +announce-response-to-bytes,,,,,15124300.0,ns,2160 +announce-response-to-bytes,,,,,13437874.0,ns,2220 +announce-response-to-bytes,,,,,12969128.0,ns,2280 +announce-response-to-bytes,,,,,13374407.0,ns,2340 +announce-response-to-bytes,,,,,13739441.0,ns,2400 +announce-response-to-bytes,,,,,14325178.0,ns,2460 +announce-response-to-bytes,,,,,14580725.0,ns,2520 +announce-response-to-bytes,,,,,17250927.0,ns,2580 +announce-response-to-bytes,,,,,16247807.0,ns,2640 +announce-response-to-bytes,,,,,15355145.0,ns,2700 +announce-response-to-bytes,,,,,15896458.0,ns,2760 +announce-response-to-bytes,,,,,16275307.0,ns,2820 +announce-response-to-bytes,,,,,16657808.0,ns,2880 +announce-response-to-bytes,,,,,16926589.0,ns,2940 +announce-response-to-bytes,,,,,17188926.0,ns,3000 +announce-response-to-bytes,,,,,17553302.0,ns,3060 +announce-response-to-bytes,,,,,18017365.0,ns,3120 +announce-response-to-bytes,,,,,18350516.0,ns,3180 +announce-response-to-bytes,,,,,18618363.0,ns,3240 +announce-response-to-bytes,,,,,18959740.0,ns,3300 +announce-response-to-bytes,,,,,19458262.0,ns,3360 +announce-response-to-bytes,,,,,19782865.0,ns,3420 +announce-response-to-bytes,,,,,23204104.0,ns,3480 +announce-response-to-bytes,,,,,20600613.0,ns,3540 +announce-response-to-bytes,,,,,20851087.0,ns,3600 +announce-response-to-bytes,,,,,21147790.0,ns,3660 +announce-response-to-bytes,,,,,21497480.0,ns,3720 +announce-response-to-bytes,,,,,21894629.0,ns,3780 +announce-response-to-bytes,,,,,22191943.0,ns,3840 +announce-response-to-bytes,,,,,26305230.0,ns,3900 +announce-response-to-bytes,,,,,22622414.0,ns,3960 +announce-response-to-bytes,,,,,23043038.0,ns,4020 +announce-response-to-bytes,,,,,24937566.0,ns,4080 +announce-response-to-bytes,,,,,23590074.0,ns,4140 +announce-response-to-bytes,,,,,24082522.0,ns,4200 +announce-response-to-bytes,,,,,24503129.0,ns,4260 +announce-response-to-bytes,,,,,27898991.0,ns,4320 +announce-response-to-bytes,,,,,25347663.0,ns,4380 +announce-response-to-bytes,,,,,25279202.0,ns,4440 +announce-response-to-bytes,,,,,29040732.0,ns,4500 +announce-response-to-bytes,,,,,25888104.0,ns,4560 +announce-response-to-bytes,,,,,26296051.0,ns,4620 +announce-response-to-bytes,,,,,26806933.0,ns,4680 +announce-response-to-bytes,,,,,32482311.0,ns,4740 +announce-response-to-bytes,,,,,32351207.0,ns,4800 +announce-response-to-bytes,,,,,30751181.0,ns,4860 +announce-response-to-bytes,,,,,28927226.0,ns,4920 +announce-response-to-bytes,,,,,28709346.0,ns,4980 +announce-response-to-bytes,,,,,29360592.0,ns,5040 +announce-response-to-bytes,,,,,32860228.0,ns,5100 +announce-response-to-bytes,,,,,29502673.0,ns,5160 +announce-response-to-bytes,,,,,32219011.0,ns,5220 +announce-response-to-bytes,,,,,34821885.0,ns,5280 +announce-response-to-bytes,,,,,33800622.0,ns,5340 +announce-response-to-bytes,,,,,33691953.0,ns,5400 +announce-response-to-bytes,,,,,35518131.0,ns,5460 +announce-response-to-bytes,,,,,33432340.0,ns,5520 +announce-response-to-bytes,,,,,34138684.0,ns,5580 +announce-response-to-bytes,,,,,32795340.0,ns,5640 +announce-response-to-bytes,,,,,35739999.0,ns,5700 +announce-response-to-bytes,,,,,33238758.0,ns,5760 +announce-response-to-bytes,,,,,36316533.0,ns,5820 +announce-response-to-bytes,,,,,33731463.0,ns,5880 +announce-response-to-bytes,,,,,34353777.0,ns,5940 +announce-response-to-bytes,,,,,35905258.0,ns,6000 +announce-response-to-bytes,,,,,38421003.0,ns,6060 +announce-response-to-bytes,,,,,38132903.0,ns,6120 +announce-response-to-bytes,,,,,36875710.0,ns,6180 +announce-response-to-bytes,,,,,36505413.0,ns,6240 +announce-response-to-bytes,,,,,36087596.0,ns,6300 +announce-response-to-bytes,,,,,37271903.0,ns,6360 +announce-response-to-bytes,,,,,37049958.0,ns,6420 +announce-response-to-bytes,,,,,40472064.0,ns,6480 +announce-response-to-bytes,,,,,41263730.0,ns,6540 +announce-response-to-bytes,,,,,37805192.0,ns,6600 +announce-response-to-bytes,,,,,38560967.0,ns,6660 +announce-response-to-bytes,,,,,38525118.0,ns,6720 +announce-response-to-bytes,,,,,42311399.0,ns,6780 +announce-response-to-bytes,,,,,42629719.0,ns,6840 +announce-response-to-bytes,,,,,40046757.0,ns,6900 +announce-response-to-bytes,,,,,39928551.0,ns,6960 +announce-response-to-bytes,,,,,43662024.0,ns,7020 +announce-response-to-bytes,,,,,41848673.0,ns,7080 +announce-response-to-bytes,,,,,41200469.0,ns,7140 +announce-response-to-bytes,,,,,41534287.0,ns,7200 +announce-response-to-bytes,,,,,41858123.0,ns,7260 +announce-response-to-bytes,,,,,45388067.0,ns,7320 +announce-response-to-bytes,,,,,44341923.0,ns,7380 +announce-response-to-bytes,,,,,44438842.0,ns,7440 +announce-response-to-bytes,,,,,46568539.0,ns,7500 +announce-response-to-bytes,,,,,44268013.0,ns,7560 +announce-response-to-bytes,,,,,43035579.0,ns,7620 +announce-response-to-bytes,,,,,44147832.0,ns,7680 +announce-response-to-bytes,,,,,55676747.0,ns,7740 +announce-response-to-bytes,,,,,46128532.0,ns,7800 +announce-response-to-bytes,,,,,48995123.0,ns,7860 +announce-response-to-bytes,,,,,49011107.0,ns,7920 +announce-response-to-bytes,,,,,49542556.0,ns,7980 +announce-response-to-bytes,,,,,46641470.0,ns,8040 +announce-response-to-bytes,,,,,46716072.0,ns,8100 +announce-response-to-bytes,,,,,48215559.0,ns,8160 +announce-response-to-bytes,,,,,49182187.0,ns,8220 +announce-response-to-bytes,,,,,47115008.0,ns,8280 +announce-response-to-bytes,,,,,47786180.0,ns,8340 +announce-response-to-bytes,,,,,55059536.0,ns,8400 +announce-response-to-bytes,,,,,50928893.0,ns,8460 +announce-response-to-bytes,,,,,48943719.0,ns,8520 +announce-response-to-bytes,,,,,49857201.0,ns,8580 +announce-response-to-bytes,,,,,53184689.0,ns,8640 +announce-response-to-bytes,,,,,60794127.0,ns,8700 +announce-response-to-bytes,,,,,50630441.0,ns,8760 +announce-response-to-bytes,,,,,54408127.0,ns,8820 +announce-response-to-bytes,,,,,51832713.0,ns,8880 +announce-response-to-bytes,,,,,53006034.0,ns,8940 +announce-response-to-bytes,,,,,64631314.0,ns,9000 +announce-response-to-bytes,,,,,52020623.0,ns,9060 +announce-response-to-bytes,,,,,56213004.0,ns,9120 +announce-response-to-bytes,,,,,52489138.0,ns,9180 +announce-response-to-bytes,,,,,53166450.0,ns,9240 +announce-response-to-bytes,,,,,56725165.0,ns,9300 +announce-response-to-bytes,,,,,56515953.0,ns,9360 +announce-response-to-bytes,,,,,53709942.0,ns,9420 +announce-response-to-bytes,,,,,54206409.0,ns,9480 +announce-response-to-bytes,,,,,56132382.0,ns,9540 +announce-response-to-bytes,,,,,58787885.0,ns,9600 +announce-response-to-bytes,,,,,62005408.0,ns,9660 +announce-response-to-bytes,,,,,56197378.0,ns,9720 +announce-response-to-bytes,,,,,56754534.0,ns,9780 +announce-response-to-bytes,,,,,57732334.0,ns,9840 +announce-response-to-bytes,,,,,60488688.0,ns,9900 +announce-response-to-bytes,,,,,60813225.0,ns,9960 +announce-response-to-bytes,,,,,57858522.0,ns,10020 +announce-response-to-bytes,,,,,58081463.0,ns,10080 +announce-response-to-bytes,,,,,61699888.0,ns,10140 +announce-response-to-bytes,,,,,62126469.0,ns,10200 +announce-response-to-bytes,,,,,59116459.0,ns,10260 +announce-response-to-bytes,,,,,63003805.0,ns,10320 +announce-response-to-bytes,,,,,60041341.0,ns,10380 +announce-response-to-bytes,,,,,66409481.0,ns,10440 +announce-response-to-bytes,,,,,60196749.0,ns,10500 +announce-response-to-bytes,,,,,63922988.0,ns,10560 +announce-response-to-bytes,,,,,60792574.0,ns,10620 +announce-response-to-bytes,,,,,64647668.0,ns,10680 +announce-response-to-bytes,,,,,61968715.0,ns,10740 +announce-response-to-bytes,,,,,65233419.0,ns,10800 +announce-response-to-bytes,,,,,71584918.0,ns,10860 +announce-response-to-bytes,,,,,66039537.0,ns,10920 +announce-response-to-bytes,,,,,63385630.0,ns,10980 +announce-response-to-bytes,,,,,68178873.0,ns,11040 +announce-response-to-bytes,,,,,65536982.0,ns,11100 +announce-response-to-bytes,,,,,67026550.0,ns,11160 +announce-response-to-bytes,,,,,69538573.0,ns,11220 +announce-response-to-bytes,,,,,66967027.0,ns,11280 +announce-response-to-bytes,,,,,67869989.0,ns,11340 +announce-response-to-bytes,,,,,82856095.0,ns,11400 +announce-response-to-bytes,,,,,82866466.0,ns,11460 +announce-response-to-bytes,,,,,72843625.0,ns,11520 +announce-response-to-bytes,,,,,72963878.0,ns,11580 +announce-response-to-bytes,,,,,71642696.0,ns,11640 +announce-response-to-bytes,,,,,69044029.0,ns,11700 +announce-response-to-bytes,,,,,72647288.0,ns,11760 +announce-response-to-bytes,,,,,67811715.0,ns,11820 +announce-response-to-bytes,,,,,72873251.0,ns,11880 +announce-response-to-bytes,,,,,69017873.0,ns,11940 +announce-response-to-bytes,,,,,75812336.0,ns,12000 +announce-response-to-bytes,,,,,71771513.0,ns,12060 +announce-response-to-bytes,,,,,75758429.0,ns,12120 +announce-response-to-bytes,,,,,70046417.0,ns,12180 +announce-response-to-bytes,,,,,73361092.0,ns,12240 +announce-response-to-bytes,,,,,73157993.0,ns,12300 +announce-response-to-bytes,,,,,78841560.0,ns,12360 +announce-response-to-bytes,,,,,71399961.0,ns,12420 +announce-response-to-bytes,,,,,74108353.0,ns,12480 +announce-response-to-bytes,,,,,74334975.0,ns,12540 +announce-response-to-bytes,,,,,74436359.0,ns,12600 +announce-response-to-bytes,,,,,77091056.0,ns,12660 +announce-response-to-bytes,,,,,73771256.0,ns,12720 +announce-response-to-bytes,,,,,73564834.0,ns,12780 +announce-response-to-bytes,,,,,79530338.0,ns,12840 +announce-response-to-bytes,,,,,84150804.0,ns,12900 +announce-response-to-bytes,,,,,75369968.0,ns,12960 +announce-response-to-bytes,,,,,77998120.0,ns,13020 +announce-response-to-bytes,,,,,76311466.0,ns,13080 +announce-response-to-bytes,,,,,75641518.0,ns,13140 +announce-response-to-bytes,,,,,79147069.0,ns,13200 +announce-response-to-bytes,,,,,80920147.0,ns,13260 +announce-response-to-bytes,,,,,79193771.0,ns,13320 +announce-response-to-bytes,,,,,83960644.0,ns,13380 +announce-response-to-bytes,,,,,81067454.0,ns,13440 +announce-response-to-bytes,,,,,84880079.0,ns,13500 +announce-response-to-bytes,,,,,77739006.0,ns,13560 +announce-response-to-bytes,,,,,82089795.0,ns,13620 +announce-response-to-bytes,,,,,82231275.0,ns,13680 +announce-response-to-bytes,,,,,83064916.0,ns,13740 +announce-response-to-bytes,,,,,82497784.0,ns,13800 +announce-response-to-bytes,,,,,83078740.0,ns,13860 +announce-response-to-bytes,,,,,86587154.0,ns,13920 +announce-response-to-bytes,,,,,80832370.0,ns,13980 +announce-response-to-bytes,,,,,84452136.0,ns,14040 +announce-response-to-bytes,,,,,80858264.0,ns,14100 +announce-response-to-bytes,,,,,88484934.0,ns,14160 +announce-response-to-bytes,,,,,85169320.0,ns,14220 +announce-response-to-bytes,,,,,83328677.0,ns,14280 +announce-response-to-bytes,,,,,89078676.0,ns,14340 +announce-response-to-bytes,,,,,82971370.0,ns,14400 +announce-response-to-bytes,,,,,87819952.0,ns,14460 +announce-response-to-bytes,,,,,84783546.0,ns,14520 +announce-response-to-bytes,,,,,89132177.0,ns,14580 +announce-response-to-bytes,,,,,85943628.0,ns,14640 +announce-response-to-bytes,,,,,87947035.0,ns,14700 +announce-response-to-bytes,,,,,88051952.0,ns,14760 +announce-response-to-bytes,,,,,86239807.0,ns,14820 +announce-response-to-bytes,,,,,89225024.0,ns,14880 +announce-response-to-bytes,,,,,90633440.0,ns,14940 +announce-response-to-bytes,,,,,89835662.0,ns,15000 +announce-response-to-bytes,,,,,86437593.0,ns,15060 +announce-response-to-bytes,,,,,95231827.0,ns,15120 +announce-response-to-bytes,,,,,87386235.0,ns,15180 +announce-response-to-bytes,,,,,90953717.0,ns,15240 +announce-response-to-bytes,,,,,99166999.0,ns,15300 +announce-response-to-bytes,,,,,93967708.0,ns,15360 +announce-response-to-bytes,,,,,89227799.0,ns,15420 +announce-response-to-bytes,,,,,95577648.0,ns,15480 +announce-response-to-bytes,,,,,92196890.0,ns,15540 +announce-response-to-bytes,,,,,90557507.0,ns,15600 +announce-response-to-bytes,,,,,93476159.0,ns,15660 +announce-response-to-bytes,,,,,102579136.0,ns,15720 +announce-response-to-bytes,,,,,97899948.0,ns,15780 +announce-response-to-bytes,,,,,98757931.0,ns,15840 +announce-response-to-bytes,,,,,96493485.0,ns,15900 +announce-response-to-bytes,,,,,104839437.0,ns,15960 +announce-response-to-bytes,,,,,93105475.0,ns,16020 +announce-response-to-bytes,,,,,96023558.0,ns,16080 +announce-response-to-bytes,,,,,109098464.0,ns,16140 +announce-response-to-bytes,,,,,96076451.0,ns,16200 +announce-response-to-bytes,,,,,93665090.0,ns,16260 +announce-response-to-bytes,,,,,103787277.0,ns,16320 +announce-response-to-bytes,,,,,94851510.0,ns,16380 +announce-response-to-bytes,,,,,101810430.0,ns,16440 +announce-response-to-bytes,,,,,95449054.0,ns,16500 +announce-response-to-bytes,,,,,99077967.0,ns,16560 +announce-response-to-bytes,,,,,105226577.0,ns,16620 +announce-response-to-bytes,,,,,98648289.0,ns,16680 +announce-response-to-bytes,,,,,96444291.0,ns,16740 +announce-response-to-bytes,,,,,101439485.0,ns,16800 +announce-response-to-bytes,,,,,100060710.0,ns,16860 +announce-response-to-bytes,,,,,98393281.0,ns,16920 +announce-response-to-bytes,,,,,100751746.0,ns,16980 +announce-response-to-bytes,,,,,101105000.0,ns,17040 +announce-response-to-bytes,,,,,102050246.0,ns,17100 +announce-response-to-bytes,,,,,98735599.0,ns,17160 +announce-response-to-bytes,,,,,104729218.0,ns,17220 +announce-response-to-bytes,,,,,101191327.0,ns,17280 +announce-response-to-bytes,,,,,99354741.0,ns,17340 +announce-response-to-bytes,,,,,103109972.0,ns,17400 +announce-response-to-bytes,,,,,106228511.0,ns,17460 +announce-response-to-bytes,,,,,104629383.0,ns,17520 +announce-response-to-bytes,,,,,104352162.0,ns,17580 +announce-response-to-bytes,,,,,108720711.0,ns,17640 +announce-response-to-bytes,,,,,105524991.0,ns,17700 +announce-response-to-bytes,,,,,111982358.0,ns,17760 +announce-response-to-bytes,,,,,108853889.0,ns,17820 +announce-response-to-bytes,,,,,106385675.0,ns,17880 +announce-response-to-bytes,,,,,109944044.0,ns,17940 +announce-response-to-bytes,,,,,106491227.0,ns,18000 +announce-response-to-bytes,,,,,107169087.0,ns,18060 +announce-response-to-bytes,,,,,103402650.0,ns,18120 +announce-response-to-bytes,,,,,105528215.0,ns,18180 +announce-response-to-bytes,,,,,106408989.0,ns,18240 +announce-response-to-bytes,,,,,118011891.0,ns,18300 +announce-response-to-bytes,,,,,108615001.0,ns,18360 +announce-response-to-bytes,,,,,108163576.0,ns,18420 +announce-response-to-bytes,,,,,115425723.0,ns,18480 +announce-response-to-bytes,,,,,106457390.0,ns,18540 +announce-response-to-bytes,,,,,112654377.0,ns,18600 +announce-response-to-bytes,,,,,114332495.0,ns,18660 +announce-response-to-bytes,,,,,115722798.0,ns,18720 +announce-response-to-bytes,,,,,113319709.0,ns,18780 +announce-response-to-bytes,,,,,112467215.0,ns,18840 +announce-response-to-bytes,,,,,131530065.0,ns,18900 +announce-response-to-bytes,,,,,112779834.0,ns,18960 +announce-response-to-bytes,,,,,113133784.0,ns,19020 +announce-response-to-bytes,,,,,116680560.0,ns,19080 +announce-response-to-bytes,,,,,120117889.0,ns,19140 +announce-response-to-bytes,,,,,114154506.0,ns,19200 +announce-response-to-bytes,,,,,116600877.0,ns,19260 +announce-response-to-bytes,,,,,111499012.0,ns,19320 +announce-response-to-bytes,,,,,115148520.0,ns,19380 +announce-response-to-bytes,,,,,119519846.0,ns,19440 +announce-response-to-bytes,,,,,121352443.0,ns,19500 +announce-response-to-bytes,,,,,116141216.0,ns,19560 +announce-response-to-bytes,,,,,120118044.0,ns,19620 +announce-response-to-bytes,,,,,117208814.0,ns,19680 +announce-response-to-bytes,,,,,118901147.0,ns,19740 +announce-response-to-bytes,,,,,118241424.0,ns,19800 +announce-response-to-bytes,,,,,117654664.0,ns,19860 +announce-response-to-bytes,,,,,116226405.0,ns,19920 +announce-response-to-bytes,,,,,116840915.0,ns,19980 +announce-response-to-bytes,,,,,122639608.0,ns,20040 +announce-response-to-bytes,,,,,119442677.0,ns,20100 +announce-response-to-bytes,,,,,122998235.0,ns,20160 +announce-response-to-bytes,,,,,116123143.0,ns,20220 +announce-response-to-bytes,,,,,116872198.0,ns,20280 +announce-response-to-bytes,,,,,138102237.0,ns,20340 +announce-response-to-bytes,,,,,128437710.0,ns,20400 +announce-response-to-bytes,,,,,121137265.0,ns,20460 +announce-response-to-bytes,,,,,126880043.0,ns,20520 +announce-response-to-bytes,,,,,122836585.0,ns,20580 +announce-response-to-bytes,,,,,216570089.0,ns,20640 +announce-response-to-bytes,,,,,141742390.0,ns,20700 +announce-response-to-bytes,,,,,128668059.0,ns,20760 +announce-response-to-bytes,,,,,130583224.0,ns,20820 +announce-response-to-bytes,,,,,123696240.0,ns,20880 +announce-response-to-bytes,,,,,130653847.0,ns,20940 +announce-response-to-bytes,,,,,123301000.0,ns,21000 +announce-response-to-bytes,,,,,124609971.0,ns,21060 +announce-response-to-bytes,,,,,132437379.0,ns,21120 +announce-response-to-bytes,,,,,128333900.0,ns,21180 +announce-response-to-bytes,,,,,125820573.0,ns,21240 +announce-response-to-bytes,,,,,132504190.0,ns,21300 +announce-response-to-bytes,,,,,132424433.0,ns,21360 +announce-response-to-bytes,,,,,129169050.0,ns,21420 +announce-response-to-bytes,,,,,130513574.0,ns,21480 +announce-response-to-bytes,,,,,128951601.0,ns,21540 +announce-response-to-bytes,,,,,128100399.0,ns,21600 +announce-response-to-bytes,,,,,127990454.0,ns,21660 +announce-response-to-bytes,,,,,137819400.0,ns,21720 +announce-response-to-bytes,,,,,144746386.0,ns,21780 +announce-response-to-bytes,,,,,216431912.0,ns,21840 +announce-response-to-bytes,,,,,150020152.0,ns,21900 +announce-response-to-bytes,,,,,135441027.0,ns,21960 +announce-response-to-bytes,,,,,130994307.0,ns,22020 +announce-response-to-bytes,,,,,133180497.0,ns,22080 +announce-response-to-bytes,,,,,127115197.0,ns,22140 +announce-response-to-bytes,,,,,131512985.0,ns,22200 +announce-response-to-bytes,,,,,129470937.0,ns,22260 +announce-response-to-bytes,,,,,132878166.0,ns,22320 +announce-response-to-bytes,,,,,138877962.0,ns,22380 +announce-response-to-bytes,,,,,134822962.0,ns,22440 +announce-response-to-bytes,,,,,129266707.0,ns,22500 +announce-response-to-bytes,,,,,132254185.0,ns,22560 +announce-response-to-bytes,,,,,133606223.0,ns,22620 +announce-response-to-bytes,,,,,136628818.0,ns,22680 +announce-response-to-bytes,,,,,135017238.0,ns,22740 +announce-response-to-bytes,,,,,134922705.0,ns,22800 +announce-response-to-bytes,,,,,131398549.0,ns,22860 +announce-response-to-bytes,,,,,136184490.0,ns,22920 +announce-response-to-bytes,,,,,136662292.0,ns,22980 +announce-response-to-bytes,,,,,139084616.0,ns,23040 +announce-response-to-bytes,,,,,142374414.0,ns,23100 +announce-response-to-bytes,,,,,139672055.0,ns,23160 +announce-response-to-bytes,,,,,139665680.0,ns,23220 +announce-response-to-bytes,,,,,140454153.0,ns,23280 +announce-response-to-bytes,,,,,134045293.0,ns,23340 +announce-response-to-bytes,,,,,143119691.0,ns,23400 +announce-response-to-bytes,,,,,144157458.0,ns,23460 +announce-response-to-bytes,,,,,142513360.0,ns,23520 +announce-response-to-bytes,,,,,145299170.0,ns,23580 +announce-response-to-bytes,,,,,145304718.0,ns,23640 +announce-response-to-bytes,,,,,143137147.0,ns,23700 +announce-response-to-bytes,,,,,150700164.0,ns,23760 +announce-response-to-bytes,,,,,143088363.0,ns,23820 +announce-response-to-bytes,,,,,140659241.0,ns,23880 +announce-response-to-bytes,,,,,151175740.0,ns,23940 +announce-response-to-bytes,,,,,149559608.0,ns,24000 +announce-response-to-bytes,,,,,141507307.0,ns,24060 +announce-response-to-bytes,,,,,142098061.0,ns,24120 +announce-response-to-bytes,,,,,142306098.0,ns,24180 +announce-response-to-bytes,,,,,145560035.0,ns,24240 +announce-response-to-bytes,,,,,145551983.0,ns,24300 +announce-response-to-bytes,,,,,154656278.0,ns,24360 +announce-response-to-bytes,,,,,146982431.0,ns,24420 +announce-response-to-bytes,,,,,149980678.0,ns,24480 +announce-response-to-bytes,,,,,149811257.0,ns,24540 +announce-response-to-bytes,,,,,144410416.0,ns,24600 +announce-response-to-bytes,,,,,145107309.0,ns,24660 +announce-response-to-bytes,,,,,148069434.0,ns,24720 +announce-response-to-bytes,,,,,147743674.0,ns,24780 +announce-response-to-bytes,,,,,158591021.0,ns,24840 +announce-response-to-bytes,,,,,153804335.0,ns,24900 +announce-response-to-bytes,,,,,151333913.0,ns,24960 +announce-response-to-bytes,,,,,147205551.0,ns,25020 +announce-response-to-bytes,,,,,151011082.0,ns,25080 +announce-response-to-bytes,,,,,150994405.0,ns,25140 +announce-response-to-bytes,,,,,151583075.0,ns,25200 +announce-response-to-bytes,,,,,150962534.0,ns,25260 +announce-response-to-bytes,,,,,158942748.0,ns,25320 +announce-response-to-bytes,,,,,153943407.0,ns,25380 +announce-response-to-bytes,,,,,152188911.0,ns,25440 +announce-response-to-bytes,,,,,157514396.0,ns,25500 +announce-response-to-bytes,,,,,155180550.0,ns,25560 +announce-response-to-bytes,,,,,152504791.0,ns,25620 +announce-response-to-bytes,,,,,160079543.0,ns,25680 +announce-response-to-bytes,,,,,151453512.0,ns,25740 +announce-response-to-bytes,,,,,151384295.0,ns,25800 +announce-response-to-bytes,,,,,156963082.0,ns,25860 +announce-response-to-bytes,,,,,150828531.0,ns,25920 +announce-response-to-bytes,,,,,157597601.0,ns,25980 +announce-response-to-bytes,,,,,158605060.0,ns,26040 +announce-response-to-bytes,,,,,154264234.0,ns,26100 +announce-response-to-bytes,,,,,157418039.0,ns,26160 +announce-response-to-bytes,,,,,157380542.0,ns,26220 +announce-response-to-bytes,,,,,154039954.0,ns,26280 +announce-response-to-bytes,,,,,155847797.0,ns,26340 +announce-response-to-bytes,,,,,159540498.0,ns,26400 +announce-response-to-bytes,,,,,158636199.0,ns,26460 +announce-response-to-bytes,,,,,162123392.0,ns,26520 +announce-response-to-bytes,,,,,160133693.0,ns,26580 +announce-response-to-bytes,,,,,167252226.0,ns,26640 +announce-response-to-bytes,,,,,157875477.0,ns,26700 +announce-response-to-bytes,,,,,160387414.0,ns,26760 +announce-response-to-bytes,,,,,159725786.0,ns,26820 +announce-response-to-bytes,,,,,159519574.0,ns,26880 +announce-response-to-bytes,,,,,159144790.0,ns,26940 +announce-response-to-bytes,,,,,159724882.0,ns,27000 +announce-response-to-bytes,,,,,159626097.0,ns,27060 +announce-response-to-bytes,,,,,167716074.0,ns,27120 +announce-response-to-bytes,,,,,165325796.0,ns,27180 +announce-response-to-bytes,,,,,159454651.0,ns,27240 +announce-response-to-bytes,,,,,166192327.0,ns,27300 +announce-response-to-bytes,,,,,164141388.0,ns,27360 +announce-response-to-bytes,,,,,159827333.0,ns,27420 +announce-response-to-bytes,,,,,159281575.0,ns,27480 +announce-response-to-bytes,,,,,161338829.0,ns,27540 +announce-response-to-bytes,,,,,201312225.0,ns,27600 +announce-response-to-bytes,,,,,173052547.0,ns,27660 +announce-response-to-bytes,,,,,174872126.0,ns,27720 +announce-response-to-bytes,,,,,162826556.0,ns,27780 +announce-response-to-bytes,,,,,166301823.0,ns,27840 +announce-response-to-bytes,,,,,172216250.0,ns,27900 +announce-response-to-bytes,,,,,164447879.0,ns,27960 +announce-response-to-bytes,,,,,165765095.0,ns,28020 +announce-response-to-bytes,,,,,165112499.0,ns,28080 +announce-response-to-bytes,,,,,172573539.0,ns,28140 +announce-response-to-bytes,,,,,168652965.0,ns,28200 +announce-response-to-bytes,,,,,195098828.0,ns,28260 +announce-response-to-bytes,,,,,182327486.0,ns,28320 +announce-response-to-bytes,,,,,170434022.0,ns,28380 +announce-response-to-bytes,,,,,169223601.0,ns,28440 +announce-response-to-bytes,,,,,162751851.0,ns,28500 +announce-response-to-bytes,,,,,179716421.0,ns,28560 +announce-response-to-bytes,,,,,171936500.0,ns,28620 +announce-response-to-bytes,,,,,168791350.0,ns,28680 +announce-response-to-bytes,,,,,172923539.0,ns,28740 +announce-response-to-bytes,,,,,173294241.0,ns,28800 +announce-response-to-bytes,,,,,178158947.0,ns,28860 +announce-response-to-bytes,,,,,170223356.0,ns,28920 +announce-response-to-bytes,,,,,174141701.0,ns,28980 +announce-response-to-bytes,,,,,181654398.0,ns,29040 +announce-response-to-bytes,,,,,173452627.0,ns,29100 +announce-response-to-bytes,,,,,176756076.0,ns,29160 +announce-response-to-bytes,,,,,173841937.0,ns,29220 +announce-response-to-bytes,,,,,182670206.0,ns,29280 +announce-response-to-bytes,,,,,182615059.0,ns,29340 +announce-response-to-bytes,,,,,183366222.0,ns,29400 +announce-response-to-bytes,,,,,176423114.0,ns,29460 +announce-response-to-bytes,,,,,179346613.0,ns,29520 +announce-response-to-bytes,,,,,179672994.0,ns,29580 +announce-response-to-bytes,,,,,187219070.0,ns,29640 +announce-response-to-bytes,,,,,182825189.0,ns,29700 +announce-response-to-bytes,,,,,175152698.0,ns,29760 +announce-response-to-bytes,,,,,178268199.0,ns,29820 +announce-response-to-bytes,,,,,174561075.0,ns,29880 +announce-response-to-bytes,,,,,174990534.0,ns,29940 +announce-response-to-bytes,,,,,177516663.0,ns,30000 +announce-response-to-bytes,,,,,181544897.0,ns,30060 +announce-response-to-bytes,,,,,176721962.0,ns,30120 +announce-response-to-bytes,,,,,180219494.0,ns,30180 +announce-response-to-bytes,,,,,180313631.0,ns,30240 +announce-response-to-bytes,,,,,186697851.0,ns,30300 +announce-response-to-bytes,,,,,183867116.0,ns,30360 +announce-response-to-bytes,,,,,178658999.0,ns,30420 +announce-response-to-bytes,,,,,185147892.0,ns,30480 +announce-response-to-bytes,,,,,178798904.0,ns,30540 +announce-response-to-bytes,,,,,182805520.0,ns,30600 +announce-response-to-bytes,,,,,185003086.0,ns,30660 +announce-response-to-bytes,,,,,186256141.0,ns,30720 +announce-response-to-bytes,,,,,191635803.0,ns,30780 +announce-response-to-bytes,,,,,183086863.0,ns,30840 +announce-response-to-bytes,,,,,184010410.0,ns,30900 +announce-response-to-bytes,,,,,178541981.0,ns,30960 +announce-response-to-bytes,,,,,183182832.0,ns,31020 +announce-response-to-bytes,,,,,186382283.0,ns,31080 +announce-response-to-bytes,,,,,190163970.0,ns,31140 +announce-response-to-bytes,,,,,189246648.0,ns,31200 +announce-response-to-bytes,,,,,189380527.0,ns,31260 +announce-response-to-bytes,,,,,188107590.0,ns,31320 +announce-response-to-bytes,,,,,186459717.0,ns,31380 +announce-response-to-bytes,,,,,190261961.0,ns,31440 +announce-response-to-bytes,,,,,182543672.0,ns,31500 +announce-response-to-bytes,,,,,187437911.0,ns,31560 +announce-response-to-bytes,,,,,295643192.0,ns,31620 +announce-response-to-bytes,,,,,189866177.0,ns,31680 +announce-response-to-bytes,,,,,187710997.0,ns,31740 +announce-response-to-bytes,,,,,190784712.0,ns,31800 +announce-response-to-bytes,,,,,188294138.0,ns,31860 +announce-response-to-bytes,,,,,197609140.0,ns,31920 +announce-response-to-bytes,,,,,191207744.0,ns,31980 +announce-response-to-bytes,,,,,194321027.0,ns,32040 +announce-response-to-bytes,,,,,191733731.0,ns,32100 +announce-response-to-bytes,,,,,185822221.0,ns,32160 +announce-response-to-bytes,,,,,200121480.0,ns,32220 +announce-response-to-bytes,,,,,188778779.0,ns,32280 +announce-response-to-bytes,,,,,189413962.0,ns,32340 +announce-response-to-bytes,,,,,230938799.0,ns,32400 +announce-response-to-bytes,,,,,190683965.0,ns,32460 +announce-response-to-bytes,,,,,195883270.0,ns,32520 +announce-response-to-bytes,,,,,193999872.0,ns,32580 +announce-response-to-bytes,,,,,201550915.0,ns,32640 +announce-response-to-bytes,,,,,198591497.0,ns,32700 +announce-response-to-bytes,,,,,197251415.0,ns,32760 +announce-response-to-bytes,,,,,192361413.0,ns,32820 +announce-response-to-bytes,,,,,192777082.0,ns,32880 +announce-response-to-bytes,,,,,203604542.0,ns,32940 +announce-response-to-bytes,,,,,195817030.0,ns,33000 +announce-response-to-bytes,,,,,198903617.0,ns,33060 +announce-response-to-bytes,,,,,203230503.0,ns,33120 +announce-response-to-bytes,,,,,191594234.0,ns,33180 +announce-response-to-bytes,,,,,202084594.0,ns,33240 +announce-response-to-bytes,,,,,204345613.0,ns,33300 +announce-response-to-bytes,,,,,196837662.0,ns,33360 +announce-response-to-bytes,,,,,197965331.0,ns,33420 +announce-response-to-bytes,,,,,202817373.0,ns,33480 +announce-response-to-bytes,,,,,209534403.0,ns,33540 +announce-response-to-bytes,,,,,200458645.0,ns,33600 +announce-response-to-bytes,,,,,205032826.0,ns,33660 +announce-response-to-bytes,,,,,198179128.0,ns,33720 +announce-response-to-bytes,,,,,197422710.0,ns,33780 +announce-response-to-bytes,,,,,201316422.0,ns,33840 +announce-response-to-bytes,,,,,205412634.0,ns,33900 +announce-response-to-bytes,,,,,201826245.0,ns,33960 +announce-response-to-bytes,,,,,199264327.0,ns,34020 +announce-response-to-bytes,,,,,206017461.0,ns,34080 +announce-response-to-bytes,,,,,208321075.0,ns,34140 +announce-response-to-bytes,,,,,205711694.0,ns,34200 +announce-response-to-bytes,,,,,207475384.0,ns,34260 +announce-response-to-bytes,,,,,202232325.0,ns,34320 +announce-response-to-bytes,,,,,201545478.0,ns,34380 +announce-response-to-bytes,,,,,202727542.0,ns,34440 +announce-response-to-bytes,,,,,205538690.0,ns,34500 +announce-response-to-bytes,,,,,217018134.0,ns,34560 +announce-response-to-bytes,,,,,210273035.0,ns,34620 +announce-response-to-bytes,,,,,208041159.0,ns,34680 +announce-response-to-bytes,,,,,207455945.0,ns,34740 +announce-response-to-bytes,,,,,207251814.0,ns,34800 +announce-response-to-bytes,,,,,209859297.0,ns,34860 +announce-response-to-bytes,,,,,209800938.0,ns,34920 +announce-response-to-bytes,,,,,205054440.0,ns,34980 +announce-response-to-bytes,,,,,205186042.0,ns,35040 +announce-response-to-bytes,,,,,206908202.0,ns,35100 +announce-response-to-bytes,,,,,250211709.0,ns,35160 +announce-response-to-bytes,,,,,207711175.0,ns,35220 +announce-response-to-bytes,,,,,213023350.0,ns,35280 +announce-response-to-bytes,,,,,213613526.0,ns,35340 +announce-response-to-bytes,,,,,212993128.0,ns,35400 +announce-response-to-bytes,,,,,214689251.0,ns,35460 +announce-response-to-bytes,,,,,210626846.0,ns,35520 +announce-response-to-bytes,,,,,213075462.0,ns,35580 +announce-response-to-bytes,,,,,211900429.0,ns,35640 +announce-response-to-bytes,,,,,210183891.0,ns,35700 +announce-response-to-bytes,,,,,217957022.0,ns,35760 +announce-response-to-bytes,,,,,221709232.0,ns,35820 +announce-response-to-bytes,,,,,219281524.0,ns,35880 +announce-response-to-bytes,,,,,215445563.0,ns,35940 +announce-response-to-bytes,,,,,236385901.0,ns,36000 +announce-response-to-bytes,,,,,223709858.0,ns,36060 +announce-response-to-bytes,,,,,221003707.0,ns,36120 +announce-response-to-bytes,,,,,211281157.0,ns,36180 +announce-response-to-bytes,,,,,214120029.0,ns,36240 +announce-response-to-bytes,,,,,215784498.0,ns,36300 +announce-response-to-bytes,,,,,220203496.0,ns,36360 +announce-response-to-bytes,,,,,227515929.0,ns,36420 +announce-response-to-bytes,,,,,211449430.0,ns,36480 +announce-response-to-bytes,,,,,222240876.0,ns,36540 +announce-response-to-bytes,,,,,219946447.0,ns,36600 +announce-response-to-bytes,,,,,217701524.0,ns,36660 +announce-response-to-bytes,,,,,217260772.0,ns,36720 +announce-response-to-bytes,,,,,221257631.0,ns,36780 +announce-response-to-bytes,,,,,228530271.0,ns,36840 +announce-response-to-bytes,,,,,222466784.0,ns,36900 +announce-response-to-bytes,,,,,215783231.0,ns,36960 +announce-response-to-bytes,,,,,224538305.0,ns,37020 +announce-response-to-bytes,,,,,228788087.0,ns,37080 +announce-response-to-bytes,,,,,222543752.0,ns,37140 +announce-response-to-bytes,,,,,235717001.0,ns,37200 +announce-response-to-bytes,,,,,217727435.0,ns,37260 +announce-response-to-bytes,,,,,218742676.0,ns,37320 +announce-response-to-bytes,,,,,218327248.0,ns,37380 +announce-response-to-bytes,,,,,253768428.0,ns,37440 +announce-response-to-bytes,,,,,224585715.0,ns,37500 +announce-response-to-bytes,,,,,322646119.0,ns,37560 +announce-response-to-bytes,,,,,236805536.0,ns,37620 +announce-response-to-bytes,,,,,220951586.0,ns,37680 +announce-response-to-bytes,,,,,229392071.0,ns,37740 +announce-response-to-bytes,,,,,227137335.0,ns,37800 +announce-response-to-bytes,,,,,227661592.0,ns,37860 +announce-response-to-bytes,,,,,257438175.0,ns,37920 +announce-response-to-bytes,,,,,224997497.0,ns,37980 +announce-response-to-bytes,,,,,231055413.0,ns,38040 +announce-response-to-bytes,,,,,243283033.0,ns,38100 +announce-response-to-bytes,,,,,225608551.0,ns,38160 +announce-response-to-bytes,,,,,230858718.0,ns,38220 +announce-response-to-bytes,,,,,236236544.0,ns,38280 +announce-response-to-bytes,,,,,247652247.0,ns,38340 +announce-response-to-bytes,,,,,228653425.0,ns,38400 +announce-response-to-bytes,,,,,227498067.0,ns,38460 +announce-response-to-bytes,,,,,221423660.0,ns,38520 +announce-response-to-bytes,,,,,229075019.0,ns,38580 +announce-response-to-bytes,,,,,232666354.0,ns,38640 +announce-response-to-bytes,,,,,228996472.0,ns,38700 +announce-response-to-bytes,,,,,244834606.0,ns,38760 +announce-response-to-bytes,,,,,233992244.0,ns,38820 +announce-response-to-bytes,,,,,222921409.0,ns,38880 +announce-response-to-bytes,,,,,225773687.0,ns,38940 +announce-response-to-bytes,,,,,227002461.0,ns,39000 +announce-response-to-bytes,,,,,237459910.0,ns,39060 +announce-response-to-bytes,,,,,236495788.0,ns,39120 +announce-response-to-bytes,,,,,231470987.0,ns,39180 +announce-response-to-bytes,,,,,239653523.0,ns,39240 +announce-response-to-bytes,,,,,233924817.0,ns,39300 +announce-response-to-bytes,,,,,235958944.0,ns,39360 +announce-response-to-bytes,,,,,234033523.0,ns,39420 +announce-response-to-bytes,,,,,243227066.0,ns,39480 +announce-response-to-bytes,,,,,233884565.0,ns,39540 +announce-response-to-bytes,,,,,244863425.0,ns,39600 +announce-response-to-bytes,,,,,237975552.0,ns,39660 +announce-response-to-bytes,,,,,232741349.0,ns,39720 +announce-response-to-bytes,,,,,240014376.0,ns,39780 +announce-response-to-bytes,,,,,239146388.0,ns,39840 +announce-response-to-bytes,,,,,234852762.0,ns,39900 +announce-response-to-bytes,,,,,257608891.0,ns,39960 +announce-response-to-bytes,,,,,236265551.0,ns,40020 +announce-response-to-bytes,,,,,247460522.0,ns,40080 +announce-response-to-bytes,,,,,240313882.0,ns,40140 +announce-response-to-bytes,,,,,239048596.0,ns,40200 +announce-response-to-bytes,,,,,242063447.0,ns,40260 +announce-response-to-bytes,,,,,231511769.0,ns,40320 +announce-response-to-bytes,,,,,232216223.0,ns,40380 +announce-response-to-bytes,,,,,245886303.0,ns,40440 +announce-response-to-bytes,,,,,250613484.0,ns,40500 +announce-response-to-bytes,,,,,237017839.0,ns,40560 +announce-response-to-bytes,,,,,251563100.0,ns,40620 +announce-response-to-bytes,,,,,238530772.0,ns,40680 +announce-response-to-bytes,,,,,236810668.0,ns,40740 +announce-response-to-bytes,,,,,253812434.0,ns,40800 +announce-response-to-bytes,,,,,241830203.0,ns,40860 +announce-response-to-bytes,,,,,246490586.0,ns,40920 +announce-response-to-bytes,,,,,241384498.0,ns,40980 +announce-response-to-bytes,,,,,249185798.0,ns,41040 +announce-response-to-bytes,,,,,244845846.0,ns,41100 +announce-response-to-bytes,,,,,247865640.0,ns,41160 +announce-response-to-bytes,,,,,243400028.0,ns,41220 +announce-response-to-bytes,,,,,245408891.0,ns,41280 +announce-response-to-bytes,,,,,246362124.0,ns,41340 +announce-response-to-bytes,,,,,248266087.0,ns,41400 +announce-response-to-bytes,,,,,238807274.0,ns,41460 +announce-response-to-bytes,,,,,253736956.0,ns,41520 +announce-response-to-bytes,,,,,238456260.0,ns,41580 +announce-response-to-bytes,,,,,253284697.0,ns,41640 +announce-response-to-bytes,,,,,240780177.0,ns,41700 +announce-response-to-bytes,,,,,243780800.0,ns,41760 +announce-response-to-bytes,,,,,261015261.0,ns,41820 +announce-response-to-bytes,,,,,264633730.0,ns,41880 +announce-response-to-bytes,,,,,253476753.0,ns,41940 +announce-response-to-bytes,,,,,249523924.0,ns,42000 +announce-response-to-bytes,,,,,247720589.0,ns,42060 +announce-response-to-bytes,,,,,242037092.0,ns,42120 +announce-response-to-bytes,,,,,280041661.0,ns,42180 +announce-response-to-bytes,,,,,254767480.0,ns,42240 +announce-response-to-bytes,,,,,253020123.0,ns,42300 +announce-response-to-bytes,,,,,250126273.0,ns,42360 +announce-response-to-bytes,,,,,268141541.0,ns,42420 +announce-response-to-bytes,,,,,252241167.0,ns,42480 +announce-response-to-bytes,,,,,260026538.0,ns,42540 +announce-response-to-bytes,,,,,246449612.0,ns,42600 +announce-response-to-bytes,,,,,253117221.0,ns,42660 +announce-response-to-bytes,,,,,257987408.0,ns,42720 +announce-response-to-bytes,,,,,260326828.0,ns,42780 +announce-response-to-bytes,,,,,254904127.0,ns,42840 +announce-response-to-bytes,,,,,253636375.0,ns,42900 +announce-response-to-bytes,,,,,257664625.0,ns,42960 +announce-response-to-bytes,,,,,256506942.0,ns,43020 +announce-response-to-bytes,,,,,253883956.0,ns,43080 +announce-response-to-bytes,,,,,259280149.0,ns,43140 +announce-response-to-bytes,,,,,260617047.0,ns,43200 +announce-response-to-bytes,,,,,257393607.0,ns,43260 +announce-response-to-bytes,,,,,260101462.0,ns,43320 +announce-response-to-bytes,,,,,256769317.0,ns,43380 +announce-response-to-bytes,,,,,343173643.0,ns,43440 +announce-response-to-bytes,,,,,290846207.0,ns,43500 +announce-response-to-bytes,,,,,254576878.0,ns,43560 +announce-response-to-bytes,,,,,259426287.0,ns,43620 +announce-response-to-bytes,,,,,259803576.0,ns,43680 +announce-response-to-bytes,,,,,254732236.0,ns,43740 +announce-response-to-bytes,,,,,263534559.0,ns,43800 +announce-response-to-bytes,,,,,259862273.0,ns,43860 +announce-response-to-bytes,,,,,271727114.0,ns,43920 +announce-response-to-bytes,,,,,260930077.0,ns,43980 +announce-response-to-bytes,,,,,260480994.0,ns,44040 +announce-response-to-bytes,,,,,268190804.0,ns,44100 +announce-response-to-bytes,,,,,275163278.0,ns,44160 +announce-response-to-bytes,,,,,257189788.0,ns,44220 +announce-response-to-bytes,,,,,274760615.0,ns,44280 +announce-response-to-bytes,,,,,263715932.0,ns,44340 +announce-response-to-bytes,,,,,263169339.0,ns,44400 +announce-response-to-bytes,,,,,269287474.0,ns,44460 +announce-response-to-bytes,,,,,261974850.0,ns,44520 +announce-response-to-bytes,,,,,269456011.0,ns,44580 +announce-response-to-bytes,,,,,277933640.0,ns,44640 +announce-response-to-bytes,,,,,269086752.0,ns,44700 +announce-response-to-bytes,,,,,271987948.0,ns,44760 +announce-response-to-bytes,,,,,283330636.0,ns,44820 +announce-response-to-bytes,,,,,289518650.0,ns,44880 +announce-response-to-bytes,,,,,278448083.0,ns,44940 +announce-response-to-bytes,,,,,271058379.0,ns,45000 +announce-response-to-bytes,,,,,270341134.0,ns,45060 +announce-response-to-bytes,,,,,271941098.0,ns,45120 +announce-response-to-bytes,,,,,271815395.0,ns,45180 +announce-response-to-bytes,,,,,268211292.0,ns,45240 +announce-response-to-bytes,,,,,280316141.0,ns,45300 +announce-response-to-bytes,,,,,265182951.0,ns,45360 +announce-response-to-bytes,,,,,268338938.0,ns,45420 +announce-response-to-bytes,,,,,270876463.0,ns,45480 +announce-response-to-bytes,,,,,285202148.0,ns,45540 +announce-response-to-bytes,,,,,266762413.0,ns,45600 +announce-response-to-bytes,,,,,271622358.0,ns,45660 +announce-response-to-bytes,,,,,265310603.0,ns,45720 +announce-response-to-bytes,,,,,274166866.0,ns,45780 +announce-response-to-bytes,,,,,313757030.0,ns,45840 +announce-response-to-bytes,,,,,274574971.0,ns,45900 +announce-response-to-bytes,,,,,268780323.0,ns,45960 +announce-response-to-bytes,,,,,271764853.0,ns,46020 +announce-response-to-bytes,,,,,282119789.0,ns,46080 +announce-response-to-bytes,,,,,274607714.0,ns,46140 +announce-response-to-bytes,,,,,284287820.0,ns,46200 +announce-response-to-bytes,,,,,283067666.0,ns,46260 +announce-response-to-bytes,,,,,271973616.0,ns,46320 +announce-response-to-bytes,,,,,282605002.0,ns,46380 +announce-response-to-bytes,,,,,281195193.0,ns,46440 +announce-response-to-bytes,,,,,389020687.0,ns,46500 +announce-response-to-bytes,,,,,276468678.0,ns,46560 +announce-response-to-bytes,,,,,280254077.0,ns,46620 +announce-response-to-bytes,,,,,280098407.0,ns,46680 +announce-response-to-bytes,,,,,281379735.0,ns,46740 +announce-response-to-bytes,,,,,279057770.0,ns,46800 +announce-response-to-bytes,,,,,285895701.0,ns,46860 +announce-response-to-bytes,,,,,281666100.0,ns,46920 +announce-response-to-bytes,,,,,293902911.0,ns,46980 +announce-response-to-bytes,,,,,274728510.0,ns,47040 +announce-response-to-bytes,,,,,294080003.0,ns,47100 +announce-response-to-bytes,,,,,311043667.0,ns,47160 +announce-response-to-bytes,,,,,287378910.0,ns,47220 +announce-response-to-bytes,,,,,284815344.0,ns,47280 +announce-response-to-bytes,,,,,284364077.0,ns,47340 +announce-response-to-bytes,,,,,369132577.0,ns,47400 +announce-response-to-bytes,,,,,316025584.0,ns,47460 +announce-response-to-bytes,,,,,282526026.0,ns,47520 +announce-response-to-bytes,,,,,282006868.0,ns,47580 +announce-response-to-bytes,,,,,290662945.0,ns,47640 +announce-response-to-bytes,,,,,290477456.0,ns,47700 +announce-response-to-bytes,,,,,321625408.0,ns,47760 +announce-response-to-bytes,,,,,286299630.0,ns,47820 +announce-response-to-bytes,,,,,286521324.0,ns,47880 +announce-response-to-bytes,,,,,285298188.0,ns,47940 +announce-response-to-bytes,,,,,295685881.0,ns,48000 +announce-response-to-bytes,,,,,282373721.0,ns,48060 +announce-response-to-bytes,,,,,283445682.0,ns,48120 +announce-response-to-bytes,,,,,292256298.0,ns,48180 +announce-response-to-bytes,,,,,287910020.0,ns,48240 +announce-response-to-bytes,,,,,287099650.0,ns,48300 +announce-response-to-bytes,,,,,277608413.0,ns,48360 +announce-response-to-bytes,,,,,283067738.0,ns,48420 +announce-response-to-bytes,,,,,292124363.0,ns,48480 +announce-response-to-bytes,,,,,287813791.0,ns,48540 +announce-response-to-bytes,,,,,300906508.0,ns,48600 +announce-response-to-bytes,,,,,294467741.0,ns,48660 +announce-response-to-bytes,,,,,303355005.0,ns,48720 +announce-response-to-bytes,,,,,302058116.0,ns,48780 +announce-response-to-bytes,,,,,297135753.0,ns,48840 +announce-response-to-bytes,,,,,288325717.0,ns,48900 +announce-response-to-bytes,,,,,294359601.0,ns,48960 +announce-response-to-bytes,,,,,300080962.0,ns,49020 +announce-response-to-bytes,,,,,299083670.0,ns,49080 +announce-response-to-bytes,,,,,293371873.0,ns,49140 +announce-response-to-bytes,,,,,297886245.0,ns,49200 +announce-response-to-bytes,,,,,297431233.0,ns,49260 +announce-response-to-bytes,,,,,294273354.0,ns,49320 +announce-response-to-bytes,,,,,300048156.0,ns,49380 +announce-response-to-bytes,,,,,303637931.0,ns,49440 +announce-response-to-bytes,,,,,291347663.0,ns,49500 +announce-response-to-bytes,,,,,335974384.0,ns,49560 +announce-response-to-bytes,,,,,313198315.0,ns,49620 +announce-response-to-bytes,,,,,289354179.0,ns,49680 +announce-response-to-bytes,,,,,295992362.0,ns,49740 +announce-response-to-bytes,,,,,301246860.0,ns,49800 +announce-response-to-bytes,,,,,298412796.0,ns,49860 +announce-response-to-bytes,,,,,318795742.0,ns,49920 +announce-response-to-bytes,,,,,292399688.0,ns,49980 +announce-response-to-bytes,,,,,297991406.0,ns,50040 +announce-response-to-bytes,,,,,292326321.0,ns,50100 +announce-response-to-bytes,,,,,300161270.0,ns,50160 +announce-response-to-bytes,,,,,302787839.0,ns,50220 +announce-response-to-bytes,,,,,300045666.0,ns,50280 +announce-response-to-bytes,,,,,297411178.0,ns,50340 +announce-response-to-bytes,,,,,309078520.0,ns,50400 +announce-response-to-bytes,,,,,305601268.0,ns,50460 +announce-response-to-bytes,,,,,305462857.0,ns,50520 +announce-response-to-bytes,,,,,303773871.0,ns,50580 +announce-response-to-bytes,,,,,305515363.0,ns,50640 +announce-response-to-bytes,,,,,300744123.0,ns,50700 +announce-response-to-bytes,,,,,314799052.0,ns,50760 +announce-response-to-bytes,,,,,315048857.0,ns,50820 +announce-response-to-bytes,,,,,301722542.0,ns,50880 +announce-response-to-bytes,,,,,336118081.0,ns,50940 +announce-response-to-bytes,,,,,319919675.0,ns,51000 +announce-response-to-bytes,,,,,298397020.0,ns,51060 +announce-response-to-bytes,,,,,314181714.0,ns,51120 +announce-response-to-bytes,,,,,301856990.0,ns,51180 +announce-response-to-bytes,,,,,304503364.0,ns,51240 +announce-response-to-bytes,,,,,313141862.0,ns,51300 +announce-response-to-bytes,,,,,307025724.0,ns,51360 +announce-response-to-bytes,,,,,305605998.0,ns,51420 +announce-response-to-bytes,,,,,404539381.0,ns,51480 +announce-response-to-bytes,,,,,329821145.0,ns,51540 +announce-response-to-bytes,,,,,314487816.0,ns,51600 +announce-response-to-bytes,,,,,339887265.0,ns,51660 +announce-response-to-bytes,,,,,302004941.0,ns,51720 +announce-response-to-bytes,,,,,350100426.0,ns,51780 +announce-response-to-bytes,,,,,312518724.0,ns,51840 +announce-response-to-bytes,,,,,315564580.0,ns,51900 +announce-response-to-bytes,,,,,312270489.0,ns,51960 +announce-response-to-bytes,,,,,316227393.0,ns,52020 +announce-response-to-bytes,,,,,312365033.0,ns,52080 +announce-response-to-bytes,,,,,323851542.0,ns,52140 +announce-response-to-bytes,,,,,312795001.0,ns,52200 +announce-response-to-bytes,,,,,302897028.0,ns,52260 +announce-response-to-bytes,,,,,317863494.0,ns,52320 +announce-response-to-bytes,,,,,309840434.0,ns,52380 +announce-response-to-bytes,,,,,316297260.0,ns,52440 +announce-response-to-bytes,,,,,319540709.0,ns,52500 +announce-response-to-bytes,,,,,324936975.0,ns,52560 +announce-response-to-bytes,,,,,319602399.0,ns,52620 +announce-response-to-bytes,,,,,323924176.0,ns,52680 +announce-response-to-bytes,,,,,313390828.0,ns,52740 +announce-response-to-bytes,,,,,313985662.0,ns,52800 +announce-response-to-bytes,,,,,346410163.0,ns,52860 +announce-response-to-bytes,,,,,319438353.0,ns,52920 +announce-response-to-bytes,,,,,308565021.0,ns,52980 +announce-response-to-bytes,,,,,319762707.0,ns,53040 +announce-response-to-bytes,,,,,315514943.0,ns,53100 +announce-response-to-bytes,,,,,324814748.0,ns,53160 +announce-response-to-bytes,,,,,328862605.0,ns,53220 +announce-response-to-bytes,,,,,328702242.0,ns,53280 +announce-response-to-bytes,,,,,323416999.0,ns,53340 +announce-response-to-bytes,,,,,317966329.0,ns,53400 +announce-response-to-bytes,,,,,320855466.0,ns,53460 +announce-response-to-bytes,,,,,309819185.0,ns,53520 +announce-response-to-bytes,,,,,318216724.0,ns,53580 +announce-response-to-bytes,,,,,317749317.0,ns,53640 +announce-response-to-bytes,,,,,318602549.0,ns,53700 +announce-response-to-bytes,,,,,316605063.0,ns,53760 +announce-response-to-bytes,,,,,326387161.0,ns,53820 +announce-response-to-bytes,,,,,326173317.0,ns,53880 +announce-response-to-bytes,,,,,323307159.0,ns,53940 +announce-response-to-bytes,,,,,333145375.0,ns,54000 +announce-response-to-bytes,,,,,330659004.0,ns,54060 +announce-response-to-bytes,,,,,324638549.0,ns,54120 +announce-response-to-bytes,,,,,328551004.0,ns,54180 +announce-response-to-bytes,,,,,360411674.0,ns,54240 +announce-response-to-bytes,,,,,319583115.0,ns,54300 +announce-response-to-bytes,,,,,321396844.0,ns,54360 +announce-response-to-bytes,,,,,330429552.0,ns,54420 +announce-response-to-bytes,,,,,331303520.0,ns,54480 +announce-response-to-bytes,,,,,317777174.0,ns,54540 +announce-response-to-bytes,,,,,326289354.0,ns,54600 +announce-response-to-bytes,,,,,335576412.0,ns,54660 +announce-response-to-bytes,,,,,320091788.0,ns,54720 +announce-response-to-bytes,,,,,340748962.0,ns,54780 +announce-response-to-bytes,,,,,359449182.0,ns,54840 +announce-response-to-bytes,,,,,320545100.0,ns,54900 +announce-response-to-bytes,,,,,329242866.0,ns,54960 +announce-response-to-bytes,,,,,328717540.0,ns,55020 +announce-response-to-bytes,,,,,333308638.0,ns,55080 +announce-response-to-bytes,,,,,343464315.0,ns,55140 +announce-response-to-bytes,,,,,335664459.0,ns,55200 +announce-response-to-bytes,,,,,329551347.0,ns,55260 +announce-response-to-bytes,,,,,329985484.0,ns,55320 +announce-response-to-bytes,,,,,341252255.0,ns,55380 +announce-response-to-bytes,,,,,328448472.0,ns,55440 +announce-response-to-bytes,,,,,333703763.0,ns,55500 +announce-response-to-bytes,,,,,331097817.0,ns,55560 +announce-response-to-bytes,,,,,336448484.0,ns,55620 +announce-response-to-bytes,,,,,330592394.0,ns,55680 +announce-response-to-bytes,,,,,344895917.0,ns,55740 +announce-response-to-bytes,,,,,356668019.0,ns,55800 +announce-response-to-bytes,,,,,342709971.0,ns,55860 +announce-response-to-bytes,,,,,327625100.0,ns,55920 +announce-response-to-bytes,,,,,333108689.0,ns,55980 +announce-response-to-bytes,,,,,329688400.0,ns,56040 +announce-response-to-bytes,,,,,336159556.0,ns,56100 +announce-response-to-bytes,,,,,341686482.0,ns,56160 +announce-response-to-bytes,,,,,374196060.0,ns,56220 +announce-response-to-bytes,,,,,332211028.0,ns,56280 +announce-response-to-bytes,,,,,345491228.0,ns,56340 +announce-response-to-bytes,,,,,451004770.0,ns,56400 +announce-response-to-bytes,,,,,330083009.0,ns,56460 +announce-response-to-bytes,,,,,338746057.0,ns,56520 +announce-response-to-bytes,,,,,345370663.0,ns,56580 +announce-response-to-bytes,,,,,338524240.0,ns,56640 +announce-response-to-bytes,,,,,333771257.0,ns,56700 +announce-response-to-bytes,,,,,337370438.0,ns,56760 +announce-response-to-bytes,,,,,350628815.0,ns,56820 +announce-response-to-bytes,,,,,340063883.0,ns,56880 +announce-response-to-bytes,,,,,348294684.0,ns,56940 +announce-response-to-bytes,,,,,334322147.0,ns,57000 +announce-response-to-bytes,,,,,351462743.0,ns,57060 +announce-response-to-bytes,,,,,343985641.0,ns,57120 +announce-response-to-bytes,,,,,337634287.0,ns,57180 +announce-response-to-bytes,,,,,346360827.0,ns,57240 +announce-response-to-bytes,,,,,351385487.0,ns,57300 +announce-response-to-bytes,,,,,337014112.0,ns,57360 +announce-response-to-bytes,,,,,337968758.0,ns,57420 +announce-response-to-bytes,,,,,343933497.0,ns,57480 +announce-response-to-bytes,,,,,377889962.0,ns,57540 +announce-response-to-bytes,,,,,364110101.0,ns,57600 +announce-response-to-bytes,,,,,359452951.0,ns,57660 +announce-response-to-bytes,,,,,346597895.0,ns,57720 +announce-response-to-bytes,,,,,354849417.0,ns,57780 +announce-response-to-bytes,,,,,342847496.0,ns,57840 +announce-response-to-bytes,,,,,457551549.0,ns,57900 +announce-response-to-bytes,,,,,343396183.0,ns,57960 +announce-response-to-bytes,,,,,349580716.0,ns,58020 +announce-response-to-bytes,,,,,345427379.0,ns,58080 +announce-response-to-bytes,,,,,372504256.0,ns,58140 +announce-response-to-bytes,,,,,353939468.0,ns,58200 +announce-response-to-bytes,,,,,346426932.0,ns,58260 +announce-response-to-bytes,,,,,352422324.0,ns,58320 +announce-response-to-bytes,,,,,385265078.0,ns,58380 +announce-response-to-bytes,,,,,348400466.0,ns,58440 +announce-response-to-bytes,,,,,354741369.0,ns,58500 +announce-response-to-bytes,,,,,367264389.0,ns,58560 +announce-response-to-bytes,,,,,357025872.0,ns,58620 +announce-response-to-bytes,,,,,355555903.0,ns,58680 +announce-response-to-bytes,,,,,352633987.0,ns,58740 +announce-response-to-bytes,,,,,361858537.0,ns,58800 +announce-response-to-bytes,,,,,345121713.0,ns,58860 +announce-response-to-bytes,,,,,340087431.0,ns,58920 +announce-response-to-bytes,,,,,347626178.0,ns,58980 +announce-response-to-bytes,,,,,353849107.0,ns,59040 +announce-response-to-bytes,,,,,362700723.0,ns,59100 +announce-response-to-bytes,,,,,363318834.0,ns,59160 +announce-response-to-bytes,,,,,350700411.0,ns,59220 +announce-response-to-bytes,,,,,352156126.0,ns,59280 +announce-response-to-bytes,,,,,367802511.0,ns,59340 +announce-response-to-bytes,,,,,363201106.0,ns,59400 +announce-response-to-bytes,,,,,357230944.0,ns,59460 +announce-response-to-bytes,,,,,359225756.0,ns,59520 +announce-response-to-bytes,,,,,350581831.0,ns,59580 +announce-response-to-bytes,,,,,359627543.0,ns,59640 +announce-response-to-bytes,,,,,357452853.0,ns,59700 +announce-response-to-bytes,,,,,348215503.0,ns,59760 +announce-response-to-bytes,,,,,354936586.0,ns,59820 +announce-response-to-bytes,,,,,352906566.0,ns,59880 +announce-response-to-bytes,,,,,372134842.0,ns,59940 +announce-response-to-bytes,,,,,362584548.0,ns,60000 diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/sample.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/sample.json new file mode 100644 index 0000000..6593e3d --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/sample.json @@ -0,0 +1 @@ +{"sampling_mode":"Linear","iters":[60.0,120.0,180.0,240.0,300.0,360.0,420.0,480.0,540.0,600.0,660.0,720.0,780.0,840.0,900.0,960.0,1020.0,1080.0,1140.0,1200.0,1260.0,1320.0,1380.0,1440.0,1500.0,1560.0,1620.0,1680.0,1740.0,1800.0,1860.0,1920.0,1980.0,2040.0,2100.0,2160.0,2220.0,2280.0,2340.0,2400.0,2460.0,2520.0,2580.0,2640.0,2700.0,2760.0,2820.0,2880.0,2940.0,3000.0,3060.0,3120.0,3180.0,3240.0,3300.0,3360.0,3420.0,3480.0,3540.0,3600.0,3660.0,3720.0,3780.0,3840.0,3900.0,3960.0,4020.0,4080.0,4140.0,4200.0,4260.0,4320.0,4380.0,4440.0,4500.0,4560.0,4620.0,4680.0,4740.0,4800.0,4860.0,4920.0,4980.0,5040.0,5100.0,5160.0,5220.0,5280.0,5340.0,5400.0,5460.0,5520.0,5580.0,5640.0,5700.0,5760.0,5820.0,5880.0,5940.0,6000.0,6060.0,6120.0,6180.0,6240.0,6300.0,6360.0,6420.0,6480.0,6540.0,6600.0,6660.0,6720.0,6780.0,6840.0,6900.0,6960.0,7020.0,7080.0,7140.0,7200.0,7260.0,7320.0,7380.0,7440.0,7500.0,7560.0,7620.0,7680.0,7740.0,7800.0,7860.0,7920.0,7980.0,8040.0,8100.0,8160.0,8220.0,8280.0,8340.0,8400.0,8460.0,8520.0,8580.0,8640.0,8700.0,8760.0,8820.0,8880.0,8940.0,9000.0,9060.0,9120.0,9180.0,9240.0,9300.0,9360.0,9420.0,9480.0,9540.0,9600.0,9660.0,9720.0,9780.0,9840.0,9900.0,9960.0,10020.0,10080.0,10140.0,10200.0,10260.0,10320.0,10380.0,10440.0,10500.0,10560.0,10620.0,10680.0,10740.0,10800.0,10860.0,10920.0,10980.0,11040.0,11100.0,11160.0,11220.0,11280.0,11340.0,11400.0,11460.0,11520.0,11580.0,11640.0,11700.0,11760.0,11820.0,11880.0,11940.0,12000.0,12060.0,12120.0,12180.0,12240.0,12300.0,12360.0,12420.0,12480.0,12540.0,12600.0,12660.0,12720.0,12780.0,12840.0,12900.0,12960.0,13020.0,13080.0,13140.0,13200.0,13260.0,13320.0,13380.0,13440.0,13500.0,13560.0,13620.0,13680.0,13740.0,13800.0,13860.0,13920.0,13980.0,14040.0,14100.0,14160.0,14220.0,14280.0,14340.0,14400.0,14460.0,14520.0,14580.0,14640.0,14700.0,14760.0,14820.0,14880.0,14940.0,15000.0,15060.0,15120.0,15180.0,15240.0,15300.0,15360.0,15420.0,15480.0,15540.0,15600.0,15660.0,15720.0,15780.0,15840.0,15900.0,15960.0,16020.0,16080.0,16140.0,16200.0,16260.0,16320.0,16380.0,16440.0,16500.0,16560.0,16620.0,16680.0,16740.0,16800.0,16860.0,16920.0,16980.0,17040.0,17100.0,17160.0,17220.0,17280.0,17340.0,17400.0,17460.0,17520.0,17580.0,17640.0,17700.0,17760.0,17820.0,17880.0,17940.0,18000.0,18060.0,18120.0,18180.0,18240.0,18300.0,18360.0,18420.0,18480.0,18540.0,18600.0,18660.0,18720.0,18780.0,18840.0,18900.0,18960.0,19020.0,19080.0,19140.0,19200.0,19260.0,19320.0,19380.0,19440.0,19500.0,19560.0,19620.0,19680.0,19740.0,19800.0,19860.0,19920.0,19980.0,20040.0,20100.0,20160.0,20220.0,20280.0,20340.0,20400.0,20460.0,20520.0,20580.0,20640.0,20700.0,20760.0,20820.0,20880.0,20940.0,21000.0,21060.0,21120.0,21180.0,21240.0,21300.0,21360.0,21420.0,21480.0,21540.0,21600.0,21660.0,21720.0,21780.0,21840.0,21900.0,21960.0,22020.0,22080.0,22140.0,22200.0,22260.0,22320.0,22380.0,22440.0,22500.0,22560.0,22620.0,22680.0,22740.0,22800.0,22860.0,22920.0,22980.0,23040.0,23100.0,23160.0,23220.0,23280.0,23340.0,23400.0,23460.0,23520.0,23580.0,23640.0,23700.0,23760.0,23820.0,23880.0,23940.0,24000.0,24060.0,24120.0,24180.0,24240.0,24300.0,24360.0,24420.0,24480.0,24540.0,24600.0,24660.0,24720.0,24780.0,24840.0,24900.0,24960.0,25020.0,25080.0,25140.0,25200.0,25260.0,25320.0,25380.0,25440.0,25500.0,25560.0,25620.0,25680.0,25740.0,25800.0,25860.0,25920.0,25980.0,26040.0,26100.0,26160.0,26220.0,26280.0,26340.0,26400.0,26460.0,26520.0,26580.0,26640.0,26700.0,26760.0,26820.0,26880.0,26940.0,27000.0,27060.0,27120.0,27180.0,27240.0,27300.0,27360.0,27420.0,27480.0,27540.0,27600.0,27660.0,27720.0,27780.0,27840.0,27900.0,27960.0,28020.0,28080.0,28140.0,28200.0,28260.0,28320.0,28380.0,28440.0,28500.0,28560.0,28620.0,28680.0,28740.0,28800.0,28860.0,28920.0,28980.0,29040.0,29100.0,29160.0,29220.0,29280.0,29340.0,29400.0,29460.0,29520.0,29580.0,29640.0,29700.0,29760.0,29820.0,29880.0,29940.0,30000.0,30060.0,30120.0,30180.0,30240.0,30300.0,30360.0,30420.0,30480.0,30540.0,30600.0,30660.0,30720.0,30780.0,30840.0,30900.0,30960.0,31020.0,31080.0,31140.0,31200.0,31260.0,31320.0,31380.0,31440.0,31500.0,31560.0,31620.0,31680.0,31740.0,31800.0,31860.0,31920.0,31980.0,32040.0,32100.0,32160.0,32220.0,32280.0,32340.0,32400.0,32460.0,32520.0,32580.0,32640.0,32700.0,32760.0,32820.0,32880.0,32940.0,33000.0,33060.0,33120.0,33180.0,33240.0,33300.0,33360.0,33420.0,33480.0,33540.0,33600.0,33660.0,33720.0,33780.0,33840.0,33900.0,33960.0,34020.0,34080.0,34140.0,34200.0,34260.0,34320.0,34380.0,34440.0,34500.0,34560.0,34620.0,34680.0,34740.0,34800.0,34860.0,34920.0,34980.0,35040.0,35100.0,35160.0,35220.0,35280.0,35340.0,35400.0,35460.0,35520.0,35580.0,35640.0,35700.0,35760.0,35820.0,35880.0,35940.0,36000.0,36060.0,36120.0,36180.0,36240.0,36300.0,36360.0,36420.0,36480.0,36540.0,36600.0,36660.0,36720.0,36780.0,36840.0,36900.0,36960.0,37020.0,37080.0,37140.0,37200.0,37260.0,37320.0,37380.0,37440.0,37500.0,37560.0,37620.0,37680.0,37740.0,37800.0,37860.0,37920.0,37980.0,38040.0,38100.0,38160.0,38220.0,38280.0,38340.0,38400.0,38460.0,38520.0,38580.0,38640.0,38700.0,38760.0,38820.0,38880.0,38940.0,39000.0,39060.0,39120.0,39180.0,39240.0,39300.0,39360.0,39420.0,39480.0,39540.0,39600.0,39660.0,39720.0,39780.0,39840.0,39900.0,39960.0,40020.0,40080.0,40140.0,40200.0,40260.0,40320.0,40380.0,40440.0,40500.0,40560.0,40620.0,40680.0,40740.0,40800.0,40860.0,40920.0,40980.0,41040.0,41100.0,41160.0,41220.0,41280.0,41340.0,41400.0,41460.0,41520.0,41580.0,41640.0,41700.0,41760.0,41820.0,41880.0,41940.0,42000.0,42060.0,42120.0,42180.0,42240.0,42300.0,42360.0,42420.0,42480.0,42540.0,42600.0,42660.0,42720.0,42780.0,42840.0,42900.0,42960.0,43020.0,43080.0,43140.0,43200.0,43260.0,43320.0,43380.0,43440.0,43500.0,43560.0,43620.0,43680.0,43740.0,43800.0,43860.0,43920.0,43980.0,44040.0,44100.0,44160.0,44220.0,44280.0,44340.0,44400.0,44460.0,44520.0,44580.0,44640.0,44700.0,44760.0,44820.0,44880.0,44940.0,45000.0,45060.0,45120.0,45180.0,45240.0,45300.0,45360.0,45420.0,45480.0,45540.0,45600.0,45660.0,45720.0,45780.0,45840.0,45900.0,45960.0,46020.0,46080.0,46140.0,46200.0,46260.0,46320.0,46380.0,46440.0,46500.0,46560.0,46620.0,46680.0,46740.0,46800.0,46860.0,46920.0,46980.0,47040.0,47100.0,47160.0,47220.0,47280.0,47340.0,47400.0,47460.0,47520.0,47580.0,47640.0,47700.0,47760.0,47820.0,47880.0,47940.0,48000.0,48060.0,48120.0,48180.0,48240.0,48300.0,48360.0,48420.0,48480.0,48540.0,48600.0,48660.0,48720.0,48780.0,48840.0,48900.0,48960.0,49020.0,49080.0,49140.0,49200.0,49260.0,49320.0,49380.0,49440.0,49500.0,49560.0,49620.0,49680.0,49740.0,49800.0,49860.0,49920.0,49980.0,50040.0,50100.0,50160.0,50220.0,50280.0,50340.0,50400.0,50460.0,50520.0,50580.0,50640.0,50700.0,50760.0,50820.0,50880.0,50940.0,51000.0,51060.0,51120.0,51180.0,51240.0,51300.0,51360.0,51420.0,51480.0,51540.0,51600.0,51660.0,51720.0,51780.0,51840.0,51900.0,51960.0,52020.0,52080.0,52140.0,52200.0,52260.0,52320.0,52380.0,52440.0,52500.0,52560.0,52620.0,52680.0,52740.0,52800.0,52860.0,52920.0,52980.0,53040.0,53100.0,53160.0,53220.0,53280.0,53340.0,53400.0,53460.0,53520.0,53580.0,53640.0,53700.0,53760.0,53820.0,53880.0,53940.0,54000.0,54060.0,54120.0,54180.0,54240.0,54300.0,54360.0,54420.0,54480.0,54540.0,54600.0,54660.0,54720.0,54780.0,54840.0,54900.0,54960.0,55020.0,55080.0,55140.0,55200.0,55260.0,55320.0,55380.0,55440.0,55500.0,55560.0,55620.0,55680.0,55740.0,55800.0,55860.0,55920.0,55980.0,56040.0,56100.0,56160.0,56220.0,56280.0,56340.0,56400.0,56460.0,56520.0,56580.0,56640.0,56700.0,56760.0,56820.0,56880.0,56940.0,57000.0,57060.0,57120.0,57180.0,57240.0,57300.0,57360.0,57420.0,57480.0,57540.0,57600.0,57660.0,57720.0,57780.0,57840.0,57900.0,57960.0,58020.0,58080.0,58140.0,58200.0,58260.0,58320.0,58380.0,58440.0,58500.0,58560.0,58620.0,58680.0,58740.0,58800.0,58860.0,58920.0,58980.0,59040.0,59100.0,59160.0,59220.0,59280.0,59340.0,59400.0,59460.0,59520.0,59580.0,59640.0,59700.0,59760.0,59820.0,59880.0,59940.0,60000.0],"times":[353477.0,671839.0,1043769.0,1387044.0,1817564.0,2091668.0,2412910.0,2765877.0,3138818.0,3405635.0,3762510.0,4223891.0,4500944.0,4843678.0,5250885.0,5554563.0,5991116.0,6222664.0,6607356.0,6983522.0,8637887.0,8257978.0,7885038.0,8335095.0,8453124.0,8877779.0,9232114.0,9442849.0,10166530.0,10433222.0,10610175.0,11098298.0,14430476.0,12476277.0,12170643.0,15124300.0,13437874.0,12969128.0,13374407.0,13739441.0,14325178.0,14580725.0,17250927.0,16247807.0,15355145.0,15896458.0,16275307.0,16657808.0,16926589.0,17188926.0,17553302.0,18017365.0,18350516.0,18618363.0,18959740.0,19458262.0,19782865.0,23204104.0,20600613.0,20851087.0,21147790.0,21497480.0,21894629.0,22191943.0,26305230.0,22622414.0,23043038.0,24937566.0,23590074.0,24082522.0,24503129.0,27898991.0,25347663.0,25279202.0,29040732.0,25888104.0,26296051.0,26806933.0,32482311.0,32351207.0,30751181.0,28927226.0,28709346.0,29360592.0,32860228.0,29502673.0,32219011.0,34821885.0,33800622.0,33691953.0,35518131.0,33432340.0,34138684.0,32795340.0,35739999.0,33238758.0,36316533.0,33731463.0,34353777.0,35905258.0,38421003.0,38132903.0,36875710.0,36505413.0,36087596.0,37271903.0,37049958.0,40472064.0,41263730.0,37805192.0,38560967.0,38525118.0,42311399.0,42629719.0,40046757.0,39928551.0,43662024.0,41848673.0,41200469.0,41534287.0,41858123.0,45388067.0,44341923.0,44438842.0,46568539.0,44268013.0,43035579.0,44147832.0,55676747.0,46128532.0,48995123.0,49011107.0,49542556.0,46641470.0,46716072.0,48215559.0,49182187.0,47115008.0,47786180.0,55059536.0,50928893.0,48943719.0,49857201.0,53184689.0,60794127.0,50630441.0,54408127.0,51832713.0,53006034.0,64631314.0,52020623.0,56213004.0,52489138.0,53166450.0,56725165.0,56515953.0,53709942.0,54206409.0,56132382.0,58787885.0,62005408.0,56197378.0,56754534.0,57732334.0,60488688.0,60813225.0,57858522.0,58081463.0,61699888.0,62126469.0,59116459.0,63003805.0,60041341.0,66409481.0,60196749.0,63922988.0,60792574.0,64647668.0,61968715.0,65233419.0,71584918.0,66039537.0,63385630.0,68178873.0,65536982.0,67026550.0,69538573.0,66967027.0,67869989.0,82856095.0,82866466.0,72843625.0,72963878.0,71642696.0,69044029.0,72647288.0,67811715.0,72873251.0,69017873.0,75812336.0,71771513.0,75758429.0,70046417.0,73361092.0,73157993.0,78841560.0,71399961.0,74108353.0,74334975.0,74436359.0,77091056.0,73771256.0,73564834.0,79530338.0,84150804.0,75369968.0,77998120.0,76311466.0,75641518.0,79147069.0,80920147.0,79193771.0,83960644.0,81067454.0,84880079.0,77739006.0,82089795.0,82231275.0,83064916.0,82497784.0,83078740.0,86587154.0,80832370.0,84452136.0,80858264.0,88484934.0,85169320.0,83328677.0,89078676.0,82971370.0,87819952.0,84783546.0,89132177.0,85943628.0,87947035.0,88051952.0,86239807.0,89225024.0,90633440.0,89835662.0,86437593.0,95231827.0,87386235.0,90953717.0,99166999.0,93967708.0,89227799.0,95577648.0,92196890.0,90557507.0,93476159.0,102579136.0,97899948.0,98757931.0,96493485.0,104839437.0,93105475.0,96023558.0,109098464.0,96076451.0,93665090.0,103787277.0,94851510.0,101810430.0,95449054.0,99077967.0,105226577.0,98648289.0,96444291.0,101439485.0,100060710.0,98393281.0,100751746.0,101105000.0,102050246.0,98735599.0,104729218.0,101191327.0,99354741.0,103109972.0,106228511.0,104629383.0,104352162.0,108720711.0,105524991.0,111982358.0,108853889.0,106385675.0,109944044.0,106491227.0,107169087.0,103402650.0,105528215.0,106408989.0,118011891.0,108615001.0,108163576.0,115425723.0,106457390.0,112654377.0,114332495.0,115722798.0,113319709.0,112467215.0,131530065.0,112779834.0,113133784.0,116680560.0,120117889.0,114154506.0,116600877.0,111499012.0,115148520.0,119519846.0,121352443.0,116141216.0,120118044.0,117208814.0,118901147.0,118241424.0,117654664.0,116226405.0,116840915.0,122639608.0,119442677.0,122998235.0,116123143.0,116872198.0,138102237.0,128437710.0,121137265.0,126880043.0,122836585.0,216570089.0,141742390.0,128668059.0,130583224.0,123696240.0,130653847.0,123301000.0,124609971.0,132437379.0,128333900.0,125820573.0,132504190.0,132424433.0,129169050.0,130513574.0,128951601.0,128100399.0,127990454.0,137819400.0,144746386.0,216431912.0,150020152.0,135441027.0,130994307.0,133180497.0,127115197.0,131512985.0,129470937.0,132878166.0,138877962.0,134822962.0,129266707.0,132254185.0,133606223.0,136628818.0,135017238.0,134922705.0,131398549.0,136184490.0,136662292.0,139084616.0,142374414.0,139672055.0,139665680.0,140454153.0,134045293.0,143119691.0,144157458.0,142513360.0,145299170.0,145304718.0,143137147.0,150700164.0,143088363.0,140659241.0,151175740.0,149559608.0,141507307.0,142098061.0,142306098.0,145560035.0,145551983.0,154656278.0,146982431.0,149980678.0,149811257.0,144410416.0,145107309.0,148069434.0,147743674.0,158591021.0,153804335.0,151333913.0,147205551.0,151011082.0,150994405.0,151583075.0,150962534.0,158942748.0,153943407.0,152188911.0,157514396.0,155180550.0,152504791.0,160079543.0,151453512.0,151384295.0,156963082.0,150828531.0,157597601.0,158605060.0,154264234.0,157418039.0,157380542.0,154039954.0,155847797.0,159540498.0,158636199.0,162123392.0,160133693.0,167252226.0,157875477.0,160387414.0,159725786.0,159519574.0,159144790.0,159724882.0,159626097.0,167716074.0,165325796.0,159454651.0,166192327.0,164141388.0,159827333.0,159281575.0,161338829.0,201312225.0,173052547.0,174872126.0,162826556.0,166301823.0,172216250.0,164447879.0,165765095.0,165112499.0,172573539.0,168652965.0,195098828.0,182327486.0,170434022.0,169223601.0,162751851.0,179716421.0,171936500.0,168791350.0,172923539.0,173294241.0,178158947.0,170223356.0,174141701.0,181654398.0,173452627.0,176756076.0,173841937.0,182670206.0,182615059.0,183366222.0,176423114.0,179346613.0,179672994.0,187219070.0,182825189.0,175152698.0,178268199.0,174561075.0,174990534.0,177516663.0,181544897.0,176721962.0,180219494.0,180313631.0,186697851.0,183867116.0,178658999.0,185147892.0,178798904.0,182805520.0,185003086.0,186256141.0,191635803.0,183086863.0,184010410.0,178541981.0,183182832.0,186382283.0,190163970.0,189246648.0,189380527.0,188107590.0,186459717.0,190261961.0,182543672.0,187437911.0,295643192.0,189866177.0,187710997.0,190784712.0,188294138.0,197609140.0,191207744.0,194321027.0,191733731.0,185822221.0,200121480.0,188778779.0,189413962.0,230938799.0,190683965.0,195883270.0,193999872.0,201550915.0,198591497.0,197251415.0,192361413.0,192777082.0,203604542.0,195817030.0,198903617.0,203230503.0,191594234.0,202084594.0,204345613.0,196837662.0,197965331.0,202817373.0,209534403.0,200458645.0,205032826.0,198179128.0,197422710.0,201316422.0,205412634.0,201826245.0,199264327.0,206017461.0,208321075.0,205711694.0,207475384.0,202232325.0,201545478.0,202727542.0,205538690.0,217018134.0,210273035.0,208041159.0,207455945.0,207251814.0,209859297.0,209800938.0,205054440.0,205186042.0,206908202.0,250211709.0,207711175.0,213023350.0,213613526.0,212993128.0,214689251.0,210626846.0,213075462.0,211900429.0,210183891.0,217957022.0,221709232.0,219281524.0,215445563.0,236385901.0,223709858.0,221003707.0,211281157.0,214120029.0,215784498.0,220203496.0,227515929.0,211449430.0,222240876.0,219946447.0,217701524.0,217260772.0,221257631.0,228530271.0,222466784.0,215783231.0,224538305.0,228788087.0,222543752.0,235717001.0,217727435.0,218742676.0,218327248.0,253768428.0,224585715.0,322646119.0,236805536.0,220951586.0,229392071.0,227137335.0,227661592.0,257438175.0,224997497.0,231055413.0,243283033.0,225608551.0,230858718.0,236236544.0,247652247.0,228653425.0,227498067.0,221423660.0,229075019.0,232666354.0,228996472.0,244834606.0,233992244.0,222921409.0,225773687.0,227002461.0,237459910.0,236495788.0,231470987.0,239653523.0,233924817.0,235958944.0,234033523.0,243227066.0,233884565.0,244863425.0,237975552.0,232741349.0,240014376.0,239146388.0,234852762.0,257608891.0,236265551.0,247460522.0,240313882.0,239048596.0,242063447.0,231511769.0,232216223.0,245886303.0,250613484.0,237017839.0,251563100.0,238530772.0,236810668.0,253812434.0,241830203.0,246490586.0,241384498.0,249185798.0,244845846.0,247865640.0,243400028.0,245408891.0,246362124.0,248266087.0,238807274.0,253736956.0,238456260.0,253284697.0,240780177.0,243780800.0,261015261.0,264633730.0,253476753.0,249523924.0,247720589.0,242037092.0,280041661.0,254767480.0,253020123.0,250126273.0,268141541.0,252241167.0,260026538.0,246449612.0,253117221.0,257987408.0,260326828.0,254904127.0,253636375.0,257664625.0,256506942.0,253883956.0,259280149.0,260617047.0,257393607.0,260101462.0,256769317.0,343173643.0,290846207.0,254576878.0,259426287.0,259803576.0,254732236.0,263534559.0,259862273.0,271727114.0,260930077.0,260480994.0,268190804.0,275163278.0,257189788.0,274760615.0,263715932.0,263169339.0,269287474.0,261974850.0,269456011.0,277933640.0,269086752.0,271987948.0,283330636.0,289518650.0,278448083.0,271058379.0,270341134.0,271941098.0,271815395.0,268211292.0,280316141.0,265182951.0,268338938.0,270876463.0,285202148.0,266762413.0,271622358.0,265310603.0,274166866.0,313757030.0,274574971.0,268780323.0,271764853.0,282119789.0,274607714.0,284287820.0,283067666.0,271973616.0,282605002.0,281195193.0,389020687.0,276468678.0,280254077.0,280098407.0,281379735.0,279057770.0,285895701.0,281666100.0,293902911.0,274728510.0,294080003.0,311043667.0,287378910.0,284815344.0,284364077.0,369132577.0,316025584.0,282526026.0,282006868.0,290662945.0,290477456.0,321625408.0,286299630.0,286521324.0,285298188.0,295685881.0,282373721.0,283445682.0,292256298.0,287910020.0,287099650.0,277608413.0,283067738.0,292124363.0,287813791.0,300906508.0,294467741.0,303355005.0,302058116.0,297135753.0,288325717.0,294359601.0,300080962.0,299083670.0,293371873.0,297886245.0,297431233.0,294273354.0,300048156.0,303637931.0,291347663.0,335974384.0,313198315.0,289354179.0,295992362.0,301246860.0,298412796.0,318795742.0,292399688.0,297991406.0,292326321.0,300161270.0,302787839.0,300045666.0,297411178.0,309078520.0,305601268.0,305462857.0,303773871.0,305515363.0,300744123.0,314799052.0,315048857.0,301722542.0,336118081.0,319919675.0,298397020.0,314181714.0,301856990.0,304503364.0,313141862.0,307025724.0,305605998.0,404539381.0,329821145.0,314487816.0,339887265.0,302004941.0,350100426.0,312518724.0,315564580.0,312270489.0,316227393.0,312365033.0,323851542.0,312795001.0,302897028.0,317863494.0,309840434.0,316297260.0,319540709.0,324936975.0,319602399.0,323924176.0,313390828.0,313985662.0,346410163.0,319438353.0,308565021.0,319762707.0,315514943.0,324814748.0,328862605.0,328702242.0,323416999.0,317966329.0,320855466.0,309819185.0,318216724.0,317749317.0,318602549.0,316605063.0,326387161.0,326173317.0,323307159.0,333145375.0,330659004.0,324638549.0,328551004.0,360411674.0,319583115.0,321396844.0,330429552.0,331303520.0,317777174.0,326289354.0,335576412.0,320091788.0,340748962.0,359449182.0,320545100.0,329242866.0,328717540.0,333308638.0,343464315.0,335664459.0,329551347.0,329985484.0,341252255.0,328448472.0,333703763.0,331097817.0,336448484.0,330592394.0,344895917.0,356668019.0,342709971.0,327625100.0,333108689.0,329688400.0,336159556.0,341686482.0,374196060.0,332211028.0,345491228.0,451004770.0,330083009.0,338746057.0,345370663.0,338524240.0,333771257.0,337370438.0,350628815.0,340063883.0,348294684.0,334322147.0,351462743.0,343985641.0,337634287.0,346360827.0,351385487.0,337014112.0,337968758.0,343933497.0,377889962.0,364110101.0,359452951.0,346597895.0,354849417.0,342847496.0,457551549.0,343396183.0,349580716.0,345427379.0,372504256.0,353939468.0,346426932.0,352422324.0,385265078.0,348400466.0,354741369.0,367264389.0,357025872.0,355555903.0,352633987.0,361858537.0,345121713.0,340087431.0,347626178.0,353849107.0,362700723.0,363318834.0,350700411.0,352156126.0,367802511.0,363201106.0,357230944.0,359225756.0,350581831.0,359627543.0,357452853.0,348215503.0,354936586.0,352906566.0,372134842.0,362584548.0]} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/tukey.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/tukey.json new file mode 100644 index 0000000..a66cd75 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/bendy/tukey.json @@ -0,0 +1 @@ +[5184.137608004838,5534.60305616611,6469.177584596171,6819.643032757444] \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/benchmark.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/benchmark.json new file mode 100644 index 0000000..1c97482 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/benchmark.json @@ -0,0 +1 @@ +{"group_id":"announce-response-to-bytes","function_id":null,"value_str":null,"throughput":null,"full_id":"announce-response-to-bytes","directory_name":"announce-response-to-bytes","title":"announce-response-to-bytes"} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/estimates.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/estimates.json new file mode 100644 index 0000000..b952a33 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/estimates.json @@ -0,0 +1 @@ +{"mean":{"confidence_interval":{"confidence_level":0.95,"lower_bound":816.5793263757998,"upper_bound":829.8277072322014},"point_estimate":823.0324170546021,"standard_error":3.3713205895235987},"median":{"confidence_interval":{"confidence_level":0.95,"lower_bound":785.8508214740125,"upper_bound":790.3983678702459},"point_estimate":787.3168084640594,"standard_error":1.2374611050301572},"median_abs_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":34.7791109454705,"upper_bound":44.243901222281416},"point_estimate":40.0754205033,"standard_error":2.42022909705503},"slope":{"confidence_interval":{"confidence_level":0.95,"lower_bound":811.6440256190905,"upper_bound":823.2086243755138},"point_estimate":817.2846212085899,"standard_error":2.95472132616886},"std_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":92.90279248590167,"upper_bound":121.73387529852707},"point_estimate":107.2944955313405,"standard_error":7.401429548815175}} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/raw.csv b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/raw.csv new file mode 100644 index 0000000..52a7124 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/raw.csv @@ -0,0 +1,1001 @@ +group,function,value,throughput_num,throughput_type,sample_measured_value,unit,iteration_count +announce-response-to-bytes,,,,,401430.0,ns,454 +announce-response-to-bytes,,,,,667976.0,ns,908 +announce-response-to-bytes,,,,,1025418.0,ns,1362 +announce-response-to-bytes,,,,,1760457.0,ns,1816 +announce-response-to-bytes,,,,,2773514.0,ns,2270 +announce-response-to-bytes,,,,,2931972.0,ns,2724 +announce-response-to-bytes,,,,,3005834.0,ns,3178 +announce-response-to-bytes,,,,,3173571.0,ns,3632 +announce-response-to-bytes,,,,,3046619.0,ns,4086 +announce-response-to-bytes,,,,,3328132.0,ns,4540 +announce-response-to-bytes,,,,,3720452.0,ns,4994 +announce-response-to-bytes,,,,,4090810.0,ns,5448 +announce-response-to-bytes,,,,,4484545.0,ns,5902 +announce-response-to-bytes,,,,,4812796.0,ns,6356 +announce-response-to-bytes,,,,,4908935.0,ns,6810 +announce-response-to-bytes,,,,,6288992.0,ns,7264 +announce-response-to-bytes,,,,,6077287.0,ns,7718 +announce-response-to-bytes,,,,,6176622.0,ns,8172 +announce-response-to-bytes,,,,,6485603.0,ns,8626 +announce-response-to-bytes,,,,,6758164.0,ns,9080 +announce-response-to-bytes,,,,,7122672.0,ns,9534 +announce-response-to-bytes,,,,,7474194.0,ns,9988 +announce-response-to-bytes,,,,,7915038.0,ns,10442 +announce-response-to-bytes,,,,,8363532.0,ns,10896 +announce-response-to-bytes,,,,,11128327.0,ns,11350 +announce-response-to-bytes,,,,,9225973.0,ns,11804 +announce-response-to-bytes,,,,,9294452.0,ns,12258 +announce-response-to-bytes,,,,,9556524.0,ns,12712 +announce-response-to-bytes,,,,,9912145.0,ns,13166 +announce-response-to-bytes,,,,,10414551.0,ns,13620 +announce-response-to-bytes,,,,,13321072.0,ns,14074 +announce-response-to-bytes,,,,,10618032.0,ns,14528 +announce-response-to-bytes,,,,,12276543.0,ns,14982 +announce-response-to-bytes,,,,,14405695.0,ns,15436 +announce-response-to-bytes,,,,,11831439.0,ns,15890 +announce-response-to-bytes,,,,,15333712.0,ns,16344 +announce-response-to-bytes,,,,,12449106.0,ns,16798 +announce-response-to-bytes,,,,,13055819.0,ns,17252 +announce-response-to-bytes,,,,,13379622.0,ns,17706 +announce-response-to-bytes,,,,,13554285.0,ns,18160 +announce-response-to-bytes,,,,,13915354.0,ns,18614 +announce-response-to-bytes,,,,,13918870.0,ns,19068 +announce-response-to-bytes,,,,,14529285.0,ns,19522 +announce-response-to-bytes,,,,,15208879.0,ns,19976 +announce-response-to-bytes,,,,,35365935.0,ns,20430 +announce-response-to-bytes,,,,,35391975.0,ns,20884 +announce-response-to-bytes,,,,,32370107.0,ns,21338 +announce-response-to-bytes,,,,,29786276.0,ns,21792 +announce-response-to-bytes,,,,,27895965.0,ns,22246 +announce-response-to-bytes,,,,,26050119.0,ns,22700 +announce-response-to-bytes,,,,,25461907.0,ns,23154 +announce-response-to-bytes,,,,,24759043.0,ns,23608 +announce-response-to-bytes,,,,,23173216.0,ns,24062 +announce-response-to-bytes,,,,,21967972.0,ns,24516 +announce-response-to-bytes,,,,,21480828.0,ns,24970 +announce-response-to-bytes,,,,,23522828.0,ns,25424 +announce-response-to-bytes,,,,,23447193.0,ns,25878 +announce-response-to-bytes,,,,,23068253.0,ns,26332 +announce-response-to-bytes,,,,,19623914.0,ns,26786 +announce-response-to-bytes,,,,,19997173.0,ns,27240 +announce-response-to-bytes,,,,,20687167.0,ns,27694 +announce-response-to-bytes,,,,,22585354.0,ns,28148 +announce-response-to-bytes,,,,,22761581.0,ns,28602 +announce-response-to-bytes,,,,,21631034.0,ns,29056 +announce-response-to-bytes,,,,,30078711.0,ns,29510 +announce-response-to-bytes,,,,,32243243.0,ns,29964 +announce-response-to-bytes,,,,,29687350.0,ns,30418 +announce-response-to-bytes,,,,,28020842.0,ns,30872 +announce-response-to-bytes,,,,,27195725.0,ns,31326 +announce-response-to-bytes,,,,,26288864.0,ns,31780 +announce-response-to-bytes,,,,,24869828.0,ns,32234 +announce-response-to-bytes,,,,,24636026.0,ns,32688 +announce-response-to-bytes,,,,,24642881.0,ns,33142 +announce-response-to-bytes,,,,,24985993.0,ns,33596 +announce-response-to-bytes,,,,,25912676.0,ns,34050 +announce-response-to-bytes,,,,,28150331.0,ns,34504 +announce-response-to-bytes,,,,,27374609.0,ns,34958 +announce-response-to-bytes,,,,,29088827.0,ns,35412 +announce-response-to-bytes,,,,,26495182.0,ns,35866 +announce-response-to-bytes,,,,,30321313.0,ns,36320 +announce-response-to-bytes,,,,,28349286.0,ns,36774 +announce-response-to-bytes,,,,,29293434.0,ns,37228 +announce-response-to-bytes,,,,,29357346.0,ns,37682 +announce-response-to-bytes,,,,,29627752.0,ns,38136 +announce-response-to-bytes,,,,,29182241.0,ns,38590 +announce-response-to-bytes,,,,,29163745.0,ns,39044 +announce-response-to-bytes,,,,,29498414.0,ns,39498 +announce-response-to-bytes,,,,,33033710.0,ns,39952 +announce-response-to-bytes,,,,,30856419.0,ns,40406 +announce-response-to-bytes,,,,,31083017.0,ns,40860 +announce-response-to-bytes,,,,,31074225.0,ns,41314 +announce-response-to-bytes,,,,,44616166.0,ns,41768 +announce-response-to-bytes,,,,,42008316.0,ns,42222 +announce-response-to-bytes,,,,,37734299.0,ns,42676 +announce-response-to-bytes,,,,,36964876.0,ns,43130 +announce-response-to-bytes,,,,,39743870.0,ns,43584 +announce-response-to-bytes,,,,,32169980.0,ns,44038 +announce-response-to-bytes,,,,,36547954.0,ns,44492 +announce-response-to-bytes,,,,,33679339.0,ns,44946 +announce-response-to-bytes,,,,,34398414.0,ns,45400 +announce-response-to-bytes,,,,,37019472.0,ns,45854 +announce-response-to-bytes,,,,,34286398.0,ns,46308 +announce-response-to-bytes,,,,,34492101.0,ns,46762 +announce-response-to-bytes,,,,,36504626.0,ns,47216 +announce-response-to-bytes,,,,,51376487.0,ns,47670 +announce-response-to-bytes,,,,,47886653.0,ns,48124 +announce-response-to-bytes,,,,,44420256.0,ns,48578 +announce-response-to-bytes,,,,,43905657.0,ns,49032 +announce-response-to-bytes,,,,,39962929.0,ns,49486 +announce-response-to-bytes,,,,,37223378.0,ns,49940 +announce-response-to-bytes,,,,,42935243.0,ns,50394 +announce-response-to-bytes,,,,,38532368.0,ns,50848 +announce-response-to-bytes,,,,,42393190.0,ns,51302 +announce-response-to-bytes,,,,,41997163.0,ns,51756 +announce-response-to-bytes,,,,,38993650.0,ns,52210 +announce-response-to-bytes,,,,,42504681.0,ns,52664 +announce-response-to-bytes,,,,,38417329.0,ns,53118 +announce-response-to-bytes,,,,,39048380.0,ns,53572 +announce-response-to-bytes,,,,,40756221.0,ns,54026 +announce-response-to-bytes,,,,,44107559.0,ns,54480 +announce-response-to-bytes,,,,,40826380.0,ns,54934 +announce-response-to-bytes,,,,,44788659.0,ns,55388 +announce-response-to-bytes,,,,,41148476.0,ns,55842 +announce-response-to-bytes,,,,,42033177.0,ns,56296 +announce-response-to-bytes,,,,,45154098.0,ns,56750 +announce-response-to-bytes,,,,,43171854.0,ns,57204 +announce-response-to-bytes,,,,,45861192.0,ns,57658 +announce-response-to-bytes,,,,,46899987.0,ns,58112 +announce-response-to-bytes,,,,,43436436.0,ns,58566 +announce-response-to-bytes,,,,,55520040.0,ns,59020 +announce-response-to-bytes,,,,,59813174.0,ns,59474 +announce-response-to-bytes,,,,,56002247.0,ns,59928 +announce-response-to-bytes,,,,,47217552.0,ns,60382 +announce-response-to-bytes,,,,,45115273.0,ns,60836 +announce-response-to-bytes,,,,,55174899.0,ns,61290 +announce-response-to-bytes,,,,,48991179.0,ns,61744 +announce-response-to-bytes,,,,,46507742.0,ns,62198 +announce-response-to-bytes,,,,,49404150.0,ns,62652 +announce-response-to-bytes,,,,,46546074.0,ns,63106 +announce-response-to-bytes,,,,,53173411.0,ns,63560 +announce-response-to-bytes,,,,,48597379.0,ns,64014 +announce-response-to-bytes,,,,,51232324.0,ns,64468 +announce-response-to-bytes,,,,,48940226.0,ns,64922 +announce-response-to-bytes,,,,,51389031.0,ns,65376 +announce-response-to-bytes,,,,,52886229.0,ns,65830 +announce-response-to-bytes,,,,,51579257.0,ns,66284 +announce-response-to-bytes,,,,,50045827.0,ns,66738 +announce-response-to-bytes,,,,,51090435.0,ns,67192 +announce-response-to-bytes,,,,,50168765.0,ns,67646 +announce-response-to-bytes,,,,,50585612.0,ns,68100 +announce-response-to-bytes,,,,,51915277.0,ns,68554 +announce-response-to-bytes,,,,,55172077.0,ns,69008 +announce-response-to-bytes,,,,,58296604.0,ns,69462 +announce-response-to-bytes,,,,,71262556.0,ns,69916 +announce-response-to-bytes,,,,,65626454.0,ns,70370 +announce-response-to-bytes,,,,,61401919.0,ns,70824 +announce-response-to-bytes,,,,,53444318.0,ns,71278 +announce-response-to-bytes,,,,,56772754.0,ns,71732 +announce-response-to-bytes,,,,,54228308.0,ns,72186 +announce-response-to-bytes,,,,,64026040.0,ns,72640 +announce-response-to-bytes,,,,,73088637.0,ns,73094 +announce-response-to-bytes,,,,,64530803.0,ns,73548 +announce-response-to-bytes,,,,,58068750.0,ns,74002 +announce-response-to-bytes,,,,,58649987.0,ns,74456 +announce-response-to-bytes,,,,,54654937.0,ns,74910 +announce-response-to-bytes,,,,,58627628.0,ns,75364 +announce-response-to-bytes,,,,,59489562.0,ns,75818 +announce-response-to-bytes,,,,,57041165.0,ns,76272 +announce-response-to-bytes,,,,,61189228.0,ns,76726 +announce-response-to-bytes,,,,,59968331.0,ns,77180 +announce-response-to-bytes,,,,,57680550.0,ns,77634 +announce-response-to-bytes,,,,,61277175.0,ns,78088 +announce-response-to-bytes,,,,,114510262.0,ns,78542 +announce-response-to-bytes,,,,,97989580.0,ns,78996 +announce-response-to-bytes,,,,,79340870.0,ns,79450 +announce-response-to-bytes,,,,,66757350.0,ns,79904 +announce-response-to-bytes,,,,,63587361.0,ns,80358 +announce-response-to-bytes,,,,,61030198.0,ns,80812 +announce-response-to-bytes,,,,,60051965.0,ns,81266 +announce-response-to-bytes,,,,,64272121.0,ns,81720 +announce-response-to-bytes,,,,,61413799.0,ns,82174 +announce-response-to-bytes,,,,,61943571.0,ns,82628 +announce-response-to-bytes,,,,,71371288.0,ns,83082 +announce-response-to-bytes,,,,,61270697.0,ns,83536 +announce-response-to-bytes,,,,,62369625.0,ns,83990 +announce-response-to-bytes,,,,,66659623.0,ns,84444 +announce-response-to-bytes,,,,,63397171.0,ns,84898 +announce-response-to-bytes,,,,,113134517.0,ns,85352 +announce-response-to-bytes,,,,,108420521.0,ns,85806 +announce-response-to-bytes,,,,,83354595.0,ns,86260 +announce-response-to-bytes,,,,,73340464.0,ns,86714 +announce-response-to-bytes,,,,,68614513.0,ns,87168 +announce-response-to-bytes,,,,,71416685.0,ns,87622 +announce-response-to-bytes,,,,,69822100.0,ns,88076 +announce-response-to-bytes,,,,,69366564.0,ns,88530 +announce-response-to-bytes,,,,,67171782.0,ns,88984 +announce-response-to-bytes,,,,,70919293.0,ns,89438 +announce-response-to-bytes,,,,,70310196.0,ns,89892 +announce-response-to-bytes,,,,,67274380.0,ns,90346 +announce-response-to-bytes,,,,,71666094.0,ns,90800 +announce-response-to-bytes,,,,,72700249.0,ns,91254 +announce-response-to-bytes,,,,,68034994.0,ns,91708 +announce-response-to-bytes,,,,,81137150.0,ns,92162 +announce-response-to-bytes,,,,,89751869.0,ns,92616 +announce-response-to-bytes,,,,,75189615.0,ns,93070 +announce-response-to-bytes,,,,,77163018.0,ns,93524 +announce-response-to-bytes,,,,,76885868.0,ns,93978 +announce-response-to-bytes,,,,,96298749.0,ns,94432 +announce-response-to-bytes,,,,,80598989.0,ns,94886 +announce-response-to-bytes,,,,,70731444.0,ns,95340 +announce-response-to-bytes,,,,,71193713.0,ns,95794 +announce-response-to-bytes,,,,,75778589.0,ns,96248 +announce-response-to-bytes,,,,,76779761.0,ns,96702 +announce-response-to-bytes,,,,,73293733.0,ns,97156 +announce-response-to-bytes,,,,,100871546.0,ns,97610 +announce-response-to-bytes,,,,,87401859.0,ns,98064 +announce-response-to-bytes,,,,,75113910.0,ns,98518 +announce-response-to-bytes,,,,,74899683.0,ns,98972 +announce-response-to-bytes,,,,,73397098.0,ns,99426 +announce-response-to-bytes,,,,,74433784.0,ns,99880 +announce-response-to-bytes,,,,,75948784.0,ns,100334 +announce-response-to-bytes,,,,,78760151.0,ns,100788 +announce-response-to-bytes,,,,,77567043.0,ns,101242 +announce-response-to-bytes,,,,,86814772.0,ns,101696 +announce-response-to-bytes,,,,,151206275.0,ns,102150 +announce-response-to-bytes,,,,,104296881.0,ns,102604 +announce-response-to-bytes,,,,,92357440.0,ns,103058 +announce-response-to-bytes,,,,,83609302.0,ns,103512 +announce-response-to-bytes,,,,,78863635.0,ns,103966 +announce-response-to-bytes,,,,,106344548.0,ns,104420 +announce-response-to-bytes,,,,,88457924.0,ns,104874 +announce-response-to-bytes,,,,,78020903.0,ns,105328 +announce-response-to-bytes,,,,,81690727.0,ns,105782 +announce-response-to-bytes,,,,,82896527.0,ns,106236 +announce-response-to-bytes,,,,,79508368.0,ns,106690 +announce-response-to-bytes,,,,,82985455.0,ns,107144 +announce-response-to-bytes,,,,,84652782.0,ns,107598 +announce-response-to-bytes,,,,,79978207.0,ns,108052 +announce-response-to-bytes,,,,,87981709.0,ns,108506 +announce-response-to-bytes,,,,,159956575.0,ns,108960 +announce-response-to-bytes,,,,,114310461.0,ns,109414 +announce-response-to-bytes,,,,,95378693.0,ns,109868 +announce-response-to-bytes,,,,,92728676.0,ns,110322 +announce-response-to-bytes,,,,,87124067.0,ns,110776 +announce-response-to-bytes,,,,,85703206.0,ns,111230 +announce-response-to-bytes,,,,,85862053.0,ns,111684 +announce-response-to-bytes,,,,,89982004.0,ns,112138 +announce-response-to-bytes,,,,,93100414.0,ns,112592 +announce-response-to-bytes,,,,,83990160.0,ns,113046 +announce-response-to-bytes,,,,,94475296.0,ns,113500 +announce-response-to-bytes,,,,,92055569.0,ns,113954 +announce-response-to-bytes,,,,,89352370.0,ns,114408 +announce-response-to-bytes,,,,,85358383.0,ns,114862 +announce-response-to-bytes,,,,,89276389.0,ns,115316 +announce-response-to-bytes,,,,,87367587.0,ns,115770 +announce-response-to-bytes,,,,,90582297.0,ns,116224 +announce-response-to-bytes,,,,,91725575.0,ns,116678 +announce-response-to-bytes,,,,,87388798.0,ns,117132 +announce-response-to-bytes,,,,,87928110.0,ns,117586 +announce-response-to-bytes,,,,,88976306.0,ns,118040 +announce-response-to-bytes,,,,,91631113.0,ns,118494 +announce-response-to-bytes,,,,,86521557.0,ns,118948 +announce-response-to-bytes,,,,,91757076.0,ns,119402 +announce-response-to-bytes,,,,,99682167.0,ns,119856 +announce-response-to-bytes,,,,,92593955.0,ns,120310 +announce-response-to-bytes,,,,,95259570.0,ns,120764 +announce-response-to-bytes,,,,,90181764.0,ns,121218 +announce-response-to-bytes,,,,,94443119.0,ns,121672 +announce-response-to-bytes,,,,,91524080.0,ns,122126 +announce-response-to-bytes,,,,,94477809.0,ns,122580 +announce-response-to-bytes,,,,,99200558.0,ns,123034 +announce-response-to-bytes,,,,,98619448.0,ns,123488 +announce-response-to-bytes,,,,,99893491.0,ns,123942 +announce-response-to-bytes,,,,,100295004.0,ns,124396 +announce-response-to-bytes,,,,,96485808.0,ns,124850 +announce-response-to-bytes,,,,,94283354.0,ns,125304 +announce-response-to-bytes,,,,,101180179.0,ns,125758 +announce-response-to-bytes,,,,,100585524.0,ns,126212 +announce-response-to-bytes,,,,,102885850.0,ns,126666 +announce-response-to-bytes,,,,,101063306.0,ns,127120 +announce-response-to-bytes,,,,,133843830.0,ns,127574 +announce-response-to-bytes,,,,,158645712.0,ns,128028 +announce-response-to-bytes,,,,,122169820.0,ns,128482 +announce-response-to-bytes,,,,,97533036.0,ns,128936 +announce-response-to-bytes,,,,,103794040.0,ns,129390 +announce-response-to-bytes,,,,,101696375.0,ns,129844 +announce-response-to-bytes,,,,,105048423.0,ns,130298 +announce-response-to-bytes,,,,,97382468.0,ns,130752 +announce-response-to-bytes,,,,,102254749.0,ns,131206 +announce-response-to-bytes,,,,,104069503.0,ns,131660 +announce-response-to-bytes,,,,,99651353.0,ns,132114 +announce-response-to-bytes,,,,,102823512.0,ns,132568 +announce-response-to-bytes,,,,,99604245.0,ns,133022 +announce-response-to-bytes,,,,,132082275.0,ns,133476 +announce-response-to-bytes,,,,,108193195.0,ns,133930 +announce-response-to-bytes,,,,,103798546.0,ns,134384 +announce-response-to-bytes,,,,,101037951.0,ns,134838 +announce-response-to-bytes,,,,,101509377.0,ns,135292 +announce-response-to-bytes,,,,,104133401.0,ns,135746 +announce-response-to-bytes,,,,,113868082.0,ns,136200 +announce-response-to-bytes,,,,,104754114.0,ns,136654 +announce-response-to-bytes,,,,,108023631.0,ns,137108 +announce-response-to-bytes,,,,,108221898.0,ns,137562 +announce-response-to-bytes,,,,,128146191.0,ns,138016 +announce-response-to-bytes,,,,,116538692.0,ns,138470 +announce-response-to-bytes,,,,,103148974.0,ns,138924 +announce-response-to-bytes,,,,,106027570.0,ns,139378 +announce-response-to-bytes,,,,,106781849.0,ns,139832 +announce-response-to-bytes,,,,,111416888.0,ns,140286 +announce-response-to-bytes,,,,,117275860.0,ns,140740 +announce-response-to-bytes,,,,,105629357.0,ns,141194 +announce-response-to-bytes,,,,,113008523.0,ns,141648 +announce-response-to-bytes,,,,,111500588.0,ns,142102 +announce-response-to-bytes,,,,,117592176.0,ns,142556 +announce-response-to-bytes,,,,,138573402.0,ns,143010 +announce-response-to-bytes,,,,,120568948.0,ns,143464 +announce-response-to-bytes,,,,,128509197.0,ns,143918 +announce-response-to-bytes,,,,,130109399.0,ns,144372 +announce-response-to-bytes,,,,,111099822.0,ns,144826 +announce-response-to-bytes,,,,,113706590.0,ns,145280 +announce-response-to-bytes,,,,,114090399.0,ns,145734 +announce-response-to-bytes,,,,,111581633.0,ns,146188 +announce-response-to-bytes,,,,,116715433.0,ns,146642 +announce-response-to-bytes,,,,,111743608.0,ns,147096 +announce-response-to-bytes,,,,,115980102.0,ns,147550 +announce-response-to-bytes,,,,,116165004.0,ns,148004 +announce-response-to-bytes,,,,,117840515.0,ns,148458 +announce-response-to-bytes,,,,,114481663.0,ns,148912 +announce-response-to-bytes,,,,,121905531.0,ns,149366 +announce-response-to-bytes,,,,,114857168.0,ns,149820 +announce-response-to-bytes,,,,,122478128.0,ns,150274 +announce-response-to-bytes,,,,,116461625.0,ns,150728 +announce-response-to-bytes,,,,,118914865.0,ns,151182 +announce-response-to-bytes,,,,,116137184.0,ns,151636 +announce-response-to-bytes,,,,,116110817.0,ns,152090 +announce-response-to-bytes,,,,,114904319.0,ns,152544 +announce-response-to-bytes,,,,,203420209.0,ns,152998 +announce-response-to-bytes,,,,,147176776.0,ns,153452 +announce-response-to-bytes,,,,,119130160.0,ns,153906 +announce-response-to-bytes,,,,,118124938.0,ns,154360 +announce-response-to-bytes,,,,,121226369.0,ns,154814 +announce-response-to-bytes,,,,,120159833.0,ns,155268 +announce-response-to-bytes,,,,,123224429.0,ns,155722 +announce-response-to-bytes,,,,,123683081.0,ns,156176 +announce-response-to-bytes,,,,,125055031.0,ns,156630 +announce-response-to-bytes,,,,,117320747.0,ns,157084 +announce-response-to-bytes,,,,,122586708.0,ns,157538 +announce-response-to-bytes,,,,,157466162.0,ns,157992 +announce-response-to-bytes,,,,,122166399.0,ns,158446 +announce-response-to-bytes,,,,,126265894.0,ns,158900 +announce-response-to-bytes,,,,,121922877.0,ns,159354 +announce-response-to-bytes,,,,,122422679.0,ns,159808 +announce-response-to-bytes,,,,,129213097.0,ns,160262 +announce-response-to-bytes,,,,,119991894.0,ns,160716 +announce-response-to-bytes,,,,,126630881.0,ns,161170 +announce-response-to-bytes,,,,,126730358.0,ns,161624 +announce-response-to-bytes,,,,,128928650.0,ns,162078 +announce-response-to-bytes,,,,,153712298.0,ns,162532 +announce-response-to-bytes,,,,,128922821.0,ns,162986 +announce-response-to-bytes,,,,,122210095.0,ns,163440 +announce-response-to-bytes,,,,,127808674.0,ns,163894 +announce-response-to-bytes,,,,,131592352.0,ns,164348 +announce-response-to-bytes,,,,,123790925.0,ns,164802 +announce-response-to-bytes,,,,,126473221.0,ns,165256 +announce-response-to-bytes,,,,,128068018.0,ns,165710 +announce-response-to-bytes,,,,,125535935.0,ns,166164 +announce-response-to-bytes,,,,,149423183.0,ns,166618 +announce-response-to-bytes,,,,,142909056.0,ns,167072 +announce-response-to-bytes,,,,,129140128.0,ns,167526 +announce-response-to-bytes,,,,,157696636.0,ns,167980 +announce-response-to-bytes,,,,,138497001.0,ns,168434 +announce-response-to-bytes,,,,,129266904.0,ns,168888 +announce-response-to-bytes,,,,,127780811.0,ns,169342 +announce-response-to-bytes,,,,,164397546.0,ns,169796 +announce-response-to-bytes,,,,,130232792.0,ns,170250 +announce-response-to-bytes,,,,,134136458.0,ns,170704 +announce-response-to-bytes,,,,,135973193.0,ns,171158 +announce-response-to-bytes,,,,,130869078.0,ns,171612 +announce-response-to-bytes,,,,,129854172.0,ns,172066 +announce-response-to-bytes,,,,,143467685.0,ns,172520 +announce-response-to-bytes,,,,,136134658.0,ns,172974 +announce-response-to-bytes,,,,,134711368.0,ns,173428 +announce-response-to-bytes,,,,,143020541.0,ns,173882 +announce-response-to-bytes,,,,,131241328.0,ns,174336 +announce-response-to-bytes,,,,,141298924.0,ns,174790 +announce-response-to-bytes,,,,,161916202.0,ns,175244 +announce-response-to-bytes,,,,,147070198.0,ns,175698 +announce-response-to-bytes,,,,,136252979.0,ns,176152 +announce-response-to-bytes,,,,,134014277.0,ns,176606 +announce-response-to-bytes,,,,,136773736.0,ns,177060 +announce-response-to-bytes,,,,,183082874.0,ns,177514 +announce-response-to-bytes,,,,,200961949.0,ns,177968 +announce-response-to-bytes,,,,,140175423.0,ns,178422 +announce-response-to-bytes,,,,,136609615.0,ns,178876 +announce-response-to-bytes,,,,,142932671.0,ns,179330 +announce-response-to-bytes,,,,,133909487.0,ns,179784 +announce-response-to-bytes,,,,,144758756.0,ns,180238 +announce-response-to-bytes,,,,,134594385.0,ns,180692 +announce-response-to-bytes,,,,,136881780.0,ns,181146 +announce-response-to-bytes,,,,,228618847.0,ns,181600 +announce-response-to-bytes,,,,,167984860.0,ns,182054 +announce-response-to-bytes,,,,,137027114.0,ns,182508 +announce-response-to-bytes,,,,,165625955.0,ns,182962 +announce-response-to-bytes,,,,,148708745.0,ns,183416 +announce-response-to-bytes,,,,,140858973.0,ns,183870 +announce-response-to-bytes,,,,,140789170.0,ns,184324 +announce-response-to-bytes,,,,,140185347.0,ns,184778 +announce-response-to-bytes,,,,,141982083.0,ns,185232 +announce-response-to-bytes,,,,,142501858.0,ns,185686 +announce-response-to-bytes,,,,,143367879.0,ns,186140 +announce-response-to-bytes,,,,,147731030.0,ns,186594 +announce-response-to-bytes,,,,,143819588.0,ns,187048 +announce-response-to-bytes,,,,,145475937.0,ns,187502 +announce-response-to-bytes,,,,,150970716.0,ns,187956 +announce-response-to-bytes,,,,,143902811.0,ns,188410 +announce-response-to-bytes,,,,,146856226.0,ns,188864 +announce-response-to-bytes,,,,,186377324.0,ns,189318 +announce-response-to-bytes,,,,,142702270.0,ns,189772 +announce-response-to-bytes,,,,,147189632.0,ns,190226 +announce-response-to-bytes,,,,,155494491.0,ns,190680 +announce-response-to-bytes,,,,,177852045.0,ns,191134 +announce-response-to-bytes,,,,,151691540.0,ns,191588 +announce-response-to-bytes,,,,,149968503.0,ns,192042 +announce-response-to-bytes,,,,,154219258.0,ns,192496 +announce-response-to-bytes,,,,,176730990.0,ns,192950 +announce-response-to-bytes,,,,,163733009.0,ns,193404 +announce-response-to-bytes,,,,,150078710.0,ns,193858 +announce-response-to-bytes,,,,,155011160.0,ns,194312 +announce-response-to-bytes,,,,,145642824.0,ns,194766 +announce-response-to-bytes,,,,,154018323.0,ns,195220 +announce-response-to-bytes,,,,,154932805.0,ns,195674 +announce-response-to-bytes,,,,,183096342.0,ns,196128 +announce-response-to-bytes,,,,,154786350.0,ns,196582 +announce-response-to-bytes,,,,,160676315.0,ns,197036 +announce-response-to-bytes,,,,,181589572.0,ns,197490 +announce-response-to-bytes,,,,,153417039.0,ns,197944 +announce-response-to-bytes,,,,,165686714.0,ns,198398 +announce-response-to-bytes,,,,,148099636.0,ns,198852 +announce-response-to-bytes,,,,,155902020.0,ns,199306 +announce-response-to-bytes,,,,,152546539.0,ns,199760 +announce-response-to-bytes,,,,,151356795.0,ns,200214 +announce-response-to-bytes,,,,,151572866.0,ns,200668 +announce-response-to-bytes,,,,,163695045.0,ns,201122 +announce-response-to-bytes,,,,,152585139.0,ns,201576 +announce-response-to-bytes,,,,,160440772.0,ns,202030 +announce-response-to-bytes,,,,,153371615.0,ns,202484 +announce-response-to-bytes,,,,,199721452.0,ns,202938 +announce-response-to-bytes,,,,,167832199.0,ns,203392 +announce-response-to-bytes,,,,,156673062.0,ns,203846 +announce-response-to-bytes,,,,,159309870.0,ns,204300 +announce-response-to-bytes,,,,,159502831.0,ns,204754 +announce-response-to-bytes,,,,,154617677.0,ns,205208 +announce-response-to-bytes,,,,,157046936.0,ns,205662 +announce-response-to-bytes,,,,,159024613.0,ns,206116 +announce-response-to-bytes,,,,,161723352.0,ns,206570 +announce-response-to-bytes,,,,,170463400.0,ns,207024 +announce-response-to-bytes,,,,,186043394.0,ns,207478 +announce-response-to-bytes,,,,,165538684.0,ns,207932 +announce-response-to-bytes,,,,,157513302.0,ns,208386 +announce-response-to-bytes,,,,,164801747.0,ns,208840 +announce-response-to-bytes,,,,,162628332.0,ns,209294 +announce-response-to-bytes,,,,,168514860.0,ns,209748 +announce-response-to-bytes,,,,,164495431.0,ns,210202 +announce-response-to-bytes,,,,,157241920.0,ns,210656 +announce-response-to-bytes,,,,,263440212.0,ns,211110 +announce-response-to-bytes,,,,,176988605.0,ns,211564 +announce-response-to-bytes,,,,,162036114.0,ns,212018 +announce-response-to-bytes,,,,,166418986.0,ns,212472 +announce-response-to-bytes,,,,,238071956.0,ns,212926 +announce-response-to-bytes,,,,,211365092.0,ns,213380 +announce-response-to-bytes,,,,,166222569.0,ns,213834 +announce-response-to-bytes,,,,,163265361.0,ns,214288 +announce-response-to-bytes,,,,,162303160.0,ns,214742 +announce-response-to-bytes,,,,,173062999.0,ns,215196 +announce-response-to-bytes,,,,,173733449.0,ns,215650 +announce-response-to-bytes,,,,,198449731.0,ns,216104 +announce-response-to-bytes,,,,,167237507.0,ns,216558 +announce-response-to-bytes,,,,,170723360.0,ns,217012 +announce-response-to-bytes,,,,,180987802.0,ns,217466 +announce-response-to-bytes,,,,,206170231.0,ns,217920 +announce-response-to-bytes,,,,,244701925.0,ns,218374 +announce-response-to-bytes,,,,,172946872.0,ns,218828 +announce-response-to-bytes,,,,,164867992.0,ns,219282 +announce-response-to-bytes,,,,,170502704.0,ns,219736 +announce-response-to-bytes,,,,,176632681.0,ns,220190 +announce-response-to-bytes,,,,,209222843.0,ns,220644 +announce-response-to-bytes,,,,,172378327.0,ns,221098 +announce-response-to-bytes,,,,,178923617.0,ns,221552 +announce-response-to-bytes,,,,,171037007.0,ns,222006 +announce-response-to-bytes,,,,,176514736.0,ns,222460 +announce-response-to-bytes,,,,,172342821.0,ns,222914 +announce-response-to-bytes,,,,,171481037.0,ns,223368 +announce-response-to-bytes,,,,,167859194.0,ns,223822 +announce-response-to-bytes,,,,,177469350.0,ns,224276 +announce-response-to-bytes,,,,,168760325.0,ns,224730 +announce-response-to-bytes,,,,,172392820.0,ns,225184 +announce-response-to-bytes,,,,,169906576.0,ns,225638 +announce-response-to-bytes,,,,,171789091.0,ns,226092 +announce-response-to-bytes,,,,,188119846.0,ns,226546 +announce-response-to-bytes,,,,,194125298.0,ns,227000 +announce-response-to-bytes,,,,,173322626.0,ns,227454 +announce-response-to-bytes,,,,,210832158.0,ns,227908 +announce-response-to-bytes,,,,,176129605.0,ns,228362 +announce-response-to-bytes,,,,,180015767.0,ns,228816 +announce-response-to-bytes,,,,,247020460.0,ns,229270 +announce-response-to-bytes,,,,,226417665.0,ns,229724 +announce-response-to-bytes,,,,,177105640.0,ns,230178 +announce-response-to-bytes,,,,,176160761.0,ns,230632 +announce-response-to-bytes,,,,,182796592.0,ns,231086 +announce-response-to-bytes,,,,,181611228.0,ns,231540 +announce-response-to-bytes,,,,,180289095.0,ns,231994 +announce-response-to-bytes,,,,,215888256.0,ns,232448 +announce-response-to-bytes,,,,,180433113.0,ns,232902 +announce-response-to-bytes,,,,,174944560.0,ns,233356 +announce-response-to-bytes,,,,,180350408.0,ns,233810 +announce-response-to-bytes,,,,,179463547.0,ns,234264 +announce-response-to-bytes,,,,,182184241.0,ns,234718 +announce-response-to-bytes,,,,,183561335.0,ns,235172 +announce-response-to-bytes,,,,,189635250.0,ns,235626 +announce-response-to-bytes,,,,,184613902.0,ns,236080 +announce-response-to-bytes,,,,,181214727.0,ns,236534 +announce-response-to-bytes,,,,,191488918.0,ns,236988 +announce-response-to-bytes,,,,,191947712.0,ns,237442 +announce-response-to-bytes,,,,,183642884.0,ns,237896 +announce-response-to-bytes,,,,,263683701.0,ns,238350 +announce-response-to-bytes,,,,,221740575.0,ns,238804 +announce-response-to-bytes,,,,,179043045.0,ns,239258 +announce-response-to-bytes,,,,,193570869.0,ns,239712 +announce-response-to-bytes,,,,,184855232.0,ns,240166 +announce-response-to-bytes,,,,,189125421.0,ns,240620 +announce-response-to-bytes,,,,,178310424.0,ns,241074 +announce-response-to-bytes,,,,,234040798.0,ns,241528 +announce-response-to-bytes,,,,,189436473.0,ns,241982 +announce-response-to-bytes,,,,,185248442.0,ns,242436 +announce-response-to-bytes,,,,,184484232.0,ns,242890 +announce-response-to-bytes,,,,,205194381.0,ns,243344 +announce-response-to-bytes,,,,,186457791.0,ns,243798 +announce-response-to-bytes,,,,,182545024.0,ns,244252 +announce-response-to-bytes,,,,,208744968.0,ns,244706 +announce-response-to-bytes,,,,,277153632.0,ns,245160 +announce-response-to-bytes,,,,,190022319.0,ns,245614 +announce-response-to-bytes,,,,,192405531.0,ns,246068 +announce-response-to-bytes,,,,,186941376.0,ns,246522 +announce-response-to-bytes,,,,,225843858.0,ns,246976 +announce-response-to-bytes,,,,,207479525.0,ns,247430 +announce-response-to-bytes,,,,,189218841.0,ns,247884 +announce-response-to-bytes,,,,,194333414.0,ns,248338 +announce-response-to-bytes,,,,,194553633.0,ns,248792 +announce-response-to-bytes,,,,,197281758.0,ns,249246 +announce-response-to-bytes,,,,,192678824.0,ns,249700 +announce-response-to-bytes,,,,,194953869.0,ns,250154 +announce-response-to-bytes,,,,,238223707.0,ns,250608 +announce-response-to-bytes,,,,,276973132.0,ns,251062 +announce-response-to-bytes,,,,,190717236.0,ns,251516 +announce-response-to-bytes,,,,,198253360.0,ns,251970 +announce-response-to-bytes,,,,,227633139.0,ns,252424 +announce-response-to-bytes,,,,,191242806.0,ns,252878 +announce-response-to-bytes,,,,,196840834.0,ns,253332 +announce-response-to-bytes,,,,,197618214.0,ns,253786 +announce-response-to-bytes,,,,,294034310.0,ns,254240 +announce-response-to-bytes,,,,,213295013.0,ns,254694 +announce-response-to-bytes,,,,,199769357.0,ns,255148 +announce-response-to-bytes,,,,,194119968.0,ns,255602 +announce-response-to-bytes,,,,,299180927.0,ns,256056 +announce-response-to-bytes,,,,,215136032.0,ns,256510 +announce-response-to-bytes,,,,,230794222.0,ns,256964 +announce-response-to-bytes,,,,,204279647.0,ns,257418 +announce-response-to-bytes,,,,,205778394.0,ns,257872 +announce-response-to-bytes,,,,,209487042.0,ns,258326 +announce-response-to-bytes,,,,,215796962.0,ns,258780 +announce-response-to-bytes,,,,,295432707.0,ns,259234 +announce-response-to-bytes,,,,,215876240.0,ns,259688 +announce-response-to-bytes,,,,,195367269.0,ns,260142 +announce-response-to-bytes,,,,,200968544.0,ns,260596 +announce-response-to-bytes,,,,,208539705.0,ns,261050 +announce-response-to-bytes,,,,,203401687.0,ns,261504 +announce-response-to-bytes,,,,,206014845.0,ns,261958 +announce-response-to-bytes,,,,,204010711.0,ns,262412 +announce-response-to-bytes,,,,,203794103.0,ns,262866 +announce-response-to-bytes,,,,,208477042.0,ns,263320 +announce-response-to-bytes,,,,,210990443.0,ns,263774 +announce-response-to-bytes,,,,,234126624.0,ns,264228 +announce-response-to-bytes,,,,,210338117.0,ns,264682 +announce-response-to-bytes,,,,,203798616.0,ns,265136 +announce-response-to-bytes,,,,,203309834.0,ns,265590 +announce-response-to-bytes,,,,,210068917.0,ns,266044 +announce-response-to-bytes,,,,,210102852.0,ns,266498 +announce-response-to-bytes,,,,,245071871.0,ns,266952 +announce-response-to-bytes,,,,,206338196.0,ns,267406 +announce-response-to-bytes,,,,,205180868.0,ns,267860 +announce-response-to-bytes,,,,,206809853.0,ns,268314 +announce-response-to-bytes,,,,,205851855.0,ns,268768 +announce-response-to-bytes,,,,,213330764.0,ns,269222 +announce-response-to-bytes,,,,,247626150.0,ns,269676 +announce-response-to-bytes,,,,,206126207.0,ns,270130 +announce-response-to-bytes,,,,,209931806.0,ns,270584 +announce-response-to-bytes,,,,,206913350.0,ns,271038 +announce-response-to-bytes,,,,,207562305.0,ns,271492 +announce-response-to-bytes,,,,,213205947.0,ns,271946 +announce-response-to-bytes,,,,,240857207.0,ns,272400 +announce-response-to-bytes,,,,,208247811.0,ns,272854 +announce-response-to-bytes,,,,,245827907.0,ns,273308 +announce-response-to-bytes,,,,,207181339.0,ns,273762 +announce-response-to-bytes,,,,,213274024.0,ns,274216 +announce-response-to-bytes,,,,,216167482.0,ns,274670 +announce-response-to-bytes,,,,,214731493.0,ns,275124 +announce-response-to-bytes,,,,,211637653.0,ns,275578 +announce-response-to-bytes,,,,,241929788.0,ns,276032 +announce-response-to-bytes,,,,,217295248.0,ns,276486 +announce-response-to-bytes,,,,,214711472.0,ns,276940 +announce-response-to-bytes,,,,,212209095.0,ns,277394 +announce-response-to-bytes,,,,,248514558.0,ns,277848 +announce-response-to-bytes,,,,,223284626.0,ns,278302 +announce-response-to-bytes,,,,,220307305.0,ns,278756 +announce-response-to-bytes,,,,,212300769.0,ns,279210 +announce-response-to-bytes,,,,,241733487.0,ns,279664 +announce-response-to-bytes,,,,,230186427.0,ns,280118 +announce-response-to-bytes,,,,,247919471.0,ns,280572 +announce-response-to-bytes,,,,,227701683.0,ns,281026 +announce-response-to-bytes,,,,,216284737.0,ns,281480 +announce-response-to-bytes,,,,,257844570.0,ns,281934 +announce-response-to-bytes,,,,,220678268.0,ns,282388 +announce-response-to-bytes,,,,,213008484.0,ns,282842 +announce-response-to-bytes,,,,,223712703.0,ns,283296 +announce-response-to-bytes,,,,,216188118.0,ns,283750 +announce-response-to-bytes,,,,,214633901.0,ns,284204 +announce-response-to-bytes,,,,,220063814.0,ns,284658 +announce-response-to-bytes,,,,,216745351.0,ns,285112 +announce-response-to-bytes,,,,,218125868.0,ns,285566 +announce-response-to-bytes,,,,,224900105.0,ns,286020 +announce-response-to-bytes,,,,,215952910.0,ns,286474 +announce-response-to-bytes,,,,,223571394.0,ns,286928 +announce-response-to-bytes,,,,,220165276.0,ns,287382 +announce-response-to-bytes,,,,,228108821.0,ns,287836 +announce-response-to-bytes,,,,,221455669.0,ns,288290 +announce-response-to-bytes,,,,,235478435.0,ns,288744 +announce-response-to-bytes,,,,,247533823.0,ns,289198 +announce-response-to-bytes,,,,,228497214.0,ns,289652 +announce-response-to-bytes,,,,,220139171.0,ns,290106 +announce-response-to-bytes,,,,,219789332.0,ns,290560 +announce-response-to-bytes,,,,,249307296.0,ns,291014 +announce-response-to-bytes,,,,,242118748.0,ns,291468 +announce-response-to-bytes,,,,,261393215.0,ns,291922 +announce-response-to-bytes,,,,,322102865.0,ns,292376 +announce-response-to-bytes,,,,,252024304.0,ns,292830 +announce-response-to-bytes,,,,,231714476.0,ns,293284 +announce-response-to-bytes,,,,,236338544.0,ns,293738 +announce-response-to-bytes,,,,,241892377.0,ns,294192 +announce-response-to-bytes,,,,,322770354.0,ns,294646 +announce-response-to-bytes,,,,,225699782.0,ns,295100 +announce-response-to-bytes,,,,,260038967.0,ns,295554 +announce-response-to-bytes,,,,,225893357.0,ns,296008 +announce-response-to-bytes,,,,,231658148.0,ns,296462 +announce-response-to-bytes,,,,,348516194.0,ns,296916 +announce-response-to-bytes,,,,,240371879.0,ns,297370 +announce-response-to-bytes,,,,,234481115.0,ns,297824 +announce-response-to-bytes,,,,,245099262.0,ns,298278 +announce-response-to-bytes,,,,,232634832.0,ns,298732 +announce-response-to-bytes,,,,,316909192.0,ns,299186 +announce-response-to-bytes,,,,,268184917.0,ns,299640 +announce-response-to-bytes,,,,,233766881.0,ns,300094 +announce-response-to-bytes,,,,,234945400.0,ns,300548 +announce-response-to-bytes,,,,,269048909.0,ns,301002 +announce-response-to-bytes,,,,,246224338.0,ns,301456 +announce-response-to-bytes,,,,,225362866.0,ns,301910 +announce-response-to-bytes,,,,,235432805.0,ns,302364 +announce-response-to-bytes,,,,,236357023.0,ns,302818 +announce-response-to-bytes,,,,,234482449.0,ns,303272 +announce-response-to-bytes,,,,,264111934.0,ns,303726 +announce-response-to-bytes,,,,,339580506.0,ns,304180 +announce-response-to-bytes,,,,,244317485.0,ns,304634 +announce-response-to-bytes,,,,,246060128.0,ns,305088 +announce-response-to-bytes,,,,,241414676.0,ns,305542 +announce-response-to-bytes,,,,,240956987.0,ns,305996 +announce-response-to-bytes,,,,,264249543.0,ns,306450 +announce-response-to-bytes,,,,,321642787.0,ns,306904 +announce-response-to-bytes,,,,,240015044.0,ns,307358 +announce-response-to-bytes,,,,,259478387.0,ns,307812 +announce-response-to-bytes,,,,,239414297.0,ns,308266 +announce-response-to-bytes,,,,,254491339.0,ns,308720 +announce-response-to-bytes,,,,,248765124.0,ns,309174 +announce-response-to-bytes,,,,,253045660.0,ns,309628 +announce-response-to-bytes,,,,,272511183.0,ns,310082 +announce-response-to-bytes,,,,,244718343.0,ns,310536 +announce-response-to-bytes,,,,,253910398.0,ns,310990 +announce-response-to-bytes,,,,,240441463.0,ns,311444 +announce-response-to-bytes,,,,,266975702.0,ns,311898 +announce-response-to-bytes,,,,,255037572.0,ns,312352 +announce-response-to-bytes,,,,,236798254.0,ns,312806 +announce-response-to-bytes,,,,,238942957.0,ns,313260 +announce-response-to-bytes,,,,,246723054.0,ns,313714 +announce-response-to-bytes,,,,,250622654.0,ns,314168 +announce-response-to-bytes,,,,,242326298.0,ns,314622 +announce-response-to-bytes,,,,,241115747.0,ns,315076 +announce-response-to-bytes,,,,,237656148.0,ns,315530 +announce-response-to-bytes,,,,,281620408.0,ns,315984 +announce-response-to-bytes,,,,,246502020.0,ns,316438 +announce-response-to-bytes,,,,,275679915.0,ns,316892 +announce-response-to-bytes,,,,,261075618.0,ns,317346 +announce-response-to-bytes,,,,,247242701.0,ns,317800 +announce-response-to-bytes,,,,,277204694.0,ns,318254 +announce-response-to-bytes,,,,,249363201.0,ns,318708 +announce-response-to-bytes,,,,,247083062.0,ns,319162 +announce-response-to-bytes,,,,,252386722.0,ns,319616 +announce-response-to-bytes,,,,,262062668.0,ns,320070 +announce-response-to-bytes,,,,,257042629.0,ns,320524 +announce-response-to-bytes,,,,,285380462.0,ns,320978 +announce-response-to-bytes,,,,,260616278.0,ns,321432 +announce-response-to-bytes,,,,,270512510.0,ns,321886 +announce-response-to-bytes,,,,,279497878.0,ns,322340 +announce-response-to-bytes,,,,,265166975.0,ns,322794 +announce-response-to-bytes,,,,,284225320.0,ns,323248 +announce-response-to-bytes,,,,,257482021.0,ns,323702 +announce-response-to-bytes,,,,,250209381.0,ns,324156 +announce-response-to-bytes,,,,,251370419.0,ns,324610 +announce-response-to-bytes,,,,,264998276.0,ns,325064 +announce-response-to-bytes,,,,,285149243.0,ns,325518 +announce-response-to-bytes,,,,,288456767.0,ns,325972 +announce-response-to-bytes,,,,,268237472.0,ns,326426 +announce-response-to-bytes,,,,,281029707.0,ns,326880 +announce-response-to-bytes,,,,,252462333.0,ns,327334 +announce-response-to-bytes,,,,,260747654.0,ns,327788 +announce-response-to-bytes,,,,,291876077.0,ns,328242 +announce-response-to-bytes,,,,,260462462.0,ns,328696 +announce-response-to-bytes,,,,,250597862.0,ns,329150 +announce-response-to-bytes,,,,,284897834.0,ns,329604 +announce-response-to-bytes,,,,,278527156.0,ns,330058 +announce-response-to-bytes,,,,,257402659.0,ns,330512 +announce-response-to-bytes,,,,,258143074.0,ns,330966 +announce-response-to-bytes,,,,,270586960.0,ns,331420 +announce-response-to-bytes,,,,,250989931.0,ns,331874 +announce-response-to-bytes,,,,,252889271.0,ns,332328 +announce-response-to-bytes,,,,,335172980.0,ns,332782 +announce-response-to-bytes,,,,,304549454.0,ns,333236 +announce-response-to-bytes,,,,,363124883.0,ns,333690 +announce-response-to-bytes,,,,,266572157.0,ns,334144 +announce-response-to-bytes,,,,,260228688.0,ns,334598 +announce-response-to-bytes,,,,,259246595.0,ns,335052 +announce-response-to-bytes,,,,,258000194.0,ns,335506 +announce-response-to-bytes,,,,,265576033.0,ns,335960 +announce-response-to-bytes,,,,,268913493.0,ns,336414 +announce-response-to-bytes,,,,,250921305.0,ns,336868 +announce-response-to-bytes,,,,,266606744.0,ns,337322 +announce-response-to-bytes,,,,,272879165.0,ns,337776 +announce-response-to-bytes,,,,,277056780.0,ns,338230 +announce-response-to-bytes,,,,,261120451.0,ns,338684 +announce-response-to-bytes,,,,,267794083.0,ns,339138 +announce-response-to-bytes,,,,,297070504.0,ns,339592 +announce-response-to-bytes,,,,,337838824.0,ns,340046 +announce-response-to-bytes,,,,,328911324.0,ns,340500 +announce-response-to-bytes,,,,,315034309.0,ns,340954 +announce-response-to-bytes,,,,,264502129.0,ns,341408 +announce-response-to-bytes,,,,,276203066.0,ns,341862 +announce-response-to-bytes,,,,,262188485.0,ns,342316 +announce-response-to-bytes,,,,,268757454.0,ns,342770 +announce-response-to-bytes,,,,,271572739.0,ns,343224 +announce-response-to-bytes,,,,,263080044.0,ns,343678 +announce-response-to-bytes,,,,,310686789.0,ns,344132 +announce-response-to-bytes,,,,,261343941.0,ns,344586 +announce-response-to-bytes,,,,,268306684.0,ns,345040 +announce-response-to-bytes,,,,,275757781.0,ns,345494 +announce-response-to-bytes,,,,,298809924.0,ns,345948 +announce-response-to-bytes,,,,,264705818.0,ns,346402 +announce-response-to-bytes,,,,,257199192.0,ns,346856 +announce-response-to-bytes,,,,,302033750.0,ns,347310 +announce-response-to-bytes,,,,,274051014.0,ns,347764 +announce-response-to-bytes,,,,,289835877.0,ns,348218 +announce-response-to-bytes,,,,,284281963.0,ns,348672 +announce-response-to-bytes,,,,,268945048.0,ns,349126 +announce-response-to-bytes,,,,,277065725.0,ns,349580 +announce-response-to-bytes,,,,,298846867.0,ns,350034 +announce-response-to-bytes,,,,,280237701.0,ns,350488 +announce-response-to-bytes,,,,,266690515.0,ns,350942 +announce-response-to-bytes,,,,,274718361.0,ns,351396 +announce-response-to-bytes,,,,,270834145.0,ns,351850 +announce-response-to-bytes,,,,,284800547.0,ns,352304 +announce-response-to-bytes,,,,,284977682.0,ns,352758 +announce-response-to-bytes,,,,,271905112.0,ns,353212 +announce-response-to-bytes,,,,,267346474.0,ns,353666 +announce-response-to-bytes,,,,,313184251.0,ns,354120 +announce-response-to-bytes,,,,,302531823.0,ns,354574 +announce-response-to-bytes,,,,,268311892.0,ns,355028 +announce-response-to-bytes,,,,,272260457.0,ns,355482 +announce-response-to-bytes,,,,,312345644.0,ns,355936 +announce-response-to-bytes,,,,,318634991.0,ns,356390 +announce-response-to-bytes,,,,,275580061.0,ns,356844 +announce-response-to-bytes,,,,,316819757.0,ns,357298 +announce-response-to-bytes,,,,,275813126.0,ns,357752 +announce-response-to-bytes,,,,,269193796.0,ns,358206 +announce-response-to-bytes,,,,,275968781.0,ns,358660 +announce-response-to-bytes,,,,,366920258.0,ns,359114 +announce-response-to-bytes,,,,,297248429.0,ns,359568 +announce-response-to-bytes,,,,,275269024.0,ns,360022 +announce-response-to-bytes,,,,,281615886.0,ns,360476 +announce-response-to-bytes,,,,,284409807.0,ns,360930 +announce-response-to-bytes,,,,,278603648.0,ns,361384 +announce-response-to-bytes,,,,,284418789.0,ns,361838 +announce-response-to-bytes,,,,,284743354.0,ns,362292 +announce-response-to-bytes,,,,,297439675.0,ns,362746 +announce-response-to-bytes,,,,,290438634.0,ns,363200 +announce-response-to-bytes,,,,,284248460.0,ns,363654 +announce-response-to-bytes,,,,,288445668.0,ns,364108 +announce-response-to-bytes,,,,,312739465.0,ns,364562 +announce-response-to-bytes,,,,,275808459.0,ns,365016 +announce-response-to-bytes,,,,,279938580.0,ns,365470 +announce-response-to-bytes,,,,,287739526.0,ns,365924 +announce-response-to-bytes,,,,,278673840.0,ns,366378 +announce-response-to-bytes,,,,,293439850.0,ns,366832 +announce-response-to-bytes,,,,,290152528.0,ns,367286 +announce-response-to-bytes,,,,,286888832.0,ns,367740 +announce-response-to-bytes,,,,,316372353.0,ns,368194 +announce-response-to-bytes,,,,,295213242.0,ns,368648 +announce-response-to-bytes,,,,,278965451.0,ns,369102 +announce-response-to-bytes,,,,,293404295.0,ns,369556 +announce-response-to-bytes,,,,,289427801.0,ns,370010 +announce-response-to-bytes,,,,,295309416.0,ns,370464 +announce-response-to-bytes,,,,,288546007.0,ns,370918 +announce-response-to-bytes,,,,,319048943.0,ns,371372 +announce-response-to-bytes,,,,,370474979.0,ns,371826 +announce-response-to-bytes,,,,,287372380.0,ns,372280 +announce-response-to-bytes,,,,,325573866.0,ns,372734 +announce-response-to-bytes,,,,,290125824.0,ns,373188 +announce-response-to-bytes,,,,,282307089.0,ns,373642 +announce-response-to-bytes,,,,,311806351.0,ns,374096 +announce-response-to-bytes,,,,,295532074.0,ns,374550 +announce-response-to-bytes,,,,,316644308.0,ns,375004 +announce-response-to-bytes,,,,,378234391.0,ns,375458 +announce-response-to-bytes,,,,,325534557.0,ns,375912 +announce-response-to-bytes,,,,,295965582.0,ns,376366 +announce-response-to-bytes,,,,,296487475.0,ns,376820 +announce-response-to-bytes,,,,,295388977.0,ns,377274 +announce-response-to-bytes,,,,,303106994.0,ns,377728 +announce-response-to-bytes,,,,,348270451.0,ns,378182 +announce-response-to-bytes,,,,,341498015.0,ns,378636 +announce-response-to-bytes,,,,,297864613.0,ns,379090 +announce-response-to-bytes,,,,,293888708.0,ns,379544 +announce-response-to-bytes,,,,,295261194.0,ns,379998 +announce-response-to-bytes,,,,,388387054.0,ns,380452 +announce-response-to-bytes,,,,,320793325.0,ns,380906 +announce-response-to-bytes,,,,,294055585.0,ns,381360 +announce-response-to-bytes,,,,,335121383.0,ns,381814 +announce-response-to-bytes,,,,,297618916.0,ns,382268 +announce-response-to-bytes,,,,,293449464.0,ns,382722 +announce-response-to-bytes,,,,,305117206.0,ns,383176 +announce-response-to-bytes,,,,,289757703.0,ns,383630 +announce-response-to-bytes,,,,,296561125.0,ns,384084 +announce-response-to-bytes,,,,,310132843.0,ns,384538 +announce-response-to-bytes,,,,,322745341.0,ns,384992 +announce-response-to-bytes,,,,,307869215.0,ns,385446 +announce-response-to-bytes,,,,,295246813.0,ns,385900 +announce-response-to-bytes,,,,,340148719.0,ns,386354 +announce-response-to-bytes,,,,,306166784.0,ns,386808 +announce-response-to-bytes,,,,,293293184.0,ns,387262 +announce-response-to-bytes,,,,,305228949.0,ns,387716 +announce-response-to-bytes,,,,,299807429.0,ns,388170 +announce-response-to-bytes,,,,,305688062.0,ns,388624 +announce-response-to-bytes,,,,,316935529.0,ns,389078 +announce-response-to-bytes,,,,,299223439.0,ns,389532 +announce-response-to-bytes,,,,,305982925.0,ns,389986 +announce-response-to-bytes,,,,,306049675.0,ns,390440 +announce-response-to-bytes,,,,,390558060.0,ns,390894 +announce-response-to-bytes,,,,,318663819.0,ns,391348 +announce-response-to-bytes,,,,,324990008.0,ns,391802 +announce-response-to-bytes,,,,,410223685.0,ns,392256 +announce-response-to-bytes,,,,,316206340.0,ns,392710 +announce-response-to-bytes,,,,,295467458.0,ns,393164 +announce-response-to-bytes,,,,,319102192.0,ns,393618 +announce-response-to-bytes,,,,,294361374.0,ns,394072 +announce-response-to-bytes,,,,,296562025.0,ns,394526 +announce-response-to-bytes,,,,,306266810.0,ns,394980 +announce-response-to-bytes,,,,,408098817.0,ns,395434 +announce-response-to-bytes,,,,,324022105.0,ns,395888 +announce-response-to-bytes,,,,,347399203.0,ns,396342 +announce-response-to-bytes,,,,,299714255.0,ns,396796 +announce-response-to-bytes,,,,,297665293.0,ns,397250 +announce-response-to-bytes,,,,,324251184.0,ns,397704 +announce-response-to-bytes,,,,,343706470.0,ns,398158 +announce-response-to-bytes,,,,,302996025.0,ns,398612 +announce-response-to-bytes,,,,,315496703.0,ns,399066 +announce-response-to-bytes,,,,,318954906.0,ns,399520 +announce-response-to-bytes,,,,,306220743.0,ns,399974 +announce-response-to-bytes,,,,,327474341.0,ns,400428 +announce-response-to-bytes,,,,,333535282.0,ns,400882 +announce-response-to-bytes,,,,,311543123.0,ns,401336 +announce-response-to-bytes,,,,,304456544.0,ns,401790 +announce-response-to-bytes,,,,,316442355.0,ns,402244 +announce-response-to-bytes,,,,,331014547.0,ns,402698 +announce-response-to-bytes,,,,,327869569.0,ns,403152 +announce-response-to-bytes,,,,,326019915.0,ns,403606 +announce-response-to-bytes,,,,,315004463.0,ns,404060 +announce-response-to-bytes,,,,,345699633.0,ns,404514 +announce-response-to-bytes,,,,,314355300.0,ns,404968 +announce-response-to-bytes,,,,,320981892.0,ns,405422 +announce-response-to-bytes,,,,,311047847.0,ns,405876 +announce-response-to-bytes,,,,,324710480.0,ns,406330 +announce-response-to-bytes,,,,,320074546.0,ns,406784 +announce-response-to-bytes,,,,,307822024.0,ns,407238 +announce-response-to-bytes,,,,,355312214.0,ns,407692 +announce-response-to-bytes,,,,,318344223.0,ns,408146 +announce-response-to-bytes,,,,,323725480.0,ns,408600 +announce-response-to-bytes,,,,,431904721.0,ns,409054 +announce-response-to-bytes,,,,,320677523.0,ns,409508 +announce-response-to-bytes,,,,,310018007.0,ns,409962 +announce-response-to-bytes,,,,,330275114.0,ns,410416 +announce-response-to-bytes,,,,,315216465.0,ns,410870 +announce-response-to-bytes,,,,,443638856.0,ns,411324 +announce-response-to-bytes,,,,,315681184.0,ns,411778 +announce-response-to-bytes,,,,,327473350.0,ns,412232 +announce-response-to-bytes,,,,,327072928.0,ns,412686 +announce-response-to-bytes,,,,,320399284.0,ns,413140 +announce-response-to-bytes,,,,,359983328.0,ns,413594 +announce-response-to-bytes,,,,,368163334.0,ns,414048 +announce-response-to-bytes,,,,,379981772.0,ns,414502 +announce-response-to-bytes,,,,,338885639.0,ns,414956 +announce-response-to-bytes,,,,,319062844.0,ns,415410 +announce-response-to-bytes,,,,,333480342.0,ns,415864 +announce-response-to-bytes,,,,,324392640.0,ns,416318 +announce-response-to-bytes,,,,,364863509.0,ns,416772 +announce-response-to-bytes,,,,,336683440.0,ns,417226 +announce-response-to-bytes,,,,,334514351.0,ns,417680 +announce-response-to-bytes,,,,,323009886.0,ns,418134 +announce-response-to-bytes,,,,,325235241.0,ns,418588 +announce-response-to-bytes,,,,,327096903.0,ns,419042 +announce-response-to-bytes,,,,,331598753.0,ns,419496 +announce-response-to-bytes,,,,,448017616.0,ns,419950 +announce-response-to-bytes,,,,,324304028.0,ns,420404 +announce-response-to-bytes,,,,,357442388.0,ns,420858 +announce-response-to-bytes,,,,,328798225.0,ns,421312 +announce-response-to-bytes,,,,,372113775.0,ns,421766 +announce-response-to-bytes,,,,,346022701.0,ns,422220 +announce-response-to-bytes,,,,,350048623.0,ns,422674 +announce-response-to-bytes,,,,,332412823.0,ns,423128 +announce-response-to-bytes,,,,,328091028.0,ns,423582 +announce-response-to-bytes,,,,,325228983.0,ns,424036 +announce-response-to-bytes,,,,,338240603.0,ns,424490 +announce-response-to-bytes,,,,,355416467.0,ns,424944 +announce-response-to-bytes,,,,,362192302.0,ns,425398 +announce-response-to-bytes,,,,,326836858.0,ns,425852 +announce-response-to-bytes,,,,,328823688.0,ns,426306 +announce-response-to-bytes,,,,,337332801.0,ns,426760 +announce-response-to-bytes,,,,,328649818.0,ns,427214 +announce-response-to-bytes,,,,,370144388.0,ns,427668 +announce-response-to-bytes,,,,,341015896.0,ns,428122 +announce-response-to-bytes,,,,,328324729.0,ns,428576 +announce-response-to-bytes,,,,,355010588.0,ns,429030 +announce-response-to-bytes,,,,,340989496.0,ns,429484 +announce-response-to-bytes,,,,,334527053.0,ns,429938 +announce-response-to-bytes,,,,,343925490.0,ns,430392 +announce-response-to-bytes,,,,,335182519.0,ns,430846 +announce-response-to-bytes,,,,,339142211.0,ns,431300 +announce-response-to-bytes,,,,,338464237.0,ns,431754 +announce-response-to-bytes,,,,,362881756.0,ns,432208 +announce-response-to-bytes,,,,,345786326.0,ns,432662 +announce-response-to-bytes,,,,,366570560.0,ns,433116 +announce-response-to-bytes,,,,,376993086.0,ns,433570 +announce-response-to-bytes,,,,,372948333.0,ns,434024 +announce-response-to-bytes,,,,,378375295.0,ns,434478 +announce-response-to-bytes,,,,,335838388.0,ns,434932 +announce-response-to-bytes,,,,,373267271.0,ns,435386 +announce-response-to-bytes,,,,,452810414.0,ns,435840 +announce-response-to-bytes,,,,,359384109.0,ns,436294 +announce-response-to-bytes,,,,,358555506.0,ns,436748 +announce-response-to-bytes,,,,,338560408.0,ns,437202 +announce-response-to-bytes,,,,,410960580.0,ns,437656 +announce-response-to-bytes,,,,,380614341.0,ns,438110 +announce-response-to-bytes,,,,,354449936.0,ns,438564 +announce-response-to-bytes,,,,,328123002.0,ns,439018 +announce-response-to-bytes,,,,,337401615.0,ns,439472 +announce-response-to-bytes,,,,,336193685.0,ns,439926 +announce-response-to-bytes,,,,,366427805.0,ns,440380 +announce-response-to-bytes,,,,,355942636.0,ns,440834 +announce-response-to-bytes,,,,,351054033.0,ns,441288 +announce-response-to-bytes,,,,,338782705.0,ns,441742 +announce-response-to-bytes,,,,,342917207.0,ns,442196 +announce-response-to-bytes,,,,,344690826.0,ns,442650 +announce-response-to-bytes,,,,,341635371.0,ns,443104 +announce-response-to-bytes,,,,,360412886.0,ns,443558 +announce-response-to-bytes,,,,,358031707.0,ns,444012 +announce-response-to-bytes,,,,,342841668.0,ns,444466 +announce-response-to-bytes,,,,,352303859.0,ns,444920 +announce-response-to-bytes,,,,,432951100.0,ns,445374 +announce-response-to-bytes,,,,,368307092.0,ns,445828 +announce-response-to-bytes,,,,,351872053.0,ns,446282 +announce-response-to-bytes,,,,,461614298.0,ns,446736 +announce-response-to-bytes,,,,,348807260.0,ns,447190 +announce-response-to-bytes,,,,,371148309.0,ns,447644 +announce-response-to-bytes,,,,,372346802.0,ns,448098 +announce-response-to-bytes,,,,,359296150.0,ns,448552 +announce-response-to-bytes,,,,,383177361.0,ns,449006 +announce-response-to-bytes,,,,,339618919.0,ns,449460 +announce-response-to-bytes,,,,,337955907.0,ns,449914 +announce-response-to-bytes,,,,,391271484.0,ns,450368 +announce-response-to-bytes,,,,,356328973.0,ns,450822 +announce-response-to-bytes,,,,,352205613.0,ns,451276 +announce-response-to-bytes,,,,,350664936.0,ns,451730 +announce-response-to-bytes,,,,,364194590.0,ns,452184 +announce-response-to-bytes,,,,,350638811.0,ns,452638 +announce-response-to-bytes,,,,,357475153.0,ns,453092 +announce-response-to-bytes,,,,,350818125.0,ns,453546 +announce-response-to-bytes,,,,,357442938.0,ns,454000 diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/sample.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/sample.json new file mode 100644 index 0000000..17824ac --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/sample.json @@ -0,0 +1 @@ +{"sampling_mode":"Linear","iters":[454.0,908.0,1362.0,1816.0,2270.0,2724.0,3178.0,3632.0,4086.0,4540.0,4994.0,5448.0,5902.0,6356.0,6810.0,7264.0,7718.0,8172.0,8626.0,9080.0,9534.0,9988.0,10442.0,10896.0,11350.0,11804.0,12258.0,12712.0,13166.0,13620.0,14074.0,14528.0,14982.0,15436.0,15890.0,16344.0,16798.0,17252.0,17706.0,18160.0,18614.0,19068.0,19522.0,19976.0,20430.0,20884.0,21338.0,21792.0,22246.0,22700.0,23154.0,23608.0,24062.0,24516.0,24970.0,25424.0,25878.0,26332.0,26786.0,27240.0,27694.0,28148.0,28602.0,29056.0,29510.0,29964.0,30418.0,30872.0,31326.0,31780.0,32234.0,32688.0,33142.0,33596.0,34050.0,34504.0,34958.0,35412.0,35866.0,36320.0,36774.0,37228.0,37682.0,38136.0,38590.0,39044.0,39498.0,39952.0,40406.0,40860.0,41314.0,41768.0,42222.0,42676.0,43130.0,43584.0,44038.0,44492.0,44946.0,45400.0,45854.0,46308.0,46762.0,47216.0,47670.0,48124.0,48578.0,49032.0,49486.0,49940.0,50394.0,50848.0,51302.0,51756.0,52210.0,52664.0,53118.0,53572.0,54026.0,54480.0,54934.0,55388.0,55842.0,56296.0,56750.0,57204.0,57658.0,58112.0,58566.0,59020.0,59474.0,59928.0,60382.0,60836.0,61290.0,61744.0,62198.0,62652.0,63106.0,63560.0,64014.0,64468.0,64922.0,65376.0,65830.0,66284.0,66738.0,67192.0,67646.0,68100.0,68554.0,69008.0,69462.0,69916.0,70370.0,70824.0,71278.0,71732.0,72186.0,72640.0,73094.0,73548.0,74002.0,74456.0,74910.0,75364.0,75818.0,76272.0,76726.0,77180.0,77634.0,78088.0,78542.0,78996.0,79450.0,79904.0,80358.0,80812.0,81266.0,81720.0,82174.0,82628.0,83082.0,83536.0,83990.0,84444.0,84898.0,85352.0,85806.0,86260.0,86714.0,87168.0,87622.0,88076.0,88530.0,88984.0,89438.0,89892.0,90346.0,90800.0,91254.0,91708.0,92162.0,92616.0,93070.0,93524.0,93978.0,94432.0,94886.0,95340.0,95794.0,96248.0,96702.0,97156.0,97610.0,98064.0,98518.0,98972.0,99426.0,99880.0,100334.0,100788.0,101242.0,101696.0,102150.0,102604.0,103058.0,103512.0,103966.0,104420.0,104874.0,105328.0,105782.0,106236.0,106690.0,107144.0,107598.0,108052.0,108506.0,108960.0,109414.0,109868.0,110322.0,110776.0,111230.0,111684.0,112138.0,112592.0,113046.0,113500.0,113954.0,114408.0,114862.0,115316.0,115770.0,116224.0,116678.0,117132.0,117586.0,118040.0,118494.0,118948.0,119402.0,119856.0,120310.0,120764.0,121218.0,121672.0,122126.0,122580.0,123034.0,123488.0,123942.0,124396.0,124850.0,125304.0,125758.0,126212.0,126666.0,127120.0,127574.0,128028.0,128482.0,128936.0,129390.0,129844.0,130298.0,130752.0,131206.0,131660.0,132114.0,132568.0,133022.0,133476.0,133930.0,134384.0,134838.0,135292.0,135746.0,136200.0,136654.0,137108.0,137562.0,138016.0,138470.0,138924.0,139378.0,139832.0,140286.0,140740.0,141194.0,141648.0,142102.0,142556.0,143010.0,143464.0,143918.0,144372.0,144826.0,145280.0,145734.0,146188.0,146642.0,147096.0,147550.0,148004.0,148458.0,148912.0,149366.0,149820.0,150274.0,150728.0,151182.0,151636.0,152090.0,152544.0,152998.0,153452.0,153906.0,154360.0,154814.0,155268.0,155722.0,156176.0,156630.0,157084.0,157538.0,157992.0,158446.0,158900.0,159354.0,159808.0,160262.0,160716.0,161170.0,161624.0,162078.0,162532.0,162986.0,163440.0,163894.0,164348.0,164802.0,165256.0,165710.0,166164.0,166618.0,167072.0,167526.0,167980.0,168434.0,168888.0,169342.0,169796.0,170250.0,170704.0,171158.0,171612.0,172066.0,172520.0,172974.0,173428.0,173882.0,174336.0,174790.0,175244.0,175698.0,176152.0,176606.0,177060.0,177514.0,177968.0,178422.0,178876.0,179330.0,179784.0,180238.0,180692.0,181146.0,181600.0,182054.0,182508.0,182962.0,183416.0,183870.0,184324.0,184778.0,185232.0,185686.0,186140.0,186594.0,187048.0,187502.0,187956.0,188410.0,188864.0,189318.0,189772.0,190226.0,190680.0,191134.0,191588.0,192042.0,192496.0,192950.0,193404.0,193858.0,194312.0,194766.0,195220.0,195674.0,196128.0,196582.0,197036.0,197490.0,197944.0,198398.0,198852.0,199306.0,199760.0,200214.0,200668.0,201122.0,201576.0,202030.0,202484.0,202938.0,203392.0,203846.0,204300.0,204754.0,205208.0,205662.0,206116.0,206570.0,207024.0,207478.0,207932.0,208386.0,208840.0,209294.0,209748.0,210202.0,210656.0,211110.0,211564.0,212018.0,212472.0,212926.0,213380.0,213834.0,214288.0,214742.0,215196.0,215650.0,216104.0,216558.0,217012.0,217466.0,217920.0,218374.0,218828.0,219282.0,219736.0,220190.0,220644.0,221098.0,221552.0,222006.0,222460.0,222914.0,223368.0,223822.0,224276.0,224730.0,225184.0,225638.0,226092.0,226546.0,227000.0,227454.0,227908.0,228362.0,228816.0,229270.0,229724.0,230178.0,230632.0,231086.0,231540.0,231994.0,232448.0,232902.0,233356.0,233810.0,234264.0,234718.0,235172.0,235626.0,236080.0,236534.0,236988.0,237442.0,237896.0,238350.0,238804.0,239258.0,239712.0,240166.0,240620.0,241074.0,241528.0,241982.0,242436.0,242890.0,243344.0,243798.0,244252.0,244706.0,245160.0,245614.0,246068.0,246522.0,246976.0,247430.0,247884.0,248338.0,248792.0,249246.0,249700.0,250154.0,250608.0,251062.0,251516.0,251970.0,252424.0,252878.0,253332.0,253786.0,254240.0,254694.0,255148.0,255602.0,256056.0,256510.0,256964.0,257418.0,257872.0,258326.0,258780.0,259234.0,259688.0,260142.0,260596.0,261050.0,261504.0,261958.0,262412.0,262866.0,263320.0,263774.0,264228.0,264682.0,265136.0,265590.0,266044.0,266498.0,266952.0,267406.0,267860.0,268314.0,268768.0,269222.0,269676.0,270130.0,270584.0,271038.0,271492.0,271946.0,272400.0,272854.0,273308.0,273762.0,274216.0,274670.0,275124.0,275578.0,276032.0,276486.0,276940.0,277394.0,277848.0,278302.0,278756.0,279210.0,279664.0,280118.0,280572.0,281026.0,281480.0,281934.0,282388.0,282842.0,283296.0,283750.0,284204.0,284658.0,285112.0,285566.0,286020.0,286474.0,286928.0,287382.0,287836.0,288290.0,288744.0,289198.0,289652.0,290106.0,290560.0,291014.0,291468.0,291922.0,292376.0,292830.0,293284.0,293738.0,294192.0,294646.0,295100.0,295554.0,296008.0,296462.0,296916.0,297370.0,297824.0,298278.0,298732.0,299186.0,299640.0,300094.0,300548.0,301002.0,301456.0,301910.0,302364.0,302818.0,303272.0,303726.0,304180.0,304634.0,305088.0,305542.0,305996.0,306450.0,306904.0,307358.0,307812.0,308266.0,308720.0,309174.0,309628.0,310082.0,310536.0,310990.0,311444.0,311898.0,312352.0,312806.0,313260.0,313714.0,314168.0,314622.0,315076.0,315530.0,315984.0,316438.0,316892.0,317346.0,317800.0,318254.0,318708.0,319162.0,319616.0,320070.0,320524.0,320978.0,321432.0,321886.0,322340.0,322794.0,323248.0,323702.0,324156.0,324610.0,325064.0,325518.0,325972.0,326426.0,326880.0,327334.0,327788.0,328242.0,328696.0,329150.0,329604.0,330058.0,330512.0,330966.0,331420.0,331874.0,332328.0,332782.0,333236.0,333690.0,334144.0,334598.0,335052.0,335506.0,335960.0,336414.0,336868.0,337322.0,337776.0,338230.0,338684.0,339138.0,339592.0,340046.0,340500.0,340954.0,341408.0,341862.0,342316.0,342770.0,343224.0,343678.0,344132.0,344586.0,345040.0,345494.0,345948.0,346402.0,346856.0,347310.0,347764.0,348218.0,348672.0,349126.0,349580.0,350034.0,350488.0,350942.0,351396.0,351850.0,352304.0,352758.0,353212.0,353666.0,354120.0,354574.0,355028.0,355482.0,355936.0,356390.0,356844.0,357298.0,357752.0,358206.0,358660.0,359114.0,359568.0,360022.0,360476.0,360930.0,361384.0,361838.0,362292.0,362746.0,363200.0,363654.0,364108.0,364562.0,365016.0,365470.0,365924.0,366378.0,366832.0,367286.0,367740.0,368194.0,368648.0,369102.0,369556.0,370010.0,370464.0,370918.0,371372.0,371826.0,372280.0,372734.0,373188.0,373642.0,374096.0,374550.0,375004.0,375458.0,375912.0,376366.0,376820.0,377274.0,377728.0,378182.0,378636.0,379090.0,379544.0,379998.0,380452.0,380906.0,381360.0,381814.0,382268.0,382722.0,383176.0,383630.0,384084.0,384538.0,384992.0,385446.0,385900.0,386354.0,386808.0,387262.0,387716.0,388170.0,388624.0,389078.0,389532.0,389986.0,390440.0,390894.0,391348.0,391802.0,392256.0,392710.0,393164.0,393618.0,394072.0,394526.0,394980.0,395434.0,395888.0,396342.0,396796.0,397250.0,397704.0,398158.0,398612.0,399066.0,399520.0,399974.0,400428.0,400882.0,401336.0,401790.0,402244.0,402698.0,403152.0,403606.0,404060.0,404514.0,404968.0,405422.0,405876.0,406330.0,406784.0,407238.0,407692.0,408146.0,408600.0,409054.0,409508.0,409962.0,410416.0,410870.0,411324.0,411778.0,412232.0,412686.0,413140.0,413594.0,414048.0,414502.0,414956.0,415410.0,415864.0,416318.0,416772.0,417226.0,417680.0,418134.0,418588.0,419042.0,419496.0,419950.0,420404.0,420858.0,421312.0,421766.0,422220.0,422674.0,423128.0,423582.0,424036.0,424490.0,424944.0,425398.0,425852.0,426306.0,426760.0,427214.0,427668.0,428122.0,428576.0,429030.0,429484.0,429938.0,430392.0,430846.0,431300.0,431754.0,432208.0,432662.0,433116.0,433570.0,434024.0,434478.0,434932.0,435386.0,435840.0,436294.0,436748.0,437202.0,437656.0,438110.0,438564.0,439018.0,439472.0,439926.0,440380.0,440834.0,441288.0,441742.0,442196.0,442650.0,443104.0,443558.0,444012.0,444466.0,444920.0,445374.0,445828.0,446282.0,446736.0,447190.0,447644.0,448098.0,448552.0,449006.0,449460.0,449914.0,450368.0,450822.0,451276.0,451730.0,452184.0,452638.0,453092.0,453546.0,454000.0],"times":[401430.0,667976.0,1025418.0,1760457.0,2773514.0,2931972.0,3005834.0,3173571.0,3046619.0,3328132.0,3720452.0,4090810.0,4484545.0,4812796.0,4908935.0,6288992.0,6077287.0,6176622.0,6485603.0,6758164.0,7122672.0,7474194.0,7915038.0,8363532.0,11128327.0,9225973.0,9294452.0,9556524.0,9912145.0,10414551.0,13321072.0,10618032.0,12276543.0,14405695.0,11831439.0,15333712.0,12449106.0,13055819.0,13379622.0,13554285.0,13915354.0,13918870.0,14529285.0,15208879.0,35365935.0,35391975.0,32370107.0,29786276.0,27895965.0,26050119.0,25461907.0,24759043.0,23173216.0,21967972.0,21480828.0,23522828.0,23447193.0,23068253.0,19623914.0,19997173.0,20687167.0,22585354.0,22761581.0,21631034.0,30078711.0,32243243.0,29687350.0,28020842.0,27195725.0,26288864.0,24869828.0,24636026.0,24642881.0,24985993.0,25912676.0,28150331.0,27374609.0,29088827.0,26495182.0,30321313.0,28349286.0,29293434.0,29357346.0,29627752.0,29182241.0,29163745.0,29498414.0,33033710.0,30856419.0,31083017.0,31074225.0,44616166.0,42008316.0,37734299.0,36964876.0,39743870.0,32169980.0,36547954.0,33679339.0,34398414.0,37019472.0,34286398.0,34492101.0,36504626.0,51376487.0,47886653.0,44420256.0,43905657.0,39962929.0,37223378.0,42935243.0,38532368.0,42393190.0,41997163.0,38993650.0,42504681.0,38417329.0,39048380.0,40756221.0,44107559.0,40826380.0,44788659.0,41148476.0,42033177.0,45154098.0,43171854.0,45861192.0,46899987.0,43436436.0,55520040.0,59813174.0,56002247.0,47217552.0,45115273.0,55174899.0,48991179.0,46507742.0,49404150.0,46546074.0,53173411.0,48597379.0,51232324.0,48940226.0,51389031.0,52886229.0,51579257.0,50045827.0,51090435.0,50168765.0,50585612.0,51915277.0,55172077.0,58296604.0,71262556.0,65626454.0,61401919.0,53444318.0,56772754.0,54228308.0,64026040.0,73088637.0,64530803.0,58068750.0,58649987.0,54654937.0,58627628.0,59489562.0,57041165.0,61189228.0,59968331.0,57680550.0,61277175.0,114510262.0,97989580.0,79340870.0,66757350.0,63587361.0,61030198.0,60051965.0,64272121.0,61413799.0,61943571.0,71371288.0,61270697.0,62369625.0,66659623.0,63397171.0,113134517.0,108420521.0,83354595.0,73340464.0,68614513.0,71416685.0,69822100.0,69366564.0,67171782.0,70919293.0,70310196.0,67274380.0,71666094.0,72700249.0,68034994.0,81137150.0,89751869.0,75189615.0,77163018.0,76885868.0,96298749.0,80598989.0,70731444.0,71193713.0,75778589.0,76779761.0,73293733.0,100871546.0,87401859.0,75113910.0,74899683.0,73397098.0,74433784.0,75948784.0,78760151.0,77567043.0,86814772.0,151206275.0,104296881.0,92357440.0,83609302.0,78863635.0,106344548.0,88457924.0,78020903.0,81690727.0,82896527.0,79508368.0,82985455.0,84652782.0,79978207.0,87981709.0,159956575.0,114310461.0,95378693.0,92728676.0,87124067.0,85703206.0,85862053.0,89982004.0,93100414.0,83990160.0,94475296.0,92055569.0,89352370.0,85358383.0,89276389.0,87367587.0,90582297.0,91725575.0,87388798.0,87928110.0,88976306.0,91631113.0,86521557.0,91757076.0,99682167.0,92593955.0,95259570.0,90181764.0,94443119.0,91524080.0,94477809.0,99200558.0,98619448.0,99893491.0,100295004.0,96485808.0,94283354.0,101180179.0,100585524.0,102885850.0,101063306.0,133843830.0,158645712.0,122169820.0,97533036.0,103794040.0,101696375.0,105048423.0,97382468.0,102254749.0,104069503.0,99651353.0,102823512.0,99604245.0,132082275.0,108193195.0,103798546.0,101037951.0,101509377.0,104133401.0,113868082.0,104754114.0,108023631.0,108221898.0,128146191.0,116538692.0,103148974.0,106027570.0,106781849.0,111416888.0,117275860.0,105629357.0,113008523.0,111500588.0,117592176.0,138573402.0,120568948.0,128509197.0,130109399.0,111099822.0,113706590.0,114090399.0,111581633.0,116715433.0,111743608.0,115980102.0,116165004.0,117840515.0,114481663.0,121905531.0,114857168.0,122478128.0,116461625.0,118914865.0,116137184.0,116110817.0,114904319.0,203420209.0,147176776.0,119130160.0,118124938.0,121226369.0,120159833.0,123224429.0,123683081.0,125055031.0,117320747.0,122586708.0,157466162.0,122166399.0,126265894.0,121922877.0,122422679.0,129213097.0,119991894.0,126630881.0,126730358.0,128928650.0,153712298.0,128922821.0,122210095.0,127808674.0,131592352.0,123790925.0,126473221.0,128068018.0,125535935.0,149423183.0,142909056.0,129140128.0,157696636.0,138497001.0,129266904.0,127780811.0,164397546.0,130232792.0,134136458.0,135973193.0,130869078.0,129854172.0,143467685.0,136134658.0,134711368.0,143020541.0,131241328.0,141298924.0,161916202.0,147070198.0,136252979.0,134014277.0,136773736.0,183082874.0,200961949.0,140175423.0,136609615.0,142932671.0,133909487.0,144758756.0,134594385.0,136881780.0,228618847.0,167984860.0,137027114.0,165625955.0,148708745.0,140858973.0,140789170.0,140185347.0,141982083.0,142501858.0,143367879.0,147731030.0,143819588.0,145475937.0,150970716.0,143902811.0,146856226.0,186377324.0,142702270.0,147189632.0,155494491.0,177852045.0,151691540.0,149968503.0,154219258.0,176730990.0,163733009.0,150078710.0,155011160.0,145642824.0,154018323.0,154932805.0,183096342.0,154786350.0,160676315.0,181589572.0,153417039.0,165686714.0,148099636.0,155902020.0,152546539.0,151356795.0,151572866.0,163695045.0,152585139.0,160440772.0,153371615.0,199721452.0,167832199.0,156673062.0,159309870.0,159502831.0,154617677.0,157046936.0,159024613.0,161723352.0,170463400.0,186043394.0,165538684.0,157513302.0,164801747.0,162628332.0,168514860.0,164495431.0,157241920.0,263440212.0,176988605.0,162036114.0,166418986.0,238071956.0,211365092.0,166222569.0,163265361.0,162303160.0,173062999.0,173733449.0,198449731.0,167237507.0,170723360.0,180987802.0,206170231.0,244701925.0,172946872.0,164867992.0,170502704.0,176632681.0,209222843.0,172378327.0,178923617.0,171037007.0,176514736.0,172342821.0,171481037.0,167859194.0,177469350.0,168760325.0,172392820.0,169906576.0,171789091.0,188119846.0,194125298.0,173322626.0,210832158.0,176129605.0,180015767.0,247020460.0,226417665.0,177105640.0,176160761.0,182796592.0,181611228.0,180289095.0,215888256.0,180433113.0,174944560.0,180350408.0,179463547.0,182184241.0,183561335.0,189635250.0,184613902.0,181214727.0,191488918.0,191947712.0,183642884.0,263683701.0,221740575.0,179043045.0,193570869.0,184855232.0,189125421.0,178310424.0,234040798.0,189436473.0,185248442.0,184484232.0,205194381.0,186457791.0,182545024.0,208744968.0,277153632.0,190022319.0,192405531.0,186941376.0,225843858.0,207479525.0,189218841.0,194333414.0,194553633.0,197281758.0,192678824.0,194953869.0,238223707.0,276973132.0,190717236.0,198253360.0,227633139.0,191242806.0,196840834.0,197618214.0,294034310.0,213295013.0,199769357.0,194119968.0,299180927.0,215136032.0,230794222.0,204279647.0,205778394.0,209487042.0,215796962.0,295432707.0,215876240.0,195367269.0,200968544.0,208539705.0,203401687.0,206014845.0,204010711.0,203794103.0,208477042.0,210990443.0,234126624.0,210338117.0,203798616.0,203309834.0,210068917.0,210102852.0,245071871.0,206338196.0,205180868.0,206809853.0,205851855.0,213330764.0,247626150.0,206126207.0,209931806.0,206913350.0,207562305.0,213205947.0,240857207.0,208247811.0,245827907.0,207181339.0,213274024.0,216167482.0,214731493.0,211637653.0,241929788.0,217295248.0,214711472.0,212209095.0,248514558.0,223284626.0,220307305.0,212300769.0,241733487.0,230186427.0,247919471.0,227701683.0,216284737.0,257844570.0,220678268.0,213008484.0,223712703.0,216188118.0,214633901.0,220063814.0,216745351.0,218125868.0,224900105.0,215952910.0,223571394.0,220165276.0,228108821.0,221455669.0,235478435.0,247533823.0,228497214.0,220139171.0,219789332.0,249307296.0,242118748.0,261393215.0,322102865.0,252024304.0,231714476.0,236338544.0,241892377.0,322770354.0,225699782.0,260038967.0,225893357.0,231658148.0,348516194.0,240371879.0,234481115.0,245099262.0,232634832.0,316909192.0,268184917.0,233766881.0,234945400.0,269048909.0,246224338.0,225362866.0,235432805.0,236357023.0,234482449.0,264111934.0,339580506.0,244317485.0,246060128.0,241414676.0,240956987.0,264249543.0,321642787.0,240015044.0,259478387.0,239414297.0,254491339.0,248765124.0,253045660.0,272511183.0,244718343.0,253910398.0,240441463.0,266975702.0,255037572.0,236798254.0,238942957.0,246723054.0,250622654.0,242326298.0,241115747.0,237656148.0,281620408.0,246502020.0,275679915.0,261075618.0,247242701.0,277204694.0,249363201.0,247083062.0,252386722.0,262062668.0,257042629.0,285380462.0,260616278.0,270512510.0,279497878.0,265166975.0,284225320.0,257482021.0,250209381.0,251370419.0,264998276.0,285149243.0,288456767.0,268237472.0,281029707.0,252462333.0,260747654.0,291876077.0,260462462.0,250597862.0,284897834.0,278527156.0,257402659.0,258143074.0,270586960.0,250989931.0,252889271.0,335172980.0,304549454.0,363124883.0,266572157.0,260228688.0,259246595.0,258000194.0,265576033.0,268913493.0,250921305.0,266606744.0,272879165.0,277056780.0,261120451.0,267794083.0,297070504.0,337838824.0,328911324.0,315034309.0,264502129.0,276203066.0,262188485.0,268757454.0,271572739.0,263080044.0,310686789.0,261343941.0,268306684.0,275757781.0,298809924.0,264705818.0,257199192.0,302033750.0,274051014.0,289835877.0,284281963.0,268945048.0,277065725.0,298846867.0,280237701.0,266690515.0,274718361.0,270834145.0,284800547.0,284977682.0,271905112.0,267346474.0,313184251.0,302531823.0,268311892.0,272260457.0,312345644.0,318634991.0,275580061.0,316819757.0,275813126.0,269193796.0,275968781.0,366920258.0,297248429.0,275269024.0,281615886.0,284409807.0,278603648.0,284418789.0,284743354.0,297439675.0,290438634.0,284248460.0,288445668.0,312739465.0,275808459.0,279938580.0,287739526.0,278673840.0,293439850.0,290152528.0,286888832.0,316372353.0,295213242.0,278965451.0,293404295.0,289427801.0,295309416.0,288546007.0,319048943.0,370474979.0,287372380.0,325573866.0,290125824.0,282307089.0,311806351.0,295532074.0,316644308.0,378234391.0,325534557.0,295965582.0,296487475.0,295388977.0,303106994.0,348270451.0,341498015.0,297864613.0,293888708.0,295261194.0,388387054.0,320793325.0,294055585.0,335121383.0,297618916.0,293449464.0,305117206.0,289757703.0,296561125.0,310132843.0,322745341.0,307869215.0,295246813.0,340148719.0,306166784.0,293293184.0,305228949.0,299807429.0,305688062.0,316935529.0,299223439.0,305982925.0,306049675.0,390558060.0,318663819.0,324990008.0,410223685.0,316206340.0,295467458.0,319102192.0,294361374.0,296562025.0,306266810.0,408098817.0,324022105.0,347399203.0,299714255.0,297665293.0,324251184.0,343706470.0,302996025.0,315496703.0,318954906.0,306220743.0,327474341.0,333535282.0,311543123.0,304456544.0,316442355.0,331014547.0,327869569.0,326019915.0,315004463.0,345699633.0,314355300.0,320981892.0,311047847.0,324710480.0,320074546.0,307822024.0,355312214.0,318344223.0,323725480.0,431904721.0,320677523.0,310018007.0,330275114.0,315216465.0,443638856.0,315681184.0,327473350.0,327072928.0,320399284.0,359983328.0,368163334.0,379981772.0,338885639.0,319062844.0,333480342.0,324392640.0,364863509.0,336683440.0,334514351.0,323009886.0,325235241.0,327096903.0,331598753.0,448017616.0,324304028.0,357442388.0,328798225.0,372113775.0,346022701.0,350048623.0,332412823.0,328091028.0,325228983.0,338240603.0,355416467.0,362192302.0,326836858.0,328823688.0,337332801.0,328649818.0,370144388.0,341015896.0,328324729.0,355010588.0,340989496.0,334527053.0,343925490.0,335182519.0,339142211.0,338464237.0,362881756.0,345786326.0,366570560.0,376993086.0,372948333.0,378375295.0,335838388.0,373267271.0,452810414.0,359384109.0,358555506.0,338560408.0,410960580.0,380614341.0,354449936.0,328123002.0,337401615.0,336193685.0,366427805.0,355942636.0,351054033.0,338782705.0,342917207.0,344690826.0,341635371.0,360412886.0,358031707.0,342841668.0,352303859.0,432951100.0,368307092.0,351872053.0,461614298.0,348807260.0,371148309.0,372346802.0,359296150.0,383177361.0,339618919.0,337955907.0,391271484.0,356328973.0,352205613.0,350664936.0,364194590.0,350638811.0,357475153.0,350818125.0,357442938.0]} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/tukey.json b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/tukey.json new file mode 100644 index 0000000..f8dbc17 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/announce-response-to-bytes/latest/tukey.json @@ -0,0 +1 @@ +[565.2398956433274,665.7749574634894,933.8684556505881,1034.40351747075] \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/benchmark.json b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/benchmark.json new file mode 100644 index 0000000..4a12bef --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/benchmark.json @@ -0,0 +1 @@ +{"group_id":"request-from-bytes","function_id":null,"value_str":null,"throughput":null,"full_id":"request-from-bytes","directory_name":"request-from-bytes","title":"request-from-bytes"} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/estimates.json b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/estimates.json new file mode 100644 index 0000000..b2386d1 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/estimates.json @@ -0,0 +1 @@ +{"mean":{"confidence_interval":{"confidence_level":0.95,"lower_bound":791.6783637138329,"upper_bound":798.2060382161882},"point_estimate":794.7777653239414,"standard_error":1.670679553768017},"median":{"confidence_interval":{"confidence_level":0.95,"lower_bound":786.1377247215969,"upper_bound":789.3747173913043},"point_estimate":788.2154281612928,"standard_error":0.9080984924572599},"median_abs_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":34.47577000388577,"upper_bound":38.99231743541378},"point_estimate":37.25560574108035,"standard_error":1.1689453074940308},"slope":{"confidence_interval":{"confidence_level":0.95,"lower_bound":791.1964524096214,"upper_bound":798.189227060581},"point_estimate":794.5503586699593,"standard_error":1.785366051793957},"std_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":41.22148757811178,"upper_bound":64.85026519223337},"point_estimate":52.942361554527636,"standard_error":6.055601310575156}} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/raw.csv b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/raw.csv new file mode 100644 index 0000000..a0d1562 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/raw.csv @@ -0,0 +1,1001 @@ +group,function,value,throughput_num,throughput_type,sample_measured_value,unit,iteration_count +request-from-bytes,,,,,423396.0,ns,460 +request-from-bytes,,,,,763368.0,ns,920 +request-from-bytes,,,,,1139009.0,ns,1380 +request-from-bytes,,,,,1442643.0,ns,1840 +request-from-bytes,,,,,1863104.0,ns,2300 +request-from-bytes,,,,,2224093.0,ns,2760 +request-from-bytes,,,,,2558578.0,ns,3220 +request-from-bytes,,,,,2904812.0,ns,3680 +request-from-bytes,,,,,3246499.0,ns,4140 +request-from-bytes,,,,,3608606.0,ns,4600 +request-from-bytes,,,,,4048003.0,ns,5060 +request-from-bytes,,,,,4328930.0,ns,5520 +request-from-bytes,,,,,5013826.0,ns,5980 +request-from-bytes,,,,,5440118.0,ns,6440 +request-from-bytes,,,,,5581925.0,ns,6900 +request-from-bytes,,,,,6444760.0,ns,7360 +request-from-bytes,,,,,6646863.0,ns,7820 +request-from-bytes,,,,,6535022.0,ns,8280 +request-from-bytes,,,,,7042539.0,ns,8740 +request-from-bytes,,,,,7213840.0,ns,9200 +request-from-bytes,,,,,7598918.0,ns,9660 +request-from-bytes,,,,,7973458.0,ns,10120 +request-from-bytes,,,,,8335864.0,ns,10580 +request-from-bytes,,,,,8655360.0,ns,11040 +request-from-bytes,,,,,9078081.0,ns,11500 +request-from-bytes,,,,,9376160.0,ns,11960 +request-from-bytes,,,,,9962427.0,ns,12420 +request-from-bytes,,,,,10171708.0,ns,12880 +request-from-bytes,,,,,10578636.0,ns,13340 +request-from-bytes,,,,,10850147.0,ns,13800 +request-from-bytes,,,,,11180130.0,ns,14260 +request-from-bytes,,,,,11581329.0,ns,14720 +request-from-bytes,,,,,12040742.0,ns,15180 +request-from-bytes,,,,,12414532.0,ns,15640 +request-from-bytes,,,,,12621262.0,ns,16100 +request-from-bytes,,,,,13419443.0,ns,16560 +request-from-bytes,,,,,17325675.0,ns,17020 +request-from-bytes,,,,,13828911.0,ns,17480 +request-from-bytes,,,,,14115473.0,ns,17940 +request-from-bytes,,,,,14429913.0,ns,18400 +request-from-bytes,,,,,14907534.0,ns,18860 +request-from-bytes,,,,,15195192.0,ns,19320 +request-from-bytes,,,,,15561657.0,ns,19780 +request-from-bytes,,,,,16116673.0,ns,20240 +request-from-bytes,,,,,16280330.0,ns,20700 +request-from-bytes,,,,,16615179.0,ns,21160 +request-from-bytes,,,,,17071766.0,ns,21620 +request-from-bytes,,,,,17362158.0,ns,22080 +request-from-bytes,,,,,17751493.0,ns,22540 +request-from-bytes,,,,,18118889.0,ns,23000 +request-from-bytes,,,,,18451559.0,ns,23460 +request-from-bytes,,,,,21876222.0,ns,23920 +request-from-bytes,,,,,20475868.0,ns,24380 +request-from-bytes,,,,,20108021.0,ns,24840 +request-from-bytes,,,,,23565093.0,ns,25300 +request-from-bytes,,,,,19515505.0,ns,25760 +request-from-bytes,,,,,19873714.0,ns,26220 +request-from-bytes,,,,,20268315.0,ns,26680 +request-from-bytes,,,,,20791379.0,ns,27140 +request-from-bytes,,,,,20945076.0,ns,27600 +request-from-bytes,,,,,21256444.0,ns,28060 +request-from-bytes,,,,,21660321.0,ns,28520 +request-from-bytes,,,,,21949370.0,ns,28980 +request-from-bytes,,,,,22619533.0,ns,29440 +request-from-bytes,,,,,22778183.0,ns,29900 +request-from-bytes,,,,,23032863.0,ns,30360 +request-from-bytes,,,,,27791577.0,ns,30820 +request-from-bytes,,,,,24672641.0,ns,31280 +request-from-bytes,,,,,24938247.0,ns,31740 +request-from-bytes,,,,,25309670.0,ns,32200 +request-from-bytes,,,,,25668647.0,ns,32660 +request-from-bytes,,,,,26249075.0,ns,33120 +request-from-bytes,,,,,26390427.0,ns,33580 +request-from-bytes,,,,,26747527.0,ns,34040 +request-from-bytes,,,,,27325198.0,ns,34500 +request-from-bytes,,,,,28156513.0,ns,34960 +request-from-bytes,,,,,28426781.0,ns,35420 +request-from-bytes,,,,,29085663.0,ns,35880 +request-from-bytes,,,,,29325971.0,ns,36340 +request-from-bytes,,,,,29750781.0,ns,36800 +request-from-bytes,,,,,30215380.0,ns,37260 +request-from-bytes,,,,,30678510.0,ns,37720 +request-from-bytes,,,,,31108055.0,ns,38180 +request-from-bytes,,,,,31390350.0,ns,38640 +request-from-bytes,,,,,31982561.0,ns,39100 +request-from-bytes,,,,,31930288.0,ns,39560 +request-from-bytes,,,,,32692516.0,ns,40020 +request-from-bytes,,,,,36018656.0,ns,40480 +request-from-bytes,,,,,31082184.0,ns,40940 +request-from-bytes,,,,,31435928.0,ns,41400 +request-from-bytes,,,,,36273427.0,ns,41860 +request-from-bytes,,,,,36833988.0,ns,42320 +request-from-bytes,,,,,32486131.0,ns,42780 +request-from-bytes,,,,,32855880.0,ns,43240 +request-from-bytes,,,,,33266494.0,ns,43700 +request-from-bytes,,,,,33476981.0,ns,44160 +request-from-bytes,,,,,33843069.0,ns,44620 +request-from-bytes,,,,,34610028.0,ns,45080 +request-from-bytes,,,,,34669921.0,ns,45540 +request-from-bytes,,,,,35239798.0,ns,46000 +request-from-bytes,,,,,35270914.0,ns,46460 +request-from-bytes,,,,,35598266.0,ns,46920 +request-from-bytes,,,,,35954093.0,ns,47380 +request-from-bytes,,,,,36510435.0,ns,47840 +request-from-bytes,,,,,36777463.0,ns,48300 +request-from-bytes,,,,,37107350.0,ns,48760 +request-from-bytes,,,,,37398604.0,ns,49220 +request-from-bytes,,,,,37714950.0,ns,49680 +request-from-bytes,,,,,38073525.0,ns,50140 +request-from-bytes,,,,,38409887.0,ns,50600 +request-from-bytes,,,,,38748317.0,ns,51060 +request-from-bytes,,,,,39144640.0,ns,51520 +request-from-bytes,,,,,39719624.0,ns,51980 +request-from-bytes,,,,,39775154.0,ns,52440 +request-from-bytes,,,,,40177580.0,ns,52900 +request-from-bytes,,,,,40489456.0,ns,53360 +request-from-bytes,,,,,40834276.0,ns,53820 +request-from-bytes,,,,,41208996.0,ns,54280 +request-from-bytes,,,,,46357406.0,ns,54740 +request-from-bytes,,,,,46808276.0,ns,55200 +request-from-bytes,,,,,42263855.0,ns,55660 +request-from-bytes,,,,,42653821.0,ns,56120 +request-from-bytes,,,,,42943466.0,ns,56580 +request-from-bytes,,,,,43466585.0,ns,57040 +request-from-bytes,,,,,48070872.0,ns,57500 +request-from-bytes,,,,,45742540.0,ns,57960 +request-from-bytes,,,,,46215261.0,ns,58420 +request-from-bytes,,,,,46395431.0,ns,58880 +request-from-bytes,,,,,48287119.0,ns,59340 +request-from-bytes,,,,,48150983.0,ns,59800 +request-from-bytes,,,,,45872531.0,ns,60260 +request-from-bytes,,,,,46146988.0,ns,60720 +request-from-bytes,,,,,46592409.0,ns,61180 +request-from-bytes,,,,,46825398.0,ns,61640 +request-from-bytes,,,,,47352814.0,ns,62100 +request-from-bytes,,,,,52388229.0,ns,62560 +request-from-bytes,,,,,49561640.0,ns,63020 +request-from-bytes,,,,,53013864.0,ns,63480 +request-from-bytes,,,,,48787182.0,ns,63940 +request-from-bytes,,,,,53513886.0,ns,64400 +request-from-bytes,,,,,51448405.0,ns,64860 +request-from-bytes,,,,,51510404.0,ns,65320 +request-from-bytes,,,,,52029213.0,ns,65780 +request-from-bytes,,,,,52462404.0,ns,66240 +request-from-bytes,,,,,52961346.0,ns,66700 +request-from-bytes,,,,,55147686.0,ns,67160 +request-from-bytes,,,,,51357773.0,ns,67620 +request-from-bytes,,,,,56852390.0,ns,68080 +request-from-bytes,,,,,53990135.0,ns,68540 +request-from-bytes,,,,,54262271.0,ns,69000 +request-from-bytes,,,,,55075060.0,ns,69460 +request-from-bytes,,,,,56161292.0,ns,69920 +request-from-bytes,,,,,57190544.0,ns,70380 +request-from-bytes,,,,,57754173.0,ns,70840 +request-from-bytes,,,,,58549544.0,ns,71300 +request-from-bytes,,,,,58768882.0,ns,71760 +request-from-bytes,,,,,59432335.0,ns,72220 +request-from-bytes,,,,,63463022.0,ns,72680 +request-from-bytes,,,,,55502143.0,ns,73140 +request-from-bytes,,,,,56358263.0,ns,73600 +request-from-bytes,,,,,56310457.0,ns,74060 +request-from-bytes,,,,,56894054.0,ns,74520 +request-from-bytes,,,,,57537325.0,ns,74980 +request-from-bytes,,,,,57143092.0,ns,75440 +request-from-bytes,,,,,58003193.0,ns,75900 +request-from-bytes,,,,,57707272.0,ns,76360 +request-from-bytes,,,,,58650169.0,ns,76820 +request-from-bytes,,,,,58410275.0,ns,77280 +request-from-bytes,,,,,59088214.0,ns,77740 +request-from-bytes,,,,,60120044.0,ns,78200 +request-from-bytes,,,,,59427953.0,ns,78660 +request-from-bytes,,,,,60470191.0,ns,79120 +request-from-bytes,,,,,60156623.0,ns,79580 +request-from-bytes,,,,,61074397.0,ns,80040 +request-from-bytes,,,,,61430387.0,ns,80500 +request-from-bytes,,,,,61183741.0,ns,80960 +request-from-bytes,,,,,61915941.0,ns,81420 +request-from-bytes,,,,,62075184.0,ns,81880 +request-from-bytes,,,,,62912408.0,ns,82340 +request-from-bytes,,,,,63311045.0,ns,82800 +request-from-bytes,,,,,63010893.0,ns,83260 +request-from-bytes,,,,,63943212.0,ns,83720 +request-from-bytes,,,,,63782219.0,ns,84180 +request-from-bytes,,,,,64291645.0,ns,84640 +request-from-bytes,,,,,64824927.0,ns,85100 +request-from-bytes,,,,,69973514.0,ns,85560 +request-from-bytes,,,,,68043654.0,ns,86020 +request-from-bytes,,,,,68565923.0,ns,86480 +request-from-bytes,,,,,71077157.0,ns,86940 +request-from-bytes,,,,,66748562.0,ns,87400 +request-from-bytes,,,,,73184243.0,ns,87860 +request-from-bytes,,,,,69941169.0,ns,88320 +request-from-bytes,,,,,69980471.0,ns,88780 +request-from-bytes,,,,,71055198.0,ns,89240 +request-from-bytes,,,,,71352144.0,ns,89700 +request-from-bytes,,,,,71105752.0,ns,90160 +request-from-bytes,,,,,71786697.0,ns,90620 +request-from-bytes,,,,,71909497.0,ns,91080 +request-from-bytes,,,,,72301204.0,ns,91540 +request-from-bytes,,,,,79124916.0,ns,92000 +request-from-bytes,,,,,72960786.0,ns,92460 +request-from-bytes,,,,,73144933.0,ns,92920 +request-from-bytes,,,,,133767236.0,ns,93380 +request-from-bytes,,,,,116447501.0,ns,93840 +request-from-bytes,,,,,90945761.0,ns,94300 +request-from-bytes,,,,,77456444.0,ns,94760 +request-from-bytes,,,,,76187669.0,ns,95220 +request-from-bytes,,,,,72805047.0,ns,95680 +request-from-bytes,,,,,73173069.0,ns,96140 +request-from-bytes,,,,,73238888.0,ns,96600 +request-from-bytes,,,,,73545377.0,ns,97060 +request-from-bytes,,,,,74317071.0,ns,97520 +request-from-bytes,,,,,74628828.0,ns,97980 +request-from-bytes,,,,,74540756.0,ns,98440 +request-from-bytes,,,,,75343427.0,ns,98900 +request-from-bytes,,,,,84331790.0,ns,99360 +request-from-bytes,,,,,78959218.0,ns,99820 +request-from-bytes,,,,,79168441.0,ns,100280 +request-from-bytes,,,,,84714154.0,ns,100740 +request-from-bytes,,,,,79626623.0,ns,101200 +request-from-bytes,,,,,80749932.0,ns,101660 +request-from-bytes,,,,,82073128.0,ns,102120 +request-from-bytes,,,,,77891034.0,ns,102580 +request-from-bytes,,,,,78244498.0,ns,103040 +request-from-bytes,,,,,78629983.0,ns,103500 +request-from-bytes,,,,,79103551.0,ns,103960 +request-from-bytes,,,,,79335376.0,ns,104420 +request-from-bytes,,,,,80063340.0,ns,104880 +request-from-bytes,,,,,84688215.0,ns,105340 +request-from-bytes,,,,,80346665.0,ns,105800 +request-from-bytes,,,,,80881578.0,ns,106260 +request-from-bytes,,,,,81160319.0,ns,106720 +request-from-bytes,,,,,81623092.0,ns,107180 +request-from-bytes,,,,,82070914.0,ns,107640 +request-from-bytes,,,,,82088719.0,ns,108100 +request-from-bytes,,,,,82407145.0,ns,108560 +request-from-bytes,,,,,82738287.0,ns,109020 +request-from-bytes,,,,,83090696.0,ns,109480 +request-from-bytes,,,,,83507559.0,ns,109940 +request-from-bytes,,,,,87876654.0,ns,110400 +request-from-bytes,,,,,89426470.0,ns,110860 +request-from-bytes,,,,,84610034.0,ns,111320 +request-from-bytes,,,,,85670407.0,ns,111780 +request-from-bytes,,,,,84950090.0,ns,112240 +request-from-bytes,,,,,91517369.0,ns,112700 +request-from-bytes,,,,,92055915.0,ns,113160 +request-from-bytes,,,,,89389913.0,ns,113620 +request-from-bytes,,,,,93023819.0,ns,114080 +request-from-bytes,,,,,94868892.0,ns,114540 +request-from-bytes,,,,,95881459.0,ns,115000 +request-from-bytes,,,,,110051474.0,ns,115460 +request-from-bytes,,,,,117222763.0,ns,115920 +request-from-bytes,,,,,93281618.0,ns,116380 +request-from-bytes,,,,,89284352.0,ns,116840 +request-from-bytes,,,,,89481690.0,ns,117300 +request-from-bytes,,,,,90208446.0,ns,117760 +request-from-bytes,,,,,89548744.0,ns,118220 +request-from-bytes,,,,,96916755.0,ns,118680 +request-from-bytes,,,,,93730272.0,ns,119140 +request-from-bytes,,,,,94676136.0,ns,119600 +request-from-bytes,,,,,91177046.0,ns,120060 +request-from-bytes,,,,,96633986.0,ns,120520 +request-from-bytes,,,,,95426395.0,ns,120980 +request-from-bytes,,,,,96391928.0,ns,121440 +request-from-bytes,,,,,96418060.0,ns,121900 +request-from-bytes,,,,,96749845.0,ns,122360 +request-from-bytes,,,,,99396129.0,ns,122820 +request-from-bytes,,,,,93518753.0,ns,123280 +request-from-bytes,,,,,93914950.0,ns,123740 +request-from-bytes,,,,,94265033.0,ns,124200 +request-from-bytes,,,,,94885673.0,ns,124660 +request-from-bytes,,,,,95013249.0,ns,125120 +request-from-bytes,,,,,99320859.0,ns,125580 +request-from-bytes,,,,,95623895.0,ns,126040 +request-from-bytes,,,,,97362077.0,ns,126500 +request-from-bytes,,,,,96442056.0,ns,126960 +request-from-bytes,,,,,96518860.0,ns,127420 +request-from-bytes,,,,,97386057.0,ns,127880 +request-from-bytes,,,,,97465999.0,ns,128340 +request-from-bytes,,,,,97940533.0,ns,128800 +request-from-bytes,,,,,99116136.0,ns,129260 +request-from-bytes,,,,,102814733.0,ns,129720 +request-from-bytes,,,,,104938039.0,ns,130180 +request-from-bytes,,,,,103192719.0,ns,130640 +request-from-bytes,,,,,104070140.0,ns,131100 +request-from-bytes,,,,,103742358.0,ns,131560 +request-from-bytes,,,,,105404373.0,ns,132020 +request-from-bytes,,,,,111483343.0,ns,132480 +request-from-bytes,,,,,105856989.0,ns,132940 +request-from-bytes,,,,,105293322.0,ns,133400 +request-from-bytes,,,,,110630482.0,ns,133860 +request-from-bytes,,,,,113523602.0,ns,134320 +request-from-bytes,,,,,116226353.0,ns,134780 +request-from-bytes,,,,,112298578.0,ns,135240 +request-from-bytes,,,,,102946565.0,ns,135700 +request-from-bytes,,,,,103662794.0,ns,136160 +request-from-bytes,,,,,104161586.0,ns,136620 +request-from-bytes,,,,,104237017.0,ns,137080 +request-from-bytes,,,,,104456678.0,ns,137540 +request-from-bytes,,,,,104866214.0,ns,138000 +request-from-bytes,,,,,110920296.0,ns,138460 +request-from-bytes,,,,,110272274.0,ns,138920 +request-from-bytes,,,,,106110402.0,ns,139380 +request-from-bytes,,,,,106230767.0,ns,139840 +request-from-bytes,,,,,106847683.0,ns,140300 +request-from-bytes,,,,,110623890.0,ns,140760 +request-from-bytes,,,,,107744248.0,ns,141220 +request-from-bytes,,,,,113965988.0,ns,141680 +request-from-bytes,,,,,112420588.0,ns,142140 +request-from-bytes,,,,,112958203.0,ns,142600 +request-from-bytes,,,,,113519093.0,ns,143060 +request-from-bytes,,,,,108861778.0,ns,143520 +request-from-bytes,,,,,109927855.0,ns,143980 +request-from-bytes,,,,,109655662.0,ns,144440 +request-from-bytes,,,,,112300112.0,ns,144900 +request-from-bytes,,,,,116504337.0,ns,145360 +request-from-bytes,,,,,149163274.0,ns,145820 +request-from-bytes,,,,,130071907.0,ns,146280 +request-from-bytes,,,,,117208821.0,ns,146740 +request-from-bytes,,,,,116373946.0,ns,147200 +request-from-bytes,,,,,116570662.0,ns,147660 +request-from-bytes,,,,,116722432.0,ns,148120 +request-from-bytes,,,,,117535943.0,ns,148580 +request-from-bytes,,,,,118662931.0,ns,149040 +request-from-bytes,,,,,113462716.0,ns,149500 +request-from-bytes,,,,,118414990.0,ns,149960 +request-from-bytes,,,,,122873305.0,ns,150420 +request-from-bytes,,,,,122382180.0,ns,150880 +request-from-bytes,,,,,120301627.0,ns,151340 +request-from-bytes,,,,,120404213.0,ns,151800 +request-from-bytes,,,,,120369981.0,ns,152260 +request-from-bytes,,,,,122897788.0,ns,152720 +request-from-bytes,,,,,121529879.0,ns,153180 +request-from-bytes,,,,,121403908.0,ns,153640 +request-from-bytes,,,,,128443005.0,ns,154100 +request-from-bytes,,,,,125891732.0,ns,154560 +request-from-bytes,,,,,117879701.0,ns,155020 +request-from-bytes,,,,,118185060.0,ns,155480 +request-from-bytes,,,,,118424489.0,ns,155940 +request-from-bytes,,,,,119160183.0,ns,156400 +request-from-bytes,,,,,119250356.0,ns,156860 +request-from-bytes,,,,,119402688.0,ns,157320 +request-from-bytes,,,,,119881237.0,ns,157780 +request-from-bytes,,,,,125997242.0,ns,158240 +request-from-bytes,,,,,126045162.0,ns,158700 +request-from-bytes,,,,,126758348.0,ns,159160 +request-from-bytes,,,,,127098406.0,ns,159620 +request-from-bytes,,,,,121527795.0,ns,160080 +request-from-bytes,,,,,129935786.0,ns,160540 +request-from-bytes,,,,,129980525.0,ns,161000 +request-from-bytes,,,,,122609662.0,ns,161460 +request-from-bytes,,,,,129337413.0,ns,161920 +request-from-bytes,,,,,128078142.0,ns,162380 +request-from-bytes,,,,,132808216.0,ns,162840 +request-from-bytes,,,,,135267711.0,ns,163300 +request-from-bytes,,,,,138995587.0,ns,163760 +request-from-bytes,,,,,140289177.0,ns,164220 +request-from-bytes,,,,,231351159.0,ns,164680 +request-from-bytes,,,,,158986039.0,ns,165140 +request-from-bytes,,,,,133459051.0,ns,165600 +request-from-bytes,,,,,139930072.0,ns,166060 +request-from-bytes,,,,,135578341.0,ns,166520 +request-from-bytes,,,,,127585753.0,ns,166980 +request-from-bytes,,,,,139895515.0,ns,167440 +request-from-bytes,,,,,144650315.0,ns,167900 +request-from-bytes,,,,,137660988.0,ns,168360 +request-from-bytes,,,,,132683375.0,ns,168820 +request-from-bytes,,,,,128570245.0,ns,169280 +request-from-bytes,,,,,128942509.0,ns,169740 +request-from-bytes,,,,,129776311.0,ns,170200 +request-from-bytes,,,,,129606813.0,ns,170660 +request-from-bytes,,,,,129997618.0,ns,171120 +request-from-bytes,,,,,130297182.0,ns,171580 +request-from-bytes,,,,,139401184.0,ns,172040 +request-from-bytes,,,,,136874887.0,ns,172500 +request-from-bytes,,,,,136997456.0,ns,172960 +request-from-bytes,,,,,136741767.0,ns,173420 +request-from-bytes,,,,,137621278.0,ns,173880 +request-from-bytes,,,,,137520753.0,ns,174340 +request-from-bytes,,,,,138101447.0,ns,174800 +request-from-bytes,,,,,139095855.0,ns,175260 +request-from-bytes,,,,,133679433.0,ns,175720 +request-from-bytes,,,,,133788634.0,ns,176180 +request-from-bytes,,,,,134605309.0,ns,176640 +request-from-bytes,,,,,140160883.0,ns,177100 +request-from-bytes,,,,,147021531.0,ns,177560 +request-from-bytes,,,,,140924887.0,ns,178020 +request-from-bytes,,,,,143169218.0,ns,178480 +request-from-bytes,,,,,135908954.0,ns,178940 +request-from-bytes,,,,,142056050.0,ns,179400 +request-from-bytes,,,,,148644367.0,ns,179860 +request-from-bytes,,,,,154751576.0,ns,180320 +request-from-bytes,,,,,155833911.0,ns,180780 +request-from-bytes,,,,,155552218.0,ns,181240 +request-from-bytes,,,,,151922852.0,ns,181700 +request-from-bytes,,,,,145082059.0,ns,182160 +request-from-bytes,,,,,145604384.0,ns,182620 +request-from-bytes,,,,,145697896.0,ns,183080 +request-from-bytes,,,,,149174396.0,ns,183540 +request-from-bytes,,,,,154718493.0,ns,184000 +request-from-bytes,,,,,161320188.0,ns,184460 +request-from-bytes,,,,,142201175.0,ns,184920 +request-from-bytes,,,,,141259213.0,ns,185380 +request-from-bytes,,,,,141561144.0,ns,185840 +request-from-bytes,,,,,141591524.0,ns,186300 +request-from-bytes,,,,,154572683.0,ns,186760 +request-from-bytes,,,,,152623069.0,ns,187220 +request-from-bytes,,,,,181686867.0,ns,187680 +request-from-bytes,,,,,171265518.0,ns,188140 +request-from-bytes,,,,,143191108.0,ns,188600 +request-from-bytes,,,,,143680618.0,ns,189060 +request-from-bytes,,,,,144107656.0,ns,189520 +request-from-bytes,,,,,144508532.0,ns,189980 +request-from-bytes,,,,,144819617.0,ns,190440 +request-from-bytes,,,,,153195937.0,ns,190900 +request-from-bytes,,,,,151138974.0,ns,191360 +request-from-bytes,,,,,152413010.0,ns,191820 +request-from-bytes,,,,,151657820.0,ns,192280 +request-from-bytes,,,,,152456999.0,ns,192740 +request-from-bytes,,,,,152479027.0,ns,193200 +request-from-bytes,,,,,155950471.0,ns,193660 +request-from-bytes,,,,,147664840.0,ns,194120 +request-from-bytes,,,,,148095818.0,ns,194580 +request-from-bytes,,,,,148367069.0,ns,195040 +request-from-bytes,,,,,148585456.0,ns,195500 +request-from-bytes,,,,,152901277.0,ns,195960 +request-from-bytes,,,,,148992271.0,ns,196420 +request-from-bytes,,,,,153902899.0,ns,196880 +request-from-bytes,,,,,149985409.0,ns,197340 +request-from-bytes,,,,,150238809.0,ns,197800 +request-from-bytes,,,,,150628490.0,ns,198260 +request-from-bytes,,,,,151366269.0,ns,198720 +request-from-bytes,,,,,156641774.0,ns,199180 +request-from-bytes,,,,,168131584.0,ns,199640 +request-from-bytes,,,,,161934973.0,ns,200100 +request-from-bytes,,,,,157793957.0,ns,200560 +request-from-bytes,,,,,152913229.0,ns,201020 +request-from-bytes,,,,,153138433.0,ns,201480 +request-from-bytes,,,,,153482900.0,ns,201940 +request-from-bytes,,,,,158158900.0,ns,202400 +request-from-bytes,,,,,162025503.0,ns,202860 +request-from-bytes,,,,,170627795.0,ns,203320 +request-from-bytes,,,,,160822747.0,ns,203780 +request-from-bytes,,,,,161074080.0,ns,204240 +request-from-bytes,,,,,161845796.0,ns,204700 +request-from-bytes,,,,,163920748.0,ns,205160 +request-from-bytes,,,,,159977286.0,ns,205620 +request-from-bytes,,,,,156658634.0,ns,206080 +request-from-bytes,,,,,163153129.0,ns,206540 +request-from-bytes,,,,,176139734.0,ns,207000 +request-from-bytes,,,,,180388660.0,ns,207460 +request-from-bytes,,,,,164401025.0,ns,207920 +request-from-bytes,,,,,166050961.0,ns,208380 +request-from-bytes,,,,,159005660.0,ns,208840 +request-from-bytes,,,,,159378147.0,ns,209300 +request-from-bytes,,,,,159229922.0,ns,209760 +request-from-bytes,,,,,159902688.0,ns,210220 +request-from-bytes,,,,,159936523.0,ns,210680 +request-from-bytes,,,,,164490663.0,ns,211140 +request-from-bytes,,,,,160984763.0,ns,211600 +request-from-bytes,,,,,161157224.0,ns,212060 +request-from-bytes,,,,,161480569.0,ns,212520 +request-from-bytes,,,,,162277382.0,ns,212980 +request-from-bytes,,,,,161852388.0,ns,213440 +request-from-bytes,,,,,167183574.0,ns,213900 +request-from-bytes,,,,,168012391.0,ns,214360 +request-from-bytes,,,,,167397140.0,ns,214820 +request-from-bytes,,,,,164001917.0,ns,215280 +request-from-bytes,,,,,163905001.0,ns,215740 +request-from-bytes,,,,,164457302.0,ns,216200 +request-from-bytes,,,,,174585150.0,ns,216660 +request-from-bytes,,,,,171171845.0,ns,217120 +request-from-bytes,,,,,175470595.0,ns,217580 +request-from-bytes,,,,,178139632.0,ns,218040 +request-from-bytes,,,,,187157837.0,ns,218500 +request-from-bytes,,,,,190764654.0,ns,218960 +request-from-bytes,,,,,181354591.0,ns,219420 +request-from-bytes,,,,,167144915.0,ns,219880 +request-from-bytes,,,,,167366075.0,ns,220340 +request-from-bytes,,,,,167746570.0,ns,220800 +request-from-bytes,,,,,168118470.0,ns,221260 +request-from-bytes,,,,,168578445.0,ns,221720 +request-from-bytes,,,,,177146990.0,ns,222180 +request-from-bytes,,,,,176132561.0,ns,222640 +request-from-bytes,,,,,169442346.0,ns,223100 +request-from-bytes,,,,,169944079.0,ns,223560 +request-from-bytes,,,,,177887767.0,ns,224020 +request-from-bytes,,,,,187005816.0,ns,224480 +request-from-bytes,,,,,189055067.0,ns,224940 +request-from-bytes,,,,,171601806.0,ns,225400 +request-from-bytes,,,,,172149967.0,ns,225860 +request-from-bytes,,,,,172248999.0,ns,226320 +request-from-bytes,,,,,172122877.0,ns,226780 +request-from-bytes,,,,,173268132.0,ns,227240 +request-from-bytes,,,,,173367660.0,ns,227700 +request-from-bytes,,,,,190130103.0,ns,228160 +request-from-bytes,,,,,196661048.0,ns,228620 +request-from-bytes,,,,,200867935.0,ns,229080 +request-from-bytes,,,,,200909623.0,ns,229540 +request-from-bytes,,,,,184888169.0,ns,230000 +request-from-bytes,,,,,175433164.0,ns,230460 +request-from-bytes,,,,,175414133.0,ns,230920 +request-from-bytes,,,,,179423050.0,ns,231380 +request-from-bytes,,,,,185483527.0,ns,231840 +request-from-bytes,,,,,184198286.0,ns,232300 +request-from-bytes,,,,,176982645.0,ns,232760 +request-from-bytes,,,,,181806408.0,ns,233220 +request-from-bytes,,,,,184545659.0,ns,233680 +request-from-bytes,,,,,186660379.0,ns,234140 +request-from-bytes,,,,,178312065.0,ns,234600 +request-from-bytes,,,,,178622725.0,ns,235060 +request-from-bytes,,,,,190048729.0,ns,235520 +request-from-bytes,,,,,192369250.0,ns,235980 +request-from-bytes,,,,,179524654.0,ns,236440 +request-from-bytes,,,,,180628373.0,ns,236900 +request-from-bytes,,,,,184303554.0,ns,237360 +request-from-bytes,,,,,190899785.0,ns,237820 +request-from-bytes,,,,,181004553.0,ns,238280 +request-from-bytes,,,,,191926485.0,ns,238740 +request-from-bytes,,,,,208324982.0,ns,239200 +request-from-bytes,,,,,204571109.0,ns,239660 +request-from-bytes,,,,,182367935.0,ns,240120 +request-from-bytes,,,,,201076622.0,ns,240580 +request-from-bytes,,,,,209842216.0,ns,241040 +request-from-bytes,,,,,211854275.0,ns,241500 +request-from-bytes,,,,,207525151.0,ns,241960 +request-from-bytes,,,,,192123190.0,ns,242420 +request-from-bytes,,,,,191842442.0,ns,242880 +request-from-bytes,,,,,194942011.0,ns,243340 +request-from-bytes,,,,,185313511.0,ns,243800 +request-from-bytes,,,,,199353428.0,ns,244260 +request-from-bytes,,,,,186101530.0,ns,244720 +request-from-bytes,,,,,195815983.0,ns,245180 +request-from-bytes,,,,,193928169.0,ns,245640 +request-from-bytes,,,,,204877521.0,ns,246100 +request-from-bytes,,,,,305143872.0,ns,246560 +request-from-bytes,,,,,212689145.0,ns,247020 +request-from-bytes,,,,,198042400.0,ns,247480 +request-from-bytes,,,,,207339135.0,ns,247940 +request-from-bytes,,,,,216339976.0,ns,248400 +request-from-bytes,,,,,203885178.0,ns,248860 +request-from-bytes,,,,,197598290.0,ns,249320 +request-from-bytes,,,,,197483051.0,ns,249780 +request-from-bytes,,,,,197733183.0,ns,250240 +request-from-bytes,,,,,206965250.0,ns,250700 +request-from-bytes,,,,,196801884.0,ns,251160 +request-from-bytes,,,,,191548444.0,ns,251620 +request-from-bytes,,,,,195411419.0,ns,252080 +request-from-bytes,,,,,201607764.0,ns,252540 +request-from-bytes,,,,,218396456.0,ns,253000 +request-from-bytes,,,,,199152602.0,ns,253460 +request-from-bytes,,,,,193063329.0,ns,253920 +request-from-bytes,,,,,194329366.0,ns,254380 +request-from-bytes,,,,,204355042.0,ns,254840 +request-from-bytes,,,,,222049013.0,ns,255300 +request-from-bytes,,,,,224931479.0,ns,255760 +request-from-bytes,,,,,194929236.0,ns,256220 +request-from-bytes,,,,,214116209.0,ns,256680 +request-from-bytes,,,,,216480443.0,ns,257140 +request-from-bytes,,,,,217881117.0,ns,257600 +request-from-bytes,,,,,196371649.0,ns,258060 +request-from-bytes,,,,,196223196.0,ns,258520 +request-from-bytes,,,,,197407600.0,ns,258980 +request-from-bytes,,,,,197422509.0,ns,259440 +request-from-bytes,,,,,201802591.0,ns,259900 +request-from-bytes,,,,,198206730.0,ns,260360 +request-from-bytes,,,,,207575568.0,ns,260820 +request-from-bytes,,,,,206693407.0,ns,261280 +request-from-bytes,,,,,203493226.0,ns,261740 +request-from-bytes,,,,,217527628.0,ns,262200 +request-from-bytes,,,,,218107247.0,ns,262660 +request-from-bytes,,,,,199960555.0,ns,263120 +request-from-bytes,,,,,200863178.0,ns,263580 +request-from-bytes,,,,,200719888.0,ns,264040 +request-from-bytes,,,,,205460568.0,ns,264500 +request-from-bytes,,,,,201577245.0,ns,264960 +request-from-bytes,,,,,201697407.0,ns,265420 +request-from-bytes,,,,,202299423.0,ns,265880 +request-from-bytes,,,,,202433942.0,ns,266340 +request-from-bytes,,,,,212216975.0,ns,266800 +request-from-bytes,,,,,211066776.0,ns,267260 +request-from-bytes,,,,,211826637.0,ns,267720 +request-from-bytes,,,,,208505205.0,ns,268180 +request-from-bytes,,,,,212197968.0,ns,268640 +request-from-bytes,,,,,211357543.0,ns,269100 +request-from-bytes,,,,,209208966.0,ns,269560 +request-from-bytes,,,,,209201770.0,ns,270020 +request-from-bytes,,,,,205308785.0,ns,270480 +request-from-bytes,,,,,210651839.0,ns,270940 +request-from-bytes,,,,,214548162.0,ns,271400 +request-from-bytes,,,,,214880042.0,ns,271860 +request-from-bytes,,,,,215955502.0,ns,272320 +request-from-bytes,,,,,215463071.0,ns,272780 +request-from-bytes,,,,,216437600.0,ns,273240 +request-from-bytes,,,,,213793989.0,ns,273700 +request-from-bytes,,,,,235983165.0,ns,274160 +request-from-bytes,,,,,243119031.0,ns,274620 +request-from-bytes,,,,,244885471.0,ns,275080 +request-from-bytes,,,,,210762007.0,ns,275540 +request-from-bytes,,,,,209718240.0,ns,276000 +request-from-bytes,,,,,210350711.0,ns,276460 +request-from-bytes,,,,,210244139.0,ns,276920 +request-from-bytes,,,,,214640010.0,ns,277380 +request-from-bytes,,,,,211715876.0,ns,277840 +request-from-bytes,,,,,219729027.0,ns,278300 +request-from-bytes,,,,,240448508.0,ns,278760 +request-from-bytes,,,,,250350540.0,ns,279220 +request-from-bytes,,,,,232291828.0,ns,279680 +request-from-bytes,,,,,217340496.0,ns,280140 +request-from-bytes,,,,,213402642.0,ns,280600 +request-from-bytes,,,,,213726081.0,ns,281060 +request-from-bytes,,,,,223837778.0,ns,281520 +request-from-bytes,,,,,214529534.0,ns,281980 +request-from-bytes,,,,,214547373.0,ns,282440 +request-from-bytes,,,,,214977918.0,ns,282900 +request-from-bytes,,,,,219219351.0,ns,283360 +request-from-bytes,,,,,216375244.0,ns,283820 +request-from-bytes,,,,,216098218.0,ns,284280 +request-from-bytes,,,,,216439389.0,ns,284740 +request-from-bytes,,,,,216482171.0,ns,285200 +request-from-bytes,,,,,223322575.0,ns,285660 +request-from-bytes,,,,,229840031.0,ns,286120 +request-from-bytes,,,,,231588219.0,ns,286580 +request-from-bytes,,,,,218415507.0,ns,287040 +request-from-bytes,,,,,222253437.0,ns,287500 +request-from-bytes,,,,,232139976.0,ns,287960 +request-from-bytes,,,,,227619724.0,ns,288420 +request-from-bytes,,,,,229114018.0,ns,288880 +request-from-bytes,,,,,228620266.0,ns,289340 +request-from-bytes,,,,,228388319.0,ns,289800 +request-from-bytes,,,,,221580484.0,ns,290260 +request-from-bytes,,,,,221467127.0,ns,290720 +request-from-bytes,,,,,221295875.0,ns,291180 +request-from-bytes,,,,,224067804.0,ns,291640 +request-from-bytes,,,,,235145526.0,ns,292100 +request-from-bytes,,,,,241158848.0,ns,292560 +request-from-bytes,,,,,256808930.0,ns,293020 +request-from-bytes,,,,,252321004.0,ns,293480 +request-from-bytes,,,,,236079138.0,ns,293940 +request-from-bytes,,,,,234907988.0,ns,294400 +request-from-bytes,,,,,228748541.0,ns,294860 +request-from-bytes,,,,,253227668.0,ns,295320 +request-from-bytes,,,,,239141489.0,ns,295780 +request-from-bytes,,,,,225773008.0,ns,296240 +request-from-bytes,,,,,225214317.0,ns,296700 +request-from-bytes,,,,,226317780.0,ns,297160 +request-from-bytes,,,,,229835965.0,ns,297620 +request-from-bytes,,,,,227124220.0,ns,298080 +request-from-bytes,,,,,227093174.0,ns,298540 +request-from-bytes,,,,,234749494.0,ns,299000 +request-from-bytes,,,,,230714854.0,ns,299460 +request-from-bytes,,,,,229430592.0,ns,299920 +request-from-bytes,,,,,228051036.0,ns,300380 +request-from-bytes,,,,,228813189.0,ns,300840 +request-from-bytes,,,,,244902026.0,ns,301300 +request-from-bytes,,,,,239488714.0,ns,301760 +request-from-bytes,,,,,230613500.0,ns,302220 +request-from-bytes,,,,,229655879.0,ns,302680 +request-from-bytes,,,,,234706392.0,ns,303140 +request-from-bytes,,,,,247200446.0,ns,303600 +request-from-bytes,,,,,231620274.0,ns,304060 +request-from-bytes,,,,,245709577.0,ns,304520 +request-from-bytes,,,,,266282899.0,ns,304980 +request-from-bytes,,,,,249821059.0,ns,305440 +request-from-bytes,,,,,233060575.0,ns,305900 +request-from-bytes,,,,,232809201.0,ns,306360 +request-from-bytes,,,,,243491109.0,ns,306820 +request-from-bytes,,,,,244145380.0,ns,307280 +request-from-bytes,,,,,234484728.0,ns,307740 +request-from-bytes,,,,,234383418.0,ns,308200 +request-from-bytes,,,,,234761088.0,ns,308660 +request-from-bytes,,,,,238423284.0,ns,309120 +request-from-bytes,,,,,237064208.0,ns,309580 +request-from-bytes,,,,,236474575.0,ns,310040 +request-from-bytes,,,,,236411523.0,ns,310500 +request-from-bytes,,,,,240089431.0,ns,310960 +request-from-bytes,,,,,251976443.0,ns,311420 +request-from-bytes,,,,,261867597.0,ns,311880 +request-from-bytes,,,,,274671808.0,ns,312340 +request-from-bytes,,,,,278485362.0,ns,312800 +request-from-bytes,,,,,249265383.0,ns,313260 +request-from-bytes,,,,,247276477.0,ns,313720 +request-from-bytes,,,,,238575451.0,ns,314180 +request-from-bytes,,,,,239050117.0,ns,314640 +request-from-bytes,,,,,243854851.0,ns,315100 +request-from-bytes,,,,,261170829.0,ns,315560 +request-from-bytes,,,,,278178306.0,ns,316020 +request-from-bytes,,,,,280140694.0,ns,316480 +request-from-bytes,,,,,281682164.0,ns,316940 +request-from-bytes,,,,,255467426.0,ns,317400 +request-from-bytes,,,,,261271353.0,ns,317860 +request-from-bytes,,,,,370922120.0,ns,318320 +request-from-bytes,,,,,278072171.0,ns,318780 +request-from-bytes,,,,,252310302.0,ns,319240 +request-from-bytes,,,,,246013521.0,ns,319700 +request-from-bytes,,,,,256063456.0,ns,320160 +request-from-bytes,,,,,260567745.0,ns,320620 +request-from-bytes,,,,,254045186.0,ns,321080 +request-from-bytes,,,,,256759711.0,ns,321540 +request-from-bytes,,,,,245370058.0,ns,322000 +request-from-bytes,,,,,245271220.0,ns,322460 +request-from-bytes,,,,,246322698.0,ns,322920 +request-from-bytes,,,,,249884564.0,ns,323380 +request-from-bytes,,,,,246132473.0,ns,323840 +request-from-bytes,,,,,253759766.0,ns,324300 +request-from-bytes,,,,,256196878.0,ns,324760 +request-from-bytes,,,,,259944407.0,ns,325220 +request-from-bytes,,,,,247440486.0,ns,325680 +request-from-bytes,,,,,251806811.0,ns,326140 +request-from-bytes,,,,,248050290.0,ns,326600 +request-from-bytes,,,,,253967009.0,ns,327060 +request-from-bytes,,,,,254965466.0,ns,327520 +request-from-bytes,,,,,249448597.0,ns,327980 +request-from-bytes,,,,,249398158.0,ns,328440 +request-from-bytes,,,,,254454619.0,ns,328900 +request-from-bytes,,,,,257812116.0,ns,329360 +request-from-bytes,,,,,251417510.0,ns,329820 +request-from-bytes,,,,,254167995.0,ns,330280 +request-from-bytes,,,,,267036563.0,ns,330740 +request-from-bytes,,,,,261496110.0,ns,331200 +request-from-bytes,,,,,262454685.0,ns,331660 +request-from-bytes,,,,,263747356.0,ns,332120 +request-from-bytes,,,,,262967968.0,ns,332580 +request-from-bytes,,,,,253360662.0,ns,333040 +request-from-bytes,,,,,253556092.0,ns,333500 +request-from-bytes,,,,,267319261.0,ns,333960 +request-from-bytes,,,,,265450758.0,ns,334420 +request-from-bytes,,,,,255011703.0,ns,334880 +request-from-bytes,,,,,254894826.0,ns,335340 +request-from-bytes,,,,,258671907.0,ns,335800 +request-from-bytes,,,,,259271284.0,ns,336260 +request-from-bytes,,,,,266644894.0,ns,336720 +request-from-bytes,,,,,266873783.0,ns,337180 +request-from-bytes,,,,,281058524.0,ns,337640 +request-from-bytes,,,,,260024073.0,ns,338100 +request-from-bytes,,,,,257376413.0,ns,338560 +request-from-bytes,,,,,263402514.0,ns,339020 +request-from-bytes,,,,,286442462.0,ns,339480 +request-from-bytes,,,,,269186882.0,ns,339940 +request-from-bytes,,,,,265664747.0,ns,340400 +request-from-bytes,,,,,259461487.0,ns,340860 +request-from-bytes,,,,,263462504.0,ns,341320 +request-from-bytes,,,,,275423195.0,ns,341780 +request-from-bytes,,,,,266056443.0,ns,342240 +request-from-bytes,,,,,260548060.0,ns,342700 +request-from-bytes,,,,,274467286.0,ns,343160 +request-from-bytes,,,,,262835887.0,ns,343620 +request-from-bytes,,,,,261465983.0,ns,344080 +request-from-bytes,,,,,261754978.0,ns,344540 +request-from-bytes,,,,,277126395.0,ns,345000 +request-from-bytes,,,,,272633788.0,ns,345460 +request-from-bytes,,,,,274130758.0,ns,345920 +request-from-bytes,,,,,270419344.0,ns,346380 +request-from-bytes,,,,,273503416.0,ns,346840 +request-from-bytes,,,,,274314890.0,ns,347300 +request-from-bytes,,,,,275224853.0,ns,347760 +request-from-bytes,,,,,264703562.0,ns,348220 +request-from-bytes,,,,,265251209.0,ns,348680 +request-from-bytes,,,,,281659072.0,ns,349140 +request-from-bytes,,,,,308969401.0,ns,349600 +request-from-bytes,,,,,297879062.0,ns,350060 +request-from-bytes,,,,,266299688.0,ns,350520 +request-from-bytes,,,,,266984024.0,ns,350980 +request-from-bytes,,,,,281064992.0,ns,351440 +request-from-bytes,,,,,267698480.0,ns,351900 +request-from-bytes,,,,,274946813.0,ns,352360 +request-from-bytes,,,,,301186866.0,ns,352820 +request-from-bytes,,,,,322104674.0,ns,353280 +request-from-bytes,,,,,279603457.0,ns,353740 +request-from-bytes,,,,,279472975.0,ns,354200 +request-from-bytes,,,,,288572149.0,ns,354660 +request-from-bytes,,,,,320762706.0,ns,355120 +request-from-bytes,,,,,270600514.0,ns,355580 +request-from-bytes,,,,,275207395.0,ns,356040 +request-from-bytes,,,,,282272682.0,ns,356500 +request-from-bytes,,,,,279121721.0,ns,356960 +request-from-bytes,,,,,271681686.0,ns,357420 +request-from-bytes,,,,,281485162.0,ns,357880 +request-from-bytes,,,,,283283104.0,ns,358340 +request-from-bytes,,,,,283425112.0,ns,358800 +request-from-bytes,,,,,283744342.0,ns,359260 +request-from-bytes,,,,,288923476.0,ns,359720 +request-from-bytes,,,,,284459751.0,ns,360180 +request-from-bytes,,,,,285475239.0,ns,360640 +request-from-bytes,,,,,287105003.0,ns,361100 +request-from-bytes,,,,,286201394.0,ns,361560 +request-from-bytes,,,,,286001895.0,ns,362020 +request-from-bytes,,,,,286362799.0,ns,362480 +request-from-bytes,,,,,285872610.0,ns,362940 +request-from-bytes,,,,,287385253.0,ns,363400 +request-from-bytes,,,,,290586583.0,ns,363860 +request-from-bytes,,,,,299221688.0,ns,364320 +request-from-bytes,,,,,292385987.0,ns,364780 +request-from-bytes,,,,,313135360.0,ns,365240 +request-from-bytes,,,,,316422845.0,ns,365700 +request-from-bytes,,,,,282626502.0,ns,366160 +request-from-bytes,,,,,278400715.0,ns,366620 +request-from-bytes,,,,,279249343.0,ns,367080 +request-from-bytes,,,,,291403684.0,ns,367540 +request-from-bytes,,,,,292199883.0,ns,368000 +request-from-bytes,,,,,294589790.0,ns,368460 +request-from-bytes,,,,,284913547.0,ns,368920 +request-from-bytes,,,,,292608869.0,ns,369380 +request-from-bytes,,,,,292732722.0,ns,369840 +request-from-bytes,,,,,292941075.0,ns,370300 +request-from-bytes,,,,,287378252.0,ns,370760 +request-from-bytes,,,,,283667167.0,ns,371220 +request-from-bytes,,,,,405400888.0,ns,371680 +request-from-bytes,,,,,309498685.0,ns,372140 +request-from-bytes,,,,,295717973.0,ns,372600 +request-from-bytes,,,,,304923994.0,ns,373060 +request-from-bytes,,,,,311233275.0,ns,373520 +request-from-bytes,,,,,313603937.0,ns,373980 +request-from-bytes,,,,,328244506.0,ns,374440 +request-from-bytes,,,,,320168947.0,ns,374900 +request-from-bytes,,,,,285755987.0,ns,375360 +request-from-bytes,,,,,285363935.0,ns,375820 +request-from-bytes,,,,,286425134.0,ns,376280 +request-from-bytes,,,,,294417302.0,ns,376740 +request-from-bytes,,,,,287457230.0,ns,377200 +request-from-bytes,,,,,301631902.0,ns,377660 +request-from-bytes,,,,,299175353.0,ns,378120 +request-from-bytes,,,,,299408528.0,ns,378580 +request-from-bytes,,,,,299685865.0,ns,379040 +request-from-bytes,,,,,302136252.0,ns,379500 +request-from-bytes,,,,,309081005.0,ns,379960 +request-from-bytes,,,,,301050290.0,ns,380420 +request-from-bytes,,,,,301591656.0,ns,380880 +request-from-bytes,,,,,299309638.0,ns,381340 +request-from-bytes,,,,,290431419.0,ns,381800 +request-from-bytes,,,,,300795994.0,ns,382260 +request-from-bytes,,,,,300770906.0,ns,382720 +request-from-bytes,,,,,297215048.0,ns,383180 +request-from-bytes,,,,,306157791.0,ns,383640 +request-from-bytes,,,,,305685893.0,ns,384100 +request-from-bytes,,,,,292468215.0,ns,384560 +request-from-bytes,,,,,292633271.0,ns,385020 +request-from-bytes,,,,,299039888.0,ns,385480 +request-from-bytes,,,,,299250530.0,ns,385940 +request-from-bytes,,,,,293610923.0,ns,386400 +request-from-bytes,,,,,299394698.0,ns,386860 +request-from-bytes,,,,,305370452.0,ns,387320 +request-from-bytes,,,,,295022763.0,ns,387780 +request-from-bytes,,,,,294978748.0,ns,388240 +request-from-bytes,,,,,300816771.0,ns,388700 +request-from-bytes,,,,,341153107.0,ns,389160 +request-from-bytes,,,,,339378618.0,ns,389620 +request-from-bytes,,,,,341291706.0,ns,390080 +request-from-bytes,,,,,312908200.0,ns,390540 +request-from-bytes,,,,,297836154.0,ns,391000 +request-from-bytes,,,,,297455220.0,ns,391460 +request-from-bytes,,,,,314196126.0,ns,391920 +request-from-bytes,,,,,298359357.0,ns,392380 +request-from-bytes,,,,,299046197.0,ns,392840 +request-from-bytes,,,,,309768121.0,ns,393300 +request-from-bytes,,,,,307810273.0,ns,393760 +request-from-bytes,,,,,299127488.0,ns,394220 +request-from-bytes,,,,,300723690.0,ns,394680 +request-from-bytes,,,,,300436049.0,ns,395140 +request-from-bytes,,,,,305083254.0,ns,395600 +request-from-bytes,,,,,300634124.0,ns,396060 +request-from-bytes,,,,,317635268.0,ns,396520 +request-from-bytes,,,,,313254486.0,ns,396980 +request-from-bytes,,,,,314364930.0,ns,397440 +request-from-bytes,,,,,308845099.0,ns,397900 +request-from-bytes,,,,,303329832.0,ns,398360 +request-from-bytes,,,,,310902025.0,ns,398820 +request-from-bytes,,,,,345558573.0,ns,399280 +request-from-bytes,,,,,306363414.0,ns,399740 +request-from-bytes,,,,,310871873.0,ns,400200 +request-from-bytes,,,,,316141103.0,ns,400660 +request-from-bytes,,,,,328645937.0,ns,401120 +request-from-bytes,,,,,305033037.0,ns,401580 +request-from-bytes,,,,,325659683.0,ns,402040 +request-from-bytes,,,,,316184023.0,ns,402500 +request-from-bytes,,,,,315362980.0,ns,402960 +request-from-bytes,,,,,348551004.0,ns,403420 +request-from-bytes,,,,,316009602.0,ns,403880 +request-from-bytes,,,,,314770759.0,ns,404340 +request-from-bytes,,,,,343741513.0,ns,404800 +request-from-bytes,,,,,308627222.0,ns,405260 +request-from-bytes,,,,,308238540.0,ns,405720 +request-from-bytes,,,,,318964480.0,ns,406180 +request-from-bytes,,,,,315396528.0,ns,406640 +request-from-bytes,,,,,309309159.0,ns,407100 +request-from-bytes,,,,,309654967.0,ns,407560 +request-from-bytes,,,,,319522819.0,ns,408020 +request-from-bytes,,,,,322427669.0,ns,408480 +request-from-bytes,,,,,323512487.0,ns,408940 +request-from-bytes,,,,,327305270.0,ns,409400 +request-from-bytes,,,,,311678477.0,ns,409860 +request-from-bytes,,,,,311604647.0,ns,410320 +request-from-bytes,,,,,324201333.0,ns,410780 +request-from-bytes,,,,,313850483.0,ns,411240 +request-from-bytes,,,,,312478082.0,ns,411700 +request-from-bytes,,,,,321626642.0,ns,412160 +request-from-bytes,,,,,323300272.0,ns,412620 +request-from-bytes,,,,,321921200.0,ns,413080 +request-from-bytes,,,,,329384825.0,ns,413540 +request-from-bytes,,,,,319487890.0,ns,414000 +request-from-bytes,,,,,323780857.0,ns,414460 +request-from-bytes,,,,,350675620.0,ns,414920 +request-from-bytes,,,,,327442838.0,ns,415380 +request-from-bytes,,,,,383280934.0,ns,415840 +request-from-bytes,,,,,329063784.0,ns,416300 +request-from-bytes,,,,,330817364.0,ns,416760 +request-from-bytes,,,,,329814455.0,ns,417220 +request-from-bytes,,,,,329965866.0,ns,417680 +request-from-bytes,,,,,318120063.0,ns,418140 +request-from-bytes,,,,,317834280.0,ns,418600 +request-from-bytes,,,,,325145059.0,ns,419060 +request-from-bytes,,,,,336235396.0,ns,419520 +request-from-bytes,,,,,338749030.0,ns,419980 +request-from-bytes,,,,,362216778.0,ns,420440 +request-from-bytes,,,,,338259690.0,ns,420900 +request-from-bytes,,,,,333208001.0,ns,421360 +request-from-bytes,,,,,348138429.0,ns,421820 +request-from-bytes,,,,,340683111.0,ns,422280 +request-from-bytes,,,,,325796474.0,ns,422740 +request-from-bytes,,,,,334559859.0,ns,423200 +request-from-bytes,,,,,342767493.0,ns,423660 +request-from-bytes,,,,,343662077.0,ns,424120 +request-from-bytes,,,,,341530622.0,ns,424580 +request-from-bytes,,,,,323640751.0,ns,425040 +request-from-bytes,,,,,347712689.0,ns,425500 +request-from-bytes,,,,,365737343.0,ns,425960 +request-from-bytes,,,,,346807904.0,ns,426420 +request-from-bytes,,,,,331343500.0,ns,426880 +request-from-bytes,,,,,342881186.0,ns,427340 +request-from-bytes,,,,,326792264.0,ns,427800 +request-from-bytes,,,,,339528201.0,ns,428260 +request-from-bytes,,,,,339029226.0,ns,428720 +request-from-bytes,,,,,339799804.0,ns,429180 +request-from-bytes,,,,,339838132.0,ns,429640 +request-from-bytes,,,,,337371678.0,ns,430100 +request-from-bytes,,,,,370904023.0,ns,430560 +request-from-bytes,,,,,327673224.0,ns,431020 +request-from-bytes,,,,,333784781.0,ns,431480 +request-from-bytes,,,,,329974959.0,ns,431940 +request-from-bytes,,,,,328506508.0,ns,432400 +request-from-bytes,,,,,338373558.0,ns,432860 +request-from-bytes,,,,,329281013.0,ns,433320 +request-from-bytes,,,,,329842707.0,ns,433780 +request-from-bytes,,,,,351953461.0,ns,434240 +request-from-bytes,,,,,330281110.0,ns,434700 +request-from-bytes,,,,,330684101.0,ns,435160 +request-from-bytes,,,,,347112555.0,ns,435620 +request-from-bytes,,,,,344288480.0,ns,436080 +request-from-bytes,,,,,344090996.0,ns,436540 +request-from-bytes,,,,,348319477.0,ns,437000 +request-from-bytes,,,,,349270847.0,ns,437460 +request-from-bytes,,,,,333817280.0,ns,437920 +request-from-bytes,,,,,334023392.0,ns,438380 +request-from-bytes,,,,,335348169.0,ns,438840 +request-from-bytes,,,,,365904229.0,ns,439300 +request-from-bytes,,,,,347673975.0,ns,439760 +request-from-bytes,,,,,347856827.0,ns,440220 +request-from-bytes,,,,,345932823.0,ns,440680 +request-from-bytes,,,,,348963578.0,ns,441140 +request-from-bytes,,,,,358709134.0,ns,441600 +request-from-bytes,,,,,381408270.0,ns,442060 +request-from-bytes,,,,,450358962.0,ns,442520 +request-from-bytes,,,,,412449248.0,ns,442980 +request-from-bytes,,,,,337110162.0,ns,443440 +request-from-bytes,,,,,337171229.0,ns,443900 +request-from-bytes,,,,,352980824.0,ns,444360 +request-from-bytes,,,,,337828513.0,ns,444820 +request-from-bytes,,,,,342534433.0,ns,445280 +request-from-bytes,,,,,345901183.0,ns,445740 +request-from-bytes,,,,,353332661.0,ns,446200 +request-from-bytes,,,,,352851367.0,ns,446660 +request-from-bytes,,,,,349161559.0,ns,447120 +request-from-bytes,,,,,340648740.0,ns,447580 +request-from-bytes,,,,,340046539.0,ns,448040 +request-from-bytes,,,,,374910329.0,ns,448500 +request-from-bytes,,,,,390214208.0,ns,448960 +request-from-bytes,,,,,384656604.0,ns,449420 +request-from-bytes,,,,,357805281.0,ns,449880 +request-from-bytes,,,,,351927770.0,ns,450340 +request-from-bytes,,,,,346976288.0,ns,450800 +request-from-bytes,,,,,357100370.0,ns,451260 +request-from-bytes,,,,,356739477.0,ns,451720 +request-from-bytes,,,,,358396461.0,ns,452180 +request-from-bytes,,,,,344587998.0,ns,452640 +request-from-bytes,,,,,377341921.0,ns,453100 +request-from-bytes,,,,,396739258.0,ns,453560 +request-from-bytes,,,,,347475759.0,ns,454020 +request-from-bytes,,,,,345751989.0,ns,454480 +request-from-bytes,,,,,370150530.0,ns,454940 +request-from-bytes,,,,,371384844.0,ns,455400 +request-from-bytes,,,,,362807820.0,ns,455860 +request-from-bytes,,,,,362975944.0,ns,456320 +request-from-bytes,,,,,361214397.0,ns,456780 +request-from-bytes,,,,,381641270.0,ns,457240 +request-from-bytes,,,,,382669116.0,ns,457700 +request-from-bytes,,,,,367249338.0,ns,458160 +request-from-bytes,,,,,361298857.0,ns,458620 +request-from-bytes,,,,,363286547.0,ns,459080 +request-from-bytes,,,,,362780766.0,ns,459540 +request-from-bytes,,,,,360617392.0,ns,460000 diff --git a/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/sample.json b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/sample.json new file mode 100644 index 0000000..b81e190 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/sample.json @@ -0,0 +1 @@ +{"sampling_mode":"Linear","iters":[460.0,920.0,1380.0,1840.0,2300.0,2760.0,3220.0,3680.0,4140.0,4600.0,5060.0,5520.0,5980.0,6440.0,6900.0,7360.0,7820.0,8280.0,8740.0,9200.0,9660.0,10120.0,10580.0,11040.0,11500.0,11960.0,12420.0,12880.0,13340.0,13800.0,14260.0,14720.0,15180.0,15640.0,16100.0,16560.0,17020.0,17480.0,17940.0,18400.0,18860.0,19320.0,19780.0,20240.0,20700.0,21160.0,21620.0,22080.0,22540.0,23000.0,23460.0,23920.0,24380.0,24840.0,25300.0,25760.0,26220.0,26680.0,27140.0,27600.0,28060.0,28520.0,28980.0,29440.0,29900.0,30360.0,30820.0,31280.0,31740.0,32200.0,32660.0,33120.0,33580.0,34040.0,34500.0,34960.0,35420.0,35880.0,36340.0,36800.0,37260.0,37720.0,38180.0,38640.0,39100.0,39560.0,40020.0,40480.0,40940.0,41400.0,41860.0,42320.0,42780.0,43240.0,43700.0,44160.0,44620.0,45080.0,45540.0,46000.0,46460.0,46920.0,47380.0,47840.0,48300.0,48760.0,49220.0,49680.0,50140.0,50600.0,51060.0,51520.0,51980.0,52440.0,52900.0,53360.0,53820.0,54280.0,54740.0,55200.0,55660.0,56120.0,56580.0,57040.0,57500.0,57960.0,58420.0,58880.0,59340.0,59800.0,60260.0,60720.0,61180.0,61640.0,62100.0,62560.0,63020.0,63480.0,63940.0,64400.0,64860.0,65320.0,65780.0,66240.0,66700.0,67160.0,67620.0,68080.0,68540.0,69000.0,69460.0,69920.0,70380.0,70840.0,71300.0,71760.0,72220.0,72680.0,73140.0,73600.0,74060.0,74520.0,74980.0,75440.0,75900.0,76360.0,76820.0,77280.0,77740.0,78200.0,78660.0,79120.0,79580.0,80040.0,80500.0,80960.0,81420.0,81880.0,82340.0,82800.0,83260.0,83720.0,84180.0,84640.0,85100.0,85560.0,86020.0,86480.0,86940.0,87400.0,87860.0,88320.0,88780.0,89240.0,89700.0,90160.0,90620.0,91080.0,91540.0,92000.0,92460.0,92920.0,93380.0,93840.0,94300.0,94760.0,95220.0,95680.0,96140.0,96600.0,97060.0,97520.0,97980.0,98440.0,98900.0,99360.0,99820.0,100280.0,100740.0,101200.0,101660.0,102120.0,102580.0,103040.0,103500.0,103960.0,104420.0,104880.0,105340.0,105800.0,106260.0,106720.0,107180.0,107640.0,108100.0,108560.0,109020.0,109480.0,109940.0,110400.0,110860.0,111320.0,111780.0,112240.0,112700.0,113160.0,113620.0,114080.0,114540.0,115000.0,115460.0,115920.0,116380.0,116840.0,117300.0,117760.0,118220.0,118680.0,119140.0,119600.0,120060.0,120520.0,120980.0,121440.0,121900.0,122360.0,122820.0,123280.0,123740.0,124200.0,124660.0,125120.0,125580.0,126040.0,126500.0,126960.0,127420.0,127880.0,128340.0,128800.0,129260.0,129720.0,130180.0,130640.0,131100.0,131560.0,132020.0,132480.0,132940.0,133400.0,133860.0,134320.0,134780.0,135240.0,135700.0,136160.0,136620.0,137080.0,137540.0,138000.0,138460.0,138920.0,139380.0,139840.0,140300.0,140760.0,141220.0,141680.0,142140.0,142600.0,143060.0,143520.0,143980.0,144440.0,144900.0,145360.0,145820.0,146280.0,146740.0,147200.0,147660.0,148120.0,148580.0,149040.0,149500.0,149960.0,150420.0,150880.0,151340.0,151800.0,152260.0,152720.0,153180.0,153640.0,154100.0,154560.0,155020.0,155480.0,155940.0,156400.0,156860.0,157320.0,157780.0,158240.0,158700.0,159160.0,159620.0,160080.0,160540.0,161000.0,161460.0,161920.0,162380.0,162840.0,163300.0,163760.0,164220.0,164680.0,165140.0,165600.0,166060.0,166520.0,166980.0,167440.0,167900.0,168360.0,168820.0,169280.0,169740.0,170200.0,170660.0,171120.0,171580.0,172040.0,172500.0,172960.0,173420.0,173880.0,174340.0,174800.0,175260.0,175720.0,176180.0,176640.0,177100.0,177560.0,178020.0,178480.0,178940.0,179400.0,179860.0,180320.0,180780.0,181240.0,181700.0,182160.0,182620.0,183080.0,183540.0,184000.0,184460.0,184920.0,185380.0,185840.0,186300.0,186760.0,187220.0,187680.0,188140.0,188600.0,189060.0,189520.0,189980.0,190440.0,190900.0,191360.0,191820.0,192280.0,192740.0,193200.0,193660.0,194120.0,194580.0,195040.0,195500.0,195960.0,196420.0,196880.0,197340.0,197800.0,198260.0,198720.0,199180.0,199640.0,200100.0,200560.0,201020.0,201480.0,201940.0,202400.0,202860.0,203320.0,203780.0,204240.0,204700.0,205160.0,205620.0,206080.0,206540.0,207000.0,207460.0,207920.0,208380.0,208840.0,209300.0,209760.0,210220.0,210680.0,211140.0,211600.0,212060.0,212520.0,212980.0,213440.0,213900.0,214360.0,214820.0,215280.0,215740.0,216200.0,216660.0,217120.0,217580.0,218040.0,218500.0,218960.0,219420.0,219880.0,220340.0,220800.0,221260.0,221720.0,222180.0,222640.0,223100.0,223560.0,224020.0,224480.0,224940.0,225400.0,225860.0,226320.0,226780.0,227240.0,227700.0,228160.0,228620.0,229080.0,229540.0,230000.0,230460.0,230920.0,231380.0,231840.0,232300.0,232760.0,233220.0,233680.0,234140.0,234600.0,235060.0,235520.0,235980.0,236440.0,236900.0,237360.0,237820.0,238280.0,238740.0,239200.0,239660.0,240120.0,240580.0,241040.0,241500.0,241960.0,242420.0,242880.0,243340.0,243800.0,244260.0,244720.0,245180.0,245640.0,246100.0,246560.0,247020.0,247480.0,247940.0,248400.0,248860.0,249320.0,249780.0,250240.0,250700.0,251160.0,251620.0,252080.0,252540.0,253000.0,253460.0,253920.0,254380.0,254840.0,255300.0,255760.0,256220.0,256680.0,257140.0,257600.0,258060.0,258520.0,258980.0,259440.0,259900.0,260360.0,260820.0,261280.0,261740.0,262200.0,262660.0,263120.0,263580.0,264040.0,264500.0,264960.0,265420.0,265880.0,266340.0,266800.0,267260.0,267720.0,268180.0,268640.0,269100.0,269560.0,270020.0,270480.0,270940.0,271400.0,271860.0,272320.0,272780.0,273240.0,273700.0,274160.0,274620.0,275080.0,275540.0,276000.0,276460.0,276920.0,277380.0,277840.0,278300.0,278760.0,279220.0,279680.0,280140.0,280600.0,281060.0,281520.0,281980.0,282440.0,282900.0,283360.0,283820.0,284280.0,284740.0,285200.0,285660.0,286120.0,286580.0,287040.0,287500.0,287960.0,288420.0,288880.0,289340.0,289800.0,290260.0,290720.0,291180.0,291640.0,292100.0,292560.0,293020.0,293480.0,293940.0,294400.0,294860.0,295320.0,295780.0,296240.0,296700.0,297160.0,297620.0,298080.0,298540.0,299000.0,299460.0,299920.0,300380.0,300840.0,301300.0,301760.0,302220.0,302680.0,303140.0,303600.0,304060.0,304520.0,304980.0,305440.0,305900.0,306360.0,306820.0,307280.0,307740.0,308200.0,308660.0,309120.0,309580.0,310040.0,310500.0,310960.0,311420.0,311880.0,312340.0,312800.0,313260.0,313720.0,314180.0,314640.0,315100.0,315560.0,316020.0,316480.0,316940.0,317400.0,317860.0,318320.0,318780.0,319240.0,319700.0,320160.0,320620.0,321080.0,321540.0,322000.0,322460.0,322920.0,323380.0,323840.0,324300.0,324760.0,325220.0,325680.0,326140.0,326600.0,327060.0,327520.0,327980.0,328440.0,328900.0,329360.0,329820.0,330280.0,330740.0,331200.0,331660.0,332120.0,332580.0,333040.0,333500.0,333960.0,334420.0,334880.0,335340.0,335800.0,336260.0,336720.0,337180.0,337640.0,338100.0,338560.0,339020.0,339480.0,339940.0,340400.0,340860.0,341320.0,341780.0,342240.0,342700.0,343160.0,343620.0,344080.0,344540.0,345000.0,345460.0,345920.0,346380.0,346840.0,347300.0,347760.0,348220.0,348680.0,349140.0,349600.0,350060.0,350520.0,350980.0,351440.0,351900.0,352360.0,352820.0,353280.0,353740.0,354200.0,354660.0,355120.0,355580.0,356040.0,356500.0,356960.0,357420.0,357880.0,358340.0,358800.0,359260.0,359720.0,360180.0,360640.0,361100.0,361560.0,362020.0,362480.0,362940.0,363400.0,363860.0,364320.0,364780.0,365240.0,365700.0,366160.0,366620.0,367080.0,367540.0,368000.0,368460.0,368920.0,369380.0,369840.0,370300.0,370760.0,371220.0,371680.0,372140.0,372600.0,373060.0,373520.0,373980.0,374440.0,374900.0,375360.0,375820.0,376280.0,376740.0,377200.0,377660.0,378120.0,378580.0,379040.0,379500.0,379960.0,380420.0,380880.0,381340.0,381800.0,382260.0,382720.0,383180.0,383640.0,384100.0,384560.0,385020.0,385480.0,385940.0,386400.0,386860.0,387320.0,387780.0,388240.0,388700.0,389160.0,389620.0,390080.0,390540.0,391000.0,391460.0,391920.0,392380.0,392840.0,393300.0,393760.0,394220.0,394680.0,395140.0,395600.0,396060.0,396520.0,396980.0,397440.0,397900.0,398360.0,398820.0,399280.0,399740.0,400200.0,400660.0,401120.0,401580.0,402040.0,402500.0,402960.0,403420.0,403880.0,404340.0,404800.0,405260.0,405720.0,406180.0,406640.0,407100.0,407560.0,408020.0,408480.0,408940.0,409400.0,409860.0,410320.0,410780.0,411240.0,411700.0,412160.0,412620.0,413080.0,413540.0,414000.0,414460.0,414920.0,415380.0,415840.0,416300.0,416760.0,417220.0,417680.0,418140.0,418600.0,419060.0,419520.0,419980.0,420440.0,420900.0,421360.0,421820.0,422280.0,422740.0,423200.0,423660.0,424120.0,424580.0,425040.0,425500.0,425960.0,426420.0,426880.0,427340.0,427800.0,428260.0,428720.0,429180.0,429640.0,430100.0,430560.0,431020.0,431480.0,431940.0,432400.0,432860.0,433320.0,433780.0,434240.0,434700.0,435160.0,435620.0,436080.0,436540.0,437000.0,437460.0,437920.0,438380.0,438840.0,439300.0,439760.0,440220.0,440680.0,441140.0,441600.0,442060.0,442520.0,442980.0,443440.0,443900.0,444360.0,444820.0,445280.0,445740.0,446200.0,446660.0,447120.0,447580.0,448040.0,448500.0,448960.0,449420.0,449880.0,450340.0,450800.0,451260.0,451720.0,452180.0,452640.0,453100.0,453560.0,454020.0,454480.0,454940.0,455400.0,455860.0,456320.0,456780.0,457240.0,457700.0,458160.0,458620.0,459080.0,459540.0,460000.0],"times":[423396.0,763368.0,1139009.0,1442643.0,1863104.0,2224093.0,2558578.0,2904812.0,3246499.0,3608606.0,4048003.0,4328930.0,5013826.0,5440118.0,5581925.0,6444760.0,6646863.0,6535022.0,7042539.0,7213840.0,7598918.0,7973458.0,8335864.0,8655360.0,9078081.0,9376160.0,9962427.0,10171708.0,10578636.0,10850147.0,11180130.0,11581329.0,12040742.0,12414532.0,12621262.0,13419443.0,17325675.0,13828911.0,14115473.0,14429913.0,14907534.0,15195192.0,15561657.0,16116673.0,16280330.0,16615179.0,17071766.0,17362158.0,17751493.0,18118889.0,18451559.0,21876222.0,20475868.0,20108021.0,23565093.0,19515505.0,19873714.0,20268315.0,20791379.0,20945076.0,21256444.0,21660321.0,21949370.0,22619533.0,22778183.0,23032863.0,27791577.0,24672641.0,24938247.0,25309670.0,25668647.0,26249075.0,26390427.0,26747527.0,27325198.0,28156513.0,28426781.0,29085663.0,29325971.0,29750781.0,30215380.0,30678510.0,31108055.0,31390350.0,31982561.0,31930288.0,32692516.0,36018656.0,31082184.0,31435928.0,36273427.0,36833988.0,32486131.0,32855880.0,33266494.0,33476981.0,33843069.0,34610028.0,34669921.0,35239798.0,35270914.0,35598266.0,35954093.0,36510435.0,36777463.0,37107350.0,37398604.0,37714950.0,38073525.0,38409887.0,38748317.0,39144640.0,39719624.0,39775154.0,40177580.0,40489456.0,40834276.0,41208996.0,46357406.0,46808276.0,42263855.0,42653821.0,42943466.0,43466585.0,48070872.0,45742540.0,46215261.0,46395431.0,48287119.0,48150983.0,45872531.0,46146988.0,46592409.0,46825398.0,47352814.0,52388229.0,49561640.0,53013864.0,48787182.0,53513886.0,51448405.0,51510404.0,52029213.0,52462404.0,52961346.0,55147686.0,51357773.0,56852390.0,53990135.0,54262271.0,55075060.0,56161292.0,57190544.0,57754173.0,58549544.0,58768882.0,59432335.0,63463022.0,55502143.0,56358263.0,56310457.0,56894054.0,57537325.0,57143092.0,58003193.0,57707272.0,58650169.0,58410275.0,59088214.0,60120044.0,59427953.0,60470191.0,60156623.0,61074397.0,61430387.0,61183741.0,61915941.0,62075184.0,62912408.0,63311045.0,63010893.0,63943212.0,63782219.0,64291645.0,64824927.0,69973514.0,68043654.0,68565923.0,71077157.0,66748562.0,73184243.0,69941169.0,69980471.0,71055198.0,71352144.0,71105752.0,71786697.0,71909497.0,72301204.0,79124916.0,72960786.0,73144933.0,133767236.0,116447501.0,90945761.0,77456444.0,76187669.0,72805047.0,73173069.0,73238888.0,73545377.0,74317071.0,74628828.0,74540756.0,75343427.0,84331790.0,78959218.0,79168441.0,84714154.0,79626623.0,80749932.0,82073128.0,77891034.0,78244498.0,78629983.0,79103551.0,79335376.0,80063340.0,84688215.0,80346665.0,80881578.0,81160319.0,81623092.0,82070914.0,82088719.0,82407145.0,82738287.0,83090696.0,83507559.0,87876654.0,89426470.0,84610034.0,85670407.0,84950090.0,91517369.0,92055915.0,89389913.0,93023819.0,94868892.0,95881459.0,110051474.0,117222763.0,93281618.0,89284352.0,89481690.0,90208446.0,89548744.0,96916755.0,93730272.0,94676136.0,91177046.0,96633986.0,95426395.0,96391928.0,96418060.0,96749845.0,99396129.0,93518753.0,93914950.0,94265033.0,94885673.0,95013249.0,99320859.0,95623895.0,97362077.0,96442056.0,96518860.0,97386057.0,97465999.0,97940533.0,99116136.0,102814733.0,104938039.0,103192719.0,104070140.0,103742358.0,105404373.0,111483343.0,105856989.0,105293322.0,110630482.0,113523602.0,116226353.0,112298578.0,102946565.0,103662794.0,104161586.0,104237017.0,104456678.0,104866214.0,110920296.0,110272274.0,106110402.0,106230767.0,106847683.0,110623890.0,107744248.0,113965988.0,112420588.0,112958203.0,113519093.0,108861778.0,109927855.0,109655662.0,112300112.0,116504337.0,149163274.0,130071907.0,117208821.0,116373946.0,116570662.0,116722432.0,117535943.0,118662931.0,113462716.0,118414990.0,122873305.0,122382180.0,120301627.0,120404213.0,120369981.0,122897788.0,121529879.0,121403908.0,128443005.0,125891732.0,117879701.0,118185060.0,118424489.0,119160183.0,119250356.0,119402688.0,119881237.0,125997242.0,126045162.0,126758348.0,127098406.0,121527795.0,129935786.0,129980525.0,122609662.0,129337413.0,128078142.0,132808216.0,135267711.0,138995587.0,140289177.0,231351159.0,158986039.0,133459051.0,139930072.0,135578341.0,127585753.0,139895515.0,144650315.0,137660988.0,132683375.0,128570245.0,128942509.0,129776311.0,129606813.0,129997618.0,130297182.0,139401184.0,136874887.0,136997456.0,136741767.0,137621278.0,137520753.0,138101447.0,139095855.0,133679433.0,133788634.0,134605309.0,140160883.0,147021531.0,140924887.0,143169218.0,135908954.0,142056050.0,148644367.0,154751576.0,155833911.0,155552218.0,151922852.0,145082059.0,145604384.0,145697896.0,149174396.0,154718493.0,161320188.0,142201175.0,141259213.0,141561144.0,141591524.0,154572683.0,152623069.0,181686867.0,171265518.0,143191108.0,143680618.0,144107656.0,144508532.0,144819617.0,153195937.0,151138974.0,152413010.0,151657820.0,152456999.0,152479027.0,155950471.0,147664840.0,148095818.0,148367069.0,148585456.0,152901277.0,148992271.0,153902899.0,149985409.0,150238809.0,150628490.0,151366269.0,156641774.0,168131584.0,161934973.0,157793957.0,152913229.0,153138433.0,153482900.0,158158900.0,162025503.0,170627795.0,160822747.0,161074080.0,161845796.0,163920748.0,159977286.0,156658634.0,163153129.0,176139734.0,180388660.0,164401025.0,166050961.0,159005660.0,159378147.0,159229922.0,159902688.0,159936523.0,164490663.0,160984763.0,161157224.0,161480569.0,162277382.0,161852388.0,167183574.0,168012391.0,167397140.0,164001917.0,163905001.0,164457302.0,174585150.0,171171845.0,175470595.0,178139632.0,187157837.0,190764654.0,181354591.0,167144915.0,167366075.0,167746570.0,168118470.0,168578445.0,177146990.0,176132561.0,169442346.0,169944079.0,177887767.0,187005816.0,189055067.0,171601806.0,172149967.0,172248999.0,172122877.0,173268132.0,173367660.0,190130103.0,196661048.0,200867935.0,200909623.0,184888169.0,175433164.0,175414133.0,179423050.0,185483527.0,184198286.0,176982645.0,181806408.0,184545659.0,186660379.0,178312065.0,178622725.0,190048729.0,192369250.0,179524654.0,180628373.0,184303554.0,190899785.0,181004553.0,191926485.0,208324982.0,204571109.0,182367935.0,201076622.0,209842216.0,211854275.0,207525151.0,192123190.0,191842442.0,194942011.0,185313511.0,199353428.0,186101530.0,195815983.0,193928169.0,204877521.0,305143872.0,212689145.0,198042400.0,207339135.0,216339976.0,203885178.0,197598290.0,197483051.0,197733183.0,206965250.0,196801884.0,191548444.0,195411419.0,201607764.0,218396456.0,199152602.0,193063329.0,194329366.0,204355042.0,222049013.0,224931479.0,194929236.0,214116209.0,216480443.0,217881117.0,196371649.0,196223196.0,197407600.0,197422509.0,201802591.0,198206730.0,207575568.0,206693407.0,203493226.0,217527628.0,218107247.0,199960555.0,200863178.0,200719888.0,205460568.0,201577245.0,201697407.0,202299423.0,202433942.0,212216975.0,211066776.0,211826637.0,208505205.0,212197968.0,211357543.0,209208966.0,209201770.0,205308785.0,210651839.0,214548162.0,214880042.0,215955502.0,215463071.0,216437600.0,213793989.0,235983165.0,243119031.0,244885471.0,210762007.0,209718240.0,210350711.0,210244139.0,214640010.0,211715876.0,219729027.0,240448508.0,250350540.0,232291828.0,217340496.0,213402642.0,213726081.0,223837778.0,214529534.0,214547373.0,214977918.0,219219351.0,216375244.0,216098218.0,216439389.0,216482171.0,223322575.0,229840031.0,231588219.0,218415507.0,222253437.0,232139976.0,227619724.0,229114018.0,228620266.0,228388319.0,221580484.0,221467127.0,221295875.0,224067804.0,235145526.0,241158848.0,256808930.0,252321004.0,236079138.0,234907988.0,228748541.0,253227668.0,239141489.0,225773008.0,225214317.0,226317780.0,229835965.0,227124220.0,227093174.0,234749494.0,230714854.0,229430592.0,228051036.0,228813189.0,244902026.0,239488714.0,230613500.0,229655879.0,234706392.0,247200446.0,231620274.0,245709577.0,266282899.0,249821059.0,233060575.0,232809201.0,243491109.0,244145380.0,234484728.0,234383418.0,234761088.0,238423284.0,237064208.0,236474575.0,236411523.0,240089431.0,251976443.0,261867597.0,274671808.0,278485362.0,249265383.0,247276477.0,238575451.0,239050117.0,243854851.0,261170829.0,278178306.0,280140694.0,281682164.0,255467426.0,261271353.0,370922120.0,278072171.0,252310302.0,246013521.0,256063456.0,260567745.0,254045186.0,256759711.0,245370058.0,245271220.0,246322698.0,249884564.0,246132473.0,253759766.0,256196878.0,259944407.0,247440486.0,251806811.0,248050290.0,253967009.0,254965466.0,249448597.0,249398158.0,254454619.0,257812116.0,251417510.0,254167995.0,267036563.0,261496110.0,262454685.0,263747356.0,262967968.0,253360662.0,253556092.0,267319261.0,265450758.0,255011703.0,254894826.0,258671907.0,259271284.0,266644894.0,266873783.0,281058524.0,260024073.0,257376413.0,263402514.0,286442462.0,269186882.0,265664747.0,259461487.0,263462504.0,275423195.0,266056443.0,260548060.0,274467286.0,262835887.0,261465983.0,261754978.0,277126395.0,272633788.0,274130758.0,270419344.0,273503416.0,274314890.0,275224853.0,264703562.0,265251209.0,281659072.0,308969401.0,297879062.0,266299688.0,266984024.0,281064992.0,267698480.0,274946813.0,301186866.0,322104674.0,279603457.0,279472975.0,288572149.0,320762706.0,270600514.0,275207395.0,282272682.0,279121721.0,271681686.0,281485162.0,283283104.0,283425112.0,283744342.0,288923476.0,284459751.0,285475239.0,287105003.0,286201394.0,286001895.0,286362799.0,285872610.0,287385253.0,290586583.0,299221688.0,292385987.0,313135360.0,316422845.0,282626502.0,278400715.0,279249343.0,291403684.0,292199883.0,294589790.0,284913547.0,292608869.0,292732722.0,292941075.0,287378252.0,283667167.0,405400888.0,309498685.0,295717973.0,304923994.0,311233275.0,313603937.0,328244506.0,320168947.0,285755987.0,285363935.0,286425134.0,294417302.0,287457230.0,301631902.0,299175353.0,299408528.0,299685865.0,302136252.0,309081005.0,301050290.0,301591656.0,299309638.0,290431419.0,300795994.0,300770906.0,297215048.0,306157791.0,305685893.0,292468215.0,292633271.0,299039888.0,299250530.0,293610923.0,299394698.0,305370452.0,295022763.0,294978748.0,300816771.0,341153107.0,339378618.0,341291706.0,312908200.0,297836154.0,297455220.0,314196126.0,298359357.0,299046197.0,309768121.0,307810273.0,299127488.0,300723690.0,300436049.0,305083254.0,300634124.0,317635268.0,313254486.0,314364930.0,308845099.0,303329832.0,310902025.0,345558573.0,306363414.0,310871873.0,316141103.0,328645937.0,305033037.0,325659683.0,316184023.0,315362980.0,348551004.0,316009602.0,314770759.0,343741513.0,308627222.0,308238540.0,318964480.0,315396528.0,309309159.0,309654967.0,319522819.0,322427669.0,323512487.0,327305270.0,311678477.0,311604647.0,324201333.0,313850483.0,312478082.0,321626642.0,323300272.0,321921200.0,329384825.0,319487890.0,323780857.0,350675620.0,327442838.0,383280934.0,329063784.0,330817364.0,329814455.0,329965866.0,318120063.0,317834280.0,325145059.0,336235396.0,338749030.0,362216778.0,338259690.0,333208001.0,348138429.0,340683111.0,325796474.0,334559859.0,342767493.0,343662077.0,341530622.0,323640751.0,347712689.0,365737343.0,346807904.0,331343500.0,342881186.0,326792264.0,339528201.0,339029226.0,339799804.0,339838132.0,337371678.0,370904023.0,327673224.0,333784781.0,329974959.0,328506508.0,338373558.0,329281013.0,329842707.0,351953461.0,330281110.0,330684101.0,347112555.0,344288480.0,344090996.0,348319477.0,349270847.0,333817280.0,334023392.0,335348169.0,365904229.0,347673975.0,347856827.0,345932823.0,348963578.0,358709134.0,381408270.0,450358962.0,412449248.0,337110162.0,337171229.0,352980824.0,337828513.0,342534433.0,345901183.0,353332661.0,352851367.0,349161559.0,340648740.0,340046539.0,374910329.0,390214208.0,384656604.0,357805281.0,351927770.0,346976288.0,357100370.0,356739477.0,358396461.0,344587998.0,377341921.0,396739258.0,347475759.0,345751989.0,370150530.0,371384844.0,362807820.0,362975944.0,361214397.0,381641270.0,382669116.0,367249338.0,361298857.0,363286547.0,362780766.0,360617392.0]} \ No newline at end of file diff --git a/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/tukey.json b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/tukey.json new file mode 100644 index 0000000..8dd5fa1 --- /dev/null +++ b/apps/aquatic/crates/http_protocol/target/criterion/request-from-bytes/latest/tukey.json @@ -0,0 +1 @@ +[635.6000013134935,698.449239826088,866.0472091930068,928.8964477056013] \ No newline at end of file diff --git a/apps/aquatic/crates/peer_id/Cargo.toml b/apps/aquatic/crates/peer_id/Cargo.toml new file mode 100644 index 0000000..5db42cf --- /dev/null +++ b/apps/aquatic/crates/peer_id/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "aquatic_peer_id" +description = "BitTorrent peer ID handling" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[lib] +name = "aquatic_peer_id" + +[features] +default = ["quickcheck"] + +[dependencies] +compact_str = "0.8" +hex = "0.4" +regex = "1" +serde = { version = "1", features = ["derive"] } +quickcheck = { version = "1", optional = true } +zerocopy = { version = "0.7", features = ["derive"] } \ No newline at end of file diff --git a/apps/aquatic/crates/peer_id/README.md b/apps/aquatic/crates/peer_id/README.md new file mode 100644 index 0000000..010f1bf --- /dev/null +++ b/apps/aquatic/crates/peer_id/README.md @@ -0,0 +1,3 @@ +# aquatic_peer_id + +Extract BitTorrent client information from announce request peer IDs. \ No newline at end of file diff --git a/apps/aquatic/crates/peer_id/src/lib.rs b/apps/aquatic/crates/peer_id/src/lib.rs new file mode 100644 index 0000000..304e1e5 --- /dev/null +++ b/apps/aquatic/crates/peer_id/src/lib.rs @@ -0,0 +1,293 @@ +use std::{borrow::Cow, fmt::Display, sync::OnceLock}; + +use compact_str::{format_compact, CompactString}; +use regex::bytes::Regex; +use serde::{Deserialize, Serialize}; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + AsBytes, + FromBytes, + FromZeroes, +)] +#[repr(transparent)] +pub struct PeerId(pub [u8; 20]); + +impl PeerId { + pub fn client(&self) -> PeerClient { + PeerClient::from_peer_id(self) + } + pub fn first_8_bytes_hex(&self) -> CompactString { + let mut buf = [0u8; 16]; + + hex::encode_to_slice(&self.0[..8], &mut buf) + .expect("PeerId.first_8_bytes_hex buffer too small"); + + CompactString::from_utf8_lossy(&buf) + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PeerClient { + BitTorrent(CompactString), + Deluge(CompactString), + LibTorrentRakshasa(CompactString), + LibTorrentRasterbar(CompactString), + QBitTorrent(CompactString), + Transmission(CompactString), + UTorrent(CompactString), + UTorrentEmbedded(CompactString), + UTorrentMac(CompactString), + UTorrentWeb(CompactString), + Vuze(CompactString), + WebTorrent(CompactString), + WebTorrentDesktop(CompactString), + Mainline(CompactString), + OtherWithPrefixAndVersion { + prefix: CompactString, + version: CompactString, + }, + OtherWithPrefix(CompactString), + Other, +} + +impl PeerClient { + pub fn from_prefix_and_version(prefix: &[u8], version: &[u8]) -> Self { + fn three_digits_plus_prerelease(v1: char, v2: char, v3: char, v4: char) -> CompactString { + let prerelease: Cow = match v4 { + 'd' | 'D' => " dev".into(), + 'a' | 'A' => " alpha".into(), + 'b' | 'B' => " beta".into(), + 'r' | 'R' => " rc".into(), + 's' | 'S' => " stable".into(), + other => format_compact!("{}", other).into(), + }; + + format_compact!("{}.{}.{}{}", v1, v2, v3, prerelease) + } + + fn webtorrent(v1: char, v2: char, v3: char, v4: char) -> CompactString { + let major = if v1 == '0' { + format_compact!("{}", v2) + } else { + format_compact!("{}{}", v1, v2) + }; + + let minor = if v3 == '0' { + format_compact!("{}", v4) + } else { + format_compact!("{}{}", v3, v4) + }; + + format_compact!("{}.{}", major, minor) + } + + if let [v1, v2, v3, v4] = version { + let (v1, v2, v3, v4) = (*v1 as char, *v2 as char, *v3 as char, *v4 as char); + + match prefix { + b"AZ" => Self::Vuze(format_compact!("{}.{}.{}.{}", v1, v2, v3, v4)), + b"BT" => Self::BitTorrent(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"DE" => Self::Deluge(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"lt" => Self::LibTorrentRakshasa(format_compact!("{}.{}{}.{}", v1, v2, v3, v4)), + b"LT" => Self::LibTorrentRasterbar(format_compact!("{}.{}{}.{}", v1, v2, v3, v4)), + b"qB" => Self::QBitTorrent(format_compact!("{}.{}.{}", v1, v2, v3)), + b"TR" => { + let v = match (v1, v2, v3, v4) { + ('0', '0', '0', v4) => format_compact!("0.{}", v4), + ('0', '0', v3, v4) => format_compact!("0.{}{}", v3, v4), + _ => format_compact!("{}.{}{}", v1, v2, v3), + }; + + Self::Transmission(v) + } + b"UE" => Self::UTorrentEmbedded(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"UM" => Self::UTorrentMac(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"UT" => Self::UTorrent(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"UW" => Self::UTorrentWeb(three_digits_plus_prerelease(v1, v2, v3, v4)), + b"WD" => Self::WebTorrentDesktop(webtorrent(v1, v2, v3, v4)), + b"WW" => Self::WebTorrent(webtorrent(v1, v2, v3, v4)), + _ => Self::OtherWithPrefixAndVersion { + prefix: CompactString::from_utf8_lossy(prefix), + version: CompactString::from_utf8_lossy(version), + }, + } + } else { + match (prefix, version) { + (b"M", &[major, b'-', minor, b'-', patch, b'-']) => Self::Mainline( + format_compact!("{}.{}.{}", major as char, minor as char, patch as char), + ), + (b"M", &[major, b'-', minor1, minor2, b'-', patch]) => { + Self::Mainline(format_compact!( + "{}.{}{}.{}", + major as char, + minor1 as char, + minor2 as char, + patch as char + )) + } + _ => Self::OtherWithPrefixAndVersion { + prefix: CompactString::from_utf8_lossy(prefix), + version: CompactString::from_utf8_lossy(version), + }, + } + } + } + + pub fn from_peer_id(peer_id: &PeerId) -> Self { + static AZ_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = AZ_RE + .get_or_init(|| { + Regex::new(r"^\-(?P[a-zA-Z]{2})(?P[0-9]{3}[0-9a-zA-Z])") + .expect("compile AZ_RE regex") + }) + .captures(&peer_id.0) + { + return Self::from_prefix_and_version(&caps["name"], &caps["version"]); + } + + static MAINLINE_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = MAINLINE_RE + .get_or_init(|| { + Regex::new(r"^(?P[a-zA-Z])(?P[0-9\-]{6})\-") + .expect("compile MAINLINE_RE regex") + }) + .captures(&peer_id.0) + { + return Self::from_prefix_and_version(&caps["name"], &caps["version"]); + } + + static PREFIX_RE: OnceLock = OnceLock::new(); + + if let Some(caps) = PREFIX_RE + .get_or_init(|| { + Regex::new(r"^(?P[a-zA-Z0-9\-]+)\-").expect("compile PREFIX_RE regex") + }) + .captures(&peer_id.0) + { + return Self::OtherWithPrefix(CompactString::from_utf8_lossy(&caps["prefix"])); + } + + Self::Other + } +} + +impl Display for PeerClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BitTorrent(v) => write!(f, "BitTorrent {}", v.as_str()), + Self::Deluge(v) => write!(f, "Deluge {}", v.as_str()), + Self::LibTorrentRakshasa(v) => write!(f, "lt (rakshasa) {}", v.as_str()), + Self::LibTorrentRasterbar(v) => write!(f, "lt (rasterbar) {}", v.as_str()), + Self::QBitTorrent(v) => write!(f, "QBitTorrent {}", v.as_str()), + Self::Transmission(v) => write!(f, "Transmission {}", v.as_str()), + Self::UTorrent(v) => write!(f, "µTorrent {}", v.as_str()), + Self::UTorrentEmbedded(v) => write!(f, "µTorrent Emb. {}", v.as_str()), + Self::UTorrentMac(v) => write!(f, "µTorrent Mac {}", v.as_str()), + Self::UTorrentWeb(v) => write!(f, "µTorrent Web {}", v.as_str()), + Self::Vuze(v) => write!(f, "Vuze {}", v.as_str()), + Self::WebTorrent(v) => write!(f, "WebTorrent {}", v.as_str()), + Self::WebTorrentDesktop(v) => write!(f, "WebTorrent Desktop {}", v.as_str()), + Self::Mainline(v) => write!(f, "Mainline {}", v.as_str()), + Self::OtherWithPrefixAndVersion { prefix, version } => { + write!(f, "Other ({}) ({})", prefix.as_str(), version.as_str()) + } + Self::OtherWithPrefix(prefix) => write!(f, "Other ({})", prefix.as_str()), + Self::Other => f.write_str("Other"), + } + } +} + +#[cfg(feature = "quickcheck")] +impl quickcheck::Arbitrary for PeerId { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut bytes = [0u8; 20]; + + for byte in bytes.iter_mut() { + *byte = u8::arbitrary(g); + } + + Self(bytes) + } +} + +#[cfg(feature = "quickcheck")] +#[cfg(test)] +mod tests { + use super::*; + + fn create_peer_id(bytes: &[u8]) -> PeerId { + let mut peer_id = PeerId([0; 20]); + + let len = bytes.len(); + + peer_id.0[..len].copy_from_slice(bytes); + + peer_id + } + + #[test] + fn test_client_from_peer_id() { + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-lt1234-k/asdh3")), + PeerClient::LibTorrentRakshasa("1.23.4".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-DE123s-k/asdh3")), + PeerClient::Deluge("1.2.3 stable".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-DE123r-k/asdh3")), + PeerClient::Deluge("1.2.3 rc".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-UT123A-k/asdh3")), + PeerClient::UTorrent("1.2.3 alpha".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-TR0012-k/asdh3")), + PeerClient::Transmission("0.12".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-TR1212-k/asdh3")), + PeerClient::Transmission("1.21".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-WW0102-k/asdh3")), + PeerClient::WebTorrent("1.2".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-WW1302-k/asdh3")), + PeerClient::WebTorrent("13.2".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"-WW1324-k/asdh3")), + PeerClient::WebTorrent("13.24".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"M1-2-3--k/asdh3")), + PeerClient::Mainline("1.2.3".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"M1-23-4-k/asdh3")), + PeerClient::Mainline("1.23.4".into()) + ); + assert_eq!( + PeerClient::from_peer_id(&create_peer_id(b"S3-k/asdh3")), + PeerClient::OtherWithPrefix("S3".into()) + ); + } +} diff --git a/apps/aquatic/crates/toml_config/Cargo.toml b/apps/aquatic/crates/toml_config/Cargo.toml new file mode 100644 index 0000000..048d66c --- /dev/null +++ b/apps/aquatic/crates/toml_config/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "aquatic_toml_config" +description = "Serialize toml with comments" +keywords = ["toml"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +rust-version.workspace = true + +[lib] +name = "aquatic_toml_config" + +[dependencies] +toml = "0.5" +aquatic_toml_config_derive.workspace = true + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/toml_config/src/lib.rs b/apps/aquatic/crates/toml_config/src/lib.rs new file mode 100644 index 0000000..ed77803 --- /dev/null +++ b/apps/aquatic/crates/toml_config/src/lib.rs @@ -0,0 +1,128 @@ +pub use aquatic_toml_config_derive::TomlConfig; +pub use toml; + +/// Run this on your struct implementing TomlConfig to generate a +/// serialization/deserialization test for it. +#[macro_export] +macro_rules! gen_serialize_deserialize_test { + ($ident:ident) => { + #[test] + fn test_cargo_toml_serialize_deserialize() { + use ::aquatic_toml_config::TomlConfig; + let serialized = $ident::default_to_string(); + let deserialized = ::aquatic_toml_config::toml::de::from_str(&serialized).unwrap(); + + assert_eq!($ident::default(), deserialized); + } + }; +} + +/// Export structs to toml, converting Rust doc strings to comments. +/// +/// Supports one level of nesting. Fields containing structs must come +/// after regular fields. +/// +/// Usage: +/// ``` +/// use aquatic_toml_config::TomlConfig; +/// +/// #[derive(TomlConfig)] +/// struct SubConfig { +/// /// A +/// a: usize, +/// /// B +/// b: String, +/// } +/// +/// impl Default for SubConfig { +/// fn default() -> Self { +/// Self { +/// a: 200, +/// b: "subconfig hello".into(), +/// } +/// } +/// } +/// +/// #[derive(TomlConfig)] +/// struct Config { +/// /// A +/// a: usize, +/// /// B +/// b: String, +/// /// C +/// c: SubConfig, +/// } +/// +/// impl Default for Config { +/// fn default() -> Self { +/// Self { +/// a: 100, +/// b: "hello".into(), +/// c: Default::default(), +/// } +/// } +/// } +/// +/// let expected = "# A\na = 100\n# B\nb = \"hello\"\n\n# C\n[c]\n# A\na = 200\n# B\nb = \"subconfig hello\"\n"; +/// +/// assert_eq!( +/// Config::default_to_string(), +/// expected, +/// ); +/// ``` +pub trait TomlConfig: Default { + fn default_to_string() -> String; +} + +pub mod __private { + use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6}; + use std::path::PathBuf; + + pub trait Private { + fn __to_string(&self, comment: Option, field_name: String) -> String; + } + + macro_rules! impl_trait { + ($ident:ident) => { + impl Private for $ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + + if let Some(comment) = comment { + output.push_str(&comment); + } + + let value = crate::toml::ser::to_string(self).unwrap(); + + output.push_str(&format!("{} = {}\n", field_name, value)); + + output + } + } + }; + } + + impl_trait!(isize); + impl_trait!(i8); + impl_trait!(i16); + impl_trait!(i32); + impl_trait!(i64); + + impl_trait!(usize); + impl_trait!(u8); + impl_trait!(u16); + impl_trait!(u32); + impl_trait!(u64); + + impl_trait!(f32); + impl_trait!(f64); + + impl_trait!(bool); + + impl_trait!(String); + + impl_trait!(PathBuf); + impl_trait!(SocketAddr); + impl_trait!(SocketAddrV4); + impl_trait!(SocketAddrV6); +} diff --git a/apps/aquatic/crates/toml_config/tests/test.rs b/apps/aquatic/crates/toml_config/tests/test.rs new file mode 100644 index 0000000..0cd1554 --- /dev/null +++ b/apps/aquatic/crates/toml_config/tests/test.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +use aquatic_toml_config::{gen_serialize_deserialize_test, TomlConfig}; + +#[derive(Clone, Debug, PartialEq, Eq, TomlConfig, Deserialize)] +struct TestConfigInnerA { + /// Comment for a + a: String, + /// Comment for b + b: usize, +} + +impl Default for TestConfigInnerA { + fn default() -> Self { + Self { + a: "Inner hello world".into(), + b: 100, + } + } +} + +/// Comment for TestConfig +#[derive(Clone, Debug, PartialEq, Eq, TomlConfig, Deserialize)] +struct TestConfig { + /// Comment for a that stretches over + /// multiple lines + a: String, + /// Comment for b + b: usize, + c: bool, + /// Comment for TestConfigInnerA + inner_a: TestConfigInnerA, +} + +impl Default for TestConfig { + fn default() -> Self { + Self { + a: "Hello, world!".into(), + b: 100, + c: true, + inner_a: Default::default(), + } + } +} + +gen_serialize_deserialize_test!(TestConfig); diff --git a/apps/aquatic/crates/toml_config_derive/Cargo.toml b/apps/aquatic/crates/toml_config_derive/Cargo.toml new file mode 100644 index 0000000..951fe97 --- /dev/null +++ b/apps/aquatic/crates/toml_config_derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "aquatic_toml_config_derive" +description = "Serialize toml with comments" +exclude = ["target"] +keywords = ["toml"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = "1" diff --git a/apps/aquatic/crates/toml_config_derive/src/lib.rs b/apps/aquatic/crates/toml_config_derive/src/lib.rs new file mode 100644 index 0000000..89cdff7 --- /dev/null +++ b/apps/aquatic/crates/toml_config_derive/src/lib.rs @@ -0,0 +1,174 @@ +use proc_macro2::{TokenStream, TokenTree}; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, Type}; + +#[proc_macro_derive(TomlConfig)] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let comment = extract_comment_string(input.attrs); + let ident = input.ident; + + match input.data { + Data::Struct(struct_data) => { + let mut output_stream = quote! { + let mut output = String::new(); + }; + + extract_from_struct(ident.clone(), struct_data, &mut output_stream); + + proc_macro::TokenStream::from(quote! { + impl ::aquatic_toml_config::TomlConfig for #ident { + fn default_to_string() -> String { + let mut output = String::new(); + + let comment: Option = #comment; + + if let Some(comment) = comment { + output.push_str(&comment); + output.push('\n'); + } + + let body = { + #output_stream + + output + }; + + output.push_str(&body); + + output + } + } + impl ::aquatic_toml_config::__private::Private for #ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + + output.push('\n'); + + if let Some(comment) = comment { + output.push_str(&comment); + } + output.push_str(&format!("[{}]\n", field_name)); + + let body = { + #output_stream + + output + }; + + output.push_str(&body); + + output + } + } + }) + } + Data::Enum(_) => proc_macro::TokenStream::from(quote! { + impl ::aquatic_toml_config::__private::Private for #ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + let wrapping_comment: Option = #comment; + + if let Some(comment) = wrapping_comment { + output.push_str(&comment); + } + + if let Some(comment) = comment { + output.push_str(&comment); + } + + let value = match ::aquatic_toml_config::toml::ser::to_string(self) { + Ok(value) => value, + Err(err) => panic!("Couldn't serialize enum to toml: {:#}", err), + }; + + output.push_str(&format!("{} = {}\n", field_name, value)); + + output + } + } + }), + Data::Union(_) => panic!("Unions are not supported"), + } +} + +fn extract_from_struct( + struct_ty_ident: Ident, + struct_data: DataStruct, + output_stream: &mut TokenStream, +) { + let fields = if let Fields::Named(fields) = struct_data.fields { + fields + } else { + panic!("Fields are not named"); + }; + + output_stream.extend(::std::iter::once(quote! { + let struct_default = #struct_ty_ident::default(); + })); + + for field in fields.named.into_iter() { + let ident = field.ident.expect("Encountered unnamed field"); + let ident_string = format!("{}", ident); + let comment = extract_comment_string(field.attrs); + + if let Type::Path(path) = field.ty { + output_stream.extend(::std::iter::once(quote! { + { + let comment: Option = #comment; + let field_default: #path = struct_default.#ident; + + let s: String = ::aquatic_toml_config::__private::Private::__to_string( + &field_default, + comment, + #ident_string.to_string() + ); + output.push_str(&s); + } + })); + } + } +} + +fn extract_comment_string(attrs: Vec) -> TokenStream { + let mut output = String::new(); + + for attr in attrs.into_iter() { + let path_ident = if let Some(path_ident) = attr.path.get_ident() { + path_ident + } else { + continue; + }; + + if format!("{}", path_ident) != "doc" { + continue; + } + + for token_tree in attr.tokens { + if let TokenTree::Literal(literal) = token_tree { + let mut comment = format!("{}", literal); + + // Strip leading and trailing quotation marks + comment.remove(comment.len() - 1); + comment.remove(0); + + // Add toml comment indicator + comment.insert(0, '#'); + + output.push_str(&comment); + output.push('\n'); + } + } + } + + if output.is_empty() { + quote! { + None + } + } else { + quote! { + Some(#output.to_string()) + } + } +} diff --git a/apps/aquatic/crates/udp/Cargo.toml b/apps/aquatic/crates/udp/Cargo.toml new file mode 100644 index 0000000..6e95bf7 --- /dev/null +++ b/apps/aquatic/crates/udp/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "aquatic_udp" +description = "High-performance open UDP BitTorrent tracker" +keywords = ["udp", "server", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[lib] +name = "aquatic_udp" + +[[bin]] +name = "aquatic_udp" + +[features] +default = ["prometheus", "mimalloc"] +# Export prometheus metrics +prometheus = ["metrics", "aquatic_common/prometheus"] +# Experimental io_uring support (Linux 6.0 or later required) +io-uring = ["dep:io-uring"] +# Use mimalloc allocator for much better performance. +# +# Requires cmake and a C compiler +mimalloc = ["dep:mimalloc"] + +[dependencies] +aquatic_common.workspace = true +aquatic_toml_config.workspace = true +aquatic_udp_protocol.workspace = true + +anyhow = "1" +arrayvec = "0.7" +blake3 = "1" +cfg-if = "1" +compact_str = "0.8" +constant_time_eq = "0.3" +crossbeam-channel = "0.5" +crossbeam-utils = "0.8" +getrandom = "0.2" +hashbrown = { version = "0.15", default-features = false } +hdrhistogram = "7" +hex = "0.4" +libc = "0.2" +log = "0.4" +mio = { version = "1", features = ["net", "os-poll"] } +num-format = "0.4" +parking_lot = "0.12" +rand = { version = "0.8", features = ["small_rng"] } +serde = { version = "1", features = ["derive"] } +signal-hook = { version = "0.3" } +slab = "0.4" +socket2 = { version = "0.5", features = ["all"] } +time = { version = "0.3", features = ["formatting"] } +tinytemplate = "1" + +# prometheus feature +metrics = { version = "0.24", optional = true } + +# io-uring feature +io-uring = { version = "0.7", optional = true } + +# mimalloc feature +mimalloc = { version = "0.1", default-features = false, optional = true } + +[dev-dependencies] +tempfile = "3" +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/udp/README.md b/apps/aquatic/crates/udp/README.md new file mode 100644 index 0000000..8af68b9 --- /dev/null +++ b/apps/aquatic/crates/udp/README.md @@ -0,0 +1,94 @@ +# aquatic_udp: high-performance open UDP BitTorrent tracker + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +High-performance open UDP BitTorrent tracker for Unix-like operating systems. + +Features at a glance: + +- Multithreaded design for handling large amounts of traffic +- All data is stored in-memory (no database needed) +- IPv4 and IPv6 support +- Supports forbidding/allowing info hashes +- Prometheus metrics +- Automated CI testing of full file transfers + +Known users: + +- [explodie.org public tracker](https://explodie.org/opentracker.html) (`udp://explodie.org:6969`), typically [serving ~100,000 requests per second](https://explodie.org/tracker-stats.html) + +This is the most mature implementation in the aquatic family. I consider it fully ready for production use. + +## Performance + +![UDP BitTorrent tracker throughput](../../documents/aquatic-udp-load-test-2024-02-10.png) + +More benchmark details are available [here](../../documents/aquatic-udp-load-test-2024-02-10.md). + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_udp +``` + +### Configuring and running + +Generate the configuration file: + +```sh +./target/release/aquatic_udp -p > "aquatic-udp-config.toml" +``` + +Make necessary adjustments to the file. You will likely want to adjust +listening addresses under the `network` section. + +Once done, start the application: + +```sh +./target/release/aquatic_udp -c "aquatic-udp-config.toml" +``` + +If your server is pointed to by domain `example.com` and you configured the +tracker to run on port 3000, people can now use it by adding the URL +`udp://example.com:3000` to their torrent files or magnet links. + +### Load testing + +A load test application is available. It supports generation and loading of +configuration files in a similar manner to the tracker application. + +After starting the tracker, run the load tester: + +```sh +. ./scripts/env-native-cpu-without-avx-512 # Optional + +cargo run --release -p aquatic_udp_load_test -- --help +``` + +## Details + +Implements [BEP 015](https://www.bittorrent.org/beps/bep_0015.html) ([more details](https://libtorrent.org/udp_tracker_protocol.html)) with the following exceptions: + +- Ignores IP addresses sent in announce requests. The packet source IP is always used. +- Doesn't track the number of torrent downloads (0 is always sent). + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. diff --git a/apps/aquatic/crates/udp/src/common.rs b/apps/aquatic/crates/udp/src/common.rs new file mode 100644 index 0000000..dce58cb --- /dev/null +++ b/apps/aquatic/crates/udp/src/common.rs @@ -0,0 +1,148 @@ +use std::iter::repeat_with; +use std::sync::atomic::AtomicUsize; +use std::sync::Arc; + +use aquatic_common::access_list::AccessListArcSwap; +use aquatic_common::ServerStartInstant; +use aquatic_udp_protocol::*; +use crossbeam_utils::CachePadded; +use hdrhistogram::Histogram; + +use crate::config::Config; +use crate::swarm::TorrentMaps; + +pub const BUFFER_SIZE: usize = 8192; + +#[derive(Clone, Copy, Debug)] +pub enum IpVersion { + V4, + V6, +} + +#[cfg(feature = "prometheus")] +impl IpVersion { + pub fn prometheus_str(&self) -> &'static str { + match self { + Self::V4 => "4", + Self::V6 => "6", + } + } +} + +#[derive(Clone)] +pub struct Statistics { + pub socket: Vec>>, + pub swarm: CachePaddedArc>, +} + +impl Statistics { + pub fn new(config: &Config) -> Self { + Self { + socket: repeat_with(Default::default) + .take(config.socket_workers) + .collect(), + swarm: Default::default(), + } + } +} + +#[derive(Default)] +pub struct IpVersionStatistics { + pub ipv4: T, + pub ipv6: T, +} + +impl IpVersionStatistics { + pub fn by_ip_version(&self, ip_version: IpVersion) -> &T { + match ip_version { + IpVersion::V4 => &self.ipv4, + IpVersion::V6 => &self.ipv6, + } + } +} + +#[derive(Default)] +pub struct SocketWorkerStatistics { + pub requests: AtomicUsize, + pub responses_connect: AtomicUsize, + pub responses_announce: AtomicUsize, + pub responses_scrape: AtomicUsize, + pub responses_error: AtomicUsize, + pub bytes_received: AtomicUsize, + pub bytes_sent: AtomicUsize, +} + +pub type CachePaddedArc = CachePadded>>; + +#[derive(Default)] +pub struct SwarmWorkerStatistics { + pub torrents: AtomicUsize, + pub peers: AtomicUsize, +} + +pub enum StatisticsMessage { + Ipv4PeerHistogram(Histogram), + Ipv6PeerHistogram(Histogram), + PeerAdded(PeerId), + PeerRemoved(PeerId), +} + +#[derive(Clone)] +pub struct State { + pub access_list: Arc, + pub torrent_maps: TorrentMaps, + pub server_start_instant: ServerStartInstant, +} + +impl Default for State { + fn default() -> Self { + Self { + access_list: Arc::new(AccessListArcSwap::default()), + torrent_maps: TorrentMaps::default(), + server_start_instant: ServerStartInstant::new(), + } + } +} + +#[cfg(test)] +mod tests { + use std::{net::Ipv6Addr, num::NonZeroU16}; + + use crate::config::Config; + + use super::*; + + // Assumes that announce response with maximum amount of ipv6 peers will + // be the longest + #[test] + fn test_buffer_size() { + use aquatic_udp_protocol::*; + + let config = Config::default(); + + let peers = ::std::iter::repeat(ResponsePeer { + ip_address: Ipv6AddrBytes(Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1).octets()), + port: Port::new(NonZeroU16::new(1).unwrap()), + }) + .take(config.protocol.max_response_peers) + .collect(); + + let response = Response::AnnounceIpv6(AnnounceResponse { + fixed: AnnounceResponseFixedData { + transaction_id: TransactionId::new(1), + announce_interval: AnnounceInterval::new(1), + seeders: NumberOfPeers::new(1), + leechers: NumberOfPeers::new(1), + }, + peers, + }); + + let mut buf = Vec::new(); + + response.write_bytes(&mut buf).unwrap(); + + println!("Buffer len: {}", buf.len()); + + assert!(buf.len() <= BUFFER_SIZE); + } +} diff --git a/apps/aquatic/crates/udp/src/config.rs b/apps/aquatic/crates/udp/src/config.rs new file mode 100644 index 0000000..0d3e6bc --- /dev/null +++ b/apps/aquatic/crates/udp/src/config.rs @@ -0,0 +1,262 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + path::PathBuf, +}; + +use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; +use cfg_if::cfg_if; +use serde::{Deserialize, Serialize}; + +use aquatic_common::cli::LogLevel; +use aquatic_toml_config::TomlConfig; + +/// aquatic_udp configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + /// Number of socket workers + /// + /// 0 = automatically set to number of available virtual CPUs + pub socket_workers: usize, + pub log_level: LogLevel, + pub network: NetworkConfig, + pub protocol: ProtocolConfig, + pub statistics: StatisticsConfig, + pub cleaning: CleaningConfig, + pub privileges: PrivilegeConfig, + /// Access list configuration + /// + /// The file is read on start and when the program receives `SIGUSR1`. If + /// initial parsing fails, the program exits. Later failures result in in + /// emitting of an error-level log message, while successful updates of the + /// access list result in emitting of an info-level log message. + pub access_list: AccessListConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + socket_workers: 1, + log_level: LogLevel::Error, + network: NetworkConfig::default(), + protocol: ProtocolConfig::default(), + statistics: StatisticsConfig::default(), + cleaning: CleaningConfig::default(), + privileges: PrivilegeConfig::default(), + access_list: AccessListConfig::default(), + } + } +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct NetworkConfig { + /// Use IPv4 + pub use_ipv4: bool, + /// Use IPv6 + pub use_ipv6: bool, + /// IPv4 address and port + /// + /// Examples: + /// - Use 0.0.0.0:3000 to bind to all interfaces on port 3000 + /// - Use 127.0.0.1:3000 to bind to the loopback interface (localhost) on + /// port 3000 + pub address_ipv4: SocketAddrV4, + /// IPv6 address and port + /// + /// Examples: + /// - Use [::]:3000 to bind to all interfaces on port 3000 + /// - Use [::1]:3000 to bind to the loopback interface (localhost) on + /// port 3000 + pub address_ipv6: SocketAddrV6, + /// Size of socket recv buffer. Use 0 for OS default. + /// + /// This setting can have a big impact on dropped packages. 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 + pub socket_recv_buffer_size: usize, + /// Poll timeout in milliseconds (mio backend only) + pub poll_timeout_ms: u64, + /// Store this many responses at most for retrying (once) on send failure + /// (mio backend only) + /// + /// Useful on operating systems that do not provide an udp send buffer, + /// such as FreeBSD. Setting the value to zero disables resending + /// functionality. + pub resend_buffer_max_len: usize, + /// Set flag on IPv6 socket to only accept IPv6 traffic. + /// + /// This should typically be set to true unless your OS does not support + /// double-stack sockets (that is, sockets that receive both IPv4 and IPv6 + /// packets). + pub set_only_ipv6: bool, + #[cfg(feature = "io-uring")] + pub use_io_uring: bool, + /// Number of ring entries (io_uring backend only) + /// + /// Will be rounded to next power of two if not already one. + #[cfg(feature = "io-uring")] + pub ring_size: u16, +} + +impl NetworkConfig { + pub fn ipv4_active(&self) -> bool { + self.use_ipv4 + } + pub fn ipv6_active(&self) -> bool { + self.use_ipv6 + } +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + use_ipv4: true, + use_ipv6: true, + address_ipv4: SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 3000), + address_ipv6: SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 3000, 0, 0), + socket_recv_buffer_size: 8_000_000, + poll_timeout_ms: 50, + resend_buffer_max_len: 0, + set_only_ipv6: true, + #[cfg(feature = "io-uring")] + use_io_uring: true, + #[cfg(feature = "io-uring")] + ring_size: 128, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct ProtocolConfig { + /// Maximum number of torrents to allow in scrape request + pub max_scrape_torrents: u8, + /// Maximum number of peers to return in announce response + pub max_response_peers: usize, + /// Ask peers to announce this often (seconds) + pub peer_announce_interval: i32, +} + +impl Default for ProtocolConfig { + fn default() -> Self { + Self { + max_scrape_torrents: 70, + max_response_peers: 30, + peer_announce_interval: 60 * 15, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct StatisticsConfig { + /// Collect and print/write statistics this often (seconds) + pub interval: u64, + /// Collect statistics on number of peers per torrent + /// + /// Will increase time taken for torrent cleaning. + pub torrent_peer_histograms: bool, + /// Collect statistics on peer clients. + /// + /// Also, see `prometheus_peer_id_prefixes`. + /// + /// Expect a certain CPU hit (maybe 5% higher consumption) and a bit higher + /// memory use + pub peer_clients: bool, + /// Print statistics to standard output + pub print_to_stdout: bool, + /// Save statistics as HTML to a file + pub write_html_to_file: bool, + /// Path to save HTML file to + pub html_file_path: PathBuf, + /// Run a prometheus endpoint + #[cfg(feature = "prometheus")] + pub run_prometheus_endpoint: bool, + /// Address to run prometheus endpoint on + #[cfg(feature = "prometheus")] + pub prometheus_endpoint_address: SocketAddr, + /// Serve information on all peer id prefixes on the prometheus endpoint. + /// + /// Requires `peer_clients` to be activated. + /// + /// May consume quite a bit of CPU and RAM, since data on every single peer + /// client will be reported continuously on the endpoint + #[cfg(feature = "prometheus")] + pub prometheus_peer_id_prefixes: bool, +} + +impl StatisticsConfig { + cfg_if! { + if #[cfg(feature = "prometheus")] { + pub fn active(&self) -> bool { + (self.interval != 0) & + (self.print_to_stdout | self.write_html_to_file | self.run_prometheus_endpoint) + } + } else { + pub fn active(&self) -> bool { + (self.interval != 0) & (self.print_to_stdout | self.write_html_to_file) + } + } + } +} + +impl Default for StatisticsConfig { + fn default() -> Self { + Self { + interval: 5, + torrent_peer_histograms: false, + peer_clients: false, + print_to_stdout: false, + write_html_to_file: false, + html_file_path: "tmp/statistics.html".into(), + #[cfg(feature = "prometheus")] + run_prometheus_endpoint: false, + #[cfg(feature = "prometheus")] + prometheus_endpoint_address: SocketAddr::from(([0, 0, 0, 0], 9000)), + #[cfg(feature = "prometheus")] + prometheus_peer_id_prefixes: false, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CleaningConfig { + /// Clean torrents this often (seconds) + pub torrent_cleaning_interval: u64, + /// Allow clients to use a connection token for this long (seconds) + pub max_connection_age: u32, + /// Remove peers who have not announced for this long (seconds) + pub max_peer_age: u32, +} + +impl Default for CleaningConfig { + fn default() -> Self { + Self { + torrent_cleaning_interval: 60 * 2, + max_connection_age: 60 * 2, + max_peer_age: 60 * 20, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/udp/src/lib.rs b/apps/aquatic/crates/udp/src/lib.rs new file mode 100644 index 0000000..058e6bc --- /dev/null +++ b/apps/aquatic/crates/udp/src/lib.rs @@ -0,0 +1,188 @@ +pub mod common; +pub mod config; +pub mod swarm; +pub mod workers; + +use std::thread::{available_parallelism, sleep, Builder, JoinHandle}; +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::WorkerType; +use crossbeam_channel::unbounded; +use signal_hook::consts::SIGUSR1; +use signal_hook::iterator::Signals; + +use aquatic_common::access_list::update_access_list; +use aquatic_common::privileges::PrivilegeDropper; + +use common::{State, Statistics}; +use config::Config; +use workers::socket::ConnectionValidator; + +pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker"; +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn run(mut config: Config) -> ::anyhow::Result<()> { + let mut signals = Signals::new([SIGUSR1])?; + + if !(config.network.use_ipv4 || config.network.use_ipv6) { + return Result::Err(anyhow::anyhow!( + "Both use_ipv4 and use_ipv6 can not be set to false" + )); + } + + if config.socket_workers == 0 { + config.socket_workers = available_parallelism().map(Into::into).unwrap_or(1); + }; + + let num_sockets_per_worker = + if config.network.use_ipv4 { 1 } else { 0 } + if config.network.use_ipv6 { 1 } else { 0 }; + + let state = State::default(); + let statistics = Statistics::new(&config); + let connection_validator = ConnectionValidator::new(&config)?; + let priv_dropper = PrivilegeDropper::new( + config.privileges.clone(), + config.socket_workers * num_sockets_per_worker, + ); + let (statistics_sender, statistics_receiver) = unbounded(); + + update_access_list(&config.access_list, &state.access_list)?; + + let mut join_handles = Vec::new(); + + // Spawn socket worker threads + for i in 0..config.socket_workers { + let state = state.clone(); + let config = config.clone(); + let connection_validator = connection_validator.clone(); + let statistics = statistics.socket[i].clone(); + let statistics_sender = statistics_sender.clone(); + + let mut priv_droppers = Vec::new(); + + for _ in 0..num_sockets_per_worker { + priv_droppers.push(priv_dropper.clone()); + } + + let handle = Builder::new() + .name(format!("socket-{:02}", i + 1)) + .spawn(move || { + workers::socket::run_socket_worker( + config, + state, + statistics, + statistics_sender, + connection_validator, + priv_droppers, + ) + }) + .with_context(|| "spawn socket worker")?; + + join_handles.push((WorkerType::Socket(i), handle)); + } + + // Spawn cleaning thread + { + let state = state.clone(); + let config = config.clone(); + let statistics = statistics.swarm.clone(); + let statistics_sender = statistics_sender.clone(); + + let handle = Builder::new().name("cleaning".into()).spawn(move || loop { + sleep(Duration::from_secs( + config.cleaning.torrent_cleaning_interval, + )); + + state.torrent_maps.clean_and_update_statistics( + &config, + &statistics, + &statistics_sender, + &state.access_list, + state.server_start_instant, + ); + })?; + + join_handles.push((WorkerType::Cleaning, handle)); + } + + // Spawn statistics thread + if config.statistics.active() { + let state = state.clone(); + let config = config.clone(); + + let handle = Builder::new() + .name("statistics".into()) + .spawn(move || { + workers::statistics::run_statistics_worker( + config, + state, + statistics, + statistics_receiver, + ) + }) + .with_context(|| "spawn statistics worker")?; + + join_handles.push((WorkerType::Statistics, handle)); + } + + // Spawn prometheus endpoint thread + #[cfg(feature = "prometheus")] + if config.statistics.active() && config.statistics.run_prometheus_endpoint { + let handle = aquatic_common::spawn_prometheus_endpoint( + config.statistics.prometheus_endpoint_address, + Some(Duration::from_secs( + config.cleaning.torrent_cleaning_interval * 2, + )), + None, + )?; + + join_handles.push((WorkerType::Prometheus, handle)); + } + + // Spawn signal handler thread + { + let config = config.clone(); + + let handle: JoinHandle> = Builder::new() + .name("signals".into()) + .spawn(move || { + for signal in &mut signals { + match signal { + SIGUSR1 => { + let _ = update_access_list(&config.access_list, &state.access_list); + } + _ => unreachable!(), + } + } + + Ok(()) + }) + .context("spawn signal worker")?; + + join_handles.push((WorkerType::Signals, handle)); + } + + // Quit application if any worker returns or panics + loop { + for (i, (_, handle)) in join_handles.iter().enumerate() { + if handle.is_finished() { + let (worker_type, handle) = join_handles.remove(i); + + match handle.join() { + Ok(Ok(())) => { + return Err(anyhow::anyhow!("{} stopped", worker_type)); + } + Ok(Err(err)) => { + return Err(err.context(format!("{} stopped", worker_type))); + } + Err(_) => { + return Err(anyhow::anyhow!("{} panicked", worker_type)); + } + } + } + } + + sleep(Duration::from_secs(5)); + } +} diff --git a/apps/aquatic/crates/udp/src/main.rs b/apps/aquatic/crates/udp/src/main.rs new file mode 100644 index 0000000..7dab664 --- /dev/null +++ b/apps/aquatic/crates/udp/src/main.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() { + aquatic_common::cli::run_app_with_cli_and_config::( + aquatic_udp::APP_NAME, + aquatic_udp::APP_VERSION, + aquatic_udp::run, + None, + ) +} diff --git a/apps/aquatic/crates/udp/src/swarm.rs b/apps/aquatic/crates/udp/src/swarm.rs new file mode 100644 index 0000000..2d422f9 --- /dev/null +++ b/apps/aquatic/crates/udp/src/swarm.rs @@ -0,0 +1,706 @@ +use std::iter::repeat_with; +use std::net::IpAddr; +use std::ops::DerefMut; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use aquatic_common::SecondsSinceServerStart; +use aquatic_common::ServerStartInstant; +use aquatic_common::{ + access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, + ValidUntil, +}; +use aquatic_common::{CanonicalSocketAddr, IndexMap}; + +use aquatic_udp_protocol::*; +use arrayvec::ArrayVec; +use crossbeam_channel::Sender; +use hashbrown::HashMap; +use hdrhistogram::Histogram; +use parking_lot::RwLockUpgradableReadGuard; +use rand::prelude::SmallRng; +use rand::Rng; + +use crate::common::*; +use crate::config::Config; + +const SMALL_PEER_MAP_CAPACITY: usize = 2; + +use aquatic_udp_protocol::InfoHash; +use parking_lot::RwLock; + +#[derive(Clone)] +pub struct TorrentMaps { + ipv4: TorrentMapShards, + ipv6: TorrentMapShards, +} + +impl Default for TorrentMaps { + fn default() -> Self { + const NUM_SHARDS: usize = 16; + + Self { + ipv4: TorrentMapShards::new(NUM_SHARDS), + ipv6: TorrentMapShards::new(NUM_SHARDS), + } + } +} + +impl TorrentMaps { + pub fn announce( + &self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + src: CanonicalSocketAddr, + valid_until: ValidUntil, + ) -> Response { + match src.get().ip() { + IpAddr::V4(ip_address) => Response::AnnounceIpv4(self.ipv4.announce( + config, + statistics_sender, + rng, + request, + ip_address.into(), + valid_until, + )), + IpAddr::V6(ip_address) => Response::AnnounceIpv6(self.ipv6.announce( + config, + statistics_sender, + rng, + request, + ip_address.into(), + valid_until, + )), + } + } + + pub fn scrape(&self, request: ScrapeRequest, src: CanonicalSocketAddr) -> ScrapeResponse { + if src.is_ipv4() { + self.ipv4.scrape(request) + } else { + self.ipv6.scrape(request) + } + } + + /// Remove forbidden or inactive torrents, reclaim space and update statistics + pub fn clean_and_update_statistics( + &self, + config: &Config, + statistics: &CachePaddedArc>, + statistics_sender: &Sender, + access_list: &Arc, + server_start_instant: ServerStartInstant, + ) { + let mut cache = create_access_list_cache(access_list); + let mode = config.access_list.mode; + let now = server_start_instant.seconds_elapsed(); + + let mut statistics_messages = Vec::new(); + + let ipv4 = self.ipv4.clean_and_get_statistics( + config, + &mut statistics_messages, + &mut cache, + mode, + now, + ); + let ipv6 = self.ipv6.clean_and_get_statistics( + config, + &mut statistics_messages, + &mut cache, + mode, + now, + ); + + if config.statistics.active() { + statistics.ipv4.torrents.store(ipv4.0, Ordering::Relaxed); + statistics.ipv6.torrents.store(ipv6.0, Ordering::Relaxed); + statistics.ipv4.peers.store(ipv4.1, Ordering::Relaxed); + statistics.ipv6.peers.store(ipv6.1, Ordering::Relaxed); + + if let Some(message) = ipv4.2 { + statistics_messages.push(StatisticsMessage::Ipv4PeerHistogram(message)); + } + if let Some(message) = ipv6.2 { + statistics_messages.push(StatisticsMessage::Ipv6PeerHistogram(message)); + } + + for message in statistics_messages { + if let Err(err) = statistics_sender.try_send(message) { + ::log::error!("couldn't send statistics message: {:#}", err); + } + } + } + } +} + +#[derive(Clone)] +pub struct TorrentMapShards(Arc<[RwLock>]>); + +impl TorrentMapShards { + fn new(num_shards: usize) -> Self { + Self( + repeat_with(Default::default) + .take(num_shards) + .collect::>() + .into_boxed_slice() + .into(), + ) + } + + fn announce( + &self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + ip_address: I, + valid_until: ValidUntil, + ) -> AnnounceResponse { + let torrent_data = { + let torrent_map_shard = self.get_shard(&request.info_hash).upgradable_read(); + + // Clone Arc here to avoid keeping lock on whole shard + if let Some(torrent_data) = torrent_map_shard.get(&request.info_hash) { + torrent_data.clone() + } else { + // Don't overwrite entry if created in the meantime + RwLockUpgradableReadGuard::upgrade(torrent_map_shard) + .entry(request.info_hash) + .or_default() + .clone() + } + }; + + let mut peer_map = torrent_data.peer_map.write(); + + peer_map.announce( + config, + statistics_sender, + rng, + request, + ip_address, + valid_until, + ) + } + + fn scrape(&self, request: ScrapeRequest) -> ScrapeResponse { + let mut response = ScrapeResponse { + transaction_id: request.transaction_id, + torrent_stats: Vec::with_capacity(request.info_hashes.len()), + }; + + for info_hash in request.info_hashes { + let torrent_map_shard = self.get_shard(&info_hash); + + let statistics = if let Some(torrent_data) = torrent_map_shard.read().get(&info_hash) { + torrent_data.peer_map.read().scrape_statistics() + } else { + TorrentScrapeStatistics { + seeders: NumberOfPeers::new(0), + leechers: NumberOfPeers::new(0), + completed: NumberOfDownloads::new(0), + } + }; + + response.torrent_stats.push(statistics); + } + + response + } + + fn clean_and_get_statistics( + &self, + config: &Config, + statistics_messages: &mut Vec, + access_list_cache: &mut AccessListCache, + access_list_mode: AccessListMode, + now: SecondsSinceServerStart, + ) -> (usize, usize, Option>) { + let mut total_num_torrents = 0; + let mut total_num_peers = 0; + + let mut opt_histogram: Option> = config + .statistics + .torrent_peer_histograms + .then(|| Histogram::new(3).expect("create peer histogram")); + + for torrent_map_shard in self.0.iter() { + for torrent_data in torrent_map_shard.read().values() { + let mut peer_map = torrent_data.peer_map.write(); + + let num_peers = match peer_map.deref_mut() { + PeerMap::Small(small_peer_map) => { + small_peer_map.clean_and_get_num_peers(config, statistics_messages, now) + } + PeerMap::Large(large_peer_map) => { + let num_peers = large_peer_map.clean_and_get_num_peers( + config, + statistics_messages, + now, + ); + + if let Some(small_peer_map) = large_peer_map.try_shrink() { + *peer_map = PeerMap::Small(small_peer_map); + } + + num_peers + } + }; + + drop(peer_map); + + match opt_histogram.as_mut() { + Some(histogram) if num_peers > 0 => { + if let Err(err) = histogram.record(num_peers as u64) { + ::log::error!("Couldn't record {} to histogram: {:#}", num_peers, err); + } + } + _ => (), + } + + total_num_peers += num_peers; + + torrent_data + .pending_removal + .store(num_peers == 0, Ordering::Release); + } + + let mut torrent_map_shard = torrent_map_shard.write(); + + torrent_map_shard.retain(|info_hash, torrent_data| { + if !access_list_cache + .load() + .allows(access_list_mode, &info_hash.0) + { + return false; + } + + // Check pending_removal flag set in previous cleaning step. This + // prevents us from removing TorrentData entries that were just + // added but do not yet contain any peers. Also double-check that + // no peers have been added since we last checked. + if torrent_data + .pending_removal + .fetch_and(false, Ordering::Acquire) + && torrent_data.peer_map.read().is_empty() + { + return false; + } + + true + }); + + torrent_map_shard.shrink_to_fit(); + + total_num_torrents += torrent_map_shard.len(); + } + + (total_num_torrents, total_num_peers, opt_histogram) + } + + fn get_shard(&self, info_hash: &InfoHash) -> &RwLock> { + self.0.get(info_hash.0[0] as usize % self.0.len()).unwrap() + } +} + +/// Use HashMap instead of IndexMap for better lookup performance +type TorrentMapShard = HashMap>>; + +pub struct TorrentData { + peer_map: RwLock>, + pending_removal: AtomicBool, +} + +impl Default for TorrentData { + fn default() -> Self { + Self { + peer_map: Default::default(), + pending_removal: Default::default(), + } + } +} + +pub enum PeerMap { + Small(SmallPeerMap), + Large(LargePeerMap), +} + +impl PeerMap { + fn announce( + &mut self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + ip_address: I, + valid_until: ValidUntil, + ) -> AnnounceResponse { + let max_num_peers_to_take: usize = if request.peers_wanted.0.get() <= 0 { + config.protocol.max_response_peers + } else { + ::std::cmp::min( + config.protocol.max_response_peers, + request.peers_wanted.0.get().try_into().unwrap(), + ) + }; + + let status = + PeerStatus::from_event_and_bytes_left(request.event.into(), request.bytes_left); + + let peer_map_key = ResponsePeer { + ip_address, + port: request.port, + }; + + // Create the response before inserting the peer. This means that we + // don't have to filter it out from the response peers, and that the + // reported number of seeders/leechers will not include it + let (response, opt_removed_peer) = match self { + Self::Small(peer_map) => { + let opt_removed_peer = peer_map.remove(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + + let response = AnnounceResponse { + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval::new( + config.protocol.peer_announce_interval, + ), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + }, + peers: peer_map.extract_response_peers(max_num_peers_to_take), + }; + + // Convert peer map to large variant if it is full and + // announcing peer is not stopped and will therefore be + // inserted + if peer_map.is_full() && status != PeerStatus::Stopped { + *self = Self::Large(peer_map.to_large()); + } + + (response, opt_removed_peer) + } + Self::Large(peer_map) => { + let opt_removed_peer = peer_map.remove_peer(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + + let response = AnnounceResponse { + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval::new( + config.protocol.peer_announce_interval, + ), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + }, + peers: peer_map.extract_response_peers(rng, max_num_peers_to_take), + }; + + // Try shrinking the map if announcing peer is stopped and + // will therefore not be inserted + if status == PeerStatus::Stopped { + if let Some(peer_map) = peer_map.try_shrink() { + *self = Self::Small(peer_map); + } + } + + (response, opt_removed_peer) + } + }; + + match status { + PeerStatus::Leeching | PeerStatus::Seeding => { + let peer = Peer { + peer_id: request.peer_id, + is_seeder: status == PeerStatus::Seeding, + valid_until, + }; + + match self { + Self::Small(peer_map) => peer_map.insert(peer_map_key, peer), + Self::Large(peer_map) => peer_map.insert(peer_map_key, peer), + } + + if config.statistics.peer_clients && opt_removed_peer.is_none() { + statistics_sender + .try_send(StatisticsMessage::PeerAdded(request.peer_id)) + .expect("statistics channel should be unbounded"); + } + } + PeerStatus::Stopped => { + if config.statistics.peer_clients && opt_removed_peer.is_some() { + statistics_sender + .try_send(StatisticsMessage::PeerRemoved(request.peer_id)) + .expect("statistics channel should be unbounded"); + } + } + }; + + response + } + + fn scrape_statistics(&self) -> TorrentScrapeStatistics { + let (seeders, leechers) = match self { + Self::Small(peer_map) => peer_map.num_seeders_leechers(), + Self::Large(peer_map) => peer_map.num_seeders_leechers(), + }; + + TorrentScrapeStatistics { + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + completed: NumberOfDownloads::new(0), + } + } + + fn is_empty(&self) -> bool { + match self { + Self::Small(peer_map) => peer_map.0.is_empty(), + Self::Large(peer_map) => peer_map.peers.is_empty(), + } + } +} + +impl Default for PeerMap { + fn default() -> Self { + Self::Small(SmallPeerMap(ArrayVec::default())) + } +} + +/// Store torrents with up to two peers without an extra heap allocation +/// +/// On public open trackers, this is likely to be the majority of torrents. +#[derive(Default, Debug)] +pub struct SmallPeerMap(ArrayVec<(ResponsePeer, Peer), SMALL_PEER_MAP_CAPACITY>); + +impl SmallPeerMap { + fn is_full(&self) -> bool { + self.0.is_full() + } + + fn num_seeders_leechers(&self) -> (usize, usize) { + let seeders = self.0.iter().filter(|(_, p)| p.is_seeder).count(); + let leechers = self.0.len() - seeders; + + (seeders, leechers) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + self.0.push((key, peer)); + } + + fn remove(&mut self, key: &ResponsePeer) -> Option { + for (i, (k, _)) in self.0.iter().enumerate() { + if k == key { + return Some(self.0.remove(i).1); + } + } + + None + } + + fn extract_response_peers(&self, max_num_peers_to_take: usize) -> Vec> { + Vec::from_iter(self.0.iter().take(max_num_peers_to_take).map(|(k, _)| *k)) + } + + fn clean_and_get_num_peers( + &mut self, + config: &Config, + statistics_messages: &mut Vec, + now: SecondsSinceServerStart, + ) -> usize { + self.0.retain(|(_, peer)| { + let keep = peer.valid_until.valid(now); + + if !keep && config.statistics.peer_clients { + statistics_messages.push(StatisticsMessage::PeerRemoved(peer.peer_id)); + } + + keep + }); + + self.0.len() + } + + fn to_large(&self) -> LargePeerMap { + let (num_seeders, _) = self.num_seeders_leechers(); + let peers = self.0.iter().copied().collect(); + + LargePeerMap { peers, num_seeders } + } +} + +#[derive(Default)] +pub struct LargePeerMap { + peers: IndexMap, Peer>, + num_seeders: usize, +} + +impl LargePeerMap { + fn num_seeders_leechers(&self) -> (usize, usize) { + (self.num_seeders, self.peers.len() - self.num_seeders) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + if peer.is_seeder { + self.num_seeders += 1; + } + + self.peers.insert(key, peer); + } + + fn remove_peer(&mut self, key: &ResponsePeer) -> Option { + let opt_removed_peer = self.peers.swap_remove(key); + + if let Some(Peer { + is_seeder: true, .. + }) = opt_removed_peer + { + self.num_seeders -= 1; + } + + opt_removed_peer + } + + /// Extract response peers + /// + /// If there are more peers in map than `max_num_peers_to_take`, do a + /// random selection of peers from first and second halves of map in + /// order to avoid returning too homogeneous peers. This is a lot more + /// cache-friendly than doing a fully random selection. + fn extract_response_peers( + &self, + rng: &mut impl Rng, + max_num_peers_to_take: usize, + ) -> Vec> { + if self.peers.len() <= max_num_peers_to_take { + self.peers.keys().copied().collect() + } else { + let middle_index = self.peers.len() / 2; + let num_to_take_per_half = max_num_peers_to_take / 2; + + let offset_half_one = { + let from = 0; + let to = usize::max(1, middle_index - num_to_take_per_half); + + rng.gen_range(from..to) + }; + let offset_half_two = { + let from = middle_index; + let to = usize::max(middle_index + 1, self.peers.len() - num_to_take_per_half); + + rng.gen_range(from..to) + }; + + let end_half_one = offset_half_one + num_to_take_per_half; + let end_half_two = offset_half_two + num_to_take_per_half; + + let mut peers = Vec::with_capacity(max_num_peers_to_take); + + if let Some(slice) = self.peers.get_range(offset_half_one..end_half_one) { + peers.extend(slice.keys().copied()); + } + if let Some(slice) = self.peers.get_range(offset_half_two..end_half_two) { + peers.extend(slice.keys().copied()); + } + + peers + } + } + + fn clean_and_get_num_peers( + &mut self, + config: &Config, + statistics_messages: &mut Vec, + now: SecondsSinceServerStart, + ) -> usize { + self.peers.retain(|_, peer| { + let keep = peer.valid_until.valid(now); + + if !keep { + if peer.is_seeder { + self.num_seeders -= 1; + } + if config.statistics.peer_clients { + statistics_messages.push(StatisticsMessage::PeerRemoved(peer.peer_id)); + } + } + + keep + }); + + if !self.peers.is_empty() { + self.peers.shrink_to_fit(); + } + + self.peers.len() + } + + fn try_shrink(&mut self) -> Option> { + (self.peers.len() <= SMALL_PEER_MAP_CAPACITY).then(|| { + SmallPeerMap(ArrayVec::from_iter( + self.peers.iter().map(|(k, v)| (*k, *v)), + )) + }) + } +} + +#[derive(Clone, Copy, Debug)] +struct Peer { + peer_id: PeerId, + is_seeder: bool, + valid_until: ValidUntil, +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum PeerStatus { + Seeding, + Leeching, + Stopped, +} + +impl PeerStatus { + /// Determine peer status from announce event and number of bytes left. + /// + /// Likely, the last branch will be taken most of the time. + #[inline] + pub fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: NumberOfBytes) -> Self { + if event == AnnounceEvent::Stopped { + Self::Stopped + } else if bytes_left.0.get() == 0 { + Self::Seeding + } else { + Self::Leeching + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_peer_status_from_event_and_bytes_left() { + use PeerStatus::*; + + let f = PeerStatus::from_event_and_bytes_left; + + assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(0))); + assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::Started, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::Started, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::Completed, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::Completed, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::None, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::None, NumberOfBytes::new(1))); + } +} diff --git a/apps/aquatic/crates/udp/src/workers/mod.rs b/apps/aquatic/crates/udp/src/workers/mod.rs new file mode 100644 index 0000000..02af829 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/mod.rs @@ -0,0 +1,2 @@ +pub mod socket; +pub mod statistics; diff --git a/apps/aquatic/crates/udp/src/workers/socket/mio/mod.rs b/apps/aquatic/crates/udp/src/workers/socket/mio/mod.rs new file mode 100644 index 0000000..3b0ebc4 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/mio/mod.rs @@ -0,0 +1,194 @@ +mod socket; + +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::access_list::AccessListCache; +use crossbeam_channel::Sender; +use mio::{Events, Interest, Poll, Token}; + +use aquatic_common::{ + access_list::create_access_list_cache, privileges::PrivilegeDropper, CanonicalSocketAddr, + ValidUntil, +}; +use aquatic_udp_protocol::*; +use rand::rngs::SmallRng; +use rand::SeedableRng; + +use crate::common::*; +use crate::config::Config; + +use socket::Socket; + +use super::validator::ConnectionValidator; +use super::{EXTRA_PACKET_SIZE_IPV4, EXTRA_PACKET_SIZE_IPV6}; + +const TOKEN_V4: Token = Token(0); +const TOKEN_V6: Token = Token(1); + +pub fn run( + config: Config, + shared_state: State, + statistics: CachePaddedArc>, + statistics_sender: Sender, + validator: ConnectionValidator, + mut priv_droppers: Vec, +) -> anyhow::Result<()> { + let mut opt_socket_ipv4 = if config.network.use_ipv4 { + let priv_dropper = priv_droppers.pop().expect("not enough privilege droppers"); + + Some(Socket::::create(&config, priv_dropper)?) + } else { + None + }; + let mut opt_socket_ipv6 = if config.network.use_ipv6 { + let priv_dropper = priv_droppers.pop().expect("not enough privilege droppers"); + + Some(Socket::::create(&config, priv_dropper)?) + } else { + None + }; + + let access_list_cache = create_access_list_cache(&shared_state.access_list); + let peer_valid_until = ValidUntil::new( + shared_state.server_start_instant, + config.cleaning.max_peer_age, + ); + + let mut shared = WorkerSharedData { + config, + shared_state, + statistics, + statistics_sender, + validator, + access_list_cache, + buffer: [0; BUFFER_SIZE], + rng: SmallRng::from_entropy(), + peer_valid_until, + }; + + let mut events = Events::with_capacity(2); + let mut poll = Poll::new().context("create poll")?; + + if let Some(socket) = opt_socket_ipv4.as_mut() { + poll.registry() + .register(&mut socket.socket, TOKEN_V4, Interest::READABLE) + .context("register poll")?; + } + if let Some(socket) = opt_socket_ipv6.as_mut() { + poll.registry() + .register(&mut socket.socket, TOKEN_V6, Interest::READABLE) + .context("register poll")?; + } + + let poll_timeout = Duration::from_millis(shared.config.network.poll_timeout_ms); + + let mut iter_counter = 0u64; + + loop { + poll.poll(&mut events, Some(poll_timeout)).context("poll")?; + + for event in events.iter() { + if event.is_readable() { + match event.token() { + TOKEN_V4 => { + if let Some(socket) = opt_socket_ipv4.as_mut() { + socket.read_and_handle_requests(&mut shared); + } + } + TOKEN_V6 => { + if let Some(socket) = opt_socket_ipv6.as_mut() { + socket.read_and_handle_requests(&mut shared); + } + } + _ => (), + } + } + } + + if let Some(socket) = opt_socket_ipv4.as_mut() { + socket.resend_failed(&mut shared); + } + if let Some(socket) = opt_socket_ipv6.as_mut() { + socket.resend_failed(&mut shared); + } + + if iter_counter % 256 == 0 { + shared.validator.update_elapsed(); + + shared.peer_valid_until = ValidUntil::new( + shared.shared_state.server_start_instant, + shared.config.cleaning.max_peer_age, + ); + } + + iter_counter = iter_counter.wrapping_add(1); + } +} + +pub struct WorkerSharedData { + config: Config, + shared_state: State, + statistics: CachePaddedArc>, + statistics_sender: Sender, + access_list_cache: AccessListCache, + validator: ConnectionValidator, + buffer: [u8; BUFFER_SIZE], + rng: SmallRng, + peer_valid_until: ValidUntil, +} + +impl WorkerSharedData { + fn handle_request(&mut self, request: Request, src: CanonicalSocketAddr) -> Option { + let access_list_mode = self.config.access_list.mode; + + match request { + Request::Connect(request) => { + return Some(Response::Connect(ConnectResponse { + connection_id: self.validator.create_connection_id(src), + transaction_id: request.transaction_id, + })); + } + Request::Announce(request) => { + if self + .validator + .connection_id_valid(src, request.connection_id) + { + if self + .access_list_cache + .load() + .allows(access_list_mode, &request.info_hash.0) + { + let response = self.shared_state.torrent_maps.announce( + &self.config, + &self.statistics_sender, + &mut self.rng, + &request, + src, + self.peer_valid_until, + ); + + return Some(response); + } else { + return Some(Response::Error(ErrorResponse { + transaction_id: request.transaction_id, + message: "Info hash not allowed".into(), + })); + } + } + } + Request::Scrape(request) => { + if self + .validator + .connection_id_valid(src, request.connection_id) + { + return Some(Response::Scrape( + self.shared_state.torrent_maps.scrape(request, src), + )); + } + } + } + + None + } +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/mio/socket.rs b/apps/aquatic/crates/udp/src/workers/socket/mio/socket.rs new file mode 100644 index 0000000..e315ebc --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/mio/socket.rs @@ -0,0 +1,323 @@ +use std::io::{Cursor, ErrorKind}; +use std::marker::PhantomData; +use std::sync::atomic::Ordering; + +use anyhow::Context; +use mio::net::UdpSocket; +use socket2::{Domain, Protocol, Type}; + +use aquatic_common::{privileges::PrivilegeDropper, CanonicalSocketAddr}; +use aquatic_udp_protocol::*; + +use crate::config::Config; + +use super::{WorkerSharedData, EXTRA_PACKET_SIZE_IPV4, EXTRA_PACKET_SIZE_IPV6}; + +pub trait IpVersion { + fn is_v4() -> bool; +} + +#[derive(Clone, Copy, Debug)] +pub struct Ipv4; + +impl IpVersion for Ipv4 { + fn is_v4() -> bool { + true + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Ipv6; + +impl IpVersion for Ipv6 { + fn is_v4() -> bool { + false + } +} + +pub struct Socket { + pub socket: UdpSocket, + opt_resend_buffer: Option>, + phantom_data: PhantomData, +} + +impl Socket { + pub fn create(config: &Config, priv_dropper: PrivilegeDropper) -> anyhow::Result { + let socket = socket2::Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; + + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + socket + .set_nonblocking(true) + .with_context(|| "socket: set nonblocking")?; + + let recv_buffer_size = config.network.socket_recv_buffer_size; + + if recv_buffer_size != 0 { + if let Err(err) = socket.set_recv_buffer_size(recv_buffer_size) { + ::log::error!( + "socket: failed setting recv buffer to {}: {:?}", + recv_buffer_size, + err + ); + } + } + + socket + .bind(&config.network.address_ipv4.into()) + .with_context(|| format!("socket: bind to {}", config.network.address_ipv4))?; + + priv_dropper.after_socket_creation()?; + + let mut s = Self { + socket: UdpSocket::from_std(::std::net::UdpSocket::from(socket)), + opt_resend_buffer: None, + phantom_data: Default::default(), + }; + + if config.network.resend_buffer_max_len > 0 { + s.opt_resend_buffer = Some(Vec::new()); + } + + Ok(s) + } +} + +impl Socket { + pub fn create(config: &Config, priv_dropper: PrivilegeDropper) -> anyhow::Result { + let socket = socket2::Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?; + + if config.network.set_only_ipv6 { + socket + .set_only_v6(true) + .with_context(|| "socket: set only ipv6")?; + } + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + socket + .set_nonblocking(true) + .with_context(|| "socket: set nonblocking")?; + + let recv_buffer_size = config.network.socket_recv_buffer_size; + + if recv_buffer_size != 0 { + if let Err(err) = socket.set_recv_buffer_size(recv_buffer_size) { + ::log::error!( + "socket: failed setting recv buffer to {}: {:?}", + recv_buffer_size, + err + ); + } + } + + socket + .bind(&config.network.address_ipv6.into()) + .with_context(|| format!("socket: bind to {}", config.network.address_ipv6))?; + + priv_dropper.after_socket_creation()?; + + let mut s = Self { + socket: UdpSocket::from_std(::std::net::UdpSocket::from(socket)), + opt_resend_buffer: None, + phantom_data: Default::default(), + }; + + if config.network.resend_buffer_max_len > 0 { + s.opt_resend_buffer = Some(Vec::new()); + } + + Ok(s) + } +} + +impl Socket { + pub fn read_and_handle_requests(&mut self, shared: &mut WorkerSharedData) { + let max_scrape_torrents = shared.config.protocol.max_scrape_torrents; + + loop { + match self.socket.recv_from(&mut shared.buffer[..]) { + Ok((bytes_read, src)) => { + let src_port = src.port(); + let src = CanonicalSocketAddr::new(src); + + // Use canonical address for statistics + let opt_statistics = if shared.config.statistics.active() { + if src.is_ipv4() { + let statistics = &shared.statistics.ipv4; + + statistics + .bytes_received + .fetch_add(bytes_read + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); + + Some(statistics) + } else { + let statistics = &shared.statistics.ipv6; + + statistics + .bytes_received + .fetch_add(bytes_read + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); + + Some(statistics) + } + } else { + None + }; + + if src_port == 0 { + ::log::debug!("Ignored request because source port is zero"); + + continue; + } + + match Request::parse_bytes(&shared.buffer[..bytes_read], max_scrape_torrents) { + Ok(request) => { + if let Some(statistics) = opt_statistics { + statistics.requests.fetch_add(1, Ordering::Relaxed); + } + + if let Some(response) = shared.handle_request(request, src) { + self.send_response(shared, src, response, false); + } + } + Err(RequestParseError::Sendable { + connection_id, + transaction_id, + err, + }) if shared.validator.connection_id_valid(src, connection_id) => { + let response = ErrorResponse { + transaction_id, + message: err.into(), + }; + + self.send_response(shared, src, Response::Error(response), false); + + ::log::debug!("request parse error (sent error response): {:?}", err); + } + Err(err) => { + ::log::debug!( + "request parse error (didn't send error response): {:?}", + err + ); + } + }; + } + Err(err) if err.kind() == ErrorKind::WouldBlock => { + break; + } + Err(err) => { + ::log::warn!("recv_from error: {:#}", err); + } + } + } + } + pub fn send_response( + &mut self, + shared: &mut WorkerSharedData, + canonical_addr: CanonicalSocketAddr, + response: Response, + disable_resend_buffer: bool, + ) { + let mut buffer = Cursor::new(&mut shared.buffer[..]); + + if let Err(err) = response.write_bytes(&mut buffer) { + ::log::error!("failed writing response to buffer: {:#}", err); + + return; + } + + let bytes_written = buffer.position() as usize; + + let addr = if V::is_v4() { + canonical_addr + .get_ipv4() + .expect("found peer ipv6 address while running bound to ipv4 address") + } else { + canonical_addr.get_ipv6_mapped() + }; + + match self + .socket + .send_to(&buffer.into_inner()[..bytes_written], addr) + { + Ok(bytes_sent) if shared.config.statistics.active() => { + let stats = if canonical_addr.is_ipv4() { + let stats = &shared.statistics.ipv4; + + stats + .bytes_sent + .fetch_add(bytes_sent + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); + + stats + } else { + let stats = &shared.statistics.ipv6; + + stats + .bytes_sent + .fetch_add(bytes_sent + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); + + stats + }; + + match response { + Response::Connect(_) => { + stats.responses_connect.fetch_add(1, Ordering::Relaxed); + } + Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => { + stats.responses_announce.fetch_add(1, Ordering::Relaxed); + } + Response::Scrape(_) => { + stats.responses_scrape.fetch_add(1, Ordering::Relaxed); + } + Response::Error(_) => { + stats.responses_error.fetch_add(1, Ordering::Relaxed); + } + } + } + Ok(_) => (), + Err(err) => match self.opt_resend_buffer.as_mut() { + Some(resend_buffer) + if !disable_resend_buffer + && ((err.raw_os_error() == Some(libc::ENOBUFS)) + || (err.kind() == ErrorKind::WouldBlock)) => + { + if resend_buffer.len() < shared.config.network.resend_buffer_max_len { + ::log::debug!("Adding response to resend queue, since sending it to {} failed with: {:#}", addr, err); + + resend_buffer.push((canonical_addr, response)); + } else { + ::log::warn!("Response resend buffer full, dropping response"); + } + } + _ => { + ::log::warn!("Sending response to {} failed: {:#}", addr, err); + } + }, + } + + ::log::debug!("send response fn finished"); + } + + /// If resend buffer is enabled, send any responses in it + pub fn resend_failed(&mut self, shared: &mut WorkerSharedData) { + if self.opt_resend_buffer.is_some() { + let mut tmp_resend_buffer = Vec::new(); + + // Do memory swap shenanigans to get around false positive in + // borrow checker regarding double mut borrowing of self + + if let Some(resend_buffer) = self.opt_resend_buffer.as_mut() { + ::std::mem::swap(resend_buffer, &mut tmp_resend_buffer); + } + + for (addr, response) in tmp_resend_buffer.drain(..) { + self.send_response(shared, addr, response, true); + } + + if let Some(resend_buffer) = self.opt_resend_buffer.as_mut() { + ::std::mem::swap(resend_buffer, &mut tmp_resend_buffer); + } + } + } +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/mod.rs b/apps/aquatic/crates/udp/src/workers/socket/mod.rs new file mode 100644 index 0000000..282530a --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/mod.rs @@ -0,0 +1,71 @@ +mod mio; +#[cfg(all(target_os = "linux", feature = "io-uring"))] +mod uring; +mod validator; + +use aquatic_common::privileges::PrivilegeDropper; +use crossbeam_channel::Sender; + +use crate::{ + common::{ + CachePaddedArc, IpVersionStatistics, SocketWorkerStatistics, State, StatisticsMessage, + }, + config::Config, +}; + +pub use self::validator::ConnectionValidator; + +#[cfg(all(not(target_os = "linux"), feature = "io-uring"))] +compile_error!("io_uring feature is only supported on Linux"); + +/// Bytes of data transmitted when sending an IPv4 UDP packet, in addition to payload size +/// +/// Consists of: +/// - 8 bit ethernet frame +/// - 14 + 4 bit MAC header and checksum +/// - 20 bit IPv4 header +/// - 8 bit udp header +const EXTRA_PACKET_SIZE_IPV4: usize = 8 + 18 + 20 + 8; + +/// Bytes of data transmitted when sending an IPv4 UDP packet, in addition to payload size +/// +/// Consists of: +/// - 8 bit ethernet frame +/// - 14 + 4 bit MAC header and checksum +/// - 40 bit IPv6 header +/// - 8 bit udp header +const EXTRA_PACKET_SIZE_IPV6: usize = 8 + 18 + 40 + 8; + +pub fn run_socket_worker( + config: Config, + shared_state: State, + statistics: CachePaddedArc>, + statistics_sender: Sender, + validator: ConnectionValidator, + priv_droppers: Vec, +) -> anyhow::Result<()> { + #[cfg(all(target_os = "linux", feature = "io-uring"))] + if config.network.use_io_uring { + use anyhow::Context; + + self::uring::supported_on_current_kernel().context("check for io_uring compatibility")?; + + return self::uring::SocketWorker::run( + config, + shared_state, + statistics, + statistics_sender, + validator, + priv_droppers, + ); + } + + self::mio::run( + config, + shared_state, + statistics, + statistics_sender, + validator, + priv_droppers, + ) +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/uring/buf_ring.rs b/apps/aquatic/crates/udp/src/workers/socket/uring/buf_ring.rs new file mode 100644 index 0000000..1365b84 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/uring/buf_ring.rs @@ -0,0 +1,947 @@ +// Copyright (c) 2021 Carl Lerche +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Copied (with slight modifications) from +// - https://github.com/FrankReh/tokio-uring/tree/9387c92c98138451f7d760432a04b0b95a406f22/src/buf/bufring +// - https://github.com/FrankReh/tokio-uring/blob/9387c92c98138451f7d760432a04b0b95a406f22/src/buf/bufgroup/mod.rs + +//! Module for the io_uring device's buf_ring feature. + +// Developer's note about io_uring return codes when a buf_ring is used: +// +// While a buf_ring pool is exhaused, new calls to read that are, or are not, ready to read will +// fail with the 105 error, "no buffers", while existing calls that were waiting to become ready to +// read will not fail. Only when the data becomes ready to read will they fail, if the buffer ring +// is still empty at that time. This makes sense when thinking about it from how the kernel +// implements the start of a read command; it can be confusing when first working with these +// commands from the userland perspective. + +// While the file! calls yield the clippy false positive. +#![allow(clippy::print_literal)] + +use io_uring::types; +use std::cell::Cell; +use std::io; +use std::rc::Rc; +use std::sync::atomic::{self, AtomicU16}; + +use super::CurrentRing; + +/// The buffer group ID. +/// +/// The creater of a buffer group is responsible for picking a buffer group id +/// that does not conflict with other buffer group ids also being registered with the uring +/// interface. +pub(crate) type Bgid = u16; + +// Future: Maybe create a bgid module with a trivial implementation of a type that tracks the next +// bgid to use. The crate's driver could do that perhaps, but there could be a benefit to tracking +// them across multiple thread's drivers. So there is flexibility in not building it into the +// driver. + +/// The buffer ID. Buffer ids are assigned and used by the crate and probably are not visible +/// to the crate user. +pub(crate) type Bid = u16; + +/// This tracks a buffer that has been filled in by the kernel, having gotten the memory +/// from a buffer ring, and returned to userland via a cqe entry. +pub struct BufX { + bgroup: BufRing, + bid: Bid, + len: usize, +} + +impl BufX { + // # Safety + // + // The bid must be the buffer id supplied by the kernel as having been chosen and written to. + // The length of the buffer must represent the length written to by the kernel. + pub(crate) unsafe fn new(bgroup: BufRing, bid: Bid, len: usize) -> Self { + // len will already have been checked against the buf_capacity + // so it is guaranteed that len <= bgroup.buf_capacity. + + Self { bgroup, bid, len } + } + + /// Return the number of bytes initialized. + /// + /// This value initially came from the kernel, as reported in the cqe. This value may have been + /// modified with a call to the IoBufMut::set_init method. + #[inline] + pub fn len(&self) -> usize { + self.len + } + + /// Return true if this represents an empty buffer. The length reported by the kernel was 0. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Return the capacity of this buffer. + #[inline] + pub fn cap(&self) -> usize { + self.bgroup.buf_capacity(self.bid) + } + + /// Return a byte slice reference. + #[inline] + pub fn as_slice(&self) -> &[u8] { + let p = self.bgroup.stable_ptr(self.bid); + // Safety: the pointer returned by stable_ptr is valid for the lifetime of self, + // and self's len is set when the kernel reports the amount of data that was + // written into the buffer. + unsafe { std::slice::from_raw_parts(p, self.len) } + } + + /// Return a mutable byte slice reference. + #[inline] + pub fn as_slice_mut(&mut self) -> &mut [u8] { + let p = self.bgroup.stable_mut_ptr(self.bid); + // Safety: the pointer returned by stable_mut_ptr is valid for the lifetime of self, + // and self's len is set when the kernel reports the amount of data that was + // written into the buffer. In addition, we hold a &mut reference to self. + unsafe { std::slice::from_raw_parts_mut(p, self.len) } + } + + // Future: provide access to the uninit space between len and cap if the buffer is being + // repurposed before being dropped. The set_init below does that too. +} + +impl Drop for BufX { + fn drop(&mut self) { + // Add the buffer back to the bgroup, for the kernel to reuse. + // Safety: this function may only be called by the buffer's drop function. + unsafe { self.bgroup.dropping_bid(self.bid) }; + } +} + +/* +unsafe impl crate::buf::IoBuf for BufX { + fn stable_ptr(&self) -> *const u8 { + self.bgroup.stable_ptr(self.bid) + } + + fn bytes_init(&self) -> usize { + self.len + } + + fn bytes_total(&self) -> usize { + self.cap() + } +} + +unsafe impl crate::buf::IoBufMut for BufX { + fn stable_mut_ptr(&mut self) -> *mut u8 { + self.bgroup.stable_mut_ptr(self.bid) + } + + unsafe fn set_init(&mut self, init_len: usize) { + if self.len < init_len { + let cap = self.bgroup.buf_capacity(self.bid); + assert!(init_len <= cap); + self.len = init_len; + } + } +} +*/ + +impl From for Vec { + fn from(item: BufX) -> Self { + item.as_slice().to_vec() + } +} + +/// A `BufRing` represents the ring and the buffers used with the kernel's io_uring buf_ring +/// feature. +/// +/// In this implementation, it is both the ring of buffer entries and the actual buffer +/// allocations. +/// +/// A BufRing is created through the [`Builder`] and can be registered automatically by the +/// builder's `build` step or at a later time by the user. Registration involves informing the +/// kernel of the ring's dimensions and its identifier (its buffer group id, which goes by the name +/// `bgid`). +/// +/// Multiple buf_rings, here multiple BufRings, can be created and registered. BufRings are +/// reference counted to ensure their memory is live while their BufX buffers are live. When a BufX +/// buffer is dropped, it releases itself back to the BufRing from which it came allowing it to be +/// reused by the kernel. +/// +/// It is perhaps worth pointing out that it is the ring itself that is registered with the kernel, +/// not the buffers per se. While a given buf_ring cannot have it size changed dynamically, the +/// buffers that are pushed to the ring by userland, and later potentially re-pushed in the ring, +/// can change. The buffers can be of different sizes and they could come from different allocation +/// blocks. This implementation does not provide that flexibility. Each BufRing comes with its own +/// equal length buffer allocation. And when a BufRing buffer, a BufX, is dropped, its id is pushed +/// back to the ring. +/// +/// This is the one and only `Provided Buffers` implementation in `tokio_uring` at the moment and +/// in this version, is a purely concrete type, with a concrete BufX type for buffers that are +/// returned by operations like `recv_provbuf` to the userland application. +/// +/// Aside from the register and unregister steps, there are no syscalls used to pass buffers to the +/// kernel. The ring contains a tail memory address that this userland type updates as buffers are +/// added to the ring and which the kernel reads when it needs to pull a buffer from the ring. The +/// kernel does not have a head pointer address that it updates for the userland. The userland +/// (this type), is expected to avoid overwriting the head of the circular ring by keeping track of +/// how many buffers were added to the ring and how many have been returned through the CQE +/// mechanism. This particular implementation does not track the count because all buffers are +/// allocated at the beginning, by the builder, and only its own buffers that came back via a CQE +/// are ever added back to the ring, so it should be impossible to overflow the ring. +#[derive(Clone, Debug)] +pub struct BufRing { + // RawBufRing uses cell for fields where necessary. + raw: Rc, +} + +// Methods the BufX needs. + +impl BufRing { + pub(crate) fn buf_capacity(&self, _: Bid) -> usize { + self.raw.buf_capacity_i() + } + + pub(crate) fn stable_ptr(&self, bid: Bid) -> *const u8 { + // Will panic if bid is out of range. + self.raw.stable_ptr_i(bid) + } + + pub(crate) fn stable_mut_ptr(&mut self, bid: Bid) -> *mut u8 { + // Safety: self is &mut, we're good. + unsafe { self.raw.stable_mut_ptr_i(bid) } + } + + // # Safety + // + // `dropping_bid` should only be called by the buffer's drop function because once called, the + // buffer may be given back to the kernel for reuse. + pub(crate) unsafe fn dropping_bid(&self, bid: Bid) { + self.raw.dropping_bid_i(bid); + } +} + +// Methods the io operations need. + +impl BufRing { + pub(crate) fn bgid(&self) -> Bgid { + self.raw.bgid() + } + + // # Safety + // + // The res and flags values are used to lookup a buffer and set its initialized length. + // The caller is responsible for these being correct. This is expected to be called + // when these two values are received from the kernel via a CQE and we rely on the kernel to + // give us correct information. + pub(crate) unsafe fn get_buf(&self, res: u32, flags: u32) -> io::Result> { + let bid = match io_uring::cqueue::buffer_select(flags) { + Some(bid) => bid, + None => { + // Have seen res == 0, flags == 4 with a TCP socket. res == 0 we take to mean the + // socket is empty so return None to show there is no buffer returned, which should + // be interpreted to mean there is no more data to read from this file or socket. + if res == 0 { + return Ok(None); + } + + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "BufRing::get_buf failed as the buffer bit, IORING_CQE_F_BUFFER, was missing from flags, res = {}, flags = {}", + res, flags) + )); + } + }; + + let len = res as usize; + + /* + let flags = flags & !io_uring::sys::IORING_CQE_F_BUFFER; // for tracing flags + println!( + "{}:{}: get_buf res({res})=len({len}) flags({:#x})->bid({bid})\n\n", + file!(), + line!(), + flags + ); + */ + + assert!(len <= self.raw.buf_len); + + // TODO maybe later + // #[cfg(any(debug, feature = "cautious"))] + // { + // let mut debug_bitmap = self.debug_bitmap.borrow_mut(); + // let m = 1 << (bid % 8); + // assert!(debug_bitmap[(bid / 8) as usize] & m == m); + // debug_bitmap[(bid / 8) as usize] &= !m; + // } + + self.raw.metric_getting_another(); + /* + println!( + "{}:{}: get_buf cur {}, min {}", + file!(), + line!(), + self.possible_cur.get(), + self.possible_min.get(), + ); + */ + + // Safety: the len provided to BufX::new is given to us from the kernel. + Ok(Some(unsafe { BufX::new(self.clone(), bid, len) })) + } +} + +#[derive(Debug, Copy, Clone)] +/// Build the arguments to call build() that returns a [`BufRing`]. +/// +/// Refer to the methods descriptions for details. +#[allow(dead_code)] +pub struct Builder { + page_size: usize, + bgid: Bgid, + ring_entries: u16, + buf_cnt: u16, + buf_len: usize, + buf_align: usize, + ring_pad: usize, + bufend_align: usize, + + skip_register: bool, +} + +#[allow(dead_code)] +impl Builder { + /// Create a new Builder with the given buffer group ID and defaults. + /// + /// The buffer group ID, `bgid`, is the id the kernel's io_uring device uses to identify the + /// provided buffer pool to use by operations that are posted to the device. + /// + /// The user is responsible for picking a bgid that does not conflict with other buffer groups + /// that have been registered with the same uring interface. + pub fn new(bgid: Bgid) -> Builder { + Builder { + page_size: 4096, + bgid, + ring_entries: 128, + buf_cnt: 0, + buf_len: 4096, + buf_align: 0, + ring_pad: 0, + bufend_align: 0, + skip_register: false, + } + } + + /// The page size of the kernel. Defaults to 4096. + /// + /// The io_uring device requires the BufRing is allocated on the start of a page, i.e. with a + /// page size alignment. + /// + /// The caller should determine the page size, and may want to cache the info if multiple buf + /// rings are to be created. Crates are available to get this information or the user may want + /// to call the libc sysconf directly: + /// + /// use libc::{_SC_PAGESIZE, sysconf}; + /// let page_size: usize = unsafe { sysconf(_SC_PAGESIZE) as usize }; + pub fn page_size(mut self, page_size: usize) -> Builder { + self.page_size = page_size; + self + } + + /// The number of ring entries to create for the buffer ring. + /// + /// This defaults to 128 or the `buf_cnt`, whichever is larger. + /// + /// The number will be made a power of 2, and will be the maximum of the ring_entries setting + /// and the buf_cnt setting. The interface will enforce a maximum of 2^15 (32768) so it can do + /// rollover calculation. + /// + /// Each ring entry is 16 bytes. + pub fn ring_entries(mut self, ring_entries: u16) -> Builder { + self.ring_entries = ring_entries; + self + } + + /// The number of buffers to allocate. If left zero, the ring_entries value will be used and + /// that value defaults to 128. + pub fn buf_cnt(mut self, buf_cnt: u16) -> Builder { + self.buf_cnt = buf_cnt; + self + } + + /// The length of each allocated buffer. Defaults to 4096. + /// + /// Non-alignment values are possible and `buf_align` can be used to allocate each buffer on + /// an alignment buffer, even if the buffer length is not desired to equal the alignment. + pub fn buf_len(mut self, buf_len: usize) -> Builder { + self.buf_len = buf_len; + self + } + + /// The alignment of the first buffer allocated. + /// + /// Generally not needed. + /// + /// The buffers are allocated right after the ring unless `ring_pad` is used and generally the + /// buffers are allocated contiguous to one another unless the `buf_len` is set to something + /// different. + pub fn buf_align(mut self, buf_align: usize) -> Builder { + self.buf_align = buf_align; + self + } + + /// Pad to place after ring to ensure separation between rings and first buffer. + /// + /// Generally not needed but may be useful if the ring's end and the buffers' start are to have + /// some separation, perhaps for cacheline reasons. + pub fn ring_pad(mut self, ring_pad: usize) -> Builder { + self.ring_pad = ring_pad; + self + } + + /// The alignment of the end of the buffer allocated. To keep other things out of a cache line + /// or out of a page, if that's desired. + pub fn bufend_align(mut self, bufend_align: usize) -> Builder { + self.bufend_align = bufend_align; + self + } + + /// Skip automatic registration. The caller can manually invoke the buf_ring.register() + /// function later. Regardless, the unregister() method will be called automatically when the + /// BufRing goes out of scope if the caller hadn't manually called buf_ring.unregister() + /// already. + pub fn skip_auto_register(mut self, skip: bool) -> Builder { + self.skip_register = skip; + self + } + + /// Return a BufRing, having computed the layout for the single aligned allocation + /// of both the buffer ring elements and the buffers themselves. + /// + /// If auto_register was left enabled, register the BufRing with the driver. + pub fn build(&self) -> io::Result { + let mut b: Builder = *self; + + // Two cases where both buf_cnt and ring_entries are set to the max of the two. + if b.buf_cnt == 0 || b.ring_entries < b.buf_cnt { + let max = std::cmp::max(b.ring_entries, b.buf_cnt); + b.buf_cnt = max; + b.ring_entries = max; + } + + // Don't allow the next_power_of_two calculation to be done if already larger than 2^15 + // because 2^16 reads back as 0 in a u16. And the interface doesn't allow for ring_entries + // larger than 2^15 anyway, so this is a good place to catch it. Here we return a unique + // error that is more descriptive than the InvalidArg that would come from the interface. + if b.ring_entries > (1 << 15) { + return Err(io::Error::new( + io::ErrorKind::Other, + "ring_entries exceeded 32768", + )); + } + + // Requirement of the interface is the ring entries is a power of two, making its and our + // mask calculation trivial. + b.ring_entries = b.ring_entries.next_power_of_two(); + + Ok(BufRing { + raw: Rc::new(RawBufRing::new(NewArgs { + page_size: b.page_size, + bgid: b.bgid, + ring_entries: b.ring_entries, + buf_cnt: b.buf_cnt, + buf_len: b.buf_len, + buf_align: b.buf_align, + ring_pad: b.ring_pad, + bufend_align: b.bufend_align, + auto_register: !b.skip_register, + })?), + }) + } +} + +// Trivial helper struct for this module. +struct NewArgs { + page_size: usize, + bgid: Bgid, + ring_entries: u16, + buf_cnt: u16, + buf_len: usize, + buf_align: usize, + ring_pad: usize, + bufend_align: usize, + auto_register: bool, +} + +#[derive(Debug)] +struct RawBufRing { + bgid: Bgid, + + // Keep mask rather than ring size because mask is used often, ring size not. + //ring_entries: u16, // Invariants: > 0, power of 2, max 2^15 (32768). + ring_entries_mask: u16, // Invariant one less than ring_entries which is > 0, power of 2, max 2^15 (32768). + + buf_cnt: u16, // Invariants: > 0, <= ring_entries. + buf_len: usize, // Invariant: > 0. + layout: std::alloc::Layout, + ring_addr: *const types::BufRingEntry, // Invariant: constant. + buffers_addr: *mut u8, // Invariant: constant. + local_tail: Cell, + tail_addr: *const AtomicU16, + registered: Cell, + + // The first `possible` field is a best effort at tracking the current buffer pool usage and + // from that, tracking the lowest level that has been reached. The two are an attempt at + // letting the user check the sizing needs of their buf_ring pool. + // + // We don't really know how deep the uring device has gone into the pool because we never see + // its head value and it can be taking buffers from the ring, in-flight, while we add buffers + // back to the ring. All we know is when a CQE arrives and a buffer lookup is performed, a + // buffer has already been taken from the pool, and when the buffer is dropped, we add it back + // to the the ring and it is about to be considered part of the pool again. + possible_cur: Cell, + possible_min: Cell, + // + // TODO maybe later + // #[cfg(any(debug, feature = "cautious"))] + // debug_bitmap: RefCell>, +} + +impl RawBufRing { + fn new(new_args: NewArgs) -> io::Result { + #[allow(non_upper_case_globals)] + const trace: bool = false; + + let NewArgs { + page_size, + bgid, + ring_entries, + buf_cnt, + buf_len, + buf_align, + ring_pad, + bufend_align, + auto_register, + } = new_args; + + // Check that none of the important args are zero and the ring_entries is at least large + // enough to hold all the buffers and that ring_entries is a power of 2. + + if (buf_cnt == 0) + || (buf_cnt > ring_entries) + || (buf_len == 0) + || ((ring_entries & (ring_entries - 1)) != 0) + { + return Err(io::Error::from(io::ErrorKind::InvalidInput)); + } + + // entry_size is 16 bytes. + let entry_size = std::mem::size_of::(); + let mut ring_size = entry_size * (ring_entries as usize); + if trace { + println!( + "{}:{}: entry_size {} * ring_entries {} = ring_size {} {:#x}", + file!(), + line!(), + entry_size, + ring_entries, + ring_size, + ring_size, + ); + } + + ring_size += ring_pad; + + if trace { + println!( + "{}:{}: after +ring_pad {} ring_size {} {:#x}", + file!(), + line!(), + ring_pad, + ring_size, + ring_size, + ); + } + + if buf_align > 0 { + let buf_align = buf_align.next_power_of_two(); + ring_size = (ring_size + (buf_align - 1)) & !(buf_align - 1); + if trace { + println!( + "{}:{}: after buf_align ring_size {} {:#x}", + file!(), + line!(), + ring_size, + ring_size, + ); + } + } + let buf_size = buf_len * (buf_cnt as usize); + assert!(ring_size != 0); + assert!(buf_size != 0); + let mut tot_size: usize = ring_size + buf_size; + if trace { + println!( + "{}:{}: ring_size {} {:#x} + buf_size {} {:#x} = tot_size {} {:#x}", + file!(), + line!(), + ring_size, + ring_size, + buf_size, + buf_size, + tot_size, + tot_size + ); + } + if bufend_align > 0 { + // for example, if bufend_align is 4096, would make total size a multiple of pages + let bufend_align = bufend_align.next_power_of_two(); + tot_size = (tot_size + (bufend_align - 1)) & !(bufend_align - 1); + if trace { + println!( + "{}:{}: after bufend_align tot_size {} {:#x}", + file!(), + line!(), + tot_size, + tot_size, + ); + } + } + + let align: usize = page_size; // alignment must be at least the page size + let align = align.next_power_of_two(); + let layout = std::alloc::Layout::from_size_align(tot_size, align).unwrap(); + + assert!(layout.size() >= ring_size); + // Safety: we are assured layout has nonzero size, we passed the assert just above. + let ring_addr: *mut u8 = unsafe { std::alloc::alloc_zeroed(layout) }; + + // Buffers starts after the ring_size. + // Safety: are we assured the address and the offset are in bounds because the ring_addr is + // the value we got from the alloc call, and the layout.size was shown to be at least as + // large as the ring_size. + let buffers_addr: *mut u8 = unsafe { ring_addr.add(ring_size) }; + if trace { + println!( + "{}:{}: ring_addr {} {:#x}, layout: size {} align {}", + file!(), + line!(), + ring_addr as u64, + ring_addr as u64, + layout.size(), + layout.align() + ); + println!( + "{}:{}: buffers_addr {} {:#x}", + file!(), + line!(), + buffers_addr as u64, + buffers_addr as u64, + ); + } + + let ring_addr: *const types::BufRingEntry = ring_addr as _; + + // Safety: the ring_addr passed into tail is the start of the ring. It is both the start of + // the ring and the first entry in the ring. + let tail_addr = unsafe { types::BufRingEntry::tail(ring_addr) } as *const AtomicU16; + + let ring_entries_mask = ring_entries - 1; + assert!((ring_entries & ring_entries_mask) == 0); + + let buf_ring = RawBufRing { + bgid, + ring_entries_mask, + buf_cnt, + buf_len, + layout, + ring_addr, + buffers_addr, + local_tail: Cell::new(0), + tail_addr, + registered: Cell::new(false), + possible_cur: Cell::new(0), + possible_min: Cell::new(buf_cnt), + // + // TODO maybe later + // #[cfg(any(debug, feature = "cautious"))] + // debug_bitmap: RefCell::new(std::vec![0; ((buf_cnt+7)/8) as usize]), + }; + + // Question had come up: where should the initial buffers be added to the ring? + // Here when the ring is created, even before it is registered potentially? + // Or after registration? + // + // For this type, BufRing, we are adding the buffers to the ring as the last part of creating the BufRing, + // even before registration is optionally performed. + // + // We've seen the registration to be successful, even when the ring starts off empty. + + // Add the buffers here where the ring is created. + + for bid in 0..buf_cnt { + buf_ring.buf_ring_add(bid); + } + buf_ring.buf_ring_sync(); + + // The default is to register the buffer ring right here. There is usually no reason the + // caller should want to register it some time later. + // + // Perhaps the caller wants to allocate the buffer ring before the CONTEXT driver is in + // place - that would be a reason to delay the register call until later. + + if auto_register { + buf_ring.register()?; + } + Ok(buf_ring) + } + + /// Register the buffer ring with the kernel. + /// Normally this is done automatically when building a BufRing. + /// + /// This method must be called in the context of a `tokio-uring` runtime. + /// The registration persists for the lifetime of the runtime, unless + /// revoked by the [`unregister`] method. Dropping the + /// instance this method has been called on does revoke + /// the registration and deallocate the buffer space. + /// + /// [`unregister`]: Self::unregister + /// + /// # Errors + /// + /// If a `Provided Buffers` group with the same `bgid` is already registered, the function + /// returns an error. + fn register(&self) -> io::Result<()> { + let bgid = self.bgid; + //println!("{}:{}: register bgid {bgid}", file!(), line!()); + + // Future: move to separate public function so other buf_ring implementations + // can register, and unregister, the same way. + + let res = CurrentRing::with(|ring| unsafe { + ring.submitter() + .register_buf_ring(self.ring_addr as _, self.ring_entries(), bgid) + }); + // println!("{}:{}: res {:?}", file!(), line!(), res); + + if let Err(e) = res { + match e.raw_os_error() { + Some(22) => { + // using buf_ring requires kernel 5.19 or greater. + // TODO turn these eprintln into new, more expressive error being returned. + // TODO what convention should we follow in this crate for adding information + // onto an error? + eprintln!( + "buf_ring.register returned {e}, most likely indicating this kernel is not 5.19+", + ); + } + Some(17) => { + // Registering a duplicate bgid is not allowed. There is an `unregister` + // operations that can remove the first. + eprintln!( + "buf_ring.register returned `{e}`, indicating the attempted buffer group id {bgid} was already registered", + ); + } + _ => { + eprintln!("buf_ring.register returned `{e}` for group id {bgid}"); + } + } + return Err(e); + }; + + self.registered.set(true); + + res + } + + /// Unregister the buffer ring from the io_uring. + /// Normally this is done automatically when the BufRing goes out of scope. + /// + /// Warning: requires the CONTEXT driver is already in place or will panic. + fn unregister(&self) -> io::Result<()> { + // If not registered, make this a no-op. + if !self.registered.get() { + return Ok(()); + } + + self.registered.set(false); + + let bgid = self.bgid; + + CurrentRing::with(|ring| ring.submitter().unregister_buf_ring(bgid)) + } + + /// Returns the buffer group id. + #[inline] + fn bgid(&self) -> Bgid { + self.bgid + } + + fn metric_getting_another(&self) { + self.possible_cur.set(self.possible_cur.get() - 1); + self.possible_min.set(std::cmp::min( + self.possible_min.get(), + self.possible_cur.get(), + )); + } + + // # Safety + // + // Dropping a duplicate bid is likely to cause undefined behavior + // as the kernel uses the same buffer for different data concurrently. + unsafe fn dropping_bid_i(&self, bid: Bid) { + self.buf_ring_add(bid); + self.buf_ring_sync(); + } + + #[inline] + fn buf_capacity_i(&self) -> usize { + self.buf_len as _ + } + + #[inline] + // # Panic + // + // This function will panic if given a bid that is not within the valid range 0..self.buf_cnt. + fn stable_ptr_i(&self, bid: Bid) -> *const u8 { + assert!(bid < self.buf_cnt); + let offset: usize = self.buf_len * (bid as usize); + // Safety: buffers_addr is an u8 pointer and was part of an allocation large enough to hold + // buf_cnt number of buf_len buffers. buffers_addr, buf_cnt and buf_len are treated as + // constants and bid was just asserted to be less than buf_cnt. + unsafe { self.buffers_addr.add(offset) } + } + + // # Safety + // + // This may only be called by an owned or &mut object. + // + // # Panic + // This will panic if bid is out of range. + #[inline] + unsafe fn stable_mut_ptr_i(&self, bid: Bid) -> *mut u8 { + assert!(bid < self.buf_cnt); + let offset: usize = self.buf_len * (bid as usize); + // Safety: buffers_addr is an u8 pointer and was part of an allocation large enough to hold + // buf_cnt number of buf_len buffers. buffers_addr, buf_cnt and buf_len are treated as + // constants and bid was just asserted to be less than buf_cnt. + self.buffers_addr.add(offset) + } + + #[inline] + fn ring_entries(&self) -> u16 { + self.ring_entries_mask + 1 + } + + #[inline] + fn mask(&self) -> u16 { + self.ring_entries_mask + } + + // Writes to a ring entry and updates our local copy of the tail. + // + // Adds the buffer known by its buffer id to the buffer ring. The buffer's address and length + // are known given its bid. + // + // This does not sync the new tail value. The caller should use `buf_ring_sync` for that. + // + // Panics if the bid is out of range. + fn buf_ring_add(&self, bid: Bid) { + // Compute address of current tail position, increment the local copy of the tail. Then + // write the buffer's address, length and bid into the current tail entry. + + let cur_tail = self.local_tail.get(); + self.local_tail.set(cur_tail.wrapping_add(1)); + let ring_idx = cur_tail & self.mask(); + + let ring_addr = self.ring_addr as *mut types::BufRingEntry; + + // Safety: + // 1. the pointer address (ring_addr), is set and const at self creation time, + // and points to a block of memory at least as large as the number of ring_entries, + // 2. the mask used to create ring_idx is one less than + // the number of ring_entries, and ring_entries was tested to be a power of two, + // So the address gotten by adding ring_idx entries to ring_addr is guaranteed to + // be a valid address of a ring entry. + let entry = unsafe { &mut *ring_addr.add(ring_idx as usize) }; + + entry.set_addr(self.stable_ptr_i(bid) as _); + entry.set_len(self.buf_len as _); + entry.set_bid(bid); + + // Update accounting. + self.possible_cur.set(self.possible_cur.get() + 1); + + // TODO maybe later + // #[cfg(any(debug, feature = "cautious"))] + // { + // let mut debug_bitmap = self.debug_bitmap.borrow_mut(); + // let m = 1 << (bid % 8); + // assert!(debug_bitmap[(bid / 8) as usize] & m == 0); + // debug_bitmap[(bid / 8) as usize] |= m; + // } + } + + // Make 'count' new buffers visible to the kernel. Called after + // io_uring_buf_ring_add() has been called 'count' times to fill in new + // buffers. + #[inline] + fn buf_ring_sync(&self) { + // Safety: dereferencing this raw pointer is safe. The tail_addr was computed once at init + // to refer to the tail address in the ring and is held const for self's lifetime. + unsafe { + (*self.tail_addr).store(self.local_tail.get(), atomic::Ordering::Release); + } + // The liburing code did io_uring_smp_store_release(&br.tail, local_tail); + } + + // Return the possible_min buffer pool size. + #[allow(dead_code)] + fn possible_min(&self) -> u16 { + self.possible_min.get() + } + + // Return the possible_min buffer pool size and reset to allow fresh counting going forward. + #[allow(dead_code)] + fn possible_min_and_reset(&self) -> u16 { + let res = self.possible_min.get(); + self.possible_min.set(self.buf_cnt); + res + } +} + +impl Drop for RawBufRing { + fn drop(&mut self) { + if self.registered.get() { + _ = self.unregister(); + } + // Safety: the ptr and layout are treated as constant, and ptr (ring_addr) was assigned by + // a call to std::alloc::alloc_zeroed using the same layout. + unsafe { std::alloc::dealloc(self.ring_addr as *mut u8, self.layout) }; + } +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/uring/mod.rs b/apps/aquatic/crates/udp/src/workers/socket/uring/mod.rs new file mode 100644 index 0000000..a115f47 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/uring/mod.rs @@ -0,0 +1,618 @@ +mod buf_ring; +mod recv_helper; +mod send_buffers; + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::net::SocketAddr; +use std::net::UdpSocket; +use std::ops::DerefMut; +use std::os::fd::AsRawFd; +use std::sync::atomic::Ordering; + +use anyhow::Context; +use aquatic_common::access_list::AccessListCache; +use crossbeam_channel::Sender; +use io_uring::opcode::Timeout; +use io_uring::types::{Fixed, Timespec}; +use io_uring::{IoUring, Probe}; +use recv_helper::RecvHelper; +use socket2::{Domain, Protocol, Socket, Type}; + +use aquatic_common::{ + access_list::create_access_list_cache, privileges::PrivilegeDropper, CanonicalSocketAddr, + ValidUntil, +}; +use aquatic_udp_protocol::*; +use rand::rngs::SmallRng; +use rand::SeedableRng; + +use crate::common::*; +use crate::config::Config; + +use self::buf_ring::BufRing; +use self::recv_helper::{RecvHelperV4, RecvHelperV6}; +use self::send_buffers::{ResponseType, SendBuffers}; + +use super::validator::ConnectionValidator; +use super::{EXTRA_PACKET_SIZE_IPV4, EXTRA_PACKET_SIZE_IPV6}; + +/// Size of each request buffer +/// +/// Needs to fit recvmsg metadata in addition to the payload. +/// +/// The payload of a scrape request with 20 info hashes fits in 256 bytes. +const REQUEST_BUF_LEN: usize = 512; + +/// Size of each response buffer +/// +/// Enough for: +/// - IPv6 announce response with 112 peers +/// - scrape response for 170 info hashes +const RESPONSE_BUF_LEN: usize = 2048; + +const USER_DATA_RECV_V4: u64 = u64::MAX; +const USER_DATA_RECV_V6: u64 = u64::MAX - 1; +const USER_DATA_PULSE_TIMEOUT: u64 = u64::MAX - 2; + +const SOCKET_IDENTIFIER_V4: Fixed = Fixed(0); +const SOCKET_IDENTIFIER_V6: Fixed = Fixed(1); + +thread_local! { + /// Store IoUring instance here so that it can be accessed in BufRing::drop + pub static CURRENT_RING: CurrentRing = CurrentRing(RefCell::new(None)); +} + +pub struct CurrentRing(RefCell>); + +impl CurrentRing { + fn with(mut f: F) -> T + where + F: FnMut(&mut IoUring) -> T, + { + CURRENT_RING.with(|r| { + let mut opt_ring = r.0.borrow_mut(); + + f(Option::as_mut(opt_ring.deref_mut()).expect("IoUring not set")) + }) + } +} + +pub struct SocketWorker { + config: Config, + shared_state: State, + statistics: CachePaddedArc>, + statistics_sender: Sender, + access_list_cache: AccessListCache, + validator: ConnectionValidator, + #[allow(dead_code)] + opt_socket_ipv4: Option, + #[allow(dead_code)] + opt_socket_ipv6: Option, + buf_ring: BufRing, + send_buffers: SendBuffers, + recv_helper_v4: RecvHelperV4, + recv_helper_v6: RecvHelperV6, + local_responses: VecDeque<(CanonicalSocketAddr, Response)>, + resubmittable_sqe_buf: Vec, + recv_sqe_ipv4: io_uring::squeue::Entry, + recv_sqe_ipv6: io_uring::squeue::Entry, + pulse_timeout_sqe: io_uring::squeue::Entry, + peer_valid_until: ValidUntil, + rng: SmallRng, +} + +impl SocketWorker { + pub fn run( + config: Config, + shared_state: State, + statistics: CachePaddedArc>, + statistics_sender: Sender, + validator: ConnectionValidator, + mut priv_droppers: Vec, + ) -> anyhow::Result<()> { + let ring_entries = config.network.ring_size.next_power_of_two(); + // Try to fill up the ring with send requests + let send_buffer_entries = ring_entries; + + let opt_socket_ipv4 = if config.network.use_ipv4 { + let priv_dropper = priv_droppers.pop().expect("not enough priv droppers"); + + Some( + create_socket(&config, priv_dropper, config.network.address_ipv4.into()) + .context("create ipv4 socket")?, + ) + } else { + None + }; + let opt_socket_ipv6 = if config.network.use_ipv6 { + let priv_dropper = priv_droppers.pop().expect("not enough priv droppers"); + + Some( + create_socket(&config, priv_dropper, config.network.address_ipv6.into()) + .context("create ipv6 socket")?, + ) + } else { + None + }; + + let access_list_cache = create_access_list_cache(&shared_state.access_list); + + let send_buffers = SendBuffers::new(send_buffer_entries as usize); + let recv_helper_v4 = RecvHelperV4::new(&config); + let recv_helper_v6 = RecvHelperV6::new(&config); + + let ring = IoUring::builder() + .setup_coop_taskrun() + .setup_single_issuer() + .setup_submit_all() + .build(ring_entries.into()) + .unwrap(); + + ring.submitter() + .register_files(&[ + opt_socket_ipv4 + .as_ref() + .map(|s| s.as_raw_fd()) + .unwrap_or(-1), + opt_socket_ipv6 + .as_ref() + .map(|s| s.as_raw_fd()) + .unwrap_or(-1), + ]) + .unwrap(); + + // Store ring in thread local storage before creating BufRing + CURRENT_RING.with(|r| *r.0.borrow_mut() = Some(ring)); + + let buf_ring = buf_ring::Builder::new(0) + .ring_entries(ring_entries) + .buf_len(REQUEST_BUF_LEN) + .build() + .unwrap(); + + // This timeout enables regular updates of ConnectionValidator and + // peer_valid_until + let pulse_timeout_sqe = { + let timespec_ptr = Box::into_raw(Box::new(Timespec::new().sec(5))) as *const _; + + Timeout::new(timespec_ptr) + .build() + .user_data(USER_DATA_PULSE_TIMEOUT) + }; + + let mut resubmittable_sqe_buf = vec![pulse_timeout_sqe.clone()]; + + let recv_sqe_ipv4 = recv_helper_v4.create_entry(buf_ring.bgid()); + let recv_sqe_ipv6 = recv_helper_v6.create_entry(buf_ring.bgid()); + + if opt_socket_ipv4.is_some() { + resubmittable_sqe_buf.push(recv_sqe_ipv4.clone()); + } + if opt_socket_ipv6.is_some() { + resubmittable_sqe_buf.push(recv_sqe_ipv6.clone()); + } + + let peer_valid_until = ValidUntil::new( + shared_state.server_start_instant, + config.cleaning.max_peer_age, + ); + + let mut worker = Self { + config, + shared_state, + statistics, + statistics_sender, + validator, + access_list_cache, + opt_socket_ipv4, + opt_socket_ipv6, + send_buffers, + recv_helper_v4, + recv_helper_v6, + local_responses: Default::default(), + buf_ring, + recv_sqe_ipv4, + recv_sqe_ipv6, + pulse_timeout_sqe, + resubmittable_sqe_buf, + peer_valid_until, + rng: SmallRng::from_entropy(), + }; + + CurrentRing::with(|ring| worker.run_inner(ring)); + + Ok(()) + } + + fn run_inner(&mut self, ring: &mut IoUring) { + loop { + for sqe in self.resubmittable_sqe_buf.drain(..) { + unsafe { ring.submission().push(&sqe).unwrap() }; + } + + let sq_space = { + let sq = ring.submission(); + + sq.capacity() - sq.len() + }; + + let mut num_send_added = 0; + + // Enqueue local responses + for _ in 0..sq_space { + if let Some((addr, response)) = self.local_responses.pop_front() { + let send_to_ipv4_socket = if addr.is_ipv4() { + if self.opt_socket_ipv4.is_some() { + true + } else if self.opt_socket_ipv6.is_some() { + false + } else { + panic!("No socket open") + } + } else if self.opt_socket_ipv6.is_some() { + false + } else { + panic!("IPv6 response with no IPv6 socket") + }; + + match self + .send_buffers + .prepare_entry(send_to_ipv4_socket, response, addr) + { + Ok(entry) => { + unsafe { ring.submission().push(&entry).unwrap() }; + + num_send_added += 1; + } + Err(send_buffers::Error::NoBuffers(response)) => { + self.local_responses.push_front((addr, response)); + + break; + } + Err(send_buffers::Error::SerializationFailed(err)) => { + ::log::error!("Failed serializing response: {:#}", err); + } + } + } else { + break; + } + } + + // Wait for all sendmsg entries to complete. If none were added, + // wait for at least one recvmsg or timeout in order to avoid + // busy-polling if there is no incoming data. + ring.submitter() + .submit_and_wait(num_send_added.max(1)) + .unwrap(); + + for cqe in ring.completion() { + self.handle_cqe(cqe); + } + + self.send_buffers.reset_likely_next_free_index(); + } + } + + fn handle_cqe(&mut self, cqe: io_uring::cqueue::Entry) { + match cqe.user_data() { + USER_DATA_RECV_V4 => { + if let Some((addr, response)) = self.handle_recv_cqe(&cqe, true) { + self.local_responses.push_back((addr, response)); + } + + if !io_uring::cqueue::more(cqe.flags()) { + self.resubmittable_sqe_buf.push(self.recv_sqe_ipv4.clone()); + } + } + USER_DATA_RECV_V6 => { + if let Some((addr, response)) = self.handle_recv_cqe(&cqe, false) { + self.local_responses.push_back((addr, response)); + } + + if !io_uring::cqueue::more(cqe.flags()) { + self.resubmittable_sqe_buf.push(self.recv_sqe_ipv6.clone()); + } + } + USER_DATA_PULSE_TIMEOUT => { + self.validator.update_elapsed(); + + self.peer_valid_until = ValidUntil::new( + self.shared_state.server_start_instant, + self.config.cleaning.max_peer_age, + ); + + self.resubmittable_sqe_buf + .push(self.pulse_timeout_sqe.clone()); + } + send_buffer_index => { + let result = cqe.result(); + + if result < 0 { + ::log::error!( + "Couldn't send response: {:#}", + ::std::io::Error::from_raw_os_error(-result) + ); + } else if self.config.statistics.active() { + let send_buffer_index = send_buffer_index as usize; + + let (response_type, receiver_is_ipv4) = + self.send_buffers.response_type_and_ipv4(send_buffer_index); + + let (statistics, extra_bytes) = if receiver_is_ipv4 { + (&self.statistics.ipv4, EXTRA_PACKET_SIZE_IPV4) + } else { + (&self.statistics.ipv6, EXTRA_PACKET_SIZE_IPV6) + }; + + statistics + .bytes_sent + .fetch_add(result as usize + extra_bytes, Ordering::Relaxed); + + let response_counter = match response_type { + ResponseType::Connect => &statistics.responses_connect, + ResponseType::Announce => &statistics.responses_announce, + ResponseType::Scrape => &statistics.responses_scrape, + ResponseType::Error => &statistics.responses_error, + }; + + response_counter.fetch_add(1, Ordering::Relaxed); + } + + // Safety: OK because cqe using buffer has been returned and + // contents will no longer be accessed by kernel + unsafe { + self.send_buffers + .mark_buffer_as_free(send_buffer_index as usize); + } + } + } + } + + fn handle_recv_cqe( + &mut self, + cqe: &io_uring::cqueue::Entry, + received_on_ipv4_socket: bool, + ) -> Option<(CanonicalSocketAddr, Response)> { + let result = cqe.result(); + + if result < 0 { + if -result == libc::ENOBUFS { + ::log::info!("recv failed due to lack of buffers, try increasing ring size"); + } else { + ::log::warn!( + "recv failed: {:#}", + ::std::io::Error::from_raw_os_error(-result) + ); + } + + return None; + } + + let buffer = unsafe { + match self.buf_ring.get_buf(result as u32, cqe.flags()) { + Ok(Some(buffer)) => buffer, + Ok(None) => { + ::log::error!("Couldn't get recv buffer"); + + return None; + } + Err(err) => { + ::log::error!("Couldn't get recv buffer: {:#}", err); + + return None; + } + } + }; + + let recv_helper = if received_on_ipv4_socket { + &self.recv_helper_v4 as &dyn RecvHelper + } else { + &self.recv_helper_v6 as &dyn RecvHelper + }; + + match recv_helper.parse(buffer.as_slice()) { + Ok((request, addr)) => { + if self.config.statistics.active() { + let (statistics, extra_bytes) = if addr.is_ipv4() { + (&self.statistics.ipv4, EXTRA_PACKET_SIZE_IPV4) + } else { + (&self.statistics.ipv6, EXTRA_PACKET_SIZE_IPV6) + }; + + statistics + .bytes_received + .fetch_add(buffer.len() + extra_bytes, Ordering::Relaxed); + statistics.requests.fetch_add(1, Ordering::Relaxed); + } + + return self.handle_request(request, addr); + } + Err(self::recv_helper::Error::RequestParseError(err, addr)) => { + if self.config.statistics.active() { + if addr.is_ipv4() { + self.statistics + .ipv4 + .bytes_received + .fetch_add(buffer.len() + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); + } else { + self.statistics + .ipv6 + .bytes_received + .fetch_add(buffer.len() + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); + } + } + + match err { + RequestParseError::Sendable { + connection_id, + transaction_id, + err, + } => { + ::log::debug!("Couldn't parse request from {:?}: {}", addr, err); + + if self.validator.connection_id_valid(addr, connection_id) { + let response = ErrorResponse { + transaction_id, + message: err.into(), + }; + + return Some((addr, Response::Error(response))); + } + } + RequestParseError::Unsendable { err } => { + ::log::debug!("Couldn't parse request from {:?}: {}", addr, err); + } + } + } + Err(self::recv_helper::Error::InvalidSocketAddress) => { + ::log::debug!("Ignored request claiming to be from port 0"); + } + Err(self::recv_helper::Error::RecvMsgParseError) => { + ::log::error!("RecvMsgOut::parse failed"); + } + Err(self::recv_helper::Error::RecvMsgTruncated) => { + ::log::warn!("RecvMsgOut::parse failed: sockaddr or payload truncated"); + } + } + + None + } + + fn handle_request( + &mut self, + request: Request, + src: CanonicalSocketAddr, + ) -> Option<(CanonicalSocketAddr, Response)> { + let access_list_mode = self.config.access_list.mode; + + match request { + Request::Connect(request) => { + let response = Response::Connect(ConnectResponse { + connection_id: self.validator.create_connection_id(src), + transaction_id: request.transaction_id, + }); + + return Some((src, response)); + } + Request::Announce(request) => { + if self + .validator + .connection_id_valid(src, request.connection_id) + { + if self + .access_list_cache + .load() + .allows(access_list_mode, &request.info_hash.0) + { + let response = self.shared_state.torrent_maps.announce( + &self.config, + &self.statistics_sender, + &mut self.rng, + &request, + src, + self.peer_valid_until, + ); + + return Some((src, response)); + } else { + let response = Response::Error(ErrorResponse { + transaction_id: request.transaction_id, + message: "Info hash not allowed".into(), + }); + + return Some((src, response)); + } + } + } + Request::Scrape(request) => { + if self + .validator + .connection_id_valid(src, request.connection_id) + { + let response = + Response::Scrape(self.shared_state.torrent_maps.scrape(request, src)); + + return Some((src, response)); + } + } + } + + None + } +} + +fn create_socket( + config: &Config, + priv_dropper: PrivilegeDropper, + address: SocketAddr, +) -> anyhow::Result<::std::net::UdpSocket> { + let socket = if address.is_ipv4() { + Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))? + } else { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?; + + if config.network.set_only_ipv6 { + socket + .set_only_v6(true) + .with_context(|| "socket: set only ipv6")?; + } + + socket + }; + + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + + socket + .set_nonblocking(true) + .with_context(|| "socket: set nonblocking")?; + + let recv_buffer_size = config.network.socket_recv_buffer_size; + + if recv_buffer_size != 0 { + if let Err(err) = socket.set_recv_buffer_size(recv_buffer_size) { + ::log::error!( + "socket: failed setting recv buffer to {}: {:?}", + recv_buffer_size, + err + ); + } + } + + socket + .bind(&address.into()) + .with_context(|| format!("socket: bind to {}", address))?; + + priv_dropper.after_socket_creation()?; + + Ok(socket.into()) +} + +pub fn supported_on_current_kernel() -> anyhow::Result<()> { + let opcodes = [ + // We can't probe for RecvMsgMulti, so we probe for SendZc, which was + // also introduced in Linux 6.0 + io_uring::opcode::SendZc::CODE, + ]; + + let ring = IoUring::new(1).with_context(|| "create ring")?; + + let mut probe = Probe::new(); + + ring.submitter() + .register_probe(&mut probe) + .with_context(|| "register probe")?; + + for opcode in opcodes { + if !probe.is_supported(opcode) { + return Err(anyhow::anyhow!( + "io_uring opcode {:b} not supported", + opcode + )); + } + } + + Ok(()) +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/uring/recv_helper.rs b/apps/aquatic/crates/udp/src/workers/socket/uring/recv_helper.rs new file mode 100644 index 0000000..2248a5b --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/uring/recv_helper.rs @@ -0,0 +1,169 @@ +use std::{ + mem::MaybeUninit, + net::{Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, +}; + +use aquatic_common::CanonicalSocketAddr; +use aquatic_udp_protocol::{Request, RequestParseError}; +use io_uring::{opcode::RecvMsgMulti, types::RecvMsgOut}; + +use crate::config::Config; + +use super::{SOCKET_IDENTIFIER_V4, SOCKET_IDENTIFIER_V6, USER_DATA_RECV_V4, USER_DATA_RECV_V6}; + +#[allow(clippy::enum_variant_names)] +pub enum Error { + RecvMsgParseError, + RecvMsgTruncated, + RequestParseError(RequestParseError, CanonicalSocketAddr), + InvalidSocketAddress, +} + +pub trait RecvHelper { + fn parse(&self, buffer: &[u8]) -> Result<(Request, CanonicalSocketAddr), Error>; +} + +// For IPv4 sockets +pub struct RecvHelperV4 { + max_scrape_torrents: u8, + #[allow(dead_code)] + name_v4: *const libc::sockaddr_in, + msghdr_v4: *const libc::msghdr, +} + +impl RecvHelperV4 { + pub fn new(config: &Config) -> Self { + let name_v4 = Box::into_raw(Box::new(libc::sockaddr_in { + sin_family: 0, + sin_port: 0, + sin_addr: libc::in_addr { s_addr: 0 }, + sin_zero: [0; 8], + })); + + // XXX: on musl libc, msghdr contains private padding fields + let msghdr_v4 = unsafe { + let mut hdr = MaybeUninit::::zeroed().assume_init(); + hdr.msg_name = name_v4 as *mut libc::c_void; + hdr.msg_namelen = core::mem::size_of::() as u32; + Box::into_raw(Box::new(hdr)) + }; + + Self { + max_scrape_torrents: config.protocol.max_scrape_torrents, + name_v4, + msghdr_v4, + } + } + + pub fn create_entry(&self, buf_group: u16) -> io_uring::squeue::Entry { + RecvMsgMulti::new(SOCKET_IDENTIFIER_V4, self.msghdr_v4, buf_group) + .build() + .user_data(USER_DATA_RECV_V4) + } +} + +impl RecvHelper for RecvHelperV4 { + fn parse(&self, buffer: &[u8]) -> Result<(Request, CanonicalSocketAddr), Error> { + // Safe as long as kernel only reads from the pointer and doesn't + // write to it. I think this is the case. + let msghdr = unsafe { self.msghdr_v4.read() }; + + let msg = RecvMsgOut::parse(buffer, &msghdr).map_err(|_| Error::RecvMsgParseError)?; + + if msg.is_name_data_truncated() | msg.is_payload_truncated() { + return Err(Error::RecvMsgTruncated); + } + + let name_data = unsafe { *(msg.name_data().as_ptr() as *const libc::sockaddr_in) }; + + let addr = SocketAddr::V4(SocketAddrV4::new( + u32::from_be(name_data.sin_addr.s_addr).into(), + u16::from_be(name_data.sin_port), + )); + + if addr.port() == 0 { + return Err(Error::InvalidSocketAddress); + } + + let addr = CanonicalSocketAddr::new(addr); + + let request = Request::parse_bytes(msg.payload_data(), self.max_scrape_torrents) + .map_err(|err| Error::RequestParseError(err, addr))?; + + Ok((request, addr)) + } +} + +// For IPv6 sockets (can theoretically still receive IPv4 packets, though) +pub struct RecvHelperV6 { + max_scrape_torrents: u8, + #[allow(dead_code)] + name_v6: *const libc::sockaddr_in6, + msghdr_v6: *const libc::msghdr, +} + +impl RecvHelperV6 { + pub fn new(config: &Config) -> Self { + let name_v6 = Box::into_raw(Box::new(libc::sockaddr_in6 { + sin6_family: 0, + sin6_port: 0, + sin6_flowinfo: 0, + sin6_addr: libc::in6_addr { s6_addr: [0; 16] }, + sin6_scope_id: 0, + })); + + // XXX: on musl libc, msghdr contains private padding fields + let msghdr_v6 = unsafe { + let mut hdr = MaybeUninit::::zeroed().assume_init(); + hdr.msg_name = name_v6 as *mut libc::c_void; + hdr.msg_namelen = core::mem::size_of::() as u32; + Box::into_raw(Box::new(hdr)) + }; + + Self { + max_scrape_torrents: config.protocol.max_scrape_torrents, + name_v6, + msghdr_v6, + } + } + + pub fn create_entry(&self, buf_group: u16) -> io_uring::squeue::Entry { + RecvMsgMulti::new(SOCKET_IDENTIFIER_V6, self.msghdr_v6, buf_group) + .build() + .user_data(USER_DATA_RECV_V6) + } +} + +impl RecvHelper for RecvHelperV6 { + fn parse(&self, buffer: &[u8]) -> Result<(Request, CanonicalSocketAddr), Error> { + // Safe as long as kernel only reads from the pointer and doesn't + // write to it. I think this is the case. + let msghdr = unsafe { self.msghdr_v6.read() }; + + let msg = RecvMsgOut::parse(buffer, &msghdr).map_err(|_| Error::RecvMsgParseError)?; + + if msg.is_name_data_truncated() | msg.is_payload_truncated() { + return Err(Error::RecvMsgTruncated); + } + + let name_data = unsafe { *(msg.name_data().as_ptr() as *const libc::sockaddr_in6) }; + + let addr = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::from(name_data.sin6_addr.s6_addr), + u16::from_be(name_data.sin6_port), + u32::from_be(name_data.sin6_flowinfo), + u32::from_be(name_data.sin6_scope_id), + )); + + if addr.port() == 0 { + return Err(Error::InvalidSocketAddress); + } + + let addr = CanonicalSocketAddr::new(addr); + + let request = Request::parse_bytes(msg.payload_data(), self.max_scrape_torrents) + .map_err(|err| Error::RequestParseError(err, addr))?; + + Ok((request, addr)) + } +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/uring/send_buffers.rs b/apps/aquatic/crates/udp/src/workers/socket/uring/send_buffers.rs new file mode 100644 index 0000000..9016f46 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/uring/send_buffers.rs @@ -0,0 +1,242 @@ +use std::{ + io::Cursor, + iter::repeat_with, + mem::MaybeUninit, + net::SocketAddr, + ptr::{addr_of_mut, null_mut}, +}; + +use aquatic_common::CanonicalSocketAddr; +use aquatic_udp_protocol::Response; +use io_uring::opcode::SendMsg; + +use super::{RESPONSE_BUF_LEN, SOCKET_IDENTIFIER_V4, SOCKET_IDENTIFIER_V6}; + +pub enum Error { + NoBuffers(Response), + SerializationFailed(std::io::Error), +} + +pub struct SendBuffers { + likely_next_free_index: usize, + buffers: Vec<(SendBufferMetadata, *mut SendBuffer)>, +} + +impl SendBuffers { + pub fn new(capacity: usize) -> Self { + let buffers = repeat_with(|| (Default::default(), SendBuffer::new())) + .take(capacity) + .collect::>(); + + Self { + likely_next_free_index: 0, + buffers, + } + } + + pub fn response_type_and_ipv4(&self, index: usize) -> (ResponseType, bool) { + let meta = &self.buffers.get(index).unwrap().0; + + (meta.response_type, meta.receiver_is_ipv4) + } + + /// # Safety + /// + /// Only safe to call once buffer is no longer referenced by in-flight + /// io_uring queue entries + pub unsafe fn mark_buffer_as_free(&mut self, index: usize) { + self.buffers[index].0.free = true; + } + + /// Call after going through completion queue + pub fn reset_likely_next_free_index(&mut self) { + self.likely_next_free_index = 0; + } + + pub fn prepare_entry( + &mut self, + send_to_ipv4_socket: bool, + response: Response, + addr: CanonicalSocketAddr, + ) -> Result { + let index = if let Some(index) = self.next_free_index() { + index + } else { + return Err(Error::NoBuffers(response)); + }; + + let (buffer_metadata, buffer) = self.buffers.get_mut(index).unwrap(); + + // Safe as long as `mark_buffer_as_free` was used correctly + let buffer = unsafe { &mut *(*buffer) }; + + match buffer.prepare_entry(response, addr, send_to_ipv4_socket, buffer_metadata) { + Ok(entry) => { + buffer_metadata.free = false; + + self.likely_next_free_index = index + 1; + + Ok(entry.user_data(index as u64)) + } + Err(err) => Err(err), + } + } + + fn next_free_index(&self) -> Option { + if self.likely_next_free_index >= self.buffers.len() { + return None; + } + + for (i, (meta, _)) in self.buffers[self.likely_next_free_index..] + .iter() + .enumerate() + { + if meta.free { + return Some(self.likely_next_free_index + i); + } + } + + None + } +} + +/// Make sure not to hold any reference to this struct while kernel can +/// write to its contents +struct SendBuffer { + name_v4: libc::sockaddr_in, + name_v6: libc::sockaddr_in6, + bytes: [u8; RESPONSE_BUF_LEN], + iovec: libc::iovec, + msghdr: libc::msghdr, +} + +impl SendBuffer { + fn new() -> *mut Self { + let mut instance = Box::new(Self { + name_v4: libc::sockaddr_in { + sin_family: libc::AF_INET as u16, + sin_port: 0, + sin_addr: libc::in_addr { s_addr: 0 }, + sin_zero: [0; 8], + }, + name_v6: libc::sockaddr_in6 { + sin6_family: libc::AF_INET6 as u16, + sin6_port: 0, + sin6_flowinfo: 0, + sin6_addr: libc::in6_addr { s6_addr: [0; 16] }, + sin6_scope_id: 0, + }, + bytes: [0; RESPONSE_BUF_LEN], + iovec: libc::iovec { + iov_base: null_mut(), + iov_len: 0, + }, + msghdr: unsafe { MaybeUninit::::zeroed().assume_init() }, + }); + + instance.iovec.iov_base = addr_of_mut!(instance.bytes) as *mut libc::c_void; + instance.iovec.iov_len = instance.bytes.len(); + + instance.msghdr.msg_iov = addr_of_mut!(instance.iovec); + instance.msghdr.msg_iovlen = 1; + + // Set IPv4 initially. Will be overridden with each prepare_entry call + instance.msghdr.msg_name = addr_of_mut!(instance.name_v4) as *mut libc::c_void; + instance.msghdr.msg_namelen = core::mem::size_of::() as u32; + + Box::into_raw(instance) + } + + fn prepare_entry( + &mut self, + response: Response, + addr: CanonicalSocketAddr, + send_to_ipv4_socket: bool, + metadata: &mut SendBufferMetadata, + ) -> Result { + let entry_fd = if send_to_ipv4_socket { + metadata.receiver_is_ipv4 = true; + + let addr = if let Some(SocketAddr::V4(addr)) = addr.get_ipv4() { + addr + } else { + panic!("ipv6 address in ipv4 mode"); + }; + + self.name_v4.sin_port = addr.port().to_be(); + self.name_v4.sin_addr.s_addr = u32::from(*addr.ip()).to_be(); + self.msghdr.msg_name = addr_of_mut!(self.name_v4) as *mut libc::c_void; + self.msghdr.msg_namelen = core::mem::size_of::() as u32; + + SOCKET_IDENTIFIER_V4 + } else { + // Set receiver protocol type before calling addr.get_ipv6_mapped() + metadata.receiver_is_ipv4 = addr.is_ipv4(); + + let addr = if let SocketAddr::V6(addr) = addr.get_ipv6_mapped() { + addr + } else { + panic!("ipv4 address when ipv6 or ipv6-mapped address expected"); + }; + + self.name_v6.sin6_port = addr.port().to_be(); + self.name_v6.sin6_addr.s6_addr = addr.ip().octets(); + self.msghdr.msg_name = addr_of_mut!(self.name_v6) as *mut libc::c_void; + self.msghdr.msg_namelen = core::mem::size_of::() as u32; + + SOCKET_IDENTIFIER_V6 + }; + + let mut cursor = Cursor::new(&mut self.bytes[..]); + + match response.write_bytes(&mut cursor) { + Ok(()) => { + self.iovec.iov_len = cursor.position() as usize; + + metadata.response_type = ResponseType::from_response(&response); + + Ok(SendMsg::new(entry_fd, addr_of_mut!(self.msghdr)).build()) + } + Err(err) => Err(Error::SerializationFailed(err)), + } + } +} + +#[derive(Debug)] +struct SendBufferMetadata { + free: bool, + /// Only used for statistics + receiver_is_ipv4: bool, + /// Only used for statistics + response_type: ResponseType, +} + +impl Default for SendBufferMetadata { + fn default() -> Self { + Self { + free: true, + receiver_is_ipv4: true, + response_type: Default::default(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum ResponseType { + #[default] + Connect, + Announce, + Scrape, + Error, +} + +impl ResponseType { + fn from_response(response: &Response) -> Self { + match response { + Response::Connect(_) => Self::Connect, + Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => Self::Announce, + Response::Scrape(_) => Self::Scrape, + Response::Error(_) => Self::Error, + } + } +} diff --git a/apps/aquatic/crates/udp/src/workers/socket/validator.rs b/apps/aquatic/crates/udp/src/workers/socket/validator.rs new file mode 100644 index 0000000..f96b915 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/socket/validator.rs @@ -0,0 +1,165 @@ +use std::net::IpAddr; +use std::time::Instant; + +use anyhow::Context; +use constant_time_eq::constant_time_eq; +use getrandom::getrandom; + +use aquatic_common::CanonicalSocketAddr; +use aquatic_udp_protocol::ConnectionId; + +use crate::config::Config; + +/// HMAC (BLAKE3) based ConnectionId creator and validator +/// +/// Method update_elapsed must be called at least once a minute. +/// +/// The purpose of using ConnectionIds is to make IP spoofing costly, mainly to +/// prevent the tracker from being used as an amplification vector for DDoS +/// attacks. By including 32 bits of BLAKE3 keyed hash output in the Ids, an +/// attacker would have to make on average 2^31 attemps to correctly guess a +/// single hash. Furthermore, such a hash would only be valid for at most +/// `max_connection_age` seconds, a short duration to get value for the +/// bandwidth spent brute forcing it. +/// +/// Structure of created ConnectionID (bytes making up inner i64): +/// - &[0..4]: ConnectionId creation time as number of seconds after +/// ConnectionValidator instance was created, encoded as u32 bytes. A u32 +/// fits around 136 years in seconds. +/// - &[4..8]: truncated keyed BLAKE3 hash of: +/// - previous 4 bytes +/// - octets of client IP address +#[derive(Clone)] +pub struct ConnectionValidator { + start_time: Instant, + max_connection_age: u64, + keyed_hasher: blake3::Hasher, + seconds_since_start: u32, +} + +impl ConnectionValidator { + /// Create new instance. Must be created once and cloned if used in several + /// threads. + pub fn new(config: &Config) -> anyhow::Result { + let mut key = [0; 32]; + + getrandom(&mut key) + .with_context(|| "Couldn't get random bytes for ConnectionValidator key")?; + + let keyed_hasher = blake3::Hasher::new_keyed(&key); + + Ok(Self { + keyed_hasher, + start_time: Instant::now(), + max_connection_age: config.cleaning.max_connection_age.into(), + seconds_since_start: 0, + }) + } + + pub fn create_connection_id(&mut self, source_addr: CanonicalSocketAddr) -> ConnectionId { + let elapsed = (self.seconds_since_start).to_ne_bytes(); + + let hash = self.hash(elapsed, source_addr.get().ip()); + + let mut connection_id_bytes = [0u8; 8]; + + connection_id_bytes[..4].copy_from_slice(&elapsed); + connection_id_bytes[4..].copy_from_slice(&hash); + + ConnectionId::new(i64::from_ne_bytes(connection_id_bytes)) + } + + pub fn connection_id_valid( + &mut self, + source_addr: CanonicalSocketAddr, + connection_id: ConnectionId, + ) -> bool { + let bytes = connection_id.0.get().to_ne_bytes(); + let (elapsed, hash) = bytes.split_at(4); + let elapsed: [u8; 4] = elapsed.try_into().unwrap(); + + if !constant_time_eq(hash, &self.hash(elapsed, source_addr.get().ip())) { + return false; + } + + let seconds_since_start = self.seconds_since_start as u64; + let client_elapsed = u64::from(u32::from_ne_bytes(elapsed)); + let client_expiration_time = client_elapsed + self.max_connection_age; + + // In addition to checking if the client connection is expired, + // disallow client_elapsed values that are too far in future and thus + // could not have been sent by the tracker. This prevents brute forcing + // with `u32::MAX` as 'elapsed' part of ConnectionId to find a hash that + // works until the tracker is restarted. + let client_not_expired = client_expiration_time > seconds_since_start; + let client_elapsed_not_in_far_future = client_elapsed <= (seconds_since_start + 60); + + client_not_expired & client_elapsed_not_in_far_future + } + + pub fn update_elapsed(&mut self) { + self.seconds_since_start = self.start_time.elapsed().as_secs() as u32; + } + + fn hash(&mut self, elapsed: [u8; 4], ip_addr: IpAddr) -> [u8; 4] { + self.keyed_hasher.update(&elapsed); + + match ip_addr { + IpAddr::V4(ip) => self.keyed_hasher.update(&ip.octets()), + IpAddr::V6(ip) => self.keyed_hasher.update(&ip.octets()), + }; + + let mut hash = [0u8; 4]; + + self.keyed_hasher.finalize_xof().fill(&mut hash); + self.keyed_hasher.reset(); + + hash + } +} + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use quickcheck_macros::quickcheck; + + use super::*; + + #[quickcheck] + fn test_connection_validator( + original_addr: IpAddr, + different_addr: IpAddr, + max_connection_age: u32, + ) -> quickcheck::TestResult { + let original_addr = CanonicalSocketAddr::new(SocketAddr::new(original_addr, 0)); + let different_addr = CanonicalSocketAddr::new(SocketAddr::new(different_addr, 0)); + + if original_addr == different_addr { + return quickcheck::TestResult::discard(); + } + + let mut validator = { + let mut config = Config::default(); + + config.cleaning.max_connection_age = max_connection_age; + + ConnectionValidator::new(&config).unwrap() + }; + + let connection_id = validator.create_connection_id(original_addr); + + let original_valid = validator.connection_id_valid(original_addr, connection_id); + let different_valid = validator.connection_id_valid(different_addr, connection_id); + + if different_valid { + return quickcheck::TestResult::failed(); + } + + if max_connection_age == 0 { + quickcheck::TestResult::from_bool(!original_valid) + } else { + quickcheck::TestResult::from_bool(original_valid) + } + } +} diff --git a/apps/aquatic/crates/udp/src/workers/statistics/collector.rs b/apps/aquatic/crates/udp/src/workers/statistics/collector.rs new file mode 100644 index 0000000..93fe11d --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/statistics/collector.rs @@ -0,0 +1,331 @@ +use std::sync::atomic::Ordering; +use std::time::Instant; + +use hdrhistogram::Histogram; +use num_format::{Locale, ToFormattedString}; +use serde::Serialize; + +use crate::config::Config; + +use super::{IpVersion, Statistics}; + +#[cfg(feature = "prometheus")] +macro_rules! set_peer_histogram_gauge { + ($ip_version:expr, $data:expr, $type_label:expr) => { + ::metrics::gauge!( + "aquatic_peers_per_torrent", + "type" => $type_label, + "ip_version" => $ip_version, + ) + .set($data as f64); + }; +} + +pub struct StatisticsCollector { + statistics: Statistics, + ip_version: IpVersion, + last_update: Instant, + last_complete_histogram: PeerHistogramStatistics, +} + +impl StatisticsCollector { + pub fn new(statistics: Statistics, ip_version: IpVersion) -> Self { + Self { + statistics, + last_update: Instant::now(), + last_complete_histogram: Default::default(), + ip_version, + } + } + + pub fn add_histogram(&mut self, histogram: Histogram) { + self.last_complete_histogram = PeerHistogramStatistics::new(histogram); + } + + pub fn collect_from_shared( + &mut self, + #[cfg(feature = "prometheus")] config: &Config, + ) -> CollectedStatistics { + let mut requests = 0; + let mut responses_connect: usize = 0; + let mut responses_announce: usize = 0; + let mut responses_scrape: usize = 0; + let mut responses_error: usize = 0; + let mut bytes_received: usize = 0; + let mut bytes_sent: usize = 0; + + #[cfg(feature = "prometheus")] + let ip_version_prometheus_str = self.ip_version.prometheus_str(); + + for (i, statistics) in self + .statistics + .socket + .iter() + .map(|s| s.by_ip_version(self.ip_version)) + .enumerate() + { + { + let n = statistics.requests.fetch_and(0, Ordering::Relaxed); + + requests += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_requests_total", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics.responses_connect.fetch_and(0, Ordering::Relaxed); + + responses_connect += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_responses_total", + "type" => "connect", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics + .responses_announce + .fetch_and(0, Ordering::Relaxed); + + responses_announce += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_responses_total", + "type" => "announce", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics.responses_scrape.fetch_and(0, Ordering::Relaxed); + + responses_scrape += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_responses_total", + "type" => "scrape", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics.responses_error.fetch_and(0, Ordering::Relaxed); + + responses_error += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_responses_total", + "type" => "error", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics.bytes_received.fetch_and(0, Ordering::Relaxed); + + bytes_received += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_rx_bytes", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + { + let n = statistics.bytes_sent.fetch_and(0, Ordering::Relaxed); + + bytes_sent += n; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::counter!( + "aquatic_tx_bytes", + "ip_version" => ip_version_prometheus_str, + "worker_index" => i.to_string(), + ) + .increment(n.try_into().unwrap()); + } + } + } + + let swarm_statistics = &self.statistics.swarm.by_ip_version(self.ip_version); + + let num_torrents = { + let num_torrents = swarm_statistics.torrents.load(Ordering::Relaxed); + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => ip_version_prometheus_str, + ) + .set(num_torrents as f64); + } + + num_torrents + }; + + let num_peers = { + let num_peers = swarm_statistics.peers.load(Ordering::Relaxed); + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::gauge!( + "aquatic_peers", + "ip_version" => ip_version_prometheus_str, + ) + .set(num_peers as f64); + } + + num_peers + }; + + let elapsed = { + let now = Instant::now(); + + let elapsed = (now - self.last_update).as_secs_f64(); + + self.last_update = now; + + elapsed + }; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint && config.statistics.torrent_peer_histograms { + self.last_complete_histogram + .update_metrics(ip_version_prometheus_str); + } + + let requests_per_second = requests as f64 / elapsed; + let responses_per_second_connect = responses_connect as f64 / elapsed; + let responses_per_second_announce = responses_announce as f64 / elapsed; + let responses_per_second_scrape = responses_scrape as f64 / elapsed; + let responses_per_second_error = responses_error as f64 / elapsed; + let bytes_received_per_second = bytes_received as f64 / elapsed; + let bytes_sent_per_second = bytes_sent as f64 / elapsed; + + let responses_per_second_total = responses_per_second_connect + + responses_per_second_announce + + responses_per_second_scrape + + responses_per_second_error; + + CollectedStatistics { + requests_per_second: (requests_per_second as usize).to_formatted_string(&Locale::en), + responses_per_second_total: (responses_per_second_total as usize) + .to_formatted_string(&Locale::en), + responses_per_second_connect: (responses_per_second_connect as usize) + .to_formatted_string(&Locale::en), + responses_per_second_announce: (responses_per_second_announce as usize) + .to_formatted_string(&Locale::en), + responses_per_second_scrape: (responses_per_second_scrape as usize) + .to_formatted_string(&Locale::en), + responses_per_second_error: (responses_per_second_error as usize) + .to_formatted_string(&Locale::en), + rx_mbits: format!("{:.2}", bytes_received_per_second * 8.0 / 1_000_000.0), + tx_mbits: format!("{:.2}", bytes_sent_per_second * 8.0 / 1_000_000.0), + num_torrents: num_torrents.to_formatted_string(&Locale::en), + num_peers: num_peers.to_formatted_string(&Locale::en), + peer_histogram: self.last_complete_histogram.clone(), + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct CollectedStatistics { + pub requests_per_second: String, + pub responses_per_second_total: String, + pub responses_per_second_connect: String, + pub responses_per_second_announce: String, + pub responses_per_second_scrape: String, + pub responses_per_second_error: String, + pub rx_mbits: String, + pub tx_mbits: String, + pub num_torrents: String, + pub num_peers: String, + pub peer_histogram: PeerHistogramStatistics, +} + +#[derive(Clone, Debug, Serialize, Default)] +pub struct PeerHistogramStatistics { + pub min: u64, + pub p10: u64, + pub p20: u64, + pub p30: u64, + pub p40: u64, + pub p50: u64, + pub p60: u64, + pub p70: u64, + pub p80: u64, + pub p90: u64, + pub p95: u64, + pub p99: u64, + pub p999: u64, + pub max: u64, +} + +impl PeerHistogramStatistics { + fn new(h: Histogram) -> Self { + Self { + min: h.min(), + p10: h.value_at_percentile(10.0), + p20: h.value_at_percentile(20.0), + p30: h.value_at_percentile(30.0), + p40: h.value_at_percentile(40.0), + p50: h.value_at_percentile(50.0), + p60: h.value_at_percentile(60.0), + p70: h.value_at_percentile(70.0), + p80: h.value_at_percentile(80.0), + p90: h.value_at_percentile(90.0), + p95: h.value_at_percentile(95.0), + p99: h.value_at_percentile(99.0), + p999: h.value_at_percentile(99.9), + max: h.max(), + } + } + + #[cfg(feature = "prometheus")] + fn update_metrics(&self, ip_version: &'static str) { + set_peer_histogram_gauge!(ip_version, self.min, "min"); + set_peer_histogram_gauge!(ip_version, self.p10, "p10"); + set_peer_histogram_gauge!(ip_version, self.p20, "p20"); + set_peer_histogram_gauge!(ip_version, self.p30, "p30"); + set_peer_histogram_gauge!(ip_version, self.p40, "p40"); + set_peer_histogram_gauge!(ip_version, self.p50, "p50"); + set_peer_histogram_gauge!(ip_version, self.p60, "p60"); + set_peer_histogram_gauge!(ip_version, self.p70, "p70"); + set_peer_histogram_gauge!(ip_version, self.p80, "p80"); + set_peer_histogram_gauge!(ip_version, self.p90, "p90"); + set_peer_histogram_gauge!(ip_version, self.p99, "p99"); + set_peer_histogram_gauge!(ip_version, self.p999, "p999"); + set_peer_histogram_gauge!(ip_version, self.max, "max"); + } +} diff --git a/apps/aquatic/crates/udp/src/workers/statistics/mod.rs b/apps/aquatic/crates/udp/src/workers/statistics/mod.rs new file mode 100644 index 0000000..9ae5b91 --- /dev/null +++ b/apps/aquatic/crates/udp/src/workers/statistics/mod.rs @@ -0,0 +1,298 @@ +mod collector; + +use std::fs::File; +use std::io::Write; +use std::time::{Duration, Instant}; + +use anyhow::Context; +use aquatic_common::IndexMap; +use aquatic_udp_protocol::{PeerClient, PeerId}; +use compact_str::CompactString; +use crossbeam_channel::Receiver; +use num_format::{Locale, ToFormattedString}; +use serde::Serialize; +use time::format_description::well_known::Rfc2822; +use time::OffsetDateTime; +use tinytemplate::TinyTemplate; + +use collector::{CollectedStatistics, StatisticsCollector}; + +use crate::common::*; +use crate::config::Config; + +const TEMPLATE_KEY: &str = "statistics"; +const TEMPLATE_CONTENTS: &str = include_str!("../../../templates/statistics.html"); +const STYLESHEET_CONTENTS: &str = concat!( + "" +); + +#[derive(Debug, Serialize)] +struct TemplateData { + stylesheet: String, + ipv4_active: bool, + ipv6_active: bool, + extended_active: bool, + ipv4: CollectedStatistics, + ipv6: CollectedStatistics, + last_updated: String, + peer_update_interval: String, + peer_clients: Vec<(String, String)>, +} + +pub fn run_statistics_worker( + config: Config, + shared_state: State, + statistics: Statistics, + statistics_receiver: Receiver, +) -> anyhow::Result<()> { + let process_peer_client_data = { + let mut collect = config.statistics.write_html_to_file; + + #[cfg(feature = "prometheus")] + { + collect |= config.statistics.run_prometheus_endpoint; + } + + collect & config.statistics.peer_clients + }; + + let opt_tt = if config.statistics.write_html_to_file { + let mut tt = TinyTemplate::new(); + + tt.add_template(TEMPLATE_KEY, TEMPLATE_CONTENTS) + .context("parse statistics html template")?; + + Some(tt) + } else { + None + }; + + let mut ipv4_collector = StatisticsCollector::new(statistics.clone(), IpVersion::V4); + let mut ipv6_collector = StatisticsCollector::new(statistics, IpVersion::V6); + + // Store a count to enable not removing peers from the count completely + // just because they were removed from one torrent + let mut peers: IndexMap = IndexMap::default(); + + loop { + let start_time = Instant::now(); + + for message in statistics_receiver.try_iter() { + match message { + StatisticsMessage::Ipv4PeerHistogram(h) => ipv4_collector.add_histogram(h), + StatisticsMessage::Ipv6PeerHistogram(h) => ipv6_collector.add_histogram(h), + StatisticsMessage::PeerAdded(peer_id) => { + if process_peer_client_data { + peers + .entry(peer_id) + .or_insert_with(|| (0, peer_id.client(), peer_id.first_8_bytes_hex())) + .0 += 1; + } + } + StatisticsMessage::PeerRemoved(peer_id) => { + if process_peer_client_data { + if let Some((count, _, _)) = peers.get_mut(&peer_id) { + *count -= 1; + + if *count == 0 { + peers.swap_remove(&peer_id); + } + } + } + } + } + } + + let statistics_ipv4 = ipv4_collector.collect_from_shared( + #[cfg(feature = "prometheus")] + &config, + ); + let statistics_ipv6 = ipv6_collector.collect_from_shared( + #[cfg(feature = "prometheus")] + &config, + ); + + let peer_clients = if process_peer_client_data { + let mut clients: IndexMap = IndexMap::default(); + + #[cfg(feature = "prometheus")] + let mut prefixes: IndexMap = IndexMap::default(); + + // Only count peer_ids once, even if they are in multiple torrents + for (_, peer_client, prefix) in peers.values() { + *clients.entry(peer_client.to_owned()).or_insert(0) += 1; + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint + && config.statistics.prometheus_peer_id_prefixes + { + *prefixes.entry(prefix.to_owned()).or_insert(0) += 1; + } + } + + clients.sort_unstable_by(|_, a, _, b| b.cmp(a)); + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint + && config.statistics.prometheus_peer_id_prefixes + { + for (prefix, count) in prefixes { + ::metrics::gauge!( + "aquatic_peer_id_prefixes", + "prefix_hex" => prefix.to_string(), + ) + .set(count as f64); + } + } + + let mut client_vec = Vec::with_capacity(clients.len()); + + for (client, count) in clients { + if config.statistics.write_html_to_file { + client_vec.push((client.to_string(), count.to_formatted_string(&Locale::en))); + } + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::gauge!( + "aquatic_peer_clients", + "client" => client.to_string(), + ) + .set(count as f64); + } + } + + client_vec + } else { + Vec::new() + }; + + if config.statistics.print_to_stdout { + println!("General:"); + println!( + " access list entries: {}", + shared_state.access_list.load().len() + ); + + if config.network.ipv4_active() { + println!("IPv4:"); + print_to_stdout(&config, &statistics_ipv4); + } + if config.network.ipv6_active() { + println!("IPv6:"); + print_to_stdout(&config, &statistics_ipv6); + } + + println!(); + } + + if let Some(tt) = opt_tt.as_ref() { + let template_data = TemplateData { + stylesheet: STYLESHEET_CONTENTS.to_string(), + ipv4_active: config.network.ipv4_active(), + ipv6_active: config.network.ipv6_active(), + extended_active: config.statistics.torrent_peer_histograms, + ipv4: statistics_ipv4, + ipv6: statistics_ipv6, + last_updated: OffsetDateTime::now_utc() + .format(&Rfc2822) + .unwrap_or("(formatting error)".into()), + peer_update_interval: format!("{}", config.cleaning.torrent_cleaning_interval), + peer_clients, + }; + + if let Err(err) = save_html_to_file(&config, tt, &template_data) { + ::log::error!("Couldn't save statistics to file: {:#}", err) + } + } + + peers.shrink_to_fit(); + + if let Some(time_remaining) = + Duration::from_secs(config.statistics.interval).checked_sub(start_time.elapsed()) + { + ::std::thread::sleep(time_remaining); + } else { + ::log::warn!( + "statistics interval not long enough to process all data, output may be misleading" + ); + } + } +} + +fn print_to_stdout(config: &Config, statistics: &CollectedStatistics) { + println!( + " bandwidth: {:>7} Mbit/s in, {:7} Mbit/s out", + statistics.rx_mbits, statistics.tx_mbits, + ); + println!(" requests/second: {:>10}", statistics.requests_per_second); + println!(" responses/second"); + println!( + " total: {:>10}", + statistics.responses_per_second_total + ); + println!( + " connect: {:>10}", + statistics.responses_per_second_connect + ); + println!( + " announce: {:>10}", + statistics.responses_per_second_announce + ); + println!( + " scrape: {:>10}", + statistics.responses_per_second_scrape + ); + println!( + " error: {:>10}", + statistics.responses_per_second_error + ); + println!( + " torrents: {:>10} (updated every {}s)", + statistics.num_torrents, config.cleaning.torrent_cleaning_interval + ); + println!( + " peers: {:>10} (updated every {}s)", + statistics.num_peers, config.cleaning.torrent_cleaning_interval + ); + + if config.statistics.torrent_peer_histograms { + println!( + " peers per torrent (updated every {}s)", + config.cleaning.torrent_cleaning_interval + ); + println!(" min {:>10}", statistics.peer_histogram.min); + println!(" p10 {:>10}", statistics.peer_histogram.p10); + println!(" p20 {:>10}", statistics.peer_histogram.p20); + println!(" p30 {:>10}", statistics.peer_histogram.p30); + println!(" p40 {:>10}", statistics.peer_histogram.p40); + println!(" p50 {:>10}", statistics.peer_histogram.p50); + println!(" p60 {:>10}", statistics.peer_histogram.p60); + println!(" p70 {:>10}", statistics.peer_histogram.p70); + println!(" p80 {:>10}", statistics.peer_histogram.p80); + println!(" p90 {:>10}", statistics.peer_histogram.p90); + println!(" p95 {:>10}", statistics.peer_histogram.p95); + println!(" p99 {:>10}", statistics.peer_histogram.p99); + println!(" p99.9 {:>10}", statistics.peer_histogram.p999); + println!(" max {:>10}", statistics.peer_histogram.max); + } +} + +fn save_html_to_file( + config: &Config, + tt: &TinyTemplate, + template_data: &TemplateData, +) -> anyhow::Result<()> { + let mut file = File::create(&config.statistics.html_file_path).with_context(|| { + format!( + "File path: {}", + &config.statistics.html_file_path.to_string_lossy() + ) + })?; + + write!(file, "{}", tt.render(TEMPLATE_KEY, template_data)?)?; + + Ok(()) +} diff --git a/apps/aquatic/crates/udp/templates/statistics.css b/apps/aquatic/crates/udp/templates/statistics.css new file mode 100644 index 0000000..ea8872a --- /dev/null +++ b/apps/aquatic/crates/udp/templates/statistics.css @@ -0,0 +1,22 @@ +body { + font-family: arial, sans-serif; + font-size: 16px; +} + +table { + border-collapse: collapse +} + +caption { + caption-side: bottom; + padding-top: 0.5rem; +} + +th, td { + padding: 0.5rem 2rem; + border: 1px solid #ccc; +} + +th { + background-color: #eee; +} \ No newline at end of file diff --git a/apps/aquatic/crates/udp/templates/statistics.html b/apps/aquatic/crates/udp/templates/statistics.html new file mode 100644 index 0000000..01bd37f --- /dev/null +++ b/apps/aquatic/crates/udp/templates/statistics.html @@ -0,0 +1,278 @@ + + + + + + + + UDP BitTorrent tracker statistics + + {#- Include stylesheet like this to prevent code editor syntax warnings #} + { stylesheet | unescaped } + + + +

BitTorrent tracker statistics

+ + {#-

Tracker software: aquatic_udp

#} + +

+ Updated: { last_updated } (UTC) +

+ + {{ if ipv4_active }} + +

IPv4

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
* Torrent/peer count is updated every { peer_update_interval } seconds
Number of torrents{ ipv4.num_torrents } *
Number of peers{ ipv4.num_peers } *
Requests / second{ ipv4.requests_per_second }
Total responses / second{ ipv4.responses_per_second_total }
Connect responses / second{ ipv4.responses_per_second_connect }
Announce responses / second{ ipv4.responses_per_second_announce }
Scrape responses / second{ ipv4.responses_per_second_scrape }
Error responses / second{ ipv4.responses_per_second_error }
Bandwidth (RX){ ipv4.rx_mbits } mbit/s
Bandwidth (TX){ ipv4.tx_mbits } mbit/s
+ + {{ if extended_active }} + +

Peers per torrent

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Updated every { peer_update_interval } seconds
Minimum{ ipv4.peer_histogram.min }
10th percentile{ ipv4.peer_histogram.p10 }
20th percentile{ ipv4.peer_histogram.p20 }
30th percentile{ ipv4.peer_histogram.p30 }
40th percentile{ ipv4.peer_histogram.p40 }
50th percentile{ ipv4.peer_histogram.p50 }
60th percentile{ ipv4.peer_histogram.p60 }
70th percentile{ ipv4.peer_histogram.p70 }
80th percentile{ ipv4.peer_histogram.p80 }
90th percentile{ ipv4.peer_histogram.p90 }
95th percentile{ ipv4.peer_histogram.p95 }
99th percentile{ ipv4.peer_histogram.p99 }
99.9th percentile{ ipv4.peer_histogram.p999 }
Maximum{ ipv4.peer_histogram.max }
+ + {{ endif }} + + {{ endif }} + + {{ if ipv6_active }} + +

IPv6

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
* Torrent/peer count is updated every { peer_update_interval } seconds
Number of torrents{ ipv6.num_torrents } *
Number of peers{ ipv6.num_peers } *
Requests / second{ ipv6.requests_per_second }
Total responses / second{ ipv6.responses_per_second_total }
Connect responses / second{ ipv6.responses_per_second_connect }
Announce responses / second{ ipv6.responses_per_second_announce }
Scrape responses / second{ ipv6.responses_per_second_scrape }
Error responses / second{ ipv6.responses_per_second_error }
Bandwidth (RX){ ipv6.rx_mbits } mbit/s
Bandwidth (TX){ ipv6.tx_mbits } mbit/s
+ + {{ if extended_active }} + +

Peers per torrent

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Updated every { peer_update_interval } seconds
Minimum{ ipv6.peer_histogram.min }
10th percentile{ ipv6.peer_histogram.p10 }
20th percentile{ ipv6.peer_histogram.p20 }
30th percentile{ ipv6.peer_histogram.p30 }
40th percentile{ ipv6.peer_histogram.p40 }
50th percentile{ ipv6.peer_histogram.p50 }
60th percentile{ ipv6.peer_histogram.p60 }
70th percentile{ ipv6.peer_histogram.p70 }
80th percentile{ ipv6.peer_histogram.p80 }
90th percentile{ ipv6.peer_histogram.p90 }
95th percentile{ ipv6.peer_histogram.p95 }
99th percentile{ ipv6.peer_histogram.p99 }
99.9th percentile{ ipv6.peer_histogram.p999 }
Maximum{ ipv6.peer_histogram.max }
+ + {{ endif }} + + {{ endif }} + + {{ if extended_active }} + +

Peer clients

+ + + + + + + + + + {{ for value in peer_clients }} + + + + + {{ endfor }} + +
ClientCount
{ value.0 }{ value.1 }
+ + {{ endif }} + + diff --git a/apps/aquatic/crates/udp/tests/access_list.rs b/apps/aquatic/crates/udp/tests/access_list.rs new file mode 100644 index 0000000..dab3e0d --- /dev/null +++ b/apps/aquatic/crates/udp/tests/access_list.rs @@ -0,0 +1,110 @@ +mod common; + +use common::*; + +use std::{ + fs::File, + io::Write, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, + num::NonZeroU16, + time::Duration, +}; + +use anyhow::Context; +use aquatic_common::access_list::AccessListMode; +use aquatic_udp::config::Config; +use aquatic_udp_protocol::{InfoHash, Response}; + +#[test] +fn test_access_list_deny() -> anyhow::Result<()> { + const TRACKER_PORT: u16 = 40_113; + + let deny = InfoHash([0; 20]); + let allow = InfoHash([1; 20]); + + test_access_list(TRACKER_PORT, allow, deny, deny, AccessListMode::Deny)?; + + Ok(()) +} + +#[test] +fn test_access_list_allow() -> anyhow::Result<()> { + const TRACKER_PORT: u16 = 40_114; + + let allow = InfoHash([0; 20]); + let deny = InfoHash([1; 20]); + + test_access_list(TRACKER_PORT, allow, deny, allow, AccessListMode::Allow)?; + + Ok(()) +} + +fn test_access_list( + tracker_port: u16, + info_hash_success: InfoHash, + info_hash_fail: InfoHash, + info_hash_in_list: InfoHash, + mode: AccessListMode, +) -> anyhow::Result<()> { + let access_list_dir = tempfile::tempdir().with_context(|| "get temporary directory")?; + let access_list_path = access_list_dir.path().join("access-list.txt"); + + let mut access_list_file = + File::create(&access_list_path).with_context(|| "create access list file")?; + writeln!( + access_list_file, + "{}", + hex::encode_upper(info_hash_in_list.0) + ) + .with_context(|| "write to access list file")?; + + let mut config = Config::default(); + + config.network.address_ipv4.set_port(tracker_port); + config.network.use_ipv6 = false; + + config.access_list.mode = mode; + config.access_list.path = access_list_path; + + run_tracker(config); + + let tracker_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, tracker_port)); + let peer_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)); + + let socket = UdpSocket::bind(peer_addr)?; + socket.set_read_timeout(Some(Duration::from_secs(1)))?; + + let connection_id = connect(&socket, tracker_addr).with_context(|| "connect")?; + + let response = announce( + &socket, + tracker_addr, + connection_id, + NonZeroU16::new(1).unwrap(), + info_hash_fail, + 10, + false, + ) + .with_context(|| "announce")?; + + assert!( + matches!(response, Response::Error(_)), + "response should be error but is {:?}", + response + ); + + let response = announce( + &socket, + tracker_addr, + connection_id, + NonZeroU16::new(1).unwrap(), + info_hash_success, + 10, + false, + ) + .with_context(|| "announce")?; + + assert!(matches!(response, Response::AnnounceIpv4(_))); + + Ok(()) +} diff --git a/apps/aquatic/crates/udp/tests/common/mod.rs b/apps/aquatic/crates/udp/tests/common/mod.rs new file mode 100644 index 0000000..7fbec29 --- /dev/null +++ b/apps/aquatic/crates/udp/tests/common/mod.rs @@ -0,0 +1,125 @@ +#![allow(dead_code)] + +use std::{ + io::Cursor, + net::{SocketAddr, UdpSocket}, + num::NonZeroU16, + time::Duration, +}; + +use anyhow::Context; +use aquatic_udp::{common::BUFFER_SIZE, config::Config}; +use aquatic_udp_protocol::{ + common::PeerId, AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, InfoHash, + Ipv4AddrBytes, NumberOfBytes, NumberOfPeers, PeerKey, Port, Request, Response, ScrapeRequest, + ScrapeResponse, TransactionId, +}; + +// FIXME: should ideally try different ports and use sync primitives to find +// out if tracker was successfully started +pub fn run_tracker(config: Config) { + ::std::thread::spawn(move || { + aquatic_udp::run(config).unwrap(); + }); + + ::std::thread::sleep(Duration::from_secs(1)); +} + +pub fn connect(socket: &UdpSocket, tracker_addr: SocketAddr) -> anyhow::Result { + let request = Request::Connect(ConnectRequest { + transaction_id: TransactionId::new(0), + }); + + let response = request_and_response(socket, tracker_addr, request)?; + + if let Response::Connect(response) = response { + Ok(response.connection_id) + } else { + Err(anyhow::anyhow!("not connect response: {:?}", response)) + } +} + +pub fn announce( + socket: &UdpSocket, + tracker_addr: SocketAddr, + connection_id: ConnectionId, + peer_port: NonZeroU16, + info_hash: InfoHash, + peers_wanted: usize, + seeder: bool, +) -> anyhow::Result { + let mut peer_id = PeerId([0; 20]); + + for chunk in peer_id.0.chunks_exact_mut(2) { + chunk.copy_from_slice(&peer_port.get().to_ne_bytes()); + } + + let request = Request::Announce(AnnounceRequest { + connection_id, + action_placeholder: Default::default(), + transaction_id: TransactionId::new(0), + info_hash, + peer_id, + bytes_downloaded: NumberOfBytes::new(0), + bytes_uploaded: NumberOfBytes::new(0), + bytes_left: NumberOfBytes::new(if seeder { 0 } else { 1 }), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4AddrBytes([0; 4]), + key: PeerKey::new(0), + peers_wanted: NumberOfPeers::new(peers_wanted as i32), + port: Port::new(peer_port), + }); + + request_and_response(socket, tracker_addr, request) +} + +pub fn scrape( + socket: &UdpSocket, + tracker_addr: SocketAddr, + connection_id: ConnectionId, + info_hashes: Vec, +) -> anyhow::Result { + let request = Request::Scrape(ScrapeRequest { + connection_id, + transaction_id: TransactionId::new(0), + info_hashes, + }); + + let response = request_and_response(socket, tracker_addr, request)?; + + if let Response::Scrape(response) = response { + Ok(response) + } else { + Err(anyhow::anyhow!("not scrape response: {:?}", response)) + } +} + +pub fn request_and_response( + socket: &UdpSocket, + tracker_addr: SocketAddr, + request: Request, +) -> anyhow::Result { + let mut buffer = [0u8; BUFFER_SIZE]; + + { + let mut buffer = Cursor::new(&mut buffer[..]); + + request + .write_bytes(&mut buffer) + .with_context(|| "write request")?; + + let bytes_written = buffer.position() as usize; + + socket + .send_to(&(buffer.into_inner())[..bytes_written], tracker_addr) + .with_context(|| "send request")?; + } + + { + let (bytes_read, _) = socket + .recv_from(&mut buffer) + .with_context(|| "recv response")?; + + Response::parse_bytes(&buffer[..bytes_read], true).with_context(|| "parse response") + } +} diff --git a/apps/aquatic/crates/udp/tests/invalid_connection_id.rs b/apps/aquatic/crates/udp/tests/invalid_connection_id.rs new file mode 100644 index 0000000..c3a6cd6 --- /dev/null +++ b/apps/aquatic/crates/udp/tests/invalid_connection_id.rs @@ -0,0 +1,97 @@ +mod common; + +use common::*; + +use std::{ + io::{Cursor, ErrorKind}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, + num::NonZeroU16, + time::Duration, +}; + +use anyhow::Context; +use aquatic_udp::{common::BUFFER_SIZE, config::Config}; +use aquatic_udp_protocol::{ + common::PeerId, AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, Ipv4AddrBytes, + NumberOfBytes, NumberOfPeers, PeerKey, Port, Request, ScrapeRequest, TransactionId, +}; + +#[test] +fn test_invalid_connection_id() -> anyhow::Result<()> { + const TRACKER_PORT: u16 = 40_112; + + let mut config = Config::default(); + + config.network.address_ipv4.set_port(TRACKER_PORT); + config.network.use_ipv6 = false; + + run_tracker(config); + + let tracker_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, TRACKER_PORT)); + let peer_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)); + + let socket = UdpSocket::bind(peer_addr)?; + + socket.set_read_timeout(Some(Duration::from_secs(1)))?; + + // Send connect request to make sure that the tracker in fact responds to + // valid requests + let connection_id = connect(&socket, tracker_addr).with_context(|| "connect")?; + + let invalid_connection_id = ConnectionId(!connection_id.0); + + let announce_request = Request::Announce(AnnounceRequest { + connection_id: invalid_connection_id, + action_placeholder: Default::default(), + transaction_id: TransactionId::new(0), + info_hash: InfoHash([0; 20]), + peer_id: PeerId([0; 20]), + bytes_downloaded: NumberOfBytes::new(0), + bytes_uploaded: NumberOfBytes::new(0), + bytes_left: NumberOfBytes::new(0), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4AddrBytes([0; 4]), + key: PeerKey::new(0), + peers_wanted: NumberOfPeers::new(10), + port: Port::new(NonZeroU16::new(1).unwrap()), + }); + + let scrape_request = Request::Scrape(ScrapeRequest { + connection_id: invalid_connection_id, + transaction_id: TransactionId::new(0), + info_hashes: vec![InfoHash([0; 20])], + }); + + no_response(&socket, tracker_addr, announce_request).with_context(|| "announce")?; + no_response(&socket, tracker_addr, scrape_request).with_context(|| "scrape")?; + + Ok(()) +} + +fn no_response( + socket: &UdpSocket, + tracker_addr: SocketAddr, + request: Request, +) -> anyhow::Result<()> { + let mut buffer = [0u8; BUFFER_SIZE]; + + { + let mut buffer = Cursor::new(&mut buffer[..]); + + request + .write_bytes(&mut buffer) + .with_context(|| "write request")?; + + let bytes_written = buffer.position() as usize; + + socket + .send_to(&(buffer.into_inner())[..bytes_written], tracker_addr) + .with_context(|| "send request")?; + } + + match socket.recv_from(&mut buffer) { + Ok(_) => Err(anyhow::anyhow!("received response")), + Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(()), + Err(err) => Err(err.into()), + } +} diff --git a/apps/aquatic/crates/udp/tests/requests_responses.rs b/apps/aquatic/crates/udp/tests/requests_responses.rs new file mode 100644 index 0000000..d00163e --- /dev/null +++ b/apps/aquatic/crates/udp/tests/requests_responses.rs @@ -0,0 +1,108 @@ +mod common; + +use common::*; + +use std::{ + collections::{hash_map::RandomState, HashSet}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, + num::NonZeroU16, + time::Duration, +}; + +use anyhow::Context; +use aquatic_udp::config::Config; +use aquatic_udp_protocol::{InfoHash, Response}; + +#[test] +fn test_multiple_connect_announce_scrape() -> anyhow::Result<()> { + const TRACKER_PORT: u16 = 40_111; + const PEER_PORT_START: u16 = 30_000; + const PEERS_WANTED: usize = 10; + + let mut config = Config::default(); + + config.network.address_ipv4.set_port(TRACKER_PORT); + config.network.use_ipv6 = false; + + run_tracker(config); + + let tracker_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, TRACKER_PORT)); + let peer_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)); + + let info_hash = InfoHash([0; 20]); + + let mut num_seeders = 0; + let mut num_leechers = 0; + + for i in 0..20 { + let is_seeder = i % 3 == 0; + + let socket = UdpSocket::bind(peer_addr)?; + socket.set_read_timeout(Some(Duration::from_secs(1)))?; + + let connection_id = connect(&socket, tracker_addr).with_context(|| "connect")?; + + let announce_response = { + let response = announce( + &socket, + tracker_addr, + connection_id, + NonZeroU16::new(PEER_PORT_START + i as u16).unwrap(), + info_hash, + PEERS_WANTED, + is_seeder, + ) + .with_context(|| "announce")?; + + if let Response::AnnounceIpv4(response) = response { + response + } else { + return Err(anyhow::anyhow!("not announce response: {:?}", response)); + } + }; + + assert_eq!(announce_response.peers.len(), i.min(PEERS_WANTED)); + + assert_eq!(announce_response.fixed.seeders.0.get(), num_seeders); + assert_eq!(announce_response.fixed.leechers.0.get(), num_leechers); + + let response_peer_ports: HashSet = + HashSet::from_iter(announce_response.peers.iter().map(|p| p.port.0.get())); + let expected_peer_ports: HashSet = + HashSet::from_iter((0..i).map(|i| PEER_PORT_START + i as u16)); + + if i > PEERS_WANTED { + assert!(response_peer_ports.is_subset(&expected_peer_ports)); + } else { + assert_eq!(response_peer_ports, expected_peer_ports); + } + + // Do this after announce is evaluated, since it is expected not to include announcing peer + if is_seeder { + num_seeders += 1; + } else { + num_leechers += 1; + } + + let scrape_response = scrape( + &socket, + tracker_addr, + connection_id, + vec![info_hash, InfoHash([1; 20])], + ) + .with_context(|| "scrape")?; + + assert_eq!( + scrape_response.torrent_stats[0].seeders.0.get(), + num_seeders + ); + assert_eq!( + scrape_response.torrent_stats[0].leechers.0.get(), + num_leechers + ); + assert_eq!(scrape_response.torrent_stats[1].seeders.0.get(), 0); + assert_eq!(scrape_response.torrent_stats[1].leechers.0.get(), 0); + } + + Ok(()) +} diff --git a/apps/aquatic/crates/udp_load_test/Cargo.toml b/apps/aquatic/crates/udp_load_test/Cargo.toml new file mode 100644 index 0000000..4445030 --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "aquatic_udp_load_test" +description = "BitTorrent (UDP) load tester" +keywords = ["udp", "benchmark", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "README.md" + +[features] +cpu-pinning = ["aquatic_common/cpu-pinning"] + +[lib] +name = "aquatic_udp_load_test" + +[[bin]] +name = "aquatic_udp_load_test" + +[dependencies] +aquatic_common.workspace = true +aquatic_toml_config.workspace = true +aquatic_udp_protocol.workspace = true + +anyhow = "1" +crossbeam-channel = "0.5" +hdrhistogram = "7" +mimalloc = { version = "0.1", default-features = false } +rand_distr = "0.4" +rand = { version = "0.8", features = ["small_rng"] } +serde = { version = "1", features = ["derive"] } +socket2 = { version = "0.5", features = ["all"] } + +[dev-dependencies] +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/udp_load_test/README.md b/apps/aquatic/crates/udp_load_test/README.md new file mode 100644 index 0000000..db10b5d --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/README.md @@ -0,0 +1,49 @@ +# aquatic_udp_load_test: UDP BitTorrent tracker load tester + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +High-performance load tester for UDP BitTorrent trackers, for Unix-like operating systems. + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_udp_load_test +``` + +### Configuring and running + +Generate the configuration file: + +```sh +./target/release/aquatic_udp_load_test -p > "load-test-config.toml" +``` + +Make necessary adjustments to the file. + +Once done, first start the tracker application that you want to test. Then, +start the load tester: + +```sh +./target/release/aquatic_udp_load_test -c "load-test-config.toml" +``` + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. diff --git a/apps/aquatic/crates/udp_load_test/src/common.rs b/apps/aquatic/crates/udp_load_test/src/common.rs new file mode 100644 index 0000000..847a24c --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/src/common.rs @@ -0,0 +1,32 @@ +use std::sync::{atomic::AtomicUsize, Arc}; + +use aquatic_common::IndexMap; +use aquatic_udp_protocol::*; + +#[derive(Clone)] +pub struct LoadTestState { + pub info_hashes: Arc<[InfoHash]>, + pub statistics: Arc, +} + +#[derive(Default)] +pub struct SharedStatistics { + pub requests: AtomicUsize, + pub response_peers: AtomicUsize, + pub responses_connect: AtomicUsize, + pub responses_announce: AtomicUsize, + pub responses_scrape: AtomicUsize, + pub responses_error: AtomicUsize, +} + +pub struct Peer { + pub announce_info_hash_index: usize, + pub announce_info_hash: InfoHash, + pub announce_port: Port, + pub scrape_info_hash_indices: Box<[usize]>, + pub socket_index: u8, +} + +pub enum StatisticsMessage { + ResponsesPerInfoHash(IndexMap), +} diff --git a/apps/aquatic/crates/udp_load_test/src/config.rs b/apps/aquatic/crates/udp_load_test/src/config.rs new file mode 100644 index 0000000..2316443 --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/src/config.rs @@ -0,0 +1,140 @@ +use std::net::SocketAddr; + +use serde::{Deserialize, Serialize}; + +use aquatic_common::cli::LogLevel; +#[cfg(feature = "cpu-pinning")] +use aquatic_common::cpu_pinning::desc::CpuPinningConfigDesc; +use aquatic_toml_config::TomlConfig; + +/// aquatic_udp_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + /// Server address + /// + /// If you want to send IPv4 requests to a IPv4+IPv6 tracker, put an IPv4 + /// address here. + pub server_address: SocketAddr, + pub log_level: LogLevel, + /// Number of workers sending requests + pub workers: u8, + /// Run duration (quit and generate report after this many seconds) + pub duration: usize, + /// Only report summary for the last N seconds of run + /// + /// 0 = include whole run + pub summarize_last: usize, + /// Display extra statistics + pub extra_statistics: bool, + pub network: NetworkConfig, + pub requests: RequestConfig, + #[cfg(feature = "cpu-pinning")] + pub cpu_pinning: CpuPinningConfigDesc, +} + +impl Default for Config { + fn default() -> Self { + Self { + server_address: "127.0.0.1:3000".parse().unwrap(), + log_level: LogLevel::Error, + workers: 1, + duration: 0, + summarize_last: 0, + extra_statistics: true, + network: NetworkConfig::default(), + requests: RequestConfig::default(), + #[cfg(feature = "cpu-pinning")] + cpu_pinning: Default::default(), + } + } +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct NetworkConfig { + /// 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. + pub multiple_client_ipv4s: bool, + /// Number of sockets to open per worker + pub sockets_per_worker: u8, + /// Size of socket recv buffer. Use 0 for OS default. + /// + /// This setting can have a big impact on dropped packages. 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 + pub recv_buffer: usize, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + multiple_client_ipv4s: true, + sockets_per_worker: 4, + recv_buffer: 8_000_000, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct RequestConfig { + /// Number of torrents to simulate + pub number_of_torrents: usize, + /// Number of peers to simulate + pub number_of_peers: usize, + /// Maximum number of torrents to ask about in scrape requests + pub scrape_max_torrents: usize, + /// Ask for this number of peers in announce requests + pub announce_peers_wanted: i32, + /// Probability that a generated request is a connect request as part + /// of sum of the various weight arguments. + pub weight_connect: usize, + /// Probability that a generated request is a announce request, as part + /// of sum of the various weight arguments. + pub weight_announce: usize, + /// Probability that a generated request is a scrape request, as part + /// of sum of the various weight arguments. + pub weight_scrape: usize, + /// Probability that a generated peer is a seeder + pub peer_seeder_probability: f64, +} + +impl Default for RequestConfig { + fn default() -> Self { + Self { + number_of_torrents: 1_000_000, + number_of_peers: 2_000_000, + scrape_max_torrents: 10, + announce_peers_wanted: 30, + weight_connect: 50, + weight_announce: 50, + weight_scrape: 1, + peer_seeder_probability: 0.75, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/udp_load_test/src/lib.rs b/apps/aquatic/crates/udp_load_test/src/lib.rs new file mode 100644 index 0000000..7bef0e5 --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/src/lib.rs @@ -0,0 +1,336 @@ +use std::iter::repeat_with; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::atomic::AtomicUsize; +use std::sync::{atomic::Ordering, Arc}; +use std::thread::{self, Builder}; +use std::time::{Duration, Instant}; + +use aquatic_common::IndexMap; +use aquatic_udp_protocol::{InfoHash, Port}; +use crossbeam_channel::{unbounded, Receiver}; +use hdrhistogram::Histogram; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use rand_distr::{Distribution, WeightedAliasIndex}; + +mod common; +pub mod config; +mod worker; + +use common::*; +use config::Config; +use worker::*; + +const PERCENTILES: &[f64] = &[10.0, 25.0, 50.0, 75.0, 90.0, 95.0, 99.0, 99.9, 100.0]; + +pub fn run(config: Config) -> ::anyhow::Result<()> { + if config.requests.weight_announce + + config.requests.weight_connect + + config.requests.weight_scrape + == 0 + { + panic!("Error: at least one weight must be larger than zero."); + } + + if config.summarize_last > config.duration { + panic!("Error: report_last_seconds can't be larger than duration"); + } + + println!("Starting client with config: {:#?}\n", config); + + let info_hash_dist = InfoHashDist::new(&config)?; + let peers_by_worker = create_peers(&config, &info_hash_dist); + + let state = LoadTestState { + info_hashes: info_hash_dist.into_arc_info_hashes(), + statistics: Arc::new(SharedStatistics::default()), + }; + + let (statistics_sender, statistics_receiver) = unbounded(); + + // Start workers + + for (i, peers) in (0..config.workers).zip(peers_by_worker) { + let ip = if config.server_address.is_ipv6() { + Ipv6Addr::LOCALHOST.into() + } else if config.network.multiple_client_ipv4s { + Ipv4Addr::new(127, 0, 0, 1 + i).into() + } else { + Ipv4Addr::LOCALHOST.into() + }; + + let addr = SocketAddr::new(ip, 0); + let config = config.clone(); + let state = state.clone(); + let statistics_sender = statistics_sender.clone(); + + Builder::new() + .name("load-test".into()) + .spawn(move || Worker::run(config, state, statistics_sender, peers, addr))?; + } + + monitor_statistics(state, &config, statistics_receiver); + + Ok(()) +} + +fn monitor_statistics( + state: LoadTestState, + config: &Config, + statistics_receiver: Receiver, +) { + let mut report_avg_connect: Vec = Vec::new(); + let mut report_avg_announce: Vec = Vec::new(); + let mut report_avg_scrape: Vec = Vec::new(); + let mut report_avg_error: Vec = Vec::new(); + + const INTERVAL: u64 = 5; + + let start_time = Instant::now(); + let duration = Duration::from_secs(config.duration as u64); + + let mut last = start_time; + + let time_elapsed = loop { + thread::sleep(Duration::from_secs(INTERVAL)); + + let mut opt_responses_per_info_hash: Option> = + config.extra_statistics.then_some(Default::default()); + + for message in statistics_receiver.try_iter() { + match message { + StatisticsMessage::ResponsesPerInfoHash(data) => { + if let Some(responses_per_info_hash) = opt_responses_per_info_hash.as_mut() { + for (k, v) in data { + *responses_per_info_hash.entry(k).or_default() += v; + } + } + } + } + } + + let requests = fetch_and_reset(&state.statistics.requests); + let response_peers = fetch_and_reset(&state.statistics.response_peers); + let responses_connect = fetch_and_reset(&state.statistics.responses_connect); + let responses_announce = fetch_and_reset(&state.statistics.responses_announce); + let responses_scrape = fetch_and_reset(&state.statistics.responses_scrape); + let responses_error = fetch_and_reset(&state.statistics.responses_error); + + let now = Instant::now(); + + let elapsed = (now - last).as_secs_f64(); + + last = now; + + let peers_per_announce_response = response_peers / responses_announce; + + let avg_requests = requests / elapsed; + let avg_responses_connect = responses_connect / elapsed; + let avg_responses_announce = responses_announce / elapsed; + let avg_responses_scrape = responses_scrape / elapsed; + let avg_responses_error = responses_error / elapsed; + + let avg_responses = avg_responses_connect + + avg_responses_announce + + avg_responses_scrape + + avg_responses_error; + + report_avg_connect.push(avg_responses_connect); + report_avg_announce.push(avg_responses_announce); + report_avg_scrape.push(avg_responses_scrape); + report_avg_error.push(avg_responses_error); + + println!(); + println!("Requests out: {:.2}/second", avg_requests); + println!("Responses in: {:.2}/second", avg_responses); + println!(" - Connect responses: {:.2}", avg_responses_connect); + println!(" - Announce responses: {:.2}", avg_responses_announce); + println!(" - Scrape responses: {:.2}", avg_responses_scrape); + println!(" - Error responses: {:.2}", avg_responses_error); + println!( + "Peers per announce response: {:.2}", + peers_per_announce_response + ); + + if let Some(responses_per_info_hash) = opt_responses_per_info_hash.as_ref() { + let mut histogram = Histogram::::new(2).unwrap(); + + for num_responses in responses_per_info_hash.values().copied() { + histogram.record(num_responses).unwrap(); + } + + println!("Announce responses per info hash:"); + + for p in PERCENTILES { + println!(" - p{}: {}", p, histogram.value_at_percentile(*p)); + } + } + + let time_elapsed = start_time.elapsed(); + + if config.duration != 0 && time_elapsed >= duration { + break time_elapsed; + } + }; + + if config.summarize_last != 0 { + let split_at = (config.duration - config.summarize_last) / INTERVAL as usize; + + report_avg_connect = report_avg_connect.split_off(split_at); + report_avg_announce = report_avg_announce.split_off(split_at); + report_avg_scrape = report_avg_scrape.split_off(split_at); + report_avg_error = report_avg_error.split_off(split_at); + } + + let len = report_avg_connect.len() as f64; + + let avg_connect: f64 = report_avg_connect.into_iter().sum::() / len; + let avg_announce: f64 = report_avg_announce.into_iter().sum::() / len; + let avg_scrape: f64 = report_avg_scrape.into_iter().sum::() / len; + let avg_error: f64 = report_avg_error.into_iter().sum::() / len; + + let avg_total = avg_connect + avg_announce + avg_scrape + avg_error; + + println!(); + println!("# aquatic load test report"); + println!(); + println!( + "Test ran for {} seconds {}", + time_elapsed.as_secs(), + if config.summarize_last != 0 { + format!("(only last {} included in summary)", config.summarize_last) + } else { + "".to_string() + } + ); + println!("Average responses per second: {:.2}", avg_total); + println!(" - Connect responses: {:.2}", avg_connect); + println!(" - Announce responses: {:.2}", avg_announce); + println!(" - Scrape responses: {:.2}", avg_scrape); + println!(" - Error responses: {:.2}", avg_error); + println!(); + println!("Config: {:#?}", config); + println!(); +} + +fn fetch_and_reset(atomic_usize: &AtomicUsize) -> f64 { + atomic_usize.fetch_and(0, Ordering::Relaxed) as f64 +} + +fn create_peers(config: &Config, info_hash_dist: &InfoHashDist) -> Vec> { + let mut rng = SmallRng::seed_from_u64(0xc3a58be617b3acce); + + let mut opt_peers_per_info_hash: Option> = + config.extra_statistics.then_some(IndexMap::default()); + + let mut all_peers = repeat_with(|| { + let num_scrape_indices = rng.gen_range(1..config.requests.scrape_max_torrents + 1); + + let scrape_info_hash_indices = repeat_with(|| info_hash_dist.get_random_index(&mut rng)) + .take(num_scrape_indices) + .collect::>() + .into_boxed_slice(); + + let (announce_info_hash_index, announce_info_hash) = info_hash_dist.get_random(&mut rng); + + if let Some(peers_per_info_hash) = opt_peers_per_info_hash.as_mut() { + *peers_per_info_hash + .entry(announce_info_hash_index) + .or_default() += 1; + } + + Peer { + announce_info_hash_index, + announce_info_hash, + announce_port: Port::new(rng.gen()), + scrape_info_hash_indices, + socket_index: rng.gen_range(0..config.network.sockets_per_worker), + } + }) + .take(config.requests.number_of_peers) + .collect::>(); + + if let Some(peers_per_info_hash) = opt_peers_per_info_hash { + println!("Number of info hashes: {}", peers_per_info_hash.len()); + + let mut histogram = Histogram::::new(2).unwrap(); + + for num_peers in peers_per_info_hash.values() { + histogram.record(*num_peers).unwrap(); + } + + println!("Peers per info hash:"); + + for p in PERCENTILES { + println!(" - p{}: {}", p, histogram.value_at_percentile(*p)); + } + } + + let mut peers_by_worker = Vec::new(); + + let num_peers_per_worker = all_peers.len() / config.workers as usize; + + for _ in 0..(config.workers as usize) { + peers_by_worker.push( + all_peers + .split_off(all_peers.len() - num_peers_per_worker) + .into_boxed_slice(), + ); + + all_peers.shrink_to_fit(); + } + + peers_by_worker +} + +struct InfoHashDist { + info_hashes: Box<[InfoHash]>, + dist: WeightedAliasIndex, +} + +impl InfoHashDist { + fn new(config: &Config) -> anyhow::Result { + let mut rng = SmallRng::seed_from_u64(0xc3aa8be617b3acce); + + let info_hashes = repeat_with(|| { + let mut bytes = [0u8; 20]; + + for byte in bytes.iter_mut() { + *byte = rng.gen(); + } + + InfoHash(bytes) + }) + .take(config.requests.number_of_torrents) + .collect::>() + .into_boxed_slice(); + + let num_torrents = config.requests.number_of_torrents as u32; + + let weights = (0..num_torrents) + .map(|i| { + let floor = num_torrents as f64 / config.requests.number_of_peers as f64; + + floor + (6.5f64 - ((500.0 * f64::from(i)) / f64::from(num_torrents))).exp() + }) + .collect(); + + let dist = WeightedAliasIndex::new(weights)?; + + Ok(Self { info_hashes, dist }) + } + + fn get_random(&self, rng: &mut impl Rng) -> (usize, InfoHash) { + let index = self.dist.sample(rng); + + (index, self.info_hashes[index]) + } + + fn get_random_index(&self, rng: &mut impl Rng) -> usize { + self.dist.sample(rng) + } + + fn into_arc_info_hashes(self) -> Arc<[InfoHash]> { + Arc::from(self.info_hashes) + } +} diff --git a/apps/aquatic/crates/udp_load_test/src/main.rs b/apps/aquatic/crates/udp_load_test/src/main.rs new file mode 100644 index 0000000..74c76b8 --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/src/main.rs @@ -0,0 +1,13 @@ +use aquatic_udp_load_test::config::Config; + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +pub fn main() { + aquatic_common::cli::run_app_with_cli_and_config::( + "aquatic_udp_load_test: BitTorrent load tester", + env!("CARGO_PKG_VERSION"), + aquatic_udp_load_test::run, + None, + ) +} diff --git a/apps/aquatic/crates/udp_load_test/src/worker.rs b/apps/aquatic/crates/udp_load_test/src/worker.rs new file mode 100644 index 0000000..570b1bb --- /dev/null +++ b/apps/aquatic/crates/udp_load_test/src/worker.rs @@ -0,0 +1,439 @@ +use std::io::{Cursor, ErrorKind}; +use std::net::{SocketAddr, UdpSocket}; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use aquatic_common::IndexMap; +use crossbeam_channel::Sender; +use rand::Rng; +use rand::{prelude::SmallRng, SeedableRng}; +use rand_distr::{Distribution, WeightedIndex}; +use socket2::{Domain, Protocol, Socket, Type}; + +use aquatic_udp_protocol::*; + +use crate::common::{LoadTestState, Peer}; +use crate::config::Config; +use crate::StatisticsMessage; + +const MAX_PACKET_SIZE: usize = 8192; + +pub struct Worker { + config: Config, + shared_state: LoadTestState, + peers: Box<[Peer]>, + request_type_dist: RequestTypeDist, + addr: SocketAddr, + sockets: Vec, + buffer: [u8; MAX_PACKET_SIZE], + rng: SmallRng, + statistics: LocalStatistics, + statistics_sender: Sender, + announce_responses_per_info_hash: IndexMap, +} + +impl Worker { + pub fn run( + config: Config, + shared_state: LoadTestState, + statistics_sender: Sender, + peers: Box<[Peer]>, + addr: SocketAddr, + ) { + let mut sockets = Vec::new(); + + for _ in 0..config.network.sockets_per_worker { + sockets.push(create_socket(&config, addr)); + } + + let buffer = [0u8; MAX_PACKET_SIZE]; + let rng = SmallRng::seed_from_u64(0xc3aa8be617b3acce); + let statistics = LocalStatistics::default(); + let request_type_dist = RequestTypeDist::new(&config).unwrap(); + + let mut instance = Self { + config, + shared_state, + peers, + request_type_dist, + addr, + sockets, + buffer, + rng, + statistics, + statistics_sender, + announce_responses_per_info_hash: Default::default(), + }; + + instance.run_inner(); + } + + fn run_inner(&mut self) { + let mut connection_ids = Vec::new(); + + for _ in 0..self.config.network.sockets_per_worker { + connection_ids.push(self.acquire_connection_id()); + } + + let mut requests_sent = 0usize; + let mut responses_received = 0usize; + + let mut connect_socket_index = 0u8; + let mut peer_index = 0usize; + let mut loop_index = 0usize; + + loop { + let response_ratio = responses_received as f64 / requests_sent.max(1) as f64; + + if response_ratio >= 0.90 || requests_sent == 0 || self.rng.gen::() == 0 { + for _ in 0..self.sockets.len() { + match self.request_type_dist.sample(&mut self.rng) { + RequestType::Connect => { + self.send_connect_request( + connect_socket_index, + connect_socket_index.into(), + ); + + connect_socket_index = connect_socket_index.wrapping_add(1) + % self.config.network.sockets_per_worker; + } + RequestType::Announce => { + self.send_announce_request(&connection_ids, peer_index); + + peer_index = (peer_index + 1) % self.peers.len(); + } + RequestType::Scrape => { + self.send_scrape_request(&connection_ids, peer_index); + + peer_index = (peer_index + 1) % self.peers.len(); + } + } + + requests_sent += 1; + } + } + + for socket_index in 0..self.sockets.len() { + // Do this instead of iterating over Vec to fix borrow checker complaint + let socket = self.sockets.get(socket_index).unwrap(); + + match socket.recv(&mut self.buffer[..]) { + Ok(amt) => { + match Response::parse_bytes(&self.buffer[0..amt], self.addr.is_ipv4()) { + Ok(Response::Connect(r)) => { + // If we're sending connect requests, we might + // as well keep connection IDs valid + let connection_id_index = + u32::from_ne_bytes(r.transaction_id.0.get().to_ne_bytes()) + as usize; + connection_ids[connection_id_index] = r.connection_id; + + self.handle_response(Response::Connect(r)); + } + Ok(response) => { + self.handle_response(response); + } + Err(err) => { + eprintln!("Received invalid response: {:#?}", err); + } + } + + responses_received += 1; + } + Err(err) if err.kind() == ErrorKind::WouldBlock => (), + Err(err) => { + eprintln!("recv error: {:#}", err); + } + } + } + + if loop_index % 1024 == 0 { + self.update_shared_statistics(); + } + + loop_index = loop_index.wrapping_add(1); + } + } + + fn acquire_connection_id(&mut self) -> ConnectionId { + loop { + self.send_connect_request(0, u32::MAX); + + for _ in 0..100 { + match self.sockets[0].recv(&mut self.buffer[..]) { + Ok(amt) => { + match Response::parse_bytes(&self.buffer[0..amt], self.addr.is_ipv4()) { + Ok(Response::Connect(r)) => { + return r.connection_id; + } + Ok(r) => { + eprintln!("Received non-connect response: {:?}", r); + } + Err(err) => { + eprintln!("Received invalid response: {:#?}", err); + } + } + } + Err(err) if err.kind() == ErrorKind::WouldBlock => { + ::std::thread::sleep(Duration::from_millis(10)); + } + Err(err) => { + eprintln!("recv error: {:#}", err); + } + }; + } + } + } + + fn send_connect_request(&mut self, socket_index: u8, transaction_id: u32) { + let transaction_id = TransactionId::new(i32::from_ne_bytes(transaction_id.to_ne_bytes())); + + let request = ConnectRequest { transaction_id }; + + let mut cursor = Cursor::new(self.buffer); + + request.write_bytes(&mut cursor).unwrap(); + + let position = cursor.position() as usize; + + match self.sockets[socket_index as usize].send(&cursor.get_ref()[..position]) { + Ok(_) => { + self.statistics.requests += 1; + } + Err(err) => { + eprintln!("Couldn't send packet: {:?}", err); + } + } + } + + fn send_announce_request(&mut self, connection_ids: &[ConnectionId], peer_index: usize) { + let peer = self.peers.get(peer_index).unwrap(); + + let (event, bytes_left) = { + if self + .rng + .gen_bool(self.config.requests.peer_seeder_probability) + { + (AnnounceEvent::Completed, NumberOfBytes::new(0)) + } else { + (AnnounceEvent::Started, NumberOfBytes::new(50)) + } + }; + + let transaction_id = + TransactionId::new(i32::from_ne_bytes((peer_index as u32).to_ne_bytes())); + + let request = AnnounceRequest { + connection_id: connection_ids[peer.socket_index as usize], + action_placeholder: Default::default(), + transaction_id, + info_hash: peer.announce_info_hash, + peer_id: PeerId([0; 20]), + bytes_downloaded: NumberOfBytes::new(50), + bytes_uploaded: NumberOfBytes::new(50), + bytes_left, + event: event.into(), + ip_address: Ipv4AddrBytes([0; 4]), + key: PeerKey::new(0), + peers_wanted: NumberOfPeers::new(self.config.requests.announce_peers_wanted), + port: peer.announce_port, + }; + + let mut cursor = Cursor::new(self.buffer); + + request.write_bytes(&mut cursor).unwrap(); + + let position = cursor.position() as usize; + + match self.sockets[peer.socket_index as usize].send(&cursor.get_ref()[..position]) { + Ok(_) => { + self.statistics.requests += 1; + } + Err(err) => { + eprintln!("Couldn't send packet: {:?}", err); + } + } + } + + fn send_scrape_request(&mut self, connection_ids: &[ConnectionId], peer_index: usize) { + let peer = self.peers.get(peer_index).unwrap(); + + let transaction_id = + TransactionId::new(i32::from_ne_bytes((peer_index as u32).to_ne_bytes())); + + let mut info_hashes = Vec::with_capacity(peer.scrape_info_hash_indices.len()); + + for i in peer.scrape_info_hash_indices.iter() { + info_hashes.push(self.shared_state.info_hashes[*i].to_owned()) + } + + let request = ScrapeRequest { + connection_id: connection_ids[peer.socket_index as usize], + transaction_id, + info_hashes, + }; + + let mut cursor = Cursor::new(self.buffer); + + request.write_bytes(&mut cursor).unwrap(); + + let position = cursor.position() as usize; + + match self.sockets[peer.socket_index as usize].send(&cursor.get_ref()[..position]) { + Ok(_) => { + self.statistics.requests += 1; + } + Err(err) => { + eprintln!("Couldn't send packet: {:?}", err); + } + } + } + + fn handle_response(&mut self, response: Response) { + match response { + Response::Connect(_) => { + self.statistics.responses_connect += 1; + } + Response::AnnounceIpv4(r) => { + self.statistics.responses_announce += 1; + self.statistics.response_peers += r.peers.len(); + + let peer_index = + u32::from_ne_bytes(r.fixed.transaction_id.0.get().to_ne_bytes()) as usize; + + if let Some(peer) = self.peers.get(peer_index) { + *self + .announce_responses_per_info_hash + .entry(peer.announce_info_hash_index) + .or_default() += 1; + } + } + Response::AnnounceIpv6(r) => { + self.statistics.responses_announce += 1; + self.statistics.response_peers += r.peers.len(); + + let peer_index = + u32::from_ne_bytes(r.fixed.transaction_id.0.get().to_ne_bytes()) as usize; + + if let Some(peer) = self.peers.get(peer_index) { + *self + .announce_responses_per_info_hash + .entry(peer.announce_info_hash_index) + .or_default() += 1; + } + } + Response::Scrape(_) => { + self.statistics.responses_scrape += 1; + } + Response::Error(_) => { + self.statistics.responses_error += 1; + } + } + } + + fn update_shared_statistics(&mut self) { + let shared_statistics = &self.shared_state.statistics; + + shared_statistics + .requests + .fetch_add(self.statistics.requests, Ordering::Relaxed); + shared_statistics + .responses_connect + .fetch_add(self.statistics.responses_connect, Ordering::Relaxed); + shared_statistics + .responses_announce + .fetch_add(self.statistics.responses_announce, Ordering::Relaxed); + shared_statistics + .responses_scrape + .fetch_add(self.statistics.responses_scrape, Ordering::Relaxed); + shared_statistics + .responses_error + .fetch_add(self.statistics.responses_error, Ordering::Relaxed); + shared_statistics + .response_peers + .fetch_add(self.statistics.response_peers, Ordering::Relaxed); + + if self.config.extra_statistics { + let message = StatisticsMessage::ResponsesPerInfoHash( + self.announce_responses_per_info_hash.split_off(0), + ); + + self.statistics_sender.try_send(message).unwrap(); + } + + self.statistics = LocalStatistics::default(); + } +} + +fn create_socket(config: &Config, addr: SocketAddr) -> ::std::net::UdpSocket { + let socket = if addr.is_ipv4() { + Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) + } else { + Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP)) + } + .expect("create socket"); + + socket + .set_nonblocking(true) + .expect("socket: set nonblocking"); + + if config.network.recv_buffer != 0 { + if let Err(err) = socket.set_recv_buffer_size(config.network.recv_buffer) { + eprintln!( + "socket: failed setting recv buffer to {}: {:?}", + config.network.recv_buffer, err + ); + } + } + + socket + .bind(&addr.into()) + .unwrap_or_else(|err| panic!("socket: bind to {}: {:?}", addr, err)); + + socket + .connect(&config.server_address.into()) + .expect("socket: connect to server"); + + socket.into() +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum RequestType { + Announce, + Connect, + Scrape, +} + +pub struct RequestTypeDist(WeightedIndex); + +impl RequestTypeDist { + fn new(config: &Config) -> anyhow::Result { + let weights = [ + config.requests.weight_announce, + config.requests.weight_connect, + config.requests.weight_scrape, + ]; + + Ok(Self(WeightedIndex::new(weights)?)) + } + + fn sample(&self, rng: &mut impl Rng) -> RequestType { + const ITEMS: [RequestType; 3] = [ + RequestType::Announce, + RequestType::Connect, + RequestType::Scrape, + ]; + + ITEMS[self.0.sample(rng)] + } +} + +#[derive(Default)] +pub struct LocalStatistics { + pub requests: usize, + pub response_peers: usize, + pub responses_connect: usize, + pub responses_announce: usize, + pub responses_scrape: usize, + pub responses_error: usize, +} diff --git a/apps/aquatic/crates/udp_protocol/Cargo.toml b/apps/aquatic/crates/udp_protocol/Cargo.toml new file mode 100644 index 0000000..2142ccf --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "aquatic_udp_protocol" +description = "UDP BitTorrent tracker protocol" +keywords = ["udp", "protocol", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[dependencies] +aquatic_peer_id.workspace = true + +byteorder = "1" +either = "1" +zerocopy = { version = "0.7", features = ["derive"] } + +[dev-dependencies] +pretty_assertions = "1" +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/udp_protocol/README.md b/apps/aquatic/crates/udp_protocol/README.md new file mode 100644 index 0000000..19f32c4 --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/README.md @@ -0,0 +1,5 @@ +# aquatic_udp_protocol: UDP BitTorrent tracker protocol + +UDP BitTorrent tracker message parsing and serialization. + +Implements [BEP 015](https://www.bittorrent.org/beps/bep_0015.html) ([more details](https://libtorrent.org/udp_tracker_protocol.html)). diff --git a/apps/aquatic/crates/udp_protocol/src/common.rs b/apps/aquatic/crates/udp_protocol/src/common.rs new file mode 100644 index 0000000..01e5b1c --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/src/common.rs @@ -0,0 +1,299 @@ +use std::fmt::Debug; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::num::NonZeroU16; + +pub use aquatic_peer_id::{PeerClient, PeerId}; +use zerocopy::network_endian::{I32, I64, U16, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +pub trait Ip: Clone + Copy + Debug + PartialEq + Eq + std::hash::Hash + AsBytes {} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct AnnounceInterval(pub I32); + +impl AnnounceInterval { + pub fn new(v: i32) -> Self { + Self(I32::new(v)) + } +} + +impl Ord for AnnounceInterval { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for AnnounceInterval { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive( + PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes, +)] +#[repr(transparent)] +pub struct InfoHash(pub [u8; 20]); + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct ConnectionId(pub I64); + +impl ConnectionId { + pub fn new(v: i64) -> Self { + Self(I64::new(v)) + } +} + +impl Ord for ConnectionId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for ConnectionId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct TransactionId(pub I32); + +impl TransactionId { + pub fn new(v: i32) -> Self { + Self(I32::new(v)) + } +} + +impl Ord for TransactionId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for TransactionId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct NumberOfBytes(pub I64); + +impl NumberOfBytes { + pub fn new(v: i64) -> Self { + Self(I64::new(v)) + } +} + +impl Ord for NumberOfBytes { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for NumberOfBytes { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct NumberOfPeers(pub I32); + +impl NumberOfPeers { + pub fn new(v: i32) -> Self { + Self(I32::new(v)) + } +} + +impl Ord for NumberOfPeers { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for NumberOfPeers { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct NumberOfDownloads(pub I32); + +impl NumberOfDownloads { + pub fn new(v: i32) -> Self { + Self(I32::new(v)) + } +} + +impl Ord for NumberOfDownloads { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for NumberOfDownloads { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct Port(pub U16); + +impl Port { + pub fn new(v: NonZeroU16) -> Self { + Self(U16::new(v.into())) + } +} + +impl Ord for Port { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for Port { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct PeerKey(pub I32); + +impl PeerKey { + pub fn new(v: i32) -> Self { + Self(I32::new(v)) + } +} + +impl Ord for PeerKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.get().cmp(&other.0.get()) + } +} + +impl PartialOrd for PeerKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +#[derive( + PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash, AsBytes, FromBytes, FromZeroes, +)] +#[repr(C, packed)] +pub struct ResponsePeer { + pub ip_address: I, + pub port: Port, +} + +#[derive( + PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes, +)] +#[repr(transparent)] +pub struct Ipv4AddrBytes(pub [u8; 4]); + +impl Ip for Ipv4AddrBytes {} + +impl From for Ipv4Addr { + fn from(val: Ipv4AddrBytes) -> Self { + Ipv4Addr::from(val.0) + } +} + +impl From for Ipv4AddrBytes { + fn from(val: Ipv4Addr) -> Self { + Ipv4AddrBytes(val.octets()) + } +} + +#[derive( + PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes, +)] +#[repr(transparent)] +pub struct Ipv6AddrBytes(pub [u8; 16]); + +impl Ip for Ipv6AddrBytes {} + +impl From for Ipv6Addr { + fn from(val: Ipv6AddrBytes) -> Self { + Ipv6Addr::from(val.0) + } +} + +impl From for Ipv6AddrBytes { + fn from(val: Ipv6Addr) -> Self { + Ipv6AddrBytes(val.octets()) + } +} + +pub fn read_i32_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result { + let mut tmp = [0u8; 4]; + + bytes.read_exact(&mut tmp)?; + + Ok(I32::from_bytes(tmp)) +} + +pub fn read_i64_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result { + let mut tmp = [0u8; 8]; + + bytes.read_exact(&mut tmp)?; + + Ok(I64::from_bytes(tmp)) +} + +pub fn read_u16_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result { + let mut tmp = [0u8; 2]; + + bytes.read_exact(&mut tmp)?; + + Ok(U16::from_bytes(tmp)) +} + +pub fn read_u32_ne(bytes: &mut impl ::std::io::Read) -> ::std::io::Result { + let mut tmp = [0u8; 4]; + + bytes.read_exact(&mut tmp)?; + + Ok(U32::from_bytes(tmp)) +} + +pub fn invalid_data() -> ::std::io::Error { + ::std::io::Error::new(::std::io::ErrorKind::InvalidData, "invalid data") +} + +#[cfg(test)] +impl quickcheck::Arbitrary for InfoHash { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut bytes = [0u8; 20]; + + for byte in bytes.iter_mut() { + *byte = u8::arbitrary(g); + } + + Self(bytes) + } +} + +#[cfg(test)] +impl quickcheck::Arbitrary for ResponsePeer { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + ip_address: quickcheck::Arbitrary::arbitrary(g), + port: Port(u16::arbitrary(g).into()), + } + } +} diff --git a/apps/aquatic/crates/udp_protocol/src/lib.rs b/apps/aquatic/crates/udp_protocol/src/lib.rs new file mode 100644 index 0000000..bf686fa --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/src/lib.rs @@ -0,0 +1,7 @@ +pub mod common; +pub mod request; +pub mod response; + +pub use self::common::*; +pub use self::request::*; +pub use self::response::*; diff --git a/apps/aquatic/crates/udp_protocol/src/request.rs b/apps/aquatic/crates/udp_protocol/src/request.rs new file mode 100644 index 0000000..ad28a68 --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/src/request.rs @@ -0,0 +1,414 @@ +use std::io::{self, Cursor, Write}; + +use byteorder::{NetworkEndian, WriteBytesExt}; +use either::Either; +use zerocopy::FromZeroes; +use zerocopy::{byteorder::network_endian::I32, AsBytes, FromBytes}; + +use aquatic_peer_id::PeerId; + +use super::common::*; + +const PROTOCOL_IDENTIFIER: i64 = 4_497_486_125_440; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Request { + Connect(ConnectRequest), + Announce(AnnounceRequest), + Scrape(ScrapeRequest), +} + +impl Request { + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + match self { + Request::Connect(r) => r.write_bytes(bytes), + Request::Announce(r) => r.write_bytes(bytes), + Request::Scrape(r) => r.write_bytes(bytes), + } + } + + pub fn parse_bytes(bytes: &[u8], max_scrape_torrents: u8) -> Result { + let action = bytes + .get(8..12) + .map(|bytes| I32::from_bytes(bytes.try_into().unwrap())) + .ok_or_else(|| RequestParseError::unsendable_text("Couldn't parse action"))?; + + match action.get() { + // Connect + 0 => { + let mut bytes = Cursor::new(bytes); + + let protocol_identifier = + read_i64_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?; + let _action = read_i32_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?; + let transaction_id = read_i32_ne(&mut bytes) + .map(TransactionId) + .map_err(RequestParseError::unsendable_io)?; + + if protocol_identifier.get() == PROTOCOL_IDENTIFIER { + Ok((ConnectRequest { transaction_id }).into()) + } else { + Err(RequestParseError::unsendable_text( + "Protocol identifier missing", + )) + } + } + // Announce + 1 => { + let request = AnnounceRequest::read_from_prefix(bytes) + .ok_or_else(|| RequestParseError::unsendable_text("invalid data"))?; + + if request.port.0.get() == 0 { + Err(RequestParseError::sendable_text( + "Port can't be 0", + request.connection_id, + request.transaction_id, + )) + } else if !matches!(request.event.0.get(), (0..=3)) { + // Make sure not to allow AnnounceEventBytes with invalid value + Err(RequestParseError::sendable_text( + "Invalid announce event", + request.connection_id, + request.transaction_id, + )) + } else { + Ok(Request::Announce(request)) + } + } + // Scrape + 2 => { + let mut bytes = Cursor::new(bytes); + + let connection_id = read_i64_ne(&mut bytes) + .map(ConnectionId) + .map_err(RequestParseError::unsendable_io)?; + let _action = read_i32_ne(&mut bytes).map_err(RequestParseError::unsendable_io)?; + let transaction_id = read_i32_ne(&mut bytes) + .map(TransactionId) + .map_err(RequestParseError::unsendable_io)?; + + let remaining_bytes = { + let position = bytes.position() as usize; + let inner = bytes.into_inner(); + + // Slice will be empty if position == inner.len() + &inner[position..] + }; + + if remaining_bytes.is_empty() { + return Err(RequestParseError::sendable_text( + "Full scrapes are not allowed", + connection_id, + transaction_id, + )); + } + + let info_hashes = FromBytes::slice_from(remaining_bytes).ok_or_else(|| { + RequestParseError::sendable_text( + "Invalid info hash list", + connection_id, + transaction_id, + ) + })?; + + let info_hashes = Vec::from( + &info_hashes[..(max_scrape_torrents as usize).min(info_hashes.len())], + ); + + Ok((ScrapeRequest { + connection_id, + transaction_id, + info_hashes, + }) + .into()) + } + + _ => Err(RequestParseError::unsendable_text("Invalid action")), + } + } +} + +impl From for Request { + fn from(r: ConnectRequest) -> Self { + Self::Connect(r) + } +} + +impl From for Request { + fn from(r: AnnounceRequest) -> Self { + Self::Announce(r) + } +} + +impl From for Request { + fn from(r: ScrapeRequest) -> Self { + Self::Scrape(r) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct ConnectRequest { + pub transaction_id: TransactionId, +} + +impl ConnectRequest { + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_i64::(PROTOCOL_IDENTIFIER)?; + bytes.write_i32::(0)?; + bytes.write_all(self.transaction_id.as_bytes())?; + + Ok(()) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(C, packed)] +pub struct AnnounceRequest { + pub connection_id: ConnectionId, + /// This field is only present to enable zero-copy serialization and + /// deserialization. + pub action_placeholder: AnnounceActionPlaceholder, + pub transaction_id: TransactionId, + pub info_hash: InfoHash, + pub peer_id: PeerId, + pub bytes_downloaded: NumberOfBytes, + pub bytes_left: NumberOfBytes, + pub bytes_uploaded: NumberOfBytes, + pub event: AnnounceEventBytes, + pub ip_address: Ipv4AddrBytes, + pub key: PeerKey, + pub peers_wanted: NumberOfPeers, + pub port: Port, +} + +impl AnnounceRequest { + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_all(self.as_bytes()) + } +} + +/// Note: Request::from_bytes only creates this struct with value 1 +#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct AnnounceActionPlaceholder(I32); + +impl Default for AnnounceActionPlaceholder { + fn default() -> Self { + Self(I32::new(1)) + } +} + +/// Note: Request::from_bytes only creates this struct with values 0..=3 +#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(transparent)] +pub struct AnnounceEventBytes(I32); + +impl From for AnnounceEventBytes { + fn from(value: AnnounceEvent) -> Self { + Self(I32::new(match value { + AnnounceEvent::None => 0, + AnnounceEvent::Completed => 1, + AnnounceEvent::Started => 2, + AnnounceEvent::Stopped => 3, + })) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum AnnounceEvent { + Started, + Stopped, + Completed, + None, +} + +impl From for AnnounceEvent { + fn from(value: AnnounceEventBytes) -> Self { + match value.0.get() { + 1 => Self::Completed, + 2 => Self::Started, + 3 => Self::Stopped, + _ => Self::None, + } + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct ScrapeRequest { + pub connection_id: ConnectionId, + pub transaction_id: TransactionId, + pub info_hashes: Vec, +} + +impl ScrapeRequest { + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_all(self.connection_id.as_bytes())?; + bytes.write_i32::(2)?; + bytes.write_all(self.transaction_id.as_bytes())?; + bytes.write_all((*self.info_hashes.as_slice()).as_bytes())?; + + Ok(()) + } +} + +#[derive(Debug)] +pub enum RequestParseError { + Sendable { + connection_id: ConnectionId, + transaction_id: TransactionId, + err: &'static str, + }, + Unsendable { + err: Either, + }, +} + +impl RequestParseError { + pub fn sendable_text( + text: &'static str, + connection_id: ConnectionId, + transaction_id: TransactionId, + ) -> Self { + Self::Sendable { + connection_id, + transaction_id, + err: text, + } + } + pub fn unsendable_io(err: io::Error) -> Self { + Self::Unsendable { + err: Either::Left(err), + } + } + pub fn unsendable_text(text: &'static str) -> Self { + Self::Unsendable { + err: Either::Right(text), + } + } +} + +#[cfg(test)] +mod tests { + use quickcheck::TestResult; + use quickcheck_macros::quickcheck; + use zerocopy::network_endian::{I32, I64}; + + use super::*; + + impl quickcheck::Arbitrary for AnnounceEvent { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + match (bool::arbitrary(g), bool::arbitrary(g)) { + (false, false) => Self::Started, + (true, false) => Self::Started, + (false, true) => Self::Completed, + (true, true) => Self::None, + } + } + } + + impl quickcheck::Arbitrary for ConnectRequest { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + } + } + } + + impl quickcheck::Arbitrary for AnnounceRequest { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + connection_id: ConnectionId(I64::new(i64::arbitrary(g))), + action_placeholder: AnnounceActionPlaceholder::default(), + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + info_hash: InfoHash::arbitrary(g), + peer_id: PeerId::arbitrary(g), + bytes_downloaded: NumberOfBytes(I64::new(i64::arbitrary(g))), + bytes_uploaded: NumberOfBytes(I64::new(i64::arbitrary(g))), + bytes_left: NumberOfBytes(I64::new(i64::arbitrary(g))), + event: AnnounceEvent::arbitrary(g).into(), + ip_address: Ipv4AddrBytes::arbitrary(g), + key: PeerKey::new(i32::arbitrary(g)), + peers_wanted: NumberOfPeers(I32::new(i32::arbitrary(g))), + port: Port::new(quickcheck::Arbitrary::arbitrary(g)), + } + } + } + + impl quickcheck::Arbitrary for ScrapeRequest { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let info_hashes = (0..u8::arbitrary(g)) + .map(|_| InfoHash::arbitrary(g)) + .collect(); + + Self { + connection_id: ConnectionId(I64::new(i64::arbitrary(g))), + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + info_hashes, + } + } + } + + fn same_after_conversion(request: Request) -> bool { + let mut buf = Vec::new(); + + request.clone().write_bytes(&mut buf).unwrap(); + let r2 = Request::parse_bytes(&buf[..], ::std::u8::MAX).unwrap(); + + let success = request == r2; + + if !success { + ::pretty_assertions::assert_eq!(request, r2); + } + + success + } + + #[quickcheck] + fn test_connect_request_convert_identity(request: ConnectRequest) -> bool { + same_after_conversion(request.into()) + } + + #[quickcheck] + fn test_announce_request_convert_identity(request: AnnounceRequest) -> bool { + same_after_conversion(request.into()) + } + + #[quickcheck] + fn test_scrape_request_convert_identity(request: ScrapeRequest) -> TestResult { + if request.info_hashes.is_empty() { + return TestResult::discard(); + } + + TestResult::from_bool(same_after_conversion(request.into())) + } + + #[test] + fn test_various_input_lengths() { + for action in 0i32..4 { + for max_scrape_torrents in 0..3 { + for num_bytes in 0..256 { + let mut request_bytes = + ::std::iter::repeat(0).take(num_bytes).collect::>(); + + if let Some(action_bytes) = request_bytes.get_mut(8..12) { + action_bytes.copy_from_slice(&action.to_be_bytes()) + } + + // Should never panic + let _ = Request::parse_bytes(&request_bytes, max_scrape_torrents); + } + } + } + } + + #[test] + fn test_scrape_request_with_no_info_hashes() { + let mut request_bytes = Vec::new(); + + request_bytes.extend(0i64.to_be_bytes()); + request_bytes.extend(2i32.to_be_bytes()); + request_bytes.extend(0i32.to_be_bytes()); + + Request::parse_bytes(&request_bytes, 1).unwrap_err(); + } +} diff --git a/apps/aquatic/crates/udp_protocol/src/response.rs b/apps/aquatic/crates/udp_protocol/src/response.rs new file mode 100644 index 0000000..98c5e6b --- /dev/null +++ b/apps/aquatic/crates/udp_protocol/src/response.rs @@ -0,0 +1,342 @@ +use std::borrow::Cow; +use std::io::{self, Write}; +use std::mem::size_of; + +use byteorder::{NetworkEndian, WriteBytesExt}; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; + +use super::common::*; + +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Response { + Connect(ConnectResponse), + AnnounceIpv4(AnnounceResponse), + AnnounceIpv6(AnnounceResponse), + Scrape(ScrapeResponse), + Error(ErrorResponse), +} + +impl Response { + #[inline] + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + match self { + Response::Connect(r) => r.write_bytes(bytes), + Response::AnnounceIpv4(r) => r.write_bytes(bytes), + Response::AnnounceIpv6(r) => r.write_bytes(bytes), + Response::Scrape(r) => r.write_bytes(bytes), + Response::Error(r) => r.write_bytes(bytes), + } + } + + #[inline] + pub fn parse_bytes(mut bytes: &[u8], ipv4: bool) -> Result { + let action = read_i32_ne(&mut bytes)?; + + match action.get() { + // Connect + 0 => Ok(Response::Connect( + ConnectResponse::read_from_prefix(bytes).ok_or_else(invalid_data)?, + )), + // Announce + 1 if ipv4 => { + let fixed = + AnnounceResponseFixedData::read_from_prefix(bytes).ok_or_else(invalid_data)?; + + let peers = if let Some(bytes) = bytes.get(size_of::()..) + { + Vec::from( + ResponsePeer::::slice_from(bytes) + .ok_or_else(invalid_data)?, + ) + } else { + Vec::new() + }; + + Ok(Response::AnnounceIpv4(AnnounceResponse { fixed, peers })) + } + 1 if !ipv4 => { + let fixed = + AnnounceResponseFixedData::read_from_prefix(bytes).ok_or_else(invalid_data)?; + + let peers = if let Some(bytes) = bytes.get(size_of::()..) + { + Vec::from( + ResponsePeer::::slice_from(bytes) + .ok_or_else(invalid_data)?, + ) + } else { + Vec::new() + }; + + Ok(Response::AnnounceIpv6(AnnounceResponse { fixed, peers })) + } + // Scrape + 2 => { + let transaction_id = read_i32_ne(&mut bytes).map(TransactionId)?; + let torrent_stats = + Vec::from(TorrentScrapeStatistics::slice_from(bytes).ok_or_else(invalid_data)?); + + Ok((ScrapeResponse { + transaction_id, + torrent_stats, + }) + .into()) + } + // Error + 3 => { + let transaction_id = read_i32_ne(&mut bytes).map(TransactionId)?; + let message = String::from_utf8_lossy(bytes).into_owned().into(); + + Ok((ErrorResponse { + transaction_id, + message, + }) + .into()) + } + _ => Err(invalid_data()), + } + } +} + +impl From for Response { + fn from(r: ConnectResponse) -> Self { + Self::Connect(r) + } +} + +impl From> for Response { + fn from(r: AnnounceResponse) -> Self { + Self::AnnounceIpv4(r) + } +} + +impl From> for Response { + fn from(r: AnnounceResponse) -> Self { + Self::AnnounceIpv6(r) + } +} + +impl From for Response { + fn from(r: ScrapeResponse) -> Self { + Self::Scrape(r) + } +} + +impl From for Response { + fn from(r: ErrorResponse) -> Self { + Self::Error(r) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(C, packed)] +pub struct ConnectResponse { + pub transaction_id: TransactionId, + pub connection_id: ConnectionId, +} + +impl ConnectResponse { + #[inline] + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_i32::(0)?; + bytes.write_all(self.as_bytes())?; + + Ok(()) + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct AnnounceResponse { + pub fixed: AnnounceResponseFixedData, + pub peers: Vec>, +} + +impl AnnounceResponse { + pub fn empty() -> Self { + Self { + fixed: FromZeroes::new_zeroed(), + peers: Default::default(), + } + } + + #[inline] + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_i32::(1)?; + bytes.write_all(self.fixed.as_bytes())?; + bytes.write_all((*self.peers.as_slice()).as_bytes())?; + + Ok(()) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, AsBytes, FromBytes, FromZeroes)] +#[repr(C, packed)] +pub struct AnnounceResponseFixedData { + pub transaction_id: TransactionId, + pub announce_interval: AnnounceInterval, + pub leechers: NumberOfPeers, + pub seeders: NumberOfPeers, +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct ScrapeResponse { + pub transaction_id: TransactionId, + pub torrent_stats: Vec, +} + +impl ScrapeResponse { + #[inline] + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_i32::(2)?; + bytes.write_all(self.transaction_id.as_bytes())?; + bytes.write_all((*self.torrent_stats.as_slice()).as_bytes())?; + + Ok(()) + } +} + +#[derive(PartialEq, Eq, Debug, Copy, Clone, AsBytes, FromBytes, FromZeroes)] +#[repr(C, packed)] +pub struct TorrentScrapeStatistics { + pub seeders: NumberOfPeers, + pub completed: NumberOfDownloads, + pub leechers: NumberOfPeers, +} + +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct ErrorResponse { + pub transaction_id: TransactionId, + pub message: Cow<'static, str>, +} + +impl ErrorResponse { + #[inline] + pub fn write_bytes(&self, bytes: &mut impl Write) -> Result<(), io::Error> { + bytes.write_i32::(3)?; + bytes.write_all(self.transaction_id.as_bytes())?; + bytes.write_all(self.message.as_bytes())?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use quickcheck_macros::quickcheck; + use zerocopy::network_endian::I32; + use zerocopy::network_endian::I64; + + use super::*; + + impl quickcheck::Arbitrary for Ipv4AddrBytes { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self([ + u8::arbitrary(g), + u8::arbitrary(g), + u8::arbitrary(g), + u8::arbitrary(g), + ]) + } + } + + impl quickcheck::Arbitrary for Ipv6AddrBytes { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut bytes = [0; 16]; + + for byte in bytes.iter_mut() { + *byte = u8::arbitrary(g) + } + + Self(bytes) + } + } + + impl quickcheck::Arbitrary for TorrentScrapeStatistics { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + seeders: NumberOfPeers(I32::new(i32::arbitrary(g))), + completed: NumberOfDownloads(I32::new(i32::arbitrary(g))), + leechers: NumberOfPeers(I32::new(i32::arbitrary(g))), + } + } + } + + impl quickcheck::Arbitrary for ConnectResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + connection_id: ConnectionId(I64::new(i64::arbitrary(g))), + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + } + } + } + + impl quickcheck::Arbitrary for AnnounceResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let peers = (0..u8::arbitrary(g)) + .map(|_| ResponsePeer::arbitrary(g)) + .collect(); + + Self { + fixed: AnnounceResponseFixedData { + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + announce_interval: AnnounceInterval(I32::new(i32::arbitrary(g))), + leechers: NumberOfPeers(I32::new(i32::arbitrary(g))), + seeders: NumberOfPeers(I32::new(i32::arbitrary(g))), + }, + peers, + } + } + } + + impl quickcheck::Arbitrary for ScrapeResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let torrent_stats = (0..u8::arbitrary(g)) + .map(|_| TorrentScrapeStatistics::arbitrary(g)) + .collect(); + + Self { + transaction_id: TransactionId(I32::new(i32::arbitrary(g))), + torrent_stats, + } + } + } + + fn same_after_conversion(response: Response, ipv4: bool) -> bool { + let mut buf = Vec::new(); + + response.clone().write_bytes(&mut buf).unwrap(); + let r2 = Response::parse_bytes(&buf[..], ipv4).unwrap(); + + let success = response == r2; + + if !success { + ::pretty_assertions::assert_eq!(response, r2); + } + + success + } + + #[quickcheck] + fn test_connect_response_convert_identity(response: ConnectResponse) -> bool { + same_after_conversion(response.into(), true) + } + + #[quickcheck] + fn test_announce_response_ipv4_convert_identity( + response: AnnounceResponse, + ) -> bool { + same_after_conversion(response.into(), true) + } + + #[quickcheck] + fn test_announce_response_ipv6_convert_identity( + response: AnnounceResponse, + ) -> bool { + same_after_conversion(response.into(), false) + } + + #[quickcheck] + fn test_scrape_response_convert_identity(response: ScrapeResponse) -> bool { + same_after_conversion(response.into(), true) + } +} diff --git a/apps/aquatic/crates/ws/Cargo.toml b/apps/aquatic/crates/ws/Cargo.toml new file mode 100644 index 0000000..765274d --- /dev/null +++ b/apps/aquatic/crates/ws/Cargo.toml @@ -0,0 +1,67 @@ +[package] +name = "aquatic_ws" +description = "High-performance open WebTorrent tracker" +keywords = ["webtorrent", "websocket", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +readme = "./README.md" +rust-version = "1.70" + +[lib] +name = "aquatic_ws" + +[[bin]] +name = "aquatic_ws" + +[features] +default = ["prometheus", "mimalloc"] +prometheus = ["metrics", "aquatic_common/prometheus"] +metrics = ["dep:metrics", "dep:metrics-util"] +# Use mimalloc allocator for much better performance. +# +# Requires cmake and a C compiler +mimalloc = ["dep:mimalloc"] + +[dependencies] +aquatic_common = { workspace = true, features = ["rustls"] } +aquatic_peer_id.workspace = true +aquatic_toml_config.workspace = true +aquatic_ws_protocol.workspace = true + +anyhow = "1" +async-tungstenite = "0.28" +arc-swap = "1" +cfg-if = "1" +futures = "0.3" +futures-lite = "1" +futures-rustls = "0.26" +glommio = "0.9" +hashbrown = { version = "0.15", features = ["serde"] } +httparse = "1" +indexmap = "2" +log = "0.4" +privdrop = "0.5" +rand = { version = "0.8", features = ["small_rng"] } +rustls = "0.23" +rustls-pemfile = "2" +serde = { version = "1", features = ["derive"] } +signal-hook = { version = "0.3" } +slab = "0.4" +slotmap = "1" +socket2 = { version = "0.5", features = ["all"] } +tungstenite = "0.24" + +# metrics feature +metrics = { version = "0.24", optional = true } +metrics-util = { version = "0.19", optional = true } + +# mimalloc feature +mimalloc = { version = "0.1", default-features = false, optional = true } + +[dev-dependencies] +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/ws/README.md b/apps/aquatic/crates/ws/README.md new file mode 100644 index 0000000..fbebbd1 --- /dev/null +++ b/apps/aquatic/crates/ws/README.md @@ -0,0 +1,121 @@ +# aquatic_ws: high-performance open WebTorrent tracker + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +High-performance WebTorrent tracker for Linux 5.8 or later. + +Features at a glance: + +- Multithreaded design for handling large amounts of traffic +- All data is stored in-memory (no database needed) +- IPv4 and IPv6 support +- Supports forbidding/allowing info hashes +- Prometheus metrics +- Automated CI testing of full file transfers + +Known users: + +- [tracker.webtorrent.dev](https://tracker.webtorrent.dev) (`wss://tracker.webtorrent.dev`) + +## Performance + +![WebTorrent tracker throughput comparison](../../documents/aquatic-ws-load-test-illustration-2023-01-25.png) + +More details are available [here](../../documents/aquatic-ws-load-test-2023-01-25.pdf). + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_ws +``` + +### Configuring + +Generate the configuration file: + +```sh +./target/release/aquatic_ws -p > "aquatic-ws-config.toml" +``` + +Make necessary adjustments to the file. You will likely want to adjust `address` +(listening address) under the `network` section. + +To run over TLS, configure certificate and private key files. + +Running behind a reverse proxy is supported, as long as IPv4 requests are +proxied to IPv4 requests, and IPv6 requests to IPv6 requests. + +### Running + +Make sure locked memory limits are sufficient: +- If you're using a systemd service file, add `LimitMEMLOCK=65536000` to it +- If you're using openrc, make sure you have `rc_ulimit='-l 65536'` in your init script +- Otherwise, add the following lines to +`/etc/security/limits.conf`, and then log out and back in: + +``` +* hard memlock 65536 +* soft memlock 65536 +``` + +In Alpine Linux you will likely need a [higher limit](https://github.com/greatest-ape/aquatic/issues/211). + +Once done, start the application: + +```sh +./target/release/aquatic_ws -c "aquatic-ws-config.toml" +``` + +If your server is pointed to by domain `example.com` and you configured the +tracker to run on port 3000, people can now use it by adding the URL +`wss://example.com:3000` to their torrent files or magnet links. + +### Load testing + +A load test application is available. It supports generation and loading of +configuration files in a similar manner to the tracker application. + +After starting the tracker, run the load tester: + +```sh +. ./scripts/env-native-cpu-without-avx-512 # Optional + +cargo run --release -p aquatic_ws_load_test -- --help +``` + +## Details + +Aims for compatibility with [WebTorrent](https://github.com/webtorrent) +clients. Notes: + + * Doesn't track the number of torrent downloads (0 is always sent). + * Doesn't allow full scrapes, i.e. of all registered info hashes + +`aquatic_ws` has not been tested as much as `aquatic_udp`, but likely works +fine in production. + +## Architectural overview + +![Architectural overview of aquatic](../../documents/aquatic-architecture-2024.svg) + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. + diff --git a/apps/aquatic/crates/ws/src/common.rs b/apps/aquatic/crates/ws/src/common.rs new file mode 100644 index 0000000..aced74a --- /dev/null +++ b/apps/aquatic/crates/ws/src/common.rs @@ -0,0 +1,76 @@ +use std::{net::IpAddr, sync::Arc}; + +use aquatic_common::access_list::AccessListArcSwap; + +pub use aquatic_common::ValidUntil; +use aquatic_ws_protocol::common::{InfoHash, PeerId}; + +#[derive(Copy, Clone, Debug)] +pub enum IpVersion { + V4, + V6, +} + +impl IpVersion { + pub fn canonical_from_ip(ip: IpAddr) -> IpVersion { + match ip { + IpAddr::V4(_) => Self::V4, + IpAddr::V6(addr) => match addr.octets() { + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, _, _, _, _] => Self::V4, + _ => Self::V6, + }, + } + } +} + +#[derive(Default, Clone)] +pub struct State { + pub access_list: Arc, +} + +#[derive(Copy, Clone, Debug)] +pub struct PendingScrapeId(pub u8); + +#[derive(Copy, Clone, Debug)] +pub struct ConsumerId(pub u8); + +slotmap::new_key_type! { + pub struct ConnectionId; +} + +#[derive(Clone, Copy, Debug)] +pub struct InMessageMeta { + /// Index of socket worker responsible for this connection. Required for + /// sending back response through correct channel to correct worker. + pub out_message_consumer_id: ConsumerId, + pub connection_id: ConnectionId, + pub ip_version: IpVersion, + pub pending_scrape_id: Option, +} + +#[derive(Clone, Copy, Debug)] +pub struct OutMessageMeta { + /// Index of socket worker responsible for this connection. Required for + /// sending back response through correct channel to correct worker. + pub out_message_consumer_id: ConsumerId, + pub connection_id: ConnectionId, + pub pending_scrape_id: Option, +} + +impl From for OutMessageMeta { + fn from(val: InMessageMeta) -> Self { + OutMessageMeta { + out_message_consumer_id: val.out_message_consumer_id, + connection_id: val.connection_id, + pending_scrape_id: val.pending_scrape_id, + } + } +} + +#[derive(Clone, Debug)] +pub enum SwarmControlMessage { + ConnectionClosed { + ip_version: IpVersion, + announced_info_hashes: Vec<(InfoHash, PeerId)>, + }, +} diff --git a/apps/aquatic/crates/ws/src/config.rs b/apps/aquatic/crates/ws/src/config.rs new file mode 100644 index 0000000..4502686 --- /dev/null +++ b/apps/aquatic/crates/ws/src/config.rs @@ -0,0 +1,235 @@ +use std::net::SocketAddr; +use std::path::PathBuf; + +use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; +use serde::Deserialize; + +use aquatic_common::cli::LogLevel; +use aquatic_toml_config::TomlConfig; + +/// aquatic_ws configuration +/// +/// Running behind a reverse proxy is supported, but IPv4 peer requests have +/// to be proxied to IPv4 requests, and IPv6 requests to IPv6 requests. +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + /// Number of socket workers. + /// + /// On servers with 1-7 physical cores, using a worker per core is + /// recommended. With more cores, using two workers less than the + /// number of cores is recommended. + /// + /// Socket workers receive requests from the socket, parse them and send + /// them on to the swarm workers. They then receive responses from the + /// swarm workers, encode them and send them back over the socket. + pub socket_workers: usize, + /// Number of swarm workers. + /// + /// A single worker is recommended for servers with 1-7 physical cores. + /// With more cores, using two workers is recommended. + /// + /// Swarm workers receive a number of requests from socket workers, + /// generate responses and send them back to the socket workers. + pub swarm_workers: usize, + pub log_level: LogLevel, + pub network: NetworkConfig, + pub protocol: ProtocolConfig, + pub cleaning: CleaningConfig, + pub privileges: PrivilegeConfig, + /// Access list configuration + /// + /// The file is read on start and when the program receives `SIGUSR1`. If + /// initial parsing fails, the program exits. Later failures result in in + /// emitting of an error-level log message, while successful updates of the + /// access list result in emitting of an info-level log message. + pub access_list: AccessListConfig, + #[cfg(feature = "metrics")] + pub metrics: MetricsConfig, +} + +impl Default for Config { + fn default() -> Self { + Self { + socket_workers: 1, + swarm_workers: 1, + log_level: LogLevel::default(), + network: NetworkConfig::default(), + protocol: ProtocolConfig::default(), + cleaning: CleaningConfig::default(), + privileges: PrivilegeConfig::default(), + access_list: AccessListConfig::default(), + #[cfg(feature = "metrics")] + metrics: Default::default(), + } + } +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct NetworkConfig { + /// Bind to this address + /// + /// When providing an IPv4 style address, only IPv4 traffic will be + /// handled. Examples: + /// - "0.0.0.0:3000" binds to port 3000 on all network interfaces + /// - "127.0.0.1:3000" binds to port 3000 on the loopback interface + /// (localhost) + /// + /// When it comes to IPv6-style addresses, behaviour is more complex and + /// differs between operating systems. On Linux, to accept both IPv4 and + /// IPv6 traffic on any interface, use "[::]:3000". Set the "only_ipv6" + /// flag below to limit traffic to IPv6. To bind to the loopback interface + /// and only accept IPv6 packets, use "[::1]:3000" and set the only_ipv6 + /// flag. Receiving both IPv4 and IPv6 traffic on loopback is currently + /// not supported. For other operating systems, please refer to their + /// respective documentation. + pub address: SocketAddr, + /// Only allow access over IPv6 + pub only_ipv6: bool, + /// Maximum number of pending TCP connections + pub tcp_backlog: i32, + + /// Enable TLS + /// + /// The TLS files are read on start and when the program receives `SIGUSR1`. + /// If initial parsing fails, the program exits. Later failures result in + /// in emitting of an error-level log message, while successful updates + /// result in emitting of an info-level log message. Updates only affect + /// new connections. + pub enable_tls: bool, + /// Path to TLS certificate (DER-encoded X.509) + pub tls_certificate_path: PathBuf, + /// Path to TLS private key (DER-encoded ASN.1 in PKCS#8 or PKCS#1 format) + pub tls_private_key_path: PathBuf, + + pub websocket_max_message_size: usize, + pub websocket_max_frame_size: usize, + pub websocket_write_buffer_size: usize, + + /// Return a HTTP 200 Ok response when receiving GET /health. Can not be + /// combined with enable_tls. + pub enable_http_health_checks: bool, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + address: SocketAddr::from(([0, 0, 0, 0], 3000)), + only_ipv6: false, + tcp_backlog: 1024, + + enable_tls: false, + tls_certificate_path: "".into(), + tls_private_key_path: "".into(), + + websocket_max_message_size: 64 * 1024, + websocket_max_frame_size: 16 * 1024, + websocket_write_buffer_size: 8 * 1024, + + enable_http_health_checks: false, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct ProtocolConfig { + /// Maximum number of torrents to accept in scrape request + pub max_scrape_torrents: usize, + /// Maximum number of offers to accept in announce request + pub max_offers: usize, + /// Ask peers to announce this often (seconds) + pub peer_announce_interval: usize, +} + +impl Default for ProtocolConfig { + fn default() -> Self { + Self { + max_scrape_torrents: 255, + max_offers: 10, + peer_announce_interval: 120, + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CleaningConfig { + /// Clean peers this often (seconds) + pub torrent_cleaning_interval: u64, + /// Remove peers that have not announced for this long (seconds) + pub max_peer_age: u32, + /// Require that offers are answered to withing this period (seconds) + pub max_offer_age: u32, + // Clean connections this often (seconds) + pub connection_cleaning_interval: u64, + /// Close connections if no responses have been sent to them for this long (seconds) + pub max_connection_idle: u32, + /// After updating TLS certificates, close connections running with + /// previous certificates after this long (seconds) + /// + /// Countdown starts at next connection cleaning. + pub close_after_tls_update_grace_period: u32, +} + +impl Default for CleaningConfig { + fn default() -> Self { + Self { + torrent_cleaning_interval: 30, + max_peer_age: 180, + max_offer_age: 120, + max_connection_idle: 180, + connection_cleaning_interval: 30, + close_after_tls_update_grace_period: 60 * 60 * 60, + } + } +} + +#[cfg(feature = "metrics")] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct MetricsConfig { + /// Run a prometheus endpoint + pub run_prometheus_endpoint: bool, + /// Address to run prometheus endpoint on + pub prometheus_endpoint_address: SocketAddr, + /// Update metrics for torrent count this often (seconds) + pub torrent_count_update_interval: u64, + /// Serve information on peer clients + /// + /// Expect a certain CPU hit + pub peer_clients: bool, + /// Serve information on all peer id prefixes + /// + /// Requires `peer_clients` to be activated. + /// + /// Expect a certain CPU hit + pub peer_id_prefixes: bool, +} + +#[cfg(feature = "metrics")] +impl Default for MetricsConfig { + fn default() -> Self { + Self { + run_prometheus_endpoint: false, + prometheus_endpoint_address: SocketAddr::from(([0, 0, 0, 0], 9000)), + torrent_count_update_interval: 10, + peer_clients: false, + peer_id_prefixes: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/ws/src/lib.rs b/apps/aquatic/crates/ws/src/lib.rs new file mode 100644 index 0000000..76e60a4 --- /dev/null +++ b/apps/aquatic/crates/ws/src/lib.rs @@ -0,0 +1,215 @@ +pub mod common; +pub mod config; +pub mod workers; + +use std::sync::Arc; +use std::thread::{sleep, Builder, JoinHandle}; +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::rustls_config::create_rustls_config; +use aquatic_common::{ServerStartInstant, WorkerType}; +use arc_swap::ArcSwap; +use glommio::{channels::channel_mesh::MeshBuilder, prelude::*}; +use signal_hook::{consts::SIGUSR1, iterator::Signals}; + +use aquatic_common::access_list::update_access_list; +use aquatic_common::privileges::PrivilegeDropper; + +use common::*; +use config::Config; + +pub const APP_NAME: &str = "aquatic_ws: WebTorrent tracker"; +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const SHARED_IN_CHANNEL_SIZE: usize = 1024; + +pub fn run(config: Config) -> ::anyhow::Result<()> { + if config.network.enable_tls && config.network.enable_http_health_checks { + return Err(anyhow::anyhow!( + "configuration: network.enable_tls and network.enable_http_health_check can't both be set to true" + )); + } + + let mut signals = Signals::new([SIGUSR1])?; + + let state = State::default(); + + update_access_list(&config.access_list, &state.access_list)?; + + let num_mesh_peers = config.socket_workers + config.swarm_workers; + + let request_mesh_builder = MeshBuilder::partial(num_mesh_peers, SHARED_IN_CHANNEL_SIZE); + let response_mesh_builder = MeshBuilder::partial(num_mesh_peers, SHARED_IN_CHANNEL_SIZE * 16); + let control_mesh_builder = MeshBuilder::partial(num_mesh_peers, SHARED_IN_CHANNEL_SIZE * 16); + + let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); + + let opt_tls_config = if config.network.enable_tls { + Some(Arc::new(ArcSwap::from_pointee( + create_rustls_config( + &config.network.tls_certificate_path, + &config.network.tls_private_key_path, + ) + .with_context(|| "create rustls config")?, + ))) + } else { + None + }; + let mut opt_tls_cert_data = if config.network.enable_tls { + Some( + ::std::fs::read(&config.network.tls_certificate_path) + .with_context(|| "open tls certificate file")?, + ) + } else { + None + }; + + let server_start_instant = ServerStartInstant::new(); + + let mut join_handles = Vec::new(); + + for i in 0..(config.socket_workers) { + let config = config.clone(); + let state = state.clone(); + let opt_tls_config = opt_tls_config.clone(); + let control_mesh_builder = control_mesh_builder.clone(); + let request_mesh_builder = request_mesh_builder.clone(); + let response_mesh_builder = response_mesh_builder.clone(); + let priv_dropper = priv_dropper.clone(); + + let handle = Builder::new() + .name(format!("socket-{:02}", i + 1)) + .spawn(move || { + LocalExecutorBuilder::default() + .make() + .map_err(|err| anyhow::anyhow!("Spawning executor failed: {:#}", err))? + .run(workers::socket::run_socket_worker( + config, + state, + opt_tls_config, + control_mesh_builder, + request_mesh_builder, + response_mesh_builder, + priv_dropper, + server_start_instant, + i, + )) + }) + .context("spawn socket worker")?; + + join_handles.push((WorkerType::Socket(i), handle)); + } + + for i in 0..(config.swarm_workers) { + let config = config.clone(); + let state = state.clone(); + let control_mesh_builder = control_mesh_builder.clone(); + let request_mesh_builder = request_mesh_builder.clone(); + let response_mesh_builder = response_mesh_builder.clone(); + + let handle = Builder::new() + .name(format!("swarm-{:02}", i + 1)) + .spawn(move || { + LocalExecutorBuilder::default() + .make() + .map_err(|err| anyhow::anyhow!("Spawning executor failed: {:#}", err))? + .run(workers::swarm::run_swarm_worker( + config, + state, + control_mesh_builder, + request_mesh_builder, + response_mesh_builder, + server_start_instant, + i, + )) + }) + .context("spawn swarm worker")?; + + join_handles.push((WorkerType::Swarm(i), handle)); + } + + #[cfg(feature = "prometheus")] + if config.metrics.run_prometheus_endpoint { + let idle_timeout = config + .cleaning + .connection_cleaning_interval + .max(config.cleaning.torrent_cleaning_interval) + .max(config.metrics.torrent_count_update_interval) + * 2; + + let handle = aquatic_common::spawn_prometheus_endpoint( + config.metrics.prometheus_endpoint_address, + Some(Duration::from_secs(idle_timeout)), + Some(metrics_util::MetricKindMask::GAUGE), + )?; + + join_handles.push((WorkerType::Prometheus, handle)); + } + + // Spawn signal handler thread + { + let handle: JoinHandle> = Builder::new() + .name("signals".into()) + .spawn(move || { + for signal in &mut signals { + match signal { + SIGUSR1 => { + let _ = update_access_list(&config.access_list, &state.access_list); + + if let Some(tls_config) = opt_tls_config.as_ref() { + match ::std::fs::read(&config.network.tls_certificate_path) { + Ok(data) if &data == opt_tls_cert_data.as_ref().unwrap() => { + ::log::info!("skipping tls config update: certificate identical to currently loaded"); + } + Ok(data) => { + match create_rustls_config( + &config.network.tls_certificate_path, + &config.network.tls_private_key_path, + ) { + Ok(config) => { + tls_config.store(Arc::new(config)); + opt_tls_cert_data = Some(data); + + ::log::info!("successfully updated tls config"); + } + Err(err) => ::log::error!("could not update tls config: {:#}", err), + } + } + Err(err) => ::log::error!("couldn't read tls certificate file: {:#}", err), + } + } + } + _ => unreachable!(), + } + } + + Ok(()) + }) + .context("spawn signal worker")?; + + join_handles.push((WorkerType::Signals, handle)); + } + + loop { + for (i, (_, handle)) in join_handles.iter().enumerate() { + if handle.is_finished() { + let (worker_type, handle) = join_handles.remove(i); + + match handle.join() { + Ok(Ok(())) => { + return Err(anyhow::anyhow!("{} stopped", worker_type)); + } + Ok(Err(err)) => { + return Err(err.context(format!("{} stopped", worker_type))); + } + Err(_) => { + return Err(anyhow::anyhow!("{} panicked", worker_type)); + } + } + } + } + + sleep(Duration::from_secs(5)); + } +} diff --git a/apps/aquatic/crates/ws/src/main.rs b/apps/aquatic/crates/ws/src/main.rs new file mode 100644 index 0000000..0a39f95 --- /dev/null +++ b/apps/aquatic/crates/ws/src/main.rs @@ -0,0 +1,15 @@ +use aquatic_common::cli::run_app_with_cli_and_config; +use aquatic_ws::config::Config; + +#[cfg(feature = "mimalloc")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() { + run_app_with_cli_and_config::( + aquatic_ws::APP_NAME, + aquatic_ws::APP_VERSION, + aquatic_ws::run, + None, + ) +} diff --git a/apps/aquatic/crates/ws/src/workers/mod.rs b/apps/aquatic/crates/ws/src/workers/mod.rs new file mode 100644 index 0000000..28ef095 --- /dev/null +++ b/apps/aquatic/crates/ws/src/workers/mod.rs @@ -0,0 +1,2 @@ +pub mod socket; +pub mod swarm; diff --git a/apps/aquatic/crates/ws/src/workers/socket/connection.rs b/apps/aquatic/crates/ws/src/workers/socket/connection.rs new file mode 100644 index 0000000..2587ca1 --- /dev/null +++ b/apps/aquatic/crates/ws/src/workers/socket/connection.rs @@ -0,0 +1,658 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache}; +use aquatic_common::rustls_config::RustlsConfig; +use aquatic_common::ServerStartInstant; +use aquatic_ws_protocol::common::{InfoHash, PeerId, ScrapeAction}; +use aquatic_ws_protocol::incoming::{ + AnnounceEvent, AnnounceRequest, InMessage, ScrapeRequest, ScrapeRequestInfoHashes, +}; +use aquatic_ws_protocol::outgoing::{ + ErrorResponse, ErrorResponseAction, OutMessage, ScrapeResponse, ScrapeStatistics, +}; +use arc_swap::ArcSwap; +use async_tungstenite::WebSocketStream; +use futures::stream::{SplitSink, SplitStream}; +use futures::{AsyncWriteExt, StreamExt}; +use futures_lite::future::race; +use futures_rustls::TlsAcceptor; +use glommio::channels::channel_mesh::Senders; +use glommio::channels::local_channel::{LocalReceiver, LocalSender}; +use glommio::net::TcpStream; +use glommio::timer::timeout; +use glommio::{enclose, prelude::*}; +use hashbrown::hash_map::Entry; +use hashbrown::HashMap; +use slab::Slab; + +#[cfg(feature = "metrics")] +use metrics::{Counter, Gauge}; + +use crate::common::*; +use crate::config::Config; +use crate::workers::socket::calculate_in_message_consumer_index; + +#[cfg(feature = "metrics")] +use crate::workers::socket::{ip_version_to_metrics_str, WORKER_INDEX}; + +/// Optional second tuple field is for peer id hex representation +#[cfg(feature = "metrics")] +type PeerClientGauge = (Gauge, Option); + +pub struct ConnectionRunner { + pub config: Rc, + pub access_list: Arc, + pub in_message_senders: Rc>, + pub connection_valid_until: Rc>, + pub out_message_sender: Rc>, + pub out_message_receiver: LocalReceiver<(OutMessageMeta, OutMessage)>, + pub server_start_instant: ServerStartInstant, + pub out_message_consumer_id: ConsumerId, + pub connection_id: ConnectionId, + pub opt_tls_config: Option>>, + pub ip_version: IpVersion, +} + +impl ConnectionRunner { + pub async fn run( + self, + control_message_senders: Rc>, + close_conn_receiver: LocalReceiver<()>, + stream: TcpStream, + ) { + let clean_up_data = ConnectionCleanupData { + announced_info_hashes: Default::default(), + ip_version: self.ip_version, + #[cfg(feature = "metrics")] + opt_peer_client: Default::default(), + #[cfg(feature = "metrics")] + active_connections_gauge: ::metrics::gauge!( + "aquatic_active_connections", + "ip_version" => ip_version_to_metrics_str(self.ip_version), + "worker_index" => WORKER_INDEX.get().to_string(), + ), + }; + + clean_up_data.before_open(); + + let config = self.config.clone(); + let connection_id = self.connection_id; + + race( + async { + if let Err(err) = self.run_inner(clean_up_data.clone(), stream).await { + ::log::debug!("connection {:?} closed: {:#}", connection_id, err); + } + }, + async { + close_conn_receiver.recv().await; + }, + ) + .await; + + ::log::debug!("connection {:?} starting clean up", connection_id); + + clean_up_data + .after_close(&config, control_message_senders) + .await; + + ::log::debug!("connection {:?} finished clean up", connection_id); + } + + async fn run_inner( + self, + clean_up_data: ConnectionCleanupData, + mut stream: TcpStream, + ) -> anyhow::Result<()> { + if let Some(tls_config) = self.opt_tls_config.as_ref() { + let tls_config = tls_config.load_full(); + let tls_acceptor = TlsAcceptor::from(tls_config); + + let stream = tls_acceptor.accept(stream).await?; + + self.run_inner_stream_agnostic(clean_up_data, stream).await + } else { + // Implementing this over TLS is too cumbersome, since the crate used + // for TLS streams doesn't support peek and tungstenite doesn't + // properly support sending a HTTP error response in accept_hdr + // callback. + if self.config.network.enable_http_health_checks { + let mut peek_buf = [0u8; 11]; + + stream + .peek(&mut peek_buf) + .await + .map_err(|err| anyhow::anyhow!("error peeking: {:#}", err))?; + + if &peek_buf == b"GET /health" { + stream + .write_all(b"HTTP/1.1 200 Ok\r\nContent-Length: 2\r\n\r\nOk") + .await + .map_err(|err| { + anyhow::anyhow!("error sending health check response: {:#}", err) + })?; + stream.flush().await.map_err(|err| { + anyhow::anyhow!("error flushing health check response: {:#}", err) + })?; + + return Err(anyhow::anyhow!( + "client requested health check, skipping websocket negotiation" + )); + } + } + + self.run_inner_stream_agnostic(clean_up_data, stream).await + } + } + + async fn run_inner_stream_agnostic( + self, + clean_up_data: ConnectionCleanupData, + stream: S, + ) -> anyhow::Result<()> + where + S: futures::AsyncRead + futures::AsyncWrite + Unpin + 'static, + { + let ws_config = tungstenite::protocol::WebSocketConfig { + max_frame_size: Some(self.config.network.websocket_max_frame_size), + max_message_size: Some(self.config.network.websocket_max_message_size), + write_buffer_size: self.config.network.websocket_write_buffer_size, + max_write_buffer_size: self.config.network.websocket_write_buffer_size * 3, + ..Default::default() + }; + let stream = async_tungstenite::accept_async_with_config(stream, Some(ws_config)).await?; + let (ws_out, ws_in) = futures::StreamExt::split(stream); + + let pending_scrape_slab = Rc::new(RefCell::new(Slab::new())); + let access_list_cache = create_access_list_cache(&self.access_list); + + let config = self.config.clone(); + + let reader_future = enclose!((pending_scrape_slab, clean_up_data) async move { + let mut reader = ConnectionReader { + config: self.config.clone(), + access_list_cache, + in_message_senders: self.in_message_senders, + out_message_sender: self.out_message_sender, + pending_scrape_slab, + out_message_consumer_id: self.out_message_consumer_id, + ws_in, + ip_version: self.ip_version, + connection_id: self.connection_id, + clean_up_data: clean_up_data.clone(), + #[cfg(feature = "metrics")] + total_announce_requests_counter: ::metrics::counter!( + "aquatic_requests_total", + "type" => "announce", + "ip_version" => ip_version_to_metrics_str(self.ip_version), + "worker_index" => WORKER_INDEX.with(|index| index.get()).to_string(), + ), + #[cfg(feature = "metrics")] + total_scrape_requests_counter: ::metrics::counter!( + "aquatic_requests_total", + "type" => "scrape", + "ip_version" => ip_version_to_metrics_str(self.ip_version), + "worker_index" => WORKER_INDEX.with(|index| index.get()).to_string(), + ) + }; + + reader.run_in_message_loop().await + }); + + let writer_future = async move { + let mut writer = ConnectionWriter { + config, + out_message_receiver: self.out_message_receiver, + connection_valid_until: self.connection_valid_until, + ws_out, + pending_scrape_slab, + server_start_instant: self.server_start_instant, + ip_version: self.ip_version, + clean_up_data, + }; + + writer.run_out_message_loop().await + }; + + race(reader_future, writer_future).await + } +} + +struct ConnectionReader { + config: Rc, + access_list_cache: AccessListCache, + in_message_senders: Rc>, + out_message_sender: Rc>, + pending_scrape_slab: Rc>>, + out_message_consumer_id: ConsumerId, + ws_in: SplitStream>, + ip_version: IpVersion, + connection_id: ConnectionId, + clean_up_data: ConnectionCleanupData, + #[cfg(feature = "metrics")] + total_announce_requests_counter: Counter, + #[cfg(feature = "metrics")] + total_scrape_requests_counter: Counter, +} + +impl ConnectionReader { + async fn run_in_message_loop(&mut self) -> anyhow::Result<()> { + loop { + let message = self + .ws_in + .next() + .await + .ok_or_else(|| anyhow::anyhow!("Stream ended"))??; + + match &message { + tungstenite::Message::Text(_) | tungstenite::Message::Binary(_) => { + match InMessage::from_ws_message(message) { + Ok(InMessage::AnnounceRequest(request)) => { + self.handle_announce_request(request).await?; + } + Ok(InMessage::ScrapeRequest(request)) => { + self.handle_scrape_request(request).await?; + } + Err(err) => { + ::log::debug!("Couldn't parse in_message: {:#}", err); + + self.send_error_response("Invalid request".into(), None, None) + .await?; + } + } + } + tungstenite::Message::Ping(_) => { + ::log::trace!("Received ping message"); + // tungstenite sends a pong response by itself + } + tungstenite::Message::Pong(_) => { + ::log::trace!("Received pong message"); + } + tungstenite::Message::Close(_) => { + ::log::debug!("Client sent close frame"); + + break Ok(()); + } + tungstenite::Message::Frame(_) => { + ::log::warn!("Read raw websocket frame, this should not happen"); + } + } + + yield_if_needed().await; + } + } + + // Silence RefCell lint due to false positives + #[allow(clippy::await_holding_refcell_ref)] + async fn handle_announce_request(&mut self, request: AnnounceRequest) -> anyhow::Result<()> { + #[cfg(feature = "metrics")] + self.total_announce_requests_counter.increment(1); + + let info_hash = request.info_hash; + + if self + .access_list_cache + .load() + .allows(self.config.access_list.mode, &info_hash.0) + { + let mut announced_info_hashes = self.clean_up_data.announced_info_hashes.borrow_mut(); + + // Store peer id / check if stored peer id matches + match announced_info_hashes.entry(request.info_hash) { + Entry::Occupied(entry) => { + if *entry.get() != request.peer_id { + // Drop Rc borrow before awaiting + drop(announced_info_hashes); + + self.send_error_response( + "Only one peer id can be used per torrent".into(), + Some(ErrorResponseAction::Announce), + Some(info_hash), + ) + .await?; + + return Err(anyhow::anyhow!( + "Peer used more than one PeerId for a single torrent" + )); + } + } + Entry::Vacant(entry) => { + entry.insert(request.peer_id); + + // Set peer client info if not set + #[cfg(feature = "metrics")] + if self.config.metrics.run_prometheus_endpoint + && self.config.metrics.peer_clients + && self.clean_up_data.opt_peer_client.borrow().is_none() + { + let peer_id = aquatic_peer_id::PeerId(request.peer_id.0); + + let peer_client_gauge = ::metrics::gauge!( + "aquatic_peer_clients", + "client" => peer_id.client().to_string(), + ); + + peer_client_gauge.increment(1.0); + + let opt_peer_id_prefix_gauge = + self.config.metrics.peer_id_prefixes.then(|| { + let g = ::metrics::gauge!( + "aquatic_peer_id_prefixes", + "prefix_hex" => peer_id.first_8_bytes_hex().to_string(), + ); + + g.increment(1.0); + + g + }); + + *self.clean_up_data.opt_peer_client.borrow_mut() = + Some((peer_client_gauge, opt_peer_id_prefix_gauge)); + }; + } + } + + if let Some(AnnounceEvent::Stopped) = request.event { + announced_info_hashes.remove(&request.info_hash); + } + + // Drop Rc borrow before awaiting + drop(announced_info_hashes); + + let in_message = InMessage::AnnounceRequest(request); + + let consumer_index = calculate_in_message_consumer_index(&self.config, info_hash); + + // Only fails when receiver is closed + self.in_message_senders + .send_to( + consumer_index, + (self.make_connection_meta(None), in_message), + ) + .await + .unwrap(); + } else { + self.send_error_response( + "Info hash not allowed".into(), + Some(ErrorResponseAction::Announce), + Some(info_hash), + ) + .await?; + } + + Ok(()) + } + + async fn handle_scrape_request(&mut self, request: ScrapeRequest) -> anyhow::Result<()> { + #[cfg(feature = "metrics")] + self.total_scrape_requests_counter.increment(1); + + let info_hashes = if let Some(info_hashes) = request.info_hashes { + info_hashes + } else { + // If request.info_hashes is empty, don't return scrape for all + // torrents, even though reference server does it. It is too expensive. + self.send_error_response( + "Full scrapes are not allowed".into(), + Some(ErrorResponseAction::Scrape), + None, + ) + .await?; + + return Ok(()); + }; + + let mut info_hashes_by_worker: BTreeMap> = BTreeMap::new(); + + for info_hash in info_hashes.as_vec() { + let info_hashes = info_hashes_by_worker + .entry(calculate_in_message_consumer_index(&self.config, info_hash)) + .or_default(); + + info_hashes.push(info_hash); + } + + let pending_worker_out_messages = info_hashes_by_worker.len(); + + let pending_scrape_response = PendingScrapeResponse { + pending_worker_out_messages, + stats: Default::default(), + }; + + let pending_scrape_id: u8 = self + .pending_scrape_slab + .borrow_mut() + .insert(pending_scrape_response) + .try_into() + .with_context(|| "Reached 256 pending scrape responses")?; + + let meta = self.make_connection_meta(Some(PendingScrapeId(pending_scrape_id))); + + for (consumer_index, info_hashes) in info_hashes_by_worker { + let in_message = InMessage::ScrapeRequest(ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: Some(ScrapeRequestInfoHashes::Multiple(info_hashes)), + }); + + // Only fails when receiver is closed + self.in_message_senders + .send_to(consumer_index, (meta, in_message)) + .await + .unwrap(); + } + + Ok(()) + } + + async fn send_error_response( + &self, + failure_reason: Cow<'static, str>, + action: Option, + info_hash: Option, + ) -> anyhow::Result<()> { + let out_message = OutMessage::ErrorResponse(ErrorResponse { + action, + failure_reason, + info_hash, + }); + + self.out_message_sender + .send((self.make_connection_meta(None).into(), out_message)) + .await + .map_err(|err| { + anyhow::anyhow!("ConnectionReader::send_error_response failed: {:#}", err) + }) + } + + fn make_connection_meta(&self, pending_scrape_id: Option) -> InMessageMeta { + InMessageMeta { + connection_id: self.connection_id, + out_message_consumer_id: self.out_message_consumer_id, + ip_version: self.ip_version, + pending_scrape_id, + } + } +} + +struct ConnectionWriter { + config: Rc, + out_message_receiver: LocalReceiver<(OutMessageMeta, OutMessage)>, + connection_valid_until: Rc>, + ws_out: SplitSink, tungstenite::Message>, + pending_scrape_slab: Rc>>, + server_start_instant: ServerStartInstant, + ip_version: IpVersion, + clean_up_data: ConnectionCleanupData, +} + +impl ConnectionWriter { + // Silence RefCell lint due to false positives + #[allow(clippy::await_holding_refcell_ref)] + async fn run_out_message_loop(&mut self) -> anyhow::Result<()> { + loop { + let (meta, out_message) = self.out_message_receiver.recv().await.ok_or_else(|| { + anyhow::anyhow!("ConnectionWriter couldn't receive message, sender is closed") + })?; + + match out_message { + OutMessage::ScrapeResponse(out_message) => { + let pending_scrape_id = meta + .pending_scrape_id + .expect("meta.pending_scrape_id not set"); + + let mut pending_responses = self.pending_scrape_slab.borrow_mut(); + + let pending_response = pending_responses + .get_mut(pending_scrape_id.0 as usize) + .ok_or(anyhow::anyhow!("pending scrape not found in slab"))?; + + pending_response.stats.extend(out_message.files); + pending_response.pending_worker_out_messages -= 1; + + if pending_response.pending_worker_out_messages == 0 { + let pending_response = + pending_responses.remove(pending_scrape_id.0 as usize); + + pending_responses.shrink_to_fit(); + + let out_message = OutMessage::ScrapeResponse(ScrapeResponse { + action: ScrapeAction::Scrape, + files: pending_response.stats, + }); + + // Drop Rc borrow before awaiting + drop(pending_responses); + + self.send_out_message(&out_message).await?; + } + } + out_message => { + self.send_out_message(&out_message).await?; + } + }; + + yield_if_needed().await; + } + } + + async fn send_out_message(&mut self, out_message: &OutMessage) -> anyhow::Result<()> { + timeout(Duration::from_secs(10), async { + Ok(futures::SinkExt::send(&mut self.ws_out, out_message.to_ws_message()).await) + }) + .await + .map_err(|err| { + anyhow::anyhow!("send_out_message: sending to peer took too long: {:#}", err) + })? + .with_context(|| "send_out_message")?; + + if let OutMessage::AnnounceResponse(_) | OutMessage::ScrapeResponse(_) = out_message { + *self.connection_valid_until.borrow_mut() = ValidUntil::new( + self.server_start_instant, + self.config.cleaning.max_connection_idle, + ); + } + + #[cfg(feature = "metrics")] + { + let out_message_type = match &out_message { + OutMessage::OfferOutMessage(_) => "offer", + OutMessage::AnswerOutMessage(_) => "offer_answer", + OutMessage::AnnounceResponse(_) => "announce", + OutMessage::ScrapeResponse(_) => "scrape", + OutMessage::ErrorResponse(_) => "error", + }; + + ::metrics::counter!( + "aquatic_responses_total", + "type" => out_message_type, + "ip_version" => ip_version_to_metrics_str(self.ip_version), + "worker_index" => WORKER_INDEX.with(|index| index.get()).to_string(), + ) + .increment(1); + + // As long as connection is still alive, increment peer client + // gauges by zero to prevent them from being removed due to + // idleness + if let Some((peer_client_gauge, opt_peer_id_prefix_gauge)) = + self.clean_up_data.opt_peer_client.borrow().as_ref() + { + peer_client_gauge.increment(0.0); + + if let Some(g) = opt_peer_id_prefix_gauge { + g.increment(0.0); + } + } + } + + Ok(()) + } +} + +/// Data stored with connection needed for cleanup after it closes +#[derive(Clone)] +struct ConnectionCleanupData { + announced_info_hashes: Rc>>, + ip_version: IpVersion, + #[cfg(feature = "metrics")] + opt_peer_client: Rc>>, + #[cfg(feature = "metrics")] + active_connections_gauge: Gauge, +} + +impl ConnectionCleanupData { + fn before_open(&self) { + #[cfg(feature = "metrics")] + self.active_connections_gauge.increment(1.0); + } + async fn after_close( + &self, + config: &Config, + control_message_senders: Rc>, + ) { + let mut announced_info_hashes = HashMap::new(); + + for (info_hash, peer_id) in self.announced_info_hashes.take().into_iter() { + let consumer_index = calculate_in_message_consumer_index(config, info_hash); + + announced_info_hashes + .entry(consumer_index) + .or_insert(Vec::new()) + .push((info_hash, peer_id)); + } + + for (consumer_index, announced_info_hashes) in announced_info_hashes.into_iter() { + let message = SwarmControlMessage::ConnectionClosed { + ip_version: self.ip_version, + announced_info_hashes, + }; + + control_message_senders + .send_to(consumer_index, message) + .await + .expect("control message receiver open"); + } + + #[cfg(feature = "metrics")] + self.active_connections_gauge.decrement(1.0); + + #[cfg(feature = "metrics")] + if let Some((peer_client_gauge, opt_peer_id_prefix_gauge)) = self.opt_peer_client.take() { + peer_client_gauge.decrement(1.0); + + if let Some(g) = opt_peer_id_prefix_gauge { + g.decrement(1.0); + } + } + } +} + +struct PendingScrapeResponse { + pending_worker_out_messages: usize, + stats: HashMap, +} diff --git a/apps/aquatic/crates/ws/src/workers/socket/mod.rs b/apps/aquatic/crates/ws/src/workers/socket/mod.rs new file mode 100644 index 0000000..c633d63 --- /dev/null +++ b/apps/aquatic/crates/ws/src/workers/socket/mod.rs @@ -0,0 +1,372 @@ +use std::cell::RefCell; +use std::os::unix::prelude::{FromRawFd, IntoRawFd}; +use std::rc::Rc; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use aquatic_common::privileges::PrivilegeDropper; +use aquatic_common::rustls_config::RustlsConfig; +use aquatic_common::ServerStartInstant; +use aquatic_ws_protocol::common::InfoHash; +use aquatic_ws_protocol::incoming::InMessage; +use aquatic_ws_protocol::outgoing::OutMessage; +use arc_swap::ArcSwap; +use futures::StreamExt; +use glommio::channels::channel_mesh::{MeshBuilder, Partial, Role}; +use glommio::channels::local_channel::{new_bounded, LocalSender}; +use glommio::channels::shared_channel::ConnectedReceiver; +use glommio::net::TcpListener; +use glommio::timer::TimerActionRepeat; +use glommio::{enclose, prelude::*}; +use slotmap::HopSlotMap; + +use crate::config::Config; + +use crate::common::*; +use crate::workers::socket::connection::ConnectionRunner; + +mod connection; + +type ConnectionHandles = HopSlotMap; + +const LOCAL_CHANNEL_SIZE: usize = 16; + +#[cfg(feature = "metrics")] +thread_local! { static WORKER_INDEX: ::std::cell::Cell = Default::default() } + +/// Used to interact with the connection tasks +struct ConnectionHandle { + close_conn_sender: LocalSender<()>, + /// Sender part of channel used to pass on outgoing messages from request + /// worker + out_message_sender: Rc>, + /// Updated after sending message to peer + valid_until: Rc>, + /// The TLS config used for this connection + opt_tls_config: Option>, + valid_until_after_tls_update: Option, +} + +#[allow(clippy::too_many_arguments)] +pub async fn run_socket_worker( + config: Config, + state: State, + opt_tls_config: Option>>, + control_message_mesh_builder: MeshBuilder, + in_message_mesh_builder: MeshBuilder<(InMessageMeta, InMessage), Partial>, + out_message_mesh_builder: MeshBuilder<(OutMessageMeta, OutMessage), Partial>, + priv_dropper: PrivilegeDropper, + server_start_instant: ServerStartInstant, + worker_index: usize, +) -> anyhow::Result<()> { + #[cfg(feature = "metrics")] + WORKER_INDEX.with(|index| index.set(worker_index)); + + let config = Rc::new(config); + let access_list = state.access_list; + + let listener = create_tcp_listener(&config, priv_dropper).context("create tcp listener")?; + + ::log::info!("created tcp listener"); + + let (control_message_senders, _) = control_message_mesh_builder + .join(Role::Producer) + .await + .map_err(|err| anyhow::anyhow!("join control message mesh: {:#}", err))?; + let (in_message_senders, _) = in_message_mesh_builder + .join(Role::Producer) + .await + .map_err(|err| anyhow::anyhow!("join in message mesh: {:#}", err))?; + let (_, mut out_message_receivers) = out_message_mesh_builder + .join(Role::Consumer) + .await + .map_err(|err| anyhow::anyhow!("join out message mesh: {:#}", err))?; + + let control_message_senders = Rc::new(control_message_senders); + let in_message_senders = Rc::new(in_message_senders); + + let out_message_consumer_id = ConsumerId( + out_message_receivers + .consumer_id() + .unwrap() + .try_into() + .unwrap(), + ); + + let tq_prioritized = executor().create_task_queue( + Shares::Static(100), + Latency::Matters(Duration::from_millis(1)), + "prioritized", + ); + let tq_regular = + executor().create_task_queue(Shares::Static(1), Latency::NotImportant, "regular"); + + ::log::info!("joined channels"); + + let connection_handles = Rc::new(RefCell::new(ConnectionHandles::default())); + + // Periodically clean connections + TimerActionRepeat::repeat_into( + enclose!((config, connection_handles, opt_tls_config) move || { + clean_connections( + config.clone(), + connection_handles.clone(), + server_start_instant, + opt_tls_config.clone(), + ) + }), + tq_prioritized, + ) + .map_err(|err| anyhow::anyhow!("spawn connection cleaning task: {:#}", err))?; + + for (_, out_message_receiver) in out_message_receivers.streams() { + spawn_local_into( + receive_out_messages(out_message_receiver, connection_handles.clone()), + tq_regular, + ) + .map_err(|err| anyhow::anyhow!("spawn out message receiving task: {:#}", err))? + .detach(); + } + + let mut incoming = listener.incoming(); + + while let Some(stream) = incoming.next().await { + match stream { + Err(err) => { + ::log::error!("accept connection: {:#}", err); + } + Ok(stream) => { + let ip_version = match stream.peer_addr() { + Ok(addr) => IpVersion::canonical_from_ip(addr.ip()), + Err(err) => { + ::log::info!("could not extract ip version (v4 or v6): {:#}", err); + + continue; + } + }; + + let (out_message_sender, out_message_receiver) = new_bounded(LOCAL_CHANNEL_SIZE); + let out_message_sender = Rc::new(out_message_sender); + + let (close_conn_sender, close_conn_receiver) = new_bounded(1); + + let connection_valid_until = Rc::new(RefCell::new(ValidUntil::new( + server_start_instant, + config.cleaning.max_connection_idle, + ))); + + let connection_handle = ConnectionHandle { + close_conn_sender, + out_message_sender: out_message_sender.clone(), + valid_until: connection_valid_until.clone(), + opt_tls_config: opt_tls_config.as_ref().map(|c| c.load_full()), + valid_until_after_tls_update: None, + }; + + let connection_id = connection_handles.borrow_mut().insert(connection_handle); + + spawn_local_into( + enclose!(( + config, + access_list, + in_message_senders, + connection_valid_until, + opt_tls_config, + control_message_senders, + connection_handles + ) async move { + let runner = ConnectionRunner { + config, + access_list, + in_message_senders, + connection_valid_until, + out_message_sender, + out_message_receiver, + server_start_instant, + out_message_consumer_id, + connection_id, + opt_tls_config, + ip_version + }; + + runner.run(control_message_senders, close_conn_receiver, stream).await; + + connection_handles.borrow_mut().remove(connection_id); + }), + tq_regular, + ) + .unwrap() + .detach(); + } + } + } + + Ok(()) +} + +async fn clean_connections( + config: Rc, + connection_slab: Rc>, + server_start_instant: ServerStartInstant, + opt_tls_config: Option>>, +) -> Option { + let now = server_start_instant.seconds_elapsed(); + let opt_current_tls_config = opt_tls_config.map(|c| c.load_full()); + + connection_slab.borrow_mut().retain(|_, reference| { + let mut keep = true; + + // Handle case when connection runs on an old TLS certificate + if let Some(valid_until) = reference.valid_until_after_tls_update { + if !valid_until.valid(now) { + keep = false; + } + } else if let Some(false) = opt_current_tls_config + .as_ref() + .zip(reference.opt_tls_config.as_ref()) + .map(|(a, b)| Arc::ptr_eq(a, b)) + { + reference.valid_until_after_tls_update = Some(ValidUntil::new( + server_start_instant, + config.cleaning.close_after_tls_update_grace_period, + )); + } + + keep &= reference.valid_until.borrow().valid(now); + + if keep { + true + } else { + if let Err(err) = reference.close_conn_sender.try_send(()) { + ::log::info!("couldn't tell connection to close: {:#}", err); + } + + false + } + }); + + #[cfg(feature = "metrics")] + { + ::log::info!( + "cleaned connections in worker {}, {} references remaining", + WORKER_INDEX.get(), + connection_slab.borrow_mut().len() + ); + + // Increment gauges by zero to prevent them from being removed due to + // idleness + + let worker_index = WORKER_INDEX.with(|index| index.get()).to_string(); + + if config.network.address.is_ipv4() || !config.network.only_ipv6 { + ::metrics::gauge!( + "aquatic_active_connections", + "ip_version" => "4", + "worker_index" => worker_index.clone(), + ) + .increment(0.0); + } + if config.network.address.is_ipv6() { + ::metrics::gauge!( + "aquatic_active_connections", + "ip_version" => "6", + "worker_index" => worker_index, + ) + .increment(0.0); + } + } + + Some(Duration::from_secs( + config.cleaning.connection_cleaning_interval, + )) +} + +async fn receive_out_messages( + mut out_message_receiver: ConnectedReceiver<(OutMessageMeta, OutMessage)>, + connection_references: Rc>, +) { + let connection_references = &connection_references; + + while let Some((meta, out_message)) = out_message_receiver.next().await { + if let Some(reference) = connection_references.borrow().get(meta.connection_id) { + match reference.out_message_sender.try_send((meta, out_message)) { + Ok(()) => {} + Err(GlommioError::Closed(_)) => {} + Err(GlommioError::WouldBlock(_)) => { + ::log::debug!( + "couldn't send OutMessage over local channel to Connection, channel full" + ); + } + Err(err) => { + ::log::debug!( + "couldn't send OutMessage over local channel to Connection: {:?}", + err + ); + } + } + } + } +} + +fn create_tcp_listener( + config: &Config, + priv_dropper: PrivilegeDropper, +) -> anyhow::Result { + let domain = if config.network.address.is_ipv4() { + socket2::Domain::IPV4 + } else { + socket2::Domain::IPV6 + }; + + ::log::info!("creating socket.."); + + let socket = socket2::Socket::new(domain, socket2::Type::STREAM, Some(socket2::Protocol::TCP)) + .with_context(|| "create socket")?; + + if config.network.only_ipv6 { + ::log::info!("setting socket to ipv6 only.."); + + socket + .set_only_v6(true) + .with_context(|| "socket: set only ipv6")?; + } + + ::log::info!("setting SO_REUSEPORT on socket.."); + + socket + .set_reuse_port(true) + .with_context(|| "socket: set reuse port")?; + + ::log::info!("binding socket.."); + + socket + .bind(&config.network.address.into()) + .with_context(|| format!("socket: bind to {}", config.network.address))?; + + ::log::info!("listening on socket.."); + + socket + .listen(config.network.tcp_backlog) + .with_context(|| format!("socket: listen {}", config.network.address))?; + + ::log::info!("running PrivilegeDropper::after_socket_creation.."); + + priv_dropper.after_socket_creation()?; + + ::log::info!("casting socket to glommio TcpListener.."); + + Ok(unsafe { TcpListener::from_raw_fd(socket.into_raw_fd()) }) +} + +#[cfg(feature = "metrics")] +fn ip_version_to_metrics_str(ip_version: IpVersion) -> &'static str { + match ip_version { + IpVersion::V4 => "4", + IpVersion::V6 => "6", + } +} + +fn calculate_in_message_consumer_index(config: &Config, info_hash: InfoHash) -> usize { + (info_hash.0[0] as usize) % config.swarm_workers +} diff --git a/apps/aquatic/crates/ws/src/workers/swarm/mod.rs b/apps/aquatic/crates/ws/src/workers/swarm/mod.rs new file mode 100644 index 0000000..419ee56 --- /dev/null +++ b/apps/aquatic/crates/ws/src/workers/swarm/mod.rs @@ -0,0 +1,167 @@ +mod storage; + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Duration; + +use aquatic_ws_protocol::incoming::InMessage; +use aquatic_ws_protocol::outgoing::OutMessage; +use futures::StreamExt; +use glommio::channels::channel_mesh::{MeshBuilder, Partial, Role, Senders}; +use glommio::enclose; +use glommio::prelude::*; +use glommio::timer::TimerActionRepeat; +use rand::{rngs::SmallRng, SeedableRng}; + +use aquatic_common::ServerStartInstant; + +use crate::common::*; +use crate::config::Config; +use crate::SHARED_IN_CHANNEL_SIZE; + +use self::storage::TorrentMaps; + +pub async fn run_swarm_worker( + config: Config, + state: State, + control_message_mesh_builder: MeshBuilder, + in_message_mesh_builder: MeshBuilder<(InMessageMeta, InMessage), Partial>, + out_message_mesh_builder: MeshBuilder<(OutMessageMeta, OutMessage), Partial>, + server_start_instant: ServerStartInstant, + worker_index: usize, +) -> anyhow::Result<()> { + let (_, mut control_message_receivers) = control_message_mesh_builder + .join(Role::Consumer) + .await + .map_err(|err| anyhow::anyhow!("join control message mesh: {:#}", err))?; + let (_, mut in_message_receivers) = in_message_mesh_builder + .join(Role::Consumer) + .await + .map_err(|err| anyhow::anyhow!("join in message mesh: {:#}", err))?; + let (out_message_senders, _) = out_message_mesh_builder + .join(Role::Producer) + .await + .map_err(|err| anyhow::anyhow!("join out message mesh: {:#}", err))?; + + let out_message_senders = Rc::new(out_message_senders); + + let torrents = Rc::new(RefCell::new(TorrentMaps::new(worker_index))); + let access_list = state.access_list; + + // Periodically clean torrents + TimerActionRepeat::repeat(enclose!((config, torrents, access_list) move || { + enclose!((config, torrents, access_list) move || async move { + torrents.borrow_mut().clean(&config, &access_list, server_start_instant); + + Some(Duration::from_secs(config.cleaning.torrent_cleaning_interval)) + })() + })); + + // Periodically update torrent count metrics + #[cfg(feature = "metrics")] + TimerActionRepeat::repeat(enclose!((config, torrents) move || { + enclose!((config, torrents) move || async move { + torrents.borrow_mut().update_torrent_count_metrics(); + + Some(Duration::from_secs(config.metrics.torrent_count_update_interval)) + })() + })); + + let mut handles = Vec::new(); + + for (_, receiver) in control_message_receivers.streams() { + let handle = + spawn_local(handle_control_message_stream(torrents.clone(), receiver)).detach(); + + handles.push(handle); + } + + for (_, receiver) in in_message_receivers.streams() { + let handle = spawn_local(handle_request_stream( + config.clone(), + torrents.clone(), + server_start_instant, + out_message_senders.clone(), + receiver, + )) + .detach(); + + handles.push(handle); + } + + for handle in handles { + handle.await; + } + + Ok(()) +} + +async fn handle_control_message_stream(torrents: Rc>, mut stream: S) +where + S: futures_lite::Stream + ::std::marker::Unpin, +{ + while let Some(message) = stream.next().await { + match message { + SwarmControlMessage::ConnectionClosed { + ip_version, + announced_info_hashes, + } => { + let mut torrents = torrents.borrow_mut(); + + for (info_hash, peer_id) in announced_info_hashes { + torrents.handle_connection_closed(info_hash, peer_id, ip_version); + } + } + } + } +} + +async fn handle_request_stream( + config: Config, + torrents: Rc>, + server_start_instant: ServerStartInstant, + out_message_senders: Rc>, + stream: S, +) where + S: futures_lite::Stream + ::std::marker::Unpin, +{ + let rng = Rc::new(RefCell::new(SmallRng::from_entropy())); + let config = &config; + let torrents = &torrents; + let rng = &rng; + let out_message_senders = &out_message_senders; + + stream + .for_each_concurrent( + SHARED_IN_CHANNEL_SIZE, + move |(meta, in_message)| async move { + let mut out_messages = Vec::new(); + + match in_message { + InMessage::AnnounceRequest(request) => { + torrents.borrow_mut().handle_announce_request( + config, + &mut rng.borrow_mut(), + &mut out_messages, + server_start_instant, + meta, + request, + ) + } + InMessage::ScrapeRequest(request) => torrents + .borrow_mut() + .handle_scrape_request(config, &mut out_messages, meta, request), + }; + + for (meta, out_message) in out_messages { + out_message_senders + .send_to(meta.out_message_consumer_id.0 as usize, (meta, out_message)) + .await + .expect("failed sending out_message to socket worker"); + + ::log::debug!("swarm worker sent OutMessage to socket worker"); + } + }, + ) + .await; +} diff --git a/apps/aquatic/crates/ws/src/workers/swarm/storage.rs b/apps/aquatic/crates/ws/src/workers/swarm/storage.rs new file mode 100644 index 0000000..1120305 --- /dev/null +++ b/apps/aquatic/crates/ws/src/workers/swarm/storage.rs @@ -0,0 +1,726 @@ +use std::sync::Arc; + +use aquatic_common::access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache}; +use aquatic_ws_protocol::incoming::{ + AnnounceEvent, AnnounceRequest, AnnounceRequestOffer, ScrapeRequest, +}; +use aquatic_ws_protocol::outgoing::{ + AnnounceResponse, AnswerOutMessage, ErrorResponse, ErrorResponseAction, OfferOutMessage, + OutMessage, ScrapeResponse, ScrapeStatistics, +}; +use hashbrown::HashMap; +use rand::rngs::SmallRng; + +use aquatic_common::{IndexMap, SecondsSinceServerStart, ServerStartInstant}; +use aquatic_ws_protocol::common::*; +use rand::Rng; + +use crate::common::*; +use crate::config::Config; + +pub struct TorrentMaps { + ipv4: TorrentMap, + ipv6: TorrentMap, +} + +impl TorrentMaps { + pub fn new(worker_index: usize) -> Self { + Self { + ipv4: TorrentMap::new(worker_index, IpVersion::V4), + ipv6: TorrentMap::new(worker_index, IpVersion::V6), + } + } + + pub fn handle_announce_request( + &mut self, + config: &Config, + rng: &mut SmallRng, + out_messages: &mut Vec<(OutMessageMeta, OutMessage)>, + server_start_instant: ServerStartInstant, + request_sender_meta: InMessageMeta, + request: AnnounceRequest, + ) { + let torrent_map = self.get_torrent_map_by_ip_version(request_sender_meta.ip_version); + + torrent_map.handle_announce_request( + config, + rng, + out_messages, + server_start_instant, + request_sender_meta, + request, + ); + } + + pub fn handle_scrape_request( + &mut self, + config: &Config, + out_messages: &mut Vec<(OutMessageMeta, OutMessage)>, + meta: InMessageMeta, + request: ScrapeRequest, + ) { + let torrent_map = self.get_torrent_map_by_ip_version(meta.ip_version); + + torrent_map.handle_scrape_request(config, out_messages, meta, request); + } + + pub fn clean( + &mut self, + config: &Config, + access_list: &Arc, + server_start_instant: ServerStartInstant, + ) { + let mut access_list_cache = create_access_list_cache(access_list); + let now = server_start_instant.seconds_elapsed(); + + self.ipv4.clean(config, &mut access_list_cache, now); + self.ipv6.clean(config, &mut access_list_cache, now); + } + + #[cfg(feature = "metrics")] + pub fn update_torrent_count_metrics(&self) { + self.ipv4.update_torrent_gauge(); + self.ipv6.update_torrent_gauge(); + } + + pub fn handle_connection_closed( + &mut self, + info_hash: InfoHash, + peer_id: PeerId, + ip_version: IpVersion, + ) { + let torrent_map = self.get_torrent_map_by_ip_version(ip_version); + + torrent_map.handle_connection_closed(info_hash, peer_id); + } + + fn get_torrent_map_by_ip_version(&mut self, ip_version: IpVersion) -> &mut TorrentMap { + match ip_version { + IpVersion::V4 => &mut self.ipv4, + IpVersion::V6 => &mut self.ipv6, + } + } +} + +struct TorrentMap { + torrents: IndexMap, + #[cfg(feature = "metrics")] + torrent_gauge: ::metrics::Gauge, + #[cfg(feature = "metrics")] + peer_gauge: ::metrics::Gauge, +} + +impl TorrentMap { + pub fn new(worker_index: usize, ip_version: IpVersion) -> Self { + #[cfg(feature = "metrics")] + let peer_gauge = match ip_version { + IpVersion::V4 => ::metrics::gauge!( + "aquatic_peers", + "ip_version" => "4", + "worker_index" => worker_index.to_string(), + ), + IpVersion::V6 => ::metrics::gauge!( + "aquatic_peers", + "ip_version" => "6", + "worker_index" => worker_index.to_string(), + ), + }; + #[cfg(feature = "metrics")] + let torrent_gauge = match ip_version { + IpVersion::V4 => ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => "4", + "worker_index" => worker_index.to_string(), + ), + IpVersion::V6 => ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => "6", + "worker_index" => worker_index.to_string(), + ), + }; + + Self { + torrents: Default::default(), + #[cfg(feature = "metrics")] + peer_gauge, + #[cfg(feature = "metrics")] + torrent_gauge, + } + } + + pub fn handle_announce_request( + &mut self, + config: &Config, + rng: &mut SmallRng, + out_messages: &mut Vec<(OutMessageMeta, OutMessage)>, + server_start_instant: ServerStartInstant, + request_sender_meta: InMessageMeta, + request: AnnounceRequest, + ) { + let torrent_data = self.torrents.entry(request.info_hash).or_default(); + + // If there is already a peer with this peer_id, check that connection id + // is same as that of request sender. Otherwise, ignore request. Since + // peers have access to each others peer_id's, they could send requests + // using them, causing all sorts of issues. + if let Some(previous_peer) = torrent_data.peers.get(&request.peer_id) { + if request_sender_meta.connection_id != previous_peer.connection_id { + return; + } + } + + ::log::trace!("received request from {:?}", request_sender_meta); + + let peer_status = torrent_data.insert_or_update_peer( + config, + server_start_instant, + request_sender_meta, + &request, + #[cfg(feature = "metrics")] + &self.peer_gauge, + ); + + if peer_status != PeerStatus::Stopped { + if let Some(offers) = request.offers { + torrent_data.handle_offers( + config, + rng, + server_start_instant, + request.info_hash, + request.peer_id, + offers, + out_messages, + ); + } + + if let (Some(answer), Some(answer_receiver_id), Some(offer_id)) = ( + request.answer, + request.answer_to_peer_id, + request.answer_offer_id, + ) { + let opt_out_message = torrent_data.handle_answer( + request_sender_meta, + request.info_hash, + request.peer_id, + answer_receiver_id, + offer_id, + answer, + ); + + if let Some(out_message) = opt_out_message { + out_messages.push(out_message); + } + } + } + + let response = OutMessage::AnnounceResponse(AnnounceResponse { + action: AnnounceAction::Announce, + info_hash: request.info_hash, + complete: torrent_data.num_seeders, + incomplete: torrent_data.num_leechers(), + announce_interval: config.protocol.peer_announce_interval, + }); + + out_messages.push((request_sender_meta.into(), response)); + } + + pub fn handle_scrape_request( + &mut self, + config: &Config, + out_messages: &mut Vec<(OutMessageMeta, OutMessage)>, + meta: InMessageMeta, + request: ScrapeRequest, + ) { + let info_hashes = if let Some(info_hashes) = request.info_hashes { + info_hashes.as_vec() + } else { + return; + }; + + let num_to_take = info_hashes.len().min(config.protocol.max_scrape_torrents); + + let mut out_message = ScrapeResponse { + action: ScrapeAction::Scrape, + files: HashMap::with_capacity(num_to_take), + }; + + for info_hash in info_hashes.into_iter().take(num_to_take) { + if let Some(torrent_data) = self.torrents.get(&info_hash) { + let stats = ScrapeStatistics { + complete: torrent_data.num_seeders, + downloaded: 0, // No implementation planned + incomplete: torrent_data.num_leechers(), + }; + + out_message.files.insert(info_hash, stats); + } + } + + out_messages.push((meta.into(), OutMessage::ScrapeResponse(out_message))); + } + + pub fn handle_connection_closed(&mut self, info_hash: InfoHash, peer_id: PeerId) { + if let Some(torrent_data) = self.torrents.get_mut(&info_hash) { + torrent_data.handle_connection_closed( + peer_id, + #[cfg(feature = "metrics")] + &self.peer_gauge, + ); + } + } + + #[cfg(feature = "metrics")] + pub fn update_torrent_gauge(&self) { + self.torrent_gauge.set(self.torrents.len() as f64); + } + + fn clean( + &mut self, + config: &Config, + access_list_cache: &mut AccessListCache, + now: SecondsSinceServerStart, + ) { + let mut total_num_peers = 0u64; + + self.torrents.retain(|info_hash, torrent_data| { + if !access_list_cache + .load() + .allows(config.access_list.mode, &info_hash.0) + { + return false; + } + + let num_peers = torrent_data.clean_and_get_num_peers(now); + + total_num_peers += num_peers as u64; + + num_peers > 0 + }); + + self.torrents.shrink_to_fit(); + + #[cfg(feature = "metrics")] + self.peer_gauge.set(total_num_peers as f64); + + #[cfg(feature = "metrics")] + self.update_torrent_gauge(); + } +} + +#[derive(Default)] +struct TorrentData { + peers: IndexMap, + num_seeders: usize, +} + +impl TorrentData { + fn num_leechers(&self) -> usize { + self.peers.len() - self.num_seeders + } + + pub fn insert_or_update_peer( + &mut self, + config: &Config, + server_start_instant: ServerStartInstant, + request_sender_meta: InMessageMeta, + request: &AnnounceRequest, + #[cfg(feature = "metrics")] peer_gauge: &::metrics::Gauge, + ) -> PeerStatus { + let valid_until = ValidUntil::new(server_start_instant, config.cleaning.max_peer_age); + + let peer_status = PeerStatus::from_event_and_bytes_left( + request.event.unwrap_or_default(), + request.bytes_left, + ); + + match self.peers.entry(request.peer_id) { + ::indexmap::map::Entry::Occupied(mut entry) => match peer_status { + PeerStatus::Leeching => { + let peer = entry.get_mut(); + + if peer.seeder { + self.num_seeders -= 1; + } + + peer.seeder = false; + peer.valid_until = valid_until; + } + PeerStatus::Seeding => { + let peer = entry.get_mut(); + + if !peer.seeder { + self.num_seeders += 1; + } + + peer.seeder = true; + peer.valid_until = valid_until; + } + PeerStatus::Stopped => { + let peer = entry.swap_remove(); + + if peer.seeder { + self.num_seeders -= 1; + } + + #[cfg(feature = "metrics")] + peer_gauge.decrement(1.0); + } + }, + ::indexmap::map::Entry::Vacant(entry) => match peer_status { + PeerStatus::Leeching => { + let peer = Peer { + connection_id: request_sender_meta.connection_id, + consumer_id: request_sender_meta.out_message_consumer_id, + seeder: false, + valid_until, + expecting_answers: Default::default(), + }; + + entry.insert(peer); + + #[cfg(feature = "metrics")] + peer_gauge.increment(1.0) + } + PeerStatus::Seeding => { + self.num_seeders += 1; + + let peer = Peer { + connection_id: request_sender_meta.connection_id, + consumer_id: request_sender_meta.out_message_consumer_id, + seeder: true, + valid_until, + expecting_answers: Default::default(), + }; + + entry.insert(peer); + + #[cfg(feature = "metrics")] + peer_gauge.increment(1.0); + } + PeerStatus::Stopped => (), + }, + } + + peer_status + } + + /// Pass on offers to random peers + #[allow(clippy::too_many_arguments)] + pub fn handle_offers( + &mut self, + config: &Config, + rng: &mut SmallRng, + server_start_instant: ServerStartInstant, + info_hash: InfoHash, + sender_peer_id: PeerId, + offers: Vec, + out_messages: &mut Vec<(OutMessageMeta, OutMessage)>, + ) { + let max_num_peers_to_take = offers.len().min(config.protocol.max_offers); + + let offer_receivers: Vec<(PeerId, ConnectionId, ConsumerId)> = extract_response_peers( + rng, + &self.peers, + max_num_peers_to_take, + sender_peer_id, + |peer_id, peer| (*peer_id, peer.connection_id, peer.consumer_id), + ); + + if let Some(peer) = self.peers.get_mut(&sender_peer_id) { + for ( + offer, + (offer_receiver_peer_id, offer_receiver_connection_id, offer_receiver_consumer_id), + ) in offers.into_iter().zip(offer_receivers) + { + peer.expecting_answers.insert( + ExpectingAnswer { + from_peer_id: offer_receiver_peer_id, + regarding_offer_id: offer.offer_id, + }, + ValidUntil::new(server_start_instant, config.cleaning.max_offer_age), + ); + + let offer_out_message = OfferOutMessage { + action: AnnounceAction::Announce, + info_hash, + peer_id: sender_peer_id, + offer: offer.offer, + offer_id: offer.offer_id, + }; + + let meta = OutMessageMeta { + out_message_consumer_id: offer_receiver_consumer_id, + connection_id: offer_receiver_connection_id, + pending_scrape_id: None, + }; + + out_messages.push((meta, OutMessage::OfferOutMessage(offer_out_message))); + } + } + } + + /// Pass on answer to relevant peer + fn handle_answer( + &mut self, + request_sender_meta: InMessageMeta, + info_hash: InfoHash, + peer_id: PeerId, + answer_receiver_id: PeerId, + offer_id: OfferId, + answer: RtcAnswer, + ) -> Option<(OutMessageMeta, OutMessage)> { + if let Some(answer_receiver) = self.peers.get_mut(&answer_receiver_id) { + let expecting_answer = ExpectingAnswer { + from_peer_id: peer_id, + regarding_offer_id: offer_id, + }; + + if answer_receiver + .expecting_answers + .swap_remove(&expecting_answer) + .is_some() + { + let answer_out_message = AnswerOutMessage { + action: AnnounceAction::Announce, + peer_id, + info_hash, + answer, + offer_id, + }; + + let meta = OutMessageMeta { + out_message_consumer_id: answer_receiver.consumer_id, + connection_id: answer_receiver.connection_id, + pending_scrape_id: None, + }; + + Some((meta, OutMessage::AnswerOutMessage(answer_out_message))) + } else { + let error_message = ErrorResponse { + action: Some(ErrorResponseAction::Announce), + info_hash: Some(info_hash), + failure_reason: + "Could not find the offer corresponding to your answer. It may have expired." + .into(), + }; + + Some(( + request_sender_meta.into(), + OutMessage::ErrorResponse(error_message), + )) + } + } else { + None + } + } + + pub fn handle_connection_closed( + &mut self, + peer_id: PeerId, + #[cfg(feature = "metrics")] peer_gauge: &::metrics::Gauge, + ) { + if let Some(peer) = self.peers.swap_remove(&peer_id) { + if peer.seeder { + self.num_seeders -= 1; + } + + #[cfg(feature = "metrics")] + peer_gauge.decrement(1.0); + } + } + + fn clean_and_get_num_peers(&mut self, now: SecondsSinceServerStart) -> usize { + self.peers.retain(|_, peer| { + peer.expecting_answers + .retain(|_, valid_until| valid_until.valid(now)); + peer.expecting_answers.shrink_to_fit(); + + let keep = peer.valid_until.valid(now); + + if (!keep) & peer.seeder { + self.num_seeders -= 1; + } + + keep + }); + + self.peers.shrink_to_fit(); + + self.peers.len() + } +} + +#[derive(Clone, Debug)] +struct Peer { + pub consumer_id: ConsumerId, + pub connection_id: ConnectionId, + pub seeder: bool, + pub valid_until: ValidUntil, + pub expecting_answers: IndexMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ExpectingAnswer { + pub from_peer_id: PeerId, + pub regarding_offer_id: OfferId, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum PeerStatus { + Seeding, + Leeching, + Stopped, +} + +impl PeerStatus { + /// Determine peer status from announce event and number of bytes left. + /// + /// Likely, the last branch will be taken most of the time. + #[inline] + fn from_event_and_bytes_left(event: AnnounceEvent, opt_bytes_left: Option) -> Self { + if let AnnounceEvent::Stopped = event { + Self::Stopped + } else if let Some(0) = opt_bytes_left { + Self::Seeding + } else { + Self::Leeching + } + } +} + +/// Extract response peers +/// +/// If there are more peers in map than `max_num_peers_to_take`, do a random +/// selection of peers from first and second halves of map in order to avoid +/// returning too homogeneous peers. +/// +/// Filters out announcing peer. +#[inline] +pub fn extract_response_peers( + rng: &mut impl Rng, + peer_map: &IndexMap, + max_num_peers_to_take: usize, + sender_peer_map_key: K, + peer_conversion_function: F, +) -> Vec +where + K: Eq + ::std::hash::Hash, + F: Fn(&K, &V) -> R, +{ + if peer_map.len() <= max_num_peers_to_take + 1 { + // This branch: number of peers in map (minus sender peer) is less than + // or equal to number of peers to take, so return all except sender + // peer. + let mut peers = Vec::with_capacity(peer_map.len()); + + peers.extend(peer_map.iter().filter_map(|(k, v)| { + (*k != sender_peer_map_key).then_some(peer_conversion_function(k, v)) + })); + + // Handle the case when sender peer is not in peer list. Typically, + // this function will not be called when this is the case. + if peers.len() > max_num_peers_to_take { + peers.pop(); + } + + peers + } else { + // Note: if this branch is taken, the peer map contains at least two + // more peers than max_num_peers_to_take + + let middle_index = peer_map.len() / 2; + // Add one to take two extra peers in case sender peer is among + // selected peers and will need to be filtered out + let num_to_take_per_half = (max_num_peers_to_take / 2) + 1; + + let offset_half_one = { + let from = 0; + let to = usize::max(1, middle_index - num_to_take_per_half); + + rng.gen_range(from..to) + }; + let offset_half_two = { + let from = middle_index; + let to = usize::max(middle_index + 1, peer_map.len() - num_to_take_per_half); + + rng.gen_range(from..to) + }; + + let end_half_one = offset_half_one + num_to_take_per_half; + let end_half_two = offset_half_two + num_to_take_per_half; + + let mut peers = Vec::with_capacity(max_num_peers_to_take + 2); + + if let Some(slice) = peer_map.get_range(offset_half_one..end_half_one) { + peers.extend(slice.iter().filter_map(|(k, v)| { + (*k != sender_peer_map_key).then_some(peer_conversion_function(k, v)) + })); + } + if let Some(slice) = peer_map.get_range(offset_half_two..end_half_two) { + peers.extend(slice.iter().filter_map(|(k, v)| { + (*k != sender_peer_map_key).then_some(peer_conversion_function(k, v)) + })); + } + + while peers.len() > max_num_peers_to_take { + peers.pop(); + } + + peers + } +} + +#[cfg(test)] +mod tests { + use hashbrown::HashSet; + use rand::{rngs::SmallRng, SeedableRng}; + + use super::*; + + #[test] + fn test_extract_response_peers() { + let mut rng = SmallRng::from_entropy(); + + for num_peers_in_map in 0..50 { + for max_num_peers_to_take in 0..50 { + for sender_peer_map_key in 0..50 { + test_extract_response_peers_helper( + &mut rng, + num_peers_in_map, + max_num_peers_to_take, + sender_peer_map_key, + ); + } + } + } + } + + fn test_extract_response_peers_helper( + rng: &mut SmallRng, + num_peers_in_map: usize, + max_num_peers_to_take: usize, + sender_peer_map_key: usize, + ) { + let peer_map = IndexMap::from_iter((0..num_peers_in_map).map(|i| (i, i))); + + let response_peers = extract_response_peers( + rng, + &peer_map, + max_num_peers_to_take, + sender_peer_map_key, + |_, p| *p, + ); + + if num_peers_in_map > max_num_peers_to_take + 1 { + assert_eq!(response_peers.len(), max_num_peers_to_take); + } else { + assert!(response_peers.len() <= max_num_peers_to_take); + } + + assert!(!response_peers.contains(&sender_peer_map_key)); + + let unique: HashSet<_> = response_peers.iter().copied().collect(); + + assert_eq!(response_peers.len(), unique.len(),); + } +} diff --git a/apps/aquatic/crates/ws_load_test/Cargo.toml b/apps/aquatic/crates/ws_load_test/Cargo.toml new file mode 100644 index 0000000..677e606 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "aquatic_ws_load_test" +description = "WebTorrent over TLS load tester" +keywords = ["webtorrent", "websocket", "benchmark", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "README.md" + +[[bin]] +name = "aquatic_ws_load_test" + +[dependencies] +aquatic_common.workspace = true +aquatic_toml_config.workspace = true +aquatic_ws_protocol.workspace = true + +anyhow = "1" +async-tungstenite = "0.28" +futures = "0.3" +futures-rustls = "0.26" +glommio = "0.9" +log = "0.4" +mimalloc = { version = "0.1", default-features = false } +rand = { version = "0.8", features = ["small_rng"] } +rand_distr = "0.4" +rustls = { version = "0.23" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tungstenite = "0.24" + +[dev-dependencies] +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/ws_load_test/README.md b/apps/aquatic/crates/ws_load_test/README.md new file mode 100644 index 0000000..5e31a22 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/README.md @@ -0,0 +1,55 @@ +# aquatic_ws_load_test: WebTorrent tracker load tester + +[![CI](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml/badge.svg)](https://github.com/greatest-ape/aquatic/actions/workflows/ci.yml) + +Load tester for WebTorrent trackers. Requires Linux 5.8 or later. + +## Usage + +### Compiling + +- Install Rust with [rustup](https://rustup.rs/) (latest stable release is recommended) +- Install build dependencies with your package manager (e.g., `apt-get install cmake build-essential`) +- Clone this git repository and build the application: + +```sh +git clone https://github.com/greatest-ape/aquatic.git && cd aquatic + +# Recommended: tell Rust to enable support for all SIMD extensions present on +# current CPU except for those relating to AVX-512. (If you run a processor +# that doesn't clock down when using AVX-512, you can enable those instructions +# too.) +. ./scripts/env-native-cpu-without-avx-512 + +cargo build --release -p aquatic_ws_load_test +``` + +### Configuring and running + +Generate the configuration file: + +```sh +./target/release/aquatic_ws_load_test -p > "load-test-config.toml" +``` + +Make necessary adjustments to the file. + +Make sure locked memory limits are sufficient: + +```sh +ulimit -l 65536 +``` + +First, start the tracker application that you want to test. Then +start the load tester: + +```sh +./target/release/aquatic_ws_load_test -c "load-test-config.toml" +``` + +## Copyright and license + +Copyright (c) Joakim Frostegård + +Distributed under the terms of the Apache License, Version 2.0. Please refer to +the `LICENSE` file in the repository root directory for details. diff --git a/apps/aquatic/crates/ws_load_test/src/common.rs b/apps/aquatic/crates/ws_load_test/src/common.rs new file mode 100644 index 0000000..d1d04c5 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/src/common.rs @@ -0,0 +1,29 @@ +use std::sync::{atomic::AtomicUsize, Arc}; + +use aquatic_ws_protocol::common::InfoHash; +use rand_distr::Gamma; + +#[derive(Default)] +pub struct Statistics { + pub requests: AtomicUsize, + pub response_peers: AtomicUsize, + pub responses_announce: AtomicUsize, + pub responses_offer: AtomicUsize, + pub responses_answer: AtomicUsize, + pub responses_scrape: AtomicUsize, + pub responses_error: AtomicUsize, + pub connections: AtomicUsize, +} + +#[derive(Clone)] +pub struct LoadTestState { + pub info_hashes: Arc<[InfoHash]>, + pub statistics: Arc, + pub gamma: Arc>, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum RequestType { + Announce, + Scrape, +} diff --git a/apps/aquatic/crates/ws_load_test/src/config.rs b/apps/aquatic/crates/ws_load_test/src/config.rs new file mode 100644 index 0000000..cd759e9 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/src/config.rs @@ -0,0 +1,80 @@ +use std::net::SocketAddr; + +use aquatic_common::cli::LogLevel; +use aquatic_toml_config::TomlConfig; +use serde::Deserialize; + +/// aquatic_ws_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + pub server_address: SocketAddr, + pub log_level: LogLevel, + pub num_workers: usize, + pub num_connections_per_worker: usize, + pub connection_creation_interval_ms: u64, + pub duration: usize, + pub measure_after_max_connections_reached: bool, + pub torrents: TorrentConfig, +} + +impl aquatic_common::cli::Config for Config { + fn get_log_level(&self) -> Option { + Some(self.log_level) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + server_address: "127.0.0.1:3000".parse().unwrap(), + log_level: LogLevel::Warn, + num_workers: 1, + num_connections_per_worker: 16, + connection_creation_interval_ms: 10, + duration: 0, + measure_after_max_connections_reached: true, + torrents: TorrentConfig::default(), + } + } +} + +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct TorrentConfig { + pub offers_per_request: usize, + pub number_of_torrents: usize, + /// Probability that a generated peer is a seeder + pub peer_seeder_probability: f64, + /// Probability that a generated request is a announce request, as part + /// of sum of the various weight arguments. + pub weight_announce: usize, + /// Probability that a generated request is a scrape request, as part + /// of sum of the various weight arguments. + pub weight_scrape: usize, + /// Peers choose torrents according to this Gamma distribution shape + pub torrent_gamma_shape: f64, + /// Peers choose torrents according to this Gamma distribution scale + pub torrent_gamma_scale: f64, +} + +impl Default for TorrentConfig { + fn default() -> Self { + Self { + offers_per_request: 10, + number_of_torrents: 10_000, + peer_seeder_probability: 0.25, + weight_announce: 5, + weight_scrape: 0, + torrent_gamma_shape: 0.2, + torrent_gamma_scale: 100.0, + } + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + ::aquatic_toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/apps/aquatic/crates/ws_load_test/src/main.rs b/apps/aquatic/crates/ws_load_test/src/main.rs new file mode 100644 index 0000000..94e91e8 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/src/main.rs @@ -0,0 +1,237 @@ +use std::sync::{atomic::Ordering, Arc}; +use std::thread; +use std::time::{Duration, Instant}; + +use aquatic_ws_protocol::common::InfoHash; +use glommio::LocalExecutorBuilder; +use rand::prelude::*; +use rand_distr::Gamma; + +mod common; +mod config; +mod network; +mod utils; + +use common::*; +use config::*; +use network::*; + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +pub fn main() { + aquatic_common::cli::run_app_with_cli_and_config::( + "aquatic_ws_load_test: WebTorrent load tester", + env!("CARGO_PKG_VERSION"), + run, + None, + ) +} + +fn run(config: Config) -> ::anyhow::Result<()> { + if config.torrents.weight_announce + config.torrents.weight_scrape == 0 { + panic!("Error: at least one weight must be larger than zero."); + } + + println!("Starting client with config: {:#?}", config); + + let mut rng = SmallRng::from_entropy(); + + let mut info_hashes = Vec::with_capacity(config.torrents.number_of_torrents); + + for _ in 0..config.torrents.number_of_torrents { + info_hashes.push(InfoHash(rng.gen())); + } + + let gamma = Gamma::new( + config.torrents.torrent_gamma_shape, + config.torrents.torrent_gamma_scale, + ) + .unwrap(); + + let state = LoadTestState { + info_hashes: Arc::from(info_hashes.into_boxed_slice()), + statistics: Arc::new(Statistics::default()), + gamma: Arc::new(gamma), + }; + + let tls_config = create_tls_config().unwrap(); + + for _ in 0..config.num_workers { + let config = config.clone(); + let tls_config = tls_config.clone(); + let state = state.clone(); + + LocalExecutorBuilder::default() + .name("load-test") + .spawn(move || async move { + run_socket_thread(config, tls_config, state).await.unwrap(); + }) + .unwrap(); + } + + monitor_statistics(state, &config); + + Ok(()) +} + +#[derive(Debug)] +struct FakeCertificateVerifier; + +impl rustls::client::danger::ServerCertVerifier for FakeCertificateVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::pki_types::CertificateDer<'_>, + _intermediates: &[rustls::pki_types::CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls::pki_types::CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::ED25519, + ] + } +} + +fn create_tls_config() -> anyhow::Result> { + let mut config = rustls::ClientConfig::builder() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + + config + .dangerous() + .set_certificate_verifier(Arc::new(FakeCertificateVerifier)); + + Ok(Arc::new(config)) +} + +fn monitor_statistics(state: LoadTestState, config: &Config) { + let start_time = Instant::now(); + let mut time_max_connections_reached = None; + let mut report_avg_response_vec: Vec = Vec::new(); + + let interval = 5; + let interval_f64 = interval as f64; + + loop { + thread::sleep(Duration::from_secs(interval)); + + let statistics = state.statistics.as_ref(); + + let responses_announce = statistics + .responses_announce + .fetch_and(0, Ordering::Relaxed) as f64; + // let response_peers = statistics.response_peers + // .fetch_and(0, Ordering::Relaxed) as f64; + + let requests_per_second = + statistics.requests.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_offer_per_second = + statistics.responses_offer.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_answer_per_second = + statistics.responses_answer.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_scrape_per_second = + statistics.responses_scrape.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + let responses_error_per_second = + statistics.responses_error.fetch_and(0, Ordering::Relaxed) as f64 / interval_f64; + + let responses_announce_per_second = responses_announce / interval_f64; + + let connections = statistics.connections.load(Ordering::Relaxed); + + let responses_per_second = responses_announce_per_second + + responses_offer_per_second + + responses_answer_per_second + + responses_scrape_per_second + + responses_error_per_second; + + if !config.measure_after_max_connections_reached || time_max_connections_reached.is_some() { + report_avg_response_vec.push(responses_per_second); + } else if connections >= config.num_workers * config.num_connections_per_worker { + time_max_connections_reached = Some(Instant::now()); + + println!(); + println!("Max connections reached"); + println!(); + } + + println!(); + println!("Requests out: {:.2}/second", requests_per_second); + println!("Responses in: {:.2}/second", responses_per_second); + println!( + " - Announce responses: {:.2}", + responses_announce_per_second + ); + println!(" - Offer responses: {:.2}", responses_offer_per_second); + println!(" - Answer responses: {:.2}", responses_answer_per_second); + println!(" - Scrape responses: {:.2}", responses_scrape_per_second); + println!(" - Error responses: {:.2}", responses_error_per_second); + println!("Active connections: {}", connections); + + if config.measure_after_max_connections_reached { + if let Some(start) = time_max_connections_reached { + let time_elapsed = start.elapsed(); + + if config.duration != 0 + && time_elapsed >= Duration::from_secs(config.duration as u64) + { + report(config, report_avg_response_vec, time_elapsed); + + break; + } + } + } else { + let time_elapsed = start_time.elapsed(); + + if config.duration != 0 && time_elapsed >= Duration::from_secs(config.duration as u64) { + report(config, report_avg_response_vec, time_elapsed); + + break; + } + } + } +} + +fn report(config: &Config, report_avg_response_vec: Vec, time_elapsed: Duration) { + let report_len = report_avg_response_vec.len() as f64; + let report_sum: f64 = report_avg_response_vec.into_iter().sum(); + let report_avg: f64 = report_sum / report_len; + + println!( + concat!( + "\n# aquatic load test report\n\n", + "Test ran for {} seconds.\n", + "Average responses per second: {:.2}\n\nConfig: {:#?}\n" + ), + time_elapsed.as_secs(), + report_avg, + config + ); +} diff --git a/apps/aquatic/crates/ws_load_test/src/network.rs b/apps/aquatic/crates/ws_load_test/src/network.rs new file mode 100644 index 0000000..a0aa125 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/src/network.rs @@ -0,0 +1,322 @@ +use std::{ + cell::RefCell, + convert::TryInto, + rc::Rc, + sync::{atomic::Ordering, Arc}, + time::Duration, +}; + +use aquatic_ws_protocol::incoming::{ + AnnounceEvent, AnnounceRequest, AnnounceRequestOffer, InMessage, ScrapeRequestInfoHashes, +}; +use aquatic_ws_protocol::outgoing::OutMessage; +use aquatic_ws_protocol::{ + common::{ + AnnounceAction, InfoHash, OfferId, PeerId, RtcAnswer, RtcAnswerType, RtcOffer, + RtcOfferType, ScrapeAction, + }, + incoming::ScrapeRequest, +}; +use async_tungstenite::{client_async, WebSocketStream}; +use futures::{SinkExt, StreamExt}; +use futures_rustls::{client::TlsStream, TlsConnector}; +use glommio::net::TcpStream; +use glommio::{prelude::*, timer::TimerActionRepeat}; +use rand::{prelude::SmallRng, Rng, SeedableRng}; +use rand_distr::{Distribution, WeightedIndex}; + +use crate::{ + common::{LoadTestState, RequestType}, + config::Config, + utils::select_info_hash_index, +}; + +pub async fn run_socket_thread( + config: Config, + tls_config: Arc, + load_test_state: LoadTestState, +) -> anyhow::Result<()> { + let config = Rc::new(config); + let rng = Rc::new(RefCell::new(SmallRng::from_entropy())); + let num_active_connections = Rc::new(RefCell::new(0usize)); + let connection_creation_interval = + Duration::from_millis(config.connection_creation_interval_ms); + + TimerActionRepeat::repeat(move || { + periodically_open_connections( + config.clone(), + tls_config.clone(), + load_test_state.clone(), + num_active_connections.clone(), + rng.clone(), + connection_creation_interval, + ) + }) + .join() + .await; + + Ok(()) +} + +async fn periodically_open_connections( + config: Rc, + tls_config: Arc, + load_test_state: LoadTestState, + num_active_connections: Rc>, + rng: Rc>, + connection_creation_interval: Duration, +) -> Option { + if *num_active_connections.borrow() < config.num_connections_per_worker { + spawn_local(async move { + if let Err(err) = Connection::run( + config, + tls_config, + load_test_state, + num_active_connections, + rng, + ) + .await + { + ::log::info!("connection creation error: {:#}", err); + } + }) + .detach(); + } + + Some(connection_creation_interval) +} + +struct Connection { + config: Rc, + load_test_state: LoadTestState, + rng: Rc>, + peer_id: PeerId, + can_send_answer: Option<(InfoHash, PeerId, OfferId)>, + stream: WebSocketStream>, +} + +impl Connection { + async fn run( + config: Rc, + tls_config: Arc, + load_test_state: LoadTestState, + num_active_connections: Rc>, + rng: Rc>, + ) -> anyhow::Result<()> { + let peer_id = PeerId(rng.borrow_mut().gen()); + let stream = TcpStream::connect(config.server_address) + .await + .map_err(|err| anyhow::anyhow!("connect: {:?}", err))?; + let stream = TlsConnector::from(tls_config) + .connect("example.com".try_into().unwrap(), stream) + .await?; + let request = format!( + "ws://{}:{}", + config.server_address.ip(), + config.server_address.port() + ); + let (stream, _) = client_async(request, stream).await?; + + let statistics = load_test_state.statistics.clone(); + + let mut connection = Connection { + config, + load_test_state, + rng, + stream, + peer_id, + can_send_answer: None, + }; + + *num_active_connections.borrow_mut() += 1; + statistics.connections.fetch_add(1, Ordering::Relaxed); + + if let Err(err) = connection.run_connection_loop().await { + ::log::info!("connection error: {:#}", err); + } + + *num_active_connections.borrow_mut() -= 1; + statistics.connections.fetch_sub(1, Ordering::Relaxed); + + Ok(()) + } + + async fn run_connection_loop(&mut self) -> anyhow::Result<()> { + loop { + self.send_message().await?; + self.read_message().await?; + } + } + + async fn send_message(&mut self) -> anyhow::Result<()> { + let request = self.create_request(); + + self.stream.send(request.to_ws_message()).await?; + + self.load_test_state + .statistics + .requests + .fetch_add(1, Ordering::Relaxed); + + Ok(()) + } + + fn create_request(&mut self) -> InMessage { + let mut rng = self.rng.borrow_mut(); + + let request = match random_request_type(&self.config, &mut *rng) { + RequestType::Announce => { + let (event, bytes_left) = { + if rng.gen_bool(self.config.torrents.peer_seeder_probability) { + (AnnounceEvent::Completed, 0) + } else { + (AnnounceEvent::Started, 50) + } + }; + + const SDP: &str = "abcdefg-abcdefg-abcdefg-abcdefg-abcdefg-abcdefg"; + + if let Some((info_hash, peer_id, offer_id)) = self.can_send_answer { + InMessage::AnnounceRequest(AnnounceRequest { + info_hash, + answer_to_peer_id: Some(peer_id), + answer_offer_id: Some(offer_id), + answer: Some(RtcAnswer { + t: RtcAnswerType::Answer, + sdp: SDP.into(), + }), + event: None, + offers: None, + action: AnnounceAction::Announce, + peer_id: self.peer_id, + bytes_left: Some(bytes_left), + numwant: Some(0), + }) + } else { + let info_hash_index = + select_info_hash_index(&self.config, &self.load_test_state, &mut *rng); + + let mut offers = Vec::with_capacity(self.config.torrents.offers_per_request); + + for _ in 0..self.config.torrents.offers_per_request { + offers.push(AnnounceRequestOffer { + offer_id: OfferId(rng.gen()), + offer: RtcOffer { + t: RtcOfferType::Offer, + sdp: SDP.into(), + }, + }) + } + + InMessage::AnnounceRequest(AnnounceRequest { + action: AnnounceAction::Announce, + info_hash: self.load_test_state.info_hashes[info_hash_index], + peer_id: self.peer_id, + bytes_left: Some(bytes_left), + event: Some(event), + numwant: Some(offers.len()), + offers: Some(offers), + answer: None, + answer_to_peer_id: None, + answer_offer_id: None, + }) + } + } + RequestType::Scrape => { + let mut scrape_hashes = Vec::with_capacity(5); + + for _ in 0..5 { + let info_hash_index = + select_info_hash_index(&self.config, &self.load_test_state, &mut *rng); + + scrape_hashes.push(self.load_test_state.info_hashes[info_hash_index]); + } + + InMessage::ScrapeRequest(ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: Some(ScrapeRequestInfoHashes::Multiple(scrape_hashes)), + }) + } + }; + + self.can_send_answer = None; + + request + } + + async fn read_message(&mut self) -> anyhow::Result<()> { + let message = match self + .stream + .next() + .await + .ok_or_else(|| anyhow::anyhow!("stream finished"))?? + { + message @ tungstenite::Message::Text(_) | message @ tungstenite::Message::Binary(_) => { + message + } + message => { + ::log::warn!( + "Received WebSocket message of unexpected type: {:?}", + message + ); + + return Ok(()); + } + }; + + match OutMessage::from_ws_message(message) { + Ok(OutMessage::OfferOutMessage(offer)) => { + self.load_test_state + .statistics + .responses_offer + .fetch_add(1, Ordering::Relaxed); + + self.can_send_answer = Some((offer.info_hash, offer.peer_id, offer.offer_id)); + } + Ok(OutMessage::AnswerOutMessage(_)) => { + self.load_test_state + .statistics + .responses_answer + .fetch_add(1, Ordering::Relaxed); + } + Ok(OutMessage::AnnounceResponse(_)) => { + self.load_test_state + .statistics + .responses_announce + .fetch_add(1, Ordering::Relaxed); + } + Ok(OutMessage::ScrapeResponse(_)) => { + self.load_test_state + .statistics + .responses_scrape + .fetch_add(1, Ordering::Relaxed); + } + Ok(OutMessage::ErrorResponse(response)) => { + self.load_test_state + .statistics + .responses_error + .fetch_add(1, Ordering::Relaxed); + + ::log::warn!("received error response: {:?}", response.failure_reason); + } + Err(err) => { + ::log::error!("error deserializing message: {:#}", err); + } + } + + Ok(()) + } +} + +pub fn random_request_type(config: &Config, rng: &mut impl Rng) -> RequestType { + let weights = [ + config.torrents.weight_announce as u32, + config.torrents.weight_scrape as u32, + ]; + + let items = [RequestType::Announce, RequestType::Scrape]; + + let dist = WeightedIndex::new(weights).expect("random request weighted index"); + + items[dist.sample(rng)] +} diff --git a/apps/aquatic/crates/ws_load_test/src/utils.rs b/apps/aquatic/crates/ws_load_test/src/utils.rs new file mode 100644 index 0000000..1c5c582 --- /dev/null +++ b/apps/aquatic/crates/ws_load_test/src/utils.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use rand::prelude::*; +use rand_distr::Gamma; + +use crate::common::*; +use crate::config::*; + +#[inline] +pub fn select_info_hash_index(config: &Config, state: &LoadTestState, rng: &mut impl Rng) -> usize { + gamma_usize(rng, &state.gamma, config.torrents.number_of_torrents - 1) +} + +#[inline] +fn gamma_usize(rng: &mut impl Rng, gamma: &Arc>, max: usize) -> usize { + let p: f64 = gamma.sample(rng); + let p = (p.min(101.0f64) - 1.0) / 100.0; + + (p * max as f64) as usize +} diff --git a/apps/aquatic/crates/ws_protocol/Cargo.toml b/apps/aquatic/crates/ws_protocol/Cargo.toml new file mode 100644 index 0000000..ffb4c46 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "aquatic_ws_protocol" +description = "WebTorrent tracker protocol" +exclude = ["target"] +keywords = ["webtorrent", "protocol", "peer-to-peer", "torrent", "bittorrent"] +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +readme = "./README.md" + +[features] +default = ["tungstenite"] +tungstenite = ["dep:tungstenite"] + +[lib] +name = "aquatic_ws_protocol" + +[[bench]] +name = "bench_deserialize_announce_request" +path = "benches/bench_deserialize_announce_request.rs" +harness = false + +[dependencies] +anyhow = "1" +hashbrown = { version = "0.15", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +simd-json = "0.14" +tungstenite = { version = "0.24", optional = true } + +[dev-dependencies] +criterion = "0.5" +quickcheck = "1" +quickcheck_macros = "1" diff --git a/apps/aquatic/crates/ws_protocol/README.md b/apps/aquatic/crates/ws_protocol/README.md new file mode 100644 index 0000000..f17b8be --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/README.md @@ -0,0 +1,4 @@ +# aquatic_ws_protocol: WebTorrent tracker protocol + +[WebTorrent](https://github.com/webtorrent) tracker message parsing and +serialization. \ No newline at end of file diff --git a/apps/aquatic/crates/ws_protocol/benches/bench_deserialize_announce_request.rs b/apps/aquatic/crates/ws_protocol/benches/bench_deserialize_announce_request.rs new file mode 100644 index 0000000..c239b16 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/benches/bench_deserialize_announce_request.rs @@ -0,0 +1,62 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::time::Duration; + +use aquatic_ws_protocol::{ + common::*, + incoming::{AnnounceEvent, AnnounceRequest, AnnounceRequestOffer, InMessage}, +}; + +pub fn bench(c: &mut Criterion) { + let info_hash = InfoHash([ + b'a', b'b', b'c', b'd', b'e', b'?', b'\n', b'1', b'2', b'3', 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, + ]); + let peer_id = PeerId(info_hash.0); + let offers: Vec = (0..10) + .map(|i| { + let mut offer_id = OfferId(info_hash.0); + + offer_id.0[i] = i as u8; + + AnnounceRequestOffer { + offer: RtcOffer { + t: RtcOfferType::Offer, + sdp: "abcdef".into(), + }, + offer_id, + } + }) + .collect(); + let offers_len = offers.len(); + + let request = InMessage::AnnounceRequest(AnnounceRequest { + action: AnnounceAction::Announce, + info_hash, + peer_id, + bytes_left: Some(2), + event: Some(AnnounceEvent::Started), + offers: Some(offers), + numwant: Some(offers_len), + answer: Some(RtcAnswer { + t: RtcAnswerType::Answer, + sdp: "abcdef".into(), + }), + answer_to_peer_id: Some(peer_id), + answer_offer_id: Some(OfferId(info_hash.0)), + }); + + let ws_message = request.to_ws_message(); + + c.bench_function("deserialize-announce-request", |b| { + b.iter(|| InMessage::from_ws_message(black_box(ws_message.clone()))) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .sample_size(1000) + .measurement_time(Duration::from_secs(180)) + .significance_level(0.01); + targets = bench +} +criterion_main!(benches); diff --git a/apps/aquatic/crates/ws_protocol/src/common.rs b/apps/aquatic/crates/ws_protocol/src/common.rs new file mode 100644 index 0000000..7c429cb --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/common.rs @@ -0,0 +1,223 @@ +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PeerId( + #[serde( + deserialize_with = "deserialize_20_bytes", + serialize_with = "serialize_20_bytes" + )] + pub [u8; 20], +); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InfoHash( + #[serde( + deserialize_with = "deserialize_20_bytes", + serialize_with = "serialize_20_bytes" + )] + pub [u8; 20], +); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct OfferId( + #[serde( + deserialize_with = "deserialize_20_bytes", + serialize_with = "serialize_20_bytes" + )] + pub [u8; 20], +); + +/// Serializes to and deserializes from "announce" +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AnnounceAction { + Announce, +} + +/// Serializes to and deserializes from "scrape" +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ScrapeAction { + Scrape, +} + +/// Serializes to and deserializes from "offer" +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RtcOfferType { + Offer, +} + +/// Serializes to and deserializes from "answer" +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RtcAnswerType { + Answer, +} + +/// Nested structure with SDP offer from https://www.npmjs.com/package/simple-peer +/// +/// Created using https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RtcOffer { + /// Always "offer" + #[serde(rename = "type")] + pub t: RtcOfferType, + pub sdp: String, +} + +/// Nested structure with SDP answer from https://www.npmjs.com/package/simple-peer +/// +/// Created using https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RtcAnswer { + /// Always "answer" + #[serde(rename = "type")] + pub t: RtcAnswerType, + pub sdp: String, +} + +fn serialize_20_bytes(data: &[u8; 20], serializer: S) -> Result +where + S: Serializer, +{ + // Length of 40 is enough since each char created from a byte will + // utf-8-encode to max 2 bytes + let mut str_buffer = [0u8; 40]; + let mut offset = 0; + + for byte in data { + offset += char::from(*byte) + .encode_utf8(&mut str_buffer[offset..]) + .len(); + } + + let text = ::std::str::from_utf8(&str_buffer[..offset]).unwrap(); + + serializer.serialize_str(text) +} + +struct TwentyByteVisitor; + +impl<'de> Visitor<'de> for TwentyByteVisitor { + type Value = [u8; 20]; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string consisting of 20 bytes") + } + + #[inline] + fn visit_str(self, value: &str) -> Result + where + E: ::serde::de::Error, + { + // Value is encoded in nodejs reference client something as follows: + // ``` + // var infoHash = 'abcd..'; // 40 hexadecimals + // Buffer.from(infoHash, 'hex').toString('binary'); + // ``` + // As I understand it: + // - the code above produces a UTF16 string of 20 chars, each having + // only the "low byte" set (e.g., numeric value ranges from 0-255) + // - serde_json decodes this to string of 20 chars (tested), each in + // the aforementioned range (tested), so the bytes can be extracted + // by casting each char to u8. + + let mut arr = [0u8; 20]; + let mut char_iter = value.chars(); + + for a in arr.iter_mut() { + if let Some(c) = char_iter.next() { + if c as u32 > 255 { + return Err(E::custom(format!( + "character not in single byte range: {:#?}", + c + ))); + } + + *a = c as u8; + } else { + return Err(E::custom(format!("not 20 bytes: {:#?}", value))); + } + } + + Ok(arr) + } +} + +#[inline] +fn deserialize_20_bytes<'de, D>(deserializer: D) -> Result<[u8; 20], D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(TwentyByteVisitor) +} + +#[cfg(test)] +mod tests { + use quickcheck_macros::quickcheck; + + use crate::common::InfoHash; + + fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash { + let mut arr = [0u8; 20]; + + assert!(bytes.len() == 20); + + arr.copy_from_slice(bytes); + + InfoHash(arr) + } + + #[test] + fn test_deserialize_20_bytes() { + unsafe { + let mut input = r#""aaaabbbbccccddddeeee""#.to_string(); + + let expected = info_hash_from_bytes(b"aaaabbbbccccddddeeee"); + let observed: InfoHash = ::simd_json::serde::from_str(&mut input).unwrap(); + + assert_eq!(observed, expected); + } + + unsafe { + let mut input = r#""aaaabbbbccccddddeee""#.to_string(); + let res_info_hash: Result = ::simd_json::serde::from_str(&mut input); + + assert!(res_info_hash.is_err()); + } + + unsafe { + let mut input = r#""aaaabbbbccccddddeee𝕊""#.to_string(); + let res_info_hash: Result = ::simd_json::serde::from_str(&mut input); + + assert!(res_info_hash.is_err()); + } + } + + #[test] + fn test_serde_20_bytes() { + let info_hash = info_hash_from_bytes(b"aaaabbbbccccddddeeee"); + + let info_hash_2 = unsafe { + let mut out = ::simd_json::serde::to_string(&info_hash).unwrap(); + + ::simd_json::serde::from_str(&mut out).unwrap() + }; + + assert_eq!(info_hash, info_hash_2); + } + + #[quickcheck] + fn quickcheck_serde_20_bytes(info_hash: InfoHash) -> bool { + unsafe { + let mut out = ::simd_json::serde::to_string(&info_hash).unwrap(); + let info_hash_2 = ::simd_json::serde::from_str(&mut out).unwrap(); + + info_hash == info_hash_2 + } + } +} diff --git a/apps/aquatic/crates/ws_protocol/src/incoming/announce.rs b/apps/aquatic/crates/ws_protocol/src/incoming/announce.rs new file mode 100644 index 0000000..36aef31 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/incoming/announce.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +/// Announce request +/// +/// Can optionally contain: +/// - A number of WebRTC offers to be sent on to other peers. In this case, +/// fields 'offers' and 'numwant' are set +/// - An answer to a WebRTC offer from another peer. In this case, fields +/// 'answer', 'to_peer_id' and 'offer_id' are set. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnnounceRequest { + /// Always "announce" + pub action: AnnounceAction, + pub info_hash: InfoHash, + pub peer_id: PeerId, + /// Bytes left + /// + /// Just called "left" in protocol. Is set to None in some cases, such as + /// when opening a magnet link + #[serde(rename = "left")] + pub bytes_left: Option, + /// Can be empty. Then, default is "update" + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option, + + /// WebRTC offers (with offer id's) that peer wants sent on to random other peers + /// + /// Notes from reference implementation: + /// - Only when this is an array offers are sent to other peers + /// - Length of this is number of peers wanted? + /// - Max length of this is 10 in reference client code + /// - Not sent when announce event is stopped or completed + pub offers: Option>, + /// Number of peers wanted + /// + /// Notes from reference implementation: + /// - Seems to only get sent by client when sending offers, and is also + /// same as length of offers vector (or at least never smaller) + /// - Max length of this is 10 in reference client code + /// - Could probably be ignored, `offers.len()` should provide needed info + pub numwant: Option, + + /// WebRTC answer to previous offer from other peer, to be passed on to it + /// + /// Notes from reference implementation: + /// - If empty, send response before sending offers (or possibly "skip + /// sending update back"?) + /// - Else, send AnswerOutMessage to peer with "to_peer_id" as peer_id + pub answer: Option, + /// Which peer to send answer to + #[serde(rename = "to_peer_id")] + pub answer_to_peer_id: Option, + /// OfferID of offer this is an answer to + #[serde(rename = "offer_id")] + pub answer_offer_id: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AnnounceEvent { + Started, + Stopped, + Completed, + Update, +} + +impl Default for AnnounceEvent { + fn default() -> Self { + Self::Update + } +} + +/// Element of AnnounceRequest.offers +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnnounceRequestOffer { + pub offer: RtcOffer, + pub offer_id: OfferId, +} diff --git a/apps/aquatic/crates/ws_protocol/src/incoming/mod.rs b/apps/aquatic/crates/ws_protocol/src/incoming/mod.rs new file mode 100644 index 0000000..5a57331 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/incoming/mod.rs @@ -0,0 +1,43 @@ +use anyhow::Context; +use serde::{Deserialize, Serialize}; + +pub mod announce; +pub mod scrape; + +pub use announce::*; +pub use scrape::*; + +/// Message received by tracker +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum InMessage { + AnnounceRequest(AnnounceRequest), + ScrapeRequest(ScrapeRequest), +} + +#[cfg(feature = "tungstenite")] +impl InMessage { + #[inline] + pub fn to_ws_message(&self) -> ::tungstenite::Message { + ::tungstenite::Message::from(::serde_json::to_string(&self).unwrap()) + } + + #[inline] + pub fn from_ws_message(ws_message: tungstenite::Message) -> ::anyhow::Result { + use tungstenite::Message; + + match ws_message { + Message::Text(text) => { + let mut text: Vec = text.as_bytes().to_owned(); + + ::simd_json::serde::from_slice(&mut text).context("deserialize with serde") + } + Message::Binary(bytes) => { + let mut bytes = bytes.to_vec(); + + ::simd_json::serde::from_slice(&mut bytes[..]).context("deserialize with serde") + } + _ => Err(anyhow::anyhow!("Message is neither text nor binary")), + } + } +} diff --git a/apps/aquatic/crates/ws_protocol/src/incoming/scrape.rs b/apps/aquatic/crates/ws_protocol/src/incoming/scrape.rs new file mode 100644 index 0000000..aa9a3d5 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/incoming/scrape.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScrapeRequest { + /// Always "scrape" + pub action: ScrapeAction, + /// Info hash or info hashes + /// + /// Notes from reference implementation: + /// - If omitted, scrape for all torrents, apparently + /// - Accepts a single info hash or an array of info hashes + #[serde(rename = "info_hash")] + pub info_hashes: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ScrapeRequestInfoHashes { + Single(InfoHash), + Multiple(Vec), +} + +impl ScrapeRequestInfoHashes { + pub fn as_vec(self) -> Vec { + match self { + Self::Single(info_hash) => vec![info_hash], + Self::Multiple(info_hashes) => info_hashes, + } + } +} diff --git a/apps/aquatic/crates/ws_protocol/src/lib.rs b/apps/aquatic/crates/ws_protocol/src/lib.rs new file mode 100644 index 0000000..1c91b00 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/lib.rs @@ -0,0 +1,377 @@ +//! WebTorrent protocol implementation +//! +//! Typical announce workflow: +//! - Peer A sends announce request with info hash and offers +//! - Tracker sends on offers to other peers announcing with that info hash and +//! sends back announce response to peer A +//! - Tracker receives answers to those offers from other peers and send them +//! on to peer A +//! +//! Typical scrape workflow +//! - Peer sends scrape request and receives scrape response + +pub mod common; +pub mod incoming; +pub mod outgoing; + +#[cfg(test)] +mod tests { + use quickcheck::Arbitrary; + use quickcheck_macros::quickcheck; + + use crate::{ + common::*, + incoming::{ + AnnounceEvent, AnnounceRequest, AnnounceRequestOffer, InMessage, ScrapeRequest, + ScrapeRequestInfoHashes, + }, + outgoing::{ + AnnounceResponse, AnswerOutMessage, OfferOutMessage, OutMessage, ScrapeResponse, + ScrapeStatistics, + }, + }; + + fn arbitrary_20_bytes(g: &mut quickcheck::Gen) -> [u8; 20] { + let mut bytes = [0u8; 20]; + + for byte in bytes.iter_mut() { + *byte = u8::arbitrary(g); + } + + bytes + } + + fn rtc_offer() -> RtcOffer { + RtcOffer { + t: RtcOfferType::Offer, + sdp: "test".into(), + } + } + fn rtc_answer() -> RtcAnswer { + RtcAnswer { + t: RtcAnswerType::Answer, + sdp: "test".into(), + } + } + + impl Arbitrary for InfoHash { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(arbitrary_20_bytes(g)) + } + } + + impl Arbitrary for PeerId { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(arbitrary_20_bytes(g)) + } + } + + impl Arbitrary for OfferId { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self(arbitrary_20_bytes(g)) + } + } + + impl Arbitrary for AnnounceEvent { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + match (bool::arbitrary(g), bool::arbitrary(g)) { + (false, false) => Self::Started, + (true, false) => Self::Started, + (false, true) => Self::Completed, + (true, true) => Self::Update, + } + } + } + + impl Arbitrary for OfferOutMessage { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + action: AnnounceAction::Announce, + peer_id: Arbitrary::arbitrary(g), + info_hash: Arbitrary::arbitrary(g), + offer_id: Arbitrary::arbitrary(g), + offer: rtc_offer(), + } + } + } + + impl Arbitrary for AnswerOutMessage { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + action: AnnounceAction::Announce, + peer_id: Arbitrary::arbitrary(g), + info_hash: Arbitrary::arbitrary(g), + offer_id: Arbitrary::arbitrary(g), + answer: rtc_answer(), + } + } + } + + impl Arbitrary for AnnounceRequestOffer { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + offer_id: Arbitrary::arbitrary(g), + offer: rtc_offer(), + } + } + } + + impl Arbitrary for AnnounceRequest { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let has_offers_or_answer_or_neither: Option = Arbitrary::arbitrary(g); + + let mut offers: Option> = None; + let mut answer: Option = None; + let mut to_peer_id: Option = None; + let mut offer_id: Option = None; + + match has_offers_or_answer_or_neither { + Some(true) => { + offers = Some(Arbitrary::arbitrary(g)); + } + Some(false) => { + answer = Some(rtc_answer()); + to_peer_id = Some(Arbitrary::arbitrary(g)); + offer_id = Some(Arbitrary::arbitrary(g)); + } + None => (), + } + + let numwant = offers.as_ref().map(|offers| offers.len()); + + Self { + action: AnnounceAction::Announce, + info_hash: Arbitrary::arbitrary(g), + peer_id: Arbitrary::arbitrary(g), + bytes_left: Arbitrary::arbitrary(g), + event: Arbitrary::arbitrary(g), + offers, + numwant, + answer, + answer_to_peer_id: to_peer_id, + answer_offer_id: offer_id, + } + } + } + + impl Arbitrary for AnnounceResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + action: AnnounceAction::Announce, + info_hash: Arbitrary::arbitrary(g), + complete: Arbitrary::arbitrary(g), + incomplete: Arbitrary::arbitrary(g), + announce_interval: Arbitrary::arbitrary(g), + } + } + } + + impl Arbitrary for ScrapeRequest { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + action: ScrapeAction::Scrape, + info_hashes: Arbitrary::arbitrary(g), + } + } + } + + impl Arbitrary for ScrapeRequestInfoHashes { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + if Arbitrary::arbitrary(g) { + ScrapeRequestInfoHashes::Multiple(Arbitrary::arbitrary(g)) + } else { + ScrapeRequestInfoHashes::Single(Arbitrary::arbitrary(g)) + } + } + } + + impl Arbitrary for ScrapeStatistics { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + Self { + complete: Arbitrary::arbitrary(g), + incomplete: Arbitrary::arbitrary(g), + downloaded: Arbitrary::arbitrary(g), + } + } + } + + impl Arbitrary for ScrapeResponse { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let files: Vec<(InfoHash, ScrapeStatistics)> = Arbitrary::arbitrary(g); + + Self { + action: ScrapeAction::Scrape, + files: files.into_iter().collect(), + } + } + } + + impl Arbitrary for InMessage { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + if Arbitrary::arbitrary(g) { + Self::AnnounceRequest(Arbitrary::arbitrary(g)) + } else { + Self::ScrapeRequest(Arbitrary::arbitrary(g)) + } + } + } + + impl Arbitrary for OutMessage { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + match (Arbitrary::arbitrary(g), Arbitrary::arbitrary(g)) { + (false, false) => Self::AnnounceResponse(Arbitrary::arbitrary(g)), + (true, false) => Self::ScrapeResponse(Arbitrary::arbitrary(g)), + (false, true) => Self::OfferOutMessage(Arbitrary::arbitrary(g)), + (true, true) => Self::AnswerOutMessage(Arbitrary::arbitrary(g)), + } + } + } + + #[cfg(feature = "tungstenite")] + #[quickcheck] + fn quickcheck_serde_identity_in_message(in_message_1: InMessage) -> bool { + let ws_message = in_message_1.to_ws_message(); + + let in_message_2 = InMessage::from_ws_message(ws_message.clone()).unwrap(); + + let success = in_message_1 == in_message_2; + + if !success { + dbg!(in_message_1); + dbg!(in_message_2); + if let ::tungstenite::Message::Text(text) = ws_message { + println!("{}", text); + } + } + + success + } + + #[cfg(feature = "tungstenite")] + #[quickcheck] + fn quickcheck_serde_identity_out_message(out_message_1: OutMessage) -> bool { + let ws_message = out_message_1.to_ws_message(); + + let out_message_2 = OutMessage::from_ws_message(ws_message.clone()).unwrap(); + + let success = out_message_1 == out_message_2; + + if !success { + dbg!(out_message_1); + dbg!(out_message_2); + if let ::tungstenite::Message::Text(text) = ws_message { + println!("{}", text); + } + } + + success + } + + fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash { + let mut arr = [0u8; 20]; + + assert!(bytes.len() == 20); + + arr.copy_from_slice(bytes); + + InfoHash(arr) + } + + #[test] + fn test_deserialize_info_hashes_vec() { + let info_hashes = ScrapeRequestInfoHashes::Multiple(vec![ + info_hash_from_bytes(b"aaaabbbbccccddddeeee"), + info_hash_from_bytes(b"aaaabbbbccccddddeeee"), + ]); + + let expected = ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: Some(info_hashes), + }; + + let observed: ScrapeRequest = unsafe { + let mut input: String = r#"{ + "action": "scrape", + "info_hash": ["aaaabbbbccccddddeeee", "aaaabbbbccccddddeeee"] + }"# + .into(); + + ::simd_json::serde::from_str(&mut input).unwrap() + }; + + assert_eq!(expected, observed); + } + + #[test] + fn test_deserialize_info_hashes_str() { + let info_hashes = + ScrapeRequestInfoHashes::Single(info_hash_from_bytes(b"aaaabbbbccccddddeeee")); + + let expected = ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: Some(info_hashes), + }; + + let observed: ScrapeRequest = unsafe { + let mut input: String = r#"{ + "action": "scrape", + "info_hash": "aaaabbbbccccddddeeee" + }"# + .into(); + + ::simd_json::serde::from_str(&mut input).unwrap() + }; + + assert_eq!(expected, observed); + } + + #[test] + fn test_deserialize_info_hashes_null() { + let observed: ScrapeRequest = unsafe { + let mut input: String = r#"{ + "action": "scrape", + "info_hash": null + }"# + .into(); + + ::simd_json::serde::from_str(&mut input).unwrap() + }; + + let expected = ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: None, + }; + + assert_eq!(expected, observed); + } + + #[test] + fn test_deserialize_info_hashes_missing() { + let observed: ScrapeRequest = unsafe { + let mut input: String = r#"{ + "action": "scrape" + }"# + .into(); + + ::simd_json::serde::from_str(&mut input).unwrap() + }; + + let expected = ScrapeRequest { + action: ScrapeAction::Scrape, + info_hashes: None, + }; + + assert_eq!(expected, observed); + } + + #[quickcheck] + fn quickcheck_serde_identity_info_hashes(info_hashes: ScrapeRequestInfoHashes) -> bool { + let deserialized: ScrapeRequestInfoHashes = unsafe { + let mut json = ::simd_json::serde::to_string(&info_hashes).unwrap(); + + ::simd_json::serde::from_str(&mut json).unwrap() + }; + + info_hashes == deserialized + } +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/announce.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/announce.rs new file mode 100644 index 0000000..25d0693 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/announce.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +/// Plain response to an AnnounceRequest +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnnounceResponse { + pub action: AnnounceAction, + pub info_hash: InfoHash, + /// Client checks if this is null, not clear why + pub complete: usize, + pub incomplete: usize, + #[serde(rename = "interval")] + pub announce_interval: usize, // Default 2 min probably +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/answer.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/answer.rs new file mode 100644 index 0000000..751ba31 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/answer.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +/// Message sent to peer when other peer has replied to its WebRTC offer +/// +/// Sent if fields answer, to_peer_id and offer_id are set in announce request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnswerOutMessage { + /// Always "announce" + pub action: AnnounceAction, + /// Note: if equal to client peer_id, client ignores answer + pub peer_id: PeerId, + pub info_hash: InfoHash, + pub answer: RtcAnswer, + pub offer_id: OfferId, +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/error.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/error.rs new file mode 100644 index 0000000..1293daa --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/error.rs @@ -0,0 +1,24 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ErrorResponse { + #[serde(rename = "failure reason")] + pub failure_reason: Cow<'static, str>, + /// Action of original request + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + // Should not be renamed + #[serde(skip_serializing_if = "Option::is_none")] + pub info_hash: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ErrorResponseAction { + Announce, + Scrape, +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/mod.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/mod.rs new file mode 100644 index 0000000..0af5675 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/mod.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +pub mod announce; +pub mod answer; +pub mod error; +pub mod offer; +pub mod scrape; + +pub use announce::*; +pub use answer::*; +pub use error::*; +pub use offer::*; +pub use scrape::*; + +/// Message sent by tracker +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OutMessage { + OfferOutMessage(OfferOutMessage), + AnswerOutMessage(AnswerOutMessage), + AnnounceResponse(AnnounceResponse), + ScrapeResponse(ScrapeResponse), + ErrorResponse(ErrorResponse), +} + +#[cfg(feature = "tungstenite")] +impl OutMessage { + #[inline] + pub fn to_ws_message(&self) -> tungstenite::Message { + ::tungstenite::Message::from(::serde_json::to_string(&self).unwrap()) + } + + #[inline] + pub fn from_ws_message(message: ::tungstenite::Message) -> ::anyhow::Result { + use tungstenite::Message::{Binary, Text}; + + let mut text: Vec = match message { + Text(text) => text.as_bytes().to_owned(), + Binary(bytes) => String::from_utf8(bytes.to_vec())?.into(), + _ => return Err(anyhow::anyhow!("Message is neither text nor bytes")), + }; + + Ok(::simd_json::serde::from_slice(&mut text)?) + } +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/offer.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/offer.rs new file mode 100644 index 0000000..9cbe6b9 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/offer.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +/// Message sent to peer when other peer wants to initiate a WebRTC connection +/// +/// One is sent for each offer in an announce request +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OfferOutMessage { + /// Always "announce" + pub action: AnnounceAction, + /// Peer id of peer sending offer + /// + /// Note: if equal to client peer_id, reference client ignores offer + pub peer_id: PeerId, + /// Torrent info hash + pub info_hash: InfoHash, + /// Gets copied from AnnounceRequestOffer + pub offer: RtcOffer, + /// Gets copied from AnnounceRequestOffer + pub offer_id: OfferId, +} diff --git a/apps/aquatic/crates/ws_protocol/src/outgoing/scrape.rs b/apps/aquatic/crates/ws_protocol/src/outgoing/scrape.rs new file mode 100644 index 0000000..5ac2051 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/src/outgoing/scrape.rs @@ -0,0 +1,19 @@ +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::common::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScrapeResponse { + pub action: ScrapeAction, + pub files: HashMap, + // It looks like `flags` field is ignored in reference client + // pub flags: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ScrapeStatistics { + pub complete: usize, + pub incomplete: usize, + pub downloaded: usize, +} diff --git a/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/benchmark.json b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/benchmark.json new file mode 100644 index 0000000..e153a52 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/benchmark.json @@ -0,0 +1 @@ +{"group_id":"deserialize-announce-request","function_id":null,"value_str":null,"throughput":null,"full_id":"deserialize-announce-request","directory_name":"deserialize-announce-request","title":"deserialize-announce-request"} \ No newline at end of file diff --git a/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/estimates.json b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/estimates.json new file mode 100644 index 0000000..66ad0e5 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/estimates.json @@ -0,0 +1 @@ +{"mean":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17747.517549723954,"upper_bound":17827.267537642005},"point_estimate":17784.6041414677,"standard_error":20.37407375834334},"median":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17577.94007936508,"upper_bound":17610.776785618924},"point_estimate":17593.548306902798,"standard_error":9.518459372549549},"median_abs_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":178.47341704422882,"upper_bound":242.9566257467244},"point_estimate":214.59916963759514,"standard_error":16.547608837291573},"slope":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17756.912325578603,"upper_bound":17852.279555854253},"point_estimate":17798.93543491748,"standard_error":24.423183313014615},"std_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":426.35101181405963,"upper_bound":857.9956431905033},"point_estimate":643.9714734076442,"standard_error":111.389827472484}} \ No newline at end of file diff --git a/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/raw.csv b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/raw.csv new file mode 100644 index 0000000..db0b387 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/raw.csv @@ -0,0 +1,1001 @@ +group,function,value,throughput_num,throughput_type,sample_measured_value,unit,iteration_count +deserialize-announce-request,,,,,440216.0,ns,21 +deserialize-announce-request,,,,,735170.0,ns,42 +deserialize-announce-request,,,,,1103196.0,ns,63 +deserialize-announce-request,,,,,1487746.0,ns,84 +deserialize-announce-request,,,,,1853576.0,ns,105 +deserialize-announce-request,,,,,2226238.0,ns,126 +deserialize-announce-request,,,,,2569194.0,ns,147 +deserialize-announce-request,,,,,2940587.0,ns,168 +deserialize-announce-request,,,,,3306717.0,ns,189 +deserialize-announce-request,,,,,3691449.0,ns,210 +deserialize-announce-request,,,,,4043710.0,ns,231 +deserialize-announce-request,,,,,4407865.0,ns,252 +deserialize-announce-request,,,,,4810473.0,ns,273 +deserialize-announce-request,,,,,5363261.0,ns,294 +deserialize-announce-request,,,,,5752576.0,ns,315 +deserialize-announce-request,,,,,8858221.0,ns,336 +deserialize-announce-request,,,,,7294835.0,ns,357 +deserialize-announce-request,,,,,7261542.0,ns,378 +deserialize-announce-request,,,,,7471269.0,ns,399 +deserialize-announce-request,,,,,7437979.0,ns,420 +deserialize-announce-request,,,,,7733500.0,ns,441 +deserialize-announce-request,,,,,8084148.0,ns,462 +deserialize-announce-request,,,,,8503572.0,ns,483 +deserialize-announce-request,,,,,8814843.0,ns,504 +deserialize-announce-request,,,,,9399602.0,ns,525 +deserialize-announce-request,,,,,9574901.0,ns,546 +deserialize-announce-request,,,,,10147088.0,ns,567 +deserialize-announce-request,,,,,10329222.0,ns,588 +deserialize-announce-request,,,,,10656388.0,ns,609 +deserialize-announce-request,,,,,11175719.0,ns,630 +deserialize-announce-request,,,,,11426608.0,ns,651 +deserialize-announce-request,,,,,11771207.0,ns,672 +deserialize-announce-request,,,,,12185148.0,ns,693 +deserialize-announce-request,,,,,12701113.0,ns,714 +deserialize-announce-request,,,,,12854894.0,ns,735 +deserialize-announce-request,,,,,13288629.0,ns,756 +deserialize-announce-request,,,,,13611215.0,ns,777 +deserialize-announce-request,,,,,14296947.0,ns,798 +deserialize-announce-request,,,,,14347011.0,ns,819 +deserialize-announce-request,,,,,14687861.0,ns,840 +deserialize-announce-request,,,,,15251923.0,ns,861 +deserialize-announce-request,,,,,15428709.0,ns,882 +deserialize-announce-request,,,,,16176652.0,ns,903 +deserialize-announce-request,,,,,16192924.0,ns,924 +deserialize-announce-request,,,,,16503465.0,ns,945 +deserialize-announce-request,,,,,16920376.0,ns,966 +deserialize-announce-request,,,,,17479434.0,ns,987 +deserialize-announce-request,,,,,17923276.0,ns,1008 +deserialize-announce-request,,,,,18047113.0,ns,1029 +deserialize-announce-request,,,,,18417292.0,ns,1050 +deserialize-announce-request,,,,,18774695.0,ns,1071 +deserialize-announce-request,,,,,19265603.0,ns,1092 +deserialize-announce-request,,,,,19510838.0,ns,1113 +deserialize-announce-request,,,,,20137692.0,ns,1134 +deserialize-announce-request,,,,,23112165.0,ns,1155 +deserialize-announce-request,,,,,21767615.0,ns,1176 +deserialize-announce-request,,,,,21065683.0,ns,1197 +deserialize-announce-request,,,,,25417034.0,ns,1218 +deserialize-announce-request,,,,,21744998.0,ns,1239 +deserialize-announce-request,,,,,22142914.0,ns,1260 +deserialize-announce-request,,,,,22840377.0,ns,1281 +deserialize-announce-request,,,,,22891399.0,ns,1302 +deserialize-announce-request,,,,,23250657.0,ns,1323 +deserialize-announce-request,,,,,23556338.0,ns,1344 +deserialize-announce-request,,,,,23908680.0,ns,1365 +deserialize-announce-request,,,,,24294935.0,ns,1386 +deserialize-announce-request,,,,,24645724.0,ns,1407 +deserialize-announce-request,,,,,25069069.0,ns,1428 +deserialize-announce-request,,,,,25400168.0,ns,1449 +deserialize-announce-request,,,,,25760011.0,ns,1470 +deserialize-announce-request,,,,,26195773.0,ns,1491 +deserialize-announce-request,,,,,26668673.0,ns,1512 +deserialize-announce-request,,,,,26879730.0,ns,1533 +deserialize-announce-request,,,,,27345119.0,ns,1554 +deserialize-announce-request,,,,,27631454.0,ns,1575 +deserialize-announce-request,,,,,32178911.0,ns,1596 +deserialize-announce-request,,,,,28351281.0,ns,1617 +deserialize-announce-request,,,,,29013376.0,ns,1638 +deserialize-announce-request,,,,,29125017.0,ns,1659 +deserialize-announce-request,,,,,29583410.0,ns,1680 +deserialize-announce-request,,,,,29828452.0,ns,1701 +deserialize-announce-request,,,,,30460203.0,ns,1722 +deserialize-announce-request,,,,,30602368.0,ns,1743 +deserialize-announce-request,,,,,30903537.0,ns,1764 +deserialize-announce-request,,,,,31310589.0,ns,1785 +deserialize-announce-request,,,,,31688586.0,ns,1806 +deserialize-announce-request,,,,,31997019.0,ns,1827 +deserialize-announce-request,,,,,32700766.0,ns,1848 +deserialize-announce-request,,,,,35863353.0,ns,1869 +deserialize-announce-request,,,,,37686999.0,ns,1890 +deserialize-announce-request,,,,,34722358.0,ns,1911 +deserialize-announce-request,,,,,36905225.0,ns,1932 +deserialize-announce-request,,,,,34214829.0,ns,1953 +deserialize-announce-request,,,,,34644177.0,ns,1974 +deserialize-announce-request,,,,,35055364.0,ns,1995 +deserialize-announce-request,,,,,35327521.0,ns,2016 +deserialize-announce-request,,,,,35921251.0,ns,2037 +deserialize-announce-request,,,,,36009994.0,ns,2058 +deserialize-announce-request,,,,,36428378.0,ns,2079 +deserialize-announce-request,,,,,36931868.0,ns,2100 +deserialize-announce-request,,,,,37159182.0,ns,2121 +deserialize-announce-request,,,,,41272073.0,ns,2142 +deserialize-announce-request,,,,,38405594.0,ns,2163 +deserialize-announce-request,,,,,37944146.0,ns,2184 +deserialize-announce-request,,,,,42567752.0,ns,2205 +deserialize-announce-request,,,,,39086747.0,ns,2226 +deserialize-announce-request,,,,,39459057.0,ns,2247 +deserialize-announce-request,,,,,39748591.0,ns,2268 +deserialize-announce-request,,,,,43989691.0,ns,2289 +deserialize-announce-request,,,,,40464884.0,ns,2310 +deserialize-announce-request,,,,,40477933.0,ns,2331 +deserialize-announce-request,,,,,41210098.0,ns,2352 +deserialize-announce-request,,,,,41218825.0,ns,2373 +deserialize-announce-request,,,,,41528127.0,ns,2394 +deserialize-announce-request,,,,,41891440.0,ns,2415 +deserialize-announce-request,,,,,42228275.0,ns,2436 +deserialize-announce-request,,,,,42777590.0,ns,2457 +deserialize-announce-request,,,,,42960188.0,ns,2478 +deserialize-announce-request,,,,,43336311.0,ns,2499 +deserialize-announce-request,,,,,43660346.0,ns,2520 +deserialize-announce-request,,,,,44107053.0,ns,2541 +deserialize-announce-request,,,,,44744438.0,ns,2562 +deserialize-announce-request,,,,,44792643.0,ns,2583 +deserialize-announce-request,,,,,45284148.0,ns,2604 +deserialize-announce-request,,,,,45525311.0,ns,2625 +deserialize-announce-request,,,,,45938432.0,ns,2646 +deserialize-announce-request,,,,,46279129.0,ns,2667 +deserialize-announce-request,,,,,50366870.0,ns,2688 +deserialize-announce-request,,,,,48035230.0,ns,2709 +deserialize-announce-request,,,,,47970996.0,ns,2730 +deserialize-announce-request,,,,,51201604.0,ns,2751 +deserialize-announce-request,,,,,48569065.0,ns,2772 +deserialize-announce-request,,,,,49038916.0,ns,2793 +deserialize-announce-request,,,,,49165751.0,ns,2814 +deserialize-announce-request,,,,,49566705.0,ns,2835 +deserialize-announce-request,,,,,50079454.0,ns,2856 +deserialize-announce-request,,,,,50377698.0,ns,2877 +deserialize-announce-request,,,,,50934873.0,ns,2898 +deserialize-announce-request,,,,,55193052.0,ns,2919 +deserialize-announce-request,,,,,51565112.0,ns,2940 +deserialize-announce-request,,,,,51866020.0,ns,2961 +deserialize-announce-request,,,,,56683750.0,ns,2982 +deserialize-announce-request,,,,,52537066.0,ns,3003 +deserialize-announce-request,,,,,53066837.0,ns,3024 +deserialize-announce-request,,,,,53318980.0,ns,3045 +deserialize-announce-request,,,,,53799380.0,ns,3066 +deserialize-announce-request,,,,,57968910.0,ns,3087 +deserialize-announce-request,,,,,54229761.0,ns,3108 +deserialize-announce-request,,,,,54457202.0,ns,3129 +deserialize-announce-request,,,,,55226039.0,ns,3150 +deserialize-announce-request,,,,,55772578.0,ns,3171 +deserialize-announce-request,,,,,55545825.0,ns,3192 +deserialize-announce-request,,,,,55799577.0,ns,3213 +deserialize-announce-request,,,,,56242478.0,ns,3234 +deserialize-announce-request,,,,,61017848.0,ns,3255 +deserialize-announce-request,,,,,57483204.0,ns,3276 +deserialize-announce-request,,,,,57666720.0,ns,3297 +deserialize-announce-request,,,,,58017890.0,ns,3318 +deserialize-announce-request,,,,,58640374.0,ns,3339 +deserialize-announce-request,,,,,62969050.0,ns,3360 +deserialize-announce-request,,,,,61526916.0,ns,3381 +deserialize-announce-request,,,,,61027434.0,ns,3402 +deserialize-announce-request,,,,,59540296.0,ns,3423 +deserialize-announce-request,,,,,59867778.0,ns,3444 +deserialize-announce-request,,,,,60619806.0,ns,3465 +deserialize-announce-request,,,,,60817437.0,ns,3486 +deserialize-announce-request,,,,,61422621.0,ns,3507 +deserialize-announce-request,,,,,61523786.0,ns,3528 +deserialize-announce-request,,,,,61761003.0,ns,3549 +deserialize-announce-request,,,,,62028327.0,ns,3570 +deserialize-announce-request,,,,,62725488.0,ns,3591 +deserialize-announce-request,,,,,62720464.0,ns,3612 +deserialize-announce-request,,,,,63111318.0,ns,3633 +deserialize-announce-request,,,,,63568421.0,ns,3654 +deserialize-announce-request,,,,,63953304.0,ns,3675 +deserialize-announce-request,,,,,64273973.0,ns,3696 +deserialize-announce-request,,,,,64872063.0,ns,3717 +deserialize-announce-request,,,,,65060511.0,ns,3738 +deserialize-announce-request,,,,,65563658.0,ns,3759 +deserialize-announce-request,,,,,65701822.0,ns,3780 +deserialize-announce-request,,,,,66131655.0,ns,3801 +deserialize-announce-request,,,,,66543423.0,ns,3822 +deserialize-announce-request,,,,,66794717.0,ns,3843 +deserialize-announce-request,,,,,67185142.0,ns,3864 +deserialize-announce-request,,,,,67490042.0,ns,3885 +deserialize-announce-request,,,,,67829560.0,ns,3906 +deserialize-announce-request,,,,,68330640.0,ns,3927 +deserialize-announce-request,,,,,68600145.0,ns,3948 +deserialize-announce-request,,,,,69047821.0,ns,3969 +deserialize-announce-request,,,,,69292773.0,ns,3990 +deserialize-announce-request,,,,,69586914.0,ns,4011 +deserialize-announce-request,,,,,70515789.0,ns,4032 +deserialize-announce-request,,,,,70419264.0,ns,4053 +deserialize-announce-request,,,,,70830952.0,ns,4074 +deserialize-announce-request,,,,,75496077.0,ns,4095 +deserialize-announce-request,,,,,72186781.0,ns,4116 +deserialize-announce-request,,,,,72650697.0,ns,4137 +deserialize-announce-request,,,,,73472886.0,ns,4158 +deserialize-announce-request,,,,,73323603.0,ns,4179 +deserialize-announce-request,,,,,73639928.0,ns,4200 +deserialize-announce-request,,,,,74539287.0,ns,4221 +deserialize-announce-request,,,,,74486675.0,ns,4242 +deserialize-announce-request,,,,,74716865.0,ns,4263 +deserialize-announce-request,,,,,75121023.0,ns,4284 +deserialize-announce-request,,,,,75787201.0,ns,4305 +deserialize-announce-request,,,,,76445578.0,ns,4326 +deserialize-announce-request,,,,,76272881.0,ns,4347 +deserialize-announce-request,,,,,76489455.0,ns,4368 +deserialize-announce-request,,,,,77066900.0,ns,4389 +deserialize-announce-request,,,,,77239143.0,ns,4410 +deserialize-announce-request,,,,,78242262.0,ns,4431 +deserialize-announce-request,,,,,81797643.0,ns,4452 +deserialize-announce-request,,,,,77677250.0,ns,4473 +deserialize-announce-request,,,,,82725918.0,ns,4494 +deserialize-announce-request,,,,,79109122.0,ns,4515 +deserialize-announce-request,,,,,79494001.0,ns,4536 +deserialize-announce-request,,,,,80378377.0,ns,4557 +deserialize-announce-request,,,,,80258975.0,ns,4578 +deserialize-announce-request,,,,,80624955.0,ns,4599 +deserialize-announce-request,,,,,81155298.0,ns,4620 +deserialize-announce-request,,,,,85713196.0,ns,4641 +deserialize-announce-request,,,,,81805211.0,ns,4662 +deserialize-announce-request,,,,,82246758.0,ns,4683 +deserialize-announce-request,,,,,82869297.0,ns,4704 +deserialize-announce-request,,,,,82843266.0,ns,4725 +deserialize-announce-request,,,,,87814304.0,ns,4746 +deserialize-announce-request,,,,,83405300.0,ns,4767 +deserialize-announce-request,,,,,88406810.0,ns,4788 +deserialize-announce-request,,,,,84594217.0,ns,4809 +deserialize-announce-request,,,,,85009529.0,ns,4830 +deserialize-announce-request,,,,,85409750.0,ns,4851 +deserialize-announce-request,,,,,89292755.0,ns,4872 +deserialize-announce-request,,,,,85187367.0,ns,4893 +deserialize-announce-request,,,,,85379304.0,ns,4914 +deserialize-announce-request,,,,,86230417.0,ns,4935 +deserialize-announce-request,,,,,86446352.0,ns,4956 +deserialize-announce-request,,,,,86343746.0,ns,4977 +deserialize-announce-request,,,,,91454590.0,ns,4998 +deserialize-announce-request,,,,,88293608.0,ns,5019 +deserialize-announce-request,,,,,88157468.0,ns,5040 +deserialize-announce-request,,,,,92008539.0,ns,5061 +deserialize-announce-request,,,,,88134499.0,ns,5082 +deserialize-announce-request,,,,,88846065.0,ns,5103 +deserialize-announce-request,,,,,93368336.0,ns,5124 +deserialize-announce-request,,,,,90596530.0,ns,5145 +deserialize-announce-request,,,,,91141879.0,ns,5166 +deserialize-announce-request,,,,,91297884.0,ns,5187 +deserialize-announce-request,,,,,91731994.0,ns,5208 +deserialize-announce-request,,,,,92539327.0,ns,5229 +deserialize-announce-request,,,,,92975013.0,ns,5250 +deserialize-announce-request,,,,,93160289.0,ns,5271 +deserialize-announce-request,,,,,93272268.0,ns,5292 +deserialize-announce-request,,,,,93550722.0,ns,5313 +deserialize-announce-request,,,,,96286320.0,ns,5334 +deserialize-announce-request,,,,,98483390.0,ns,5355 +deserialize-announce-request,,,,,94181858.0,ns,5376 +deserialize-announce-request,,,,,94813856.0,ns,5397 +deserialize-announce-request,,,,,94892658.0,ns,5418 +deserialize-announce-request,,,,,95565954.0,ns,5439 +deserialize-announce-request,,,,,95656349.0,ns,5460 +deserialize-announce-request,,,,,95914126.0,ns,5481 +deserialize-announce-request,,,,,100260395.0,ns,5502 +deserialize-announce-request,,,,,96057578.0,ns,5523 +deserialize-announce-request,,,,,96218314.0,ns,5544 +deserialize-announce-request,,,,,97005109.0,ns,5565 +deserialize-announce-request,,,,,96902047.0,ns,5586 +deserialize-announce-request,,,,,103929498.0,ns,5607 +deserialize-announce-request,,,,,101801937.0,ns,5628 +deserialize-announce-request,,,,,103089251.0,ns,5649 +deserialize-announce-request,,,,,98913392.0,ns,5670 +deserialize-announce-request,,,,,99063005.0,ns,5691 +deserialize-announce-request,,,,,103697712.0,ns,5712 +deserialize-announce-request,,,,,104586579.0,ns,5733 +deserialize-announce-request,,,,,101300937.0,ns,5754 +deserialize-announce-request,,,,,105539503.0,ns,5775 +deserialize-announce-request,,,,,101465012.0,ns,5796 +deserialize-announce-request,,,,,102430829.0,ns,5817 +deserialize-announce-request,,,,,106074538.0,ns,5838 +deserialize-announce-request,,,,,101882416.0,ns,5859 +deserialize-announce-request,,,,,103276355.0,ns,5880 +deserialize-announce-request,,,,,103878079.0,ns,5901 +deserialize-announce-request,,,,,104327532.0,ns,5922 +deserialize-announce-request,,,,,104491392.0,ns,5943 +deserialize-announce-request,,,,,104865428.0,ns,5964 +deserialize-announce-request,,,,,109190140.0,ns,5985 +deserialize-announce-request,,,,,109524014.0,ns,6006 +deserialize-announce-request,,,,,106406876.0,ns,6027 +deserialize-announce-request,,,,,106348694.0,ns,6048 +deserialize-announce-request,,,,,107053341.0,ns,6069 +deserialize-announce-request,,,,,107312684.0,ns,6090 +deserialize-announce-request,,,,,108328765.0,ns,6111 +deserialize-announce-request,,,,,109160144.0,ns,6132 +deserialize-announce-request,,,,,108451074.0,ns,6153 +deserialize-announce-request,,,,,112773552.0,ns,6174 +deserialize-announce-request,,,,,107825456.0,ns,6195 +deserialize-announce-request,,,,,108502881.0,ns,6216 +deserialize-announce-request,,,,,108794124.0,ns,6237 +deserialize-announce-request,,,,,113807752.0,ns,6258 +deserialize-announce-request,,,,,114408719.0,ns,6279 +deserialize-announce-request,,,,,110604963.0,ns,6300 +deserialize-announce-request,,,,,110621067.0,ns,6321 +deserialize-announce-request,,,,,111457842.0,ns,6342 +deserialize-announce-request,,,,,112062410.0,ns,6363 +deserialize-announce-request,,,,,111631565.0,ns,6384 +deserialize-announce-request,,,,,112058851.0,ns,6405 +deserialize-announce-request,,,,,112534645.0,ns,6426 +deserialize-announce-request,,,,,112790456.0,ns,6447 +deserialize-announce-request,,,,,122404726.0,ns,6468 +deserialize-announce-request,,,,,119010606.0,ns,6489 +deserialize-announce-request,,,,,118267250.0,ns,6510 +deserialize-announce-request,,,,,113941407.0,ns,6531 +deserialize-announce-request,,,,,114426612.0,ns,6552 +deserialize-announce-request,,,,,114218899.0,ns,6573 +deserialize-announce-request,,,,,115055425.0,ns,6594 +deserialize-announce-request,,,,,115079001.0,ns,6615 +deserialize-announce-request,,,,,115915048.0,ns,6636 +deserialize-announce-request,,,,,116804181.0,ns,6657 +deserialize-announce-request,,,,,117060303.0,ns,6678 +deserialize-announce-request,,,,,117743754.0,ns,6699 +deserialize-announce-request,,,,,129471771.0,ns,6720 +deserialize-announce-request,,,,,118480374.0,ns,6741 +deserialize-announce-request,,,,,123101962.0,ns,6762 +deserialize-announce-request,,,,,119277154.0,ns,6783 +deserialize-announce-request,,,,,119620757.0,ns,6804 +deserialize-announce-request,,,,,123377903.0,ns,6825 +deserialize-announce-request,,,,,124347377.0,ns,6846 +deserialize-announce-request,,,,,119673320.0,ns,6867 +deserialize-announce-request,,,,,119737394.0,ns,6888 +deserialize-announce-request,,,,,120703716.0,ns,6909 +deserialize-announce-request,,,,,120696614.0,ns,6930 +deserialize-announce-request,,,,,120847024.0,ns,6951 +deserialize-announce-request,,,,,121246034.0,ns,6972 +deserialize-announce-request,,,,,121931558.0,ns,6993 +deserialize-announce-request,,,,,121910716.0,ns,7014 +deserialize-announce-request,,,,,122031413.0,ns,7035 +deserialize-announce-request,,,,,122355031.0,ns,7056 +deserialize-announce-request,,,,,123046307.0,ns,7077 +deserialize-announce-request,,,,,123507865.0,ns,7098 +deserialize-announce-request,,,,,123914426.0,ns,7119 +deserialize-announce-request,,,,,128546741.0,ns,7140 +deserialize-announce-request,,,,,125898158.0,ns,7161 +deserialize-announce-request,,,,,130137893.0,ns,7182 +deserialize-announce-request,,,,,126157255.0,ns,7203 +deserialize-announce-request,,,,,126726992.0,ns,7224 +deserialize-announce-request,,,,,131065670.0,ns,7245 +deserialize-announce-request,,,,,128484839.0,ns,7266 +deserialize-announce-request,,,,,128125477.0,ns,7287 +deserialize-announce-request,,,,,132371696.0,ns,7308 +deserialize-announce-request,,,,,132900877.0,ns,7329 +deserialize-announce-request,,,,,128921353.0,ns,7350 +deserialize-announce-request,,,,,129250633.0,ns,7371 +deserialize-announce-request,,,,,129470761.0,ns,7392 +deserialize-announce-request,,,,,131383361.0,ns,7413 +deserialize-announce-request,,,,,132307897.0,ns,7434 +deserialize-announce-request,,,,,130947453.0,ns,7455 +deserialize-announce-request,,,,,131544115.0,ns,7476 +deserialize-announce-request,,,,,132894821.0,ns,7497 +deserialize-announce-request,,,,,133131257.0,ns,7518 +deserialize-announce-request,,,,,133785868.0,ns,7539 +deserialize-announce-request,,,,,134728903.0,ns,7560 +deserialize-announce-request,,,,,135358628.0,ns,7581 +deserialize-announce-request,,,,,139852390.0,ns,7602 +deserialize-announce-request,,,,,137850355.0,ns,7623 +deserialize-announce-request,,,,,134713989.0,ns,7644 +deserialize-announce-request,,,,,134999253.0,ns,7665 +deserialize-announce-request,,,,,135320166.0,ns,7686 +deserialize-announce-request,,,,,144417903.0,ns,7707 +deserialize-announce-request,,,,,140100056.0,ns,7728 +deserialize-announce-request,,,,,140990646.0,ns,7749 +deserialize-announce-request,,,,,136394810.0,ns,7770 +deserialize-announce-request,,,,,136811614.0,ns,7791 +deserialize-announce-request,,,,,136739878.0,ns,7812 +deserialize-announce-request,,,,,137533792.0,ns,7833 +deserialize-announce-request,,,,,141402849.0,ns,7854 +deserialize-announce-request,,,,,137914902.0,ns,7875 +deserialize-announce-request,,,,,138967880.0,ns,7896 +deserialize-announce-request,,,,,138863530.0,ns,7917 +deserialize-announce-request,,,,,144468356.0,ns,7938 +deserialize-announce-request,,,,,150792627.0,ns,7959 +deserialize-announce-request,,,,,140078702.0,ns,7980 +deserialize-announce-request,,,,,140260380.0,ns,8001 +deserialize-announce-request,,,,,144939442.0,ns,8022 +deserialize-announce-request,,,,,141448614.0,ns,8043 +deserialize-announce-request,,,,,141783062.0,ns,8064 +deserialize-announce-request,,,,,141645712.0,ns,8085 +deserialize-announce-request,,,,,146149304.0,ns,8106 +deserialize-announce-request,,,,,147092307.0,ns,8127 +deserialize-announce-request,,,,,141801373.0,ns,8148 +deserialize-announce-request,,,,,142154485.0,ns,8169 +deserialize-announce-request,,,,,147114612.0,ns,8190 +deserialize-announce-request,,,,,144838809.0,ns,8211 +deserialize-announce-request,,,,,152768256.0,ns,8232 +deserialize-announce-request,,,,,152672020.0,ns,8253 +deserialize-announce-request,,,,,149752712.0,ns,8274 +deserialize-announce-request,,,,,145632033.0,ns,8295 +deserialize-announce-request,,,,,145646167.0,ns,8316 +deserialize-announce-request,,,,,146400560.0,ns,8337 +deserialize-announce-request,,,,,146574643.0,ns,8358 +deserialize-announce-request,,,,,155193780.0,ns,8379 +deserialize-announce-request,,,,,147611086.0,ns,8400 +deserialize-announce-request,,,,,147667612.0,ns,8421 +deserialize-announce-request,,,,,148184801.0,ns,8442 +deserialize-announce-request,,,,,148394556.0,ns,8463 +deserialize-announce-request,,,,,149947630.0,ns,8484 +deserialize-announce-request,,,,,153762596.0,ns,8505 +deserialize-announce-request,,,,,149595936.0,ns,8526 +deserialize-announce-request,,,,,154307018.0,ns,8547 +deserialize-announce-request,,,,,150100287.0,ns,8568 +deserialize-announce-request,,,,,150462216.0,ns,8589 +deserialize-announce-request,,,,,151055198.0,ns,8610 +deserialize-announce-request,,,,,158724732.0,ns,8631 +deserialize-announce-request,,,,,152872499.0,ns,8652 +deserialize-announce-request,,,,,151860089.0,ns,8673 +deserialize-announce-request,,,,,152146060.0,ns,8694 +deserialize-announce-request,,,,,152602175.0,ns,8715 +deserialize-announce-request,,,,,152860154.0,ns,8736 +deserialize-announce-request,,,,,153556962.0,ns,8757 +deserialize-announce-request,,,,,153963242.0,ns,8778 +deserialize-announce-request,,,,,153879927.0,ns,8799 +deserialize-announce-request,,,,,158878389.0,ns,8820 +deserialize-announce-request,,,,,155425704.0,ns,8841 +deserialize-announce-request,,,,,158423807.0,ns,8862 +deserialize-announce-request,,,,,162945582.0,ns,8883 +deserialize-announce-request,,,,,162278802.0,ns,8904 +deserialize-announce-request,,,,,161184474.0,ns,8925 +deserialize-announce-request,,,,,156738237.0,ns,8946 +deserialize-announce-request,,,,,162259208.0,ns,8967 +deserialize-announce-request,,,,,162265527.0,ns,8988 +deserialize-announce-request,,,,,159219952.0,ns,9009 +deserialize-announce-request,,,,,158280877.0,ns,9030 +deserialize-announce-request,,,,,169529281.0,ns,9051 +deserialize-announce-request,,,,,159440157.0,ns,9072 +deserialize-announce-request,,,,,159530208.0,ns,9093 +deserialize-announce-request,,,,,163275613.0,ns,9114 +deserialize-announce-request,,,,,159162257.0,ns,9135 +deserialize-announce-request,,,,,159072202.0,ns,9156 +deserialize-announce-request,,,,,159517217.0,ns,9177 +deserialize-announce-request,,,,,169727438.0,ns,9198 +deserialize-announce-request,,,,,161757313.0,ns,9219 +deserialize-announce-request,,,,,166362260.0,ns,9240 +deserialize-announce-request,,,,,163066151.0,ns,9261 +deserialize-announce-request,,,,,163542479.0,ns,9282 +deserialize-announce-request,,,,,164472223.0,ns,9303 +deserialize-announce-request,,,,,166430357.0,ns,9324 +deserialize-announce-request,,,,,168152761.0,ns,9345 +deserialize-announce-request,,,,,162868174.0,ns,9366 +deserialize-announce-request,,,,,163067907.0,ns,9387 +deserialize-announce-request,,,,,164179824.0,ns,9408 +deserialize-announce-request,,,,,164263886.0,ns,9429 +deserialize-announce-request,,,,,166077586.0,ns,9450 +deserialize-announce-request,,,,,170869174.0,ns,9471 +deserialize-announce-request,,,,,166677714.0,ns,9492 +deserialize-announce-request,,,,,167141638.0,ns,9513 +deserialize-announce-request,,,,,167837074.0,ns,9534 +deserialize-announce-request,,,,,171770269.0,ns,9555 +deserialize-announce-request,,,,,167986583.0,ns,9576 +deserialize-announce-request,,,,,168391016.0,ns,9597 +deserialize-announce-request,,,,,168680986.0,ns,9618 +deserialize-announce-request,,,,,172824431.0,ns,9639 +deserialize-announce-request,,,,,169655907.0,ns,9660 +deserialize-announce-request,,,,,173002142.0,ns,9681 +deserialize-announce-request,,,,,168826826.0,ns,9702 +deserialize-announce-request,,,,,169021145.0,ns,9723 +deserialize-announce-request,,,,,169409270.0,ns,9744 +deserialize-announce-request,,,,,178569987.0,ns,9765 +deserialize-announce-request,,,,,172046997.0,ns,9786 +deserialize-announce-request,,,,,172380528.0,ns,9807 +deserialize-announce-request,,,,,172539384.0,ns,9828 +deserialize-announce-request,,,,,175936241.0,ns,9849 +deserialize-announce-request,,,,,171551333.0,ns,9870 +deserialize-announce-request,,,,,171987815.0,ns,9891 +deserialize-announce-request,,,,,176551784.0,ns,9912 +deserialize-announce-request,,,,,174447362.0,ns,9933 +deserialize-announce-request,,,,,178898450.0,ns,9954 +deserialize-announce-request,,,,,175868164.0,ns,9975 +deserialize-announce-request,,,,,176251790.0,ns,9996 +deserialize-announce-request,,,,,178245724.0,ns,10017 +deserialize-announce-request,,,,,180123752.0,ns,10038 +deserialize-announce-request,,,,,181228561.0,ns,10059 +deserialize-announce-request,,,,,181716050.0,ns,10080 +deserialize-announce-request,,,,,177299539.0,ns,10101 +deserialize-announce-request,,,,,179606921.0,ns,10122 +deserialize-announce-request,,,,,178353047.0,ns,10143 +deserialize-announce-request,,,,,177918671.0,ns,10164 +deserialize-announce-request,,,,,179006261.0,ns,10185 +deserialize-announce-request,,,,,183503368.0,ns,10206 +deserialize-announce-request,,,,,181043935.0,ns,10227 +deserialize-announce-request,,,,,217688847.0,ns,10248 +deserialize-announce-request,,,,,197425243.0,ns,10269 +deserialize-announce-request,,,,,178979149.0,ns,10290 +deserialize-announce-request,,,,,179851861.0,ns,10311 +deserialize-announce-request,,,,,185026606.0,ns,10332 +deserialize-announce-request,,,,,189162950.0,ns,10353 +deserialize-announce-request,,,,,182505520.0,ns,10374 +deserialize-announce-request,,,,,183051725.0,ns,10395 +deserialize-announce-request,,,,,182838708.0,ns,10416 +deserialize-announce-request,,,,,183316320.0,ns,10437 +deserialize-announce-request,,,,,183455326.0,ns,10458 +deserialize-announce-request,,,,,183892018.0,ns,10479 +deserialize-announce-request,,,,,189458072.0,ns,10500 +deserialize-announce-request,,,,,186279690.0,ns,10521 +deserialize-announce-request,,,,,188758254.0,ns,10542 +deserialize-announce-request,,,,,183708721.0,ns,10563 +deserialize-announce-request,,,,,184185365.0,ns,10584 +deserialize-announce-request,,,,,184716911.0,ns,10605 +deserialize-announce-request,,,,,195775258.0,ns,10626 +deserialize-announce-request,,,,,191572333.0,ns,10647 +deserialize-announce-request,,,,,196113664.0,ns,10668 +deserialize-announce-request,,,,,187699366.0,ns,10689 +deserialize-announce-request,,,,,187968917.0,ns,10710 +deserialize-announce-request,,,,,192866288.0,ns,10731 +deserialize-announce-request,,,,,189256784.0,ns,10752 +deserialize-announce-request,,,,,191799807.0,ns,10773 +deserialize-announce-request,,,,,196809785.0,ns,10794 +deserialize-announce-request,,,,,189983630.0,ns,10815 +deserialize-announce-request,,,,,193921023.0,ns,10836 +deserialize-announce-request,,,,,194676804.0,ns,10857 +deserialize-announce-request,,,,,197656934.0,ns,10878 +deserialize-announce-request,,,,,192195835.0,ns,10899 +deserialize-announce-request,,,,,194055896.0,ns,10920 +deserialize-announce-request,,,,,202963576.0,ns,10941 +deserialize-announce-request,,,,,193729444.0,ns,10962 +deserialize-announce-request,,,,,313369182.0,ns,10983 +deserialize-announce-request,,,,,205077565.0,ns,11004 +deserialize-announce-request,,,,,193162103.0,ns,11025 +deserialize-announce-request,,,,,201250043.0,ns,11046 +deserialize-announce-request,,,,,193903697.0,ns,11067 +deserialize-announce-request,,,,,194666645.0,ns,11088 +deserialize-announce-request,,,,,195197203.0,ns,11109 +deserialize-announce-request,,,,,195031183.0,ns,11130 +deserialize-announce-request,,,,,195277643.0,ns,11151 +deserialize-announce-request,,,,,203549749.0,ns,11172 +deserialize-announce-request,,,,,200667283.0,ns,11193 +deserialize-announce-request,,,,,203240228.0,ns,11214 +deserialize-announce-request,,,,,196899401.0,ns,11235 +deserialize-announce-request,,,,,202570885.0,ns,11256 +deserialize-announce-request,,,,,198627042.0,ns,11277 +deserialize-announce-request,,,,,204696096.0,ns,11298 +deserialize-announce-request,,,,,197228804.0,ns,11319 +deserialize-announce-request,,,,,197317144.0,ns,11340 +deserialize-announce-request,,,,,206219535.0,ns,11361 +deserialize-announce-request,,,,,198476358.0,ns,11382 +deserialize-announce-request,,,,,207523635.0,ns,11403 +deserialize-announce-request,,,,,200751710.0,ns,11424 +deserialize-announce-request,,,,,200703386.0,ns,11445 +deserialize-announce-request,,,,,201380968.0,ns,11466 +deserialize-announce-request,,,,,201640584.0,ns,11487 +deserialize-announce-request,,,,,207963157.0,ns,11508 +deserialize-announce-request,,,,,203133351.0,ns,11529 +deserialize-announce-request,,,,,204357964.0,ns,11550 +deserialize-announce-request,,,,,207119887.0,ns,11571 +deserialize-announce-request,,,,,215713121.0,ns,11592 +deserialize-announce-request,,,,,207694038.0,ns,11613 +deserialize-announce-request,,,,,203860265.0,ns,11634 +deserialize-announce-request,,,,,204423233.0,ns,11655 +deserialize-announce-request,,,,,204352068.0,ns,11676 +deserialize-announce-request,,,,,204663697.0,ns,11697 +deserialize-announce-request,,,,,210079289.0,ns,11718 +deserialize-announce-request,,,,,209583314.0,ns,11739 +deserialize-announce-request,,,,,206072314.0,ns,11760 +deserialize-announce-request,,,,,211579495.0,ns,11781 +deserialize-announce-request,,,,,210228559.0,ns,11802 +deserialize-announce-request,,,,,212659170.0,ns,11823 +deserialize-announce-request,,,,,213604713.0,ns,11844 +deserialize-announce-request,,,,,212157643.0,ns,11865 +deserialize-announce-request,,,,,207743686.0,ns,11886 +deserialize-announce-request,,,,,217124848.0,ns,11907 +deserialize-announce-request,,,,,218923907.0,ns,11928 +deserialize-announce-request,,,,,213243141.0,ns,11949 +deserialize-announce-request,,,,,211633022.0,ns,11970 +deserialize-announce-request,,,,,214556727.0,ns,11991 +deserialize-announce-request,,,,,210979099.0,ns,12012 +deserialize-announce-request,,,,,212163914.0,ns,12033 +deserialize-announce-request,,,,,213129201.0,ns,12054 +deserialize-announce-request,,,,,209926343.0,ns,12075 +deserialize-announce-request,,,,,210423386.0,ns,12096 +deserialize-announce-request,,,,,210645895.0,ns,12117 +deserialize-announce-request,,,,,211040309.0,ns,12138 +deserialize-announce-request,,,,,217332530.0,ns,12159 +deserialize-announce-request,,,,,213596010.0,ns,12180 +deserialize-announce-request,,,,,217864544.0,ns,12201 +deserialize-announce-request,,,,,218396001.0,ns,12222 +deserialize-announce-request,,,,,219465224.0,ns,12243 +deserialize-announce-request,,,,,215529564.0,ns,12264 +deserialize-announce-request,,,,,215157837.0,ns,12285 +deserialize-announce-request,,,,,222619348.0,ns,12306 +deserialize-announce-request,,,,,216994581.0,ns,12327 +deserialize-announce-request,,,,,216419323.0,ns,12348 +deserialize-announce-request,,,,,216509998.0,ns,12369 +deserialize-announce-request,,,,,217853326.0,ns,12390 +deserialize-announce-request,,,,,217452806.0,ns,12411 +deserialize-announce-request,,,,,218595369.0,ns,12432 +deserialize-announce-request,,,,,218480664.0,ns,12453 +deserialize-announce-request,,,,,224053166.0,ns,12474 +deserialize-announce-request,,,,,219700201.0,ns,12495 +deserialize-announce-request,,,,,220200938.0,ns,12516 +deserialize-announce-request,,,,,219826538.0,ns,12537 +deserialize-announce-request,,,,,220559275.0,ns,12558 +deserialize-announce-request,,,,,225308052.0,ns,12579 +deserialize-announce-request,,,,,220906025.0,ns,12600 +deserialize-announce-request,,,,,221657250.0,ns,12621 +deserialize-announce-request,,,,,224851375.0,ns,12642 +deserialize-announce-request,,,,,224836430.0,ns,12663 +deserialize-announce-request,,,,,225587527.0,ns,12684 +deserialize-announce-request,,,,,221305773.0,ns,12705 +deserialize-announce-request,,,,,221361610.0,ns,12726 +deserialize-announce-request,,,,,221540008.0,ns,12747 +deserialize-announce-request,,,,,222132856.0,ns,12768 +deserialize-announce-request,,,,,222903148.0,ns,12789 +deserialize-announce-request,,,,,227265919.0,ns,12810 +deserialize-announce-request,,,,,225595003.0,ns,12831 +deserialize-announce-request,,,,,225202721.0,ns,12852 +deserialize-announce-request,,,,,225610772.0,ns,12873 +deserialize-announce-request,,,,,226220823.0,ns,12894 +deserialize-announce-request,,,,,226625191.0,ns,12915 +deserialize-announce-request,,,,,226839633.0,ns,12936 +deserialize-announce-request,,,,,235878223.0,ns,12957 +deserialize-announce-request,,,,,228542387.0,ns,12978 +deserialize-announce-request,,,,,234857965.0,ns,12999 +deserialize-announce-request,,,,,230647966.0,ns,13020 +deserialize-announce-request,,,,,240183728.0,ns,13041 +deserialize-announce-request,,,,,236753896.0,ns,13062 +deserialize-announce-request,,,,,230058140.0,ns,13083 +deserialize-announce-request,,,,,230233015.0,ns,13104 +deserialize-announce-request,,,,,230285573.0,ns,13125 +deserialize-announce-request,,,,,233165283.0,ns,13146 +deserialize-announce-request,,,,,241923572.0,ns,13167 +deserialize-announce-request,,,,,233235785.0,ns,13188 +deserialize-announce-request,,,,,238599607.0,ns,13209 +deserialize-announce-request,,,,,238275330.0,ns,13230 +deserialize-announce-request,,,,,230870208.0,ns,13251 +deserialize-announce-request,,,,,235368600.0,ns,13272 +deserialize-announce-request,,,,,235319580.0,ns,13293 +deserialize-announce-request,,,,,240716349.0,ns,13314 +deserialize-announce-request,,,,,242985533.0,ns,13335 +deserialize-announce-request,,,,,244475810.0,ns,13356 +deserialize-announce-request,,,,,239878355.0,ns,13377 +deserialize-announce-request,,,,,241492491.0,ns,13398 +deserialize-announce-request,,,,,235724774.0,ns,13419 +deserialize-announce-request,,,,,241978333.0,ns,13440 +deserialize-announce-request,,,,,237505157.0,ns,13461 +deserialize-announce-request,,,,,243036299.0,ns,13482 +deserialize-announce-request,,,,,242445677.0,ns,13503 +deserialize-announce-request,,,,,236866179.0,ns,13524 +deserialize-announce-request,,,,,239250678.0,ns,13545 +deserialize-announce-request,,,,,243856437.0,ns,13566 +deserialize-announce-request,,,,,242420346.0,ns,13587 +deserialize-announce-request,,,,,246992581.0,ns,13608 +deserialize-announce-request,,,,,248538252.0,ns,13629 +deserialize-announce-request,,,,,249231495.0,ns,13650 +deserialize-announce-request,,,,,248484416.0,ns,13671 +deserialize-announce-request,,,,,246267183.0,ns,13692 +deserialize-announce-request,,,,,240998827.0,ns,13713 +deserialize-announce-request,,,,,245725237.0,ns,13734 +deserialize-announce-request,,,,,246783525.0,ns,13755 +deserialize-announce-request,,,,,249857337.0,ns,13776 +deserialize-announce-request,,,,,251113506.0,ns,13797 +deserialize-announce-request,,,,,251137892.0,ns,13818 +deserialize-announce-request,,,,,253502267.0,ns,13839 +deserialize-announce-request,,,,,244132121.0,ns,13860 +deserialize-announce-request,,,,,251763236.0,ns,13881 +deserialize-announce-request,,,,,247859382.0,ns,13902 +deserialize-announce-request,,,,,291929144.0,ns,13923 +deserialize-announce-request,,,,,250610061.0,ns,13944 +deserialize-announce-request,,,,,245166582.0,ns,13965 +deserialize-announce-request,,,,,245603340.0,ns,13986 +deserialize-announce-request,,,,,246465078.0,ns,14007 +deserialize-announce-request,,,,,252545903.0,ns,14028 +deserialize-announce-request,,,,,250282356.0,ns,14049 +deserialize-announce-request,,,,,246635456.0,ns,14070 +deserialize-announce-request,,,,,252821131.0,ns,14091 +deserialize-announce-request,,,,,256312596.0,ns,14112 +deserialize-announce-request,,,,,252424117.0,ns,14133 +deserialize-announce-request,,,,,246864961.0,ns,14154 +deserialize-announce-request,,,,,246450271.0,ns,14175 +deserialize-announce-request,,,,,252048154.0,ns,14196 +deserialize-announce-request,,,,,249588839.0,ns,14217 +deserialize-announce-request,,,,,250003822.0,ns,14238 +deserialize-announce-request,,,,,254268084.0,ns,14259 +deserialize-announce-request,,,,,250337047.0,ns,14280 +deserialize-announce-request,,,,,256593969.0,ns,14301 +deserialize-announce-request,,,,,255355399.0,ns,14322 +deserialize-announce-request,,,,,251176908.0,ns,14343 +deserialize-announce-request,,,,,255785790.0,ns,14364 +deserialize-announce-request,,,,,262626410.0,ns,14385 +deserialize-announce-request,,,,,260369877.0,ns,14406 +deserialize-announce-request,,,,,256785141.0,ns,14427 +deserialize-announce-request,,,,,256736896.0,ns,14448 +deserialize-announce-request,,,,,253384778.0,ns,14469 +deserialize-announce-request,,,,,253669702.0,ns,14490 +deserialize-announce-request,,,,,254395350.0,ns,14511 +deserialize-announce-request,,,,,254731863.0,ns,14532 +deserialize-announce-request,,,,,260735524.0,ns,14553 +deserialize-announce-request,,,,,301927848.0,ns,14574 +deserialize-announce-request,,,,,257135727.0,ns,14595 +deserialize-announce-request,,,,,256258577.0,ns,14616 +deserialize-announce-request,,,,,256485749.0,ns,14637 +deserialize-announce-request,,,,,261093294.0,ns,14658 +deserialize-announce-request,,,,,262190142.0,ns,14679 +deserialize-announce-request,,,,,264805710.0,ns,14700 +deserialize-announce-request,,,,,260741755.0,ns,14721 +deserialize-announce-request,,,,,256739847.0,ns,14742 +deserialize-announce-request,,,,,256458782.0,ns,14763 +deserialize-announce-request,,,,,266532231.0,ns,14784 +deserialize-announce-request,,,,,269295676.0,ns,14805 +deserialize-announce-request,,,,,260439119.0,ns,14826 +deserialize-announce-request,,,,,260474336.0,ns,14847 +deserialize-announce-request,,,,,264489829.0,ns,14868 +deserialize-announce-request,,,,,263294558.0,ns,14889 +deserialize-announce-request,,,,,270814923.0,ns,14910 +deserialize-announce-request,,,,,269786841.0,ns,14931 +deserialize-announce-request,,,,,275147452.0,ns,14952 +deserialize-announce-request,,,,,263446882.0,ns,14973 +deserialize-announce-request,,,,,262772967.0,ns,14994 +deserialize-announce-request,,,,,263303723.0,ns,15015 +deserialize-announce-request,,,,,267420504.0,ns,15036 +deserialize-announce-request,,,,,264324591.0,ns,15057 +deserialize-announce-request,,,,,269002556.0,ns,15078 +deserialize-announce-request,,,,,270244912.0,ns,15099 +deserialize-announce-request,,,,,266794907.0,ns,15120 +deserialize-announce-request,,,,,270955229.0,ns,15141 +deserialize-announce-request,,,,,270376580.0,ns,15162 +deserialize-announce-request,,,,,266048636.0,ns,15183 +deserialize-announce-request,,,,,269573449.0,ns,15204 +deserialize-announce-request,,,,,274022833.0,ns,15225 +deserialize-announce-request,,,,,270984801.0,ns,15246 +deserialize-announce-request,,,,,277608993.0,ns,15267 +deserialize-announce-request,,,,,272170509.0,ns,15288 +deserialize-announce-request,,,,,269468070.0,ns,15309 +deserialize-announce-request,,,,,268976816.0,ns,15330 +deserialize-announce-request,,,,,273386420.0,ns,15351 +deserialize-announce-request,,,,,269655493.0,ns,15372 +deserialize-announce-request,,,,,270200898.0,ns,15393 +deserialize-announce-request,,,,,270510640.0,ns,15414 +deserialize-announce-request,,,,,270545204.0,ns,15435 +deserialize-announce-request,,,,,271450849.0,ns,15456 +deserialize-announce-request,,,,,271255498.0,ns,15477 +deserialize-announce-request,,,,,271637627.0,ns,15498 +deserialize-announce-request,,,,,272217620.0,ns,15519 +deserialize-announce-request,,,,,277585748.0,ns,15540 +deserialize-announce-request,,,,,274459415.0,ns,15561 +deserialize-announce-request,,,,,278612336.0,ns,15582 +deserialize-announce-request,,,,,280389711.0,ns,15603 +deserialize-announce-request,,,,,276858355.0,ns,15624 +deserialize-announce-request,,,,,273873571.0,ns,15645 +deserialize-announce-request,,,,,282760091.0,ns,15666 +deserialize-announce-request,,,,,276935883.0,ns,15687 +deserialize-announce-request,,,,,283014218.0,ns,15708 +deserialize-announce-request,,,,,283549507.0,ns,15729 +deserialize-announce-request,,,,,288920797.0,ns,15750 +deserialize-announce-request,,,,,281505361.0,ns,15771 +deserialize-announce-request,,,,,280508402.0,ns,15792 +deserialize-announce-request,,,,,288015198.0,ns,15813 +deserialize-announce-request,,,,,286820934.0,ns,15834 +deserialize-announce-request,,,,,279246304.0,ns,15855 +deserialize-announce-request,,,,,278496307.0,ns,15876 +deserialize-announce-request,,,,,278884227.0,ns,15897 +deserialize-announce-request,,,,,279435373.0,ns,15918 +deserialize-announce-request,,,,,284763624.0,ns,15939 +deserialize-announce-request,,,,,280972427.0,ns,15960 +deserialize-announce-request,,,,,281355651.0,ns,15981 +deserialize-announce-request,,,,,287518046.0,ns,16002 +deserialize-announce-request,,,,,285150728.0,ns,16023 +deserialize-announce-request,,,,,289230972.0,ns,16044 +deserialize-announce-request,,,,,284892887.0,ns,16065 +deserialize-announce-request,,,,,287520355.0,ns,16086 +deserialize-announce-request,,,,,291125198.0,ns,16107 +deserialize-announce-request,,,,,287712302.0,ns,16128 +deserialize-announce-request,,,,,289252400.0,ns,16149 +deserialize-announce-request,,,,,285219439.0,ns,16170 +deserialize-announce-request,,,,,290621014.0,ns,16191 +deserialize-announce-request,,,,,288908103.0,ns,16212 +deserialize-announce-request,,,,,284834880.0,ns,16233 +deserialize-announce-request,,,,,285082123.0,ns,16254 +deserialize-announce-request,,,,,285064913.0,ns,16275 +deserialize-announce-request,,,,,286050476.0,ns,16296 +deserialize-announce-request,,,,,286202737.0,ns,16317 +deserialize-announce-request,,,,,291287157.0,ns,16338 +deserialize-announce-request,,,,,293660583.0,ns,16359 +deserialize-announce-request,,,,,286902026.0,ns,16380 +deserialize-announce-request,,,,,293657103.0,ns,16401 +deserialize-announce-request,,,,,289645576.0,ns,16422 +deserialize-announce-request,,,,,289290600.0,ns,16443 +deserialize-announce-request,,,,,288357711.0,ns,16464 +deserialize-announce-request,,,,,289556901.0,ns,16485 +deserialize-announce-request,,,,,293389303.0,ns,16506 +deserialize-announce-request,,,,,289721394.0,ns,16527 +deserialize-announce-request,,,,,301702898.0,ns,16548 +deserialize-announce-request,,,,,292833240.0,ns,16569 +deserialize-announce-request,,,,,297609668.0,ns,16590 +deserialize-announce-request,,,,,299901214.0,ns,16611 +deserialize-announce-request,,,,,301763599.0,ns,16632 +deserialize-announce-request,,,,,302540070.0,ns,16653 +deserialize-announce-request,,,,,302074823.0,ns,16674 +deserialize-announce-request,,,,,301120249.0,ns,16695 +deserialize-announce-request,,,,,292728604.0,ns,16716 +deserialize-announce-request,,,,,294313199.0,ns,16737 +deserialize-announce-request,,,,,294727495.0,ns,16758 +deserialize-announce-request,,,,,307549284.0,ns,16779 +deserialize-announce-request,,,,,295310336.0,ns,16800 +deserialize-announce-request,,,,,295414316.0,ns,16821 +deserialize-announce-request,,,,,296152016.0,ns,16842 +deserialize-announce-request,,,,,296392327.0,ns,16863 +deserialize-announce-request,,,,,296005277.0,ns,16884 +deserialize-announce-request,,,,,300815207.0,ns,16905 +deserialize-announce-request,,,,,308394819.0,ns,16926 +deserialize-announce-request,,,,,303655012.0,ns,16947 +deserialize-announce-request,,,,,301821949.0,ns,16968 +deserialize-announce-request,,,,,297913258.0,ns,16989 +deserialize-announce-request,,,,,298352671.0,ns,17010 +deserialize-announce-request,,,,,298189673.0,ns,17031 +deserialize-announce-request,,,,,299298110.0,ns,17052 +deserialize-announce-request,,,,,302700297.0,ns,17073 +deserialize-announce-request,,,,,297742384.0,ns,17094 +deserialize-announce-request,,,,,297473944.0,ns,17115 +deserialize-announce-request,,,,,298958641.0,ns,17136 +deserialize-announce-request,,,,,312643880.0,ns,17157 +deserialize-announce-request,,,,,304819560.0,ns,17178 +deserialize-announce-request,,,,,303916447.0,ns,17199 +deserialize-announce-request,,,,,299668235.0,ns,17220 +deserialize-announce-request,,,,,299803646.0,ns,17241 +deserialize-announce-request,,,,,300116399.0,ns,17262 +deserialize-announce-request,,,,,305253315.0,ns,17283 +deserialize-announce-request,,,,,303643602.0,ns,17304 +deserialize-announce-request,,,,,303788834.0,ns,17325 +deserialize-announce-request,,,,,304130156.0,ns,17346 +deserialize-announce-request,,,,,304548649.0,ns,17367 +deserialize-announce-request,,,,,305054443.0,ns,17388 +deserialize-announce-request,,,,,308172810.0,ns,17409 +deserialize-announce-request,,,,,302965399.0,ns,17430 +deserialize-announce-request,,,,,311079231.0,ns,17451 +deserialize-announce-request,,,,,312802060.0,ns,17472 +deserialize-announce-request,,,,,307107801.0,ns,17493 +deserialize-announce-request,,,,,310421432.0,ns,17514 +deserialize-announce-request,,,,,304852062.0,ns,17535 +deserialize-announce-request,,,,,312119223.0,ns,17556 +deserialize-announce-request,,,,,312452136.0,ns,17577 +deserialize-announce-request,,,,,308383381.0,ns,17598 +deserialize-announce-request,,,,,309140985.0,ns,17619 +deserialize-announce-request,,,,,308840317.0,ns,17640 +deserialize-announce-request,,,,,315301147.0,ns,17661 +deserialize-announce-request,,,,,312384690.0,ns,17682 +deserialize-announce-request,,,,,318223071.0,ns,17703 +deserialize-announce-request,,,,,321280742.0,ns,17724 +deserialize-announce-request,,,,,328769196.0,ns,17745 +deserialize-announce-request,,,,,311884804.0,ns,17766 +deserialize-announce-request,,,,,315907069.0,ns,17787 +deserialize-announce-request,,,,,310472223.0,ns,17808 +deserialize-announce-request,,,,,315609583.0,ns,17829 +deserialize-announce-request,,,,,318361650.0,ns,17850 +deserialize-announce-request,,,,,323036485.0,ns,17871 +deserialize-announce-request,,,,,311001103.0,ns,17892 +deserialize-announce-request,,,,,311797052.0,ns,17913 +deserialize-announce-request,,,,,312029906.0,ns,17934 +deserialize-announce-request,,,,,317391138.0,ns,17955 +deserialize-announce-request,,,,,315437733.0,ns,17976 +deserialize-announce-request,,,,,320117488.0,ns,17997 +deserialize-announce-request,,,,,316585155.0,ns,18018 +deserialize-announce-request,,,,,331060444.0,ns,18039 +deserialize-announce-request,,,,,316834435.0,ns,18060 +deserialize-announce-request,,,,,317047492.0,ns,18081 +deserialize-announce-request,,,,,317716757.0,ns,18102 +deserialize-announce-request,,,,,321867382.0,ns,18123 +deserialize-announce-request,,,,,318419054.0,ns,18144 +deserialize-announce-request,,,,,325516760.0,ns,18165 +deserialize-announce-request,,,,,322457207.0,ns,18186 +deserialize-announce-request,,,,,329868747.0,ns,18207 +deserialize-announce-request,,,,,336376852.0,ns,18228 +deserialize-announce-request,,,,,334428763.0,ns,18249 +deserialize-announce-request,,,,,330500656.0,ns,18270 +deserialize-announce-request,,,,,332371153.0,ns,18291 +deserialize-announce-request,,,,,335343820.0,ns,18312 +deserialize-announce-request,,,,,327716205.0,ns,18333 +deserialize-announce-request,,,,,326634103.0,ns,18354 +deserialize-announce-request,,,,,332206875.0,ns,18375 +deserialize-announce-request,,,,,337298795.0,ns,18396 +deserialize-announce-request,,,,,322837644.0,ns,18417 +deserialize-announce-request,,,,,323169836.0,ns,18438 +deserialize-announce-request,,,,,326572032.0,ns,18459 +deserialize-announce-request,,,,,325584006.0,ns,18480 +deserialize-announce-request,,,,,325441293.0,ns,18501 +deserialize-announce-request,,,,,325351260.0,ns,18522 +deserialize-announce-request,,,,,325814114.0,ns,18543 +deserialize-announce-request,,,,,325979568.0,ns,18564 +deserialize-announce-request,,,,,333682833.0,ns,18585 +deserialize-announce-request,,,,,329599241.0,ns,18606 +deserialize-announce-request,,,,,335702750.0,ns,18627 +deserialize-announce-request,,,,,327523938.0,ns,18648 +deserialize-announce-request,,,,,327241897.0,ns,18669 +deserialize-announce-request,,,,,327637567.0,ns,18690 +deserialize-announce-request,,,,,347647081.0,ns,18711 +deserialize-announce-request,,,,,333126623.0,ns,18732 +deserialize-announce-request,,,,,329051470.0,ns,18753 +deserialize-announce-request,,,,,332011295.0,ns,18774 +deserialize-announce-request,,,,,333644272.0,ns,18795 +deserialize-announce-request,,,,,329655789.0,ns,18816 +deserialize-announce-request,,,,,336272591.0,ns,18837 +deserialize-announce-request,,,,,337697749.0,ns,18858 +deserialize-announce-request,,,,,330738789.0,ns,18879 +deserialize-announce-request,,,,,331381948.0,ns,18900 +deserialize-announce-request,,,,,343830420.0,ns,18921 +deserialize-announce-request,,,,,338874018.0,ns,18942 +deserialize-announce-request,,,,,343801493.0,ns,18963 +deserialize-announce-request,,,,,350892614.0,ns,18984 +deserialize-announce-request,,,,,342454014.0,ns,19005 +deserialize-announce-request,,,,,338140966.0,ns,19026 +deserialize-announce-request,,,,,338942814.0,ns,19047 +deserialize-announce-request,,,,,339018086.0,ns,19068 +deserialize-announce-request,,,,,339805026.0,ns,19089 +deserialize-announce-request,,,,,340489546.0,ns,19110 +deserialize-announce-request,,,,,338738729.0,ns,19131 +deserialize-announce-request,,,,,334175653.0,ns,19152 +deserialize-announce-request,,,,,334233428.0,ns,19173 +deserialize-announce-request,,,,,340684666.0,ns,19194 +deserialize-announce-request,,,,,337533646.0,ns,19215 +deserialize-announce-request,,,,,339677910.0,ns,19236 +deserialize-announce-request,,,,,343356949.0,ns,19257 +deserialize-announce-request,,,,,344940787.0,ns,19278 +deserialize-announce-request,,,,,336017120.0,ns,19299 +deserialize-announce-request,,,,,336025862.0,ns,19320 +deserialize-announce-request,,,,,336488191.0,ns,19341 +deserialize-announce-request,,,,,337031400.0,ns,19362 +deserialize-announce-request,,,,,341615144.0,ns,19383 +deserialize-announce-request,,,,,340105212.0,ns,19404 +deserialize-announce-request,,,,,340650275.0,ns,19425 +deserialize-announce-request,,,,,340706141.0,ns,19446 +deserialize-announce-request,,,,,345441216.0,ns,19467 +deserialize-announce-request,,,,,343121229.0,ns,19488 +deserialize-announce-request,,,,,346517004.0,ns,19509 +deserialize-announce-request,,,,,347091178.0,ns,19530 +deserialize-announce-request,,,,,344572474.0,ns,19551 +deserialize-announce-request,,,,,364223779.0,ns,19572 +deserialize-announce-request,,,,,344112694.0,ns,19593 +deserialize-announce-request,,,,,343577212.0,ns,19614 +deserialize-announce-request,,,,,350402068.0,ns,19635 +deserialize-announce-request,,,,,346551522.0,ns,19656 +deserialize-announce-request,,,,,357965846.0,ns,19677 +deserialize-announce-request,,,,,345560453.0,ns,19698 +deserialize-announce-request,,,,,346259496.0,ns,19719 +deserialize-announce-request,,,,,346663296.0,ns,19740 +deserialize-announce-request,,,,,351050075.0,ns,19761 +deserialize-announce-request,,,,,349380513.0,ns,19782 +deserialize-announce-request,,,,,365633907.0,ns,19803 +deserialize-announce-request,,,,,352034221.0,ns,19824 +deserialize-announce-request,,,,,356470456.0,ns,19845 +deserialize-announce-request,,,,,356834160.0,ns,19866 +deserialize-announce-request,,,,,345728087.0,ns,19887 +deserialize-announce-request,,,,,366221326.0,ns,19908 +deserialize-announce-request,,,,,350141068.0,ns,19929 +deserialize-announce-request,,,,,349736728.0,ns,19950 +deserialize-announce-request,,,,,350246229.0,ns,19971 +deserialize-announce-request,,,,,350588432.0,ns,19992 +deserialize-announce-request,,,,,359729078.0,ns,20013 +deserialize-announce-request,,,,,358721028.0,ns,20034 +deserialize-announce-request,,,,,363973622.0,ns,20055 +deserialize-announce-request,,,,,367848891.0,ns,20076 +deserialize-announce-request,,,,,372823304.0,ns,20097 +deserialize-announce-request,,,,,368831477.0,ns,20118 +deserialize-announce-request,,,,,372707670.0,ns,20139 +deserialize-announce-request,,,,,366887155.0,ns,20160 +deserialize-announce-request,,,,,353893604.0,ns,20181 +deserialize-announce-request,,,,,364182677.0,ns,20202 +deserialize-announce-request,,,,,358717470.0,ns,20223 +deserialize-announce-request,,,,,362114434.0,ns,20244 +deserialize-announce-request,,,,,355872431.0,ns,20265 +deserialize-announce-request,,,,,355978510.0,ns,20286 +deserialize-announce-request,,,,,364040450.0,ns,20307 +deserialize-announce-request,,,,,371897109.0,ns,20328 +deserialize-announce-request,,,,,366875493.0,ns,20349 +deserialize-announce-request,,,,,356994411.0,ns,20370 +deserialize-announce-request,,,,,357560539.0,ns,20391 +deserialize-announce-request,,,,,363806813.0,ns,20412 +deserialize-announce-request,,,,,363448811.0,ns,20433 +deserialize-announce-request,,,,,360202323.0,ns,20454 +deserialize-announce-request,,,,,358785710.0,ns,20475 +deserialize-announce-request,,,,,359300613.0,ns,20496 +deserialize-announce-request,,,,,368446393.0,ns,20517 +deserialize-announce-request,,,,,361994618.0,ns,20538 +deserialize-announce-request,,,,,368923308.0,ns,20559 +deserialize-announce-request,,,,,373916927.0,ns,20580 +deserialize-announce-request,,,,,366748116.0,ns,20601 +deserialize-announce-request,,,,,364850639.0,ns,20622 +deserialize-announce-request,,,,,363212609.0,ns,20643 +deserialize-announce-request,,,,,367848881.0,ns,20664 +deserialize-announce-request,,,,,499049569.0,ns,20685 +deserialize-announce-request,,,,,368025206.0,ns,20706 +deserialize-announce-request,,,,,373385123.0,ns,20727 +deserialize-announce-request,,,,,364014199.0,ns,20748 +deserialize-announce-request,,,,,364496779.0,ns,20769 +deserialize-announce-request,,,,,364705932.0,ns,20790 +deserialize-announce-request,,,,,368651055.0,ns,20811 +deserialize-announce-request,,,,,365551867.0,ns,20832 +deserialize-announce-request,,,,,365873065.0,ns,20853 +deserialize-announce-request,,,,,373791390.0,ns,20874 +deserialize-announce-request,,,,,370199080.0,ns,20895 +deserialize-announce-request,,,,,371401921.0,ns,20916 +deserialize-announce-request,,,,,366212025.0,ns,20937 +deserialize-announce-request,,,,,371551836.0,ns,20958 +deserialize-announce-request,,,,,367793409.0,ns,20979 +deserialize-announce-request,,,,,367885778.0,ns,21000 diff --git a/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/sample.json b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/sample.json new file mode 100644 index 0000000..1c9e905 --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/sample.json @@ -0,0 +1 @@ +{"sampling_mode":"Linear","iters":[21.0,42.0,63.0,84.0,105.0,126.0,147.0,168.0,189.0,210.0,231.0,252.0,273.0,294.0,315.0,336.0,357.0,378.0,399.0,420.0,441.0,462.0,483.0,504.0,525.0,546.0,567.0,588.0,609.0,630.0,651.0,672.0,693.0,714.0,735.0,756.0,777.0,798.0,819.0,840.0,861.0,882.0,903.0,924.0,945.0,966.0,987.0,1008.0,1029.0,1050.0,1071.0,1092.0,1113.0,1134.0,1155.0,1176.0,1197.0,1218.0,1239.0,1260.0,1281.0,1302.0,1323.0,1344.0,1365.0,1386.0,1407.0,1428.0,1449.0,1470.0,1491.0,1512.0,1533.0,1554.0,1575.0,1596.0,1617.0,1638.0,1659.0,1680.0,1701.0,1722.0,1743.0,1764.0,1785.0,1806.0,1827.0,1848.0,1869.0,1890.0,1911.0,1932.0,1953.0,1974.0,1995.0,2016.0,2037.0,2058.0,2079.0,2100.0,2121.0,2142.0,2163.0,2184.0,2205.0,2226.0,2247.0,2268.0,2289.0,2310.0,2331.0,2352.0,2373.0,2394.0,2415.0,2436.0,2457.0,2478.0,2499.0,2520.0,2541.0,2562.0,2583.0,2604.0,2625.0,2646.0,2667.0,2688.0,2709.0,2730.0,2751.0,2772.0,2793.0,2814.0,2835.0,2856.0,2877.0,2898.0,2919.0,2940.0,2961.0,2982.0,3003.0,3024.0,3045.0,3066.0,3087.0,3108.0,3129.0,3150.0,3171.0,3192.0,3213.0,3234.0,3255.0,3276.0,3297.0,3318.0,3339.0,3360.0,3381.0,3402.0,3423.0,3444.0,3465.0,3486.0,3507.0,3528.0,3549.0,3570.0,3591.0,3612.0,3633.0,3654.0,3675.0,3696.0,3717.0,3738.0,3759.0,3780.0,3801.0,3822.0,3843.0,3864.0,3885.0,3906.0,3927.0,3948.0,3969.0,3990.0,4011.0,4032.0,4053.0,4074.0,4095.0,4116.0,4137.0,4158.0,4179.0,4200.0,4221.0,4242.0,4263.0,4284.0,4305.0,4326.0,4347.0,4368.0,4389.0,4410.0,4431.0,4452.0,4473.0,4494.0,4515.0,4536.0,4557.0,4578.0,4599.0,4620.0,4641.0,4662.0,4683.0,4704.0,4725.0,4746.0,4767.0,4788.0,4809.0,4830.0,4851.0,4872.0,4893.0,4914.0,4935.0,4956.0,4977.0,4998.0,5019.0,5040.0,5061.0,5082.0,5103.0,5124.0,5145.0,5166.0,5187.0,5208.0,5229.0,5250.0,5271.0,5292.0,5313.0,5334.0,5355.0,5376.0,5397.0,5418.0,5439.0,5460.0,5481.0,5502.0,5523.0,5544.0,5565.0,5586.0,5607.0,5628.0,5649.0,5670.0,5691.0,5712.0,5733.0,5754.0,5775.0,5796.0,5817.0,5838.0,5859.0,5880.0,5901.0,5922.0,5943.0,5964.0,5985.0,6006.0,6027.0,6048.0,6069.0,6090.0,6111.0,6132.0,6153.0,6174.0,6195.0,6216.0,6237.0,6258.0,6279.0,6300.0,6321.0,6342.0,6363.0,6384.0,6405.0,6426.0,6447.0,6468.0,6489.0,6510.0,6531.0,6552.0,6573.0,6594.0,6615.0,6636.0,6657.0,6678.0,6699.0,6720.0,6741.0,6762.0,6783.0,6804.0,6825.0,6846.0,6867.0,6888.0,6909.0,6930.0,6951.0,6972.0,6993.0,7014.0,7035.0,7056.0,7077.0,7098.0,7119.0,7140.0,7161.0,7182.0,7203.0,7224.0,7245.0,7266.0,7287.0,7308.0,7329.0,7350.0,7371.0,7392.0,7413.0,7434.0,7455.0,7476.0,7497.0,7518.0,7539.0,7560.0,7581.0,7602.0,7623.0,7644.0,7665.0,7686.0,7707.0,7728.0,7749.0,7770.0,7791.0,7812.0,7833.0,7854.0,7875.0,7896.0,7917.0,7938.0,7959.0,7980.0,8001.0,8022.0,8043.0,8064.0,8085.0,8106.0,8127.0,8148.0,8169.0,8190.0,8211.0,8232.0,8253.0,8274.0,8295.0,8316.0,8337.0,8358.0,8379.0,8400.0,8421.0,8442.0,8463.0,8484.0,8505.0,8526.0,8547.0,8568.0,8589.0,8610.0,8631.0,8652.0,8673.0,8694.0,8715.0,8736.0,8757.0,8778.0,8799.0,8820.0,8841.0,8862.0,8883.0,8904.0,8925.0,8946.0,8967.0,8988.0,9009.0,9030.0,9051.0,9072.0,9093.0,9114.0,9135.0,9156.0,9177.0,9198.0,9219.0,9240.0,9261.0,9282.0,9303.0,9324.0,9345.0,9366.0,9387.0,9408.0,9429.0,9450.0,9471.0,9492.0,9513.0,9534.0,9555.0,9576.0,9597.0,9618.0,9639.0,9660.0,9681.0,9702.0,9723.0,9744.0,9765.0,9786.0,9807.0,9828.0,9849.0,9870.0,9891.0,9912.0,9933.0,9954.0,9975.0,9996.0,10017.0,10038.0,10059.0,10080.0,10101.0,10122.0,10143.0,10164.0,10185.0,10206.0,10227.0,10248.0,10269.0,10290.0,10311.0,10332.0,10353.0,10374.0,10395.0,10416.0,10437.0,10458.0,10479.0,10500.0,10521.0,10542.0,10563.0,10584.0,10605.0,10626.0,10647.0,10668.0,10689.0,10710.0,10731.0,10752.0,10773.0,10794.0,10815.0,10836.0,10857.0,10878.0,10899.0,10920.0,10941.0,10962.0,10983.0,11004.0,11025.0,11046.0,11067.0,11088.0,11109.0,11130.0,11151.0,11172.0,11193.0,11214.0,11235.0,11256.0,11277.0,11298.0,11319.0,11340.0,11361.0,11382.0,11403.0,11424.0,11445.0,11466.0,11487.0,11508.0,11529.0,11550.0,11571.0,11592.0,11613.0,11634.0,11655.0,11676.0,11697.0,11718.0,11739.0,11760.0,11781.0,11802.0,11823.0,11844.0,11865.0,11886.0,11907.0,11928.0,11949.0,11970.0,11991.0,12012.0,12033.0,12054.0,12075.0,12096.0,12117.0,12138.0,12159.0,12180.0,12201.0,12222.0,12243.0,12264.0,12285.0,12306.0,12327.0,12348.0,12369.0,12390.0,12411.0,12432.0,12453.0,12474.0,12495.0,12516.0,12537.0,12558.0,12579.0,12600.0,12621.0,12642.0,12663.0,12684.0,12705.0,12726.0,12747.0,12768.0,12789.0,12810.0,12831.0,12852.0,12873.0,12894.0,12915.0,12936.0,12957.0,12978.0,12999.0,13020.0,13041.0,13062.0,13083.0,13104.0,13125.0,13146.0,13167.0,13188.0,13209.0,13230.0,13251.0,13272.0,13293.0,13314.0,13335.0,13356.0,13377.0,13398.0,13419.0,13440.0,13461.0,13482.0,13503.0,13524.0,13545.0,13566.0,13587.0,13608.0,13629.0,13650.0,13671.0,13692.0,13713.0,13734.0,13755.0,13776.0,13797.0,13818.0,13839.0,13860.0,13881.0,13902.0,13923.0,13944.0,13965.0,13986.0,14007.0,14028.0,14049.0,14070.0,14091.0,14112.0,14133.0,14154.0,14175.0,14196.0,14217.0,14238.0,14259.0,14280.0,14301.0,14322.0,14343.0,14364.0,14385.0,14406.0,14427.0,14448.0,14469.0,14490.0,14511.0,14532.0,14553.0,14574.0,14595.0,14616.0,14637.0,14658.0,14679.0,14700.0,14721.0,14742.0,14763.0,14784.0,14805.0,14826.0,14847.0,14868.0,14889.0,14910.0,14931.0,14952.0,14973.0,14994.0,15015.0,15036.0,15057.0,15078.0,15099.0,15120.0,15141.0,15162.0,15183.0,15204.0,15225.0,15246.0,15267.0,15288.0,15309.0,15330.0,15351.0,15372.0,15393.0,15414.0,15435.0,15456.0,15477.0,15498.0,15519.0,15540.0,15561.0,15582.0,15603.0,15624.0,15645.0,15666.0,15687.0,15708.0,15729.0,15750.0,15771.0,15792.0,15813.0,15834.0,15855.0,15876.0,15897.0,15918.0,15939.0,15960.0,15981.0,16002.0,16023.0,16044.0,16065.0,16086.0,16107.0,16128.0,16149.0,16170.0,16191.0,16212.0,16233.0,16254.0,16275.0,16296.0,16317.0,16338.0,16359.0,16380.0,16401.0,16422.0,16443.0,16464.0,16485.0,16506.0,16527.0,16548.0,16569.0,16590.0,16611.0,16632.0,16653.0,16674.0,16695.0,16716.0,16737.0,16758.0,16779.0,16800.0,16821.0,16842.0,16863.0,16884.0,16905.0,16926.0,16947.0,16968.0,16989.0,17010.0,17031.0,17052.0,17073.0,17094.0,17115.0,17136.0,17157.0,17178.0,17199.0,17220.0,17241.0,17262.0,17283.0,17304.0,17325.0,17346.0,17367.0,17388.0,17409.0,17430.0,17451.0,17472.0,17493.0,17514.0,17535.0,17556.0,17577.0,17598.0,17619.0,17640.0,17661.0,17682.0,17703.0,17724.0,17745.0,17766.0,17787.0,17808.0,17829.0,17850.0,17871.0,17892.0,17913.0,17934.0,17955.0,17976.0,17997.0,18018.0,18039.0,18060.0,18081.0,18102.0,18123.0,18144.0,18165.0,18186.0,18207.0,18228.0,18249.0,18270.0,18291.0,18312.0,18333.0,18354.0,18375.0,18396.0,18417.0,18438.0,18459.0,18480.0,18501.0,18522.0,18543.0,18564.0,18585.0,18606.0,18627.0,18648.0,18669.0,18690.0,18711.0,18732.0,18753.0,18774.0,18795.0,18816.0,18837.0,18858.0,18879.0,18900.0,18921.0,18942.0,18963.0,18984.0,19005.0,19026.0,19047.0,19068.0,19089.0,19110.0,19131.0,19152.0,19173.0,19194.0,19215.0,19236.0,19257.0,19278.0,19299.0,19320.0,19341.0,19362.0,19383.0,19404.0,19425.0,19446.0,19467.0,19488.0,19509.0,19530.0,19551.0,19572.0,19593.0,19614.0,19635.0,19656.0,19677.0,19698.0,19719.0,19740.0,19761.0,19782.0,19803.0,19824.0,19845.0,19866.0,19887.0,19908.0,19929.0,19950.0,19971.0,19992.0,20013.0,20034.0,20055.0,20076.0,20097.0,20118.0,20139.0,20160.0,20181.0,20202.0,20223.0,20244.0,20265.0,20286.0,20307.0,20328.0,20349.0,20370.0,20391.0,20412.0,20433.0,20454.0,20475.0,20496.0,20517.0,20538.0,20559.0,20580.0,20601.0,20622.0,20643.0,20664.0,20685.0,20706.0,20727.0,20748.0,20769.0,20790.0,20811.0,20832.0,20853.0,20874.0,20895.0,20916.0,20937.0,20958.0,20979.0,21000.0],"times":[440216.0,735170.0,1103196.0,1487746.0,1853576.0,2226238.0,2569194.0,2940587.0,3306717.0,3691449.0,4043710.0,4407865.0,4810473.0,5363261.0,5752576.0,8858221.0,7294835.0,7261542.0,7471269.0,7437979.0,7733500.0,8084148.0,8503572.0,8814843.0,9399602.0,9574901.0,10147088.0,10329222.0,10656388.0,11175719.0,11426608.0,11771207.0,12185148.0,12701113.0,12854894.0,13288629.0,13611215.0,14296947.0,14347011.0,14687861.0,15251923.0,15428709.0,16176652.0,16192924.0,16503465.0,16920376.0,17479434.0,17923276.0,18047113.0,18417292.0,18774695.0,19265603.0,19510838.0,20137692.0,23112165.0,21767615.0,21065683.0,25417034.0,21744998.0,22142914.0,22840377.0,22891399.0,23250657.0,23556338.0,23908680.0,24294935.0,24645724.0,25069069.0,25400168.0,25760011.0,26195773.0,26668673.0,26879730.0,27345119.0,27631454.0,32178911.0,28351281.0,29013376.0,29125017.0,29583410.0,29828452.0,30460203.0,30602368.0,30903537.0,31310589.0,31688586.0,31997019.0,32700766.0,35863353.0,37686999.0,34722358.0,36905225.0,34214829.0,34644177.0,35055364.0,35327521.0,35921251.0,36009994.0,36428378.0,36931868.0,37159182.0,41272073.0,38405594.0,37944146.0,42567752.0,39086747.0,39459057.0,39748591.0,43989691.0,40464884.0,40477933.0,41210098.0,41218825.0,41528127.0,41891440.0,42228275.0,42777590.0,42960188.0,43336311.0,43660346.0,44107053.0,44744438.0,44792643.0,45284148.0,45525311.0,45938432.0,46279129.0,50366870.0,48035230.0,47970996.0,51201604.0,48569065.0,49038916.0,49165751.0,49566705.0,50079454.0,50377698.0,50934873.0,55193052.0,51565112.0,51866020.0,56683750.0,52537066.0,53066837.0,53318980.0,53799380.0,57968910.0,54229761.0,54457202.0,55226039.0,55772578.0,55545825.0,55799577.0,56242478.0,61017848.0,57483204.0,57666720.0,58017890.0,58640374.0,62969050.0,61526916.0,61027434.0,59540296.0,59867778.0,60619806.0,60817437.0,61422621.0,61523786.0,61761003.0,62028327.0,62725488.0,62720464.0,63111318.0,63568421.0,63953304.0,64273973.0,64872063.0,65060511.0,65563658.0,65701822.0,66131655.0,66543423.0,66794717.0,67185142.0,67490042.0,67829560.0,68330640.0,68600145.0,69047821.0,69292773.0,69586914.0,70515789.0,70419264.0,70830952.0,75496077.0,72186781.0,72650697.0,73472886.0,73323603.0,73639928.0,74539287.0,74486675.0,74716865.0,75121023.0,75787201.0,76445578.0,76272881.0,76489455.0,77066900.0,77239143.0,78242262.0,81797643.0,77677250.0,82725918.0,79109122.0,79494001.0,80378377.0,80258975.0,80624955.0,81155298.0,85713196.0,81805211.0,82246758.0,82869297.0,82843266.0,87814304.0,83405300.0,88406810.0,84594217.0,85009529.0,85409750.0,89292755.0,85187367.0,85379304.0,86230417.0,86446352.0,86343746.0,91454590.0,88293608.0,88157468.0,92008539.0,88134499.0,88846065.0,93368336.0,90596530.0,91141879.0,91297884.0,91731994.0,92539327.0,92975013.0,93160289.0,93272268.0,93550722.0,96286320.0,98483390.0,94181858.0,94813856.0,94892658.0,95565954.0,95656349.0,95914126.0,100260395.0,96057578.0,96218314.0,97005109.0,96902047.0,103929498.0,101801937.0,103089251.0,98913392.0,99063005.0,103697712.0,104586579.0,101300937.0,105539503.0,101465012.0,102430829.0,106074538.0,101882416.0,103276355.0,103878079.0,104327532.0,104491392.0,104865428.0,109190140.0,109524014.0,106406876.0,106348694.0,107053341.0,107312684.0,108328765.0,109160144.0,108451074.0,112773552.0,107825456.0,108502881.0,108794124.0,113807752.0,114408719.0,110604963.0,110621067.0,111457842.0,112062410.0,111631565.0,112058851.0,112534645.0,112790456.0,122404726.0,119010606.0,118267250.0,113941407.0,114426612.0,114218899.0,115055425.0,115079001.0,115915048.0,116804181.0,117060303.0,117743754.0,129471771.0,118480374.0,123101962.0,119277154.0,119620757.0,123377903.0,124347377.0,119673320.0,119737394.0,120703716.0,120696614.0,120847024.0,121246034.0,121931558.0,121910716.0,122031413.0,122355031.0,123046307.0,123507865.0,123914426.0,128546741.0,125898158.0,130137893.0,126157255.0,126726992.0,131065670.0,128484839.0,128125477.0,132371696.0,132900877.0,128921353.0,129250633.0,129470761.0,131383361.0,132307897.0,130947453.0,131544115.0,132894821.0,133131257.0,133785868.0,134728903.0,135358628.0,139852390.0,137850355.0,134713989.0,134999253.0,135320166.0,144417903.0,140100056.0,140990646.0,136394810.0,136811614.0,136739878.0,137533792.0,141402849.0,137914902.0,138967880.0,138863530.0,144468356.0,150792627.0,140078702.0,140260380.0,144939442.0,141448614.0,141783062.0,141645712.0,146149304.0,147092307.0,141801373.0,142154485.0,147114612.0,144838809.0,152768256.0,152672020.0,149752712.0,145632033.0,145646167.0,146400560.0,146574643.0,155193780.0,147611086.0,147667612.0,148184801.0,148394556.0,149947630.0,153762596.0,149595936.0,154307018.0,150100287.0,150462216.0,151055198.0,158724732.0,152872499.0,151860089.0,152146060.0,152602175.0,152860154.0,153556962.0,153963242.0,153879927.0,158878389.0,155425704.0,158423807.0,162945582.0,162278802.0,161184474.0,156738237.0,162259208.0,162265527.0,159219952.0,158280877.0,169529281.0,159440157.0,159530208.0,163275613.0,159162257.0,159072202.0,159517217.0,169727438.0,161757313.0,166362260.0,163066151.0,163542479.0,164472223.0,166430357.0,168152761.0,162868174.0,163067907.0,164179824.0,164263886.0,166077586.0,170869174.0,166677714.0,167141638.0,167837074.0,171770269.0,167986583.0,168391016.0,168680986.0,172824431.0,169655907.0,173002142.0,168826826.0,169021145.0,169409270.0,178569987.0,172046997.0,172380528.0,172539384.0,175936241.0,171551333.0,171987815.0,176551784.0,174447362.0,178898450.0,175868164.0,176251790.0,178245724.0,180123752.0,181228561.0,181716050.0,177299539.0,179606921.0,178353047.0,177918671.0,179006261.0,183503368.0,181043935.0,217688847.0,197425243.0,178979149.0,179851861.0,185026606.0,189162950.0,182505520.0,183051725.0,182838708.0,183316320.0,183455326.0,183892018.0,189458072.0,186279690.0,188758254.0,183708721.0,184185365.0,184716911.0,195775258.0,191572333.0,196113664.0,187699366.0,187968917.0,192866288.0,189256784.0,191799807.0,196809785.0,189983630.0,193921023.0,194676804.0,197656934.0,192195835.0,194055896.0,202963576.0,193729444.0,313369182.0,205077565.0,193162103.0,201250043.0,193903697.0,194666645.0,195197203.0,195031183.0,195277643.0,203549749.0,200667283.0,203240228.0,196899401.0,202570885.0,198627042.0,204696096.0,197228804.0,197317144.0,206219535.0,198476358.0,207523635.0,200751710.0,200703386.0,201380968.0,201640584.0,207963157.0,203133351.0,204357964.0,207119887.0,215713121.0,207694038.0,203860265.0,204423233.0,204352068.0,204663697.0,210079289.0,209583314.0,206072314.0,211579495.0,210228559.0,212659170.0,213604713.0,212157643.0,207743686.0,217124848.0,218923907.0,213243141.0,211633022.0,214556727.0,210979099.0,212163914.0,213129201.0,209926343.0,210423386.0,210645895.0,211040309.0,217332530.0,213596010.0,217864544.0,218396001.0,219465224.0,215529564.0,215157837.0,222619348.0,216994581.0,216419323.0,216509998.0,217853326.0,217452806.0,218595369.0,218480664.0,224053166.0,219700201.0,220200938.0,219826538.0,220559275.0,225308052.0,220906025.0,221657250.0,224851375.0,224836430.0,225587527.0,221305773.0,221361610.0,221540008.0,222132856.0,222903148.0,227265919.0,225595003.0,225202721.0,225610772.0,226220823.0,226625191.0,226839633.0,235878223.0,228542387.0,234857965.0,230647966.0,240183728.0,236753896.0,230058140.0,230233015.0,230285573.0,233165283.0,241923572.0,233235785.0,238599607.0,238275330.0,230870208.0,235368600.0,235319580.0,240716349.0,242985533.0,244475810.0,239878355.0,241492491.0,235724774.0,241978333.0,237505157.0,243036299.0,242445677.0,236866179.0,239250678.0,243856437.0,242420346.0,246992581.0,248538252.0,249231495.0,248484416.0,246267183.0,240998827.0,245725237.0,246783525.0,249857337.0,251113506.0,251137892.0,253502267.0,244132121.0,251763236.0,247859382.0,291929144.0,250610061.0,245166582.0,245603340.0,246465078.0,252545903.0,250282356.0,246635456.0,252821131.0,256312596.0,252424117.0,246864961.0,246450271.0,252048154.0,249588839.0,250003822.0,254268084.0,250337047.0,256593969.0,255355399.0,251176908.0,255785790.0,262626410.0,260369877.0,256785141.0,256736896.0,253384778.0,253669702.0,254395350.0,254731863.0,260735524.0,301927848.0,257135727.0,256258577.0,256485749.0,261093294.0,262190142.0,264805710.0,260741755.0,256739847.0,256458782.0,266532231.0,269295676.0,260439119.0,260474336.0,264489829.0,263294558.0,270814923.0,269786841.0,275147452.0,263446882.0,262772967.0,263303723.0,267420504.0,264324591.0,269002556.0,270244912.0,266794907.0,270955229.0,270376580.0,266048636.0,269573449.0,274022833.0,270984801.0,277608993.0,272170509.0,269468070.0,268976816.0,273386420.0,269655493.0,270200898.0,270510640.0,270545204.0,271450849.0,271255498.0,271637627.0,272217620.0,277585748.0,274459415.0,278612336.0,280389711.0,276858355.0,273873571.0,282760091.0,276935883.0,283014218.0,283549507.0,288920797.0,281505361.0,280508402.0,288015198.0,286820934.0,279246304.0,278496307.0,278884227.0,279435373.0,284763624.0,280972427.0,281355651.0,287518046.0,285150728.0,289230972.0,284892887.0,287520355.0,291125198.0,287712302.0,289252400.0,285219439.0,290621014.0,288908103.0,284834880.0,285082123.0,285064913.0,286050476.0,286202737.0,291287157.0,293660583.0,286902026.0,293657103.0,289645576.0,289290600.0,288357711.0,289556901.0,293389303.0,289721394.0,301702898.0,292833240.0,297609668.0,299901214.0,301763599.0,302540070.0,302074823.0,301120249.0,292728604.0,294313199.0,294727495.0,307549284.0,295310336.0,295414316.0,296152016.0,296392327.0,296005277.0,300815207.0,308394819.0,303655012.0,301821949.0,297913258.0,298352671.0,298189673.0,299298110.0,302700297.0,297742384.0,297473944.0,298958641.0,312643880.0,304819560.0,303916447.0,299668235.0,299803646.0,300116399.0,305253315.0,303643602.0,303788834.0,304130156.0,304548649.0,305054443.0,308172810.0,302965399.0,311079231.0,312802060.0,307107801.0,310421432.0,304852062.0,312119223.0,312452136.0,308383381.0,309140985.0,308840317.0,315301147.0,312384690.0,318223071.0,321280742.0,328769196.0,311884804.0,315907069.0,310472223.0,315609583.0,318361650.0,323036485.0,311001103.0,311797052.0,312029906.0,317391138.0,315437733.0,320117488.0,316585155.0,331060444.0,316834435.0,317047492.0,317716757.0,321867382.0,318419054.0,325516760.0,322457207.0,329868747.0,336376852.0,334428763.0,330500656.0,332371153.0,335343820.0,327716205.0,326634103.0,332206875.0,337298795.0,322837644.0,323169836.0,326572032.0,325584006.0,325441293.0,325351260.0,325814114.0,325979568.0,333682833.0,329599241.0,335702750.0,327523938.0,327241897.0,327637567.0,347647081.0,333126623.0,329051470.0,332011295.0,333644272.0,329655789.0,336272591.0,337697749.0,330738789.0,331381948.0,343830420.0,338874018.0,343801493.0,350892614.0,342454014.0,338140966.0,338942814.0,339018086.0,339805026.0,340489546.0,338738729.0,334175653.0,334233428.0,340684666.0,337533646.0,339677910.0,343356949.0,344940787.0,336017120.0,336025862.0,336488191.0,337031400.0,341615144.0,340105212.0,340650275.0,340706141.0,345441216.0,343121229.0,346517004.0,347091178.0,344572474.0,364223779.0,344112694.0,343577212.0,350402068.0,346551522.0,357965846.0,345560453.0,346259496.0,346663296.0,351050075.0,349380513.0,365633907.0,352034221.0,356470456.0,356834160.0,345728087.0,366221326.0,350141068.0,349736728.0,350246229.0,350588432.0,359729078.0,358721028.0,363973622.0,367848891.0,372823304.0,368831477.0,372707670.0,366887155.0,353893604.0,364182677.0,358717470.0,362114434.0,355872431.0,355978510.0,364040450.0,371897109.0,366875493.0,356994411.0,357560539.0,363806813.0,363448811.0,360202323.0,358785710.0,359300613.0,368446393.0,361994618.0,368923308.0,373916927.0,366748116.0,364850639.0,363212609.0,367848881.0,499049569.0,368025206.0,373385123.0,364014199.0,364496779.0,364705932.0,368651055.0,365551867.0,365873065.0,373791390.0,370199080.0,371401921.0,366212025.0,371551836.0,367793409.0,367885778.0]} \ No newline at end of file diff --git a/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/tukey.json b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/tukey.json new file mode 100644 index 0000000..7bac2cf --- /dev/null +++ b/apps/aquatic/crates/ws_protocol/target/criterion/deserialize-announce-request/latest/tukey.json @@ -0,0 +1 @@ +[16365.852585890654,16945.45216926808,18491.05105827454,19070.650641651962] \ No newline at end of file diff --git a/apps/aquatic/deny.toml b/apps/aquatic/deny.toml new file mode 100644 index 0000000..4790f14 --- /dev/null +++ b/apps/aquatic/deny.toml @@ -0,0 +1,194 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url of the advisory database to use +db-url = "https://github.com/rustsec/advisory-db" +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "ISC", + "BSD-3-Clause", + "Zlib", + "MPL-2.0", +] +# List of explictly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.9 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# THe optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] diff --git a/apps/aquatic/docker/aquatic_udp.Dockerfile b/apps/aquatic/docker/aquatic_udp.Dockerfile new file mode 100644 index 0000000..bbcb168 --- /dev/null +++ b/apps/aquatic/docker/aquatic_udp.Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1 + +# aquatic_udp +# +# Please note that running aquatic_udp under Docker is NOT RECOMMENDED due to +# suboptimal performance. This file is provided as a starting point for those +# who still wish to do so. +# +# Customize by setting CONFIG_FILE_CONTENTS and +# ACCESS_LIST_CONTENTS environment variables. +# +# By default runs tracker on port 3000 without info hash access control. +# +# Run from repository root directory with: +# $ DOCKER_BUILDKIT=1 docker build -t aquatic-udp -f docker/aquatic_udp.Dockerfile . +# $ docker run -it -p 0.0.0.0:3000:3000/udp --name aquatic-udp aquatic-udp +# +# Pass --network="host" to run command for much better performance. + +FROM rust:latest AS builder + +WORKDIR /usr/src/aquatic + +COPY . . + +RUN . ./scripts/env-native-cpu-without-avx-512 && cargo build --release -p aquatic_udp + +FROM debian:stable-slim + +ENV CONFIG_FILE_CONTENTS "log_level = 'warn'" +ENV ACCESS_LIST_CONTENTS "" + +WORKDIR /root/ + +COPY --from=builder /usr/src/aquatic/target/release/aquatic_udp ./ + +# Create entry point script for setting config and access +# list file contents at runtime +COPY <<-"EOT" ./entrypoint.sh +#!/bin/bash +echo -e "$CONFIG_FILE_CONTENTS" > ./config.toml +echo -e "$ACCESS_LIST_CONTENTS" > ./access-list.txt +exec ./aquatic_udp -c ./config.toml "$@" +EOT + +RUN chmod +x ./entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/apps/aquatic/docker/ci.Dockerfile b/apps/aquatic/docker/ci.Dockerfile new file mode 100644 index 0000000..91890d5 --- /dev/null +++ b/apps/aquatic/docker/ci.Dockerfile @@ -0,0 +1,18 @@ +# Can be used to run file transfer CI test locally. Usage: +# 1. docker build -t aquatic -f ./docker/ci.Dockerfile . +# 2. docker run aquatic +# 3. On failure, run `docker rmi aquatic -f` and go back to step 1 + +FROM rust:bullseye + +RUN mkdir "/opt/aquatic" + +ENV "GITHUB_WORKSPACE" "/opt/aquatic" + +WORKDIR "/opt/aquatic" + +COPY ./.github/actions/test-file-transfers/entrypoint.sh entrypoint.sh +COPY Cargo.toml Cargo.lock ./ +COPY crates crates + +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/apps/aquatic/documents/aquatic-architecture-2024.svg b/apps/aquatic/documents/aquatic-architecture-2024.svg new file mode 100644 index 0000000..82abb29 --- /dev/null +++ b/apps/aquatic/documents/aquatic-architecture-2024.svg @@ -0,0 +1,3 @@ + + +
Swarm worker








Swarm worker...
Sharded
swarm state
Sharded...
Swarm worker








Swarm worker...
Sharded
swarm state
Sharded...
Socket worker








Socket worker...
Connection
state
Connection...
Socket worker








Socket worker...
Connection
state
Connection...
Socket worker








Socket worker...
Connection
state
Connection...
Socket
Socket
Socket
Socket
Socket
Socket

Socket worker responsibilities

- Bind to sockets with SO_REUSEPORT
- Establish connections
- Receive and parse requests
- Run access lists checks
- Send on announce requests to responsible swarm workers
- Split scrape requests and send on to responsible swarm workers
- Receive responses from swarm workers, serialize them and send them to peers

Socket worker responsibilities...

Swarm worker responsibilities

- Receive announce and scrape requests from socket workers
- Update swarm state as appropriate
- Create responses and send them to socket workers

Swarm worker responsibilities...
Architectural overview of aquatic BitTorrent tracker 
Architectural overview of aquatic BitTorrent tracker 
Requests
Requests
Responses
Responses
\ No newline at end of file diff --git a/apps/aquatic/documents/aquatic-http-load-test-2023-01-25.pdf b/apps/aquatic/documents/aquatic-http-load-test-2023-01-25.pdf new file mode 100644 index 0000000000000000000000000000000000000000..20ba9d631843440e6c3d47e7a9595fec2680b0ef GIT binary patch literal 152145 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 literal 0 HcmV?d00001 diff --git a/apps/aquatic/documents/aquatic-udp-load-test-2023-01-11.pdf b/apps/aquatic/documents/aquatic-udp-load-test-2023-01-11.pdf new file mode 100644 index 0000000000000000000000000000000000000000..34b7266e815c0e35b10cdc18b61bcbc40e11ebd2 GIT binary patch literal 140778 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 literal 0 HcmV?d00001 diff --git a/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.md b/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.md new file mode 100644 index 0000000..82d6e70 --- /dev/null +++ b/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.md @@ -0,0 +1,106 @@ +2024-02-10 Joakim Frostegård + +# UDP BitTorrent tracker throughput comparison + +This is a performance comparison of several UDP BitTorrent tracker implementations. + +Benchmarks were run using [aquatic_bencher](../crates/bencher), with `--cpu-mode subsequent-one-per-pair`. + +## Software and hardware + +### Tracker implementations + +| Name | Commit | +|---------------|---------| +| [aquatic_udp] | 21a5301 | +| [opentracker] | 110868e | +| [chihaya] | 2f79440 | + +[aquatic_udp]: ../crates/udp +[opentracker]: http://erdgeist.org/arts/software/opentracker/ +[chihaya]: https://github.com/chihaya/chihaya + +### OS and compilers + +| Name | Version | +|--------|---------| +| Debian | 12.4 | +| Linux | 6.5.10 | +| rustc | 1.76.0 | +| GCC | 12.2.0 | +| go | 1.19.8 | + +### Hardware + +Hetzner CCX63: 48 dedicated vCPUs (AMD Milan Epyc 7003) + +## Results + +![UDP BitTorrent tracker throughput](./aquatic-udp-load-test-2024-02-10.png) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ UDP BitTorrent tracker troughput +

Average responses per second, best result.

+
CPU coresaquatic_udp (mio)aquatic_udp (io_uring)opentrackerchihaya
1186,939226,065190,54055,989
2371,478444,353379,623111,226
4734,709876,642748,401136,983
61,034,8041,267,006901,600131,827
81,296,6931,521,1131,170,928131,779
121,871,3531,837,2231,675,059130,942
162,037,7132,258,3211,645,828127,256
diff --git a/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.png b/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.png new file mode 100644 index 0000000000000000000000000000000000000000..a91b862b0f1074c8511264a18d3325e51ba15f03 GIT binary patch literal 59692 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 CY;*Yl literal 0 HcmV?d00001 diff --git a/apps/aquatic/documents/aquatic-udp-load-test-illustration-2023-01-11.png b/apps/aquatic/documents/aquatic-udp-load-test-illustration-2023-01-11.png new file mode 100644 index 0000000000000000000000000000000000000000..42c3d38f3aed21d352400c5c43849316a75a0009 GIT binary patch literal 45761 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 z%0D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 literal 0 HcmV?d00001 diff --git a/apps/aquatic/documents/aquatic-ws-load-test-illustration-2023-01-25.png b/apps/aquatic/documents/aquatic-ws-load-test-illustration-2023-01-25.png new file mode 100644 index 0000000000000000000000000000000000000000..aa155309ffe1da3e46ed2f56116f12f88b1a792e GIT binary patch literal 65302 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%0APB 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'JU)Go1y0lop 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 literal 0 HcmV?d00001 diff --git a/apps/aquatic/scripts/bench/setup-udp-bookworm.sh b/apps/aquatic/scripts/bench/setup-udp-bookworm.sh new file mode 100755 index 0000000..ed59127 --- /dev/null +++ b/apps/aquatic/scripts/bench/setup-udp-bookworm.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Prepare for running aquatic_bench for UDP on Debian 12 + +# Install dependencies +sudo apt-get update && sudo apt-get upgrade -y +sudo apt-get install -y curl vim htop screen cmake build-essential pkg-config git screen cvs zlib1g zlib1g-dev golang +sudo echo "deb http://deb.debian.org/debian bookworm-backports main contrib" >> /etc/apt/sources.list +sudo apt-get update && sudo apt-get install -y linux-image-amd64/bookworm-backports +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" + +# Build aquatic +. ./scripts/env-native-cpu-without-avx-512 +# export RUSTFLAGS="-C target-cpu=native" +cargo build --profile "release-debug" -p aquatic_udp --features "io-uring" +cargo build --profile "release-debug" -p aquatic_udp_load_test +cargo build --profile "release-debug" -p aquatic_bencher --features udp +git log --oneline | head -n 1 + +cd $HOME +mkdir -p projects +cd projects + +# Install opentracker +cvs -d :pserver:cvs@cvs.fefe.de:/cvs -z9 co libowfat +cd libowfat +make +cd .. +git clone git://erdgeist.org/opentracker +cd opentracker +sed -i "s/^OPTS_production=-O3/OPTS_production=-O3 -march=native -mtune=native/g" Makefile +make +sudo cp ./opentracker /usr/local/bin/ +git log --oneline | head -n 1 +cd .. + +# Install chihaya +git clone https://github.com/chihaya/chihaya.git +cd chihaya +go build ./cmd/chihaya +sudo cp ./chihaya /usr/local/bin/ +git log --oneline | head -n 1 +cd .. + +rustc --version +gcc --version +go version +lscpu + +echo "Finished. Reboot before running aquatic_bencher" \ No newline at end of file diff --git a/apps/aquatic/scripts/ci-file-transfers.sh b/apps/aquatic/scripts/ci-file-transfers.sh new file mode 100644 index 0000000..bd8d527 --- /dev/null +++ b/apps/aquatic/scripts/ci-file-transfers.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Run file transfer CI test locally + +set -e + +docker build -t aquatic -f ./docker/ci.Dockerfile . +docker run aquatic +docker rmi aquatic -f \ No newline at end of file diff --git a/apps/aquatic/scripts/criterion/aquatic-http-announce-response-to-bytes.sh b/apps/aquatic/scripts/criterion/aquatic-http-announce-response-to-bytes.sh new file mode 100755 index 0000000..9785363 --- /dev/null +++ b/apps/aquatic/scripts/criterion/aquatic-http-announce-response-to-bytes.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Run benchmark, comparing against previous result. + +set -e + +. ./scripts/env-native-cpu-without-avx-512 + +cargo bench --bench bench_announce_response_to_bytes -- --noplot --baseline latest + +read -p "Replace previous benchmark result with this one (y/N)? " answer + +case ${answer:0:1} in + y|Y ) + cd aquatic_http_protocol/target/criterion/announce-response-to-bytes/ && + rm -r latest && + mv new latest && + echo "Replaced previous benchmark" + ;; +esac \ No newline at end of file diff --git a/apps/aquatic/scripts/criterion/aquatic-http-request-from-bytes.sh b/apps/aquatic/scripts/criterion/aquatic-http-request-from-bytes.sh new file mode 100755 index 0000000..4990a03 --- /dev/null +++ b/apps/aquatic/scripts/criterion/aquatic-http-request-from-bytes.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Run benchmark, comparing against previous result. + +set -e + +. ./scripts/env-native-cpu-without-avx-512 + +cargo bench --bench bench_request_from_bytes -- --noplot --baseline latest + +read -p "Replace previous benchmark result with this one (y/N)? " answer + +case ${answer:0:1} in + y|Y ) + cd aquatic_http_protocol/target/criterion/request-from-bytes/ && + rm -r latest && + mv new latest && + echo "Replaced previous benchmark" + ;; +esac \ No newline at end of file diff --git a/apps/aquatic/scripts/criterion/aquatic-ws-deserialize-announce-request.sh b/apps/aquatic/scripts/criterion/aquatic-ws-deserialize-announce-request.sh new file mode 100755 index 0000000..48dbc01 --- /dev/null +++ b/apps/aquatic/scripts/criterion/aquatic-ws-deserialize-announce-request.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Run benchmark, comparing against previous result. + +set -e + +. ./scripts/env-native-cpu-without-avx-512 + +cargo bench --bench bench_deserialize_announce_request -- --noplot --baseline latest + +read -p "Replace previous benchmark result with this one (y/N)? " answer + +case ${answer:0:1} in + y|Y ) + cd aquatic_ws_protocol/target/criterion/deserialize-announce-request/ && + rm -r latest && + mv new latest && + echo "Replaced previous benchmark" + ;; +esac \ No newline at end of file diff --git a/apps/aquatic/scripts/env-native-cpu-without-avx-512 b/apps/aquatic/scripts/env-native-cpu-without-avx-512 new file mode 100644 index 0000000..8321fd7 --- /dev/null +++ b/apps/aquatic/scripts/env-native-cpu-without-avx-512 @@ -0,0 +1,9 @@ +#!/bin/sh + +# Compile with target-cpu=native but without AVX512 features, since they +# decrease performance. + +DISABLE_AVX512=$(rustc --print target-features | grep " avx512" | grep -v "avx512fp16" | + awk '{print $1}' | sed 's/^/-C target-feature=-/' | xargs) + +export RUSTFLAGS="-C target-cpu=native $DISABLE_AVX512" \ No newline at end of file diff --git a/apps/aquatic/scripts/gen-tls.sh b/apps/aquatic/scripts/gen-tls.sh new file mode 100755 index 0000000..db8a19b --- /dev/null +++ b/apps/aquatic/scripts/gen-tls.sh @@ -0,0 +1,17 @@ +#/bin/bash +# Generate self-signed TLS cert and private key for local testing + +set -e + +TLS_DIR="./tmp/tls" + +mkdir -p "$TLS_DIR" +cd "$TLS_DIR" + +openssl ecparam -genkey -name prime256v1 -out key.pem +openssl req -new -sha256 -key key.pem -out csr.csr -subj "/C=GB/ST=Test/L=Test/O=Test/OU=Test/CN=example.com" +openssl req -x509 -sha256 -nodes -days 365 -key key.pem -in csr.csr -out cert.crt +openssl pkcs8 -in key.pem -topk8 -nocrypt -out key.pk8 + +echo "tls_certificate_path = \"$TLS_DIR/cert.crt\"" +echo "tls_private_key_path = \"$TLS_DIR/key.pk8\"" diff --git a/apps/aquatic/scripts/heaptrack.sh b/apps/aquatic/scripts/heaptrack.sh new file mode 100755 index 0000000..bd602dd --- /dev/null +++ b/apps/aquatic/scripts/heaptrack.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +heaptrack --pid $(pgrep "^aquatic_[a-z]{1,4}$") diff --git a/apps/aquatic/scripts/run-aquatic-http.sh b/apps/aquatic/scripts/run-aquatic-http.sh new file mode 100755 index 0000000..26943da --- /dev/null +++ b/apps/aquatic/scripts/run-aquatic-http.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_http --features "prometheus" -- $@ diff --git a/apps/aquatic/scripts/run-aquatic-udp-io-uring.sh b/apps/aquatic/scripts/run-aquatic-udp-io-uring.sh new file mode 100755 index 0000000..2438388 --- /dev/null +++ b/apps/aquatic/scripts/run-aquatic-udp-io-uring.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_udp --features "io-uring" -- $@ diff --git a/apps/aquatic/scripts/run-aquatic-udp.sh b/apps/aquatic/scripts/run-aquatic-udp.sh new file mode 100755 index 0000000..0007289 --- /dev/null +++ b/apps/aquatic/scripts/run-aquatic-udp.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_udp -- $@ diff --git a/apps/aquatic/scripts/run-aquatic-ws.sh b/apps/aquatic/scripts/run-aquatic-ws.sh new file mode 100755 index 0000000..d6681ce --- /dev/null +++ b/apps/aquatic/scripts/run-aquatic-ws.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_ws --features "prometheus" -- $@ diff --git a/apps/aquatic/scripts/run-load-test-http.sh b/apps/aquatic/scripts/run-load-test-http.sh new file mode 100755 index 0000000..7a76099 --- /dev/null +++ b/apps/aquatic/scripts/run-load-test-http.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_http_load_test -- $@ diff --git a/apps/aquatic/scripts/run-load-test-udp.sh b/apps/aquatic/scripts/run-load-test-udp.sh new file mode 100755 index 0000000..38ee3ab --- /dev/null +++ b/apps/aquatic/scripts/run-load-test-udp.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_udp_load_test -- $@ diff --git a/apps/aquatic/scripts/run-load-test-ws.sh b/apps/aquatic/scripts/run-load-test-ws.sh new file mode 100755 index 0000000..9c86576 --- /dev/null +++ b/apps/aquatic/scripts/run-load-test-ws.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. ./scripts/env-native-cpu-without-avx-512 + +cargo run --profile "release-debug" -p aquatic_ws_load_test -- $@ diff --git a/apps/aquatic/scripts/watch-threads.sh b/apps/aquatic/scripts/watch-threads.sh new file mode 100755 index 0000000..dfeb355 --- /dev/null +++ b/apps/aquatic/scripts/watch-threads.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +watch -n 0.5 ps H -o euser,pid,tid,comm,%mem,rss,%cpu,psr -p `pgrep aquatic`