From 5303e073a117dee8c17669143c4f6e85995b77e3 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Wed, 25 Mar 2026 22:14:19 -0600 Subject: [PATCH 1/6] Naive benchmarking pass --- .gitignore | 2 + Cargo.lock | 352 +++++++++++++++++- Cargo.toml | 5 + benches/benchmarks.rs | 122 ++++++ benchmarks/build.gradle.kts | 62 +++ .../uniffi/benchmarks/CallbackBenchmarks.java | 88 +++++ .../benchmarks/FunctionCallBenchmarks.java | 139 +++++++ .../uniffi/benchmarks/JavaCallbackImpl.java | 184 +++++++++ flake.nix | 2 +- 9 files changed, 949 insertions(+), 7 deletions(-) create mode 100644 benches/benchmarks.rs create mode 100644 benchmarks/build.gradle.kts create mode 100644 benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java create mode 100644 benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java create mode 100644 benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java diff --git a/.gitignore b/.gitignore index 1833b88..1679095 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target .direnv *.class +benchmarks/build/ +benchmarks/.gradle/ diff --git a/Cargo.lock b/Cargo.lock index be046bf..4fa0d16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,62 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -161,7 +211,7 @@ dependencies = [ "rustix", "slab", "tracing", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -317,6 +367,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.0" @@ -332,6 +388,33 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.39" @@ -348,6 +431,7 @@ version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ + "anstream", "anstyle", "clap_lex", "strsim", @@ -371,6 +455,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -380,12 +470,73 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "displaydoc" version = "0.2.5" @@ -416,7 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -619,6 +770,17 @@ dependencies = [ "scroll", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.15.3" @@ -755,6 +917,32 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -877,6 +1065,18 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "parking" version = "2.2.1" @@ -924,6 +1124,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polling" version = "3.8.0" @@ -936,7 +1164,7 @@ dependencies = [ "pin-project-lite", "rustix", "tracing", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -972,6 +1200,26 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.11.1" @@ -1023,7 +1271,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1038,6 +1286,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scroll" version = "0.12.0" @@ -1185,7 +1442,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -1229,6 +1486,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.45.1" @@ -1339,7 +1606,7 @@ dependencies = [ "clap", "glob", "heck", - "itertools", + "itertools 0.14.0", "once_cell", "paste", "regex", @@ -1351,6 +1618,7 @@ dependencies = [ "uniffi-example-futures", "uniffi-example-geometry", "uniffi-example-rondpoint", + "uniffi-fixture-benchmarks", "uniffi-fixture-coverall", "uniffi-fixture-ext-types", "uniffi-fixture-futures", @@ -1410,6 +1678,18 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-fixture-benchmarks" +version = "0.22.0" +source = "git+https://github.com/mozilla/uniffi-rs.git?tag=v0.31.0#309762f55db3f0548194a9ceba3027fa64b18a93" +dependencies = [ + "clap", + "criterion", + "regex", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-fixture-coverall" version = "0.22.0" @@ -1746,12 +2026,28 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "value-bag" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -1859,6 +2155,21 @@ dependencies = [ "nom", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -1868,6 +2179,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1986,6 +2306,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 126becb..b2c45c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,4 +56,9 @@ uniffi-fixture-proc-macro = { git = "https://github.com/mozilla/uniffi-rs.git", uniffi-fixture-rename = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-trait-methods = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } +uniffi-fixture-benchmarks = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi_testing = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } + +[[bench]] +name = "benchmarks" +harness = false diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs new file mode 100644 index 0000000..6d7b718 --- /dev/null +++ b/benches/benchmarks.rs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! JMH benchmark harness for uniffi-bindgen-java +//! +//! This builds the uniffi-fixture-benchmarks cdylib, generates Java bindings, +//! copies them into the Gradle benchmark project, and runs JMH. +//! +//! Usage: +//! cargo bench +//! cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 2 -i 3" # pass args to JMH + +use anyhow::{Context, Result, bail}; +use camino::Utf8PathBuf; +use std::env; +use std::env::consts::ARCH; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use uniffi_bindgen::{BindgenLoader, BindgenPaths}; +use uniffi_bindgen_java::{GenerateOptions, generate}; +use uniffi_testing::UniFFITestHelper; + +fn main() -> Result<()> { + let jmh_args = parse_jmh_args(); + let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let bench_project = project_root.join("benchmarks"); + let generated_dir = bench_project.join("build/generated-sources/uniffi"); + + // Clean and recreate the generated sources directory + if generated_dir.exists() { + fs::remove_dir_all(&generated_dir)?; + } + fs::create_dir_all(&generated_dir)?; + + // Build the benchmarks fixture cdylib + println!("Building benchmarks fixture..."); + let test_helper = UniFFITestHelper::new("uniffi-fixture-benchmarks")?; + let cdylib_path = test_helper.cdylib_path()?; + println!(" cdylib: {cdylib_path}"); + + // Generate Java bindings + println!("Generating Java bindings..."); + let out_dir = Utf8PathBuf::from(generated_dir.to_string_lossy().to_string()); + + let mut paths = BindgenPaths::default(); + paths.add_cargo_metadata_layer(false)?; + let loader = BindgenLoader::new(paths); + + generate( + &loader, + &GenerateOptions { + source: cdylib_path.clone(), + out_dir: out_dir.clone(), + format: false, + crate_filter: None, + }, + )?; + + // Copy the native library into JNA's expected resource path so it gets packaged + // into the JMH jar. JNA looks for native libs in {os}-{arch}/ inside the classpath. + let jna_resource_folder = if cdylib_path.extension().unwrap() == "dylib" { + format!("darwin-{}", ARCH).replace('_', "-") + } else { + format!("linux-{}", ARCH).replace('_', "-") + }; + let native_resource_dir = bench_project + .join("build/native-resources") + .join(&jna_resource_folder); + fs::create_dir_all(&native_resource_dir)?; + let cdylib_dest = native_resource_dir.join(cdylib_path.file_name().unwrap()); + fs::copy(cdylib_path.as_std_path(), &cdylib_dest)?; + println!(" native lib: {}", cdylib_dest.display()); + + // Run JMH via Gradle + println!("Running JMH benchmarks..."); + let mut cmd = Command::new("gradle"); + cmd.current_dir(&bench_project) + .arg("jmh") + .arg("--no-daemon") + .arg("--console=plain") + .env( + "UNIFFI_JNA_CLASSPATH", + env::var("CLASSPATH").unwrap_or_default(), + ); + + // Pass JMH args via project property + if !jmh_args.is_empty() { + let args_str = jmh_args.join(" "); + cmd.arg(format!("-PjmhArgs={args_str}")); + } + + let status = cmd + .spawn() + .context("Failed to spawn gradle. Is gradle available in your PATH (nix develop)?")? + .wait() + .context("Failed to wait for gradle")?; + + if !status.success() { + bail!("JMH benchmark run failed"); + } + + println!("\nResults written to: benchmarks/build/results/jmh/"); + Ok(()) +} + +fn parse_jmh_args() -> Vec { + env::args() + .skip_while(|a| a != "--") + .skip(1) + .flat_map(|a| { + if let Some(args) = a.strip_prefix("--jmh-args=") { + args.split_whitespace() + .map(String::from) + .collect::>() + } else { + vec![a] + } + }) + .collect() +} diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts new file mode 100644 index 0000000..a27695f --- /dev/null +++ b/benchmarks/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + java + id("me.champeau.jmh") version "0.7.3" +} + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +dependencies { + // JNA for FFI - pick up from CLASSPATH or UNIFFI_JNA_CLASSPATH env var (provided by nix) + val jnaClasspath = System.getenv("UNIFFI_JNA_CLASSPATH") + ?: System.getenv("CLASSPATH") + ?: "" + if (jnaClasspath.isNotEmpty()) { + implementation(files(jnaClasspath.split(":"))) + } +} + +jmh { + warmupIterations.set(3) + iterations.set(5) + fork.set(2) + resultsFile.set(layout.buildDirectory.file("results/jmh/results.txt")) + // Allow overriding JMH args via -PjmhArgs="..." + if (project.hasProperty("jmhArgs")) { + val args = (project.property("jmhArgs") as String).split(" ") + val iter = args.iterator() + while (iter.hasNext()) { + when (val arg = iter.next()) { + "-f" -> if (iter.hasNext()) fork.set(iter.next().toInt()) + "-wi" -> if (iter.hasNext()) warmupIterations.set(iter.next().toInt()) + "-i" -> if (iter.hasNext()) iterations.set(iter.next().toInt()) + "-w" -> if (iter.hasNext()) warmup.set(iter.next()) + "-r" -> if (iter.hasNext()) timeOnIteration.set(iter.next()) + else -> { + if (!arg.startsWith("-")) { + includes.add(arg) + } + } + } + } + } +} + +sourceSets { + main { + java { + srcDir(layout.buildDirectory.dir("generated-sources/uniffi")) + } + resources { + // Native library packaged for JNA resource loading + srcDir(layout.buildDirectory.dir("native-resources")) + } + } +} diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java b/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java new file mode 100644 index 0000000..3414a43 --- /dev/null +++ b/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package uniffi.benchmarks; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +public class CallbackBenchmarks { + + private JavaCallbackImpl callback; + + @Setup + public void setup() { + callback = new JavaCallbackImpl(); + } + + @Benchmark + public void callOnly() { + Benchmarks.runCallbackTest(callback, TestCase.CALL_ONLY, 1L); + } + + @Benchmark + public void primitives() { + Benchmarks.runCallbackTest(callback, TestCase.PRIMITIVES, 1L); + } + + @Benchmark + public void strings() { + Benchmarks.runCallbackTest(callback, TestCase.STRINGS, 1L); + } + + @Benchmark + public void largeStrings() { + Benchmarks.runCallbackTest(callback, TestCase.LARGE_STRINGS, 1L); + } + + @Benchmark + public void records() { + Benchmarks.runCallbackTest(callback, TestCase.RECORDS, 1L); + } + + @Benchmark + public void enums() { + Benchmarks.runCallbackTest(callback, TestCase.ENUMS, 1L); + } + + @Benchmark + public void vecs() { + Benchmarks.runCallbackTest(callback, TestCase.VECS, 1L); + } + + @Benchmark + public void hashmaps() { + Benchmarks.runCallbackTest(callback, TestCase.HASHMAPS, 1L); + } + + @Benchmark + public void interfaces() { + Benchmarks.runCallbackTest(callback, TestCase.INTERFACES, 1L); + } + + @Benchmark + public void traitInterfaces() { + Benchmarks.runCallbackTest(callback, TestCase.TRAIT_INTERFACES, 1L); + } + + @Benchmark + public void nestedData() { + Benchmarks.runCallbackTest(callback, TestCase.NESTED_DATA, 1L); + } + + @Benchmark + public void errors() { + Benchmarks.runCallbackTest(callback, TestCase.ERRORS, 1L); + } +} diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java b/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java new file mode 100644 index 0000000..21f1b85 --- /dev/null +++ b/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package uniffi.benchmarks; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Thread) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(2) +public class FunctionCallBenchmarks { + + private String testLargeString1; + private String testLargeString2; + private TestRecord testRec1; + private TestRecord testRec2; + private TestEnum testEnum1; + private TestEnum testEnum2; + private int[] testVec1; + private int[] testVec2; + private Map testMap1; + private Map testMap2; + private TestInterface testInterface1; + private TestInterface testInterface2; + private TestTraitInterface testTraitInterface1; + private TestTraitInterface testTraitInterface2; + private NestedData testNestedData1; + private NestedData testNestedData2; + + @Setup + public void setup() { + testLargeString1 = "a".repeat(2048); + testLargeString2 = "b".repeat(1500); + testRec1 = new TestRecord(-1, 1L, 1.5); + testRec2 = new TestRecord(-2, 2L, 4.5); + testEnum1 = new TestEnum.One(-1, 0L); + testEnum2 = new TestEnum.Two(1.5); + testVec1 = new int[]{0, 1}; + testVec2 = new int[]{2, 4, 6}; + testMap1 = Map.of(0, 1, 1, 2); + testMap2 = Map.of(2, 4); + testInterface1 = new TestInterface(); + testInterface2 = new TestInterface(); + testTraitInterface1 = Benchmarks.makeTestTraitInterface(); + testTraitInterface2 = Benchmarks.makeTestTraitInterface(); + testNestedData1 = new NestedData( + List.of(new TestRecord(-1, 1L, 1.5)), + List.of(List.of("one", "two"), List.of("three")), + Map.of( + "one", new TestEnum.One(-1, 1L), + "two", new TestEnum.Two(0.5) + ) + ); + testNestedData2 = new NestedData( + List.of(new TestRecord(-2, 2L, 4.5)), + List.of(List.of("four", "five")), + Map.of("two", new TestEnum.Two(-0.5)) + ); + } + + @TearDown + public void teardown() { + testInterface1.close(); + testInterface2.close(); + } + + @Benchmark + public void callOnly() { + Benchmarks.testCaseCallOnly(); + } + + @Benchmark + public double primitives() { + return Benchmarks.testCasePrimitives((byte) 0, -1); + } + + @Benchmark + public String strings() { + return Benchmarks.testCaseStrings("a", "b"); + } + + @Benchmark + public String largeStrings() { + return Benchmarks.testCaseLargeStrings(testLargeString1, testLargeString2); + } + + @Benchmark + public TestRecord records() { + return Benchmarks.testCaseRecords(testRec1, testRec2); + } + + @Benchmark + public TestEnum enums() { + return Benchmarks.testCaseEnums(testEnum1, testEnum2); + } + + @Benchmark + public int[] vecs() { + return Benchmarks.testCaseVecs(testVec1, testVec2); + } + + @Benchmark + public Map hashmaps() { + return Benchmarks.testCaseHashmaps(testMap1, testMap2); + } + + @Benchmark + public TestInterface interfaces() { + return Benchmarks.testCaseInterfaces(testInterface1, testInterface2); + } + + @Benchmark + public TestTraitInterface traitInterfaces() { + return Benchmarks.testCaseTraitInterfaces(testTraitInterface1, testTraitInterface2); + } + + @Benchmark + public NestedData nestedData() { + return Benchmarks.testCaseNestedData(testNestedData1, testNestedData2); + } + + @Benchmark + public void errors(Blackhole bh) { + try { + bh.consume(Benchmarks.testCaseErrors()); + } catch (TestException e) { + bh.consume(e); + } + } +} diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java b/benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java new file mode 100644 index 0000000..e7a4f62 --- /dev/null +++ b/benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package uniffi.benchmarks; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JavaCallbackImpl implements TestCallbackInterface { + + private static final String LARGE_STRING_A = "a".repeat(2048); + private static final String LARGE_STRING_B = "b".repeat(1500); + + @Override + public void callOnly() { + } + + @Override + public double primitives(byte a, int b) { + return (double) a + (double) b; + } + + @Override + public String strings(String a, String b) { + return a + b; + } + + @Override + public String largeStrings(String a, String b) { + return a + b; + } + + @Override + public TestRecord records(TestRecord a, TestRecord b) { + return new TestRecord(a.a() + b.a(), a.b() + b.b(), a.c() + b.c()); + } + + @Override + public TestEnum enums(TestEnum a, TestEnum b) { + double aSum = switch (a) { + case TestEnum.One one -> (double) one.a() + (double) one.b(); + case TestEnum.Two two -> two.c(); + }; + double bSum = switch (b) { + case TestEnum.One one -> (double) one.a() + (double) one.b(); + case TestEnum.Two two -> two.c(); + }; + return new TestEnum.Two(aSum + bSum); + } + + @Override + public int[] vecs(int[] a, int[] b) { + int[] result = new int[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + @Override + public Map hashMaps(Map a, Map b) { + var result = new HashMap<>(a); + result.putAll(b); + return result; + } + + @Override + public TestInterface interfaces(TestInterface a, TestInterface b) { + return a.equals(b) ? a : b; + } + + @Override + public TestTraitInterface traitInterfaces(TestTraitInterface a, TestTraitInterface b) { + return a.equals(b) ? a : b; + } + + @Override + public NestedData nestedData(NestedData a, NestedData b) { + return a.equals(b) ? a : b; + } + + @Override + public int errors() throws TestException { + throw new TestException.Two(); + } + + @Override + public long runTest(TestCase testCase, long count) { + long start = System.nanoTime(); + switch (testCase) { + case CALL_ONLY -> { + for (long i = 0; i < count; i++) { + Benchmarks.testCaseCallOnly(); + } + } + case PRIMITIVES -> { + for (long i = 0; i < count; i++) { + Benchmarks.testCasePrimitives((byte) 0, 1); + } + } + case STRINGS -> { + for (long i = 0; i < count; i++) { + Benchmarks.testCaseStrings("a", "b"); + } + } + case LARGE_STRINGS -> { + for (long i = 0; i < count; i++) { + Benchmarks.testCaseLargeStrings(LARGE_STRING_A, LARGE_STRING_B); + } + } + case RECORDS -> { + var rec1 = new TestRecord(-1, 1L, 1.5); + var rec2 = new TestRecord(-2, 2L, 4.5); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseRecords(rec1, rec2); + } + } + case ENUMS -> { + var e1 = new TestEnum.One(-1, 0L); + var e2 = new TestEnum.Two(1.5); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseEnums(e1, e2); + } + } + case VECS -> { + var v1 = new int[]{0, 1}; + var v2 = new int[]{2, 4, 6}; + for (long i = 0; i < count; i++) { + Benchmarks.testCaseVecs(v1, v2); + } + } + case HASHMAPS -> { + var m1 = Map.of(0, 1, 1, 2); + var m2 = Map.of(2, 4); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseHashmaps(m1, m2); + } + } + case INTERFACES -> { + var iface1 = new TestInterface(); + var iface2 = new TestInterface(); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseInterfaces(iface1, iface2); + } + iface1.close(); + iface2.close(); + } + case TRAIT_INTERFACES -> { + var ti1 = Benchmarks.makeTestTraitInterface(); + var ti2 = Benchmarks.makeTestTraitInterface(); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseTraitInterfaces(ti1, ti2); + } + } + case NESTED_DATA -> { + var nd1 = new NestedData( + List.of(new TestRecord(-1, 1L, 1.5)), + List.of(List.of("one", "two"), List.of("three")), + Map.of("one", new TestEnum.One(-1, 1L), "two", new TestEnum.Two(0.5)) + ); + var nd2 = new NestedData( + List.of(new TestRecord(-2, 2L, 4.5)), + List.of(List.of("four", "five")), + Map.of("two", new TestEnum.Two(-0.5)) + ); + for (long i = 0; i < count; i++) { + Benchmarks.testCaseNestedData(nd1, nd2); + } + } + case ERRORS -> { + for (long i = 0; i < count; i++) { + try { + Benchmarks.testCaseErrors(); + } catch (TestException e) { + // expected + } + } + } + } + return System.nanoTime() - start; + } +} diff --git a/flake.nix b/flake.nix index 2bfdfad..ca1e2ee 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ # nix develop devShell = pkgs.mkShell { buildInputs = with pkgs; - [ rusttoolchain pkg-config openjdk21 jna ]; + [ rusttoolchain pkg-config openjdk21 jna gradle ]; }; }); From 5263a33bec912f926ab47f516cfc2baaadbb58e2 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Thu, 26 Mar 2026 01:48:22 -0600 Subject: [PATCH 2/6] Update readme --- Cargo.toml | 5 +++ benchmarks/README.md | 80 +++++++++++++++++++++++++++++++++++++ benchmarks/build.gradle.kts | 5 +++ 3 files changed, 90 insertions(+) create mode 100644 benchmarks/README.md diff --git a/Cargo.toml b/Cargo.toml index b2c45c1..9f6a591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,3 +62,8 @@ uniffi_testing = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.3 [[bench]] name = "benchmarks" harness = false + +# Ensure fixture cdylibs are optimized when benchmarking. +# uniffi_testing builds them via `cargo test --no-run`, which uses the test profile. +[profile.test.package."uniffi-fixture-benchmarks"] +opt-level = 3 diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..5adbc5e --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,80 @@ +# JMH Benchmarks + +Microbenchmarks for uniffi-bindgen-java using [JMH](https://openjdk.org/projects/code-tools/jmh/) (Java Microbenchmark Harness). These measure FFI call overhead, type serialization costs, and callback performance for the generated Java bindings. + +## Prerequisites + +- Rust toolchain (1.87.0+) +- JDK 21+ +- JNA (on the `CLASSPATH`) +- Gradle + +All of these are provided by `nix develop` from the project root. + +## Running + +```bash +# Full benchmark suite (default: 2 forks, 3 warmup iters, 5 measurement iters) +cargo bench --bench benchmarks + +# Quick smoke test +cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 1 -i 1" + +# Filter to specific benchmarks +cargo bench --bench benchmarks -- --jmh-args="FunctionCall" +cargo bench --bench benchmarks -- --jmh-args="Callback" + +# Combine options +cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 2 -i 3 FunctionCall" +``` + +### JMH Args Reference + +| Flag | Meaning | Default | +| ----- | -------------------- | ------- | +| `-f` | Number of forks | 2 | +| `-wi` | Warmup iterations | 3 | +| `-i` | Measurement iterations | 5 | +| `-w` | Warmup time | 1s | +| `-r` | Measurement time | 1s | + +Positional args filter benchmark names (substring match). + +## How It Works + +The `cargo bench` harness (`benches/benchmarks.rs`): + +1. Builds the `uniffi-fixture-benchmarks` Rust cdylib (with `opt-level = 3`) +2. Generates Java bindings into `benchmarks/build/generated-sources/uniffi/` +3. Copies the native library into `benchmarks/build/native-resources/{os}-{arch}/` +4. Runs JMH via Gradle, which compiles the generated bindings + hand-written JMH benchmarks together + +## Results + +Results are written to `benchmarks/build/results/jmh/results.txt` after each run. + +## Benchmark Classes + +- **`FunctionCallBenchmarks`** — Measures Rust function call overhead across type categories: primitives, strings, records, enums, vecs, hashmaps, interfaces, errors, nested data +- **`CallbackBenchmarks`** — Measures Rust→Java callback overhead for the same type categories +- **`JavaCallbackImpl`** — Java callback implementations used by the callback benchmarks + +## Comparing with Upstream Kotlin/Python/Swift Benchmarks + +The upstream [uniffi-rs](https://github.com/mozilla/uniffi-rs) repo includes Criterion-based benchmarks for Kotlin, Python, and Swift using the same `uniffi-fixture-benchmarks` fixture. Run them from `fixtures/benchmarks/` in that repo: + +```bash +cargo bench -- -k # Kotlin +cargo bench -- -p # Python +cargo bench -- -s # Swift +``` + +**Function call benchmarks are directly comparable.** Both JMH and Criterion's `function-calls` group measure the same thing: foreign code calling Rust `testCase*` functions. The test data and Rust functions are identical. + +**Callback benchmarks measure differently.** The upstream Criterion benchmarks time callbacks from the Rust side — Rust calls `run_callback_test()` which invokes the foreign callback, and Criterion measures that. Our JMH benchmarks time from the Java side — JMH measures a Java→Rust call to `runCallbackTest()` which then calls back into Java. This adds an extra FFI round trip (the initial Java→Rust hop) that the upstream benchmarks don't include, so our callback numbers will be higher by roughly one bare FFI call (~2-3µs). + +## Adding Benchmarks + +1. Add JMH-annotated benchmark classes in `src/jmh/java/uniffi/benchmarks/` +2. Reference the generated bindings from `uniffi.benchmarks.*` +3. The Rust fixture lives in the upstream [uniffi-rs](https://github.com/mozilla/uniffi-rs) repo as `uniffi-fixture-benchmarks` diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index a27695f..ed49189 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -49,6 +49,11 @@ jmh { } } +// JMH task should always re-run (args come from -P properties which Gradle doesn't track) +tasks.named("jmh") { + outputs.upToDateWhen { false } +} + sourceSets { main { java { From f63425593783abdc4bfef8557221b4c1ca2785ca Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Thu, 26 Mar 2026 02:12:00 -0600 Subject: [PATCH 3/6] Switch the DX to match upstream --- .gitignore | 2 - benches/README.md | 36 ++++ benches/benchmarks.rs | 155 ++++++++++++------ .../bindings/RunBenchmarks.java | 84 +++++----- benchmarks/README.md | 80 --------- benchmarks/build.gradle.kts | 67 -------- .../uniffi/benchmarks/CallbackBenchmarks.java | 88 ---------- .../benchmarks/FunctionCallBenchmarks.java | 139 ---------------- 8 files changed, 182 insertions(+), 469 deletions(-) create mode 100644 benches/README.md rename benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java => benches/bindings/RunBenchmarks.java (62%) delete mode 100644 benchmarks/README.md delete mode 100644 benchmarks/build.gradle.kts delete mode 100644 benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java delete mode 100644 benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java diff --git a/.gitignore b/.gitignore index 1679095..1833b88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ /target .direnv *.class -benchmarks/build/ -benchmarks/.gradle/ diff --git a/benches/README.md b/benches/README.md new file mode 100644 index 0000000..cd97ce2 --- /dev/null +++ b/benches/README.md @@ -0,0 +1,36 @@ +# Benchmarks + +Criterion-based benchmarks measuring FFI call overhead for the generated Java bindings. Uses the same `uniffi-fixture-benchmarks` fixture and benchmark structure as upstream [uniffi-rs](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/benchmarks), making results directly comparable with Kotlin, Python, and Swift. + +## Prerequisites + +Rust toolchain, JDK 21+, and JNA on the `CLASSPATH` — all provided by `nix develop`. + +## Running + +```bash +cargo bench # full suite +cargo bench -- call-only # filter by name +cargo bench -- --save-baseline before # save a Criterion baseline +cargo bench -- --load-baseline before # compare against a saved baseline +``` + +## How It Works + +`cargo bench` runs `benches/benchmarks.rs` which: + +1. Builds the `uniffi-fixture-benchmarks` cdylib (opt-level 3) +2. Generates Java bindings and packages them into a jar +3. Compiles `benches/bindings/RunBenchmarks.java` +4. Runs the Java process, which calls `Benchmarks.runBenchmarks("java", callback)` + +Criterion runs inside the Rust fixture library. The Java side implements `TestCallbackInterface` and provides timing for function-call benchmarks via `runTest()` (using `System.nanoTime()`). Callback benchmarks are timed directly by Criterion on the Rust side. + +## Benchmark Groups + +- **function-calls** — Java calling Rust functions across 12 type categories +- **callbacks** — Rust calling Java callback methods across the same 12 categories + +## Results + +HTML reports are written to `target/criterion/`. Raw data persists across runs for regression detection. diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 6d7b718..a0b8b58 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -2,14 +2,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -//! JMH benchmark harness for uniffi-bindgen-java +//! Criterion benchmark harness for uniffi-bindgen-java //! -//! This builds the uniffi-fixture-benchmarks cdylib, generates Java bindings, -//! copies them into the Gradle benchmark project, and runs JMH. +//! Matches the upstream uniffi-rs benchmark style: builds the fixture cdylib, +//! generates Java bindings, compiles a Java benchmark runner, and executes it. +//! Criterion runs inside the Rust fixture library, driven by `runBenchmarks()`. //! //! Usage: //! cargo bench -//! cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 2 -i 3" # pass args to JMH +//! cargo bench -- --filter call-only # filter benchmarks +//! cargo bench -- --save-baseline name # save Criterion baseline use anyhow::{Context, Result, bail}; use camino::Utf8PathBuf; @@ -23,16 +25,14 @@ use uniffi_bindgen_java::{GenerateOptions, generate}; use uniffi_testing::UniFFITestHelper; fn main() -> Result<()> { - let jmh_args = parse_jmh_args(); let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let bench_project = project_root.join("benchmarks"); - let generated_dir = bench_project.join("build/generated-sources/uniffi"); + let tmp_dir = PathBuf::from(std::env!("CARGO_TARGET_TMPDIR")).join("benchmarks"); - // Clean and recreate the generated sources directory - if generated_dir.exists() { - fs::remove_dir_all(&generated_dir)?; + // Clean and recreate the temp directory + if tmp_dir.exists() { + fs::remove_dir_all(&tmp_dir)?; } - fs::create_dir_all(&generated_dir)?; + fs::create_dir_all(&tmp_dir)?; // Build the benchmarks fixture cdylib println!("Building benchmarks fixture..."); @@ -42,7 +42,7 @@ fn main() -> Result<()> { // Generate Java bindings println!("Generating Java bindings..."); - let out_dir = Utf8PathBuf::from(generated_dir.to_string_lossy().to_string()); + let out_dir = Utf8PathBuf::from(tmp_dir.to_string_lossy().to_string()); let mut paths = BindgenPaths::default(); paths.add_cargo_metadata_layer(false)?; @@ -58,65 +58,112 @@ fn main() -> Result<()> { }, )?; - // Copy the native library into JNA's expected resource path so it gets packaged - // into the JMH jar. JNA looks for native libs in {os}-{arch}/ inside the classpath. + // Set up JNA native library path: copy cdylib into {os}-{arch}/ inside a staging dir let jna_resource_folder = if cdylib_path.extension().unwrap() == "dylib" { format!("darwin-{}", ARCH).replace('_', "-") } else { format!("linux-{}", ARCH).replace('_', "-") }; - let native_resource_dir = bench_project - .join("build/native-resources") - .join(&jna_resource_folder); + let staging_dir = tmp_dir.join("staging"); + let native_resource_dir = staging_dir.join(&jna_resource_folder); fs::create_dir_all(&native_resource_dir)?; let cdylib_dest = native_resource_dir.join(cdylib_path.file_name().unwrap()); fs::copy(cdylib_path.as_std_path(), &cdylib_dest)?; - println!(" native lib: {}", cdylib_dest.display()); - - // Run JMH via Gradle - println!("Running JMH benchmarks..."); - let mut cmd = Command::new("gradle"); - cmd.current_dir(&bench_project) - .arg("jmh") - .arg("--no-daemon") - .arg("--console=plain") - .env( - "UNIFFI_JNA_CLASSPATH", - env::var("CLASSPATH").unwrap_or_default(), - ); - - // Pass JMH args via project property - if !jmh_args.is_empty() { - let args_str = jmh_args.join(" "); - cmd.arg(format!("-PjmhArgs={args_str}")); + + // Compile generated bindings into a jar + println!("Compiling Java bindings..."); + let java_sources: Vec<_> = glob::glob(&format!("{}/**/*.java", out_dir))? + .flatten() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + + let classpath = calc_classpath(vec![]); + let status = Command::new("javac") + .arg("-d") + .arg(&staging_dir) + .arg("-classpath") + .arg(&classpath) + .args(&java_sources) + .spawn() + .context("Failed to spawn `javac` to compile the bindings")? + .wait() + .context("Failed to wait for `javac`")?; + if !status.success() { + bail!("javac failed when compiling the generated bindings") } - let status = cmd + let jar_file = tmp_dir.join("benchmarks.jar"); + let jar_status = Command::new("jar") + .current_dir(&tmp_dir) + .arg("cf") + .arg(jar_file.file_name().unwrap()) + .arg("-C") + .arg(&staging_dir) + .arg(".") + .spawn() + .context("Failed to spawn `jar`")? + .wait() + .context("Failed to wait for `jar`")?; + if !jar_status.success() { + bail!("jar failed when packaging the bindings") + } + + // Compile the benchmark runner script + println!("Compiling benchmark runner..."); + let runner_src = project_root.join("benches/bindings/RunBenchmarks.java"); + let runner_classpath = calc_classpath(vec![ + jar_file.to_string_lossy().to_string(), + ]); + let status = Command::new("javac") + .arg("-classpath") + .arg(&runner_classpath) + .arg("-d") + .arg(&tmp_dir) + .arg(&runner_src) .spawn() - .context("Failed to spawn gradle. Is gradle available in your PATH (nix develop)?")? + .context("Failed to spawn `javac` to compile the benchmark runner")? .wait() - .context("Failed to wait for gradle")?; + .context("Failed to wait for `javac`")?; + if !status.success() { + bail!("javac failed when compiling the benchmark runner") + } + + // Run the benchmark. The Java process calls Benchmarks.runBenchmarks() which + // drives Criterion internally. We pass CLI args through via "--" so that the + // fixture's Args::parse_for_run_benchmarks() can pick them up. + println!("Running benchmarks..."); + let run_classpath = calc_classpath(vec![ + jar_file.to_string_lossy().to_string(), + tmp_dir.to_string_lossy().to_string(), + ]); + + // Collect args after "--" to pass through to the Java process + let pass_through_args: Vec = env::args() + .skip_while(|a| a != "--") + .collect(); + let mut cmd = Command::new("java"); + cmd.arg("-classpath") + .arg(&run_classpath) + .arg("RunBenchmarks") + .args(&pass_through_args); + + let status = cmd + .spawn() + .context("Failed to spawn `java` to run benchmarks")? + .wait() + .context("Failed to wait for `java`")?; if !status.success() { - bail!("JMH benchmark run failed"); + bail!("Benchmark run failed") } - println!("\nResults written to: benchmarks/build/results/jmh/"); Ok(()) } -fn parse_jmh_args() -> Vec { - env::args() - .skip_while(|a| a != "--") - .skip(1) - .flat_map(|a| { - if let Some(args) = a.strip_prefix("--jmh-args=") { - args.split_whitespace() - .map(String::from) - .collect::>() - } else { - vec![a] - } - }) - .collect() +fn calc_classpath(extra_paths: Vec) -> String { + extra_paths + .into_iter() + .chain(env::var("CLASSPATH")) + .collect::>() + .join(":") } diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java b/benches/bindings/RunBenchmarks.java similarity index 62% rename from benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java rename to benches/bindings/RunBenchmarks.java index e7a4f62..8f0b29a 100644 --- a/benchmarks/src/jmh/java/uniffi/benchmarks/JavaCallbackImpl.java +++ b/benches/bindings/RunBenchmarks.java @@ -2,18 +2,42 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package uniffi.benchmarks; - -import java.util.Arrays; +import uniffi.benchmarks.*; import java.util.HashMap; import java.util.List; import java.util.Map; -public class JavaCallbackImpl implements TestCallbackInterface { - - private static final String LARGE_STRING_A = "a".repeat(2048); - private static final String LARGE_STRING_B = "b".repeat(1500); +class TestData { + static final String testLargeString1 = "a".repeat(2048); + static final String testLargeString2 = "b".repeat(1500); + static final TestRecord testRec1 = new TestRecord(-1, 1L, 1.5); + static final TestRecord testRec2 = new TestRecord(-2, 2L, 4.5); + static final TestEnum testEnum1 = new TestEnum.One(-1, 0L); + static final TestEnum testEnum2 = new TestEnum.Two(1.5); + static final int[] testVec1 = new int[]{0, 1}; + static final int[] testVec2 = new int[]{2, 4, 6}; + static final Map testMap1 = Map.of(0, 1, 1, 2); + static final Map testMap2 = Map.of(2, 4); + static final TestInterface testInterface = new TestInterface(); + static final TestInterface testInterface2 = new TestInterface(); + static final TestTraitInterface testTraitInterface = Benchmarks.makeTestTraitInterface(); + static final TestTraitInterface testTraitInterface2 = Benchmarks.makeTestTraitInterface(); + static final NestedData testNestedData1 = new NestedData( + List.of(new TestRecord(-1, 1L, 1.5)), + List.of(List.of("one", "two"), List.of("three")), + Map.of( + "one", new TestEnum.One(-1, 1L), + "two", new TestEnum.Two(0.5) + ) + ); + static final NestedData testNestedData2 = new NestedData( + List.of(new TestRecord(-2, 2L, 4.5)), + List.of(List.of("four", "five")), + Map.of("two", new TestEnum.Two(-0.5)) + ); +} +class TestCallbackObj implements TestCallbackInterface { @Override public void callOnly() { } @@ -107,66 +131,42 @@ public long runTest(TestCase testCase, long count) { } case LARGE_STRINGS -> { for (long i = 0; i < count; i++) { - Benchmarks.testCaseLargeStrings(LARGE_STRING_A, LARGE_STRING_B); + Benchmarks.testCaseLargeStrings(TestData.testLargeString1, TestData.testLargeString2); } } case RECORDS -> { - var rec1 = new TestRecord(-1, 1L, 1.5); - var rec2 = new TestRecord(-2, 2L, 4.5); for (long i = 0; i < count; i++) { - Benchmarks.testCaseRecords(rec1, rec2); + Benchmarks.testCaseRecords(TestData.testRec1, TestData.testRec2); } } case ENUMS -> { - var e1 = new TestEnum.One(-1, 0L); - var e2 = new TestEnum.Two(1.5); for (long i = 0; i < count; i++) { - Benchmarks.testCaseEnums(e1, e2); + Benchmarks.testCaseEnums(TestData.testEnum1, TestData.testEnum2); } } case VECS -> { - var v1 = new int[]{0, 1}; - var v2 = new int[]{2, 4, 6}; for (long i = 0; i < count; i++) { - Benchmarks.testCaseVecs(v1, v2); + Benchmarks.testCaseVecs(TestData.testVec1, TestData.testVec2); } } case HASHMAPS -> { - var m1 = Map.of(0, 1, 1, 2); - var m2 = Map.of(2, 4); for (long i = 0; i < count; i++) { - Benchmarks.testCaseHashmaps(m1, m2); + Benchmarks.testCaseHashmaps(TestData.testMap1, TestData.testMap2); } } case INTERFACES -> { - var iface1 = new TestInterface(); - var iface2 = new TestInterface(); for (long i = 0; i < count; i++) { - Benchmarks.testCaseInterfaces(iface1, iface2); + Benchmarks.testCaseInterfaces(TestData.testInterface, TestData.testInterface2); } - iface1.close(); - iface2.close(); } case TRAIT_INTERFACES -> { - var ti1 = Benchmarks.makeTestTraitInterface(); - var ti2 = Benchmarks.makeTestTraitInterface(); for (long i = 0; i < count; i++) { - Benchmarks.testCaseTraitInterfaces(ti1, ti2); + Benchmarks.testCaseTraitInterfaces(TestData.testTraitInterface, TestData.testTraitInterface2); } } case NESTED_DATA -> { - var nd1 = new NestedData( - List.of(new TestRecord(-1, 1L, 1.5)), - List.of(List.of("one", "two"), List.of("three")), - Map.of("one", new TestEnum.One(-1, 1L), "two", new TestEnum.Two(0.5)) - ); - var nd2 = new NestedData( - List.of(new TestRecord(-2, 2L, 4.5)), - List.of(List.of("four", "five")), - Map.of("two", new TestEnum.Two(-0.5)) - ); for (long i = 0; i < count; i++) { - Benchmarks.testCaseNestedData(nd1, nd2); + Benchmarks.testCaseNestedData(TestData.testNestedData1, TestData.testNestedData2); } } case ERRORS -> { @@ -182,3 +182,9 @@ public long runTest(TestCase testCase, long count) { return System.nanoTime() - start; } } + +public class RunBenchmarks { + public static void main(String[] args) { + Benchmarks.runBenchmarks("java", new TestCallbackObj()); + } +} diff --git a/benchmarks/README.md b/benchmarks/README.md deleted file mode 100644 index 5adbc5e..0000000 --- a/benchmarks/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# JMH Benchmarks - -Microbenchmarks for uniffi-bindgen-java using [JMH](https://openjdk.org/projects/code-tools/jmh/) (Java Microbenchmark Harness). These measure FFI call overhead, type serialization costs, and callback performance for the generated Java bindings. - -## Prerequisites - -- Rust toolchain (1.87.0+) -- JDK 21+ -- JNA (on the `CLASSPATH`) -- Gradle - -All of these are provided by `nix develop` from the project root. - -## Running - -```bash -# Full benchmark suite (default: 2 forks, 3 warmup iters, 5 measurement iters) -cargo bench --bench benchmarks - -# Quick smoke test -cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 1 -i 1" - -# Filter to specific benchmarks -cargo bench --bench benchmarks -- --jmh-args="FunctionCall" -cargo bench --bench benchmarks -- --jmh-args="Callback" - -# Combine options -cargo bench --bench benchmarks -- --jmh-args="-f 1 -wi 2 -i 3 FunctionCall" -``` - -### JMH Args Reference - -| Flag | Meaning | Default | -| ----- | -------------------- | ------- | -| `-f` | Number of forks | 2 | -| `-wi` | Warmup iterations | 3 | -| `-i` | Measurement iterations | 5 | -| `-w` | Warmup time | 1s | -| `-r` | Measurement time | 1s | - -Positional args filter benchmark names (substring match). - -## How It Works - -The `cargo bench` harness (`benches/benchmarks.rs`): - -1. Builds the `uniffi-fixture-benchmarks` Rust cdylib (with `opt-level = 3`) -2. Generates Java bindings into `benchmarks/build/generated-sources/uniffi/` -3. Copies the native library into `benchmarks/build/native-resources/{os}-{arch}/` -4. Runs JMH via Gradle, which compiles the generated bindings + hand-written JMH benchmarks together - -## Results - -Results are written to `benchmarks/build/results/jmh/results.txt` after each run. - -## Benchmark Classes - -- **`FunctionCallBenchmarks`** — Measures Rust function call overhead across type categories: primitives, strings, records, enums, vecs, hashmaps, interfaces, errors, nested data -- **`CallbackBenchmarks`** — Measures Rust→Java callback overhead for the same type categories -- **`JavaCallbackImpl`** — Java callback implementations used by the callback benchmarks - -## Comparing with Upstream Kotlin/Python/Swift Benchmarks - -The upstream [uniffi-rs](https://github.com/mozilla/uniffi-rs) repo includes Criterion-based benchmarks for Kotlin, Python, and Swift using the same `uniffi-fixture-benchmarks` fixture. Run them from `fixtures/benchmarks/` in that repo: - -```bash -cargo bench -- -k # Kotlin -cargo bench -- -p # Python -cargo bench -- -s # Swift -``` - -**Function call benchmarks are directly comparable.** Both JMH and Criterion's `function-calls` group measure the same thing: foreign code calling Rust `testCase*` functions. The test data and Rust functions are identical. - -**Callback benchmarks measure differently.** The upstream Criterion benchmarks time callbacks from the Rust side — Rust calls `run_callback_test()` which invokes the foreign callback, and Criterion measures that. Our JMH benchmarks time from the Java side — JMH measures a Java→Rust call to `runCallbackTest()` which then calls back into Java. This adds an extra FFI round trip (the initial Java→Rust hop) that the upstream benchmarks don't include, so our callback numbers will be higher by roughly one bare FFI call (~2-3µs). - -## Adding Benchmarks - -1. Add JMH-annotated benchmark classes in `src/jmh/java/uniffi/benchmarks/` -2. Reference the generated bindings from `uniffi.benchmarks.*` -3. The Rust fixture lives in the upstream [uniffi-rs](https://github.com/mozilla/uniffi-rs) repo as `uniffi-fixture-benchmarks` diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts deleted file mode 100644 index ed49189..0000000 --- a/benchmarks/build.gradle.kts +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - java - id("me.champeau.jmh") version "0.7.3" -} - -repositories { - mavenCentral() -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } -} - -dependencies { - // JNA for FFI - pick up from CLASSPATH or UNIFFI_JNA_CLASSPATH env var (provided by nix) - val jnaClasspath = System.getenv("UNIFFI_JNA_CLASSPATH") - ?: System.getenv("CLASSPATH") - ?: "" - if (jnaClasspath.isNotEmpty()) { - implementation(files(jnaClasspath.split(":"))) - } -} - -jmh { - warmupIterations.set(3) - iterations.set(5) - fork.set(2) - resultsFile.set(layout.buildDirectory.file("results/jmh/results.txt")) - // Allow overriding JMH args via -PjmhArgs="..." - if (project.hasProperty("jmhArgs")) { - val args = (project.property("jmhArgs") as String).split(" ") - val iter = args.iterator() - while (iter.hasNext()) { - when (val arg = iter.next()) { - "-f" -> if (iter.hasNext()) fork.set(iter.next().toInt()) - "-wi" -> if (iter.hasNext()) warmupIterations.set(iter.next().toInt()) - "-i" -> if (iter.hasNext()) iterations.set(iter.next().toInt()) - "-w" -> if (iter.hasNext()) warmup.set(iter.next()) - "-r" -> if (iter.hasNext()) timeOnIteration.set(iter.next()) - else -> { - if (!arg.startsWith("-")) { - includes.add(arg) - } - } - } - } - } -} - -// JMH task should always re-run (args come from -P properties which Gradle doesn't track) -tasks.named("jmh") { - outputs.upToDateWhen { false } -} - -sourceSets { - main { - java { - srcDir(layout.buildDirectory.dir("generated-sources/uniffi")) - } - resources { - // Native library packaged for JNA resource loading - srcDir(layout.buildDirectory.dir("native-resources")) - } - } -} diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java b/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java deleted file mode 100644 index 3414a43..0000000 --- a/benchmarks/src/jmh/java/uniffi/benchmarks/CallbackBenchmarks.java +++ /dev/null @@ -1,88 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package uniffi.benchmarks; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.openjdk.jmh.annotations.*; - -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Thread) -@Warmup(iterations = 3, time = 1) -@Measurement(iterations = 5, time = 1) -@Fork(2) -public class CallbackBenchmarks { - - private JavaCallbackImpl callback; - - @Setup - public void setup() { - callback = new JavaCallbackImpl(); - } - - @Benchmark - public void callOnly() { - Benchmarks.runCallbackTest(callback, TestCase.CALL_ONLY, 1L); - } - - @Benchmark - public void primitives() { - Benchmarks.runCallbackTest(callback, TestCase.PRIMITIVES, 1L); - } - - @Benchmark - public void strings() { - Benchmarks.runCallbackTest(callback, TestCase.STRINGS, 1L); - } - - @Benchmark - public void largeStrings() { - Benchmarks.runCallbackTest(callback, TestCase.LARGE_STRINGS, 1L); - } - - @Benchmark - public void records() { - Benchmarks.runCallbackTest(callback, TestCase.RECORDS, 1L); - } - - @Benchmark - public void enums() { - Benchmarks.runCallbackTest(callback, TestCase.ENUMS, 1L); - } - - @Benchmark - public void vecs() { - Benchmarks.runCallbackTest(callback, TestCase.VECS, 1L); - } - - @Benchmark - public void hashmaps() { - Benchmarks.runCallbackTest(callback, TestCase.HASHMAPS, 1L); - } - - @Benchmark - public void interfaces() { - Benchmarks.runCallbackTest(callback, TestCase.INTERFACES, 1L); - } - - @Benchmark - public void traitInterfaces() { - Benchmarks.runCallbackTest(callback, TestCase.TRAIT_INTERFACES, 1L); - } - - @Benchmark - public void nestedData() { - Benchmarks.runCallbackTest(callback, TestCase.NESTED_DATA, 1L); - } - - @Benchmark - public void errors() { - Benchmarks.runCallbackTest(callback, TestCase.ERRORS, 1L); - } -} diff --git a/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java b/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java deleted file mode 100644 index 21f1b85..0000000 --- a/benchmarks/src/jmh/java/uniffi/benchmarks/FunctionCallBenchmarks.java +++ /dev/null @@ -1,139 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package uniffi.benchmarks; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; - -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Thread) -@Warmup(iterations = 3, time = 1) -@Measurement(iterations = 5, time = 1) -@Fork(2) -public class FunctionCallBenchmarks { - - private String testLargeString1; - private String testLargeString2; - private TestRecord testRec1; - private TestRecord testRec2; - private TestEnum testEnum1; - private TestEnum testEnum2; - private int[] testVec1; - private int[] testVec2; - private Map testMap1; - private Map testMap2; - private TestInterface testInterface1; - private TestInterface testInterface2; - private TestTraitInterface testTraitInterface1; - private TestTraitInterface testTraitInterface2; - private NestedData testNestedData1; - private NestedData testNestedData2; - - @Setup - public void setup() { - testLargeString1 = "a".repeat(2048); - testLargeString2 = "b".repeat(1500); - testRec1 = new TestRecord(-1, 1L, 1.5); - testRec2 = new TestRecord(-2, 2L, 4.5); - testEnum1 = new TestEnum.One(-1, 0L); - testEnum2 = new TestEnum.Two(1.5); - testVec1 = new int[]{0, 1}; - testVec2 = new int[]{2, 4, 6}; - testMap1 = Map.of(0, 1, 1, 2); - testMap2 = Map.of(2, 4); - testInterface1 = new TestInterface(); - testInterface2 = new TestInterface(); - testTraitInterface1 = Benchmarks.makeTestTraitInterface(); - testTraitInterface2 = Benchmarks.makeTestTraitInterface(); - testNestedData1 = new NestedData( - List.of(new TestRecord(-1, 1L, 1.5)), - List.of(List.of("one", "two"), List.of("three")), - Map.of( - "one", new TestEnum.One(-1, 1L), - "two", new TestEnum.Two(0.5) - ) - ); - testNestedData2 = new NestedData( - List.of(new TestRecord(-2, 2L, 4.5)), - List.of(List.of("four", "five")), - Map.of("two", new TestEnum.Two(-0.5)) - ); - } - - @TearDown - public void teardown() { - testInterface1.close(); - testInterface2.close(); - } - - @Benchmark - public void callOnly() { - Benchmarks.testCaseCallOnly(); - } - - @Benchmark - public double primitives() { - return Benchmarks.testCasePrimitives((byte) 0, -1); - } - - @Benchmark - public String strings() { - return Benchmarks.testCaseStrings("a", "b"); - } - - @Benchmark - public String largeStrings() { - return Benchmarks.testCaseLargeStrings(testLargeString1, testLargeString2); - } - - @Benchmark - public TestRecord records() { - return Benchmarks.testCaseRecords(testRec1, testRec2); - } - - @Benchmark - public TestEnum enums() { - return Benchmarks.testCaseEnums(testEnum1, testEnum2); - } - - @Benchmark - public int[] vecs() { - return Benchmarks.testCaseVecs(testVec1, testVec2); - } - - @Benchmark - public Map hashmaps() { - return Benchmarks.testCaseHashmaps(testMap1, testMap2); - } - - @Benchmark - public TestInterface interfaces() { - return Benchmarks.testCaseInterfaces(testInterface1, testInterface2); - } - - @Benchmark - public TestTraitInterface traitInterfaces() { - return Benchmarks.testCaseTraitInterfaces(testTraitInterface1, testTraitInterface2); - } - - @Benchmark - public NestedData nestedData() { - return Benchmarks.testCaseNestedData(testNestedData1, testNestedData2); - } - - @Benchmark - public void errors(Blackhole bh) { - try { - bh.consume(Benchmarks.testCaseErrors()); - } catch (TestException e) { - bh.consume(e); - } - } -} From e959de39600e0392969723a3726e8f6ddb39fd54 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Fri, 27 Mar 2026 12:54:41 -0600 Subject: [PATCH 4/6] Drop gradle now that we're not using JMH benches also update the readme with some data from local runs for comparison/a rough idea. --- benches/README.md | 38 ++++++++++++++++++++++++++++++++++++++ flake.nix | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/benches/README.md b/benches/README.md index cd97ce2..77281d3 100644 --- a/benches/README.md +++ b/benches/README.md @@ -34,3 +34,41 @@ Criterion runs inside the Rust fixture library. The Java side implements `TestCa ## Results HTML reports are written to `target/criterion/`. Raw data persists across runs for regression detection. + +### Pre-results + +A run of the benchmarks (and comparison to upstream) on March 26, 2026 with an M4 Max 2024 Macbook Pro, 36GB memory. There was an upstream error in Python's `nested-data` bench at this time. + +#### Function Calls (foreign code calling Rust) + +| Test Case | Java | Kotlin | Python | Swift | +|--------------------|----------|----------|----------|----------| +| call-only | 2.1 | 2.8 | 810 ns | 172 ns | +| primitives | 1.8 | 3.0 | 1.4 | 195 ns | +| strings | 14.7 | 16.8 | 9.2 | 973 ns | +| large-strings | 15.9 | 21.5 | 12.5 | 1.3 | +| records | 14.5 | 16.6 | 18.9 | 2.6 | +| enums | 12.6 | 16.7 | 14.6 | 2.1 | +| vecs | 15.6 | 17.2 | 18.3 | 4.4 | +| hash-maps | 12.3 | 16.9 | 23.4 | 6.3 | +| interfaces | 8.6 | 13.8 | 4.3 | 396 ns | +| trait-interfaces | 9.2 | 12.8 | 4.4 | 461 ns | +| nested-data | 13.5 | 17.9 | --- | 20.6 | +| errors | 4.7 | 7.4 | 3.2 | 642 ns | + +#### Callbacks (Rust calling foreign code) + +| Test Case | Java | Kotlin | Python | Swift | +|--------------------|----------|----------|----------|----------| +| call-only | 2.6 | 3.2 | 477 ns | 121 ns | +| primitives | 4.3 | 3.9 | 793 ns | 166 ns | +| strings | 13.5 | 18.0 | 8.0 | 811 ns | +| large-strings | 18.2 | 22.6 | 10.7 | 1.6 | +| records | 16.6 | 17.5 | 14.0 | 2.8 | +| enums | 17.0 | 17.5 | 11.0 | 2.4 | +| vecs | 16.9 | 17.9 | 18.1 | 4.8 | +| hash-maps | 12.7 | 18.1 | 21.4 | 7.1 | +| interfaces | 7.8 | 35.6 | 4.1 | 429 ns | +| trait-interfaces | 11.1 | 27.2 | 4.3 | 528 ns | +| nested-data | 21.8 | 16.8 | --- | 24.1 | +| errors | 10.4 | 8.9 | 4.3 | 566 ns | diff --git a/flake.nix b/flake.nix index ca1e2ee..2bfdfad 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ # nix develop devShell = pkgs.mkShell { buildInputs = with pkgs; - [ rusttoolchain pkg-config openjdk21 jna gradle ]; + [ rusttoolchain pkg-config openjdk21 jna ]; }; }); From 4c632a6252387a1999574e79f6440808e10d6ad9 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Fri, 27 Mar 2026 13:01:47 -0600 Subject: [PATCH 5/6] Explain empty units on example benches --- benches/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/README.md b/benches/README.md index 77281d3..4aebbe3 100644 --- a/benches/README.md +++ b/benches/README.md @@ -37,7 +37,7 @@ HTML reports are written to `target/criterion/`. Raw data persists across runs f ### Pre-results -A run of the benchmarks (and comparison to upstream) on March 26, 2026 with an M4 Max 2024 Macbook Pro, 36GB memory. There was an upstream error in Python's `nested-data` bench at this time. +A run of the benchmarks (and comparison to upstream) on March 26, 2026 with an M4 Max 2024 Macbook Pro, 36GB memory. There was an upstream error in Python's `nested-data` bench at this time. All calls are in microseconds unless noted. #### Function Calls (foreign code calling Rust) From 308bc1258858214f18d48e042c8abf4e88a62887 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Fri, 27 Mar 2026 14:14:20 -0600 Subject: [PATCH 6/6] Pre-CI checks --- Cargo.toml | 2 +- benches/benchmarks.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f6a591..3709f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ uniffi-example-custom-types = { git = "https://github.com/mozilla/uniffi-rs.git" uniffi-example-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-example-geometry = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-example-rondpoint = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } +uniffi-fixture-benchmarks = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-coverall = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-ext-types = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-futures = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } @@ -56,7 +57,6 @@ uniffi-fixture-proc-macro = { git = "https://github.com/mozilla/uniffi-rs.git", uniffi-fixture-rename = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-time = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi-fixture-trait-methods = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } -uniffi-fixture-benchmarks = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } uniffi_testing = { git = "https://github.com/mozilla/uniffi-rs.git", tag = "v0.31.0" } [[bench]] diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index a0b8b58..05f3963 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -111,9 +111,7 @@ fn main() -> Result<()> { // Compile the benchmark runner script println!("Compiling benchmark runner..."); let runner_src = project_root.join("benches/bindings/RunBenchmarks.java"); - let runner_classpath = calc_classpath(vec![ - jar_file.to_string_lossy().to_string(), - ]); + let runner_classpath = calc_classpath(vec![jar_file.to_string_lossy().to_string()]); let status = Command::new("javac") .arg("-classpath") .arg(&runner_classpath) @@ -138,9 +136,7 @@ fn main() -> Result<()> { ]); // Collect args after "--" to pass through to the Java process - let pass_through_args: Vec = env::args() - .skip_while(|a| a != "--") - .collect(); + let pass_through_args: Vec = env::args().skip_while(|a| a != "--").collect(); let mut cmd = Command::new("java"); cmd.arg("-classpath")