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 0000000..20ba9d6 Binary files /dev/null and b/apps/aquatic/documents/aquatic-http-load-test-2023-01-25.pdf differ diff --git a/apps/aquatic/documents/aquatic-http-load-test-illustration-2023-01-25.png b/apps/aquatic/documents/aquatic-http-load-test-illustration-2023-01-25.png new file mode 100644 index 0000000..cb5e48d Binary files /dev/null and b/apps/aquatic/documents/aquatic-http-load-test-illustration-2023-01-25.png differ 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 0000000..34b7266 Binary files /dev/null and b/apps/aquatic/documents/aquatic-udp-load-test-2023-01-11.pdf differ 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 0000000..a91b862 Binary files /dev/null and b/apps/aquatic/documents/aquatic-udp-load-test-2024-02-10.png differ 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 0000000..42c3d38 Binary files /dev/null and b/apps/aquatic/documents/aquatic-udp-load-test-illustration-2023-01-11.png differ diff --git a/apps/aquatic/documents/aquatic-ws-load-test-2023-01-25.pdf b/apps/aquatic/documents/aquatic-ws-load-test-2023-01-25.pdf new file mode 100644 index 0000000..9478eda Binary files /dev/null and b/apps/aquatic/documents/aquatic-ws-load-test-2023-01-25.pdf differ 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 0000000..aa15530 Binary files /dev/null and b/apps/aquatic/documents/aquatic-ws-load-test-illustration-2023-01-25.png differ 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`