├── .github ├── dependabot.yml └── workflows │ ├── audit.yml │ ├── check.yml │ └── test.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets └── screenshot.png ├── deny.toml └── src ├── client.rs ├── common ├── consts.rs ├── control.rs ├── data.rs ├── mod.rs ├── net_utils.rs ├── opts.rs ├── perf_test.rs ├── stream_worker.rs └── ui.rs ├── controller.rs ├── lib.rs ├── main.rs └── server.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: daily 11 | ignore: 12 | - dependency-name: "*" 13 | # patch and minor updates don't matter for libraries 14 | # remove this ignore rule if your package has binaries 15 | update-types: 16 | - "version-update:semver-patch" 17 | - "version-update:semver-minor" 18 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | - '**/Cargo.lock' 11 | push: 12 | branches: 13 | - master 14 | 15 | env: 16 | RUST_BACKTRACE: 1 17 | CARGO_TERM_COLOR: always 18 | CLICOLOR: 1 19 | 20 | jobs: 21 | security_audit: 22 | permissions: 23 | issues: write # to create issues (actions-rs/audit-check) 24 | checks: write # to create check (actions-rs/audit-check) 25 | runs-on: ubuntu-latest 26 | # Prevent sudden announcement of a new advisory from failing ci: 27 | continue-on-error: true 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | - uses: actions-rs/audit-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | cargo_deny: 36 | permissions: 37 | issues: write # to create issues (actions-rs/audit-check) 38 | checks: write # to create check (actions-rs/audit-check) 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | checks: 43 | - bans licenses sources 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: EmbarkStudios/cargo-deny-action@v2 47 | with: 48 | command: check ${{ matrix.checks }} 49 | rust-version: stable 50 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | name: check 12 | jobs: 13 | fmt: 14 | runs-on: ubuntu-latest 15 | name: stable / fmt 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: true 20 | - name: Install nightly 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: nightly 24 | components: rustfmt 25 | - name: cargo +nightly fmt --all -- --check 26 | run: cargo +nightly fmt --all -- --check 27 | clippy: 28 | runs-on: ubuntu-latest 29 | name: ${{ matrix.toolchain }} / clippy 30 | permissions: 31 | contents: read 32 | checks: write 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | toolchain: [stable, beta] 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | submodules: true 41 | - name: Install ${{ matrix.toolchain }} 42 | uses: dtolnay/rust-toolchain@master 43 | with: 44 | toolchain: ${{ matrix.toolchain }} 45 | components: clippy 46 | - name: cargo clippy 47 | uses: actions-rs/clippy-check@v1 48 | with: 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | doc: 51 | runs-on: ubuntu-latest 52 | name: nightly / doc 53 | steps: 54 | - uses: actions/checkout@v4 55 | with: 56 | submodules: true 57 | - name: Install nightly 58 | uses: dtolnay/rust-toolchain@nightly 59 | - name: cargo doc 60 | run: cargo doc --no-deps --all-features 61 | env: 62 | RUSTDOCFLAGS: --cfg docsrs 63 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | env: 12 | RUST_BACKTRACE: 1 13 | CARGO_TERM_COLOR: always 14 | CLICOLOR: 1 15 | 16 | name: test 17 | jobs: 18 | required: 19 | runs-on: ubuntu-latest 20 | name: ubuntu / ${{ matrix.toolchain }} 21 | strategy: 22 | matrix: 23 | toolchain: [stable, beta] 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: true 28 | - name: Install ${{ matrix.toolchain }} 29 | uses: dtolnay/rust-toolchain@master 30 | with: 31 | toolchain: ${{ matrix.toolchain }} 32 | - name: cargo generate-lockfile 33 | if: hashFiles('Cargo.lock') == '' 34 | run: cargo generate-lockfile 35 | # https://twitter.com/jonhoo/status/1571290371124260865 36 | - name: cargo test --locked 37 | run: cargo test --locked --all-features --all-targets 38 | # https://github.com/rust-lang/cargo/issues/6669 39 | - name: cargo test --doc 40 | run: cargo test --locked --all-features --doc 41 | minimal: 42 | runs-on: ubuntu-latest 43 | name: ubuntu / stable / minimal-versions 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | submodules: true 48 | - name: Install stable 49 | uses: dtolnay/rust-toolchain@stable 50 | - uses: taiki-e/install-action@cargo-hack 51 | - uses: taiki-e/install-action@cargo-minimal-versions 52 | - name: Install nightly for -Zminimal-versions 53 | uses: dtolnay/rust-toolchain@nightly 54 | - name: rustup default stable 55 | run: rustup default stable 56 | - name: cargo +nightly update -Zminimal-versions 57 | run: cargo minimal-versions check --workspace --ignore-private --tests 58 | os-check: 59 | runs-on: ${{ matrix.os }} 60 | name: ${{ matrix.os }} / stable 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | os: [macos-latest, windows-latest] 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | submodules: true 69 | - name: Install stable 70 | uses: dtolnay/rust-toolchain@stable 71 | - name: cargo generate-lockfile 72 | if: hashFiles('Cargo.lock') == '' 73 | run: cargo generate-lockfile 74 | - name: cargo test 75 | run: cargo test --locked --all-features --all-targets 76 | coverage: 77 | runs-on: ubuntu-latest 78 | name: ubuntu / stable / coverage 79 | steps: 80 | - uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | - name: Install stable 84 | uses: dtolnay/rust-toolchain@stable 85 | with: 86 | components: llvm-tools-preview 87 | - name: cargo install cargo-llvm-cov 88 | uses: taiki-e/install-action@cargo-llvm-cov 89 | - name: cargo generate-lockfile 90 | if: hashFiles('Cargo.lock') == '' 91 | run: cargo generate-lockfile 92 | - name: cargo llvm-cov 93 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info 94 | - name: Upload to codecov.io 95 | uses: codecov/codecov-action@v5 96 | with: 97 | fail_ci_if_error: false 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.98" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 90 | 91 | [[package]] 92 | name = "backtrace" 93 | version = "0.3.74" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 96 | dependencies = [ 97 | "addr2line", 98 | "cfg-if", 99 | "libc", 100 | "miniz_oxide", 101 | "object", 102 | "rustc-demangle", 103 | "windows-targets", 104 | ] 105 | 106 | [[package]] 107 | name = "bitflags" 108 | version = "2.9.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 111 | 112 | [[package]] 113 | name = "bytes" 114 | version = "1.10.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 117 | 118 | [[package]] 119 | name = "cfg-if" 120 | version = "1.0.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 123 | 124 | [[package]] 125 | name = "cfg_aliases" 126 | version = "0.2.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 129 | 130 | [[package]] 131 | name = "clap" 132 | version = "4.5.37" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" 135 | dependencies = [ 136 | "clap_builder", 137 | "clap_derive", 138 | ] 139 | 140 | [[package]] 141 | name = "clap-verbosity-flag" 142 | version = "3.0.2" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" 145 | dependencies = [ 146 | "clap", 147 | "log", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_builder" 152 | version = "4.5.37" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" 155 | dependencies = [ 156 | "anstream", 157 | "anstyle", 158 | "clap_lex", 159 | "strsim", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_derive" 164 | version = "4.5.32" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 167 | dependencies = [ 168 | "heck", 169 | "proc-macro2", 170 | "quote", 171 | "syn", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_lex" 176 | version = "0.7.4" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 179 | 180 | [[package]] 181 | name = "cling" 182 | version = "0.1.3" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "063c9cd7a8d30faea20dbbb1945fd06f320271757f0d1aadc84151d2c4693d1e" 185 | dependencies = [ 186 | "anyhow", 187 | "clap", 188 | "cling-derive", 189 | "indoc", 190 | "itertools", 191 | "rustc_version", 192 | "rustversion", 193 | "static_assertions", 194 | "termcolor", 195 | "tracing", 196 | ] 197 | 198 | [[package]] 199 | name = "cling-derive" 200 | version = "0.1.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "63a20a22d1439a0cad87318a4cfac840d1c2491f13405fcf5880c573a6f9854c" 203 | dependencies = [ 204 | "darling", 205 | "indoc", 206 | "proc-macro2", 207 | "quote", 208 | "syn", 209 | ] 210 | 211 | [[package]] 212 | name = "colorchoice" 213 | version = "1.0.3" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 216 | 217 | [[package]] 218 | name = "colored" 219 | version = "3.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 222 | dependencies = [ 223 | "windows-sys 0.59.0", 224 | ] 225 | 226 | [[package]] 227 | name = "darling" 228 | version = "0.20.11" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 231 | dependencies = [ 232 | "darling_core", 233 | "darling_macro", 234 | ] 235 | 236 | [[package]] 237 | name = "darling_core" 238 | version = "0.20.11" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 241 | dependencies = [ 242 | "fnv", 243 | "ident_case", 244 | "proc-macro2", 245 | "quote", 246 | "strsim", 247 | "syn", 248 | ] 249 | 250 | [[package]] 251 | name = "darling_macro" 252 | version = "0.20.11" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 255 | dependencies = [ 256 | "darling_core", 257 | "quote", 258 | "syn", 259 | ] 260 | 261 | [[package]] 262 | name = "diff" 263 | version = "0.1.13" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 266 | 267 | [[package]] 268 | name = "either" 269 | version = "1.15.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 272 | 273 | [[package]] 274 | name = "env_filter" 275 | version = "0.1.3" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 278 | dependencies = [ 279 | "log", 280 | "regex", 281 | ] 282 | 283 | [[package]] 284 | name = "env_logger" 285 | version = "0.11.8" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 288 | dependencies = [ 289 | "anstream", 290 | "anstyle", 291 | "env_filter", 292 | "jiff", 293 | "log", 294 | ] 295 | 296 | [[package]] 297 | name = "fnv" 298 | version = "1.0.7" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 301 | 302 | [[package]] 303 | name = "futures" 304 | version = "0.3.31" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 307 | dependencies = [ 308 | "futures-channel", 309 | "futures-core", 310 | "futures-executor", 311 | "futures-io", 312 | "futures-sink", 313 | "futures-task", 314 | "futures-util", 315 | ] 316 | 317 | [[package]] 318 | name = "futures-channel" 319 | version = "0.3.31" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 322 | dependencies = [ 323 | "futures-core", 324 | "futures-sink", 325 | ] 326 | 327 | [[package]] 328 | name = "futures-core" 329 | version = "0.3.31" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 332 | 333 | [[package]] 334 | name = "futures-executor" 335 | version = "0.3.31" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 338 | dependencies = [ 339 | "futures-core", 340 | "futures-task", 341 | "futures-util", 342 | ] 343 | 344 | [[package]] 345 | name = "futures-io" 346 | version = "0.3.31" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 349 | 350 | [[package]] 351 | name = "futures-macro" 352 | version = "0.3.31" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 355 | dependencies = [ 356 | "proc-macro2", 357 | "quote", 358 | "syn", 359 | ] 360 | 361 | [[package]] 362 | name = "futures-sink" 363 | version = "0.3.31" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 366 | 367 | [[package]] 368 | name = "futures-task" 369 | version = "0.3.31" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 372 | 373 | [[package]] 374 | name = "futures-util" 375 | version = "0.3.31" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 378 | dependencies = [ 379 | "futures-channel", 380 | "futures-core", 381 | "futures-io", 382 | "futures-macro", 383 | "futures-sink", 384 | "futures-task", 385 | "memchr", 386 | "pin-project-lite", 387 | "pin-utils", 388 | "slab", 389 | ] 390 | 391 | [[package]] 392 | name = "getrandom" 393 | version = "0.3.2" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 396 | dependencies = [ 397 | "cfg-if", 398 | "libc", 399 | "r-efi", 400 | "wasi 0.14.2+wasi-0.2.4", 401 | ] 402 | 403 | [[package]] 404 | name = "gimli" 405 | version = "0.31.1" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 408 | 409 | [[package]] 410 | name = "heck" 411 | version = "0.5.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 414 | 415 | [[package]] 416 | name = "ident_case" 417 | version = "1.0.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 420 | 421 | [[package]] 422 | name = "indoc" 423 | version = "2.0.6" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 426 | 427 | [[package]] 428 | name = "is_terminal_polyfill" 429 | version = "1.70.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 432 | 433 | [[package]] 434 | name = "itertools" 435 | version = "0.14.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 438 | dependencies = [ 439 | "either", 440 | ] 441 | 442 | [[package]] 443 | name = "itoa" 444 | version = "1.0.15" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 447 | 448 | [[package]] 449 | name = "jiff" 450 | version = "0.2.12" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" 453 | dependencies = [ 454 | "jiff-static", 455 | "log", 456 | "portable-atomic", 457 | "portable-atomic-util", 458 | "serde", 459 | ] 460 | 461 | [[package]] 462 | name = "jiff-static" 463 | version = "0.2.12" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" 466 | dependencies = [ 467 | "proc-macro2", 468 | "quote", 469 | "syn", 470 | ] 471 | 472 | [[package]] 473 | name = "libc" 474 | version = "0.2.172" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 477 | 478 | [[package]] 479 | name = "log" 480 | version = "0.4.27" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 483 | 484 | [[package]] 485 | name = "memchr" 486 | version = "2.7.4" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 489 | 490 | [[package]] 491 | name = "miniz_oxide" 492 | version = "0.8.8" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 495 | dependencies = [ 496 | "adler2", 497 | ] 498 | 499 | [[package]] 500 | name = "mio" 501 | version = "1.0.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 504 | dependencies = [ 505 | "libc", 506 | "wasi 0.11.0+wasi-snapshot-preview1", 507 | "windows-sys 0.52.0", 508 | ] 509 | 510 | [[package]] 511 | name = "netperf" 512 | version = "0.2.8" 513 | dependencies = [ 514 | "anyhow", 515 | "bytes", 516 | "clap", 517 | "clap-verbosity-flag", 518 | "cling", 519 | "colored", 520 | "env_logger", 521 | "futures", 522 | "log", 523 | "nix", 524 | "pretty_assertions", 525 | "serde", 526 | "serde_json", 527 | "thiserror", 528 | "tokio", 529 | "tokio-stream", 530 | "uuid", 531 | ] 532 | 533 | [[package]] 534 | name = "nix" 535 | version = "0.30.1" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 538 | dependencies = [ 539 | "bitflags", 540 | "cfg-if", 541 | "cfg_aliases", 542 | "libc", 543 | ] 544 | 545 | [[package]] 546 | name = "object" 547 | version = "0.36.7" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 550 | dependencies = [ 551 | "memchr", 552 | ] 553 | 554 | [[package]] 555 | name = "once_cell" 556 | version = "1.21.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 559 | 560 | [[package]] 561 | name = "pin-project-lite" 562 | version = "0.2.16" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 565 | 566 | [[package]] 567 | name = "pin-utils" 568 | version = "0.1.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 571 | 572 | [[package]] 573 | name = "portable-atomic" 574 | version = "1.11.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 577 | 578 | [[package]] 579 | name = "portable-atomic-util" 580 | version = "0.2.4" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 583 | dependencies = [ 584 | "portable-atomic", 585 | ] 586 | 587 | [[package]] 588 | name = "pretty_assertions" 589 | version = "1.4.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 592 | dependencies = [ 593 | "diff", 594 | "yansi", 595 | ] 596 | 597 | [[package]] 598 | name = "proc-macro2" 599 | version = "1.0.95" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 602 | dependencies = [ 603 | "unicode-ident", 604 | ] 605 | 606 | [[package]] 607 | name = "quote" 608 | version = "1.0.40" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 611 | dependencies = [ 612 | "proc-macro2", 613 | ] 614 | 615 | [[package]] 616 | name = "r-efi" 617 | version = "5.2.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 620 | 621 | [[package]] 622 | name = "regex" 623 | version = "1.11.1" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 626 | dependencies = [ 627 | "aho-corasick", 628 | "memchr", 629 | "regex-automata", 630 | "regex-syntax", 631 | ] 632 | 633 | [[package]] 634 | name = "regex-automata" 635 | version = "0.4.9" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 638 | dependencies = [ 639 | "aho-corasick", 640 | "memchr", 641 | "regex-syntax", 642 | ] 643 | 644 | [[package]] 645 | name = "regex-syntax" 646 | version = "0.8.5" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 649 | 650 | [[package]] 651 | name = "rustc-demangle" 652 | version = "0.1.24" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 655 | 656 | [[package]] 657 | name = "rustc_version" 658 | version = "0.4.1" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 661 | dependencies = [ 662 | "semver", 663 | ] 664 | 665 | [[package]] 666 | name = "rustversion" 667 | version = "1.0.20" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 670 | 671 | [[package]] 672 | name = "ryu" 673 | version = "1.0.20" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 676 | 677 | [[package]] 678 | name = "semver" 679 | version = "1.0.26" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 682 | 683 | [[package]] 684 | name = "serde" 685 | version = "1.0.219" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 688 | dependencies = [ 689 | "serde_derive", 690 | ] 691 | 692 | [[package]] 693 | name = "serde_derive" 694 | version = "1.0.219" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 697 | dependencies = [ 698 | "proc-macro2", 699 | "quote", 700 | "syn", 701 | ] 702 | 703 | [[package]] 704 | name = "serde_json" 705 | version = "1.0.140" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 708 | dependencies = [ 709 | "itoa", 710 | "memchr", 711 | "ryu", 712 | "serde", 713 | ] 714 | 715 | [[package]] 716 | name = "slab" 717 | version = "0.4.9" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 720 | dependencies = [ 721 | "autocfg", 722 | ] 723 | 724 | [[package]] 725 | name = "socket2" 726 | version = "0.5.9" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 729 | dependencies = [ 730 | "libc", 731 | "windows-sys 0.52.0", 732 | ] 733 | 734 | [[package]] 735 | name = "static_assertions" 736 | version = "1.1.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 739 | 740 | [[package]] 741 | name = "strsim" 742 | version = "0.11.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 745 | 746 | [[package]] 747 | name = "syn" 748 | version = "2.0.101" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 751 | dependencies = [ 752 | "proc-macro2", 753 | "quote", 754 | "unicode-ident", 755 | ] 756 | 757 | [[package]] 758 | name = "termcolor" 759 | version = "1.4.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 762 | dependencies = [ 763 | "winapi-util", 764 | ] 765 | 766 | [[package]] 767 | name = "thiserror" 768 | version = "2.0.12" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 771 | dependencies = [ 772 | "thiserror-impl", 773 | ] 774 | 775 | [[package]] 776 | name = "thiserror-impl" 777 | version = "2.0.12" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 780 | dependencies = [ 781 | "proc-macro2", 782 | "quote", 783 | "syn", 784 | ] 785 | 786 | [[package]] 787 | name = "tokio" 788 | version = "1.44.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 791 | dependencies = [ 792 | "backtrace", 793 | "bytes", 794 | "libc", 795 | "mio", 796 | "pin-project-lite", 797 | "socket2", 798 | "tokio-macros", 799 | "windows-sys 0.52.0", 800 | ] 801 | 802 | [[package]] 803 | name = "tokio-macros" 804 | version = "2.5.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 807 | dependencies = [ 808 | "proc-macro2", 809 | "quote", 810 | "syn", 811 | ] 812 | 813 | [[package]] 814 | name = "tokio-stream" 815 | version = "0.1.17" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 818 | dependencies = [ 819 | "futures-core", 820 | "pin-project-lite", 821 | "tokio", 822 | ] 823 | 824 | [[package]] 825 | name = "tracing" 826 | version = "0.1.41" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 829 | dependencies = [ 830 | "log", 831 | "pin-project-lite", 832 | "tracing-attributes", 833 | "tracing-core", 834 | ] 835 | 836 | [[package]] 837 | name = "tracing-attributes" 838 | version = "0.1.28" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 841 | dependencies = [ 842 | "proc-macro2", 843 | "quote", 844 | "syn", 845 | ] 846 | 847 | [[package]] 848 | name = "tracing-core" 849 | version = "0.1.33" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 852 | dependencies = [ 853 | "once_cell", 854 | ] 855 | 856 | [[package]] 857 | name = "unicode-ident" 858 | version = "1.0.18" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 861 | 862 | [[package]] 863 | name = "utf8parse" 864 | version = "0.2.2" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 867 | 868 | [[package]] 869 | name = "uuid" 870 | version = "1.16.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 873 | dependencies = [ 874 | "getrandom", 875 | ] 876 | 877 | [[package]] 878 | name = "wasi" 879 | version = "0.11.0+wasi-snapshot-preview1" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 882 | 883 | [[package]] 884 | name = "wasi" 885 | version = "0.14.2+wasi-0.2.4" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 888 | dependencies = [ 889 | "wit-bindgen-rt", 890 | ] 891 | 892 | [[package]] 893 | name = "winapi-util" 894 | version = "0.1.9" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 897 | dependencies = [ 898 | "windows-sys 0.59.0", 899 | ] 900 | 901 | [[package]] 902 | name = "windows-sys" 903 | version = "0.52.0" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 906 | dependencies = [ 907 | "windows-targets", 908 | ] 909 | 910 | [[package]] 911 | name = "windows-sys" 912 | version = "0.59.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 915 | dependencies = [ 916 | "windows-targets", 917 | ] 918 | 919 | [[package]] 920 | name = "windows-targets" 921 | version = "0.52.6" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 924 | dependencies = [ 925 | "windows_aarch64_gnullvm", 926 | "windows_aarch64_msvc", 927 | "windows_i686_gnu", 928 | "windows_i686_gnullvm", 929 | "windows_i686_msvc", 930 | "windows_x86_64_gnu", 931 | "windows_x86_64_gnullvm", 932 | "windows_x86_64_msvc", 933 | ] 934 | 935 | [[package]] 936 | name = "windows_aarch64_gnullvm" 937 | version = "0.52.6" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 940 | 941 | [[package]] 942 | name = "windows_aarch64_msvc" 943 | version = "0.52.6" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 946 | 947 | [[package]] 948 | name = "windows_i686_gnu" 949 | version = "0.52.6" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 952 | 953 | [[package]] 954 | name = "windows_i686_gnullvm" 955 | version = "0.52.6" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 958 | 959 | [[package]] 960 | name = "windows_i686_msvc" 961 | version = "0.52.6" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 964 | 965 | [[package]] 966 | name = "windows_x86_64_gnu" 967 | version = "0.52.6" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 970 | 971 | [[package]] 972 | name = "windows_x86_64_gnullvm" 973 | version = "0.52.6" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 976 | 977 | [[package]] 978 | name = "windows_x86_64_msvc" 979 | version = "0.52.6" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 982 | 983 | [[package]] 984 | name = "wit-bindgen-rt" 985 | version = "0.39.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 988 | dependencies = [ 989 | "bitflags", 990 | ] 991 | 992 | [[package]] 993 | name = "yansi" 994 | version = "1.0.1" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 997 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netperf" 3 | version = "0.2.8" 4 | description = "A network performance measurement tool" 5 | authors = ["Ahmed Farghal "] 6 | edition = "2021" 7 | repository = "https://github.com/AhmedSoliman/netperf" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["network", "performance", "iperf3"] 10 | categories = ["command-line-utilities", "simulation"] 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | [dependencies] 15 | anyhow = "1.0.68" 16 | colored = "3.0.0" 17 | env_logger = "0.11.8" 18 | log = "0.4.17" 19 | nix = "0.30.1" 20 | bytes = "1.4.0" 21 | clap-verbosity-flag = "3.0.2" 22 | futures = "0.3.26" 23 | serde = { version = "1.0.152", features = ["derive"] } 24 | serde_json = "1.0.91" 25 | thiserror = "2.0.12" 26 | cling = { version = "0.1.0" } 27 | tokio = { version = "1.44.2", features = [ 28 | "rt", 29 | "rt-multi-thread", 30 | "net", 31 | "time", 32 | "sync", 33 | "macros", 34 | "fs", 35 | "io-util", 36 | ] } 37 | uuid = { version = "1.3.0", default-features = false, features = ["v4"] } 38 | clap = { version = "4.1.4", features = ["color", "derive"] } 39 | 40 | [dev-dependencies] 41 | pretty_assertions = "1.3.0" 42 | tokio = { version = "1.25.0", features = ["test-util"] } 43 | tokio-stream = "0.1.17" 44 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ahmed Soliman Farghal 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netperf 2 | ![](https://img.shields.io/crates/l/netperf) 3 | ![](https://github.com/AhmedSoliman/netperf/workflows/Continuous%20Integration/badge.svg) 4 | ![https://crates.io/crates/netperf](https://img.shields.io/crates/v/netperf.svg) 5 | 6 | A network (TCP-only) performance measurement tool written in Rust, inspired by iperf3's 7 | original code. 8 | 9 | 10 | All basic features are implemented. Key differences from iperf3: 11 | - Uses a different control protocol (not compatible with iperf3's servers or clients) 12 | - Multi-threaded, parallel streams (-P) will be executed on different threads. 13 | - Design simulates realworld server/client applications in terms of work scheduling. 14 | 15 | ![](https://github.com/AhmedSoliman/netperf/blob/master/assets/screenshot.png) 16 | 17 | # Installation 18 | ``` 19 | cargo install --locked netperf 20 | ``` 21 | 22 | # Usage 23 | On one node you run netperf in server mode: 24 | ``` 25 | netperf -s 26 | ``` 27 | On a client node, you need to connect to that server (you will need an addressable IP address IPv6 is supported). 28 | ``` 29 | netperf -c ::1 30 | ``` 31 | By default, the test will use a single stream (client sends and server receives). You can control the number of parallel streams with `-P` and the direction of traffic with `-R/--bidir` 32 | 33 | # Current Limitations 34 | - Does not support configuring MSS, Congestion control algorithm. 35 | - No UDP/STCP support. 36 | - Does not collect extra stats like retransmits, cwnd, etc. (contributions are appreciated) 37 | 38 | 39 | ### License 40 | Licensed under either of Apache License, Version 2.0 or MIT license at your option. 41 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 42 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedSoliman/netperf/affe87c5fb1b742194eb2537b27ae21cd6a24589/assets/screenshot.png -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # Root options 13 | 14 | # When creating the dependency graph used as the source of truth when checks are 15 | # executed, this field can be used to prune crates from the graph, removing them 16 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 17 | # is pruned from the graph, all of its dependencies will also be pruned unless 18 | # they are connected to another crate in the graph that hasn't been pruned, 19 | # so it should be used with care. The identifiers are [Package ID Specifications] 20 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 21 | #exclude = [] 22 | # If set, these feature will be enabled when collecting metadata. If `--features` 23 | # is specified on the cmd line they will take precedence over this option. 24 | #features = [] 25 | 26 | # This section is considered when running `cargo deny check advisories` 27 | # More documentation for the advisories section can be found here: 28 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 29 | [advisories] 30 | version = 2 31 | # The path where the advisory database is cloned/fetched into 32 | db-path = "~/.cargo/advisory-db" 33 | # The url(s) of the advisory databases to use 34 | db-urls = ["https://github.com/rustsec/advisory-db"] 35 | # A list of advisory IDs to ignore. Note that ignored advisories will still 36 | # output a note when they are encountered. 37 | ignore = [ 38 | #"RUSTSEC-0000-0000", 39 | ] 40 | 41 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 42 | # If this is false, then it uses a built-in git library. 43 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 44 | # See Git Authentication for more information about setting up git authentication. 45 | #git-fetch-with-cli = true 46 | 47 | # This section is considered when running `cargo deny check licenses` 48 | # More documentation for the licenses section can be found here: 49 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 50 | [licenses] 51 | version = 2 52 | # List of explicitly allowed licenses 53 | # See https://spdx.org/licenses/ for list of possible licenses 54 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 55 | allow = [ 56 | "Apache-2.0", 57 | "MIT", 58 | "MPL-2.0", 59 | "Unicode-3.0", 60 | "Unlicense", 61 | ] 62 | # The confidence threshold for detecting a license from license text. 63 | # The higher the value, the more closely the license text must be to the 64 | # canonical license text of a valid SPDX license file. 65 | # [possible values: any between 0.0 and 1.0]. 66 | confidence-threshold = 0.8 67 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 68 | # aren't accepted for every possible crate as with the normal allow list 69 | exceptions = [ 70 | # Each entry is the crate and version constraint, and its specific allow 71 | # list 72 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 73 | ] 74 | 75 | # Some crates don't have (easily) machine readable licensing information, 76 | # adding a clarification entry for it allows you to manually specify the 77 | # licensing information 78 | #[[licenses.clarify]] 79 | # The name of the crate the clarification applies to 80 | #name = "ring" 81 | # The optional version constraint for the crate 82 | #version = "*" 83 | # The SPDX expression for the license requirements of the crate 84 | #expression = "MIT AND ISC AND OpenSSL" 85 | # One or more files in the crate's source used as the "source of truth" for 86 | # the license expression. If the contents match, the clarification will be used 87 | # when running the license check, otherwise the clarification will be ignored 88 | # and the crate will be checked normally, which may produce warnings or errors 89 | # depending on the rest of your configuration 90 | #license-files = [ 91 | # Each entry is a crate relative path, and the (opaque) hash of its contents 92 | #{ path = "LICENSE", hash = 0xbd0eed23 } 93 | #] 94 | 95 | [licenses.private] 96 | # If true, ignores workspace crates that aren't published, or are only 97 | # published to private registries. 98 | # To see how to mark a crate as unpublished (to the official registry), 99 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 100 | ignore = true 101 | # One or more private registries that you might publish crates to, if a crate 102 | # is only published to private registries, and ignore is true, the crate will 103 | # not have its license(s) checked 104 | registries = [ 105 | #"https://sekretz.com/registry 106 | ] 107 | 108 | # This section is considered when running `cargo deny check bans`. 109 | # More documentation about the 'bans' section can be found here: 110 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 111 | [bans] 112 | # Lint level for when multiple versions of the same crate are detected 113 | multiple-versions = "warn" 114 | # Lint level for when a crate version requirement is `*` 115 | wildcards = "deny" 116 | # The graph highlighting used when creating dotgraphs for crates 117 | # with multiple versions 118 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 119 | # * simplest-path - The path to the version with the fewest edges is highlighted 120 | # * all - Both lowest-version and simplest-path are used 121 | highlight = "simplest-path" 122 | # The default lint level for `default` features for crates that are members of 123 | # the workspace that is being checked. This can be overriden by allowing/denying 124 | # `default` on a crate-by-crate basis if desired. 125 | workspace-default-features = "allow" 126 | # The default lint level for `default` features for external crates that are not 127 | # members of the workspace. This can be overriden by allowing/denying `default` 128 | # on a crate-by-crate basis if desired. 129 | external-default-features = "allow" 130 | # List of crates that are allowed. Use with care! 131 | allow = [ 132 | #{ name = "ansi_term", version = "=0.11.0" }, 133 | ] 134 | # List of crates to deny 135 | deny = [ 136 | # Each entry the name of a crate and a version range. If version is 137 | # not specified, all versions will be matched. 138 | #{ name = "ansi_term", version = "=0.11.0" }, 139 | # 140 | # Wrapper crates can optionally be specified to allow the crate when it 141 | # is a direct dependency of the otherwise banned crate 142 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 143 | ] 144 | 145 | # List of features to allow/deny 146 | # Each entry the name of a crate and a version range. If version is 147 | # not specified, all versions will be matched. 148 | #[[bans.features]] 149 | #name = "reqwest" 150 | # Features to not allow 151 | #deny = ["json"] 152 | # Features to allow 153 | #allow = [ 154 | # "rustls", 155 | # "__rustls", 156 | # "__tls", 157 | # "hyper-rustls", 158 | # "rustls", 159 | # "rustls-pemfile", 160 | # "rustls-tls-webpki-roots", 161 | # "tokio-rustls", 162 | # "webpki-roots", 163 | #] 164 | # If true, the allowed features must exactly match the enabled feature set. If 165 | # this is set there is no point setting `deny` 166 | #exact = true 167 | 168 | # Certain crates/versions that will be skipped when doing duplicate detection. 169 | skip = [ 170 | #{ name = "ansi_term", version = "=0.11.0" }, 171 | ] 172 | # Similarly to `skip` allows you to skip certain crates during duplicate 173 | # detection. Unlike skip, it also includes the entire tree of transitive 174 | # dependencies starting at the specified crate, up to a certain depth, which is 175 | # by default infinite. 176 | skip-tree = [ 177 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 178 | ] 179 | 180 | # This section is considered when running `cargo deny check sources`. 181 | # More documentation about the 'sources' section can be found here: 182 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 183 | [sources] 184 | # Lint level for what to happen when a crate from a crate registry that is not 185 | # in the allow list is encountered 186 | unknown-registry = "deny" 187 | # Lint level for what to happen when a crate from a git repository that is not 188 | # in the allow list is encountered 189 | unknown-git = "deny" 190 | # List of URLs for allowed crate registries. Defaults to the crates.io index 191 | # if not specified. If it is specified but empty, no registries are allowed. 192 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 193 | # List of URLs for allowed Git repositories 194 | allow-git = [] 195 | 196 | [sources.allow-org] 197 | # 1 or more github.com organizations to allow git sources for 198 | github = [] 199 | 200 | [output] 201 | # When outputting inclusion graphs in diagnostics that include features, this 202 | # option can be used to specify the depth at which feature edges will be added. 203 | # This option is included since the graphs can be quite large and the addition 204 | # of features from the crate(s) to all of the graph roots can be far too verbose. 205 | # This option can be overridden via `--feature-depth` on the cmd line 206 | feature-depth = 1 207 | 208 | [graph] 209 | # If 1 or more target triples (and optionally, target_features) are specified, 210 | # only the specified targets will be checked when running `cargo deny check`. 211 | # This means, if a particular package is only ever used as a target specific 212 | # dependency, such as, for example, the `nix` crate only being used via the 213 | # `target_family = "unix"` configuration, that only having windows targets in 214 | # this list would mean the nix crate, as well as any of its exclusive 215 | # dependencies not shared by any other crates, would be ignored, as the target 216 | # list here is effectively saying which targets you are building for. 217 | targets = [ 218 | # The triple can be any string, but only the target triples built in to 219 | # rustc (as of 1.40) can be checked against actual config expressions 220 | #{ triple = "x86_64-unknown-linux-musl" }, 221 | # You can also specify which target_features you promise are enabled for a 222 | # particular target. target_features are currently not validated against 223 | # the actual valid features supported by the target architecture. 224 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 225 | ] 226 | # If true, metadata will be collected with `--no-default-features`. The same 227 | # caveat with `all-features` applies 228 | no-default-features = false 229 | # If true, metadata will be collected with `--all-features`. Note that this can't 230 | # be toggled off if true, if you want to conditionally enable `--all-features` it 231 | # is recommended to pass `--all-features` on the cmd line instead 232 | all-features = false 233 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::common::consts::DEFAULT_BLOCK_SIZE; 2 | use crate::common::control::{ClientMessage, ServerMessage}; 3 | use crate::common::data::{Role, TestParameters}; 4 | use crate::common::net_utils::*; 5 | use crate::common::opts::{ClientOpts, CommonOpts}; 6 | use crate::common::perf_test::PerfTest; 7 | use crate::controller::TestController; 8 | use anyhow::Result; 9 | use colored::Colorize; 10 | use log::debug; 11 | use tokio::net::TcpStream; 12 | 13 | pub async fn run_client( 14 | common_opts: &CommonOpts, 15 | client_opts: &ClientOpts, 16 | ) -> Result<(), anyhow::Error> { 17 | // We are sure this is set at this point. 18 | let client_host = client_opts.client.as_ref().unwrap(); 19 | let port = common_opts.port; 20 | let address = format!("{}:{}", client_host, port); 21 | print!("Connecting to ({}:{})...", client_host, port); 22 | let mut control_socket = TcpStream::connect(address.clone()).await?; 23 | println!("{}", " Connected!".green()); 24 | let cookie = uuid::Uuid::new_v4().hyphenated().to_string(); 25 | client_send_message( 26 | &mut control_socket, 27 | ClientMessage::Hello { 28 | cookie: cookie.clone(), 29 | }, 30 | ) 31 | .await?; 32 | debug!("Hello sent!"); 33 | // Wait for the initial Welcome message. If the server is busy, this will 34 | // return an Error of AccessDenied and the client will terminate. 35 | let _: ServerMessage = client_read_message(&mut control_socket).await?; 36 | debug!("Welcome received!"); 37 | 38 | // Sending the header of the test paramters in JSON 39 | // The format is size(4 bytes)+JSON 40 | let params = TestParameters::from_opts(client_opts, DEFAULT_BLOCK_SIZE); 41 | client_send_message( 42 | &mut control_socket, 43 | ClientMessage::SendParameters(params.clone()), 44 | ) 45 | .await?; 46 | debug!("Params sent!"); 47 | 48 | let perf_test = PerfTest::new(Some(address), cookie, control_socket, Role::Client, params); 49 | let controller = TestController::new(perf_test); 50 | let handle = tokio::spawn(async move { controller.run_controller().await }); 51 | // Wait for the test to finish. 52 | handle.await??; 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /src/common/consts.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | pub const INTERNAL_PROT_BUFFER: usize = 5; 4 | pub const PROTOCOL_TIMEOUT: Duration = Duration::from_secs(10); 5 | pub const MB: usize = 1024 * 1024; 6 | pub const MAX_CONTROL_MESSAGE: u32 = 20 * (MB) as u32; 7 | pub const DEFAULT_BLOCK_SIZE: usize = 2 * MB; 8 | // pub const MAX_BLOCK_SIZE: i32 = 1 * MB; 9 | // pub const MAX_TCP_BUFFER: i32 = 512 * MB; 10 | // pub const MAX_MSS: i32 = 9 * 1024; 11 | 12 | // Protocol Constants 13 | pub const MESSAGE_LENGTH_SIZE_BYTES: usize = std::mem::size_of::(); 14 | -------------------------------------------------------------------------------- /src/common/control.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This file holds the protocol handling for the control socket. 3 | //! 4 | //! The control socket is the first socket that gets created once 5 | //! a test is started. Communication and coordination between the 6 | //! client and server happens through this socket. The protocol is 7 | //! very simple. 8 | //! 9 | //! For every frame: 10 | //! LENGTH (u32) + JSON Object representing one of the messages 11 | //! defined in this file. 12 | //! 13 | //! The parsing of this socket will incur 2 syscalls for every frame 14 | //! this is chosen for convenience and simplicity. We first read the 15 | //! length (u32) then parse the JSON message and match it against the 16 | //! enum defined in the enums `ServerMessage` and `ClientMessage`. 17 | 18 | use crate::common::data::TestParameters; 19 | use serde::{Deserialize, Serialize}; 20 | use std::collections::HashMap; 21 | use thiserror::Error; 22 | 23 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 24 | pub struct StreamStats { 25 | pub sender: bool, 26 | pub duration_millis: u64, 27 | pub bytes_transferred: usize, 28 | pub syscalls: usize, 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 32 | pub struct TestResults { 33 | pub streams: HashMap, 34 | } 35 | 36 | /// Error messages set by server. 37 | #[derive(Serialize, Deserialize, Debug, Error, Eq, PartialEq)] 38 | pub enum ServerError { 39 | #[error("Access denied: {0}")] 40 | AccessDenied(String), 41 | #[error("Cannot accept a stream connection: {0}")] 42 | CannotAcceptStream(String), 43 | } 44 | 45 | /// Error messages set by clients. 46 | #[derive(Serialize, Deserialize, Debug, Error, Eq, PartialEq)] 47 | pub enum ClientError { 48 | #[error("Cannot create a stream connection: {0}")] 49 | CannotCreateStream(String), 50 | } 51 | 52 | /// This is the top-level message that gets serialised on the wire, 53 | /// The reason this exists is to decode whether we have an Error or 54 | /// a valid response in the protocol decoder and translate the error 55 | /// into (std::error::Error) instead of passing this Error as a normal message. 56 | /// 57 | /// This technique also makes it such that users can perform exhaustive 58 | /// pattern matching on all message types without having to handle the 59 | /// error case. 60 | #[derive(Serialize, Deserialize, Debug)] 61 | pub enum ClientEnvelope { 62 | ClientMessage(ClientMessage), 63 | Error(ClientError), 64 | } 65 | 66 | /// See docs for `ClientEnvelope` 67 | #[derive(Serialize, Deserialize, Debug)] 68 | pub enum ServerEnvelope { 69 | ServerMessage(ServerMessage), 70 | Error(ServerError), 71 | } 72 | 73 | /// A control message that can be sent from clients. 74 | /// CLIENT => SERVER 75 | #[derive(Serialize, Deserialize, Debug)] 76 | pub enum ClientMessage { 77 | /// The first message that the client needs to send to the server 78 | /// upon successful connection. The cookie is a random UUID that 79 | /// the client uses to identify itself and the subsequent stream 80 | /// connections. 81 | Hello { cookie: String }, 82 | /// Sending the test parameters 83 | SendParameters(TestParameters), 84 | /// Sending the test results 85 | SendResults(TestResults), 86 | } 87 | 88 | /// A control message that can be sent from servers. 89 | /// SERVER => CLIENT 90 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] 91 | pub enum ServerMessage { 92 | /// The server's response to Hello. 93 | Welcome, 94 | SetState(State), 95 | SendResults(TestResults), 96 | } 97 | 98 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 99 | pub enum State { 100 | // Parameters have been exchanged, but server is not ready yet to ask for data stream 101 | // connections. 102 | TestStart, 103 | // Asks the client to establish the data stream connections. 104 | CreateStreams { cookie: String }, 105 | // All connections are established, stream the data and measure. 106 | Running, 107 | // We are asked to exchange the TestResults between server and client. Client will initiate this 108 | // exchange once it receives a transition into this state. 109 | ExchangeResults, 110 | DisplayResults, 111 | } 112 | 113 | // Attempts to extract ServerError from Result<_, anyhow::Error> 114 | pub fn to_server_error(result: &Result) -> Option<&ServerError> { 115 | match result { 116 | Err(e) => match e.downcast_ref::() { 117 | Some(s) => Some(s), 118 | _ => None, 119 | }, 120 | _ => None, 121 | } 122 | } 123 | 124 | // Attempts to extract ClientError from Result<_, anyhow::Error> 125 | pub fn to_client_error(result: &Result) -> Option<&ClientError> { 126 | match result { 127 | Err(e) => match e.downcast_ref::() { 128 | Some(s) => Some(s), 129 | _ => None, 130 | }, 131 | _ => None, 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use super::*; 138 | use anyhow::anyhow; 139 | 140 | #[test] 141 | fn test_error_extraction() { 142 | // Server Errors 143 | let a: Result<(), anyhow::Error> = Ok(()); 144 | assert!(matches!(to_server_error(&a), None)); 145 | 146 | let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "Test"); 147 | let a: Result<(), anyhow::Error> = Err(err.into()); 148 | assert!(matches!(to_server_error(&a), None)); 149 | 150 | let a: Result<(), anyhow::Error> = Err(anyhow!("Missing Stuff!")); 151 | assert!(matches!(to_server_error(&a), None)); 152 | 153 | let a: Result<(), anyhow::Error> = Err(anyhow::Error::new(ServerError::AccessDenied( 154 | "Something went wrong!".to_owned(), 155 | ))); 156 | 157 | assert!(matches!( 158 | to_server_error(&a), 159 | Some(ServerError::AccessDenied(msg)) if *msg == "Something went wrong!".to_owned())); 160 | 161 | // Client Errors 162 | let a: Result<(), anyhow::Error> = Ok(()); 163 | assert!(matches!(to_client_error(&a), None)); 164 | 165 | let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "Test"); 166 | let a: Result<(), anyhow::Error> = Err(err.into()); 167 | assert!(matches!(to_client_error(&a), None)); 168 | 169 | let a: Result<(), anyhow::Error> = Err(anyhow!("Missing Stuff!")); 170 | assert!(matches!(to_client_error(&a), None)); 171 | 172 | let a: Result<(), anyhow::Error> = Err(anyhow::Error::new( 173 | ClientError::CannotCreateStream("Something went wrong!".to_owned()), 174 | )); 175 | 176 | assert!(matches!( 177 | to_client_error(&a), 178 | Some(ClientError::CannotCreateStream(msg)) if *msg == "Something went wrong!".to_owned())); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/common/data.rs: -------------------------------------------------------------------------------- 1 | use crate::common::opts::ClientOpts; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Eq, PartialEq)] 5 | pub enum Role { 6 | Server, 7 | Client, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 11 | pub enum Direction { 12 | /// Traffic flows from Client => Server (the default) 13 | ClientToServer, 14 | /// Traffic flows from Server => Client. 15 | ServerToClient, 16 | /// Both ways. 17 | Bidirectional, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] 21 | pub struct TestParameters { 22 | pub direction: Direction, 23 | /// omit the first n seconds. 24 | pub omit_seconds: u32, 25 | pub time_seconds: u64, 26 | // The number of data streams 27 | pub parallel: u16, 28 | pub block_size: usize, 29 | pub client_version: String, 30 | pub no_delay: bool, 31 | pub socket_buffers: Option, 32 | } 33 | 34 | impl TestParameters { 35 | pub fn from_opts(opts: &ClientOpts, default_block_size: usize) -> Self { 36 | let direction = if opts.bidir { 37 | Direction::Bidirectional 38 | } else if opts.reverse { 39 | Direction::ServerToClient 40 | } else { 41 | Direction::ClientToServer 42 | }; 43 | TestParameters { 44 | direction, 45 | omit_seconds: 0, 46 | time_seconds: opts.time, 47 | parallel: opts.parallel, 48 | block_size: opts.length.unwrap_or(default_block_size), 49 | client_version: env!("CARGO_PKG_VERSION").to_string(), 50 | no_delay: opts.no_delay, 51 | socket_buffers: opts.socket_buffers, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Exports 2 | pub mod consts; 3 | pub mod control; 4 | pub mod data; 5 | pub mod net_utils; 6 | pub mod opts; 7 | pub mod perf_test; 8 | pub mod stream_worker; 9 | pub mod ui; 10 | -------------------------------------------------------------------------------- /src/common/net_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::common::consts::{MAX_CONTROL_MESSAGE, MESSAGE_LENGTH_SIZE_BYTES}; 2 | use crate::common::control::*; 3 | use anyhow::{bail, Context, Result}; 4 | use bytes::{BufMut, BytesMut}; 5 | use log::debug; 6 | use serde::de::DeserializeOwned; 7 | use serde::Serialize; 8 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 9 | use tokio::net::TcpStream; 10 | 11 | pub async fn client_send_message(stream: &mut A, message: ClientMessage) -> Result<()> 12 | where 13 | A: AsyncWriteExt + Unpin, 14 | { 15 | send_control_message(stream, ClientEnvelope::ClientMessage(message)).await 16 | } 17 | 18 | pub async fn client_send_error(stream: &mut A, error: ClientError) -> Result<()> 19 | where 20 | A: AsyncWriteExt + Unpin, 21 | { 22 | send_control_message(stream, ClientEnvelope::Error(error)).await 23 | } 24 | 25 | pub async fn server_send_message(stream: &mut A, message: ServerMessage) -> Result<()> 26 | where 27 | A: AsyncWriteExt + Unpin, 28 | { 29 | send_control_message(stream, ServerEnvelope::ServerMessage(message)).await 30 | } 31 | pub async fn server_send_error(stream: &mut A, error: ServerError) -> Result<()> 32 | where 33 | A: AsyncWriteExt + Unpin, 34 | { 35 | send_control_message(stream, ServerEnvelope::Error(error)).await 36 | } 37 | 38 | // If the client sent an error, the result will be set with Err(ClientError) instead. 39 | pub async fn server_read_message(stream: &mut A) -> Result 40 | where 41 | A: AsyncReadExt + Unpin, 42 | { 43 | let envelope: ClientEnvelope = read_control_message(stream).await?; 44 | match envelope { 45 | ClientEnvelope::ClientMessage(m) => Ok(m), 46 | ClientEnvelope::Error(e) => Err(anyhow::Error::new(e)), 47 | } 48 | } 49 | 50 | pub async fn client_read_message(stream: &mut A) -> Result 51 | where 52 | A: AsyncReadExt + Unpin, 53 | { 54 | let envelope: ServerEnvelope = read_control_message(stream).await?; 55 | match envelope { 56 | ServerEnvelope::ServerMessage(m) => Ok(m), 57 | ServerEnvelope::Error(e) => Err(anyhow::Error::new(e)), 58 | } 59 | } 60 | 61 | /// A helper that encodes a json-serializable object and sends it over the stream. 62 | async fn send_control_message(stream: &mut A, message: T) -> Result<()> 63 | where 64 | A: AsyncWriteExt + Unpin, 65 | T: Serialize, 66 | { 67 | let json = serde_json::to_string(&message)?; 68 | let payload = json.as_bytes(); 69 | // This is our invariant. We cannot serialise big payloads here.Serialize 70 | assert!(payload.len() <= (u32::MAX) as usize); 71 | let mut buf = BytesMut::with_capacity(MESSAGE_LENGTH_SIZE_BYTES + payload.len()); 72 | // Shipping the length first. 73 | buf.put_u32(payload.len() as u32); 74 | buf.put_slice(payload); 75 | debug!("Sent: {} bytes", &buf[..].len()); 76 | stream.write_all(&buf[..]).await?; 77 | Ok(()) 78 | } 79 | 80 | /// A helper that encodes reads a control message off the wire and deserialize it to type T 81 | /// if possible. You should strictly use that for 'Envelope' messages. 82 | async fn read_control_message(stream: &mut A) -> Result 83 | where 84 | A: AsyncReadExt + Unpin, 85 | T: DeserializeOwned, 86 | { 87 | // Let's first read the message size in one syscall. 88 | // We know that this is inefficient but it makes handling the protocol much simpler 89 | // And saves us memory as we are not over allocating buffers. The control protocol 90 | // is not chatty anyway. 91 | let message_size = stream.read_u32().await?; 92 | // We restrict receiving control messages over 20MB (defined in consts.rs) 93 | if message_size > MAX_CONTROL_MESSAGE { 94 | bail!( 95 | "Unusually large protocol negotiation header: {}MB, max allowed: {}MB", 96 | message_size / 1024, 97 | MAX_CONTROL_MESSAGE, 98 | ); 99 | } 100 | 101 | let mut buf = BytesMut::with_capacity(message_size as usize); 102 | let mut remaining_bytes: u64 = message_size as u64; 103 | let mut counter: usize = 0; 104 | while remaining_bytes > 0 { 105 | counter += 1; 106 | // Only read up-to the remaining-bytes, don't over read. 107 | // It's important that we don't read more as we don't want to mess up 108 | // the protocol. The next read should find the LENGTH as the first 4 bytes. 109 | let mut handle = stream.take(remaining_bytes); 110 | let bytes_read = handle.read_buf(&mut buf).await?; 111 | if bytes_read == 0 { 112 | // We have reached EOF. This is unexpected. 113 | // XXX: Handle 114 | bail!("Connected was closed by peer."); 115 | } 116 | // usize is u64 in most cases. 117 | remaining_bytes -= bytes_read as u64; 118 | } 119 | debug!( 120 | "Received a control message ({} bytes) in {} iterations", 121 | message_size, counter 122 | ); 123 | assert_eq!(message_size as usize, buf.len()); 124 | 125 | let obj = serde_json::from_slice(&buf) 126 | .with_context(|| "Invalid protocol, could not deserialise JSON")?; 127 | Ok(obj) 128 | } 129 | 130 | pub fn peer_to_string(stream: &TcpStream) -> String { 131 | stream 132 | .peer_addr() 133 | .map(|addr| addr.to_string()) 134 | // The reason for or_else here is to avoid allocating the string if this was never called. 135 | .unwrap_or_else(|_| "".to_owned()) 136 | } 137 | 138 | #[cfg(test)] 139 | mod tests { 140 | use super::*; 141 | use crate::common::control::to_server_error; 142 | use pretty_assertions::assert_eq; 143 | use serde::{Deserialize, Serialize}; 144 | 145 | // A test serializable structure to use in testing 146 | #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] 147 | struct MockSerializable { 148 | username: String, 149 | } 150 | 151 | #[tokio::test] 152 | async fn test_send_control_message() -> Result<()> { 153 | let mut buf = vec![]; 154 | let obj = MockSerializable { 155 | username: "asoli".to_owned(), 156 | }; 157 | // create a stream to serialise data into 158 | send_control_message(&mut buf, obj.clone()).await?; 159 | assert_eq!(buf.len(), 24); 160 | // let's receive the same value and compare. 161 | let data: MockSerializable = read_control_message(&mut &buf[..]).await?; 162 | assert_eq!(data, obj); 163 | Ok(()) 164 | } 165 | 166 | #[tokio::test] 167 | async fn test_messages() -> Result<()> { 168 | // Send and receive server messages. 169 | { 170 | let mut buf = vec![]; 171 | server_send_message(&mut buf, ServerMessage::Welcome).await?; 172 | // let's receive the same value and compare. 173 | let data = client_read_message(&mut &buf[..]).await?; 174 | assert_eq!(data, ServerMessage::Welcome); 175 | } 176 | // Send and receive errors 177 | { 178 | let mut buf = vec![]; 179 | server_send_error( 180 | &mut buf, 181 | ServerError::AccessDenied("Something went wrong".to_owned()), 182 | ) 183 | .await?; 184 | // let's receive the same value and compare. 185 | let data = client_read_message(&mut &buf[..]).await; 186 | assert!(data.is_err()); 187 | assert!(matches!(to_server_error(&data), 188 | Some(ServerError::AccessDenied(msg)) if *msg == "Something went wrong" 189 | )); 190 | } 191 | Ok(()) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/common/opts.rs: -------------------------------------------------------------------------------- 1 | use cling::prelude::*; 2 | 3 | #[derive(Debug, Collect, Clone, Parser)] 4 | pub struct ServerOpts { 5 | /// Run in server mode 6 | #[clap(short, long, group = "server_or_client")] 7 | pub server: bool, 8 | } 9 | 10 | #[derive(Debug, Collect, Clone, Parser)] 11 | pub struct ClientOpts { 12 | /// Run in client mode, connect to 13 | #[clap(short, long, group = "server_or_client")] 14 | pub client: Option, 15 | /// Length of buffer to read or write, default is 2MiB (2097152) 16 | #[clap(short, long)] 17 | pub length: Option, 18 | /// SO_SNDBUF/SO_RECVBUF for the data streams, uses the system default if unset. 19 | #[clap(long)] 20 | pub socket_buffers: Option, 21 | /// Time in seconds to transmit for 22 | #[clap(short, long, default_value = "10")] 23 | pub time: u64, 24 | /// How many parallel streams used in testing 25 | #[clap(short = 'P', long, default_value = "1")] 26 | pub parallel: u16, 27 | /// Run in bidirectional mode. Client and server send and receive data. 28 | #[clap(long, group = "direction")] 29 | pub bidir: bool, 30 | /// Run in reverse mode (server sends, client receives) 31 | #[clap(short = 'R', long, group = "direction")] 32 | pub reverse: bool, 33 | /// Set TCP no delay, disabling Nagle's Algorithm 34 | #[clap(short = 'N', long)] 35 | pub no_delay: bool, 36 | } 37 | 38 | #[derive(Debug, Collect, Clone, Parser)] 39 | pub struct CommonOpts { 40 | /// Server port to listen on/connect to 41 | #[clap(short, long, default_value = "7559")] 42 | pub port: u16, 43 | /// Seconds between periodic throughput reports 44 | #[clap(short, long, default_value = "1")] 45 | pub interval: u16, 46 | } 47 | 48 | #[derive(Run, Debug, Parser, Clone)] 49 | #[clap( 50 | name = "netperf", 51 | groups = [ 52 | ArgGroup::new("server_or_client").required(true), 53 | ArgGroup::new("direction")]) 54 | ] 55 | #[cling(run = "crate::run")] 56 | /// A network performance measurement tool 57 | pub struct Opts { 58 | #[clap(flatten)] 59 | pub server_opts: ServerOpts, 60 | #[clap(flatten)] 61 | pub client_opts: ClientOpts, 62 | #[clap(flatten)] 63 | pub common_opts: CommonOpts, 64 | #[clap(flatten)] 65 | #[cling(collect)] 66 | pub verbose: clap_verbosity_flag::Verbosity, 67 | } 68 | -------------------------------------------------------------------------------- /src/common/perf_test.rs: -------------------------------------------------------------------------------- 1 | use crate::common::control::{ServerMessage, State}; 2 | use crate::common::data::*; 3 | use crate::common::net_utils::*; 4 | use crate::common::stream_worker::StreamWorkerRef; 5 | use anyhow::{Context, Result}; 6 | use std::collections::HashMap; 7 | use std::sync::Arc; 8 | use std::time::Duration; 9 | use tokio::net::TcpStream; 10 | use tokio::sync::Mutex; 11 | use tokio::time::Instant; 12 | 13 | #[derive(Debug)] 14 | pub struct StreamStats { 15 | pub start: Instant, 16 | pub from: Instant, 17 | pub duration: Duration, 18 | pub bytes_transferred: usize, 19 | } 20 | 21 | /// A master object that holds the state of this perf test run. 22 | pub struct PerfTest { 23 | // The connection address to the client (Only set on clients) 24 | pub client_address: Option, 25 | // A unique identifier for this test run, used by clients to authenticate data streams. 26 | pub cookie: String, 27 | // Represents where we are in the lifecyle of a test. 28 | pub state: Arc>, 29 | // The control socket stream. 30 | pub control_socket: TcpStream, 31 | // Defines whether we are a server or client in this test run. 32 | pub role: Role, 33 | // The test configuration. 34 | pub params: TestParameters, 35 | // The set of data streams 36 | pub streams: HashMap, 37 | // The number of streams at which we are sending data 38 | pub num_send_streams: u16, 39 | // The number of streams at which we are receiving data 40 | pub num_receive_streams: u16, 41 | } 42 | 43 | impl PerfTest { 44 | pub fn new( 45 | client_address: Option, 46 | cookie: String, 47 | control_socket: TcpStream, 48 | role: Role, 49 | params: TestParameters, 50 | ) -> Self { 51 | // Let's assume we are client. 52 | let mut num_send_streams: u16 = 0; 53 | let mut num_receive_streams: u16 = 0; 54 | match params.direction { 55 | Direction::ClientToServer => num_send_streams = params.parallel, 56 | Direction::ServerToClient => num_receive_streams = params.parallel, 57 | Direction::Bidirectional => { 58 | num_send_streams = params.parallel; 59 | num_receive_streams = params.parallel; 60 | } 61 | } 62 | 63 | if matches!(role, Role::Server) { 64 | // swap the streams ;) 65 | std::mem::swap(&mut num_send_streams, &mut num_receive_streams); 66 | } 67 | PerfTest { 68 | client_address, 69 | cookie, 70 | state: Arc::new(Mutex::new(State::TestStart)), 71 | control_socket, 72 | params, 73 | role, 74 | streams: HashMap::new(), 75 | num_send_streams, 76 | num_receive_streams, 77 | } 78 | } 79 | /// Sets the state of the state machine and syncs that to the client through the control socket 80 | pub async fn set_state(&mut self, state: State) -> Result<()> { 81 | if self.role == Role::Server { 82 | // We need to sync that state to the client. 83 | server_send_message( 84 | &mut self.control_socket, 85 | ServerMessage::SetState(state.clone()), 86 | ) 87 | .await 88 | .context("Cannot send the state to the client")?; 89 | } 90 | // We can perform state transition validation here if necessary. 91 | let mut locked_state = self.state.lock().await; 92 | *locked_state = state; 93 | Ok(()) 94 | } 95 | /// Returns the current state of this test 96 | pub async fn get_state_clone(&self) -> State { 97 | self.state.lock().await.clone() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/common/stream_worker.rs: -------------------------------------------------------------------------------- 1 | use crate::common::control::StreamStats; 2 | use crate::common::data::TestParameters; 3 | use crate::common::ui; 4 | use anyhow::{bail, Result}; 5 | use futures::FutureExt; 6 | use log::{debug, warn}; 7 | use std::convert::TryInto; 8 | use std::time::Duration; 9 | use tokio::fs::File; 10 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 11 | use tokio::net::TcpStream; 12 | use tokio::sync::mpsc::{Receiver, Sender}; 13 | // use tokio_stream::StreamExt; 14 | // use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; 15 | use tokio::task::JoinHandle; 16 | use tokio::time::{timeout, Instant}; 17 | 18 | #[derive(Debug, Clone)] 19 | pub enum WorkerMessage { 20 | StartLoad, 21 | Terminate, 22 | } 23 | 24 | /// Represents a connected stream connection. 25 | pub struct StreamWorkerRef { 26 | pub channel: Sender, 27 | pub join_handle: JoinHandle>, 28 | } 29 | 30 | pub struct StreamWorker { 31 | pub id: usize, 32 | pub stream: TcpStream, 33 | pub params: TestParameters, 34 | pub is_sending: bool, 35 | receiver: Receiver, 36 | } 37 | 38 | impl StreamWorker { 39 | pub fn new( 40 | id: usize, 41 | stream: TcpStream, 42 | params: TestParameters, 43 | is_sending: bool, 44 | receiver: Receiver, 45 | ) -> Self { 46 | StreamWorker { 47 | id, 48 | stream, 49 | params, 50 | is_sending, 51 | receiver, 52 | } 53 | } 54 | 55 | pub async fn run_worker(mut self) -> Result { 56 | // Let's pre-allocate a buffer for 1 block; 57 | let block_size = self.params.block_size; 58 | let mut buffer: Vec = vec![0; block_size]; 59 | // Let's fill the buffer with random block if we are a sender 60 | if self.is_sending { 61 | let mut random = File::open("/dev/urandom").await?; 62 | let count = random.read_exact(&mut buffer).await?; 63 | // The urandom buffer should be available to read the exact buffer we want 64 | assert_eq!(count, block_size); 65 | } 66 | 67 | self.configure_stream_socket()?; 68 | // First thing is that we need to wait for the `StartLoad` signal to start sending or 69 | // receiving data. The `StartLoad` signal comes in after the server receives all the 70 | // expected data stream connections as exchanged through the TestParameters. 71 | debug!( 72 | "Data stream {} created ({}), waiting for the StartLoad signal!", 73 | self.id, 74 | if self.is_sending { 75 | "sending" 76 | } else { 77 | "receiving" 78 | } 79 | ); 80 | let signal = self.receiver.recv().await; 81 | if !matches!(signal, Some(WorkerMessage::StartLoad)) { 82 | bail!("Internal communication channel for stream was terminated unexpectedly!"); 83 | } 84 | // TODO: Connect to the cmdline args. 85 | let interval = Duration::from_secs(1); 86 | let start_time = Instant::now(); 87 | let timeout_duration = Duration::from_secs(self.params.time_seconds); 88 | let mut bytes_transferred: usize = 0; 89 | let mut syscalls: usize = 0; 90 | let mut current_interval_start = Instant::now(); 91 | let mut current_interval_bytes_transferred: usize = 0; 92 | let mut current_interval_syscalls: usize = 0; 93 | loop { 94 | // Are we done? 95 | if start_time.elapsed() > timeout_duration { 96 | debug!("Test time is up!"); 97 | break; 98 | } 99 | let internal_message = self.receiver.recv().now_or_never().flatten(); 100 | match internal_message { 101 | Some(WorkerMessage::StartLoad) => { 102 | warn!( 103 | "Unexpected StartLoad from controller, we are already running with load!" 104 | ); 105 | } 106 | Some(WorkerMessage::Terminate) => { 107 | break; 108 | } 109 | None => {} 110 | }; 111 | 112 | // We don't want to be waiting for data forever, if we don't have data, after the 113 | // timeout, let's continue looping 114 | let read_or_write = if self.is_sending { 115 | self.stream.write(&buffer).left_future() 116 | } else { 117 | // Read up-to the remaining bytes from the socket. 118 | self.stream.read(&mut buffer).right_future() 119 | }; 120 | // If we cannot read within 5ms, we skip this loop and try again. This ensures that we 121 | // will still terminate this stream when the total time passes. 122 | if let Ok(bytes_count) = timeout(Duration::from_millis(100), read_or_write).await { 123 | let bytes_count = bytes_count?; 124 | current_interval_bytes_transferred += bytes_count; 125 | bytes_transferred += bytes_count; 126 | if bytes_count > 0 { 127 | syscalls += 1; 128 | current_interval_syscalls += 1; 129 | } else { 130 | // zero means that the connection is terminated. Let's wrap this up. 131 | warn!("Stream {}'s connection has been closed.", self.id); 132 | break; 133 | } 134 | } else { 135 | debug!("Stream [] taking longer than 100ms to produce data..."); 136 | } 137 | // Stats 138 | // Check if we should print stats or not. 139 | let now = Instant::now(); 140 | if now > current_interval_start + interval { 141 | // Collect the stats, print. Then reset the interval. 142 | let current_interval = now - current_interval_start; 143 | ui::print_stats( 144 | Some(self.id), 145 | (current_interval_start - start_time) 146 | .as_millis() 147 | .try_into() 148 | .unwrap(), 149 | current_interval.as_millis().try_into().unwrap(), 150 | current_interval_bytes_transferred, 151 | self.is_sending, 152 | current_interval_syscalls, 153 | block_size, 154 | ); 155 | current_interval_bytes_transferred = 0; 156 | current_interval_syscalls = 0; 157 | current_interval_start = now; 158 | } 159 | } 160 | let duration = Instant::now() - start_time; 161 | let stats = StreamStats { 162 | sender: self.is_sending, 163 | duration_millis: duration.as_millis().try_into().unwrap(), 164 | bytes_transferred, 165 | syscalls, 166 | }; 167 | 168 | // Drain the sockets if we are the receiving end, we need to do that to avoid failing the 169 | // sender stream that might still be sending data. 170 | if !self.is_sending { 171 | while self.stream.read(&mut buffer).await? != 0 {} 172 | } 173 | Ok(stats) 174 | } 175 | 176 | fn configure_stream_socket(&mut self) -> Result<()> { 177 | if self.params.no_delay { 178 | self.stream.set_nodelay(self.params.no_delay)?; 179 | } 180 | // Configure the control socket to use the send and receive buffers. 181 | if let Some(socket_buffers) = self.params.socket_buffers { 182 | let socket_buffers = socket_buffers.try_into().unwrap_or(u32::MAX); 183 | debug!("Setting socket buffers to '{}'", socket_buffers); 184 | // A hack since tokio 1 doesn't support set_send/recv buffers on TcpStream 185 | #[cfg(any(unix, windows))] 186 | unsafe { 187 | #[cfg(unix)] 188 | let sock = { 189 | use std::os::unix::io::{AsRawFd, FromRawFd}; 190 | tokio::net::TcpSocket::from_raw_fd(self.stream.as_raw_fd()) 191 | }; 192 | #[cfg(windows)] 193 | let sock = { 194 | use std::os::windows::io::{AsRawSocket, FromRawSocket}; 195 | tokio::net::TcpSocket::from_raw_socket(self.stream.as_raw_socket()) 196 | }; 197 | 198 | sock.set_recv_buffer_size(socket_buffers) 199 | .unwrap_or_else(|err| warn!("set_recv_buffer_size(), error: {}", err)); 200 | sock.set_send_buffer_size(socket_buffers) 201 | .unwrap_or_else(|err| warn!("set_send_buffer_size(), error: {}", err)); 202 | 203 | std::mem::forget(sock); 204 | } 205 | } 206 | Ok(()) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/common/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::common::control::TestResults; 2 | use crate::common::data::Direction; 3 | use colored::Colorize; 4 | 5 | // Bytes 6 | const KBYTES: usize = 1024; 7 | const MBYTES: usize = 1024 * KBYTES; 8 | const GBYTES: usize = 1024 * MBYTES; 9 | const TBYTES: usize = 1024 * GBYTES; 10 | 11 | // Bitrates 12 | const KBITS: usize = 1000; 13 | const MBITS: usize = 1000 * KBITS; 14 | const GBITS: usize = 1000 * MBITS; 15 | const TBITS: usize = 1000 * GBITS; 16 | 17 | pub fn print_header() { 18 | println!("[ ID] Interval Transfer Bitrate"); 19 | } 20 | pub fn print_server_banner(port: u16) { 21 | println!("--------------------------------------"); 22 | println!("{} {}", "Listening on port".cyan(), port); 23 | println!("--------------------------------------"); 24 | } 25 | 26 | pub fn humanize_bytes(bytes: usize) -> String { 27 | if bytes < KBYTES { 28 | format!("{} B", bytes) 29 | } else if bytes < MBYTES { 30 | format!("{:.2} KiB", bytes as f64 / KBYTES as f64) 31 | } else if bytes < GBYTES { 32 | format!("{:.2} MiB", bytes as f64 / MBYTES as f64) 33 | } else if bytes < TBYTES { 34 | format!("{:.2} GiB", bytes as f64 / GBYTES as f64) 35 | } else { 36 | format!("{:.2} TiB", bytes as f64 / TBYTES as f64) 37 | } 38 | } 39 | 40 | pub fn humanize_bitrate(bytes: usize, duration_millis: u64) -> String { 41 | // For higher accuracy we are getting the actual millis of the duration rather than the 42 | // rounded seconds. 43 | let bits = bytes * 8; 44 | // rate as fraction in seconds; 45 | let rate = (bits as f64 / duration_millis as f64) * 1000f64; 46 | if rate < KBITS as f64 { 47 | format!("{} Bits/sec", rate) 48 | } else if bytes < MBITS { 49 | format!("{:.2} Kbits/sec", rate / KBITS as f64) 50 | } else if bytes < GBITS { 51 | format!("{:.2} Mbits/sec", rate / MBITS as f64) 52 | } else if bytes < TBITS { 53 | format!("{:.2} Gbits/sec", rate / GBITS as f64) 54 | } else { 55 | format!("{:.2} Tbits/sec", rate / TBITS as f64) 56 | } 57 | } 58 | 59 | pub fn print_stats( 60 | id: Option, 61 | offset_from_start_millis: u64, 62 | duration_millis: u64, 63 | bytes_transferred: usize, 64 | sender: bool, 65 | _syscalls: usize, 66 | _block_size: usize, 67 | ) { 68 | let end_point = offset_from_start_millis + duration_millis; 69 | // Calculating the percentage of 70 | println!( 71 | "[{:>3}] {:.2}..{:.2} sec {} {} {}", 72 | id.map(|x| x.to_string()) 73 | .unwrap_or_else(|| "SUM".to_owned()), 74 | offset_from_start_millis as f64 / 1000f64, 75 | end_point as f64 / 1000f64, 76 | humanize_bytes(bytes_transferred), 77 | humanize_bitrate(bytes_transferred, duration_millis), 78 | if sender { 79 | "sender".yellow() 80 | } else { 81 | "receiver".magenta() 82 | }, 83 | ); 84 | } 85 | 86 | pub fn print_summary( 87 | local_results: &TestResults, 88 | remote_results: &TestResults, 89 | direction: &Direction, 90 | ) { 91 | println!("- - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); 92 | print_summary_header(); 93 | // Streams IDs match between server and client (they make pairs of sender/receiver) 94 | // We print each stream sender then receiver. Then the sum of all senders and receivers. 95 | let mut sender_duration_millis = 0; 96 | let mut receiver_duration_millis = 0; 97 | let mut sender_bytes_transferred = 0; 98 | let mut receiver_bytes_transferred = 0; 99 | for (id, local_stats) in &local_results.streams { 100 | print_stats( 101 | Some(*id), 102 | 0, 103 | local_stats.duration_millis, 104 | local_stats.bytes_transferred, 105 | local_stats.sender, 106 | local_stats.syscalls, 107 | 0, 108 | ); 109 | if local_stats.sender { 110 | sender_bytes_transferred += local_stats.bytes_transferred; 111 | sender_duration_millis = 112 | std::cmp::max(sender_duration_millis, local_stats.duration_millis); 113 | } else { 114 | receiver_bytes_transferred += local_stats.bytes_transferred; 115 | receiver_duration_millis = 116 | std::cmp::max(receiver_duration_millis, local_stats.duration_millis); 117 | } 118 | // find the remote counterpart. This is only valuable if we are not using 119 | // bidirectional streams. In bidirectional streams we already have the sender 120 | // and receiving data. 121 | if *direction != Direction::Bidirectional { 122 | if let Some(remote_stats) = remote_results.streams.get(id) { 123 | print_stats( 124 | Some(*id), 125 | 0, 126 | remote_stats.duration_millis, 127 | remote_stats.bytes_transferred, 128 | remote_stats.sender, 129 | remote_stats.syscalls, 130 | 0, 131 | ); 132 | if remote_stats.sender { 133 | sender_bytes_transferred += remote_stats.bytes_transferred; 134 | sender_duration_millis = 135 | std::cmp::max(sender_duration_millis, remote_stats.duration_millis); 136 | } else { 137 | receiver_bytes_transferred += remote_stats.bytes_transferred; 138 | receiver_duration_millis = 139 | std::cmp::max(receiver_duration_millis, remote_stats.duration_millis); 140 | } 141 | } 142 | } 143 | } 144 | // if we have more than one stream, let's print a SUM entry as well. 145 | if local_results.streams.len() > 1 { 146 | println!(); 147 | print_stats( 148 | None, 149 | 0, 150 | sender_duration_millis, 151 | sender_bytes_transferred, 152 | true, 153 | 0, 154 | 0, 155 | ); 156 | print_stats( 157 | None, 158 | 0, 159 | receiver_duration_millis, 160 | receiver_bytes_transferred, 161 | false, 162 | 0, 163 | 0, 164 | ); 165 | } 166 | } 167 | 168 | fn print_summary_header() { 169 | println!( 170 | "{} {} {} {}", 171 | "ID".bold(), 172 | "Interval".bold(), 173 | "Transfer".bold(), 174 | "Bitrate".bold() 175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /src/controller.rs: -------------------------------------------------------------------------------- 1 | use crate::common::consts::INTERNAL_PROT_BUFFER; 2 | use crate::common::control::{ClientMessage, ServerMessage, State, StreamStats, TestResults}; 3 | use crate::common::data::{Direction, Role}; 4 | use crate::common::net_utils::*; 5 | use crate::common::perf_test::PerfTest; 6 | use crate::common::stream_worker::{StreamWorker, StreamWorkerRef, WorkerMessage}; 7 | use crate::common::ui; 8 | use anyhow::{anyhow, Context, Result}; 9 | use log::{debug, error, warn}; 10 | use std::collections::HashMap; 11 | use tokio::net::TcpStream; 12 | use tokio::select; 13 | use tokio::sync::mpsc::{self, Receiver, Sender}; 14 | use tokio::time::{timeout, Duration}; 15 | 16 | #[derive(Debug)] 17 | pub enum ControllerMessage { 18 | // Asks the controller to create a new stream using this socket. 19 | CreateStream(TcpStream), 20 | StreamTerminated(usize), 21 | } 22 | 23 | pub struct TestController { 24 | pub sender: Sender, 25 | test: PerfTest, 26 | receiver: Receiver, 27 | stream_results: HashMap, 28 | } 29 | 30 | impl TestController { 31 | pub fn new(test: PerfTest) -> Self { 32 | let (sender, receiver) = mpsc::channel(INTERNAL_PROT_BUFFER); 33 | TestController { 34 | test, 35 | sender, 36 | receiver, 37 | stream_results: HashMap::new(), 38 | } 39 | } 40 | 41 | /// This is the test controller code, when this function terminates, the test is done. 42 | pub async fn run_controller(mut self) -> Result<()> { 43 | debug!("Controller has started"); 44 | let mut old_state = self.test.state.clone().lock().await.clone(); 45 | loop { 46 | // We need to reason about our state and decide on the next step here before 47 | // processing messages. 48 | let state = self.test.state.clone().lock().await.clone(); 49 | debug!("Controller state: {:?}", state); 50 | let role = &self.test.role; 51 | // TODO: We don't want to lock a mutex on every iteration, this is insane, yet 52 | // it's not a big deal since this is only for the low-frequency control socket. 53 | match state { 54 | State::TestStart if matches!(role, Role::Server) => { 55 | // We are a server, we just started the test. 56 | debug!("Test is initialising"); 57 | self.test 58 | .set_state(State::CreateStreams { 59 | cookie: self.test.cookie.clone(), 60 | }) 61 | .await?; 62 | continue; 63 | } 64 | State::CreateStreams { cookie: _ } if matches!(role, Role::Server) => { 65 | // We are a server, we are waiting for streams to be created. 66 | debug!("Waiting for data streams to be connected"); 67 | } 68 | State::CreateStreams { cookie: _ } if matches!(role, Role::Client) => { 69 | // We are a client, we are being asked to create streams. 70 | debug!("We should create data streams now"); 71 | self.create_streams().await?; 72 | } 73 | // Only process this on the transition. Start the load testing 74 | State::Running if old_state != State::Running => { 75 | debug!("Streams have been created, starting the load test"); 76 | // Send the StartLoad signal to all streams. 77 | ui::print_header(); 78 | self.broadcast_to_streams(WorkerMessage::StartLoad); 79 | } 80 | // We are asked to exchange the test results we have. 81 | State::ExchangeResults => { 82 | // Do we have active streams yet? We should ask these to terminate and wait 83 | // This is best-effort. 84 | self.broadcast_to_streams(WorkerMessage::Terminate); 85 | let local_results = self.collect_test_result().await?; 86 | let remote_results = self.exchange_results(local_results.clone()).await?; 87 | self.print_results(local_results, remote_results); 88 | break; 89 | } 90 | _ => {} 91 | } 92 | // Set the old_state as the new state now since we processed the transition already. 93 | old_state = state; 94 | if self.test.role == Role::Server { 95 | // We are a SERVER 96 | let message = self.receiver.recv().await; 97 | self.process_internal_message(message).await?; 98 | } else { 99 | // We are a CLIENT 100 | let internal_message = self.receiver.recv(); 101 | let server_message = client_read_message(&mut self.test.control_socket); 102 | select! { 103 | message = internal_message => self.process_internal_message(message).await? , 104 | message = server_message => self.process_server_message(message).await?, 105 | else => break, 106 | } 107 | } 108 | } 109 | println!("netperf Done!"); 110 | Ok(()) 111 | } 112 | 113 | fn print_results(&self, local: TestResults, remote: TestResults) { 114 | ui::print_summary(&local, &remote, &self.test.params.direction); 115 | } 116 | 117 | async fn collect_test_result(&mut self) -> Result { 118 | // Join all streams and collect results. 119 | for (id, worker_ref) in self.test.streams.drain() { 120 | debug!("Waiting on stream {} to terminate", id); 121 | match timeout(Duration::from_secs(5), worker_ref.join_handle).await { 122 | Err(_) => warn!( 123 | "Timeout waiting on stream {} to terminate, ignoring it.", 124 | id 125 | ), 126 | Ok(Ok(Ok(result))) => { 127 | self.stream_results.insert(id, result); 128 | debug!("Stream {} joined.", id); 129 | } 130 | Ok(Ok(Err(e))) => { 131 | warn!( 132 | "Stream {} terminated with error ({}) ignoring its results!", 133 | id, e 134 | ); 135 | } 136 | Ok(Err(e)) => warn!("Failed to join stream {}: {}", id, e), 137 | } 138 | } 139 | let results = TestResults { 140 | streams: self.stream_results.clone(), 141 | }; 142 | Ok(results) 143 | } 144 | 145 | /// Exchanges the test results with the other party returning the other party's result. 146 | async fn exchange_results(&mut self, local_results: TestResults) -> Result { 147 | if self.test.role == Role::Client { 148 | // Send ours then read the server's 149 | client_send_message( 150 | &mut self.test.control_socket, 151 | ClientMessage::SendResults(local_results), 152 | ) 153 | .await?; 154 | match client_read_message(&mut self.test.control_socket).await? { 155 | ServerMessage::SendResults(results) => Ok(results), 156 | e => Err(anyhow!( 157 | "Invalid protocol message was sent from the server: {:?}", 158 | e 159 | )), 160 | } 161 | } else { 162 | // On the server side, we read the client's results first. 163 | let remote_result = match server_read_message(&mut self.test.control_socket).await? { 164 | ClientMessage::SendResults(results) => Ok(results), 165 | e => Err(anyhow!( 166 | "Invalid protocol message was sent from the server: {:?}", 167 | e 168 | )), 169 | }; 170 | // Send ours then read the server's 171 | server_send_message( 172 | &mut self.test.control_socket, 173 | ServerMessage::SendResults(local_results), 174 | ) 175 | .await?; 176 | remote_result 177 | } 178 | } 179 | 180 | fn broadcast_to_streams(&mut self, message: WorkerMessage) { 181 | debug!("Broadcasting to streams {:?}", message); 182 | for (id, worker_ref) in &mut self.test.streams { 183 | worker_ref 184 | .channel 185 | .try_send(message.clone()) 186 | .unwrap_or_else(|e| { 187 | error!( 188 | "Failed to terminate stream {}, it might have been terminated already: {}", 189 | id, e 190 | ) 191 | }); 192 | } 193 | } 194 | 195 | /// A handler when we receive a ControllerMessage from other components. 196 | async fn process_internal_message( 197 | &mut self, 198 | message: Option, 199 | ) -> Result<(), anyhow::Error> { 200 | let message = message.unwrap(); 201 | match message { 202 | ControllerMessage::CreateStream(stream) => self.accept_stream(stream).await?, 203 | ControllerMessage::StreamTerminated(id) => { 204 | let handle = self.test.streams.remove(&id); 205 | if let Some(worker_ref) = handle { 206 | // join the task to retrieve the result. 207 | let result = worker_ref.join_handle.await.with_context(|| { 208 | format!("Couldn't join an internal task for stream: {}", id) 209 | })?; 210 | if let Ok(result) = result { 211 | self.stream_results.insert(id, result); 212 | } else { 213 | warn!( 214 | "Stream {} has terminated with an error, we cannot fetch its total \ 215 | stream stats. This means that the results might be partial", 216 | id 217 | ); 218 | } 219 | } 220 | if self.test.streams.is_empty() && self.test.role == Role::Server { 221 | // No more active streams, let's exchange results and finalise test. 222 | let _ = self.test.set_state(State::ExchangeResults).await; 223 | } 224 | } 225 | } 226 | Ok(()) 227 | } 228 | 229 | async fn process_server_message( 230 | &mut self, 231 | message: Result, 232 | ) -> Result<(), anyhow::Error> { 233 | let message = message.with_context(|| "Server terminated!")?; 234 | match message { 235 | ServerMessage::SetState(state) => { 236 | // Update our client state, ignore/panic the error on purpose. The error should 237 | // never happen on the client side. 238 | self.test.set_state(state).await?; 239 | } 240 | e => println!("Received an unexpected message from the server {:?}", e), 241 | } 242 | Ok(()) 243 | } 244 | 245 | // Executed only on the server 246 | async fn accept_stream(&mut self, stream: TcpStream) -> Result<()> { 247 | assert_eq!(self.test.role, Role::Server); 248 | let total_needed_streams: usize = 249 | (self.test.num_send_streams + self.test.num_receive_streams) as usize; 250 | self.create_and_register_stream_worker(stream); 251 | if self.test.streams.len() == total_needed_streams { 252 | // We have all streams ready, switch state and start load. 253 | self.test.set_state(State::Running).await?; 254 | } 255 | Ok(()) 256 | } 257 | 258 | /// Creates a connection to the server that serves as a data stream to test the network. 259 | /// This method initialises the connection and performs the authentication as well. 260 | async fn connect_data_stream(&self) -> Result { 261 | let address = self.test.client_address.clone().unwrap(); 262 | debug!("Opening a data stream to ({}) ...", address); 263 | let mut stream = TcpStream::connect(address).await?; 264 | // Send the hello and authenticate. 265 | client_send_message( 266 | &mut stream, 267 | ClientMessage::Hello { 268 | cookie: self.test.cookie.clone(), 269 | }, 270 | ) 271 | .await?; 272 | // Wait for the initial Welcome message. If the server is busy, this will 273 | // return an Error of AccessDenied and the client will terminate. 274 | let _: ServerMessage = client_read_message(&mut stream).await?; 275 | debug!("Data stream created to {}", peer_to_string(&stream)); 276 | Ok(stream) 277 | } 278 | 279 | fn create_and_register_stream_worker(&mut self, stream: TcpStream) { 280 | // The first (num_send_streams) are sending (meaning that we `Client` are the sending 281 | // end of this stream) 282 | let streams_created = self.test.streams.len(); 283 | let mut is_sending = streams_created < self.test.num_send_streams as usize; 284 | // The case for bidirectional is not that straight forward. 285 | // This is the only case where we have both sending and receiving streams, in this scenario 286 | // we need to flip the flag on the server. To explain: 287 | // 288 | // Client creates 1 sending, 1 recieving streams. The client will connect them in that order. 289 | // The server need to first accept the first one, in this case we want to accept it as 290 | // receiving. 291 | // As we exhaust the receiving, we will create the sending streams. 292 | // Hence this flip trick! 293 | if self.test.role == Role::Server && self.test.params.direction == Direction::Bidirectional 294 | { 295 | // Flip! 296 | is_sending = !is_sending; 297 | } 298 | let id = streams_created; 299 | let (sender, receiver) = mpsc::channel(INTERNAL_PROT_BUFFER); 300 | let worker = StreamWorker::new(id, stream, self.test.params.clone(), is_sending, receiver); 301 | let controller = self.sender.clone(); 302 | let handle = tokio::spawn(async move { 303 | let result = worker.run_worker().await; 304 | controller 305 | .try_send(ControllerMessage::StreamTerminated(id)) 306 | .unwrap_or_else(|e| debug!("Failed to communicate with controller: {}", e)); 307 | result 308 | }); 309 | let worker_ref = StreamWorkerRef { 310 | channel: sender, 311 | join_handle: handle, 312 | }; 313 | self.test.streams.insert(id, worker_ref); 314 | } 315 | 316 | /// Executed only on the client. Establishes N connections to the server according to the 317 | /// exchanged `TestParameters` 318 | async fn create_streams(&mut self) -> Result<()> { 319 | assert_eq!(self.test.role, Role::Client); 320 | // Let's connect the send streams first. We will connect and authenticate streams 321 | // sequentially for simplicity. The server expects all the (client-to-server) streams 322 | // to be created first. That's an implicit assumption as part of the protocol. 323 | let total_needed_streams: usize = 324 | (self.test.num_send_streams + self.test.num_receive_streams) as usize; 325 | 326 | while self.test.streams.len() < total_needed_streams { 327 | let stream = self.connect_data_stream().await?; 328 | self.create_and_register_stream_worker(stream); 329 | } 330 | Ok(()) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | 3 | pub mod client; 4 | pub mod common; 5 | pub mod controller; 6 | pub mod server; 7 | 8 | use crate::common::opts::{ClientOpts, CommonOpts, ServerOpts}; 9 | use cling::prelude::*; 10 | 11 | #[cling_handler] 12 | async fn run( 13 | Collected(verbosity): Collected, 14 | common_opts: &CommonOpts, 15 | server_opts: &ServerOpts, 16 | client_opts: &ClientOpts, 17 | ) -> Result<(), anyhow::Error> { 18 | // Setting the log-level for (all modules) from the verbosity argument. 19 | // -v WARN, -vv INFO, -vvv DEBUG, etc. 20 | let filter = verbosity.log_level_filter(); 21 | env_logger::builder().filter_level(filter).init(); 22 | info!("Log Level={}", filter.to_level().unwrap()); 23 | 24 | if server_opts.server { 25 | // Server mode... Blocking until we terminate. 26 | crate::server::run_server(common_opts).await 27 | } else { 28 | // Client mode... 29 | crate::client::run_client(common_opts, client_opts).await 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use cling::prelude::*; 2 | use netperf::common::opts::Opts; 3 | 4 | #[tokio::main] 5 | async fn main() -> ClingFinished { 6 | Cling::parse_and_run().await 7 | } 8 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::common::consts; 2 | use crate::common::control::*; 3 | use crate::common::data::{Role, TestParameters}; 4 | use crate::common::net_utils::*; 5 | use crate::common::opts::CommonOpts; 6 | use crate::common::perf_test::PerfTest; 7 | use crate::common::*; 8 | use crate::controller::{ControllerMessage, TestController}; 9 | use anyhow::{anyhow, Context, Result}; 10 | use log::{debug, error, info}; 11 | use std::net::Ipv6Addr; 12 | use std::sync::{Arc, Weak}; 13 | use tokio::net::{TcpListener, TcpStream}; 14 | use tokio::sync::mpsc::Sender; 15 | use tokio::sync::Mutex; 16 | use tokio::time::timeout; 17 | 18 | pub async fn run_server(common_opts: &CommonOpts) -> Result<()> { 19 | // We use IPv6Addr::UNSPECIFIED here to listen on all 20 | // IPv4 and IPv6 local interfaces. (dual stack) 21 | let listener = TcpListener::bind((Ipv6Addr::UNSPECIFIED, common_opts.port)).await?; 22 | let port = common_opts.port; 23 | ui::print_server_banner(port); 24 | // Handles a single test instance 25 | // 26 | // Note that in netperf, we don't run tests concurrently. 27 | // We will not be accepting more connections unless the test in-flight finishes execution. 28 | let mut in_flight_test_state: Weak> = Weak::new(); 29 | // Initially we will have a None sender until we have a test running in-flight. 30 | let mut controller_channel: Option> = None; 31 | while let Ok((mut inbound, _)) = listener.accept().await { 32 | let peer = peer_to_string(&inbound); 33 | 34 | // Do we have a test in-flight? 35 | match in_flight_test_state.upgrade() { 36 | Some(state_lock) => { 37 | if let State::CreateStreams { ref cookie } = *state_lock.lock().await { 38 | // Validate the cookie in this case. 39 | // Read the cookie from the Hello message and compare to cookie. 40 | let client_cookie = read_cookie(&mut inbound).await?; 41 | // Authentication 42 | if client_cookie == *cookie { 43 | // Create the stream. 44 | server_send_message(&mut inbound, ServerMessage::Welcome).await?; 45 | let controller = controller_channel.clone().unwrap(); 46 | controller.try_send(ControllerMessage::CreateStream(inbound))?; 47 | } else { 48 | let _ = server_send_error( 49 | &mut inbound, 50 | ServerError::AccessDenied("Test already in-flight".to_owned()), 51 | ) 52 | .await; 53 | } 54 | } else { 55 | // We already have a test in-flight, close the connection immediately. 56 | // Note that here we don't read anything from the socket to avoid 57 | // unnecessarily being blocked on the client not sending any data. 58 | info!("Test already in-flight, rejecting connection from {}", peer); 59 | let _ = server_send_error( 60 | &mut inbound, 61 | ServerError::AccessDenied("Test already in-flight".to_owned()), 62 | ) 63 | .await; 64 | } 65 | } 66 | None => { 67 | // No in-flight test, let's create one. 68 | info!("Accepted connection from {}", peer); 69 | // Do we have an active test running already? 70 | // If not, let's start a test session and wait for parameters from the client 71 | match create_test(inbound).await { 72 | Ok(test) => { 73 | info!("[{}] Test Created", peer); 74 | in_flight_test_state = Arc::downgrade(&test.state); 75 | // Keep a weak-ref to this test here. 76 | // Async dispatch. 77 | let controller = TestController::new(test); 78 | controller_channel = Some(controller.sender.clone()); 79 | tokio::spawn(async move { 80 | if let Err(e) = controller.run_controller().await { 81 | debug!("Controller aborted: {}", e); 82 | println!("Test aborted!"); 83 | } 84 | ui::print_server_banner(port); 85 | }); 86 | } 87 | Err(e) => { 88 | error!("[{}] {}", peer, e); 89 | } 90 | }; 91 | } 92 | }; 93 | } 94 | Ok(()) 95 | } 96 | 97 | async fn create_test(mut control_socket: TcpStream) -> Result { 98 | // It's important that we finish this initial negotiation quickly, we are setting 99 | // up a race between these reads and a timeout timer (5 seconds) for each read to 100 | // ensure we don't end up waiting forever and not accepting new potential tests. 101 | 102 | let cookie = read_cookie(&mut control_socket).await?; 103 | debug!("Hello received: {}", cookie); 104 | // Sending the WELCOME message first since this is an accepted attempt. 105 | server_send_message(&mut control_socket, ServerMessage::Welcome).await?; 106 | // Reading the test parameters length 107 | let params = timeout( 108 | consts::PROTOCOL_TIMEOUT, 109 | read_test_parameters(&mut control_socket), 110 | ) 111 | .await 112 | .context("Timed out waiting for the protocol negotiation!")??; 113 | Ok(PerfTest::new( 114 | None, // No client_address, we are a server. [Not needed] 115 | cookie, 116 | control_socket, 117 | Role::Server, 118 | params, 119 | )) 120 | } 121 | 122 | async fn read_test_parameters(stream: &mut TcpStream) -> Result { 123 | // Since we don't know the block size yet, 124 | // we will need to assume the message length size. 125 | match server_read_message(stream).await? { 126 | ClientMessage::SendParameters(params) => Ok(params), 127 | e => Err(anyhow!( 128 | "Unexpected message, we expect SendParameters, instead we got {:?}", 129 | e 130 | )), 131 | } 132 | } 133 | 134 | async fn read_cookie(socket: &mut TcpStream) -> Result { 135 | match server_read_message(socket).await { 136 | Ok(ClientMessage::Hello { cookie }) => Ok(cookie), 137 | Ok(e) => Err(anyhow!( 138 | "Client sent the wrong welcome message, got: {:?}", 139 | e 140 | )), 141 | Err(e) => Err(anyhow!("Failed to finish initial negotiation: {:?}", e)), 142 | } 143 | } 144 | --------------------------------------------------------------------------------