brogz 0.1.4

Measure wire bytes and timing for identity / gzip / brotli responses across a site's assets.
Documentation

brogz

CI License: MIT OR Apache-2.0

Measure wire bytes and timing for identity, gzip, and brotli responses across a site's static assets. One static binary — no Node, no curl in your PATH.

br + gz — the two encodings we compare.

┌─────────────────────────────────────────┬──────────┬─────────┬───────┬─────────┬───────┬──────────┐
│ path                                    │ raw      │ gzip    │ gz CE │ br      │ br CE │ br vs gz │
╞═════════════════════════════════════════╪══════════╪═════════╪═══════╪═════════╪═══════╪══════════╡
│ /index.html                             │  38.59KB │  9.89KB │ gzip  │  7.85KB │ br    │   -20.6% │
│ /assets/index-yrrrWp82.js               │  17.19KB │  6.75KB │ gzip  │  5.88KB │ br    │   -12.9% │
│ /assets/vendor-DCLJsiZC.js              │ 185.20KB │ 57.48KB │ gzip  │ 49.58KB │ br    │   -13.7% │
│ Σ TOTAL                                 │ 306.95KB │ 96.27KB │       │ 82.96KB │       │   -13.8% │
└─────────────────────────────────────────┴──────────┴─────────┴───────┴─────────┴───────┴──────────┘

Install

# Homebrew (macOS, Linux)
brew tap Segodnya/brogz
brew install brogz

# crates.io (anywhere with a Rust toolchain)
cargo install brogz

# or grab a prebuilt binary from the latest GitHub Release:
# https://github.com/Segodnya/brogz/releases

Prebuilt targets: x86_64/aarch64-unknown-linux-gnu, x86_64/aarch64-apple-darwin, x86_64-pc-windows-msvc.

The Homebrew tap lives at Segodnya/homebrew-brogz and is bumped automatically on every new release.

Usage

brogz <URL> [OPTIONS]

Arguments:
  <URL>                       Base URL, e.g. https://app.example

Options:
  -r, --runs <N>              Probes per URL × encoding [default: 10]
  -c, --concurrency <N>       Max parallel requests [default: 3 * runs]
  -k, --insecure              Skip TLS verification
  -o, --out <FILE>            Write JSON report to file (in addition to stdout table)
  -p, --path <PATH>           Skip discovery; measure these paths instead (repeatable)
      --no-color              Disable colored output
  -q, --quiet                 Suppress progress lines on stderr
  -h, --help, -V, --version

Common invocations:

# Auto-discover assets from /index.html, default 10 probes per URL × encoding.
brogz https://app.example

# More probes for tighter medians; default concurrency follows runs.
brogz https://app.example --runs 20

# Save the JSON report alongside the stdout table.
brogz https://app.example -o report.json

# Skip discovery entirely — measure exactly these paths.
brogz https://app.example -p /index.html -p /assets/app.js -p /assets/app.css

# Dev environments behind an internal CA.
brogz https://scheduling-dev.example -k

What it measures

For every URL × encoding pair, brogz sends --runs parallel HTTP requests with the matching Accept-Encoding header, records the wire byte count and total wall-clock time, and reports:

field meaning
bytes median wire body length across probes (not decompressed)
bytesMin smallest wire length seen across probes
bytesMax largest wire length seen across probes
contentEncoding Content-Encoding header from the final response
medianMs median of per-probe wall-clock time (Math.round-compatible)

On a static asset all probes agree, so bytes == bytesMin == bytesMax. Public HTML (CSRF tokens, request IDs, A/B variants) produces slightly different bodies per request — the median is representative, and bytesMin/bytesMax expose the spread. Content-Encoding mismatches across probes log a warn! to stderr; status mismatches or non-200 responses abort the URL.

Output

A unicode table (with optional colors) to stdout. The full report — including per-probe medians and totals — is written to JSON when --out is set:

{
  "baseUrl": "https://app.example",
  "runs": 10,
  "generatedAt": "2026-05-24T10:30:45Z",
  "measurements": [
    {
      "path": "/index.html",
      "identity": { "bytes": 39516, "bytesMin": 39516, "bytesMax": 39516, "contentEncoding": "identity", "medianMs": 28 },
      "gzip":     { "bytes": 10128, "bytesMin": 10112, "bytesMax": 10145, "contentEncoding": "gzip",     "medianMs": 30 },
      "br":       { "bytes":  8037, "bytesMin":  8001, "bytesMax":  8062, "contentEncoding": "br",       "medianMs": 32 }
    }
  ],
  "totals": { "identity": 314313, "gzip": 98581, "br": 84953 }
}

The JSON shape is pinned by a unit test so historical reports can be diffed with jq -S across versions without spurious churn.

Library usage

The crate exposes everything the CLI uses, behind a feature-gated split so library consumers do not pull in clap / comfy-table / crossterm:

[dependencies]
brogz = { version = "0.1", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
url   = "2"
use brogz::Config;
use url::Url;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let report = brogz::run(Config {
        base_url: Url::parse("https://app.example")?,
        runs: 10,
        concurrency: 30,
        insecure: false,
        paths: None, // None -> discover from /index.html
        progress: None, // or Some(Arc::new(|e| { ... })) to receive ProgressEvent
    }).await?;

    println!("br savings vs gzip: {} -> {} bytes",
        report.totals.gzip, report.totals.br);
    Ok(())
}

The lower-level building blocks are also public: discover_urls, measure_url, measure_encoding, probe, build_client, median.

Exit codes

code meaning
0 success
1 runtime error (transport, non-200, inconsistent status)
2 invalid CLI arguments

License

Dual-licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.