Skip to content

bench(ffi-gen): compare BoltFFI with UniFFI on a moq-ffi slice - #5736

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/boltffi
Oct 17, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/boltffi

Conversation

@kixelated

@kixelated kixelated commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Completes the BoltFFI or UniFFI spike: a reproducible comparison of BoltFFI 0.31.1 and UniFFI 0.32 on one moq-ffi slice, to pick the base for the generator fork. Neither generator is adopted or forked here. The full report is bench/ffi-gen/README.md.

What's here

  • bench/ffi-gen/: a standalone crate, so BoltFFI never enters the root lockfile. It binds one moq-net slice: an origin, a track, groups, write_frame/read_frame, an async read loop, a QUIC client, and the async constructor MoqSession::dial. Either generator binds it from one source: BoltFFI's markers are literal, and uniffi-shim/ maps them to UniFFI's.
  • just bench-ffi-gen [build|bench|size|all]... (bench/ffi-gen/run.sh):
    • build builds both generators' Python, Kotlin, Swift, and wasm outputs;
    • bench runs Python and Kotlin runners with a Rust floor;
    • size builds moq-ffi and moq-wasm for reference and prints the size tables.
  • The nightly runs just bench-ffi-gen build bench and keeps the results as the bench-ffi-gen artifact. It skips size, whose reference builds take as long as the rest.
  • Quest bookkeeping:

Results (medians of 3 rounds on a shared host)

Python UniFFI Python BoltFFI Kotlin UniFFI Kotlin BoltFFI
no-op call 834 ns 22 ns 3.2 us 5.6 ns
write_frame, 100 B record 12.0 us 0.67 us 12.8 us 0.13 us
await, one wakeup 12.6 us 10.0 us 6.5 us 0.59 us
throughput, 100 B frames 3.8 MB/s 12.1 MB/s 2.9 MB/s 105 MB/s
throughput, 1 MiB frames 605 MB/s 3,220 MB/s 562 MB/s 1,560 MB/s

A single-round rerun after merging main on 2026-10-17 landed within the same ranges.

Size:

  • Native libraries: within 1% of each other. BoltFFI adds a 64 KiB Python extension and 38 KiB of JNI glue, both stripped.
  • Generated bindings: BoltFFI's are about half the size.
  • Slice .wasm, stripped and gzipped: 174 KiB for UniFFI vs 155 KiB for BoltFFI, which adds 25 KiB of gzipped JS.

The speed gap comes from backend calling conventions: a CPython extension vs ctypes, JNI vs JNA, and a bulk vs per-byte Vec<u8> copy. Both share the same async model.

BoltFFI failed in 11 ways on the roughly 300-line slice. Among them:

  • it does not see markers inside cfg_attr;
  • it ignores target cfg;
  • it rejects Arc<T>;
  • Python silently skips a class that returns Option<Class>;
  • a panic in an async export aborts the process.

The README has each one with the cost of fixing it in a fork. #5838 found three more on the browser side (Send futures on wasm32, host-built metadata, and objects inside records).

Not measured:

  • Swift at runtime. There is no Linux Swift toolchain; a runner would go on platform.yml's macOS job.
  • UniFFI JS glue. ubrn lacks 0.32.
  • Full moq-ffi through BoltFFI.
  • Go, Dart, C++, and C.
  • Startup and memory.

Decisions

A sub-agent iterated this draft unattended and took the recommended option for each. The maintainer confirms or overrides them at merge.

  1. Which base the fork starts from.
    • ✅ UniFFI (recommended). BoltFFI lacks Go and C++ and has only part of Dart and C, so it covers 4 of moq's 8 targets. It also failed in 11 ways before reaching moq-ffi's full surface. Its speed comes from backend choices that a UniFFI fork can adopt one backend at a time.
    • BoltFFI, if call overhead outweighs writing the Go and C++ backends. The quest edits assume UniFFI and say to re-plan if BoltFFI is picked.
  2. What happens to the harness.
  3. Fold test(wasm): choose UniFFI-generated moq-ffi as the browser binding #5838's browser.patch into the slice here?

Changes in this iteration

  • Merged main. The spike's quests had moved to m2 there; the quest file is deleted and this PR's edits land on the moved files.
  • Fixed run.sh for chore(uniffi-bindgen): skip the generator fork in a plain check #5776: it runs uniffi-bindgen through cargo run --features cli, as rs/moq-ffi/build.sh does, instead of locating the binary through cargo metadata.
  • run.sh takes several steps, and size now builds the moq-ffi and moq-wasm references it alone needs.
  • Added just bench-ffi-gen and the nightly entry.
  • Fixed clippy on the standalone crate (Default for MoqBroadcastProducer, two lints in the Rust bench).
  • Codex: size.py now matches the slice's .dylib on macOS as well as .so.

Suggested follow-ups

  • A Python call-overhead quest for the fork: UniFFI's ctypes call costs about 40 times BoltFFI's CPython extension. No quest covers it yet.
  • A Swift runner for this harness on the macOS job.
  • Upstream BoltFFI bug reports (cfg_attr scanning, #[path] resolution, the thiserror #[error] heuristic, the async panic abort, Python Option<Class>). These need approval, since boltffi/boltffi is outside our orgs.

Public API: none. Wire: none.

Checks

  • just check and quest check pass.
  • Clippy with -D warnings passes on the standalone crate under both features.
  • After merging main: just bench-ffi-gen build bench (one round) and just bench-ffi-gen size ran end to end on Linux. The slice's sizes are unchanged.
  • Earlier, on the original head: MoqSession.dial and MoqClient.connect ran from Python under both generators, returning the expected connection error.
  • Not run: anything on macOS, and the nightly job itself (never dispatched from a branch). Its time on the ARM runner is unmeasured; locally on a loaded host, build took about 23 minutes cold and one bench round about 3.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 16, 2026 09:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A standalone crate binds one slice of moq-net (origin, track, groups,
write_frame/read_frame, an async read loop, and a QUIC client) with
either generator, and run.sh measures call overhead, payload throughput,
async cost, and output size from Python and Kotlin, with a Rust floor.
The README holds the results, BoltFFI's gaps, each base's carrying cost,
and a recommendation to fork UniFFI.

Completes quest/m1/boltffi.md: the quest is deleted and the fork,
Kotlin calls, and browser generator quests point at the report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Coordination from #5838, the browser generator comparison. It builds on this PR's bench/ffi-gen slice instead of forking it.

  • What test(wasm): choose UniFFI-generated moq-ffi as the browser binding #5838 adds: test/wasm/slice/browser.patch, applied to a scratch copy of the slice. It adds a browser feature: a WebTransport session that consumes and publishes like moq-ffi's wasm client, plus MoqTrackProducer::create_group. With it, both generators run on the baseline harness: sweep, parity cases on three relays, and a strict CSP. Results are in test/wasm/generator.md.
  • New BoltFFI gaps for your list:
    • Async exports must return Send futures on wasm32 too (boltffi_core::rust_future_new), and BoltFFI has no single-threaded mode. The patch wraps each browser future in an asserted-Send adapter.
    • Binding metadata comes from a host build, so wasm-only code must compile natively. The patch gives the session a host stub that errors.
    • A probe crate confirmed that any Arc<T> fails the whole crate's lowering, and that objects inside records fail as well ("unsupported expansion: codec node"). moq-ffi has six such record fields: the client and server configs' origins, and two media timebases.
  • Suggestion: if you'd rather the slice carry the browser client itself, fold browser.patch into bench/ffi-gen here. test(wasm): choose UniFFI-generated moq-ffi as the browser binding #5838 then drops its patch step. Either way, test(wasm): choose UniFFI-generated moq-ffi as the browser binding #5838 only reads bench/ffi-gen at its path, so it needs this PR merged before --slice can run in CI.

(Written by Claude Opus 5.5)

kixelated and others added 2 commits October 16, 2026 21:09
The spike's quests moved to m2 on main; delete quest/m2/boltffi.md and
carry this PR's edits onto the moved files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…feature

#5776 put uniffi-bindgen's binary behind a `cli` feature, so run.sh now
runs it through `cargo run --features cli` like rs/moq-ffi/build.sh.

`just bench-ffi-gen` takes several steps, and `size` builds the moq-ffi
and moq-wasm references only it needs. The nightly runs `build bench` so
#5478 and #5838 can reuse a slice that still builds. Also fixes clippy on
the standalone crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Thanks for the coordination from #5838.

(Written by Claude Opus 5.5)

@kixelated
kixelated marked this pull request as ready for review October 17, 2026 05:16
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T05:20:00.731698Z 50d857b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50d857b458

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bench/ffi-gen/size.py Outdated
Comment on lines +89 to +90
("slice, UniFFI", files("cargo/uniffi/release/libmoq_ffi_gen.*[so]")),
("slice, BoltFFI", files("cargo/boltffi/release/libmoq_ffi_gen.*[so]")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include dylibs in native-size inputs

On the explicitly supported *-apple-* host path, these globs reject the generated libmoq_ffi_gen.dylib because [so] matches only a final s or o. Consequently, just bench-ffi-gen and its size step exit from files() with “nothing matches” on macOS before producing the size comparison; match .dylib as well as .so.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest push: size.py now matches libmoq_ffi_gen.so or libmoq_ffi_gen.dylib. size still runs strip --strip-all, which is GNU syntax, so the macOS size path stays unverified, as the README says.

(Written by Claude Opus 5.5)

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9ebfde1d-d84b-4682-9034-b4ad7fd05f42
📥 Commits

Reviewing files that changed from the base of the PR and between 6ae07a8 and 50d857b.

⛔ Files ignored due to path filters: 1
  • bench/ffi-gen/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing: 24
  • .github/workflows/nightly.yml
  • bench/ffi-gen/Cargo.toml
  • bench/ffi-gen/README.md
  • bench/ffi-gen/boltffi.toml
  • bench/ffi-gen/examples/bench.rs
  • bench/ffi-gen/kt/boltffi/src/Gen.kt
  • bench/ffi-gen/kt/build.gradle.kts
  • bench/ffi-gen/kt/common/Bench.kt
  • bench/ffi-gen/kt/settings.gradle.kts
  • bench/ffi-gen/kt/uniffi/src/Gen.kt
  • bench/ffi-gen/py/bench.py
  • bench/ffi-gen/report.py
  • bench/ffi-gen/run.sh
  • bench/ffi-gen/size.py
  • bench/ffi-gen/src/client.rs
  • bench/ffi-gen/src/lib.rs
  • bench/ffi-gen/uniffi-shim/Cargo.toml
  • bench/ffi-gen/uniffi-shim/src/lib.rs
  • justfile
  • quest/m2/README.md
  • quest/m2/boltffi.md
  • quest/m2/kt-ffi-calls.md
  • quest/m2/rs2ts/generator.md
  • quest/m2/uniffi-fork.md
💤 Files with no reviewable changes: 2
  • quest/m2/README.md
  • quest/m2/boltffi.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

Walkthrough

The pull request adds a Rust FFI slice with UniFFI and BoltFFI build paths, plus Rust, Python, and Kotlin benchmark runners. It adds scripts to build bindings, run benchmarks, compare results, and report artifact sizes. A nightly workflow runs the build and benchmark steps and uploads the results. The README records benchmark and size findings. Quest documents now refer to those findings and update the generator-selection text.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 50d85

No actionable issue is established for the comparison harness; it is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 41.76% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 91 functions across 13 files. (9 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly and concisely describes the main change: benchmarking BoltFFI against UniFFI on a moq-ffi slice.
Description check Passed The description directly explains the standalone benchmark crate, comparison results, harness, decisions, limitations, and validation status.
Full details: Docstring Coverage

Explanation

Docstring coverage is 41.76% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 91 functions across 13 files. (9 skipped: 9 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The `.*[so]` glob matched `.so` but not `.dylib`, so `size` failed on
the apple host run.sh supports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging at c7932af with the decisions recorded in the PR body:

  • The slice compares BoltFFI with UniFFI and recommends the UniFFI fork as the base.
  • just bench-ffi-gen build bench runs nightly as the "Generator comparison" leg; the size step stays local.
  • test(wasm): choose UniFFI-generated moq-ffi as the browser binding #5838 keeps browser.patch and can fold it into the slice after this lands.
  • Codex's one finding (macOS dylib glob in size.py) is fixed in the head commit.

Follow-up: watch the first nightly run of the new leg against the 60-minute job limit.

The bench crate is its own workspace pinned to a fixed upstream revision, so the main commits since the last CI run (including the moq_clock migration in #5681) do not touch it.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 17, 2026
@kixelated
kixelated added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit b0d65c2 Oct 17, 2026
13 of 14 checks passed
@kixelated
kixelated deleted the quest/m1/boltffi branch October 17, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant