Add image re-encoding benchmarks.

This is one of the more compute-heavy workflows that the
app server performs, we should be able measure how fast it is
against past revisions.

Add a step to the general CI job which will run each benchmark
as a test without trying to actually measure many iterations.
This commit is contained in:
Adam Perry
2026-05-20 15:21:48 -07:00
parent 58be470d15
commit 4792e463c4
8 changed files with 238 additions and 1 deletions

View File

@@ -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

View File

@@ -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

3
MODULE.bazel.lock generated
View File

@@ -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\"]}}",

34
codex-rs/Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"

View File

@@ -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

View File

@@ -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<Vec<u8>>) {
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<u8>) {
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<u8>) -> 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<u8>) -> Vec<Vec<u8>> {
// 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<u8> {
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<u8> {
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<u8> {
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()
}

View File

@@ -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