Full Code of hatoo/oha for AI

master 339ec3255827 cached
38 files
365.1 KB
82.6k tokens
294 symbols
1 requests
Download .txt
Showing preview only (379K chars total). Download the full file or copy to clipboard to get everything.
Repository: hatoo/oha
Branch: master
Commit: 339ec3255827
Files: 38
Total size: 365.1 KB

Directory structure:
gitextract_llqd8vur/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── CI.yml
│       ├── release-pgo.yml
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── Cross.toml
├── Dockerfile
├── LICENSE
├── README.md
├── pgo/
│   └── server/
│       ├── Cargo.toml
│       └── src/
│           └── main.rs
├── pgo.js
├── schema.json
├── src/
│   ├── aws_auth.rs
│   ├── cli.rs
│   ├── client.rs
│   ├── client_h3.rs
│   ├── curl_compat.rs
│   ├── db.rs
│   ├── histogram.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── monitor.rs
│   ├── pcg64si.rs
│   ├── printer.rs
│   ├── request_generator.rs
│   ├── result_data.rs
│   ├── timescale.rs
│   ├── tls_config.rs
│   └── url_generator.rs
└── tests/
    ├── common/
    │   ├── mod.rs
    │   ├── server.cert
    │   └── server.key
    └── tests.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
*

!pgo
!src
!Cargo.*


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: hatoo # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: hatoo # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference

version: 2
updates:
  - package-ecosystem: "cargo" # See documentation for possible values
    directories: # Locations of package manifests
      - "/"
      - "pgo/server"
    schedule:
      interval: "weekly"
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/CI.yml
================================================
on:
  push:
    branches:
      - master
  pull_request:

name: CI

jobs:
  test:
    name: Test Suite
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest, macos-14]
        additional_args: ["", "--no-default-features --features native-tls", "--features http3"]
        # vsock feature is only on linux
        include:
          - os: ["ubuntu-latest"]
            additional_args: "--features vsock"
    steps:
      # We need nasm to build aws-lc on windows
      - uses: ilammy/setup-nasm@v1
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test ${{ matrix.additional_args }}

  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - name: Enforce formatting
        run: cargo fmt --all --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - name: Linting
        run: cargo clippy --all-targets --all-features -- -D warnings


================================================
FILE: .github/workflows/release-pgo.yml
================================================
name: Publish PGO

on:
  push:
    branches:
      - master
    tags:
      - "v*.*.*"
  pull_request:

jobs:
  publish:
    name: Publish for ${{ matrix.target }} PGO
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            artifact_name: oha
            release_name: oha-linux-amd64-pgo
            target: x86_64-unknown-linux-musl
            additional_args: "--features vsock"
          # tikv-jemalloc-sys@0.6.0+5.3.0-1: background_threads_runtime_support not supported for aarch64-unknown-linux-musl
#          - os: ubuntu-24.04-arm
#            artifact_name: oha
#            release_name: oha-linux-arm64-pgo
#            target: aarch64-unknown-linux-musl
#            additional_args: "--features vsock"
          - os: windows-latest
            artifact_name: oha.exe
            release_name: oha-windows-amd64-pgo.exe
            target: x86_64-pc-windows-msvc
            additional_args: ""
#          - os: macos-14-large
#            artifact_name: oha
#            release_name: oha-macos-amd64-pgo
#            target: x86_64-apple-darwin
#            additional_args: ""
#          - os: macos-14
#            artifact_name: oha
#            release_name: oha-macos-arm64-pgo
#            target: aarch64-apple-darwin
#            additional_args: ""

    steps:
      - uses: ilammy/setup-nasm@v1
      - uses: actions/checkout@v6
      - name: Install musl-tools on Linux
        run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
        if: contains(matrix.target, 'musl')
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
          components: llvm-tools-preview
      - uses: Swatinem/rust-cache@v2
      - run: cargo install cargo-pgo --version 0.2.9
      - uses: oven-sh/setup-bun@v2
      - run: bun run pgo.js --target ${{ matrix.target }} ${{ matrix.additional_args }}
      - uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.release_name }}
          path: target/${{ matrix.target }}/pgo/${{ matrix.artifact_name }}
      - name: Upload binaries to release
        uses: svenstaro/upload-release-action@v2
        if: startsWith(github.ref, 'refs/tags/v')
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          file: target/${{ matrix.target }}/pgo/${{ matrix.artifact_name }}
          asset_name: ${{ matrix.release_name }}


================================================
FILE: .github/workflows/release.yml
================================================
name: Publish

on:
  push:
    branches:
      - master
    tags:
      - "v*.*.*"
  pull_request:

jobs:
  publish:
    name: Publish for ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            artifact_name: oha
            release_name: oha-linux-amd64
            target: x86_64-unknown-linux-musl
            additional_args: "--features vsock"
            use_cross: false
          - os: windows-latest
            artifact_name: oha.exe
            release_name: oha-windows-amd64.exe
            target: x86_64-pc-windows-msvc
            additional_args: ""
            use_cross: false
          - os: macos-latest
            artifact_name: oha
            release_name: oha-macos-amd64
            target: x86_64-apple-darwin
            additional_args: ""
            use_cross: false
          - os: ubuntu-latest
            artifact_name: oha
            release_name: oha-linux-arm64
            target: aarch64-unknown-linux-musl
            additional_args: "--features vsock"
            use_cross: true
          - os: macos-14
            artifact_name: oha
            release_name: oha-macos-arm64
            target: aarch64-apple-darwin
            additional_args: ""
            use_cross: false
    env:
      BUILD_CMD: cargo
    steps:
      - uses: ilammy/setup-nasm@v1
      - uses: actions/checkout@v6
      - name: Install musl-tools on Linux
        run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
        if: contains(matrix.target, 'musl')
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: Install cross
        if: matrix.use_cross
        uses: taiki-e/install-action@v2
        with:
          tool: cross
      - uses: Swatinem/rust-cache@v2
      - name: Overwrite build command env variable
        if: matrix.use_cross
        shell: bash
        run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
      - name: Build
        shell: bash
        run: $BUILD_CMD build --profile release-ci --target ${{ matrix.target }} --locked --no-default-features --features rustls ${{ matrix.additional_args }}
      - name: Upload
        uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.release_name }}
          path: target/${{ matrix.target }}/release-ci/${{ matrix.artifact_name }}
      - name: Upload binaries to release
        if: startsWith(github.ref, 'refs/tags/v')
        uses: svenstaro/upload-release-action@v2
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          file: target/${{ matrix.target }}/release-ci/${{ matrix.artifact_name }}
          asset_name: ${{ matrix.release_name }}
          tag: ${{ github.ref }}

  docker:
    name: Build and Push Docker Image
    runs-on: ubuntu-latest
    needs: publish
    if: startsWith(github.ref, 'refs/tags/v')
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v6

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        if: startsWith(github.ref, 'refs/tags/v')
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ startsWith(github.ref, 'refs/tags/v') }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max


================================================
FILE: .gitignore
================================================
**/target
/.idea
/.vscode


================================================
FILE: CHANGELOG.md
================================================
# Unreleased

# 1.14.0

- fix Possible bug with latency_correction #857

# 1.13.0 (2025-02-07)

- Add first byte stats to JSON output#844
- On http3, bind ipv6 for ipv6#839
- Prefer ipv6 for localhost on macos#837

# 1.12.1 (2025-11-29)

- feat: add official docker img with better caching #830 

# 1.12.0 (2025-11-29)

- Fix dns and connection time stats #816
- Add colors to --help/-h #822 
- rename --disable-color to --no-color #824 
- fixed --urls-from-file reuses host with path from different url #825

# 1.11.0 (2025-11-01)

- feat:support decimal when using n_requests,such as 2.6k or 0.001m #808 
- connect-to now doing tcp connect instead of dns caching #807 
- feat: make connect timeout configurable #805 
- feat: support k/m suffixes for n-requests #800 
- feat: Add option to read request body line-by-line from file. #729 #789

# 1.10.0 (2025-09-06)

- fix dns lookup on http2/http3 #771 
- feat: add curl-compatible multipart form data support (-F option) #755

# 1.9.0 (2025-06-21)

- Implement experimental HTTP3 support #746 
- Allow appending to database if oha table has already been created #742 
- Add -u/--time-unit option #741 
- Add RequestResult.first_byte field for measuring first body byte latency #727 
- Add support for results in csv format #725 
- Add support for fractional QPS values #724 

# 1.8.0 (2025-02-15)

- Support mtls #687 
- Support Proxy headers #688 
- Randomize --connect-to if multiple matching options #695 

# 1.7.0 (2025-02-01)

- Impl support for calling AWS APIs with sigv4 #666 
- support -o #669 

# 1.6.0 (2025-01-11)

- Feature: Reading Urls from file #639
- Add some optimized workers to `--no-tui` mode #646

# 1.5.0 (2024-12-07)

- Add `--debug` option to check actual request/response
- Switch colors to justified latency thresholds (fixes #609) #610 
- Fix Running with -q hangs #603 
- Support HTTP proxy #614 

# 1.4.7 (2024-10-26)

- [rustls] Cache HTTPS certs

# 1.4.6 (2024-08-17)

- Add `--wait-ongoing-requests-after-deadline` option
- Add `--db-url` option to save results to SQLite database
- Add `--dump-urls` option to debug random URL generation

# 1.4.5 (2024-05-29)

- Some performance improvements

# 1.4.4 (2024-04-20)

- support Termux #464

# 1.4.3 (2024-04-06)

- fix rustls error #452

# 1.4.2 (2024-04-06)

- Fix printing of Size/request #447 

# 1.4.1 (2024-03-16)

- Enable: Profile-Guided Optimization (PGO) #268

# 1.4.0 (2024-03-09)

- No DNS lookup when unix socket or vsock #418
- Add HTTP over VSOCK support #416

# 1.3.0 (2024-02-04)

- Optimize timeout #403 
- Compact error #402 
- fix tui layout #401 

# 1.2.0 (2024-02-03)

- Print help message when no argument is given #378
- Lookup DNS at beginning and cache it #391
- Report deadlined requests #392 
- Fix MacOS Crash issues #384

# 1.1.0 (2024-01-16)

-  [HTTP2] Reconnect TCP connection when it fails #369 

# 1.0.0 (2023-11-16)

- Update hyper dependency to 1.0.0

# 1.0.0-rc.4.a8dcd7ca5df49c0701893c4d9d81ec8c1342f141 (2023-10-14)

This is a RC release for 1.0.0. Please test it and report any issues.
The version is named as same as `hyper`'s version and it's commit hash.

Since this version depends on unreleased `hyper`'s version, we can't release on crates.io. Only on binary releases.

- Support HTTP/2 #224 #201
- Make `rustls` as a default TLS backend #331
- Added `-p` option to set number of HTTP/2 parallel requests

# 0.6.5 (2023-10-09)

- Fix Apple Silicon's binary release #323

# 0.6.4 (2023-09-24)

- Fix -H option to overwrite default value #309
- feat: display 99.90- and 99.99-percentile latency #315 

# 0.6.3 (2023-09-05)

- Add style and colors to the summary view #64
- Added a stats-success-breakdown flag for more detailed status code specific response statistics #212

# 0.6.2 (2023-08-12)

- Support Burst feature #276

# 0.6.1 (2023-07-12)

- Fix sending HTTP uri #255
- Add default user agent header #257

# 0.6.0 (2023-06-24)

- Support IDNA #236
- Support randomly generated URL using rand_regex crate

# 0.5.9 (2023-06-12)

- Fix -H Header parser
-  Update printer #229
    -  Use percentage for Success rate summary value #228 
    - Latency distribution -> Response time distribution

# 0.5.8 (2023-03-25)

- Add `--unix-socket` on `unix` profiles for HTTP. #220
- Fix tui to not requiring True Color. #209

# 0.5.7 (2023-02-25)

- Fix `--latency-correction` adds the time of DNS. #211
- Fix `-z` behaviour to cancel workers at the dead line. #211
- Fix align of histogram #210

# 0.5.6 (2023-02-02)

- Update `clap` to version 4
- Release `musl` binaries #206
- Support [Ipv6] format requested_host in --connect-to #197

# 0.5.5 (2022-09-19)

- Add colors to the tui view #64

# 0.5.4 (2022-08-27)

- Support Ipv6 host #181
- Print min, max, average and pXX for Requests per second in JSON output like bombardier #177
- Add JSON Output #169
- Fix QPS control to send with correct rate for first 1 sec
- Make histogram compatible to hey
    - closes #161

# 0.5.3 (2022-07-16)

- Add support for bracketed IPv6 syntax in connect-to

# 0.5.2 (2022-04-28)

- Add `rustls` feature flag to build against `rustls` instead of `native-tls`.

# 0.5.1 (2022-03-29)

- Fix histogram to show correct response time
    - closes #157

# 0.5.0 (2022-01-01)

- Use clap 3.0.0 instead of structopt
    - closes #131

# 0.4.6 (2021-07-05)

- Add `--latency-correction` to avoid Coordinated Omission Problem.

# 0.4.5 (2021-05-04)

- Set '--no-tui' automatically when stdout isn't TTY

# 0.4.4 (2020-11-18)

- Bump `resolv-conf` to support `options edns0 trust-ad` on `/etc/resolv.conf`

# 0.4.3 (2020-11-12)

- Add --connect-to option to override DNS for a given host+port, similar to curl

# 0.4.2 (2020-10-06)

- Speed up on WSL Ubuntu 20.4

# 0.4.1 (2020-07-28)

- Support -q 0 option for unlimited qps
- Fix performance on limiting query/second


================================================
FILE: Cargo.toml
================================================
[package]
authors = ["hatoo <hato2000@gmail.com>"]
categories = [
    "command-line-utilities",
    "network-programming",
    "web-programming::http-client",
    "development-tools::profiling",
]
description = "Ohayou(おはよう), HTTP load generator, inspired by rakyll/hey with tui animation."
edition = "2024"
keywords = ["cli", "load-testing", "performance", "http"]
license = "MIT"
name = "oha"
readme = "README.md"
repository = "https://github.com/hatoo/oha"
version = "1.14.0"
rust-version = "1.87"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["rustls"]
native-tls = ["dep:native-tls", "dep:tokio-native-tls"]
rustls = [
    "dep:rustls",
    "dep:tokio-rustls",
    "dep:rustls-native-certs",
    "dep:rustls-pki-types",
]
vsock = ["dep:tokio-vsock"]
http3 = ["dep:h3", "dep:h3-quinn", "dep:quinn-proto", "dep:quinn", "dep:http"]

[dependencies]
anyhow = "1.0.86"
average = "0.16.0"
byte-unit = "5.1.4"
clap = { version = "4.5.9", features = ["derive", "env"] }
clap_complete = "4"
float-ord = "0.3.2"
kanal = "0.1.1"
humantime = "2.1.0"
libc = "0.2.155"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0.12"
tokio = { version = "1.38.1", features = ["full"] }
ratatui = { version = "0.30.0", default-features = false, features = [
    "crossterm",
] }
aws-sign-v4 = "0.3"
chrono = "0.4"
bytes = "1"

hyper = { version = "1.4", features = ["client", "http1", "http2"] }

# native-tls
native-tls = { version = "0.2.12", features = ["alpn"], optional = true }
tokio-native-tls = { version = "0.3.1", optional = true }

rustls = { version = "0.23.18", optional = true }
rustls-native-certs = { version = "0.8.0", optional = true }
tokio-rustls = { version = "0.26.0", optional = true }
rustls-pki-types = { version = "1.7.0", optional = true }

h3 = { version = "0.0.8", optional = true }
h3-quinn = { version = "0.0.10", optional = true }
quinn-proto = { version = "0.11.10", optional = true, features = ["aws-lc-rs"] }
http = { version = "1.4.0", optional = true }
quinn = { version = "0.11.7", optional = true, features = [
    "aws-lc-rs",
    "runtime-tokio",
] }

base64 = "0.22.1"
rand = "0.10.0"
hickory-resolver = { version = "0.25.2", features = ["tokio"] }
rand_regex = "0.19.0"
regex-syntax = "0.8.5"
url = "2.5.2"
http-body-util = "0.1.2"
hyper-util = { version = "0.1.6", features = ["tokio"] }
tokio-vsock = { version = "0.7.2", optional = true }
rusqlite = { version = "0.38.0", features = ["bundled"] }
num_cpus = "1.16.0"
tokio-util = "0.7.13"
clap-cargo = "0.18.3"

[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = "0.6"

[target.'cfg(unix)'.dependencies]
rlimit = "0.11.0"

[dev-dependencies]
axum = { version = "0.8.1", features = ["http2"] }
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
bytes = "1.6"
float-cmp = "0.10.0"
http-mitm-proxy = { version = "0.18.0", default-features = false }
jsonschema = "0.42.2"
lazy_static = "1.5.0"
predicates = "3.1.0"
# features = ["aws_lc_rs"] is a workaround for mac & native-tls
# https://github.com/sfackler/rust-native-tls/issues/225
rcgen = { version = "0.14.3", features = ["aws_lc_rs"] }
regex = "1.10.5"
tempfile = "3.10.1"
rustls = "0.23.18"
rstest = "0.26.0"
rstest_reuse = "0.7.0"
ctor = "0.6.1"

[target.'cfg(unix)'.dev-dependencies]
actix-web = "4"

[profile.pgo]
inherits = "release"
# https://github.com/TechEmpower/FrameworkBenchmarks/blob/master/frameworks/Rust/faf/Cargo.toml + lto=true
opt-level = 3
panic = 'abort'
codegen-units = 1
lto = true
debug = false
incremental = false
overflow-checks = false

[profile.release-ci]
inherits = "pgo"


================================================
FILE: Cross.toml
================================================
# For Asahi linux
[target.aarch64-unknown-linux-gnu.env]
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]

[target.aarch64-unknown-linux-musl.env]
passthrough = ["JEMALLOC_SYS_WITH_LG_PAGE=16"]


================================================
FILE: Dockerfile
================================================
ARG RUST_VERSION=slim
FROM docker.io/library/rust:${RUST_VERSION} AS chef

RUN cargo install cargo-chef --locked
RUN apt-get update && apt-get install -y \
    cmake \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json

RUN cargo chef cook --release --no-default-features --features rustls --recipe-path recipe.json

COPY . .
RUN cargo build --release --no-default-features --features rustls --bin oha
RUN strip /app/target/release/oha

FROM registry.fedoraproject.org/fedora-minimal AS runtime
USER 65535
COPY --chown=65535:65535 --from=builder /app/target/release/oha /bin/
ENTRYPOINT ["/bin/oha"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 hatoo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# oha (おはよう)

[![GitHub Actions](https://github.com/hatoo/oha/workflows/CI/badge.svg)](https://github.com/hatoo/oha/actions?query=workflow%3ACI)
[![Crates.io](https://img.shields.io/crates/v/oha.svg)](https://crates.io/crates/oha)
[![Arch Linux](https://img.shields.io/archlinux/v/extra/x86_64/oha)](https://archlinux.org/packages/extra/x86_64/oha/)
[![Homebrew](https://img.shields.io/homebrew/v/oha)](https://formulae.brew.sh/formula/oha)

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/hatoo)

oha is a tiny program that sends some load to a web application and show realtime tui inspired by [rakyll/hey](https://github.com/rakyll/hey).

This program is written in Rust and powered by [tokio](https://github.com/tokio-rs/tokio) and beautiful tui by [ratatui](https://github.com/ratatui-org/ratatui).

![demo](demo.gif)

# Installation

This program is built on stable Rust, with both `make` and `cmake` prerequisites to install via cargo.

    cargo install oha

You can optionally build oha against [native-tls](https://github.com/sfackler/rust-native-tls) instead of [rustls](https://github.com/rustls/rustls).

    cargo install --no-default-features --features native-tls oha

You can enable VSOCK support by enabling `vsock` feature.

    cargo install --features vsock oha

You can enable experimental HTTP3 support by enabling the `http3` feature. This uses the [H3](https://github.com/hyperium/h3) library by the developers of Hyper.
It will remain experimental as long as H3 is experimental. It currently depends on using `rustls` for TLS.

## Download pre-built binary

You can download pre-built binary from [Release page](https://github.com/hatoo/oha/releases) for each version and from [Publish workflow](https://github.com/hatoo/oha/actions/workflows/release.yml) and [Publish PGO workflow](https://github.com/hatoo/oha/actions/workflows/release-pgo.yml) for each commit.

## On Arch Linux

    pacman -S oha

## On macOS (Homebrew)

    brew install oha

## On Windows (winget)

    winget install hatoo.oha

## On Debian ([Azlux's repository](http://packages.azlux.fr/))

    echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
    sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
    apt update
    apt install oha

## X-CMD (Linux, macOS, Windows WSL/GitBash)

You can install with [x-cmd](https://www.x-cmd.com).

```sh
x env use oha
```

## Containerized

### Official Docker image

[ghcr.io/hatoo/oha](https://github.com/hatoo/oha/pkgs/container/oha)

### Build image locally

You can also build and create a container image including oha

```sh
docker build -t hatoo/oha:latest .
```

Then you can use oha directly through the container

```sh
docker run --rm -it --network=host hatoo/oha:latest https://example.com:3000
```

## Profile-Guided Optimization (PGO)

You can build `oha` with PGO by using the following commands:

```sh
bun run pgo.js
```

And the binary will be available at `target/[target-triple]/pgo/oha`.

**Note**: Please keep in mind that in order to run the aforementioned command,
you need to have installed `cargo-pgo` cargo package.

You can install it via `cargo install cargo-pgo`.

# Platform

- Linux - Tested on Ubuntu 18.04 gnome-terminal
- Windows 10 - Tested on Windows Powershell
- MacOS - Tested on iTerm2

# Usage

`-q` option works different from [rakyll/hey](https://github.com/rakyll/hey). It's set overall query per second instead of for each workers.

```sh
Ohayou(おはよう), HTTP load generator, inspired by rakyll/hey with tui animation.

Usage: oha [OPTIONS] <URL>

Arguments:
  <URL>  Target URL or file with multiple URLs.

Options:
  -n <N_REQUESTS>
          Number of requests to run. Accepts plain numbers or suffixes: k = 1,000, m = 1,000,000 (e.g. 10k, 1m). [default: 200]
  -c <N_CONNECTIONS>
          Number of connections to run concurrently. You may should increase limit to number of open files for larger `-c`. [default: 50]
  -p <N_HTTP2_PARALLEL>
          Number of parallel requests to send on HTTP/2. `oha` will run c * p concurrent workers in total. [default: 1]
  -z <DURATION>
          Duration of application to send requests.
          On HTTP/1, When the duration is reached, ongoing requests are aborted and counted as "aborted due to deadline"
          You can change this behavior with `-w` option.
          Currently, on HTTP/2, When the duration is reached, ongoing requests are waited. `-w` option is ignored.
          Examples: -z 10s -z 3m.
  -w, --wait-ongoing-requests-after-deadline
          When the duration is reached, ongoing requests are waited
  -q <QUERY_PER_SECOND>
          Rate limit for all, in queries per second (QPS)
      --burst-delay <BURST_DURATION>
          Introduce delay between a predefined number of requests.
          Note: If qps is specified, burst will be ignored
      --burst-rate <BURST_REQUESTS>
          Rates of requests for burst. Default is 1
          Note: If qps is specified, burst will be ignored
      --rand-regex-url
          Generate URL by rand_regex crate but dot is disabled for each query e.g. http://127.0.0.1/[a-z][a-z][0-9]. Currently dynamic scheme, host and port with keep-alive do not work well. See https://docs.rs/rand_regex/latest/rand_regex/struct.Regex.html for details of syntax.
      --urls-from-file
          Read the URLs to query from a file
      --max-repeat <MAX_REPEAT>
          A parameter for the '--rand-regex-url'. The max_repeat parameter gives the maximum extra repeat counts the x*, x+ and x{n,} operators will become. [default: 4]
      --dump-urls <DUMP_URLS>
          Dump target Urls <DUMP_URLS> times to debug --rand-regex-url
      --latency-correction
          Correct latency to avoid coordinated omission problem. It's ignored if -q is not set.
      --no-tui
          No realtime tui
      --fps <FPS>
          Frame per second for tui. [default: 16]
  -m, --method <METHOD>
          HTTP method [default: GET]
  -H <HEADERS>
          Custom HTTP header. Examples: -H "foo: bar"
      --proxy-header <PROXY_HEADERS>
          Custom Proxy HTTP header. Examples: --proxy-header "foo: bar"
  -t <TIMEOUT>
          Timeout for each request. Default to infinite.
      --connect-timeout <CONNECT_TIMEOUT>
          Timeout for establishing a new connection. Default to 5s. [default: 5s]
  -A <ACCEPT_HEADER>
          HTTP Accept Header.
  -d <BODY_STRING>
          HTTP request body.
  -D <BODY_PATH>
          HTTP request body from file.
  -Z <BODY_PATH_LINES>
          HTTP request body from file line by line.
  -F, --form <FORM>
          Specify HTTP multipart POST data (curl compatible). Examples: -F 'name=value' -F 'file=@path/to/file'
  -T <CONTENT_TYPE>
          Content-Type.
  -a <BASIC_AUTH>
          Basic authentication (username:password), or AWS credentials (access_key:secret_key)
      --aws-session <AWS_SESSION>
          AWS session token
      --aws-sigv4 <AWS_SIGV4>
          AWS SigV4 signing params (format: aws:amz:region:service)
  -x <PROXY>
          HTTP proxy
      --proxy-http-version <PROXY_HTTP_VERSION>
          HTTP version to connect to proxy. Available values 0.9, 1.0, 1.1, 2.
      --proxy-http2
          Use HTTP/2 to connect to proxy. Shorthand for --proxy-http-version=2
      --http-version <HTTP_VERSION>
          HTTP version. Available values 0.9, 1.0, 1.1, 2, 3
      --http2
          Use HTTP/2. Shorthand for --http-version=2
      --host <HOST>
          HTTP Host header
      --disable-compression
          Disable compression.
  -r, --redirect <REDIRECT>
          Limit for number of Redirect. Set 0 for no redirection. Redirection isn't supported for HTTP/2. [default: 10]
      --disable-keepalive
          Disable keep-alive, prevents re-use of TCP connections between different HTTP requests. This isn't supported for HTTP/2.
      --no-pre-lookup
          *Not* perform a DNS lookup at beginning to cache it
      --ipv6
          Lookup only ipv6.
      --ipv4
          Lookup only ipv4.
      --cacert <CACERT>
          (TLS) Use the specified certificate file to verify the peer. Native certificate store is used even if this argument is specified.
      --cert <CERT>
          (TLS) Use the specified client certificate file. --key must be also specified
      --key <KEY>
          (TLS) Use the specified client key file. --cert must be also specified
      --insecure
          Accept invalid certs.
      --connect-to <CONNECT_TO>
          Override DNS resolution and default port numbers with strings like 'example.org:443:localhost:8443'
          Note: if used several times for the same host:port:target_host:target_port, a random choice is made
      --no-color
          Disable the color scheme. [env: NO_COLOR=]
      --unix-socket <UNIX_SOCKET>
          Connect to a unix socket instead of the domain in the URL. Only for non-HTTPS URLs.
      --stats-success-breakdown
          Include a response status code successful or not successful breakdown for the time histogram and distribution statistics
      --db-url <DB_URL>
          Write succeeded requests to sqlite database url E.G test.db
      --debug
          Perform a single request and dump the request and response
  -o, --output <OUTPUT>
          Output file to write the results to. If not specified, results are written to stdout.
      --output-format <OUTPUT_FORMAT>
          Output format [default: text] [possible values: text, json, csv, quiet]
  -u, --time-unit <TIME_UNIT>
          Time unit to be used. If not specified, the time unit is determined automatically. This option affects only text format. [possible values: ns, us, ms, s, m, h]
  -h, --help
          Print help
  -V, --version
          Print version
```

# Performance

`oha` uses faster implementation when `--no-tui` option is set and both `-q` and `--burst-delay` are not set because it can avoid overhead to gather data realtime.

# Output

By default `oha` outputs a text summary of the results.

`oha` prints JSON summary output when `--output-format json` option is set.
The schema of JSON output is defined in [schema.json](./schema.json).

When `--output-format csv` is used result of each request is printed as a line of comma separated values.

# Tips

## Stress test in more realistic condition

`oha` uses default options inherited from [rakyll/hey](https://github.com/rakyll/hey) but you may need to change options to stress test in more realistic condition.

I suggest to run `oha` with following options.

```sh
oha <-z or -n> -c <number of concurrent connections> -q <query per seconds> --latency-correction --disable-keepalive <target-address>
```

- --disable-keepalive

    In real, user doesn't query same URL using [Keep-Alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive). You may want to run without `Keep-Alive`.
- --latency-correction

    You can avoid `Coordinated Omission Problem` by using `--latency-correction`.

## Burst feature

You can use `--burst-delay` along with `--burst-rate` option to introduce delay between a defined number of requests.

```sh
oha -n 10 --burst-delay 2s --burst-rate 4
```

In this particular scenario, every 2 seconds, 4 requests will be processed, and after 6s the total of 10 requests will be processed.
*NOTE: If you don't set `--burst-rate` option, the amount is default to 1*

## Dynamic url feature

You can use `--rand-regex-url` option to generate random url for each connection.

```sh
oha --rand-regex-url http://127.0.0.1/[a-z][a-z][0-9]
```

Each Urls are generated by [rand_regex](https://github.com/kennytm/rand_regex) crate but regex's dot is disabled since it's not useful for this purpose and it's very inconvenient if url's dots are interpreted as regex's dot.

Optionally you can set `--max-repeat` option to limit max repeat count for each regex. e.g http://127.0.0.1/[a-z]* with `--max-repeat 4` will generate url like http://127.0.0.1/[a-z]{0,4}

Currently dynamic scheme, host and port with keep-alive are not works well.

## URLs from file feature

You can use `--urls-from-file` to read the target URLs from a file. Each line of this file needs to contain one valid URL as in the example below.

```
http://domain.tld/foo/bar
http://domain.tld/assets/vendors-node_modules_highlight_js_lib_index_js-node_modules_tanstack_react-query_build_modern-3fdf40-591fb51c8a6e.js
http://domain.tld/images/test.png
http://domain.tld/foo/bar?q=test
http://domain.tld/foo
```

Such a file can for example be created from an access log to generate a more realistic load distribution over the different pages of a server. 

When this type of URL specification is used, every request goes to a random URL given in the file.

# Contribution

Feel free to help us!

Here are some areas which need improving.

- Write tests
- Improve tui design.
  - Show more information?
- Improve speed
  - I'm new to tokio. I think there are some space to optimize query scheduling.


================================================
FILE: pgo/server/Cargo.toml
================================================
[package]
name = "server"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = "0.8.1"
tokio = { version = "1", features = ["full"] }


================================================
FILE: pgo/server/src/main.rs
================================================
use std::net::SocketAddr;
use tokio::net::TcpListener;

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    // build our application with a route
    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root));

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    let addr = SocketAddr::from(([127, 0, 0, 1], 8888));
    let listener = TcpListener::bind(&addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}


================================================
FILE: pgo.js
================================================
import { $ } from "bun";

let additional = [];

if (Bun.argv.length >= 3) {
    additional = Bun.argv.slice(2);
}

let server = null;

try {
    server = Bun.spawn(['cargo', 'run', '--release', '--manifest-path', 'pgo/server/Cargo.toml']);
    await $`cargo pgo run -- --profile pgo ${additional} -- -z 3m -c 900 --no-tui http://localhost:8888`;
    await $`cargo pgo optimize build -- --profile pgo ${additional}`
} finally {
    if (server !== null) {
        server.kill();
    }
}


================================================
FILE: schema.json
================================================
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "JSON schema for the output of the `oha --output-format json`",
  "type": "object",
  "properties": {
    "summary": {
      "description": "Important statistics",
      "type": "object",
      "properties": {
        "successRate": {
          "description": "The number of success requests / All requests which isn't includes deadline",
          "type": "number"
        },
        "total": {
          "description": "Total duration in seconds",
          "type": "number"
        },
        "slowest": {
          "description": "The slowest request duration in seconds",
          "type": "number"
        },
        "fastest": {
          "description": "The fastest request duration in seconds",
          "type": "number"
        },
        "average": {
          "description": "The average request duration in seconds",
          "type": "number"
        },
        "requestsPerSec": {
          "description": "The number of requests per second",
          "type": "number"
        },
        "totalData": {
          "description": "Total data of HTTP bodies in bytes",
          "type": "integer"
        },
        "sizePerRequest": {
          "description": "The average size of HTTP bodies in bytes",
          "type": "integer"
        },
        "sizePerSec": {
          "description": "The average size of HTTP bodies per second in bytes",
          "type": "number"
        }
      },
      "required": [
        "successRate",
        "total",
        "slowest",
        "fastest",
        "average",
        "requestsPerSec",
        "totalData",
        "sizePerRequest",
        "sizePerSec"
      ]
    },
    "responseTimeHistogram": {
      "description": "The histogram of response time in seconds. The key is the response time in seconds and the value is the number of requests",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      }
    },
    "latencyPercentiles": {
      "description": "The latency percentiles in seconds",
      "type": "object",
      "properties": {
        "p10": {
          "type": "number"
        },
        "p25": {
          "type": "number"
        },
        "p50": {
          "type": "number"
        },
        "p75": {
          "type": "number"
        },
        "p90": {
          "type": "number"
        },
        "p95": {
          "type": "number"
        },
        "p99": {
          "type": "number"
        },
        "p99.9": {
          "type": "number"
        },
        "p99.99": {
          "type": "number"
        }
      },
      "required": [
        "p10",
        "p25",
        "p50",
        "p75",
        "p90",
        "p95",
        "p99",
        "p99.9",
        "p99.99"
      ]
    },
    "firstByteHistogram": {
      "description": "The histogram of first byte time in seconds. The key is the first byte time in seconds and the value is the number of requests",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      }
    },
    "firstBytePercentiles": {
      "description": "The first byte percentiles in seconds",
      "type": "object",
      "properties": {
        "p10": {
          "type": "number"
        },
        "p25": {
          "type": "number"
        },
        "p50": {
          "type": "number"
        },
        "p75": {
          "type": "number"
        },
        "p90": {
          "type": "number"
        },
        "p95": {
          "type": "number"
        },
        "p99": {
          "type": "number"
        },
        "p99.9": {
          "type": "number"
        },
        "p99.99": {
          "type": "number"
        }
      },
      "required": [
        "p10",
        "p25",
        "p50",
        "p75",
        "p90",
        "p95",
        "p99",
        "p99.9",
        "p99.99"
      ]
    },
    "responseTimeHistogramSuccessful": {
      "description": "Only present if `--stats-success-breakdown` argument is passed. The histogram of response time in seconds for successful requests. The key is the response time in seconds and the value is the number of requests",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      }
    },
    "latencyPercentileSuccessful": {
      "description": "Only present if `--stats-success-breakdown` argument is passed. The latency percentiles in seconds for successful requests",
      "type": "object",
      "properties": {
        "p10": {
          "type": "number"
        },
        "p25": {
          "type": "number"
        },
        "p50": {
          "type": "number"
        },
        "p75": {
          "type": "number"
        },
        "p90": {
          "type": "number"
        },
        "p95": {
          "type": "number"
        },
        "p99": {
          "type": "number"
        },
        "p99.9": {
          "type": "number"
        },
        "p99.99": {
          "type": "number"
        }
      },
      "required": [
        "p10",
        "p25",
        "p50",
        "p75",
        "p90",
        "p95",
        "p99",
        "p99.9",
        "p99.99"
      ]
    },
    "responseTimeHistogramNotSuccessful": {
      "description": "Only present if `--stats-success-breakdown` argument is passed. The histogram of response time in seconds for not successful requests. The key is the response time in seconds and the value is the number of requests",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      }
    },
    "latencyPercentileNotSuccessful": {
      "description": "Only present if `--stats-success-breakdown` argument is passed. The latency percentiles in seconds for not successful requests",
      "type": "object",
      "properties": {
        "p10": {
          "type": "number"
        },
        "p25": {
          "type": "number"
        },
        "p50": {
          "type": "number"
        },
        "p75": {
          "type": "number"
        },
        "p90": {
          "type": "number"
        },
        "p95": {
          "type": "number"
        },
        "p99": {
          "type": "number"
        },
        "p99.9": {
          "type": "number"
        },
        "p99.99": {
          "type": "number"
        }
      },
      "required": [
        "p10",
        "p25",
        "p50",
        "p75",
        "p90",
        "p95",
        "p99",
        "p99.9",
        "p99.99"
      ]
    },
    "rps": {
      "description": "The statistics for requests per second. Note: the way of calculating rps over time isn't obvious, see source code for details.",
      "type": "object",
      "properties": {
        "mean": {
          "type": "number"
        },
        "stddev": {
          "type": [
            "number",
            "null"
          ]
        },
        "max": {
          "type": "number"
        },
        "min": {
          "type": "number"
        },
        "percentiles": {
          "type": "object",
          "properties": {
            "p10": {
              "type": "number"
            },
            "p25": {
              "type": "number"
            },
            "p50": {
              "type": "number"
            },
            "p75": {
              "type": "number"
            },
            "p90": {
              "type": "number"
            },
            "p95": {
              "type": "number"
            },
            "p99": {
              "type": "number"
            },
            "p99.9": {
              "type": "number"
            },
            "p99.99": {
              "type": "number"
            }
          },
          "required": [
            "p10",
            "p25",
            "p50",
            "p75",
            "p90",
            "p95",
            "p99",
            "p99.9",
            "p99.99"
          ]
        }
      },
      "required": [
        "mean",
        "stddev",
        "max",
        "min",
        "percentiles"
      ]
    },
    "details": {
      "description": "The details of connection time. Note: `oha` uses keep-alive connections in default. So, the connection time may added only for the first request.",
      "type": "object",
      "properties": {
        "DNSDialup": {
          "description": "The time of DNS resolution + TCP handshake in seconds",
          "type": "object",
          "properties": {
            "average": {
              "type": "number"
            },
            "fastest": {
              "type": "number"
            },
            "slowest": {
              "type": "number"
            }
          },
          "required": [
            "average",
            "fastest",
            "slowest"
          ]
        },
        "DNSLookup": {
          "description": "The time of DNS resolution in seconds",
          "type": "object",
          "properties": {
            "average": {
              "type": "number"
            },
            "fastest": {
              "type": "number"
            },
            "slowest": {
              "type": "number"
            }
          },
          "required": [
            "average",
            "fastest",
            "slowest"
          ]
        },
        "firstByte": {
          "description": "The time to first byte in seconds",
          "type": "object",
          "properties": {
            "average": {
              "type": "number"
            },
            "fastest": {
              "type": "number"
            },
            "slowest": {
              "type": "number"
            }
          },
          "required": [
            "average",
            "fastest",
            "slowest"
          ]
        }
      },
      "required": [
        "DNSDialup",
        "DNSLookup",
        "firstByte"
      ]
    },
    "statusCodeDistribution": {
      "description": "The distribution of status codes. The key is the status code and the value is the number of requests",
      "type": "object",
      "propertyNames": {
        "type": "string",
        "pattern": "^[0-9]+$"
      },
      "additionalProperties": {
        "type": "integer"
      },
      "minProperties": 1
    },
    "errorDistribution": {
      "description": "The distribution of errors. The key is the error message and the value is the number of errors. Note: the error message is from internal libraries so the detail may change in future.",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      }
    }
  },
  "required": [
    "summary",
    "responseTimeHistogram",
    "latencyPercentiles",
    "firstByteHistogram",
    "firstBytePercentiles",
    "rps",
    "details",
    "statusCodeDistribution",
    "errorDistribution"
  ]
}

================================================
FILE: src/aws_auth.rs
================================================
use anyhow::Result;

use bytes::Bytes;
use hyper::{
    HeaderMap,
    header::{self, HeaderName},
};
use thiserror::Error;
use url::Url;

pub struct AwsSignatureConfig {
    pub access_key: String,
    pub secret_key: String,
    pub session_token: Option<String>,
    pub service: String,
    pub region: String,
}

#[derive(Error, Debug)]
pub enum AwsSignatureError {
    #[error("URL must contain a host {0}")]
    NoHost(Url),
    #[error("Invalid host header name {0}")]
    InvalidHost(String),
    #[error("Invalid authorization header name {0}")]
    InvalidAuthorization(String),
}

// Initialize unsignable headers as a static constant
static UNSIGNABLE_HEADERS: [HeaderName; 8] = [
    header::ACCEPT,
    header::ACCEPT_ENCODING,
    header::USER_AGENT,
    header::EXPECT,
    header::RANGE,
    header::CONNECTION,
    HeaderName::from_static("presigned-expires"),
    HeaderName::from_static("x-amzn-trace-id"),
];

impl AwsSignatureConfig {
    pub fn sign_request(
        &self,
        method: &str,
        headers: &mut HeaderMap,
        url: &Url,
        body: &Bytes,
    ) -> Result<(), AwsSignatureError> {
        let datetime = chrono::Utc::now();

        let header_amz_date = datetime
            .format("%Y%m%dT%H%M%SZ")
            .to_string()
            .parse()
            .unwrap();

        if !headers.contains_key(header::HOST) {
            let host = url
                .host_str()
                .ok_or_else(|| AwsSignatureError::NoHost(url.clone()))?;
            headers.insert(
                header::HOST,
                host.parse()
                    .map_err(|_| AwsSignatureError::InvalidHost(host.to_string()))?,
            );
        }
        headers.insert("x-amz-date", header_amz_date);

        if let Some(session_token) = &self.session_token {
            headers.insert("x-amz-security-token", session_token.parse().unwrap());
        }

        headers.remove(header::AUTHORIZATION);

        //remove and store headers in a vec from unsignable_headers
        let removed_headers: Vec<(header::HeaderName, header::HeaderValue)> = UNSIGNABLE_HEADERS
            .iter()
            .filter_map(|k| headers.remove(k).map(|v| (k.clone(), v)))
            .collect();

        headers.insert(
            header::CONTENT_LENGTH,
            body.len().to_string().parse().unwrap(),
        );

        let aws_sign = aws_sign_v4::AwsSign::new(
            method,
            url.as_str(),
            &datetime,
            headers,
            &self.region,
            &self.access_key,
            &self.secret_key,
            &self.service,
            body,
        );

        let signature = aws_sign.sign();

        //insert headers
        for (key, value) in removed_headers {
            headers.insert(key, value);
        }

        headers.insert(
            header::AUTHORIZATION,
            signature
                .parse()
                .map_err(|_| AwsSignatureError::InvalidAuthorization(signature.to_string()))?,
        );

        Ok(())
    }

    pub fn new(
        access_key: &str,
        secret_key: &str,
        signing_params: &str,
        session_token: Option<String>,
    ) -> Result<Self, anyhow::Error> {
        let parts: Vec<&str> = signing_params
            .strip_prefix("aws:amz:")
            .unwrap_or_default()
            .split(':')
            .collect();
        if parts.len() != 2 {
            anyhow::bail!("Invalid AWS signing params format. Expected aws:amz:region:service");
        }

        Ok(Self {
            access_key: access_key.into(),
            secret_key: secret_key.into(),
            session_token,
            region: parts[0].to_string(),
            service: parts[1].to_string(),
        })
    }
}


================================================
FILE: src/cli.rs
================================================
use hyper::http::header::{HeaderName, HeaderValue};
use std::str::FromStr;

pub fn parse_header(s: &str) -> Result<(HeaderName, HeaderValue), anyhow::Error> {
    let header = s.splitn(2, ':').collect::<Vec<_>>();
    anyhow::ensure!(header.len() == 2, anyhow::anyhow!("Parse header"));
    let name = HeaderName::from_str(header[0])?;
    let value = HeaderValue::from_str(header[1].trim_start_matches(' '))?;
    Ok::<(HeaderName, HeaderValue), anyhow::Error>((name, value))
}

pub fn parse_n_requests(s: &str) -> Result<usize, String> {
    let s = s.trim().to_lowercase();
    if let Some(num) = s.strip_suffix('k') {
        num.parse::<f64>()
            .map(|n| (n * 1000_f64) as usize)
            .map_err(|e| e.to_string())
    } else if let Some(num) = s.strip_suffix('m') {
        num.parse::<f64>()
            .map(|n| (n * 1_000_000_f64) as usize)
            .map_err(|e| e.to_string())
    } else {
        s.parse::<usize>().map_err(|e| e.to_string())
    }
}

/// An entry specified by `connect-to` to override DNS resolution and default
/// port numbers. For example, `example.org:80:localhost:5000` will connect to
/// `localhost:5000` whenever `http://example.org` is requested.
#[derive(Clone, Debug)]
pub struct ConnectToEntry {
    pub requested_host: String,
    pub requested_port: u16,
    pub target_host: String,
    pub target_port: u16,
}

impl FromStr for ConnectToEntry {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let expected_syntax: &str = "syntax for --connect-to is host:port:target_host:target_port";

        let (s, target_port) = s.rsplit_once(':').ok_or(expected_syntax)?;
        let (s, target_host) = if s.ends_with(']') {
            // ipv6
            let i = s.rfind(":[").ok_or(expected_syntax)?;
            (&s[..i], &s[i + 1..])
        } else {
            s.rsplit_once(':').ok_or(expected_syntax)?
        };
        let (requested_host, requested_port) = s.rsplit_once(':').ok_or(expected_syntax)?;

        Ok(ConnectToEntry {
            requested_host: requested_host.into(),
            requested_port: requested_port.parse().map_err(|err| {
                format!("requested port must be an u16, but got {requested_port}: {err}")
            })?,
            target_host: target_host.into(),
            target_port: target_port.parse().map_err(|err| {
                format!("target port must be an u16, but got {target_port}: {err}")
            })?,
        })
    }
}

#[cfg(feature = "vsock")]
pub fn parse_vsock_addr(s: &str) -> Result<tokio_vsock::VsockAddr, String> {
    let (cid, port) = s
        .split_once(':')
        .ok_or("syntax for --vsock-addr is cid:port")?;
    Ok(tokio_vsock::VsockAddr::new(
        cid.parse()
            .map_err(|err| format!("cid must be a u32, but got {cid}: {err}"))?,
        port.parse()
            .map_err(|err| format!("port must be a u32, but got {port}: {err}"))?,
    ))
}


================================================
FILE: src/client.rs
================================================
use bytes::Bytes;
#[cfg(test)]
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use http_body_util::{BodyExt, Full};
use hyper::{Method, Request, http};
use hyper_util::rt::{TokioExecutor, TokioIo};
use rand::{prelude::*, rng};
use std::{
    borrow::Cow,
    io::Write,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering::Relaxed},
    },
    time::Instant,
};
use thiserror::Error;
use tokio::{
    io::{AsyncRead, AsyncWrite},
    net::TcpStream,
};
use url::{ParseError, Url};

use crate::{
    ConnectToEntry,
    pcg64si::Pcg64Si,
    request_generator::{RequestGenerationError, RequestGenerator},
    url_generator::UrlGeneratorError,
};

#[cfg(feature = "http3")]
use crate::client_h3::send_debug_request_http3;

type SendRequestHttp1 = hyper::client::conn::http1::SendRequest<Full<Bytes>>;
type SendRequestHttp2 = hyper::client::conn::http2::SendRequest<Full<Bytes>>;

fn format_host_port(host: &str, port: u16) -> String {
    if host.contains(':') && !(host.starts_with('[') && host.ends_with(']')) {
        format!("[{host}]:{port}")
    } else {
        format!("{host}:{port}")
    }
}

#[derive(Debug, Clone, Copy)]
pub struct ConnectionTime {
    pub dns_lookup: std::time::Duration,
    pub dialup: std::time::Duration,
}

#[derive(Debug, Clone)]
/// a result for a request
pub struct RequestResult {
    pub rng: Pcg64Si,
    // When the query should started
    pub start_latency_correction: Option<std::time::Instant>,
    /// When the query started
    pub start: std::time::Instant,
    /// DNS + dialup
    /// None when reuse connection
    pub connection_time: Option<ConnectionTime>,
    /// First body byte received
    pub first_byte: Option<std::time::Instant>,
    /// When the query ends
    pub end: std::time::Instant,
    /// HTTP status
    pub status: http::StatusCode,
    /// Length of body
    pub len_bytes: usize,
}

impl RequestResult {
    /// Duration the request takes.
    pub fn duration(&self) -> std::time::Duration {
        self.end - self.start_latency_correction.unwrap_or(self.start)
    }
}

// encapsulates the HTTP generation of the work type. Used internally only for conditional logic.
#[derive(Debug, Clone, Copy, PartialEq)]
enum HttpWorkType {
    H1,
    H2,
    #[cfg(feature = "http3")]
    H3,
}

pub struct Dns {
    pub connect_to: Vec<ConnectToEntry>,
    pub resolver:
        hickory_resolver::Resolver<hickory_resolver::name_server::TokioConnectionProvider>,
}

impl Dns {
    fn select_connect_to<'a, R: Rng>(
        &'a self,
        host: &str,
        port: u16,
        rng: &mut R,
    ) -> Option<&'a ConnectToEntry> {
        self.connect_to
            .iter()
            .filter(|entry| entry.requested_port == port && entry.requested_host == host)
            .collect::<Vec<_>>()
            .choose(rng)
            .copied()
    }

    /// Perform a DNS lookup for a given url and returns (ip_addr, port)
    async fn lookup<R: Rng>(
        &self,
        url: &Url,
        rng: &mut R,
    ) -> Result<(std::net::IpAddr, u16), ClientError> {
        let host = url.host_str().ok_or(ClientError::HostNotFound)?;
        let port = url
            .port_or_known_default()
            .ok_or(ClientError::PortNotFound)?;

        // Try to find an override (passed via `--connect-to`) that applies to this (host, port),
        // choosing one randomly if several match.
        let (host, port) = if let Some(entry) = self.select_connect_to(host, port, rng) {
            (entry.target_host.as_str(), entry.target_port)
        } else {
            (host, port)
        };

        let host = if host.starts_with('[') && host.ends_with(']') {
            // host is [ipv6] format
            // remove first [ and last ]
            &host[1..host.len() - 1]
        } else {
            host
        };

        // Perform actual DNS lookup, either on the original (host, port), or
        // on the (host, port) specified with `--connect-to`.
        let addrs = self
            .resolver
            .lookup_ip(host)
            .await
            .map_err(Box::new)?
            .iter()
            .collect::<Vec<_>>();

        let addr = *addrs.choose(rng).ok_or(ClientError::DNSNoRecord)?;

        Ok((addr, port))
    }
}

#[derive(Error, Debug)]
pub enum ClientError {
    #[error("failed to get port from URL")]
    PortNotFound,
    #[error("failed to get host from URL")]
    HostNotFound,
    #[error("No record returned from DNS")]
    DNSNoRecord,
    #[error("Redirection limit has reached")]
    TooManyRedirect,
    #[error(transparent)]
    // Use Box here because ResolveError is big.
    Resolve(#[from] Box<hickory_resolver::ResolveError>),

    #[cfg(feature = "native-tls")]
    #[error(transparent)]
    NativeTls(#[from] native_tls::Error),

    #[cfg(feature = "rustls")]
    #[error(transparent)]
    Rustls(#[from] rustls::Error),

    #[cfg(feature = "rustls")]
    #[error(transparent)]
    InvalidDnsName(#[from] rustls_pki_types::InvalidDnsNameError),

    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Http(#[from] http::Error),
    #[error(transparent)]
    Hyper(#[from] hyper::Error),
    #[error(transparent)]
    InvalidUriParts(#[from] http::uri::InvalidUriParts),
    #[error(transparent)]
    InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
    #[error("Failed to get header from builder")]
    GetHeaderFromBuilder,
    #[error(transparent)]
    HeaderToStr(#[from] http::header::ToStrError),
    #[error(transparent)]
    InvalidUri(#[from] http::uri::InvalidUri),
    #[error("timeout")]
    Timeout,
    #[error("aborted due to deadline")]
    Deadline,
    #[error(transparent)]
    UrlGenerator(#[from] UrlGeneratorError),
    #[error(transparent)]
    UrlParse(#[from] ParseError),
    #[error("Request generation error: {0}")]
    RequestGeneration(#[from] RequestGenerationError),
    #[cfg(feature = "http3")]
    #[error(transparent)]
    Http3(#[from] crate::client_h3::Http3Error),
}

pub struct Client {
    pub request_generator: RequestGenerator,
    pub proxy_http_version: http::Version,
    pub proxy_headers: http::header::HeaderMap,
    pub dns: Dns,
    pub timeout: Option<std::time::Duration>,
    pub connect_timeout: std::time::Duration,
    pub redirect_limit: usize,
    pub disable_keepalive: bool,
    pub proxy_url: Option<Url>,
    #[cfg(unix)]
    pub unix_socket: Option<std::path::PathBuf>,
    #[cfg(feature = "vsock")]
    pub vsock_addr: Option<tokio_vsock::VsockAddr>,
    #[cfg(feature = "rustls")]
    pub rustls_configs: crate::tls_config::RuslsConfigs,
    #[cfg(all(feature = "native-tls", not(feature = "rustls")))]
    pub native_tls_connectors: crate::tls_config::NativeTlsConnectors,
}

#[cfg(test)]
impl Default for Client {
    fn default() -> Self {
        use crate::request_generator::BodyGenerator;

        let (resolver_config, resolver_opts) = crate::system_resolv_conf()
            .unwrap_or_else(|_| (ResolverConfig::default(), ResolverOpts::default()));
        let resolver = hickory_resolver::Resolver::builder_with_config(
            resolver_config,
            hickory_resolver::name_server::TokioConnectionProvider::default(),
        )
        .with_options(resolver_opts)
        .build();

        Self {
            request_generator: RequestGenerator {
                url_generator: crate::url_generator::UrlGenerator::new_static(
                    "http://example.com".parse().unwrap(),
                ),
                https: false,
                http_proxy: None,
                method: http::Method::GET,
                version: http::Version::HTTP_11,
                headers: http::header::HeaderMap::new(),
                body_generator: BodyGenerator::Static(Bytes::new()),
                aws_config: None,
            },
            proxy_http_version: http::Version::HTTP_11,
            proxy_headers: http::header::HeaderMap::new(),
            dns: Dns {
                resolver,
                connect_to: Vec::new(),
            },
            timeout: None,
            connect_timeout: std::time::Duration::from_secs(5),
            redirect_limit: 0,
            disable_keepalive: false,
            proxy_url: None,
            #[cfg(unix)]
            unix_socket: None,
            #[cfg(feature = "vsock")]
            vsock_addr: None,
            #[cfg(feature = "rustls")]
            rustls_configs: crate::tls_config::RuslsConfigs::new(false, None, None),
            #[cfg(all(feature = "native-tls", not(feature = "rustls")))]
            native_tls_connectors: crate::tls_config::NativeTlsConnectors::new(false, None, None),
        }
    }
}

struct ClientStateHttp1 {
    rng: Pcg64Si,
    send_request: Option<SendRequestHttp1>,
}

impl Default for ClientStateHttp1 {
    fn default() -> Self {
        Self {
            rng: SeedableRng::from_rng(&mut rand::rng()),
            send_request: None,
        }
    }
}

struct ClientStateHttp2 {
    rng: Pcg64Si,
    send_request: SendRequestHttp2,
}

pub enum QueryLimit {
    Qps(f64),
    Burst(std::time::Duration, usize),
}

// To avoid dynamic dispatch
// I'm not sure how much this is effective
pub(crate) enum Stream {
    Tcp(TcpStream),
    #[cfg(all(feature = "native-tls", not(feature = "rustls")))]
    Tls(tokio_native_tls::TlsStream<TcpStream>),
    #[cfg(feature = "rustls")]
    // Box for large variant
    Tls(Box<tokio_rustls::client::TlsStream<TcpStream>>),
    #[cfg(unix)]
    Unix(tokio::net::UnixStream),
    #[cfg(feature = "vsock")]
    Vsock(tokio_vsock::VsockStream),
    #[cfg(feature = "http3")]
    Quic(quinn::Connection),
}

impl Stream {
    async fn handshake_http1(self, with_upgrade: bool) -> Result<SendRequestHttp1, ClientError> {
        match self {
            Stream::Tcp(stream) => {
                let (send_request, conn) =
                    hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
                if with_upgrade {
                    tokio::spawn(conn.with_upgrades());
                } else {
                    tokio::spawn(conn);
                }
                Ok(send_request)
            }
            Stream::Tls(stream) => {
                let (send_request, conn) =
                    hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
                if with_upgrade {
                    tokio::spawn(conn.with_upgrades());
                } else {
                    tokio::spawn(conn);
                }
                Ok(send_request)
            }
            #[cfg(unix)]
            Stream::Unix(stream) => {
                let (send_request, conn) =
                    hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
                if with_upgrade {
                    tokio::spawn(conn.with_upgrades());
                } else {
                    tokio::spawn(conn);
                }
                Ok(send_request)
            }
            #[cfg(feature = "vsock")]
            Stream::Vsock(stream) => {
                let (send_request, conn) =
                    hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
                if with_upgrade {
                    tokio::spawn(conn.with_upgrades());
                } else {
                    tokio::spawn(conn);
                }
                Ok(send_request)
            }
            #[cfg(feature = "http3")]
            Stream::Quic(_) => {
                panic!("quic is not supported in http1")
            }
        }
    }
    async fn handshake_http2(self) -> Result<SendRequestHttp2, ClientError> {
        let mut builder = hyper::client::conn::http2::Builder::new(TokioExecutor::new());
        builder
            // from nghttp2's default
            .initial_stream_window_size((1 << 30) - 1)
            .initial_connection_window_size((1 << 30) - 1);

        match self {
            Stream::Tcp(stream) => {
                let (send_request, conn) = builder.handshake(TokioIo::new(stream)).await?;
                tokio::spawn(conn);
                Ok(send_request)
            }
            Stream::Tls(stream) => {
                let (send_request, conn) = builder.handshake(TokioIo::new(stream)).await?;
                tokio::spawn(conn);
                Ok(send_request)
            }
            #[cfg(unix)]
            Stream::Unix(stream) => {
                let (send_request, conn) = builder.handshake(TokioIo::new(stream)).await?;
                tokio::spawn(conn);
                Ok(send_request)
            }
            #[cfg(feature = "vsock")]
            Stream::Vsock(stream) => {
                let (send_request, conn) = builder.handshake(TokioIo::new(stream)).await?;
                tokio::spawn(conn);
                Ok(send_request)
            }
            #[cfg(feature = "http3")]
            Stream::Quic(_) => {
                panic!("quic is not supported in http2")
            }
        }
    }
}

impl Client {
    #[inline]
    fn is_http2(&self) -> bool {
        self.request_generator.version == http::Version::HTTP_2
    }

    #[inline]
    fn is_proxy_http2(&self) -> bool {
        self.proxy_http_version == http::Version::HTTP_2
    }

    fn is_work_http2(&self) -> bool {
        if self.proxy_url.is_some() {
            if self.request_generator.https {
                self.is_http2()
            } else {
                self.is_proxy_http2()
            }
        } else {
            self.is_http2()
        }
    }

    fn work_type(&self) -> HttpWorkType {
        #[cfg(feature = "http3")]
        if self.request_generator.version == http::Version::HTTP_3 {
            return HttpWorkType::H3;
        }
        if self.is_work_http2() {
            HttpWorkType::H2
        } else {
            HttpWorkType::H1
        }
    }

    /// Perform a DNS lookup to cache it
    /// This is useful to avoid DNS lookup latency at the first concurrent requests
    pub async fn pre_lookup(&self) -> Result<(), ClientError> {
        // If the client is using a unix socket, we don't need to do a DNS lookup
        #[cfg(unix)]
        if self.unix_socket.is_some() {
            return Ok(());
        }
        // If the client is using a vsock address, we don't need to do a DNS lookup
        #[cfg(feature = "vsock")]
        if self.vsock_addr.is_some() {
            return Ok(());
        }

        let mut rng = rand::rng();
        let url = self.request_generator.url_generator.generate(&mut rng)?;
        // It automatically caches the result
        self.dns.lookup(&url, &mut rng).await?;
        Ok(())
    }

    #[allow(clippy::type_complexity)]
    pub fn generate_request<R: Rng + Copy>(
        &self,
        rng: &mut R,
    ) -> Result<(Cow<'_, Url>, Request<Full<Bytes>>, R), ClientError> {
        let snapshot = *rng;
        let (url, mut req) = self.request_generator.generate(rng)?;
        if self.proxy_url.is_some() && req.uri().scheme_str() == Some("http") {
            if let Some(authority) = req.uri().authority() {
                let requested_host = authority.host();
                let requested_port = authority.port_u16().unwrap_or(80);
                if let Some(entry) = self
                    .dns
                    .select_connect_to(requested_host, requested_port, rng)
                {
                    let new_authority: http::uri::Authority =
                        format_host_port(entry.target_host.as_str(), entry.target_port).parse()?;
                    let mut parts = req.uri().clone().into_parts();
                    parts.authority = Some(new_authority);
                    let new_uri = http::Uri::from_parts(parts)?;
                    *req.uri_mut() = new_uri;
                }
            }
        }
        Ok((url, req, snapshot))
    }

    /**
     * Returns a stream of the underlying transport. NOT a HTTP client
     */
    pub(crate) async fn client<R: Rng>(
        &self,
        url: &Url,
        rng: &mut R,
        http_version: http::Version,
    ) -> Result<(Instant, Stream), ClientError> {
        let timeout_duration = self.connect_timeout;

        #[cfg(feature = "http3")]
        if http_version == http::Version::HTTP_3 {
            let addr = self.dns.lookup(url, rng).await?;
            let dns_lookup = Instant::now();
            let stream = tokio::time::timeout(timeout_duration, self.quic_client(addr, url)).await;
            return match stream {
                Ok(Ok(stream)) => Ok((dns_lookup, stream)),
                Ok(Err(err)) => Err(err),
                Err(_) => Err(ClientError::Timeout),
            };
        }
        if url.scheme() == "https" {
            let addr = self.dns.lookup(url, rng).await?;
            let dns_lookup = Instant::now();
            // If we do not put a timeout here then the connections attempts will
            // linger long past the configured timeout
            let stream =
                tokio::time::timeout(timeout_duration, self.tls_client(addr, url, http_version))
                    .await;
            return match stream {
                Ok(Ok(stream)) => Ok((dns_lookup, stream)),
                Ok(Err(err)) => Err(err),
                Err(_) => Err(ClientError::Timeout),
            };
        }
        #[cfg(unix)]
        if let Some(socket_path) = &self.unix_socket {
            let dns_lookup = Instant::now();
            let stream = tokio::time::timeout(
                timeout_duration,
                tokio::net::UnixStream::connect(socket_path),
            )
            .await;
            return match stream {
                Ok(Ok(stream)) => Ok((dns_lookup, Stream::Unix(stream))),
                Ok(Err(err)) => Err(ClientError::Io(err)),
                Err(_) => Err(ClientError::Timeout),
            };
        }
        #[cfg(feature = "vsock")]
        if let Some(addr) = self.vsock_addr {
            let dns_lookup = Instant::now();
            let stream =
                tokio::time::timeout(timeout_duration, tokio_vsock::VsockStream::connect(addr))
                    .await;
            return match stream {
                Ok(Ok(stream)) => Ok((dns_lookup, Stream::Vsock(stream))),
                Ok(Err(err)) => Err(ClientError::Io(err)),
                Err(_) => Err(ClientError::Timeout),
            };
        }
        // HTTP
        let addr = self.dns.lookup(url, rng).await?;
        let dns_lookup = Instant::now();
        let stream =
            tokio::time::timeout(timeout_duration, tokio::net::TcpStream::connect(addr)).await;
        match stream {
            Ok(Ok(stream)) => {
                stream.set_nodelay(true)?;
                Ok((dns_lookup, Stream::Tcp(stream)))
            }
            Ok(Err(err)) => Err(ClientError::Io(err)),
            Err(_) => Err(ClientError::Timeout),
        }
    }

    async fn tls_client(
        &self,
        addr: (std::net::IpAddr, u16),
        url: &Url,
        http_version: http::Version,
    ) -> Result<Stream, ClientError> {
        let stream = tokio::net::TcpStream::connect(addr).await?;
        stream.set_nodelay(true)?;

        let stream = self.connect_tls(stream, url, http_version).await?;

        Ok(Stream::Tls(stream))
    }

    #[cfg(all(feature = "native-tls", not(feature = "rustls")))]
    async fn connect_tls<S>(
        &self,
        stream: S,
        url: &Url,
        http_version: http::Version,
    ) -> Result<tokio_native_tls::TlsStream<S>, ClientError>
    where
        S: AsyncRead + AsyncWrite + Unpin,
    {
        let connector = self
            .native_tls_connectors
            .connector(http_version >= http::Version::HTTP_2);
        let stream = connector
            .connect(url.host_str().ok_or(ClientError::HostNotFound)?, stream)
            .await?;

        Ok(stream)
    }

    #[cfg(feature = "rustls")]
    async fn connect_tls<S>(
        &self,
        stream: S,
        url: &Url,
        http_version: http::Version,
    ) -> Result<Box<tokio_rustls::client::TlsStream<S>>, ClientError>
    where
        S: AsyncRead + AsyncWrite + Unpin,
    {
        let connector =
            tokio_rustls::TlsConnector::from(self.rustls_configs.config(http_version).clone());
        let domain = rustls_pki_types::ServerName::try_from(
            url.host_str().ok_or(ClientError::HostNotFound)?,
        )?;
        let stream = connector.connect(domain.to_owned(), stream).await?;

        Ok(Box::new(stream))
    }

    async fn client_http1<R: Rng>(
        &self,
        url: &Url,
        rng: &mut R,
    ) -> Result<(Instant, SendRequestHttp1), ClientError> {
        if let Some(proxy_url) = &self.proxy_url {
            let http_proxy_version = if self.is_proxy_http2() {
                http::Version::HTTP_2
            } else {
                http::Version::HTTP_11
            };
            let (dns_lookup, stream) = self.client(proxy_url, rng, http_proxy_version).await?;
            if url.scheme() == "https" {
                let requested_host = url.host_str().ok_or(ClientError::HostNotFound)?;
                let requested_port = url
                    .port_or_known_default()
                    .ok_or(ClientError::PortNotFound)?;
                let (connect_host, connect_port) = if let Some(entry) =
                    self.dns
                        .select_connect_to(requested_host, requested_port, rng)
                {
                    (entry.target_host.as_str(), entry.target_port)
                } else {
                    (requested_host, requested_port)
                };
                let connect_authority = format_host_port(connect_host, connect_port);
                // Do CONNECT request to proxy
                let req = {
                    let mut builder = http::Request::builder()
                        .method(Method::CONNECT)
                        .uri(connect_authority);
                    *builder
                        .headers_mut()
                        .ok_or(ClientError::GetHeaderFromBuilder)? = self.proxy_headers.clone();
                    builder.body(http_body_util::Full::default())?
                };
                let res = if self.proxy_http_version == http::Version::HTTP_2 {
                    let mut send_request = stream.handshake_http2().await?;
                    send_request.send_request(req).await?
                } else {
                    let mut send_request = stream.handshake_http1(true).await?;
                    send_request.send_request(req).await?
                };
                let stream = hyper::upgrade::on(res).await?;
                let stream = self
                    .connect_tls(TokioIo::new(stream), url, self.request_generator.version)
                    .await?;
                let (send_request, conn) =
                    hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
                tokio::spawn(conn);
                Ok((dns_lookup, send_request))
            } else {
                // Send full URL in request() for HTTP proxy
                Ok((dns_lookup, stream.handshake_http1(false).await?))
            }
        } else {
            let (dns_lookup, stream) = self.client(url, rng, http::Version::HTTP_11).await?;
            Ok((dns_lookup, stream.handshake_http1(false).await?))
        }
    }

    async fn work_http1(
        &self,
        client_state: &mut ClientStateHttp1,
    ) -> Result<RequestResult, ClientError> {
        let do_req = async {
            let (url, request, rng) = self.generate_request(&mut client_state.rng)?;
            let mut start = std::time::Instant::now();
            let mut first_byte: Option<std::time::Instant> = None;
            let mut connection_time: Option<ConnectionTime> = None;

            let mut send_request = if let Some(send_request) = client_state.send_request.take() {
                send_request
            } else {
                let (dns_lookup, send_request) =
                    self.client_http1(&url, &mut client_state.rng).await?;
                let dialup = std::time::Instant::now();

                connection_time = Some(ConnectionTime {
                    dns_lookup: dns_lookup - start,
                    dialup: dialup - start,
                });
                send_request
            };
            while send_request.ready().await.is_err() {
                // This gets hit when the connection for HTTP/1.1 faults
                // This re-connects
                start = std::time::Instant::now();
                let (dns_lookup, send_request_) =
                    self.client_http1(&url, &mut client_state.rng).await?;
                send_request = send_request_;
                let dialup = std::time::Instant::now();
                connection_time = Some(ConnectionTime {
                    dns_lookup: dns_lookup - start,
                    dialup: dialup - start,
                });
            }
            match send_request.send_request(request).await {
                Ok(res) => {
                    let (parts, mut stream) = res.into_parts();
                    let mut status = parts.status;

                    let mut len_bytes = 0;
                    while let Some(chunk) = stream.frame().await {
                        if first_byte.is_none() {
                            first_byte = Some(std::time::Instant::now())
                        }
                        len_bytes += chunk?.data_ref().map(|d| d.len()).unwrap_or_default();
                    }

                    if self.redirect_limit != 0 {
                        if let Some(location) = parts.headers.get("Location") {
                            let (send_request_redirect, new_status, len) = self
                                .redirect(
                                    url,
                                    rng,
                                    send_request,
                                    location,
                                    self.redirect_limit,
                                    &mut client_state.rng,
                                )
                                .await?;

                            send_request = send_request_redirect;
                            status = new_status;
                            len_bytes = len;
                        }
                    }

                    let end = std::time::Instant::now();

                    let result = RequestResult {
                        rng,
                        start_latency_correction: None,
                        start,
                        first_byte,
                        end,
                        status,
                        len_bytes,
                        connection_time,
                    };

                    if !self.disable_keepalive {
                        client_state.send_request = Some(send_request);
                    }

                    Ok::<_, ClientError>(result)
                }
                Err(e) => {
                    client_state.send_request = Some(send_request);
                    Err(e.into())
                }
            }
        };

        if let Some(timeout) = self.timeout {
            tokio::select! {
                res = do_req => {
                    res
                }
                _ = tokio::time::sleep(timeout) => {
                    Err(ClientError::Timeout)
                }
            }
        } else {
            do_req.await
        }
    }

    async fn connect_http2<R: Rng>(
        &self,
        url: &Url,
        rng: &mut R,
    ) -> Result<(ConnectionTime, SendRequestHttp2), ClientError> {
        let start = std::time::Instant::now();
        if let Some(proxy_url) = &self.proxy_url {
            let http_proxy_version = if self.is_proxy_http2() {
                http::Version::HTTP_2
            } else {
                http::Version::HTTP_11
            };
            let (dns_lookup, stream) = self.client(proxy_url, rng, http_proxy_version).await?;
            if url.scheme() == "https" {
                let requested_host = url.host_str().ok_or(ClientError::HostNotFound)?;
                let requested_port = url
                    .port_or_known_default()
                    .ok_or(ClientError::PortNotFound)?;
                let (connect_host, connect_port) = if let Some(entry) =
                    self.dns
                        .select_connect_to(requested_host, requested_port, rng)
                {
                    (entry.target_host.as_str(), entry.target_port)
                } else {
                    (requested_host, requested_port)
                };
                let connect_authority = format_host_port(connect_host, connect_port);
                let req = {
                    let mut builder = http::Request::builder()
                        .method(Method::CONNECT)
                        .uri(connect_authority);
                    *builder
                        .headers_mut()
                        .ok_or(ClientError::GetHeaderFromBuilder)? = self.proxy_headers.clone();
                    builder.body(http_body_util::Full::default())?
                };
                let res = if self.proxy_http_version == http::Version::HTTP_2 {
                    let mut send_request = stream.handshake_http2().await?;
                    send_request.send_request(req).await?
                } else {
                    let mut send_request = stream.handshake_http1(true).await?;
                    send_request.send_request(req).await?
                };
                let stream = hyper::upgrade::on(res).await?;
                let stream = self
                    .connect_tls(TokioIo::new(stream), url, http::Version::HTTP_2)
                    .await?;
                let (send_request, conn) =
                    hyper::client::conn::http2::Builder::new(TokioExecutor::new())
                        // from nghttp2's default
                        .initial_stream_window_size((1 << 30) - 1)
                        .initial_connection_window_size((1 << 30) - 1)
                        .handshake(TokioIo::new(stream))
                        .await?;
                tokio::spawn(conn);
                let dialup = std::time::Instant::now();

                Ok((
                    ConnectionTime {
                        dns_lookup: dns_lookup - start,
                        dialup: dialup - start,
                    },
                    send_request,
                ))
            } else {
                let send_request = stream.handshake_http2().await?;
                let dialup = std::time::Instant::now();
                Ok((
                    ConnectionTime {
                        dns_lookup: dns_lookup - start,
                        dialup: dialup - start,
                    },
                    send_request,
                ))
            }
        } else {
            let (dns_lookup, stream) = self
                .client(url, rng, self.request_generator.version)
                .await?;
            let send_request = stream.handshake_http2().await?;
            let dialup = std::time::Instant::now();
            Ok((
                ConnectionTime {
                    dns_lookup: dns_lookup - start,
                    dialup: dialup - start,
                },
                send_request,
            ))
        }
    }

    async fn work_http2(
        &self,
        client_state: &mut ClientStateHttp2,
    ) -> Result<RequestResult, ClientError> {
        let do_req = async {
            let (_url, request, rng) = self.generate_request(&mut client_state.rng)?;
            let start = std::time::Instant::now();
            let mut first_byte: Option<std::time::Instant> = None;
            let connection_time: Option<ConnectionTime> = None;

            match client_state.send_request.send_request(request).await {
                Ok(res) => {
                    let (parts, mut stream) = res.into_parts();
                    let status = parts.status;

                    let mut len_bytes = 0;
                    while let Some(chunk) = stream.frame().await {
                        if first_byte.is_none() {
                            first_byte = Some(std::time::Instant::now())
                        }
                        len_bytes += chunk?.data_ref().map(|d| d.len()).unwrap_or_default();
                    }

                    let end = std::time::Instant::now();

                    let result = RequestResult {
                        rng,
                        start_latency_correction: None,
                        start,
                        first_byte,
                        end,
                        status,
                        len_bytes,
                        connection_time,
                    };

                    Ok::<_, ClientError>(result)
                }
                Err(e) => Err(e.into()),
            }
        };

        if let Some(timeout) = self.timeout {
            tokio::select! {
                res = do_req => {
                    res
                }
                _ = tokio::time::sleep(timeout) => {
                    Err(ClientError::Timeout)
                }
            }
        } else {
            do_req.await
        }
    }

    #[allow(clippy::type_complexity)]
    async fn redirect<R: Rng + Send + Copy>(
        &self,
        base_url: Cow<'_, Url>,
        seed: R,
        send_request: SendRequestHttp1,
        location: &http::header::HeaderValue,
        limit: usize,
        rng: &mut R,
    ) -> Result<(SendRequestHttp1, http::StatusCode, usize), ClientError> {
        if limit == 0 {
            return Err(ClientError::TooManyRedirect);
        }
        let url = match Url::parse(location.to_str()?) {
            Ok(url) => url,
            Err(ParseError::RelativeUrlWithoutBase) => Url::options()
                .base_url(Some(&base_url))
                .parse(location.to_str()?)?,
            Err(err) => Err(err)?,
        };

        let (mut send_request, send_request_base) =
            if base_url.authority() == url.authority() && !self.disable_keepalive {
                // reuse connection
                (send_request, None)
            } else {
                let (_dns_lookup, stream) = self.client_http1(&url, rng).await?;
                (stream, Some(send_request))
            };

        while send_request.ready().await.is_err() {
            let (_dns_lookup, stream) = self.client_http1(&url, rng).await?;
            send_request = stream;
        }

        let mut request = self.generate_request(&mut seed.clone())?.1;
        if url.authority() != base_url.authority() {
            request.headers_mut().insert(
                http::header::HOST,
                http::HeaderValue::from_str(url.authority())?,
            );
        }
        *request.uri_mut() = if self.proxy_url.is_some() && url.scheme() == "http" {
            // Full URL in request() for HTTP proxy
            url.as_str().parse()?
        } else {
            url[url::Position::BeforePath..].parse()?
        };
        if self.proxy_url.is_some() && request.uri().scheme_str() == Some("http") {
            if let Some(authority) = request.uri().authority() {
                let requested_host = authority.host();
                let requested_port = authority.port_u16().unwrap_or(80);
                if let Some(entry) = self
                    .dns
                    .select_connect_to(requested_host, requested_port, rng)
                {
                    let new_authority: http::uri::Authority =
                        format_host_port(entry.target_host.as_str(), entry.target_port).parse()?;
                    let mut parts = request.uri().clone().into_parts();
                    parts.authority = Some(new_authority);
                    let new_uri = http::Uri::from_parts(parts)?;
                    *request.uri_mut() = new_uri;
                }
            }
        }
        let res = send_request.send_request(request).await?;
        let (parts, mut stream) = res.into_parts();
        let mut status = parts.status;

        let mut len_bytes = 0;
        while let Some(chunk) = stream.frame().await {
            len_bytes += chunk?.data_ref().map(|d| d.len()).unwrap_or_default();
        }

        if let Some(location) = parts.headers.get("Location") {
            let (send_request_redirect, new_status, len) =
                Box::pin(self.redirect(base_url, seed, send_request, location, limit - 1, rng))
                    .await?;
            send_request = send_request_redirect;
            status = new_status;
            len_bytes = len;
        }

        if let Some(send_request_base) = send_request_base {
            Ok((send_request_base, status, len_bytes))
        } else {
            Ok((send_request, status, len_bytes))
        }
    }
}

/// Check error and decide whether to cancel the connection
pub(crate) fn is_cancel_error(res: &Result<RequestResult, ClientError>) -> bool {
    matches!(res, Err(ClientError::Deadline)) || is_too_many_open_files(res)
}

/// Check error was "Too many open file"
fn is_too_many_open_files(res: &Result<RequestResult, ClientError>) -> bool {
    res.as_ref()
        .err()
        .map(|err| match err {
            ClientError::Io(io_error) => io_error.raw_os_error() == Some(libc::EMFILE),
            _ => false,
        })
        .unwrap_or(false)
}

/// Check error was any Hyper error (primarily for HTTP2 connection errors)
fn is_hyper_error(res: &Result<RequestResult, ClientError>) -> bool {
    res.as_ref()
        .err()
        .map(|err| match err {
            // REVIEW: IoErrors, if indicating the underlying connection has failed,
            // should also cause a stop of HTTP2 requests
            ClientError::Io(_) => true,
            ClientError::Hyper(_) => true,
            _ => false,
        })
        .unwrap_or(false)
}

async fn setup_http2<R: Rng>(
    client: &Client,
    rng: &mut R,
) -> Result<(ConnectionTime, SendRequestHttp2), ClientError> {
    let url = client.request_generator.url_generator.generate(rng)?;
    let (connection_time, send_request) = client.connect_http2(&url, rng).await?;

    Ok((connection_time, send_request))
}

async fn work_http2_once(
    client: &Client,
    client_state: &mut ClientStateHttp2,
    report_tx: &kanal::Sender<Result<RequestResult, ClientError>>,
    connection_time: ConnectionTime,
    start_latency_correction: Option<Instant>,
) -> (bool, bool) {
    let mut res = client.work_http2(client_state).await;
    let is_cancel = is_cancel_error(&res);
    let is_reconnect = is_hyper_error(&res);
    set_connection_time(&mut res, connection_time);
    if let Some(start_latency_correction) = start_latency_correction {
        set_start_latency_correction(&mut res, start_latency_correction);
    }
    report_tx.send(res).unwrap();
    (is_cancel, is_reconnect)
}

pub(crate) fn set_connection_time<E>(
    res: &mut Result<RequestResult, E>,
    connection_time: ConnectionTime,
) {
    if let Ok(res) = res {
        res.connection_time = Some(connection_time);
    }
}

pub(crate) fn set_start_latency_correction<E>(
    res: &mut Result<RequestResult, E>,
    start_latency_correction: std::time::Instant,
) {
    if let Ok(res) = res {
        res.start_latency_correction = Some(start_latency_correction);
    }
}

pub async fn work_debug<W: Write>(w: &mut W, client: Arc<Client>) -> Result<(), ClientError> {
    let mut rng = Pcg64Si::from_rng(&mut rng());
    let (url, request, _) = client.generate_request(&mut rng)?;

    writeln!(w, "{request:#?}")?;

    let response = match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => {
            let (_, (h3_connection, client_state)) = client.connect_http3(&url, &mut rng).await?;

            send_debug_request_http3(h3_connection, client_state, request).await?
        }
        HttpWorkType::H2 => {
            let (_, mut client_state) = client.connect_http2(&url, &mut rng).await?;
            let response = client_state.send_request(request).await?;
            let (parts, body) = response.into_parts();
            let body = body.collect().await.unwrap().to_bytes();

            http::Response::from_parts(parts, body)
        }
        HttpWorkType::H1 => {
            let (_dns_lookup, mut send_request) = client.client_http1(&url, &mut rng).await?;

            let response = send_request.send_request(request).await?;
            let (parts, body) = response.into_parts();
            let body = body.collect().await.unwrap().to_bytes();

            http::Response::from_parts(parts, body)
        }
    };

    writeln!(w, "{response:#?}")?;

    Ok(())
}

/// Run n tasks by m workers
pub async fn work(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    n_tasks: usize,
    n_connections: usize,
    n_http2_parallel: usize,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work(client, report_tx, n_tasks, n_connections, n_http2_parallel).await;
        return;
    }

    use std::sync::atomic::{AtomicUsize, Ordering};
    let counter = Arc::new(AtomicUsize::new(0));

    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let report_tx = report_tx.clone();
                    let counter = counter.clone();
                    let client = client.clone();
                    tokio::spawn(async move {
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    let futures = (0..n_http2_parallel)
                                        .map(|_| {
                                            let report_tx = report_tx.clone();
                                            let counter = counter.clone();
                                            let client = client.clone();

                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            tokio::spawn(async move {
                                                while counter.fetch_add(1, Ordering::Relaxed)
                                                    < n_tasks
                                                {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            None,
                                                        )
                                                        .await;

                                                    if is_cancel || is_reconnect {
                                                        return is_cancel;
                                                    }
                                                }

                                                true
                                            })
                                        })
                                        .collect::<Vec<_>>();

                                    let mut connection_gone = false;
                                    for f in futures {
                                        match f.await {
                                            Ok(true) => {
                                                // All works done
                                                connection_gone = true;
                                            }
                                            Err(_) => {
                                                // Unexpected
                                                connection_gone = true;
                                            }
                                            _ => {}
                                        }
                                    }

                                    if connection_gone {
                                        return;
                                    }
                                }
                                Err(err) => {
                                    if counter.fetch_add(1, Ordering::Relaxed) < n_tasks {
                                        report_tx.send(Err(err)).unwrap();
                                    } else {
                                        return;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();
            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let report_tx = report_tx.clone();
                    let counter = counter.clone();
                    let client = client.clone();
                    tokio::spawn(async move {
                        let mut client_state = ClientStateHttp1::default();
                        while counter.fetch_add(1, Ordering::Relaxed) < n_tasks {
                            let res = client.work_http1(&mut client_state).await;
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();
            for f in futures {
                let _ = f.await;
            }
        }
    };
}

/// n tasks by m workers limit to qps works in a second
pub async fn work_with_qps(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    n_tasks: usize,
    n_connections: usize,
    n_http_parallel: usize,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work_with_qps(
            client,
            report_tx,
            query_limit,
            n_tasks,
            n_connections,
            n_http_parallel,
        )
        .await;
        return;
    }

    let (tx, rx) = kanal::unbounded::<()>();

    let work_queue = async move {
        match query_limit {
            QueryLimit::Qps(qps) => {
                let start = std::time::Instant::now();
                for i in 0..n_tasks {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    tx.send(())?;
                }
            }
            QueryLimit::Burst(duration, rate) => {
                let mut n = 0;
                // Handle via rate till n_tasks out of bound
                while n + rate < n_tasks {
                    tokio::time::sleep(duration).await;
                    for _ in 0..rate {
                        tx.send(())?;
                    }
                    n += rate;
                }
                // Handle the remaining tasks
                if n_tasks > n {
                    tokio::time::sleep(duration).await;
                    for _ in 0..n_tasks - n {
                        tx.send(())?;
                    }
                }
            }
        }
        // tx gone
        drop(tx);
        Ok::<(), kanal::SendError>(())
    };

    let rx = rx.to_async();
    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let client = client.clone();
                    tokio::spawn(async move {
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    let futures = (0..n_http_parallel)
                                        .map(|_| {
                                            let report_tx = report_tx.clone();
                                            let rx = rx.clone();
                                            let client = client.clone();
                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            tokio::spawn(async move {
                                                while let Ok(()) = rx.recv().await {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            None,
                                                        )
                                                        .await;

                                                    if is_cancel || is_reconnect {
                                                        return is_cancel;
                                                    }
                                                }
                                                true
                                            })
                                        })
                                        .collect::<Vec<_>>();
                                    let mut connection_gone = false;
                                    for f in futures {
                                        match f.await {
                                            Ok(true) => {
                                                // All works done
                                                connection_gone = true;
                                            }
                                            Err(_) => {
                                                // Unexpected
                                                connection_gone = true;
                                            }
                                            _ => {}
                                        }
                                    }
                                    if connection_gone {
                                        return;
                                    }
                                }
                                Err(err) => {
                                    // Consume a task
                                    if let Ok(()) = rx.recv().await {
                                        report_tx.send(Err(err)).unwrap();
                                    } else {
                                        return;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            work_queue.await.unwrap();
            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let client = client.clone();
                    tokio::spawn(async move {
                        let mut client_state = ClientStateHttp1::default();
                        while let Ok(()) = rx.recv().await {
                            let res = client.work_http1(&mut client_state).await;
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            work_queue.await.unwrap();
            for f in futures {
                let _ = f.await;
            }
        }
    };
}

/// n tasks by m workers limit to qps works in a second with latency correction
pub async fn work_with_qps_latency_correction(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    n_tasks: usize,
    n_connections: usize,
    n_http2_parallel: usize,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work_with_qps_latency_correction(
            client,
            report_tx,
            query_limit,
            n_tasks,
            n_connections,
            n_http2_parallel,
        )
        .await;
        return;
    }

    let (tx, rx) = kanal::unbounded();

    let work_queue = async move {
        match query_limit {
            QueryLimit::Qps(qps) => {
                let start = std::time::Instant::now();
                for i in 0..n_tasks {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let now = std::time::Instant::now();
                    tx.send(now)?;
                }
            }
            QueryLimit::Burst(duration, rate) => {
                let mut n = 0;
                // Handle via rate till n_tasks out of bound
                while n + rate < n_tasks {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    for _ in 0..rate {
                        tx.send(now)?;
                    }
                    n += rate;
                }
                // Handle the remaining tasks
                if n_tasks > n {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    for _ in 0..n_tasks - n {
                        tx.send(now)?;
                    }
                }
            }
        }

        // tx gone
        drop(tx);
        Ok::<(), kanal::SendError>(())
    };

    let rx = rx.to_async();
    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let client = client.clone();
                    tokio::spawn(async move {
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    let futures = (0..n_http2_parallel)
                                        .map(|_| {
                                            let report_tx = report_tx.clone();
                                            let rx = rx.clone();
                                            let client = client.clone();
                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            tokio::spawn(async move {
                                                while let Ok(start) = rx.recv().await {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            Some(start),
                                                        )
                                                        .await;

                                                    if is_cancel || is_reconnect {
                                                        return is_cancel;
                                                    }
                                                }
                                                true
                                            })
                                        })
                                        .collect::<Vec<_>>();
                                    let mut connection_gone = false;
                                    for f in futures {
                                        match f.await {
                                            Ok(true) => {
                                                // All works done
                                                connection_gone = true;
                                            }
                                            Err(_) => {
                                                // Unexpected
                                                connection_gone = true;
                                            }
                                            _ => {}
                                        }
                                    }
                                    if connection_gone {
                                        return;
                                    }
                                }
                                Err(err) => {
                                    // Consume a task
                                    if rx.recv().await.is_ok() {
                                        report_tx.send(Err(err)).unwrap();
                                    } else {
                                        return;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            work_queue.await.unwrap();
            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let mut client_state = ClientStateHttp1::default();
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    tokio::spawn(async move {
                        while let Ok(start) = rx.recv().await {
                            let mut res = client.work_http1(&mut client_state).await;
                            set_start_latency_correction(&mut res, start);
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            work_queue.await.unwrap();
            for f in futures {
                let _ = f.await;
            }
        }
    }
}

/// Run until dead_line by n workers
pub async fn work_until(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http_parallel: usize,
    wait_ongoing_requests_after_deadline: bool,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work_until(
            client,
            report_tx,
            dead_line,
            n_connections,
            n_http_parallel,
            wait_ongoing_requests_after_deadline,
        )
        .await;
        return;
    }

    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            // Using semaphore to control the deadline
            // Maybe there is a better concurrent primitive to do this
            let s = Arc::new(tokio::sync::Semaphore::new(0));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let report_tx = report_tx.clone();
                    let s = s.clone();
                    tokio::spawn(async move {
                        let s = s.clone();
                        // Keep trying to establish or re-establish connections up to the deadline
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    // Setup the parallel workers for each HTTP2 connection
                                    let futures = (0..n_http_parallel)
                                        .map(|_| {
                                            let client = client.clone();
                                            let report_tx = report_tx.clone();
                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            let s = s.clone();
                                            tokio::spawn(async move {
                                                // This is where HTTP2 loops to make all the requests for a given client and worker
                                                loop {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            None,
                                                        )
                                                        .await;

                                                    let is_cancel = is_cancel || s.is_closed();
                                                    if is_cancel || is_reconnect {
                                                        break is_cancel;
                                                    }
                                                }
                                            })
                                        })
                                        .collect::<Vec<_>>();

                                    let mut connection_gone = false;
                                    for f in futures {
                                        tokio::select! {
                                            r = f => {
                                                match r {
                                                    Ok(true) => {
                                                        // All works done
                                                        connection_gone = true;
                                                    }
                                                    Err(_) => {
                                                        // Unexpected
                                                        connection_gone = true;
                                                    }
                                                    _ => {}
                                                }
                                            }
                                            _ = s.acquire() => {
                                                report_tx.send(Err(ClientError::Deadline)).unwrap();
                                                connection_gone = true;
                                            }
                                        }
                                    }
                                    if connection_gone {
                                        return;
                                    }
                                }

                                Err(err) => {
                                    report_tx.send(Err(err)).unwrap();
                                    if s.is_closed() {
                                        break;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            s.close();

            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let is_end = Arc::new(AtomicBool::new(false));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let report_tx = report_tx.clone();
                    let mut client_state = ClientStateHttp1::default();
                    let is_end = is_end.clone();
                    tokio::spawn(async move {
                        loop {
                            let res = client.work_http1(&mut client_state).await;
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel || is_end.load(Relaxed) {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            is_end.store(true, Relaxed);

            if wait_ongoing_requests_after_deadline {
                for f in futures {
                    let _ = f.await;
                }
            } else {
                for f in futures {
                    f.abort();
                    if let Err(e) = f.await {
                        if e.is_cancelled() {
                            report_tx.send(Err(ClientError::Deadline)).unwrap();
                        }
                    }
                }
            }
        }
    };
}

/// Run until dead_line by n workers limit to qps works in a second
#[allow(clippy::too_many_arguments)]
pub async fn work_until_with_qps(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    start: std::time::Instant,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http2_parallel: usize,
    wait_ongoing_requests_after_deadline: bool,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work_until_with_qps(
            client,
            report_tx,
            query_limit,
            start,
            dead_line,
            n_connections,
            n_http2_parallel,
            wait_ongoing_requests_after_deadline,
        )
        .await;
        return;
    }

    let rx = match query_limit {
        QueryLimit::Qps(qps) => {
            let (tx, rx) = kanal::unbounded::<()>();
            tokio::spawn(async move {
                for i in 0.. {
                    if std::time::Instant::now() > dead_line {
                        break;
                    }
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let _ = tx.send(());
                }
                // tx gone
            });
            rx
        }
        QueryLimit::Burst(duration, rate) => {
            let (tx, rx) = kanal::unbounded();
            tokio::spawn(async move {
                // Handle via rate till deadline is reached
                for _ in 0.. {
                    if std::time::Instant::now() > dead_line {
                        break;
                    }

                    tokio::time::sleep(duration).await;
                    for _ in 0..rate {
                        let _ = tx.send(());
                    }
                }
                // tx gone
            });
            rx
        }
    };

    let rx = rx.to_async();
    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            let s = Arc::new(tokio::sync::Semaphore::new(0));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let s = s.clone();
                    tokio::spawn(async move {
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    let futures = (0..n_http2_parallel)
                                        .map(|_| {
                                            let client = client.clone();
                                            let report_tx = report_tx.clone();
                                            let rx = rx.clone();
                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            let s = s.clone();
                                            tokio::spawn(async move {
                                                while let Ok(()) = rx.recv().await {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            None,
                                                        )
                                                        .await;

                                                    let is_cancel = is_cancel || s.is_closed();
                                                    if is_cancel || is_reconnect {
                                                        return is_cancel;
                                                    }
                                                }
                                                true
                                            })
                                        })
                                        .collect::<Vec<_>>();
                                    let mut connection_gone = false;
                                    for f in futures {
                                        tokio::select! {
                                            r = f => {
                                                match r {
                                                    Ok(true) => {
                                                        // All works done
                                                        connection_gone = true;
                                                    }
                                                    Err(_) => {
                                                        // Unexpected
                                                        connection_gone = true;
                                                    }
                                                    _ => {}
                                                }
                                            }
                                            _ = s.acquire() => {
                                                report_tx.send(Err(ClientError::Deadline)).unwrap();
                                                connection_gone = true;
                                            }
                                        }
                                    }
                                    if connection_gone {
                                        return;
                                    }
                                }
                                Err(err) => {
                                    // Consume a task
                                    if rx.recv().await.is_ok() {
                                        report_tx.send(Err(err)).unwrap();
                                    } else {
                                        return;
                                    }

                                    if s.is_closed() {
                                        return;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            s.close();

            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let is_end = Arc::new(AtomicBool::new(false));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let mut client_state = ClientStateHttp1::default();
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let is_end = is_end.clone();
                    tokio::spawn(async move {
                        while let Ok(()) = rx.recv().await {
                            let res = client.work_http1(&mut client_state).await;
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel || is_end.load(Relaxed) {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            is_end.store(true, Relaxed);

            if wait_ongoing_requests_after_deadline {
                for f in futures {
                    let _ = f.await;
                }
            } else {
                for f in futures {
                    f.abort();
                    if let Err(e) = f.await {
                        if e.is_cancelled() {
                            report_tx.send(Err(ClientError::Deadline)).unwrap();
                        }
                    }
                }
            }
        }
    }
}

/// Run until dead_line by n workers limit to qps works in a second with latency correction
#[allow(clippy::too_many_arguments)]
pub async fn work_until_with_qps_latency_correction(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    start: std::time::Instant,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http2_parallel: usize,
    wait_ongoing_requests_after_deadline: bool,
) {
    #[cfg(feature = "http3")]
    if matches!(client.work_type(), HttpWorkType::H3) {
        crate::client_h3::work_until_with_qps_latency_correction(
            client,
            report_tx,
            query_limit,
            start,
            dead_line,
            n_connections,
            n_http2_parallel,
            wait_ongoing_requests_after_deadline,
        )
        .await;
        return;
    }

    let (tx, rx) = kanal::unbounded();
    match query_limit {
        QueryLimit::Qps(qps) => {
            tokio::spawn(async move {
                for i in 0.. {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let now = std::time::Instant::now();
                    if now > dead_line {
                        break;
                    }
                    let _ = tx.send(now);
                }
                // tx gone
            });
        }
        QueryLimit::Burst(duration, rate) => {
            tokio::spawn(async move {
                // Handle via rate till deadline is reached
                loop {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    if now > dead_line {
                        break;
                    }

                    for _ in 0..rate {
                        let _ = tx.send(now);
                    }
                }
                // tx gone
            });
        }
    };

    let rx = rx.to_async();
    match client.work_type() {
        #[cfg(feature = "http3")]
        HttpWorkType::H3 => unreachable!(),
        HttpWorkType::H2 => {
            let s = Arc::new(tokio::sync::Semaphore::new(0));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let s = s.clone();
                    tokio::spawn(async move {
                        let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                        loop {
                            match setup_http2(&client, &mut rng).await {
                                Ok((connection_time, send_request)) => {
                                    let futures = (0..n_http2_parallel)
                                        .map(|_| {
                                            let client = client.clone();
                                            let report_tx = report_tx.clone();
                                            let rx = rx.clone();
                                            let mut client_state = ClientStateHttp2 {
                                                rng: SeedableRng::from_rng(&mut rand::rng()),
                                                send_request: send_request.clone(),
                                            };
                                            let s = s.clone();
                                            tokio::spawn(async move {
                                                while let Ok(start) = rx.recv().await {
                                                    let (is_cancel, is_reconnect) =
                                                        work_http2_once(
                                                            &client,
                                                            &mut client_state,
                                                            &report_tx,
                                                            connection_time,
                                                            Some(start),
                                                        )
                                                        .await;
                                                    let is_cancel = is_cancel || s.is_closed();
                                                    if is_cancel || is_reconnect {
                                                        return is_cancel;
                                                    }
                                                }
                                                true
                                            })
                                        })
                                        .collect::<Vec<_>>();
                                    let mut connection_gone = false;
                                    for f in futures {
                                        tokio::select! {
                                            r = f => {
                                                match r {
                                                    Ok(true) => {
                                                        // All works done
                                                        connection_gone = true;
                                                    }
                                                    Err(_) => {
                                                        // Unexpected
                                                        connection_gone = true;
                                                    }
                                                    _ => {}
                                                }
                                            }
                                            _ = s.acquire() => {
                                                report_tx.send(Err(ClientError::Deadline)).unwrap();
                                                connection_gone = true;
                                            }
                                        }
                                    }
                                    if connection_gone {
                                        return;
                                    }
                                }

                                Err(err) => {
                                    if rx.recv().await.is_ok() {
                                        report_tx.send(Err(err)).unwrap();
                                    } else {
                                        return;
                                    }

                                    if s.is_closed() {
                                        return;
                                    }
                                }
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            s.close();

            for f in futures {
                let _ = f.await;
            }
        }
        HttpWorkType::H1 => {
            let is_end = Arc::new(AtomicBool::new(false));

            let futures = (0..n_connections)
                .map(|_| {
                    let client = client.clone();
                    let mut client_state = ClientStateHttp1::default();
                    let report_tx = report_tx.clone();
                    let rx = rx.clone();
                    let is_end = is_end.clone();
                    tokio::spawn(async move {
                        while let Ok(start) = rx.recv().await {
                            let mut res = client.work_http1(&mut client_state).await;
                            set_start_latency_correction(&mut res, start);
                            let is_cancel = is_cancel_error(&res);
                            report_tx.send(res).unwrap();
                            if is_cancel || is_end.load(Relaxed) {
                                break;
                            }
                        }
                    })
                })
                .collect::<Vec<_>>();

            tokio::time::sleep_until(dead_line.into()).await;
            is_end.store(true, Relaxed);

            if wait_ongoing_requests_after_deadline {
                for f in futures {
                    let _ = f.await;
                }
            } else {
                for f in futures {
                    f.abort();
                    if let Err(e) = f.await {
                        if e.is_cancelled() {
                            report_tx.send(Err(ClientError::Deadline)).unwrap();
                        }
                    }
                }
            }
        }
    }
}

/// Optimized workers for `--no-tui` mode
pub mod fast {
    use std::sync::{
        Arc,
        atomic::{AtomicBool, AtomicIsize, Ordering},
    };

    use rand::SeedableRng;

    use crate::{
        client::{
            ClientError, ClientStateHttp1, ClientStateHttp2, HttpWorkType, is_cancel_error,
            is_hyper_error, set_connection_time, setup_http2,
        },
        pcg64si::Pcg64Si,
        result_data::ResultData,
    };

    use super::Client;

    /// Run n tasks by m workers
    pub async fn work(
        client: Arc<Client>,
        report_tx: kanal::Sender<ResultData>,
        n_tasks: usize,
        n_connections: usize,
        n_http_parallel: usize,
    ) {
        #[cfg(feature = "http3")]
        if matches!(client.work_type(), HttpWorkType::H3) {
            crate::client_h3::fast::work(
                client,
                report_tx,
                n_tasks,
                n_connections,
                n_http_parallel,
            )
            .await;
            return;
        }

        let counter = Arc::new(AtomicIsize::new(n_tasks as isize));
        let num_threads = num_cpus::get_physical();
        let connections = (0..num_threads).filter_map(|i| {
            let num_connection = n_connections / num_threads
                + (if (n_connections % num_threads) > i {
                    1
                } else {
                    0
                });
            if num_connection > 0 {
                Some(num_connection)
            } else {
                None
            }
        });
        let token = tokio_util::sync::CancellationToken::new();

        let handles = match client.work_type() {
            #[cfg(feature = "http3")]
            HttpWorkType::H3 => unreachable!(),
            HttpWorkType::H2 => {
                connections
                    .map(|num_connections| {
                        let report_tx = report_tx.clone();
                        let counter = counter.clone();
                        let client = client.clone();
                        let rt = tokio::runtime::Builder::new_current_thread()
                            .enable_all()
                            .build()
                            .unwrap();
                        let token = token.clone();

                        std::thread::spawn(move || {
                            let client = client.clone();
                            let local = tokio::task::LocalSet::new();
                            for _ in 0..num_connections {
                                let report_tx = report_tx.clone();
                                let counter = counter.clone();
                                let client = client.clone();
                                let token = token.clone();
                                local.spawn_local(Box::pin(async move {
                                    let mut has_err = false;
                                    let mut result_data_err = ResultData::default();
                                    let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                                    loop {
                                        let client = client.clone();
                                        match setup_http2(&client, &mut rng).await {
                                            Ok((connection_time, send_request)) => {
                                                let futures = (0..n_http_parallel)
                                                    .map(|_| {
                                                        let mut client_state = ClientStateHttp2 {
                                                            rng: SeedableRng::from_rng(
                                                                &mut rand::rng(),
                                                            ),
                                                            send_request: send_request.clone(),
                                                        };
                                                        let counter = counter.clone();
                                                        let client = client.clone();
                                                        let report_tx = report_tx.clone();
                                                        let token = token.clone();
                                                        tokio::task::spawn_local(async move {
                                                            let mut result_data =
                                                                ResultData::default();

                                                            let work = async {
                                                                while counter
                                                                    .fetch_sub(1, Ordering::Relaxed)
                                                                    > 0
                                                                {
                                                                    let mut res = client
                                                                        .work_http2(
                                                                            &mut client_state,
                                                                        )
                                                                        .await;
                                                                    let is_cancel =
                                                                        is_cancel_error(&res);
                                                                    let is_reconnect =
                                                                        is_hyper_error(&res);
                                                                    set_connection_time(
                                                                        &mut res,
                                                                        connection_time,
                                                                    );

                                                                    result_data.push(res);

                                                                    if is_cancel || is_reconnect {
                                                                        return is_cancel;
                                                                    }
                                                                }
                                                                true
                                                            };

                                                            let is_cancel = tokio::select! {
                                                                is_cancel = work => {
                                                                    is_cancel
                                                                }
                                                                _ = token.cancelled() => {
                                                                    true
                                                                }
                                                            };

                                                            report_tx.send(result_data).unwrap();
                                                            is_cancel
                                                        })
                                                    })
                                                    .collect::<Vec<_>>();

                                                let mut connection_gone = false;
                                                for f in futures {
                                                    match f.await {
                                                        Ok(true) => {
                                                            // All works done
                                                            connection_gone = true;
                                                        }
                                                        Err(_) => {
                                                            // Unexpected
                                                            connection_gone = true;
                                                        }
                                                        _ => {}
                                                    }
                                                }

                                                if connection_gone {
                                                    break;
                                                }
                                            }
                                            Err(err) => {
                                                if counter.fetch_sub(1, Ordering::Relaxed) > 0 {
                                                    has_err = true;
                                                    result_data_err.push(Err(err));
                                                } else {
                                                    break;
                                                }
                                            }
                                        }
                                    }
                                    if has_err {
                                        report_tx.send(result_data_err).unwrap();
                                    }
                                }));
                            }

                            rt.block_on(local);
                        })
                    })
                    .collect::<Vec<_>>()
            }
            HttpWorkType::H1 => connections
                .map(|num_connection| {
                    let report_tx = report_tx.clone();
                    let counter = counter.clone();
                    let client = client.clone();
                    let rt = tokio::runtime::Builder::new_current_thread()
                        .enable_all()
                        .build()
                        .unwrap();

                    let token = token.clone();
                    std::thread::spawn(move || {
                        let local = tokio::task::LocalSet::new();

                        for _ in 0..num_connection {
                            let report_tx = report_tx.clone();
                            let counter = counter.clone();
                            let client = client.clone();
                            let token = token.clone();
                            local.spawn_local(Box::pin(async move {
                                let mut result_data = ResultData::default();

                                tokio::select! {
                                    _ = token.cancelled() => {}
                                    _ = async {
                                        let mut client_state = ClientStateHttp1::default();
                                        while counter.fetch_sub(1, Ordering::Relaxed) > 0 {
                                            let res = client.work_http1(&mut client_state).await;
                                            let is_cancel = is_cancel_error(&res);
                                            result_data.push(res);
                                            if is_cancel {
                                                break;
                                            }
                                        }
                                    } => {}
                                }
                                report_tx.send(result_data).unwrap();
                            }));
                        }
                        rt.block_on(local);
                    })
                })
                .collect::<Vec<_>>(),
        };

        tokio::spawn(async move {
            tokio::signal::ctrl_c().await.unwrap();
            token.cancel();
        });

        tokio::task::block_in_place(|| {
            for handle in handles {
                let _ = handle.join();
            }
        });
    }

    /// Run until dead_line by n workers
    pub async fn work_until(
        client: Arc<Client>,
        report_tx: kanal::Sender<ResultData>,
        dead_line: std::time::Instant,
        n_connections: usize,
        n_http_parallel: usize,
        wait_ongoing_requests_after_deadline: bool,
    ) {
        #[cfg(feature = "http3")]
        if matches!(client.work_type(), HttpWorkType::H3) {
            crate::client_h3::fast::work_until(
                client,
                report_tx,
                dead_line,
                n_connections,
                n_http_parallel,
                wait_ongoing_requests_after_deadline,
            )
            .await;
            return;
        }

        let num_threads = num_cpus::get_physical();

        let is_end = Arc::new(AtomicBool::new(false));
        let connections = (0..num_threads).filter_map(|i| {
            let num_connection = n_connections / num_threads
                + (if (n_connections % num_threads) > i {
                    1
                } else {
                    0
                });
            if num_connection > 0 {
                Some(num_connection)
            } else {
                None
            }
        });
        let token = tokio_util::sync::CancellationToken::new();
        let handles = match client.work_type() {
            #[cfg(feature = "http3")]
            HttpWorkType::H3 => unreachable!(),
            HttpWorkType::H2 => {
                connections
                .map(|num_connections| {
                    let report_tx = report_tx.clone();
                    let client = client.clone();
                    let rt = tokio::runtime::Builder::new_current_thread()
                        .enable_all()
                        .build()
                        .unwrap();
                    let token = token.clone();
                    let is_end = is_end.clone();

                    std::thread::spawn(move || {
                        let client = client.clone();
                        let local = tokio::task::LocalSet::new();
                        for _ in 0..num_connections {
                            let report_tx = report_tx.clone();
                            let client = client.clone();
                            let token = token.clone();
                            let is_end = is_end.clone();
                            local.spawn_local(Box::pin(async move {
                                let mut has_err = false;
                                let mut result_data_err = ResultData::default();
                                let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
                                loop {
                                    let client = client.clone();
                                    match setup_http2(&client, &mut rng).await {
                                        Ok((connection_time, send_request)) => {
                                            let futures = (0..n_http_parallel)
                                                .map(|_| {
                                                    let mut client_state = ClientStateHttp2 {
                                                        rng: SeedableRng::from_rng(&mut rand::rng()),
                                                        send_request: send_request.clone(),
                                                    };
                                                    let client = client.clone();
                                                    let report_tx = report_tx.clone();
                                                    let token = token.clone();
                                                    let is_end = is_end.clone();
                                                    tokio::task::spawn_local(async move {
                                                        let mut result_data = ResultData::default();

                                                        let work = async {
                                                            loop {
                                                                let mut res = client
                                                                    .work_http2(&mut client_state)
                                                                    .await;
                                                                let is_cancel = is_cancel_error(&res) || is_end.load(Ordering::Relaxed);
                                                                let is_reconnect = is_hyper_error(&res);
                                                                set_connection_time(
                                                                    &mut res,
                                                                    connection_time,
                                                                );

                                                                result_data.push(res);

                                                                if is_cancel || is_reconnect {
                                                                    return is_cancel;
                                                                }
                                                            }
                                                        };

                                                        let is_cancel = tokio::select! {
                                                            is_cancel = work => {
                                                                is_cancel
                                                            }
                                                            _ = token.cancelled() => {
                                                                result_data.push(Err(ClientError::Deadline));
                                                                true
                                                            }
                                                        };

                                                        report_tx.send(result_data).unwrap();
                                                        is_cancel
                                                    })
                                                })
                                                .collect::<Vec<_>>();

                                            let mut connection_gone = false;
                                            for f in futures {
                                                match f.await {
                                                    Ok(true) => {
                                                        // All works done
                                                        connection_gone = true;
                                                    }
                                                    Err(_) => {
                                                        // Unexpected
                                                        connection_gone = true;
                                                    }
                                                    _ => {}
                                                }
                                            }

                                            if connection_gone {
                                                break;
                                            }
                                        }
                                        Err(err) => {
                                            has_err = true;
                                            result_data_err.push(Err(err));
                                            if is_end.load(Ordering::Relaxed) {
                                                break;
                                            }
                                        }
                                    }
                                }
                                if has_err {
                                    report_tx.send(result_data_err).unwrap();
                                }
                            }));
                        }

                        rt.block_on(local);
                    })
                })
                .collect::<Vec<_>>()
            }
            HttpWorkType::H1 => connections
                .map(|num_connection| {
                    let report_tx = report_tx.clone();
                    let is_end = is_end.clone();
                    let client = client.clone();
                    let rt = tokio::runtime::Builder::new_current_thread()
                        .enable_all()
                        .build()
                        .unwrap();

                    let token = token.clone();
                    std::thread::spawn(move || {
                        let local = tokio::task::LocalSet::new();

                        for _ in 0..num_connection {
                            let report_tx = report_tx.clone();
                            let is_end = is_end.clone();
                            let client = client.clone();
                            let token = token.clone();
                            local.spawn_local(Box::pin(async move {
                                let mut result_data = ResultData::default();

                                let work = async {
                                    let mut client_state = ClientStateHttp1::default();
                                    loop {
                                        let res = client.work_http1(&mut client_state).await;
                                        let is_cancel = is_cancel_error(&res);
                                        result_data.push(res);
                                        if is_cancel || is_end.load(Ordering::Relaxed) {
                                            break;
                                        }
                                    }
                                };

                                tokio::select! {
                                    _ = work => {
                                    }
                                    _ = token.cancelled() => {
                                        result_data.push(Err(ClientError::Deadline));
                                    }
                                }
                                report_tx.send(result_data).unwrap();
                            }));
                        }
                        rt.block_on(local);
                    })
                })
                .collect::<Vec<_>>(),
        };
        tokio::select! {
            _ = tokio::time::sleep_until(dead_line.into()) => {
            }
            _ = tokio::signal::ctrl_c() => {
            }
        }

        is_end.store(true, Ordering::Relaxed);

        if !wait_ongoing_requests_after_deadline {
            token.cancel();
        }
        tokio::task::block_in_place(|| {
            for handle in handles {
                let _ = handle.join();
            }
        });
    }
}


================================================
FILE: src/client_h3.rs
================================================
use bytes::Buf;
use bytes::Bytes;
use core::sync::atomic::Ordering;
use http::Request;
use http_body_util::BodyExt;
use hyper::http;
use kanal::AsyncReceiver;
use quinn::default_runtime;
use std::net::SocketAddr;
use std::net::UdpSocket;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicIsize;
use std::time::Instant;

use tokio::sync::Semaphore;
use url::Url;

pub type SendRequestHttp3 = (
    h3::client::Connection<h3_quinn::Connection, Bytes>,
    h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>,
);

// HTTP3-specific error types
#[derive(thiserror::Error, Debug)]
pub enum Http3Error {
    #[error("QUIC Client: {0}")]
    QuicClientConfig(#[from] quinn::crypto::rustls::NoInitialCipherSuite),
    #[error("QUIC connect: {0}")]
    QuicConnect(#[from] quinn::ConnectError),
    #[error("QUIC connection: {0}")]
    QuicConnection(#[from] quinn::ConnectionError),
    #[error("Quic connection closed earlier than expected")]
    QuicDriverClosedEarly(#[from] tokio::sync::oneshot::error::RecvError),
    #[error("HTTP3 connection: {0}")]
    H3Connection(#[from] h3::error::ConnectionError),
    #[error("HTTP3 Stream: {0}")]
    H3Stream(#[from] h3::error::StreamError),
}

use crate::client::QueryLimit;
use crate::client::{
    Client, ClientError, ConnectionTime, RequestResult, Stream, is_cancel_error,
    set_connection_time, set_start_latency_correction,
};
use crate::pcg64si::Pcg64Si;
use crate::result_data::ResultData;
use rand::SeedableRng;
use rand::prelude::Rng;

pub(crate) struct ClientStateHttp3 {
    pub(crate) rng: Pcg64Si,
    pub(crate) send_request: h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>,
}

impl ClientStateHttp3 {
    fn new(send_request: h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>) -> Self {
        Self {
            rng: SeedableRng::from_rng(&mut rand::rng()),
            send_request,
        }
    }
}

impl Client {
    pub(crate) async fn connect_http3<R: Rng>(
        &self,
        url: &Url,
        rng: &mut R,
    ) -> Result<(ConnectionTime, SendRequestHttp3), ClientError> {
        let start = std::time::Instant::now();
        let (dns_lookup, stream) = self.client(url, rng, http::Version::HTTP_3).await?;
        let send_request = stream.handshake_http3().await?;
        let dialup = std::time::Instant::now();
        Ok((
            ConnectionTime {
                dns_lookup: dns_lookup - start,
                dialup: dialup - start,
            },
            send_request,
        ))
    }

    pub(crate) async fn quic_client(
        &self,
        addr: (std::net::IpAddr, u16),
        url: &Url,
    ) -> Result<Stream, ClientError> {
        let endpoint_config = h3_quinn::quinn::EndpointConfig::default();
        let local_socket = if addr.0.is_ipv6() {
            UdpSocket::bind("[::]:0").expect("couldn't bind to address")
        } else {
            UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address")
        };
        // If we can set the right build flags, we can use `h3_quinn::quinn::Endpoint::client` instead
        let mut client_endpoint = h3_quinn::quinn::Endpoint::new(
            endpoint_config,
            None,
            local_socket,
            default_runtime().unwrap(),
        )
        .unwrap();

        let tls_config = self.rustls_configs.config(http::Version::HTTP_3).clone();
        let client_config = quinn::ClientConfig::new(Arc::new(
            quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
                .map_err(Http3Error::from)?,
        ));
        client_endpoint.set_default_client_config(client_config);

        let remote_socket_address = SocketAddr::new(addr.0, addr.1);
        let server_name = url.host_str().ok_or(ClientError::HostNotFound)?;
        let conn = client_endpoint
            .connect(remote_socket_address, server_name)
            .map_err(Http3Error::from)?
            .await
            .map_err(Http3Error::from)?;
        Ok(Stream::Quic(conn))
    }

    pub(crate) async fn work_http3(
        &self,
        client_state: &mut ClientStateHttp3,
    ) -> Result<RequestResult, ClientError> {
        let do_req = async {
            let (_url, request, rng) = self.generate_request(&mut client_state.rng)?;
            let start = std::time::Instant::now();
            let connection_time: Option<ConnectionTime> = None;
            let mut first_byte: Option<std::time::Instant> = None;

            // if we implement http_body::Body on our H3 SendRequest, we can do some nice streaming stuff
            // with the response here. However as we don't really use the response we can get away
            // with not doing this for now
            let (head, mut req_body) = request.into_parts();
            let request = http::request::Request::from_parts(head, ());
            let mut stream = client_state
                .send_request
                .send_request(request)
                .await
                .map_err(Http3Error::from)?;
            // send the request body now
            if let Some(Ok(frame)) = req_body.frame().await {
                if let Ok(data) = frame.into_data() {
                    stream.send_data(data).await.map_err(Http3Error::from)?;
                }
            }
            stream.finish().await.map_err(Http3Error::from)?;

            // now read the response headers
            let response = stream.recv_response().await.map_err(Http3Error::from)?;
            let (parts, _) = response.into_parts();
            let status = parts.status;
            // now read the response body
            let mut len_bytes = 0;
            while let Some(chunk) = stream.recv_data().await.map_err(Http3Error::from)? {
                if first_byte.is_none() {
                    first_byte = Some(std::time::Instant::now())
                }
                len_bytes += chunk.remaining();
            }
            let end = std::time::Instant::now();

            let result = RequestResult {
                rng,
                start_latency_correction: None,
                start,
                first_byte,
                end,
                status,
                len_bytes,
                connection_time,
            };

            Ok::<_, ClientError>(result)
        };

        if let Some(timeout) = self.timeout {
            tokio::select! {
                res = do_req => {
                    res
                }
                _ = tokio::time::sleep(timeout) => {
                    Err(ClientError::Timeout)
                }
            }
        } else {
            do_req.await
        }
    }
}

impl Stream {
    async fn handshake_http3(self) -> Result<SendRequestHttp3, Http3Error> {
        let Stream::Quic(quic_conn) = self else {
            panic!("You cannot call http3 handshake on a non-quic stream");
        };
        let h3_quinn_conn = h3_quinn::Connection::new(quic_conn);
        // TODO add configuration settings to allow 'send_grease' etc.

        Ok(h3::client::new(h3_quinn_conn).await?)
    }
}

pub(crate) async fn send_debug_request_http3(
    h3_connection: h3::client::Connection<h3_quinn::Connection, Bytes>,
    mut client_state: h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>,
    request: Request<http_body_util::Full<Bytes>>,
) -> Result<http::Response<Bytes>, Http3Error> {
    // Prepare a channel to stop the driver thread
    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
    // Run the driver
    let http3_driver = spawn_http3_driver(h3_connection, shutdown_rx);

    let (head, mut req_body) = request.into_parts();
    let request = http::request::Request::from_parts(head, ());

    let mut stream = client_state.send_request(request).await?;
    if let Some(Ok(frame)) = req_body.frame().await {
        if let Ok(data) = frame.into_data() {
            stream.send_data(data).await?;
        }
    }

    stream.finish().await?;

    let response = stream.recv_response().await.unwrap_or_else(|err| {
        panic!("{}", err);
    });
    let mut body_bytes = bytes::BytesMut::new();

    while let Some(mut chunk) = stream.recv_data().await? {
        let bytes = chunk.copy_to_bytes(chunk.remaining());
        body_bytes.extend_from_slice(&bytes);
    }
    let body = body_bytes.freeze();
    let (parts, _) = response.into_parts();
    let _ = shutdown_tx.send(());
    let _ = http3_driver.await.unwrap();
    Ok(http::Response::from_parts(parts, body))
}

/**
 * Create `n_connections` parallel HTTP3 connections (on independent QUIC connections).
 * On each of those, run `n_http3_parallel` requests continuously until `deadline` is reached.
 */
pub(crate) async fn parallel_work_http3(
    n_connections: usize,
    n_http_parallel: usize,
    rx: AsyncReceiver<Option<Instant>>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    client: Arc<Client>,
    deadline: Option<std::time::Instant>,
) -> Vec<tokio::task::JoinHandle<()>> {
    let s = Arc::new(tokio::sync::Semaphore::new(0));
    let has_deadline = deadline.is_some();

    let futures = (0..n_connections)
        .map(|_| {
            let report_tx = report_tx.clone();
            let rx = rx.clone();
            let client = client.clone();
            let s = s.clone();
            tokio::spawn(create_and_load_up_single_connection_http3(
                n_http_parallel,
                rx,
                report_tx,
                client,
                s,
            ))
        })
        .collect::<Vec<_>>();

    if has_deadline {
        tokio::time::sleep_until(deadline.unwrap().into()).await;
        s.close();
    }

    futures
}

/**
 * For use in the 'slow' functions - send a report of every response in real time for display to the end-user.
 * Semaphore is closed to shut down all the tasks.
 * Very similar to how http2 loops work, just that we explicitly spawn the HTTP3 connection driver.
 */
async fn create_and_load_up_single_connection_http3(
    n_http_parallel: usize,
    rx: AsyncReceiver<Option<Instant>>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    client: Arc<Client>,
    s: Arc<Semaphore>,
) {
    let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
    loop {
        // create a HTTP3 connection
        match setup_http3(&client, &mut rng).await {
            Ok((connection_time, (h3_connection, send_request))) => {
                let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
                let http3_driver = spawn_http3_driver(h3_connection, shutdown_rx);
                let futures = (0..n_http_parallel)
                    .map(|_| {
                        let report_tx = report_tx.clone();
                        let rx = rx.clone();
                        let client = client.clone();
                        let mut client_state = ClientStateHttp3::new(send_request.clone());
                        let s = s.clone();
                        tokio::spawn(async move {
                            // This is where HTTP3 loops to make all the requests for a given client and worker
                            while let Ok(start_time_option) = rx.recv().await {
                                let (is_cancel, is_reconnect) = work_http3_once(
                                    &client,
                                    &mut client_state,
                                    &report_tx,
                                    connection_time,
                                    start_time_option,
                                )
                                .await;

                                let is_cancel = is_cancel || s.is_closed();
                                if is_cancel || is_reconnect {
                                    return is_cancel;
                                }
                            }
                            true
                        })
                    })
                    .collect::<Vec<_>>();
                drop(send_request);

                // collect all the requests we have spawned, and end the process if/when the semaphore says
                let mut connection_gone = false;
                for f in futures {
                    tokio::select! {
                        r = f => {
                            match r {
                                Ok(true) => {
                                    // All works done
                                    connection_gone = true;
                                }
                                Err(_) => {
                                    // Unexpected
                                    connection_gone = true;
                                }
                                _ => {}
                            }
                        }
                        _ = s.acquire() => {
                            report_tx.send(Err(ClientError::Deadline)).unwrap();
                            connection_gone = true;
                        }
                    }
                }
                if connection_gone {
                    // Try and politely shut down the HTTP3 connection
                    let _ = shutdown_tx.send(());
                    let _ = http3_driver.await;
                    return;
                }
            }
            Err(err) => {
                if s.is_closed() {
                    break;
                    // Consume a task
                } else if rx.recv().await.is_ok() {
                    report_tx.send(Err(err)).unwrap();
                } else {
                    return;
                }
            }
        }
    }
}

/**
 * This is structured to work very similarly to the `setup_http2`
 * function in `client.rs`
 */
pub(crate) async fn setup_http3<R: Rng>(
    client: &Client,
    rng: &mut R,
) -> Result<(ConnectionTime, SendRequestHttp3), ClientError> {
    let url = client.request_generator.url_generator.generate(rng)?;
    // Whatever rng state, all urls should have the same authority
    let (connection_time, send_request) = client.connect_http3(&url, rng).await?;

    Ok((connection_time, send_request))
}

pub(crate) fn spawn_http3_driver(
    mut h3_connection: h3::client::Connection<h3_quinn::Connection, Bytes>,
    shutdown_rx: tokio::sync::oneshot::Receiver<()>,
) -> tokio::task::JoinHandle<std::result::Result<(), Http3Error>> {
    tokio::spawn(async move {
        tokio::select! {
            // Drive the connection
            closed = std::future::poll_fn(|cx| h3_connection.poll_close(cx)) => {
                if closed.is_h3_no_error() {
                    Ok(())
                } else {
                    Err(Http3Error::H3Connection(closed))
                }
            },
            // Listen for shutdown condition
            _ = shutdown_rx => {
                // Initiate shutdown
                h3_connection.shutdown(0).await?;
                // Wait for ongoing work to complete
                let closed = std::future::poll_fn(|cx| h3_connection.poll_close(cx)).await;
                if closed.is_h3_no_error() {
                    Ok(())
                } else {
                    Err(Http3Error::H3Connection(closed))
                }
            }
        }
    })
}

pub(crate) async fn work_http3_once(
    client: &Client,
    client_state: &mut ClientStateHttp3,
    report_tx: &kanal::Sender<Result<RequestResult, ClientError>>,
    connection_time: ConnectionTime,
    start_latency_correction: Option<Instant>,
) -> (bool, bool) {
    let mut res = client.work_http3(client_state).await;
    let is_cancel = is_cancel_error(&res);
    let is_reconnect = is_h3_error(&res);
    set_connection_time(&mut res, connection_time);
    if let Some(start_latency_correction) = start_latency_correction {
        set_start_latency_correction(&mut res, start_latency_correction);
    }
    report_tx.send(res).unwrap();
    (is_cancel, is_reconnect)
}

fn is_h3_error(res: &Result<RequestResult, ClientError>) -> bool {
    res.as_ref()
        .err()
        .map(|err| matches!(err, ClientError::Http3(_) | ClientError::Io(_)))
        .unwrap_or(false)
}

/**
 * 'Fast' implementation of HTTP3 load generation.
 * If `n_tasks` is set, it will generate up to that many tasks.
 * Othrwise it will terminate when `is_end` becomes set to true.
 */
#[allow(clippy::too_many_arguments)]
pub(crate) fn http3_connection_fast_work_until(
    num_connections: usize,
    n_http_parallel: usize,
    report_tx: kanal::Sender<ResultData>,
    client: Arc<Client>,
    token: tokio_util::sync::CancellationToken,
    counter: Option<Arc<AtomicIsize>>,
    is_end: Arc<AtomicBool>,
    rt: tokio::runtime::Runtime,
) {
    let is_counting_tasks = counter.is_some();
    let client = client.clone();
    let local = tokio::task::LocalSet::new();
    for _ in 0..num_connections {
        let report_tx = report_tx.clone();
        let client = client.clone();
        let token = token.clone();
        let is_end = is_end.clone();
        let counter = counter.clone();
        local.spawn_local(Box::pin(async move {
            let mut has_err = false;
            let mut result_data_err = ResultData::default();
            let mut rng: Pcg64Si = SeedableRng::from_rng(&mut rand::rng());
            loop {
                let client = client.clone();
                match setup_http3(&client, &mut rng).await {
                    Ok((connection_time, (h3_connection, send_request))) => {
                        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
                        let http3_driver = spawn_http3_driver(h3_connection, shutdown_rx);
                        let futures = (0..n_http_parallel)
                            .map(|_| {
                                let mut client_state = ClientStateHttp3::new(send_request.clone());
                                let client = client.clone();
                                let report_tx = report_tx.clone();
                                let token = token.clone();
                                let is_end = is_end.clone();
                                let counter = counter.clone();
                                tokio::task::spawn_local(async move {
                                    let mut result_data = ResultData::default();

                                    let work = async {
                                        loop {
                                            if is_counting_tasks
                                                && counter
                                                    .as_ref()
                                                    .unwrap()
                                                    .fetch_sub(1, Ordering::Relaxed)
                                                    <= 0
                                            {
                                                return true;
                                            }
                                            let mut res =
                                                client.work_http3(&mut client_state).await;
                                            let is_cancel = is_cancel_error(&res)
                                                || is_end.load(Ordering::Relaxed);
                                            let is_reconnect = is_h3_error(&res);
                                            set_connection_time(&mut res, connection_time);

                                            result_data.push(res);

                                            if is_cancel || is_reconnect {
                                                return is_cancel;
                                            }
                                        }
                                    };

                                    let is_cancel = tokio::select! {
                                        is_cancel = work => {
                                            is_cancel
                                        }
                                        _ = token.cancelled() => {
                                            result_data.push(Err(ClientError::Deadline));
                                            true
                                        }
                                    };

                                    report_tx.send(result_data).unwrap();
                                    is_cancel
                                })
                            })
                            .collect::<Vec<_>>();

                        let mut connection_gone = false;
                        for f in futures {
                            match f.await {
                                Ok(true) => {
                                    // All works done
                                    connection_gone = true;
                                }
                                Err(_) => {
                                    // Unexpected
                                    connection_gone = true;
                                }
                                _ => {}
                            }
                        }

                        if connection_gone {
                            let _ = shutdown_tx.send(());
                            let _ = http3_driver.await;
                            break;
                        }
                    }
                    Err(err) => {
                        has_err = true;
                        result_data_err.push(Err(err));
                        if is_end.load(Ordering::Relaxed)
                            || (is_counting_tasks
                                && counter.as_ref().unwrap().fetch_sub(1, Ordering::Relaxed) <= 0)
                        {
                            break;
                        }
                    }
                }
            }
            if has_err {
                report_tx.send(result_data_err).unwrap();
            }
        }));
    }
    rt.block_on(local);
}

/// Work function for HTTP3 client that generates `n_tasks` tasks.
pub async fn work(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    n_tasks: usize,
    n_connections: usize,
    n_http2_parallel: usize,
) {
    let (tx, rx) = kanal::unbounded::<Option<Instant>>();
    let rx = rx.to_async();

    let n_tasks_emitter = async move {
        for _ in 0..n_tasks {
            tx.send(None)?
        }
        drop(tx);
        Ok::<(), kanal::SendError>(())
    };
    let futures =
        parallel_work_http3(n_connections, n_http2_parallel, rx, report_tx, client, None).await;
    n_tasks_emitter.await.unwrap();
    for f in futures {
        let _ = f.await;
    }
}

/// n tasks by m workers limit to qps works in a second
pub async fn work_with_qps(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    n_tasks: usize,
    n_connections: usize,
    n_http_parallel: usize,
) {
    let (tx, rx) = kanal::unbounded::<Option<Instant>>();

    let work_queue = async move {
        match query_limit {
            QueryLimit::Qps(qps) => {
                let start = std::time::Instant::now();
                for i in 0..n_tasks {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    tx.send(None)?;
                }
            }
            QueryLimit::Burst(duration, rate) => {
                let mut n = 0;
                // Handle via rate till n_tasks out of bound
                while n + rate < n_tasks {
                    tokio::time::sleep(duration).await;
                    for _ in 0..rate {
                        tx.send(None)?;
                    }
                    n += rate;
                }
                // Handle the remaining tasks
                if n_tasks > n {
                    tokio::time::sleep(duration).await;
                    for _ in 0..n_tasks - n {
                        tx.send(None)?;
                    }
                }
            }
        }
        // tx gone
        drop(tx);
        Ok::<(), kanal::SendError>(())
    };

    let rx = rx.to_async();
    let futures =
        parallel_work_http3(n_connections, n_http_parallel, rx, report_tx, client, None).await;
    work_queue.await.unwrap();
    for f in futures {
        let _ = f.await;
    }
}

/// n tasks by m workers limit to qps works in a second with latency correction
pub async fn work_with_qps_latency_correction(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    n_tasks: usize,
    n_connections: usize,
    n_http2_parallel: usize,
) {
    let (tx, rx) = kanal::unbounded();

    let _work_queue = async move {
        match query_limit {
            QueryLimit::Qps(qps) => {
                let start = std::time::Instant::now();
                for i in 0..n_tasks {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let now = std::time::Instant::now();
                    tx.send(Some(now))?;
                }
            }
            QueryLimit::Burst(duration, rate) => {
                let mut n = 0;
                // Handle via rate till n_tasks out of bound
                while n + rate < n_tasks {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    for _ in 0..rate {
                        tx.send(Some(now))?;
                    }
                    n += rate;
                }
                // Handle the remaining tasks
                if n_tasks > n {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    for _ in 0..n_tasks - n {
                        tx.send(Some(now))?;
                    }
                }
            }
        }

        // tx gone
        drop(tx);
        Ok::<(), kanal::SendError>(())
    };

    let rx = rx.to_async();
    let futures =
        parallel_work_http3(n_connections, n_http2_parallel, rx, report_tx, client, None).await;
    for f in futures {
        let _ = f.await;
    }
}

/// Run until dead_line by n workers
pub async fn work_until(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http_parallel: usize,
    _wait_ongoing_requests_after_deadline: bool,
) {
    let (tx, rx) = kanal::bounded_async::<Option<Instant>>(5000);
    // This emitter is used for H3 to give it unlimited tokens to emit work.
    let cancel_token = tokio_util::sync::CancellationToken::new();
    let emitter_handle = endless_emitter(cancel_token.clone(), tx).await;
    let futures = parallel_work_http3(
        n_connections,
        n_http_parallel,
        rx,
        report_tx.clone(),
        client.clone(),
        Some(dead_line),
    )
    .await;
    for f in futures {
        let _ = f.await;
    }
    // Cancel the emitter when we're done with the futures
    cancel_token.cancel();
    // Wait for the emitter to exit cleanly
    let _ = emitter_handle.await;
}

/// Run until dead_line by n workers limit to qps works in a second
#[allow(clippy::too_many_arguments)]
pub async fn work_until_with_qps(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    start: std::time::Instant,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http2_parallel: usize,
    _wait_ongoing_requests_after_deadline: bool,
) {
    let rx = match query_limit {
        QueryLimit::Qps(qps) => {
            let (tx, rx) = kanal::unbounded::<Option<Instant>>();
            tokio::spawn(async move {
                for i in 0.. {
                    if std::time::Instant::now() > dead_line {
                        break;
                    }
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let _ = tx.send(None);
                }
                // tx gone
            });
            rx
        }
        QueryLimit::Burst(duration, rate) => {
            let (tx, rx) = kanal::unbounded();
            tokio::spawn(async move {
                // Handle via rate till deadline is reached
                for _ in 0.. {
                    if std::time::Instant::now() > dead_line {
                        break;
                    }

                    tokio::time::sleep(duration).await;
                    for _ in 0..rate {
                        let _ = tx.send(None);
                    }
                }
                // tx gone
            });
            rx
        }
    };
    let rx = rx.to_async();
    let futures = parallel_work_http3(
        n_connections,
        n_http2_parallel,
        rx,
        report_tx,
        client,
        Some(dead_line),
    )
    .await;
    for f in futures {
        let _ = f.await;
    }
}

/// Run until dead_line by n workers limit to qps works in a second with latency correction
#[allow(clippy::too_many_arguments)]
pub async fn work_until_with_qps_latency_correction(
    client: Arc<Client>,
    report_tx: kanal::Sender<Result<RequestResult, ClientError>>,
    query_limit: QueryLimit,
    start: std::time::Instant,
    dead_line: std::time::Instant,
    n_connections: usize,
    n_http2_parallel: usize,
    _wait_ongoing_requests_after_deadline: bool,
) {
    let (tx, rx) = kanal::unbounded();
    match query_limit {
        QueryLimit::Qps(qps) => {
            tokio::spawn(async move {
                for i in 0.. {
                    tokio::time::sleep_until(
                        (start + std::time::Duration::from_secs_f64(i as f64 * 1f64 / qps)).into(),
                    )
                    .await;
                    let now = std::time::Instant::now();
                    if now > dead_line {
                        break;
                    }
                    let _ = tx.send(Some(now));
                }
                // tx gone
            });
        }
        QueryLimit::Burst(duration, rate) => {
            tokio::spawn(async move {
                // Handle via rate till deadline is reached
                loop {
                    tokio::time::sleep(duration).await;
                    let now = std::time::Instant::now();
                    if now > dead_line {
                        break;
                    }

                    for _ in 0..rate {
                        let _ = tx.send(Some(now));
                    }
                }
                // tx gone
            });
        }
    };

    let rx = rx.to_async();
    let futures = parallel_work_http3(
        n_connections,
        n_http2_parallel,
        rx,
        report_tx,
        client,
        Some(dead_line),
    )
    .await;
    for f in futures {
        let _ = f.await;
    }
}

#[cfg(feature = "http3")]
async fn endless_emitter(
    cancellation_token: tokio_util::sync::CancellationToken,
    tx: kanal::AsyncSender<Option<Instant>>,
) -> tokio::task::JoinHandle<()> {
    tokio::spawn(async move {
        loop {
            tokio::select! {
                _ = cancellation_token.cancelled() => {
                    break;
                }
                _ = async {
                    // As we our `work_http2_once` function is limited by the number of `tx` we send, but we only
                    // want to stop when our semaphore is closed, just dump unlimited `Nones` into the tx to un-constrain it
                    let _ = tx.send(None).await;
                } => {}
            }
        }
    })
}

pub mod fast {
    use std::sync::{
        Arc,
        atomic::{AtomicBool, AtomicIsize, Ordering},
    };

    use crate::{
        client::Client, client_h3::http3_connection_fast_work_until, result_data::ResultData,
    };

    /// Run n tasks by m workers
    pub async fn work(
        client: Arc<Client>,
        report_tx: kanal::Sender<ResultData>,
        n_tasks: usize,
        n_connections: usize,
        n_http_parallel: usize,
    ) {
        let counter = Arc::new(AtomicIsize::new(n_tasks as isize));
        let num_threads = num_cpus::get_physical();
        let connections = (0..num_threads).filter_map(|i| {
            let num_connection = n_connections / num_threads
                + (if (n_connections % num_threads) > i {
                    1
                } else {
                    0
                });
            if num_connection > 0 {
                Some(num_connection)
            } else {
                None
            }
        });
        let token = tokio_util::sync::CancellationToken::new();
        let handles = connections
            .map(|num_connections| {
                let report_tx = report_tx.clone();
                let client = client.clone();
                let rt = tokio::runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                    .unwrap();
                let token = token.clone();
                let counter = counter.clone();
                // will let is_end just stay false permanently
                let is_end = Arc::new(AtomicBool::new(false));
                std::thread::spawn(move || {
                    http3_connection_fast_work_until(
                        num_connections,
                        n_http_parallel,
                        report_tx,
                        client,
                        token,
                        Some(counter),
                        is_end,
                        rt,
                    )
                })
            })
            .collect::<Vec<_>>();
        tokio::spawn(async move {
            tokio::signal::ctrl_c().await.unwrap();
            token.cancel();
        });

        tokio::task::block_in_place(|| {
            for handle in handles {
                let _ = handle.join();
            }
        });
    }

    /// Run until dead_line by n workers
    pub async fn work_until(
        client: Arc<Client>,
        report_tx: kanal::Sender<ResultData>,
        dead_line: std::time::Instant,
        n_conn
Download .txt
gitextract_llqd8vur/

├── .dockerignore
├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── CI.yml
│       ├── release-pgo.yml
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── Cross.toml
├── Dockerfile
├── LICENSE
├── README.md
├── pgo/
│   └── server/
│       ├── Cargo.toml
│       └── src/
│           └── main.rs
├── pgo.js
├── schema.json
├── src/
│   ├── aws_auth.rs
│   ├── cli.rs
│   ├── client.rs
│   ├── client_h3.rs
│   ├── curl_compat.rs
│   ├── db.rs
│   ├── histogram.rs
│   ├── lib.rs
│   ├── main.rs
│   ├── monitor.rs
│   ├── pcg64si.rs
│   ├── printer.rs
│   ├── request_generator.rs
│   ├── result_data.rs
│   ├── timescale.rs
│   ├── tls_config.rs
│   └── url_generator.rs
└── tests/
    ├── common/
    │   ├── mod.rs
    │   ├── server.cert
    │   └── server.key
    └── tests.rs
Download .txt
SYMBOL INDEX (294 symbols across 20 files)

FILE: pgo/server/src/main.rs
  function main (line 7) | async fn main() {
  function root (line 20) | async fn root() -> &'static str {

FILE: src/aws_auth.rs
  type AwsSignatureConfig (line 11) | pub struct AwsSignatureConfig {
    method sign_request (line 42) | pub fn sign_request(
    method new (line 115) | pub fn new(
  type AwsSignatureError (line 20) | pub enum AwsSignatureError {

FILE: src/cli.rs
  function parse_header (line 4) | pub fn parse_header(s: &str) -> Result<(HeaderName, HeaderValue), anyhow...
  function parse_n_requests (line 12) | pub fn parse_n_requests(s: &str) -> Result<usize, String> {
  type ConnectToEntry (line 31) | pub struct ConnectToEntry {
  type Err (line 39) | type Err = String;
  method from_str (line 41) | fn from_str(s: &str) -> Result<Self, Self::Err> {
  function parse_vsock_addr (line 68) | pub fn parse_vsock_addr(s: &str) -> Result<tokio_vsock::VsockAddr, Strin...

FILE: src/client.rs
  type SendRequestHttp1 (line 34) | type SendRequestHttp1 = hyper::client::conn::http1::SendRequest<Full<Byt...
  type SendRequestHttp2 (line 35) | type SendRequestHttp2 = hyper::client::conn::http2::SendRequest<Full<Byt...
  function format_host_port (line 37) | fn format_host_port(host: &str, port: u16) -> String {
  type ConnectionTime (line 46) | pub struct ConnectionTime {
  type RequestResult (line 53) | pub struct RequestResult {
    method duration (line 74) | pub fn duration(&self) -> std::time::Duration {
  type HttpWorkType (line 81) | enum HttpWorkType {
  type Dns (line 88) | pub struct Dns {
    method select_connect_to (line 95) | fn select_connect_to<'a, R: Rng>(
    method lookup (line 110) | async fn lookup<R: Rng>(
  type ClientError (line 153) | pub enum ClientError {
  type Client (line 209) | pub struct Client {
    method is_http2 (line 411) | fn is_http2(&self) -> bool {
    method is_proxy_http2 (line 416) | fn is_proxy_http2(&self) -> bool {
    method is_work_http2 (line 420) | fn is_work_http2(&self) -> bool {
    method work_type (line 432) | fn work_type(&self) -> HttpWorkType {
    method pre_lookup (line 446) | pub async fn pre_lookup(&self) -> Result<(), ClientError> {
    method generate_request (line 466) | pub fn generate_request<R: Rng + Copy>(
    method client (line 495) | pub(crate) async fn client<R: Rng>(
    method tls_client (line 569) | async fn tls_client(
    method connect_tls (line 584) | async fn connect_tls<S>(
    method connect_tls (line 604) | async fn connect_tls<S>(
    method client_http1 (line 623) | async fn client_http1<R: Rng>(
    method work_http1 (line 684) | async fn work_http1(
    method connect_http2 (line 792) | async fn connect_http2<R: Rng>(
    method work_http2 (line 883) | async fn work_http2(
    method redirect (line 940) | async fn redirect<R: Rng + Send + Copy>(
  method default (line 231) | fn default() -> Self {
  type ClientStateHttp1 (line 279) | struct ClientStateHttp1 {
  method default (line 285) | fn default() -> Self {
  type ClientStateHttp2 (line 293) | struct ClientStateHttp2 {
  type QueryLimit (line 298) | pub enum QueryLimit {
  type Stream (line 305) | pub(crate) enum Stream {
    method handshake_http1 (line 321) | async fn handshake_http1(self, with_upgrade: bool) -> Result<SendReque...
    method handshake_http2 (line 371) | async fn handshake_http2(self) -> Result<SendRequestHttp2, ClientError> {
  function is_cancel_error (line 1031) | pub(crate) fn is_cancel_error(res: &Result<RequestResult, ClientError>) ...
  function is_too_many_open_files (line 1036) | fn is_too_many_open_files(res: &Result<RequestResult, ClientError>) -> b...
  function is_hyper_error (line 1047) | fn is_hyper_error(res: &Result<RequestResult, ClientError>) -> bool {
  function setup_http2 (line 1060) | async fn setup_http2<R: Rng>(
  function work_http2_once (line 1070) | async fn work_http2_once(
  function set_connection_time (line 1088) | pub(crate) fn set_connection_time<E>(
  function set_start_latency_correction (line 1097) | pub(crate) fn set_start_latency_correction<E>(
  function work_debug (line 1106) | pub async fn work_debug<W: Write>(w: &mut W, client: Arc<Client>) -> Res...
  function work (line 1144) | pub async fn work(
  function work_with_qps (line 1270) | pub async fn work_with_qps(
  function work_with_qps_latency_correction (line 1440) | pub async fn work_with_qps_latency_correction(
  function work_until (line 1615) | pub async fn work_until(
  function work_until_with_qps (line 1780) | pub async fn work_until_with_qps(
  function work_until_with_qps_latency_correction (line 1990) | pub async fn work_until_with_qps_latency_correction(
  function work (line 2218) | pub async fn work(
  function work_until (line 2443) | pub async fn work_until(

FILE: src/client_h3.rs
  type SendRequestHttp3 (line 19) | pub type SendRequestHttp3 = (
  type Http3Error (line 26) | pub enum Http3Error {
  type ClientStateHttp3 (line 51) | pub(crate) struct ClientStateHttp3 {
    method new (line 57) | fn new(send_request: h3::client::SendRequest<h3_quinn::OpenStreams, By...
  method connect_http3 (line 66) | pub(crate) async fn connect_http3<R: Rng>(
  method quic_client (line 84) | pub(crate) async fn quic_client(
  method work_http3 (line 121) | pub(crate) async fn work_http3(
  method handshake_http3 (line 193) | async fn handshake_http3(self) -> Result<SendRequestHttp3, Http3Error> {
  function send_debug_request_http3 (line 204) | pub(crate) async fn send_debug_request_http3(
  function parallel_work_http3 (line 246) | pub(crate) async fn parallel_work_http3(
  function create_and_load_up_single_connection_http3 (line 286) | async fn create_and_load_up_single_connection_http3(
  function setup_http3 (line 378) | pub(crate) async fn setup_http3<R: Rng>(
  function spawn_http3_driver (line 389) | pub(crate) fn spawn_http3_driver(
  function work_http3_once (line 419) | pub(crate) async fn work_http3_once(
  function is_h3_error (line 437) | fn is_h3_error(res: &Result<RequestResult, ClientError>) -> bool {
  function http3_connection_fast_work_until (line 450) | pub(crate) fn http3_connection_fast_work_until(
  function work (line 574) | pub async fn work(
  function work_with_qps (line 600) | pub async fn work_with_qps(
  function work_with_qps_latency_correction (line 656) | pub async fn work_with_qps_latency_correction(
  function work_until (line 715) | pub async fn work_until(
  function work_until_with_qps (line 747) | pub async fn work_until_with_qps(
  function work_until_with_qps_latency_correction (line 811) | pub async fn work_until_with_qps_latency_correction(
  function endless_emitter (line 874) | async fn endless_emitter(
  function work (line 905) | pub async fn work(
  function work_until (line 967) | pub async fn work_until(

FILE: src/curl_compat.rs
  type Form (line 4) | pub struct Form {
    method new (line 17) | pub fn new() -> Self {
    method add_part (line 24) | pub fn add_part(&mut self, part: FormPart) {
    method content_type (line 28) | pub fn content_type(&self) -> String {
    method body (line 32) | pub fn body(&self) -> Vec<u8> {
    method generate_boundary (line 76) | fn generate_boundary() -> String {
  type FormPart (line 9) | pub struct FormPart {
  type Err (line 90) | type Err = anyhow::Error;
  method from_str (line 99) | fn from_str(s: &str) -> Result<Self, Self::Err> {
  function test_parse_simple_field (line 172) | fn test_parse_simple_field() {
  function test_parse_field_with_filename (line 181) | fn test_parse_field_with_filename() {
  function test_parse_field_with_type (line 190) | fn test_parse_field_with_type() {
  function test_parse_field_with_filename_and_type (line 199) | fn test_parse_field_with_filename_and_type() {
  function test_parse_invalid_format (line 210) | fn test_parse_invalid_format() {
  function test_parse_file_upload (line 216) | fn test_parse_file_upload() {
  function test_parse_file_upload_without_filename (line 235) | fn test_parse_file_upload_without_filename() {
  function test_form_creation_and_body_generation (line 254) | fn test_form_creation_and_body_generation() {
  function test_form_content_type (line 292) | fn test_form_content_type() {
  function test_empty_form_body (line 301) | fn test_empty_form_body() {
  function test_form_with_file_upload (line 311) | fn test_form_with_file_upload() {
  function test_boundary_generation_is_random (line 338) | fn test_boundary_generation_is_random() {

FILE: src/db.rs
  function create_db (line 5) | fn create_db(conn: &Connection) -> Result<usize, rusqlite::Error> {
  function store (line 21) | pub fn store(
  function test_store (line 64) | fn test_store() {

FILE: src/histogram.rs
  function histogram (line 1) | pub fn histogram(values: &[f64], bins: usize) -> Vec<(f64, usize)> {
  function test_histogram (line 25) | fn test_histogram() {

FILE: src/lib.rs
  type Opts (line 61) | pub struct Opts {
    method work_mode (line 946) | fn work_mode(&self) -> WorkMode {
  function run (line 335) | pub async fn run(mut opts: Opts) -> anyhow::Result<()> {
  function system_resolv_conf (line 892) | pub(crate) fn system_resolv_conf() -> anyhow::Result<(ResolverConfig, Re...
  function fallback_resolver_config (line 915) | fn fallback_resolver_config(err: anyhow::Error) -> anyhow::Result<(Resol...
  type WorkMode (line 924) | enum WorkMode {

FILE: src/main.rs
  function main (line 4) | fn main() {

FILE: src/monitor.rs
  type EndLine (line 21) | pub enum EndLine {
  type ColorScheme (line 28) | struct ColorScheme {
    method new (line 35) | fn new() -> ColorScheme {
    method set_colors (line 43) | fn set_colors(&mut self) {
  type Monitor (line 50) | pub struct Monitor {
    method monitor (line 79) | pub async fn monitor(self) -> Result<(ResultData, PrintConfig), std::i...
  type IntoRawMode (line 63) | struct IntoRawMode;
    method new (line 66) | pub fn new() -> Result<(Self, DefaultTerminal), std::io::Error> {
  method drop (line 73) | fn drop(&mut self) {

FILE: src/pcg64si.rs
  type Pcg64Si (line 8) | pub struct Pcg64Si {
  type Error (line 13) | type Error = Infallible;
  method try_next_u32 (line 15) | fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
  method try_next_u64 (line 19) | fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
  method try_fill_bytes (line 31) | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
  type Seed (line 37) | type Seed = [u8; 8];
  method from_seed (line 39) | fn from_seed(seed: Self::Seed) -> Pcg64Si {
  function test_rng_next (line 55) | fn test_rng_next() {
  function test_rng_from_seed (line 67) | fn test_rng_from_seed() {
  function test_rng_fill_bytes (line 75) | fn test_rng_fill_bytes() {

FILE: src/printer.rs
  type StyleScheme (line 14) | struct StyleScheme {
    method no_style (line 18) | fn no_style(self, text: &str) -> StyledContent<&str> {
    method heading (line 21) | fn heading(self, text: &str) -> StyledContent<&str> {
    method success_rate (line 28) | fn success_rate(self, text: &str, success_rate: f64) -> StyledContent<...
    method fastest (line 41) | fn fastest(self, text: &str) -> StyledContent<&str> {
    method slowest (line 48) | fn slowest(self, text: &str) -> StyledContent<&str> {
    method average (line 55) | fn average(self, text: &str) -> StyledContent<&str> {
    method latency_distribution (line 63) | fn latency_distribution(self, text: &str, label: f64) -> StyledContent...
    method status_distribution (line 81) | fn status_distribution(self, text: &str, status: StatusCode) -> Styled...
  type PrintMode (line 99) | pub enum PrintMode {
  type PrintConfig (line 107) | pub struct PrintConfig {
  function print_result (line 115) | pub fn print_result(
  function print_json (line 144) | fn print_json<W: Write>(
  function print_csv (line 411) | fn print_csv<W: Write>(w: &mut W, start: Instant, res: &ResultData) -> s...
  function print_summary (line 446) | fn print_summary<W: Write>(
  function print_histogram (line 647) | fn print_histogram<W: Write>(
  function bar (line 685) | fn bar<W: Write>(w: &mut W, ratio: f64, style: StyleScheme, label: f64) ...
  function percentile_iter (line 694) | fn percentile_iter(values: &mut [f64]) -> impl Iterator<Item = (f64, f64...
  function print_distribution (line 706) | fn print_distribution<W: Write>(
  function percentiles (line 729) | fn percentiles(values: &mut [f64]) -> BTreeMap<String, f64> {
  function test_percentile_iter (line 742) | fn test_percentile_iter() {

FILE: src/request_generator.rs
  type Proxy (line 15) | pub struct Proxy {
  type BodyGenerator (line 20) | pub enum BodyGenerator {
  type RequestGenerator (line 25) | pub struct RequestGenerator {
    method is_http1 (line 49) | fn is_http1(&self) -> bool {
    method generate_body (line 53) | fn generate_body<R: Rng>(&self, rng: &mut R) -> Bytes {
    method generate (line 60) | pub fn generate<R: Rng>(
  type RequestGenerationError (line 38) | pub enum RequestGenerationError {

FILE: src/result_data.rs
  type ResultData (line 18) | pub struct ResultData {
    method push (line 55) | pub fn push(&mut self, result: Result<RequestResult, ClientError>) {
    method len (line 65) | pub fn len(&self) -> usize {
    method merge (line 69) | pub fn merge(&mut self, other: ResultData) {
    method success (line 79) | pub fn success(&self) -> &[RequestResult] {
    method success_rate (line 85) | pub fn success_rate(&self) -> f64 {
    method latency_stat (line 99) | pub fn latency_stat(&self) -> MinMaxMean {
    method error_distribution (line 106) | pub fn error_distribution(&self) -> &BTreeMap<String, usize> {
    method end_times_from_start (line 110) | pub fn end_times_from_start(&self, start: Instant) -> impl Iterator<It...
    method status_code_distribution (line 114) | pub fn status_code_distribution(&self) -> BTreeMap<StatusCode, usize> {
    method dns_dialup_stat (line 123) | pub fn dns_dialup_stat(&self) -> MinMaxMean {
    method dns_lookup_stat (line 130) | pub fn dns_lookup_stat(&self) -> MinMaxMean {
    method first_byte_stat (line 137) | pub fn first_byte_stat(&self) -> MinMaxMean {
    method total_data (line 144) | pub fn total_data(&self) -> usize {
    method size_per_request (line 148) | pub fn size_per_request(&self) -> Option<u64> {
    method duration_all_statistics (line 156) | pub fn duration_all_statistics(&self) -> Statistics {
    method first_byte_all_statistics (line 165) | pub fn first_byte_all_statistics(&self) -> Statistics {
    method duration_successful_statistics (line 174) | pub fn duration_successful_statistics(&self) -> Statistics {
    method duration_not_successful_statistics (line 185) | pub fn duration_not_successful_statistics(&self) -> Statistics {
  type Statistics (line 25) | pub struct Statistics {
    method new (line 32) | fn new(data: &mut [f64]) -> Self {
  function percentile_iter (line 42) | fn percentile_iter(values: &mut [f64]) -> impl Iterator<Item = (f64, f64...
  function build_mock_request_result (line 206) | fn build_mock_request_result(
  function build_mock_request_results (line 232) | fn build_mock_request_results() -> ResultData {
  function test_calculate_success_rate (line 263) | fn test_calculate_success_rate() {
  function test_calculate_slowest_request (line 269) | fn test_calculate_slowest_request() {
  function test_calculate_average_request (line 275) | fn test_calculate_average_request() {
  function test_calculate_total_data (line 281) | fn test_calculate_total_data() {
  function test_calculate_size_per_request (line 287) | fn test_calculate_size_per_request() {
  function test_calculate_connection_times_dns_dialup_average (line 293) | fn test_calculate_connection_times_dns_dialup_average() {
  function test_calculate_connection_times_dns_dialup_fastest (line 299) | fn test_calculate_connection_times_dns_dialup_fastest() {
  function test_calculate_connection_times_dns_dialup_slowest (line 305) | fn test_calculate_connection_times_dns_dialup_slowest() {
  function test_calculate_connection_times_dns_lookup_average (line 311) | fn test_calculate_connection_times_dns_lookup_average() {
  function test_calculate_connection_times_dns_lookup_fastest (line 317) | fn test_calculate_connection_times_dns_lookup_fastest() {
  function test_calculate_connection_times_dns_lookup_slowest (line 323) | fn test_calculate_connection_times_dns_lookup_slowest() {

FILE: src/timescale.rs
  type TimeScale (line 4) | pub enum TimeScale {
    method fmt (line 22) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    method value_variants (line 76) | fn value_variants<'a>() -> &'a [Self] {
    method to_possible_value (line 87) | fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
    method as_secs_f64 (line 101) | pub fn as_secs_f64(&self) -> f64 {
    method from_f64 (line 115) | pub fn from_f64(seconds: f64) -> Self {
    method from_elapsed (line 133) | pub fn from_elapsed(duration: Duration) -> Self {
    method inc (line 137) | pub fn inc(&self) -> Self {
    method dec (line 150) | pub fn dec(&self) -> Self {
  type TimeLabel (line 16) | pub struct TimeLabel {
    method fmt (line 37) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  function assert_timescale_correct_for_seconds_range (line 168) | fn assert_timescale_correct_for_seconds_range(
  function test_timescale_ranges (line 183) | fn test_timescale_ranges() {
  function test_timescale_inc (line 225) | fn test_timescale_inc() {
  function test_timescale_dec (line 244) | fn test_timescale_dec() {

FILE: src/tls_config.rs
  type RuslsConfigs (line 2) | pub struct RuslsConfigs {
    method new (line 10) | pub fn new(
    method config (line 65) | pub fn config(&self, http: hyper::http::Version) -> &std::sync::Arc<ru...
  type NativeTlsConnectors (line 79) | pub struct NativeTlsConnectors {
    method new (line 86) | pub fn new(
    method connector (line 128) | pub fn connector(&self, is_http2: bool) -> &tokio_native_tls::TlsConne...
  type AcceptAnyServerCert (line 140) | pub struct AcceptAnyServerCert;
    method verify_server_cert (line 144) | fn verify_server_cert(
    method verify_tls12_signature (line 155) | fn verify_tls12_signature(
    method verify_tls13_signature (line 164) | fn verify_tls13_signature(
    method supported_verify_schemes (line 173) | fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {

FILE: src/url_generator.rs
  type UrlGenerator (line 9) | pub enum UrlGenerator {
    method new_static (line 28) | pub fn new_static(url: Url) -> Self {
    method new_multi_static (line 32) | pub fn new_multi_static(urls: Vec<Url>) -> Self {
    method new_dynamic (line 37) | pub fn new_dynamic(regex: Regex) -> Self {
    method generate (line 41) | pub fn generate<R: Rng>(&self, rng: &mut R) -> Result<Cow<'_, Url>, Ur...
  type UrlGeneratorError (line 16) | pub enum UrlGeneratorError {
  function test_url_generator_static (line 73) | fn test_url_generator_static() {
  function test_url_generator_multistatic (line 81) | fn test_url_generator_multistatic() {
  function test_url_generator_dynamic (line 99) | fn test_url_generator_dynamic() {
  function test_url_generator_dynamic_consistency (line 115) | fn test_url_generator_dynamic_consistency() {
  function test_url_generator_multi_consistency (line 131) | fn test_url_generator_multi_consistency() {

FILE: tests/common/mod.rs
  function h3_server (line 14) | pub async fn h3_server(
  function process_request (line 94) | async fn process_request<T>(

FILE: tests/tests.rs
  function run (line 29) | async fn run<'a>(args: impl Iterator<Item = &'a str>) {
  function next_port (line 42) | fn next_port() -> u16 {
  function install_crypto_provider (line 47) | fn install_crypto_provider() {
  function bind_port (line 56) | async fn bind_port(port: u16) -> tokio::net::TcpListener {
  function bind_port_and_increment (line 62) | async fn bind_port_and_increment() -> (tokio::net::TcpListener, u16) {
  function bind_port_ipv6 (line 68) | async fn bind_port_ipv6(port: u16) -> tokio::net::TcpListener {
  type HttpWorkType (line 75) | enum HttpWorkType {
  function http_work_type (line 82) | fn http_work_type(args: &[&str]) -> HttpWorkType {
  function test_all_http_versions (line 104) | fn test_all_http_versions(#[case] http_version_param: &str) {}
  function test_all_http_versions (line 111) | fn test_all_http_versions(#[case] http_version_param: &str) {}
  function get_req (line 113) | async fn get_req(path: &str, args: &[&str]) -> Request<Bytes> {
  function redirect (line 189) | async fn redirect(n: usize, is_relative: bool, limit: usize) -> bool {
  function get_host_with_connect_to (line 223) | async fn get_host_with_connect_to(host: &'static str) -> String {
  function get_host_with_connect_to_ipv6_target (line 250) | async fn get_host_with_connect_to_ipv6_target(host: &'static str) -> Str...
  function get_host_with_connect_to_ipv6_requested (line 278) | async fn get_host_with_connect_to_ipv6_requested() -> String {
  function get_host_with_connect_to_redirect (line 304) | async fn get_host_with_connect_to_redirect(host: &'static str) -> String {
  function test_request_count (line 335) | async fn test_request_count(args: &[&str]) -> usize {
  function distribution_on_two_matching_connect_to (line 361) | async fn distribution_on_two_matching_connect_to(host: &'static str) -> ...
  function test_enable_compression_default (line 415) | async fn test_enable_compression_default(http_version_param: &str) {
  function test_setting_custom_header (line 432) | async fn test_setting_custom_header(http_version_param: &str) {
  function test_setting_accept_header (line 443) | async fn test_setting_accept_header(http_version_param: &str) {
  function test_setting_body (line 471) | async fn test_setting_body(http_version_param: &str) {
  function test_setting_content_type_header (line 484) | async fn test_setting_content_type_header() {
  function test_setting_basic_auth (line 510) | async fn test_setting_basic_auth(http_version_param: &str) {
  function test_setting_host (line 527) | async fn test_setting_host() {
  function test_setting_method (line 545) | async fn test_setting_method() {
  function test_query (line 623) | async fn test_query() {
  function test_query_rand_regex (line 642) | async fn test_query_rand_regex() {
  function test_redirect (line 671) | async fn test_redirect() {
  function test_connect_to (line 683) | async fn test_connect_to() {
  function test_connect_to_randomness (line 691) | async fn test_connect_to_randomness() {
  function test_connect_to_ipv6_target (line 698) | async fn test_connect_to_ipv6_target() {
  function test_connect_to_ipv6_requested (line 706) | async fn test_connect_to_ipv6_requested() {
  function test_connect_to_redirect (line 711) | async fn test_connect_to_redirect() {
  function test_connect_to_http_proxy_override (line 719) | async fn test_connect_to_http_proxy_override() {
  function test_connect_to_https_proxy_connect_override (line 777) | async fn test_connect_to_https_proxy_connect_override() {
  function test_ipv6 (line 822) | async fn test_ipv6() {
  function test_query_limit (line 848) | async fn test_query_limit() {
  function test_query_limit_with_time_limit (line 857) | async fn test_query_limit_with_time_limit() {
  function test_http_versions (line 863) | async fn test_http_versions() {
  function test_unix_socket (line 882) | async fn test_unix_socket() {
  function make_root_issuer (line 919) | fn make_root_issuer() -> rcgen::Issuer<'static, rcgen::KeyPair> {
  function bind_proxy (line 938) | async fn bind_proxy<S>(service: S, http2: bool) -> (u16, impl Future<Out...
  function bind_proxy_with_recorder (line 994) | async fn bind_proxy_with_recorder<S>(
  function test_proxy_with_setting (line 1059) | async fn test_proxy_with_setting(https: bool, http2: bool, proxy_http2: ...
  function test_proxy (line 1108) | async fn test_proxy() {
  function test_google (line 1119) | async fn test_google() {
  function test_json_schema (line 1138) | async fn test_json_schema() {
  function test_csv_output (line 1202) | async fn test_csv_output() {
  function setup_mtls_server (line 1253) | fn setup_mtls_server(
  function test_mtls (line 1320) | async fn test_mtls() {
  function test_body_path_lines (line 1342) | async fn test_body_path_lines() {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (389K chars).
[
  {
    "path": ".dockerignore",
    "chars": 22,
    "preview": "*\n\n!pgo\n!src\n!Cargo.*\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 815,
    "preview": "# These are supported funding model platforms\n\ngithub: hatoo # Replace with up to 4 GitHub Sponsors-enabled usernames e."
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 726,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/CI.yml",
    "chars": 1301,
    "preview": "on:\n  push:\n    branches:\n      - master\n  pull_request:\n\nname: CI\n\njobs:\n  test:\n    name: Test Suite\n    runs-on: ${{ "
  },
  {
    "path": ".github/workflows/release-pgo.yml",
    "chars": 2439,
    "preview": "name: Publish PGO\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*.*.*\"\n  pull_request:\n\njobs:\n  publish:"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 4266,
    "preview": "name: Publish\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - \"v*.*.*\"\n  pull_request:\n\njobs:\n  publish:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 26,
    "preview": "**/target\n/.idea\n/.vscode\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 5824,
    "preview": "# Unreleased\n\n# 1.14.0\n\n- fix Possible bug with latency_correction #857\n\n# 1.13.0 (2025-02-07)\n\n- Add first byte stats t"
  },
  {
    "path": "Cargo.toml",
    "chars": 3680,
    "preview": "[package]\nauthors = [\"hatoo <hato2000@gmail.com>\"]\ncategories = [\n    \"command-line-utilities\",\n    \"network-programming"
  },
  {
    "path": "Cross.toml",
    "chars": 192,
    "preview": "# For Asahi linux\n[target.aarch64-unknown-linux-gnu.env]\npassthrough = [\"JEMALLOC_SYS_WITH_LG_PAGE=16\"]\n\n[target.aarch64"
  },
  {
    "path": "Dockerfile",
    "chars": 772,
    "preview": "ARG RUST_VERSION=slim\nFROM docker.io/library/rust:${RUST_VERSION} AS chef\n\nRUN cargo install cargo-chef --locked\nRUN apt"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2020 hatoo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 13074,
    "preview": "# oha (おはよう)\n\n[![GitHub Actions](https://github.com/hatoo/oha/workflows/CI/badge.svg)](https://github.com/hatoo/oha/acti"
  },
  {
    "path": "pgo/server/Cargo.toml",
    "chars": 237,
    "preview": "[package]\nname = \"server\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-"
  },
  {
    "path": "pgo/server/src/main.rs",
    "chars": 570,
    "preview": "use std::net::SocketAddr;\nuse tokio::net::TcpListener;\n\nuse axum::{routing::get, Router};\n\n#[tokio::main]\nasync fn main("
  },
  {
    "path": "pgo.js",
    "chars": 485,
    "preview": "import { $ } from \"bun\";\n\nlet additional = [];\n\nif (Bun.argv.length >= 3) {\n    additional = Bun.argv.slice(2);\n}\n\nlet s"
  },
  {
    "path": "schema.json",
    "chars": 10743,
    "preview": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"description\": \"JSON schema for the output of the `oha "
  },
  {
    "path": "src/aws_auth.rs",
    "chars": 3760,
    "preview": "use anyhow::Result;\n\nuse bytes::Bytes;\nuse hyper::{\n    HeaderMap,\n    header::{self, HeaderName},\n};\nuse thiserror::Err"
  },
  {
    "path": "src/cli.rs",
    "chars": 2942,
    "preview": "use hyper::http::header::{HeaderName, HeaderValue};\nuse std::str::FromStr;\n\npub fn parse_header(s: &str) -> Result<(Head"
  },
  {
    "path": "src/client.rs",
    "chars": 109841,
    "preview": "use bytes::Bytes;\n#[cfg(test)]\nuse hickory_resolver::config::{ResolverConfig, ResolverOpts};\nuse http_body_util::{BodyEx"
  },
  {
    "path": "src/client_h3.rs",
    "chars": 36684,
    "preview": "use bytes::Buf;\nuse bytes::Bytes;\nuse core::sync::atomic::Ordering;\nuse http::Request;\nuse http_body_util::BodyExt;\nuse "
  },
  {
    "path": "src/curl_compat.rs",
    "chars": 11603,
    "preview": "//! Curl compatibility utilities\nuse std::str::FromStr;\n\npub struct Form {\n    pub boundary: String,\n    pub parts: Vec<"
  },
  {
    "path": "src/db.rs",
    "chars": 2536,
    "preview": "use rusqlite::Connection;\n\nuse crate::client::{Client, RequestResult};\n\nfn create_db(conn: &Connection) -> Result<usize,"
  },
  {
    "path": "src/histogram.rs",
    "chars": 2327,
    "preview": "pub fn histogram(values: &[f64], bins: usize) -> Vec<(f64, usize)> {\n    assert!(bins >= 2);\n    let mut bucket: Vec<usi"
  },
  {
    "path": "src/lib.rs",
    "chars": 36579,
    "preview": "use anyhow::Context;\nuse aws_auth::AwsSignatureConfig;\nuse bytes::Bytes;\nuse clap::Parser;\nuse crossterm::tty::IsTty;\nus"
  },
  {
    "path": "src/main.rs",
    "chars": 830,
    "preview": "use clap::{CommandFactory, Parser};\nuse oha::{Opts, run};\n\nfn main() {\n    let num_workers_threads = std::env::var(\"TOKI"
  },
  {
    "path": "src/monitor.rs",
    "chars": 17054,
    "preview": "use byte_unit::Byte;\nuse crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};\nuse hyper::http;\nuse ratatui::{Defa"
  },
  {
    "path": "src/pcg64si.rs",
    "chars": 2503,
    "preview": "use std::convert::Infallible;\n\n// https://github.com/imneme/pcg-c\nuse rand::{SeedableRng, TryRng, rand_core::utils::fill"
  },
  {
    "path": "src/printer.rs",
    "chars": 22756,
    "preview": "use crate::{result_data::ResultData, timescale::TimeScale};\nuse average::{Max, Min, Variance};\nuse byte_unit::Byte;\nuse "
  },
  {
    "path": "src/request_generator.rs",
    "chars": 2969,
    "preview": "use std::borrow::Cow;\n\nuse bytes::Bytes;\nuse http_body_util::Full;\nuse hyper::http;\nuse hyper::{HeaderMap, Method, Versi"
  },
  {
    "path": "src/result_data.rs",
    "chars": 9781,
    "preview": "use std::{\n    collections::BTreeMap,\n    time::{Duration, Instant},\n};\n\nuse average::{Estimate, Max, Mean, Min, concate"
  },
  {
    "path": "src/timescale.rs",
    "chars": 9043,
    "preview": "use std::{fmt, time::Duration};\n\n#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]\npub enum TimeScale {\n    "
  },
  {
    "path": "src/tls_config.rs",
    "chars": 6044,
    "preview": "#[cfg(feature = \"rustls\")]\npub struct RuslsConfigs {\n    no_alpn: std::sync::Arc<rustls::ClientConfig>,\n    alpn_h2: std"
  },
  {
    "path": "src/url_generator.rs",
    "chars": 4510,
    "preview": "use std::{borrow::Cow, string::FromUtf8Error};\n\nuse rand::prelude::*;\nuse rand_regex::Regex;\nuse thiserror::Error;\nuse u"
  },
  {
    "path": "tests/common/mod.rs",
    "chars": 3996,
    "preview": "use std::{net::SocketAddr, sync::Arc};\n\nuse bytes::{Buf, Bytes};\nuse http::{Request, Response};\nuse kanal::Sender;\nuse r"
  },
  {
    "path": "tests/tests.rs",
    "chars": 41891,
    "preview": "use std::{\n    convert::Infallible,\n    error::Error as StdError,\n    fs::File,\n    future::Future,\n    io::Write,\n    n"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the hatoo/oha GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (365.1 KB), approximately 82.6k tokens, and a symbol index with 294 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!