Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ jobs:
run: cargo install cross --locked

- name: Build
run: ${{ matrix.use_cross && 'cross' || 'cargo' }} build --target ${{ matrix.target }} --all-features --examples
run: ${{ matrix.use_cross && 'cross' || 'cargo' }} build --target ${{ matrix.target }} --features tokio --examples
shell: bash

- name: Test
if: matrix.run_tests
run: ${{ matrix.use_cross && 'cross' || 'cargo' }} test --target ${{ matrix.target }} --all-features
run: ${{ matrix.use_cross && 'cross' || 'cargo' }} test --target ${{ matrix.target }} --features tokio
shell: bash

- name: Build proxytester (release)
Expand Down Expand Up @@ -150,12 +150,66 @@ jobs:
OS_PROXY_RESOLVER_OS_TESTS: "1"
run: |
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
dbus-run-session -- cargo test --all-features os_roundtrip -- --nocapture
dbus-run-session -- cargo test --features tokio os_roundtrip -- --nocapture
else
cargo test --all-features os_roundtrip -- --nocapture
cargo test --features tokio os_roundtrip -- --nocapture
fi
shell: bash

pac-bench:
name: WinHTTP vs QuickJS PAC benchmark (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust
run: |
rustup toolchain install stable --profile minimal --no-self-update
rustup default stable
shell: bash

- uses: Swatinem/rust-cache@v2
with:
key: pac-bench

# The `pac-engine` feature additionally compiles the embedded QuickJS PAC
# engine on Windows (it is always built off Windows) so it can be timed
# against WinHTTP on the same PAC script. This is the only build that
# links QuickJS on Windows — production Windows stays WinHTTP-only / pure
# Rust. windows-latest ships the MSVC toolchain the QuickJS C sources need.
- name: Run WinHTTP vs QuickJS PAC benchmark
run: cargo run --release --example pac_bench --features pac-engine -- --iterations 3000
shell: bash

electron-pac-bench:
name: Electron (Chromium) PAC baseline (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24

# No lockfile is committed (Electron pulls a large platform binary), so
# install rather than `npm ci`.
- name: Install Electron
run: npm install --no-audit --no-fund
working-directory: bench/electron
shell: bash

# Baseline for the Rust pac_bench numbers above: Chromium's own V8 PAC
# resolver, which is what Electron uses by default (WinHTTP is only used
# with --use-system-proxy-resolver). Same built-in PAC and URLs.
# resolveProxy is an async IPC to the network service and is throughput-
# serialized: --concurrency 32 shows it barely lifts throughput (~1.2x),
# which is the evidence that the ~250-310 calls/s ceiling is async-IPC
# cost (plus Windows' ~15.6ms timer granularity), not PAC evaluation.
- name: Run Electron PAC baseline
run: npm run bench -- --iterations 3000 --concurrency 32
working-directory: bench/electron
shell: bash

lint:
name: rustfmt + clippy + docs
runs-on: macos-latest
Expand Down
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ crate-type = ["rlib", "cdylib"]
default = []
# Async change notification via tokio::sync::watch.
tokio = ["dep:tokio"]
# Also build the embedded QuickJS PAC engine on Windows (it is always built off
# Windows). This is only for the `pac_bench` example, which compares WinHTTP
# against the embedded engine — production Windows stays WinHTTP-only / pure
# Rust, so this is deliberately *not* part of the default feature set.
pac-engine = ["dep:rquickjs-sys"]

[dependencies]
url = "2"
Expand All @@ -44,6 +49,11 @@ windows-sys = { version = "0.60", features = [
"Win32_System_Registry",
"Win32_System_Threading",
] }
# Optional on Windows so the default build stays pure Rust (WinHTTP handles
# PAC). Enabled by the `pac-engine` feature for the WinHTTP-vs-QuickJS
# benchmark. Off Windows the engine is always built (see the not(windows)
# table above), where this crate is a required dependency.
rquickjs-sys = { version = "0.12.1", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ cargo run --example resolve -- --watch # watch for changes
cargo run --example proxytester -- --pac-script file.pac http://url/ # test a PAC file
```

To compare the two PAC engines head-to-head — WinHTTP versus the embedded
QuickJS engine — on the same script and URLs, run the `pac_bench` example. On
Windows the QuickJS side is only built with the `pac-engine` feature (off
Windows the engine is always built); production Windows builds never link it:

```sh
cargo run --release --example pac_bench --features pac-engine
```

Builds as both `rlib` and `cdylib`. Release automation with `cargo-dist` is a
natural fit (the CI matrix below already covers the seven targets) but is not
wired up yet.
Expand All @@ -137,7 +146,11 @@ wired up yet.

GitHub Actions builds and tests: Windows x64 + arm64 (pure Rust), macOS x64 +
arm64, Linux x86_64 (native), Linux aarch64 + armv7 (via `cross`, whose images
ship the C cross-toolchain QuickJS needs).
ship the C cross-toolchain QuickJS needs). Two Windows benchmark jobs establish
the performance picture on the same runner: `pac_bench`
(`--features pac-engine`) times WinHTTP against the embedded QuickJS engine, and
[`bench/electron`](bench/electron) times Chromium's own V8 PAC resolver (what
Electron uses by default) as the baseline.

## License

Expand Down
2 changes: 2 additions & 0 deletions bench/electron/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
85 changes: 85 additions & 0 deletions bench/electron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Electron (Chromium) PAC baseline

A tiny [Electron](https://www.electronjs.org/) app that times Chromium's
built-in V8 PAC resolver via [`session.resolveProxy`](https://www.electronjs.org/docs/latest/api/session#sesresolveproxyurl),
so it can serve as the **baseline** for the Rust
[`pac_bench`](../../examples/pac_bench.rs) example.

## Why Electron?

Chromium — and therefore Electron — evaluates PAC scripts with its own
V8-based resolver by default. The OS resolver (WinHTTP on Windows,
`SystemConfiguration` on macOS) is only used with
`--use-system-proxy-resolver`, which is **not** the default. So "what Electron
actually does" for PAC is the Chromium V8 path, and that is what this harness
measures.

Put next to the Rust `pac_bench` example you get three numbers on the same
Windows machine, same PAC, same URLs:

| path | engine | measured by |
|---|---|---|
| `system` | WinHTTP | `pac_bench` (Rust) |
| `quickjs` | embedded QuickJS-NG | `pac_bench --features pac-engine` (Rust) |
| `electron` | Chromium V8 | this harness |

## Running

```sh
npm install
npm run bench -- --iterations 3000 --concurrency 32
```

Options (defaults match `examples/pac_bench.rs`):

- `--iterations N` — timed calls per run (default 2000).
- `--concurrency N` — additionally run a pass with N `resolveProxy` calls in
flight (default 1 = sequential only). See the caveats below for why this
matters.
- `--pac-script <path>` — PAC file to evaluate (default: the same built-in
script as the Rust example).
- `--data-url` — load the PAC as a `data:` URL instead of over HTTP. Chromium
supports this; **WinHTTP does not**, which is one of the capability gaps
behind Chromium avoiding WinHTTP.
- `--unique-hosts` — rewrite each request host to be unique, defeating any
per-endpoint caching so raw evaluation cost is measured. `<url>...` —
override the URL list.

## Reading the numbers (caveats)

- **`resolveProxy` is asynchronous cross-process IPC, not an in-process call.**
The benchmark runs in Electron's **main process** (`app.whenReady`, no
renderer) and times `session.resolveProxy()`, but the PAC script is actually
evaluated **out-of-process** in Chromium's network service — so each call is a
Mojo round-trip (main → network service → main), not a local V8 call in the
measuring process. The Rust `pac_bench` paths (WinHTTP, embedded QuickJS) are
synchronous in-process calls, so they measure PAC evaluation itself (~170 µs).
The Electron numbers measure evaluation **plus** the per-call IPC and
event-loop latency, and there is no public API to time Chromium's V8 PAC eval
without that IPC hop.
- **`resolveProxy` is throughput-serialized; concurrency does not help.** The
CI run bears this out: the engine's `min` latency is ~100 µs (PAC eval is
fast, and this is *not* cold start — a warmup pass runs first), yet throughput
tops out around **250–310 calls/s on Windows** (≈1300/s on macOS), and raising
`--concurrency` barely moves it (≈1.2×) while per-call latency balloons into
queuing time. In other words Chromium resolves proxies one-at-a-time through
its single-threaded resolver, so the ceiling is the async-IPC round-trip cost
(amplified on Windows by the ~15.6 ms default timer/scheduler granularity),
not PAC evaluation. `--concurrency` is kept because demonstrating that it
*doesn't* lift throughput is exactly the evidence for serialization.
- **Don't compare this to the in-process numbers as an engine benchmark.** The
~20× gap between Electron's ~250/s and WinHTTP/QuickJS's ~5000/s is the cost
of an async, serialized, cross-process API — not the V8 PAC engine being slow.
For engine-vs-engine, compare WinHTTP against the embedded QuickJS in
`pac_bench` (both ~170–200 µs, i.e. at parity).
- **Caching differs per engine.** WinHTTP keeps a session autoproxy cache, so
its steady-state `pac_bench` numbers reflect cache hits. Chromium generally
re-runs the PAC per resolution. The Rust `quickjs` path re-evaluates every
call but keeps the compiled script. For a raw eval-cost comparison, run every
tool in its cache-defeating mode (`--unique-hosts` here). For a realistic
"what a request pays" comparison, use the default modes.
- The `resolutions:` block printed before the timings lets you diff Chromium's
output against the Rust harness's `cross-check` output for the same URLs.

Pinned to Electron 42.5.0 (the version VS Code currently ships); any recent
Electron works if you bump `package.json`.
Loading
Loading