diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml index 08e0709e17..df91230888 100644 --- a/.github/workflows/rust-ci-full.yml +++ b/.github/workflows/rust-ci-full.yml @@ -30,6 +30,8 @@ jobs: components: rustfmt - name: cargo fmt run: cargo fmt -- --config imports_granularity=Item --check + - name: Rust benchmark smoke test + run: ../scripts/smoke-test-rust-benches.sh cargo_shear: name: cargo shear diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 75c5c33601..34c1742e8b 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -72,6 +72,8 @@ jobs: components: rustfmt - name: cargo fmt run: cargo fmt -- --config imports_granularity=Item --check + - name: Rust benchmark smoke test + run: ../scripts/smoke-test-rust-benches.sh cargo_shear: name: cargo shear diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 02d7c76a91..7b33916d9c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -768,6 +768,7 @@ "compact_str_0.8.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"borsh\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"castaway\",\"req\":\"^0.2.3\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"diesel\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"markup\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"proptest\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"size_32\"],\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"alloc\",\"size_32\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"name\":\"ryu\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"features\":[\"union\"],\"name\":\"smallvec\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"sqlx\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"test-case\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"test-strategy\",\"req\":\"^0.3\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"borsh\":[\"dep:borsh\"],\"bytes\":[\"dep:bytes\"],\"default\":[\"std\"],\"diesel\":[\"dep:diesel\"],\"markup\":[\"dep:markup\"],\"proptest\":[\"dep:proptest\"],\"quickcheck\":[\"dep:quickcheck\"],\"rkyv\":[\"dep:rkyv\"],\"serde\":[\"dep:serde\"],\"smallvec\":[\"dep:smallvec\"],\"sqlx\":[\"dep:sqlx\",\"std\"],\"sqlx-mysql\":[\"sqlx\",\"sqlx/mysql\"],\"sqlx-postgres\":[\"sqlx\",\"sqlx/postgres\"],\"sqlx-sqlite\":[\"sqlx\",\"sqlx/sqlite\"],\"std\":[]}}", "compiletest_rs_0.11.2": "{\"dependencies\":[{\"name\":\"diff\",\"req\":\"^0.1.10\"},{\"name\":\"filetime\",\"req\":\"^0.2\"},{\"name\":\"getopts\",\"req\":\"^0.2\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"miow\",\"req\":\"^0.6\",\"target\":\"cfg(windows)\"},{\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"rustfix\",\"req\":\"^0.8\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.0\"},{\"name\":\"tester\",\"req\":\"^0.9\"},{\"features\":[\"Win32\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"rustc\":[],\"stable\":[],\"tmp\":[\"tempfile\"]}}", "concurrent-queue_2.5.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"crossbeam-utils\",\"req\":\"^0.8.11\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", + "condtype_1.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.141\"}],\"features\":{}}", "console_0.15.11": "{\"dependencies\":[{\"name\":\"encode_unicode\",\"req\":\"^1\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2.99\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"default_features\":false,\"features\":[\"std\",\"bit-set\",\"break-dead-code\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.4.2\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Storage_FileSystem\",\"Win32_UI_Input_KeyboardAndMouse\"],\"name\":\"windows-sys\",\"req\":\"^0.59\",\"target\":\"cfg(windows)\"}],\"features\":{\"ansi-parsing\":[],\"default\":[\"unicode-width\",\"ansi-parsing\"],\"windows-console-colors\":[\"ansi-parsing\"]}}", "const-hex_1.17.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"kind\":\"dev\",\"name\":\"divan\",\"package\":\"codspeed-divan-compat\",\"req\":\"^3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"faster-hex\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"~0.4.2\"},{\"default_features\":false,\"name\":\"proptest\",\"optional\":true,\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"rustc-hex\",\"req\":\"^2.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"__fuzzing\":[\"dep:proptest\",\"std\"],\"alloc\":[\"serde_core?/alloc\",\"proptest?/alloc\"],\"core-error\":[],\"default\":[\"std\"],\"force-generic\":[],\"hex\":[],\"nightly\":[],\"portable-simd\":[],\"serde\":[\"dep:serde_core\"],\"std\":[\"serde_core?/std\",\"proptest?/std\",\"alloc\"]}}", "const-oid_0.9.6": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"}],\"features\":{\"db\":[],\"std\":[]}}", @@ -859,6 +860,8 @@ "dispatch2_0.3.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bitflags\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"block2\",\"optional\":true,\"req\":\">=0.6.1, <0.8.0\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.80\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"objc2\",\"optional\":true,\"req\":\">=0.6.1, <0.8.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"}],\"features\":{\"alloc\":[],\"block2\":[\"dep:block2\"],\"default\":[\"std\",\"block2\",\"libc\",\"objc2\"],\"libc\":[\"dep:libc\"],\"objc2\":[\"dep:objc2\"],\"std\":[\"alloc\"]}}", "display_container_0.9.0": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"indenter\",\"req\":\"^0.3.3\"}],\"features\":{}}", "displaydoc_0.2.5": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^0.6.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.24\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", + "divan-macros_0.1.21": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"clone-impls\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.18\"}],\"features\":{}}", + "divan_0.1.21": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\",\"env\"],\"name\":\"clap\",\"req\":\"^4\"},{\"name\":\"condtype\",\"req\":\"^1.3\"},{\"name\":\"divan-macros\",\"req\":\"=0.1.21\"},{\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"mimalloc\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"std\",\"string\"],\"name\":\"regex\",\"package\":\"regex-lite\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"wrap_help\"],\"dyn_thread_local\":[],\"help\":[\"clap/help\"],\"internal_benches\":[],\"wrap_help\":[\"help\",\"clap/wrap_help\"]}}", "dns-lookup_3.0.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"features\":[\"Win32_Networking_WinSock\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "document-features_0.2.12": "{\"dependencies\":[{\"name\":\"litrs\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[],\"self-test\":[]}}", "dotenvy_0.15.7": "{\"dependencies\":[{\"name\":\"clap\",\"optional\":true,\"req\":\"^3.2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.16.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.3.0\"}],\"features\":{\"cli\":[\"clap\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 7c9955323d..8f9a0beb2a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3979,6 +3979,7 @@ version = "0.0.0" dependencies = [ "base64 0.22.1", "codex-utils-cache", + "divan", "image", "mime_guess", "thiserror 2.0.18", @@ -4220,6 +4221,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console" version = "0.15.11" @@ -5216,6 +5223,31 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "divan" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "dns-lookup" version = "3.0.1" @@ -5489,7 +5521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index ec38a87cff..90376a649d 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -275,6 +275,7 @@ deno_core_icudata = "0.77.0" derive_more = "2" diffy = "0.4.2" dirs = "6" +divan = "0.1.21" dns-lookup = "3.0.1" dotenvy = "0.15.7" dunce = "1.0.4" diff --git a/codex-rs/utils/image/Cargo.toml b/codex-rs/utils/image/Cargo.toml index 5ac187caaa..7ba28f4996 100644 --- a/codex-rs/utils/image/Cargo.toml +++ b/codex-rs/utils/image/Cargo.toml @@ -16,7 +16,12 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "rt", "rt-multi-thread", "macros"] } [dev-dependencies] +divan = { workspace = true } image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] } [lib] doctest = false + +[[bench]] +name = "prompt_images" +harness = false diff --git a/codex-rs/utils/image/benches/prompt_images.rs b/codex-rs/utils/image/benches/prompt_images.rs new file mode 100644 index 0000000000..9e2719fb01 --- /dev/null +++ b/codex-rs/utils/image/benches/prompt_images.rs @@ -0,0 +1,176 @@ +use std::io::Cursor; +use std::path::Path; + +use codex_utils_image::PromptImageMode; +use codex_utils_image::load_for_prompt_bytes; +use divan::Bencher; +use image::DynamicImage; +use image::ImageFormat; +use image::Rgb; +use image::RgbImage; +use image::Rgba; +use image::RgbaImage; + +const CACHE_MISS_VARIANT_COUNT: usize = 48; + +const SMALL_SCREENSHOT: ImageSize = ImageSize { + width: 1_536, + height: 864, +}; +const LARGE_SCREENSHOT: ImageSize = ImageSize { + width: 2_560, + height: 1_440, +}; +const LARGE_PHOTO: ImageSize = ImageSize { + width: 3_264, + height: 2_448, +}; + +#[derive(Clone, Copy)] +struct ImageSize { + width: u32, + height: u32, +} + +fn main() { + divan::main(); +} + +#[divan::bench] +fn small_png_screenshot_fresh_attachment(bencher: Bencher) { + bench_fresh_attachment( + bencher, + "small-screenshot.png", + cache_miss_variants(screenshot_png(SMALL_SCREENSHOT)), + ); +} + +#[divan::bench] +fn large_png_screenshot_fresh_attachment(bencher: Bencher) { + bench_fresh_attachment( + bencher, + "large-screenshot.png", + cache_miss_variants(screenshot_png(LARGE_SCREENSHOT)), + ); +} + +#[divan::bench] +fn large_jpeg_photo_fresh_attachment(bencher: Bencher) { + bench_fresh_attachment( + bencher, + "large-photo.jpg", + cache_miss_variants(photo_jpeg(LARGE_PHOTO)), + ); +} + +#[divan::bench] +fn small_png_screenshot_repeated_attachment(bencher: Bencher) { + bench_repeated_attachment( + bencher, + "small-screenshot.png", + screenshot_png(SMALL_SCREENSHOT), + ); +} + +fn bench_fresh_attachment(bencher: Bencher, path: &'static str, images: Vec>) { + let mut image_index = 0; + + bencher + // Divan excludes `with_inputs` from the measured benchmark timing. + .with_inputs(move || { + let image = images[image_index].clone(); + image_index = (image_index + 1) % images.len(); + image + }) + .bench_local_values(move |image| prepare_prompt_data_url(path, image)); +} + +fn bench_repeated_attachment(bencher: Bencher, path: &'static str, image: Vec) { + let _ = prepare_prompt_data_url(path, image.clone()); + + bencher + // Divan excludes the per-iteration input clone from measured timing. + .with_inputs(move || image.clone()) + .bench_local_values(move |image| prepare_prompt_data_url(path, image)); +} + +fn prepare_prompt_data_url(path: &str, image: Vec) -> String { + #[allow(clippy::expect_used)] + load_for_prompt_bytes(Path::new(path), image, PromptImageMode::ResizeToFit) + .expect("benchmark fixture should load") + .into_data_url() +} + +fn cache_miss_variants(image: Vec) -> Vec> { + // The loader caches by content digest. Suffixes keep this workload on the miss path. + (0..CACHE_MISS_VARIANT_COUNT) + .map(|variant| { + let mut image = image.clone(); + image.extend_from_slice(&variant.to_le_bytes()); + image + }) + .collect() +} + +fn screenshot_png(size: ImageSize) -> Vec { + let image = RgbaImage::from_fn(size.width, size.height, |x, y| { + let toolbar = y < 52; + let sidebar = x < 240; + let panel_border = x % 320 < 2 || y % 216 < 2; + let text_row = x > 270 && y > 88 && x % 19 < 13 && y % 31 < 3; + + if toolbar { + Rgba([33, 40, 52, 255]) + } else if sidebar { + let selection = y / 68 % 5 == 2; + if selection { + Rgba([65, 106, 171, 255]) + } else { + Rgba([44, 54, 67, 255]) + } + } else if panel_border { + Rgba([198, 205, 216, 255]) + } else if text_row { + Rgba([72, 82, 96, 255]) + } else { + let panel = ((x / 320) + (y / 216) * 3) % 4; + match panel { + 0 => Rgba([246, 248, 252, 255]), + 1 => Rgba([234, 241, 250, 255]), + 2 => Rgba([240, 247, 236, 255]), + _ => Rgba([250, 240, 235, 255]), + } + } + }); + + encode_fixture(DynamicImage::ImageRgba8(image), ImageFormat::Png) +} + +fn photo_jpeg(size: ImageSize) -> Vec { + let image = RgbImage::from_fn(size.width, size.height, |x, y| { + let x_gradient = x * 255 / size.width; + let y_gradient = y * 255 / size.height; + let texture = ((x.wrapping_mul(17) ^ y.wrapping_mul(31) ^ (x / 7) ^ (y / 11)) & 0xff) as u8; + + Rgb([ + blend_channel(x_gradient, texture, 3), + blend_channel((x_gradient + y_gradient) / 2, texture, 5), + blend_channel(255 - y_gradient, texture, 4), + ]) + }); + + encode_fixture(DynamicImage::ImageRgb8(image), ImageFormat::Jpeg) +} + +fn blend_channel(gradient: u32, texture: u8, divisor: u32) -> u8 { + ((gradient + u32::from(texture) / divisor) % 256) as u8 +} + +fn encode_fixture(image: DynamicImage, format: ImageFormat) -> Vec { + let mut encoded = Cursor::new(Vec::new()); + #[allow(clippy::expect_used)] + image + .write_to(&mut encoded, format) + .expect("benchmark fixture should encode"); + encoded.into_inner() +} diff --git a/scripts/smoke-test-rust-benches.sh b/scripts/smoke-test-rust-benches.sh new file mode 100755 index 0000000000..f87047bbd6 --- /dev/null +++ b/scripts/smoke-test-rust-benches.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../codex-rs" + +bench_targets="$( + cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[] as $package | $package.targets[] | select(any(.kind[]; . == "bench")) | [$package.name, .name] | @tsv' +)" + +if [[ -n "$bench_targets" ]]; then + while IFS=$'\t' read -r package bench_target; do + cargo bench -p "$package" --bench "$bench_target" -- --test + done <<< "$bench_targets" +fi