From 13729f0b9fb3683b7192da20035e828038b0efac Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 04:56:13 +0000 Subject: [PATCH 01/15] First commit: Add write_object benchmark simulating the structure of appendable_object --- Cargo.lock | 17 ++ Cargo.toml | 1 + .../benchmarks/write_object/Cargo.toml | 40 ++++ src/storage/benchmarks/write_object/README.md | 63 +++++ .../benchmarks/write_object/run_all.sh | 61 +++++ .../benchmarks/write_object/src/args.rs | 60 +++++ .../benchmarks/write_object/src/main.rs | 223 ++++++++++++++++++ .../benchmarks/write_object/src/metrics.rs | 82 +++++++ .../benchmarks/write_object/src/reporter.rs | 177 ++++++++++++++ .../benchmarks/write_object/src/scenarios.rs | 147 ++++++++++++ .../benchmarks/write_object/src/source.rs | 43 ++++ 11 files changed, 914 insertions(+) create mode 100644 src/storage/benchmarks/write_object/Cargo.toml create mode 100644 src/storage/benchmarks/write_object/README.md create mode 100755 src/storage/benchmarks/write_object/run_all.sh create mode 100644 src/storage/benchmarks/write_object/src/args.rs create mode 100644 src/storage/benchmarks/write_object/src/main.rs create mode 100644 src/storage/benchmarks/write_object/src/metrics.rs create mode 100644 src/storage/benchmarks/write_object/src/reporter.rs create mode 100644 src/storage/benchmarks/write_object/src/scenarios.rs create mode 100644 src/storage/benchmarks/write_object/src/source.rs diff --git a/Cargo.lock b/Cargo.lock index 730f5a1a60..fe1ed2ee45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9534,6 +9534,23 @@ dependencies = [ "uuid", ] +[[package]] +name = "storage-benchmark-write-object" +version = "0.0.0" +dependencies = [ + "anyhow", + "bytes", + "clap", + "crc32c", + "google-cloud-auth", + "google-cloud-storage", + "rand 0.10.2", + "tempfile", + "tokio", + "tracing-subscriber", + "uuid", +] + [[package]] name = "storage-grpc-mock" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index c399f18a91..1186152b11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -354,6 +354,7 @@ members = [ "src/storage/benchmarks/appendable_object", "src/storage/benchmarks/random", "src/storage/benchmarks/w1r3", + "src/storage/benchmarks/write_object", "src/storage/examples", "src/storage/tests/scenarios", "src/wkt", diff --git a/src/storage/benchmarks/write_object/Cargo.toml b/src/storage/benchmarks/write_object/Cargo.toml new file mode 100644 index 0000000000..af711b1a49 --- /dev/null +++ b/src/storage/benchmarks/write_object/Cargo.toml @@ -0,0 +1,40 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[package] +name = "storage-benchmark-write-object" +version = "0.0.0" +publish = false +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +anyhow.workspace = true +bytes.workspace = true +clap = { workspace = true, features = ["derive", "env", "help", "std", "usage"] } +crc32c.workspace = true +google-cloud-auth.workspace = true +google-cloud-storage = { workspace = true, features = ["default-rustls-provider", "unstable-stream"] } +rand.workspace = true +tempfile.workspace = true +tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "std"] } +uuid.workspace = true + +[lints] +workspace = true diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md new file mode 100644 index 0000000000..55ddd9e1ca --- /dev/null +++ b/src/storage/benchmarks/write_object/README.md @@ -0,0 +1,63 @@ +# Cloud Storage WriteObject Upload Benchmark + +Benchmarks and compares upload strategies and checksum modes for [`write_object`](crate::client::Storage::write_object) in the Google Cloud Storage (GCS) Rust client library. + +## Motivation & Scenarios + +When uploading seekable data (such as local disk files), SDKs face a protocol trade-off regarding data integrity validation and throughput: + +* **Option A (`Option_A_Unbuffered_Baseline`)**: + * Code: `.send_unbuffered()` + * Single continuous stream, 0 application RAM buffer. + * Checksum calculated on the fly; verified client-side upon completion. +* **Option B (`Option_B_Unbuffered_2Pass`)**: + * Code: `.precompute_checksums().await?.send_unbuffered()` + * **Pass 1:** Scans the local file to compute SIMD CRC32C checksum. + * **Pass 2:** Streams data via a single continuous PUT request with server-side validation header (`x-goog-hash`). + * 0 application RAM buffer. +* **Option C (`Option_C_Buffered_Chunked`)**: + * Code: `.send_buffered()` + * 1-pass chunked upload using sequential 8 MiB HTTP PUT requests. + * Server-side validation enforced by attaching checksum to the final chunk. + * Requires 8 MiB in-memory buffer per upload. + +## Benchmark Matrix (5 Size Tiers) + +1. **12 MiB (Single-Shot Multipart)**: Exercises single-shot multipart upload path (below default 16 MiB threshold). +2. **64 MiB (Small Resumable)**: 8 chunks in Option C vs. 1 continuous stream in Option B. +3. **512 MiB (Medium Resumable)**: 64 chunks in Option C vs. 1 continuous stream in Option B. +4. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream in Option B. +5. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous stream in Option B. + +## Pre-requisites + +- Authenticate with GCP credentials: + ```bash + gcloud auth application-default login + ``` +- Set target bucket environment variable: + ```bash + export GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET="my-benchmark-bucket" + ``` + +## Running the Benchmark + +### Run All 5 Tiers +```bash +chmod +x run_all.sh +./run_all.sh +``` + +### Run a Single Configuration +```bash +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 67108864 \ + --scenario all \ + --warmup-iterations 1 \ + --measured-iterations 5 +``` + +### Save Output Metrics (CSV & JSON) +```bash +./run_all.sh --output-dir=/path/to/results +``` diff --git a/src/storage/benchmarks/write_object/run_all.sh b/src/storage/benchmarks/write_object/run_all.sh new file mode 100755 index 0000000000..8ed7874540 --- /dev/null +++ b/src/storage/benchmarks/write_object/run_all.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +# Parse extra args passed directly to script +EXTRA_ARGS=( "$@" ) + +echo "Building benchmark..." +cargo build --release -p storage-benchmark-write-object + +echo "=================================================================" +echo "Running Tier 1: 12 MiB (Single-Shot Multipart Upload)" +echo "=================================================================" +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 12582912 \ + "${EXTRA_ARGS[@]}" + +echo "=================================================================" +echo "Running Tier 2: 64 MiB (Small Resumable Upload - 8 chunks)" +echo "=================================================================" +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 67108864 \ + "${EXTRA_ARGS[@]}" + +echo "=================================================================" +echo "Running Tier 3: 512 MiB (Medium Resumable Upload - 64 chunks)" +echo "=================================================================" +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 536870912 \ + "${EXTRA_ARGS[@]}" + +echo "=================================================================" +echo "Running Tier 4: 2 GiB (Large Resumable Upload - 256 chunks)" +echo "=================================================================" +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 2147483648 \ + "${EXTRA_ARGS[@]}" + +echo "=================================================================" +echo "Running Tier 5: 8 GiB (Stress Resumable Upload - 1,024 chunks)" +echo "=================================================================" +cargo run --release -p storage-benchmark-write-object -- \ + --object-size 8589934592 \ + "${EXTRA_ARGS[@]}" + +echo "=================================================================" +echo "All benchmarks complete!" +echo "=================================================================" diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs new file mode 100644 index 0000000000..d1811f7d8a --- /dev/null +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::{Parser, ValueEnum}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum UploadScenario { + /// Option A: Baseline 1-pass unbuffered stream (no precomputed hash, client-side validation only) + OptionA, + /// Option B: 2-pass unbuffered stream (Pass 1: precomputed hash, Pass 2: continuous stream, server-side validation) + OptionB, + /// Option C: 1-pass buffered chunked upload (8 MiB chunks in RAM, server-side validation on final chunk) + OptionC, + /// Run all three scenarios (Option A, Option B, and Option C) for side-by-side comparison + All, +} + +#[derive(Parser, Debug)] +#[command(author, version, about = "Benchmark write_object upload strategies in GCS", long_about = None)] +pub struct Args { + /// The name of the bucket to use for the benchmark. + #[arg(long, env = "GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET")] + pub bucket_name: String, + + /// Number of warmup iterations. + #[arg(long, default_value_t = 1)] + pub warmup_iterations: usize, + + /// Number of measured iterations. + #[arg(long, default_value_t = 5)] + pub measured_iterations: usize, + + /// The size of the object to upload in bytes. + #[arg(long, default_value_t = 67_108_864)] // 64 MiB default + pub object_size: usize, + + /// Upload scenario / strategy to benchmark. + #[arg(long, value_enum, default_value_t = UploadScenario::All)] + pub scenario: UploadScenario, + + /// Directory for output artifacts (raw CSV latencies and summary JSON). If not provided, file + /// reporting is skipped. + #[arg(long)] + pub output_dir: Option, + + /// Whether to delete test objects from GCS after each iteration. + #[arg(long, default_value_t = true)] + pub cleanup: bool, +} diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs new file mode 100644 index 0000000000..a3e130ef9f --- /dev/null +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -0,0 +1,223 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `write_object` benchmark binary. + +mod args; +mod metrics; +mod reporter; +mod scenarios; +mod source; + +use args::{Args, UploadScenario}; +use clap::Parser; +use google_cloud_storage::client::{Storage, StorageControl}; +use std::path::Path; +use uuid::Uuid; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let args = Args::parse(); + if args.measured_iterations == 0 { + anyhow::bail!("Measured iterations must be greater than 0"); + } + + let credentials = google_cloud_auth::credentials::Builder::default().build()?; + let client = Storage::builder() + .with_credentials(credentials.clone()) + .build() + .await?; + let control = StorageControl::builder() + .with_credentials(credentials) + .build() + .await?; + + println!("============================================================"); + println!("GCS write_object Benchmark Suite"); + println!("Target Bucket: {}", args.bucket_name); + println!( + "Object Size: {} bytes ({:.2} MiB)", + args.object_size, + args.object_size as f64 / (1024.0 * 1024.0) + ); + println!("Warmup Iterations: {}", args.warmup_iterations); + println!("Measured Iterations: {}", args.measured_iterations); + println!("============================================================"); + + println!("Generating local test file on disk..."); + let (_temp_handle, temp_file_path) = source::create_temp_test_file(args.object_size).await?; + println!("Test file created at: {}", temp_file_path.display()); + + let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); + + match args.scenario { + UploadScenario::OptionA => { + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionA, + ) + .await?; + } + UploadScenario::OptionB => { + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionB, + ) + .await?; + } + UploadScenario::OptionC => { + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionC, + ) + .await?; + } + UploadScenario::All => { + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionA, + ) + .await?; + + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionB, + ) + .await?; + + run_single_scenario( + &client, + &control, + &formatted_bucket, + &temp_file_path, + &args, + UploadScenario::OptionC, + ) + .await?; + } + } + + Ok(()) +} + +async fn run_single_scenario( + client: &Storage, + control: &StorageControl, + bucket: &str, + file_path: &Path, + args: &Args, + scenario: UploadScenario, +) -> anyhow::Result<()> { + let scenario_name = match scenario { + UploadScenario::OptionA => "Option_A_Unbuffered_Baseline", + UploadScenario::OptionB => "Option_B_Unbuffered_2Pass", + UploadScenario::OptionC => "Option_C_Buffered_Chunked", + UploadScenario::All => unreachable!(), + }; + + println!("\n>>> Running Scenario: {} <<<", scenario_name); + let mut results = Vec::new(); + let mut errors = 0; + let total_iterations = args.warmup_iterations + args.measured_iterations; + + for i in 0..total_iterations { + let object_name = format!("bench-write-object-{}", Uuid::new_v4()); + let res = match scenario { + UploadScenario::OptionA => { + scenarios::scenario_option_a( + client, + bucket, + &object_name, + file_path, + args.object_size, + ) + .await + } + UploadScenario::OptionB => { + scenarios::scenario_option_b( + client, + bucket, + &object_name, + file_path, + args.object_size, + ) + .await + } + UploadScenario::OptionC => { + scenarios::scenario_option_c( + client, + bucket, + &object_name, + file_path, + args.object_size, + ) + .await + } + UploadScenario::All => unreachable!(), + }; + + match res { + Ok(r) => { + if i < args.warmup_iterations { + println!("Warmup {:>2}: {:?}", i + 1, r.total_elapsed); + } else { + println!( + "Measured {:>2}: {:?}{}", + i - args.warmup_iterations + 1, + r.total_elapsed, + r.precompute_duration + .map(|d| format!(" (Pass 1 Hash: {:?})", d)) + .unwrap_or_default() + ); + results.push(r); + } + } + Err(err) => { + eprintln!("Error during iteration {}: {err:#}", i + 1); + errors += 1; + } + } + + if args.cleanup { + let _ = scenarios::cleanup_object(control, bucket, &object_name).await; + } + } + + let latencies: Vec<_> = results.iter().map(|r| r.total_elapsed).collect(); + let metrics = metrics::compute_metrics(&latencies, args.object_size); + reporter::report(scenario_name, metrics, &results, errors, args)?; + + Ok(()) +} diff --git a/src/storage/benchmarks/write_object/src/metrics.rs b/src/storage/benchmarks/write_object/src/metrics.rs new file mode 100644 index 0000000000..7ce09b9846 --- /dev/null +++ b/src/storage/benchmarks/write_object/src/metrics.rs @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Duration; + +/// Aggregated latency statistical metrics. +#[derive(Debug, Clone)] +pub struct Metrics { + /// Mean execution latency. + pub mean: Duration, + /// 50th percentile (median) execution latency. + pub p50: Duration, + /// 90th percentile execution latency. + pub p90: Duration, + /// 99th percentile execution latency. + pub p99: Duration, + /// Mean throughput in MiB/s. + pub throughput_mib_per_sec: f64, +} + +/// Computes statistical metrics (mean, p50, p90, p99, throughput) from latencies. +pub fn compute_metrics(latencies: &[Duration], object_size_bytes: usize) -> Option { + if latencies.is_empty() { + return None; + } + + let mut sorted = latencies.to_vec(); + sorted.sort(); + + let sum: Duration = sorted.iter().sum(); + let mean = sum / sorted.len() as u32; + let len = sorted.len(); + let p50 = sorted[((len - 1) as f64 * 0.50).round() as usize]; + let p90 = sorted[((len - 1) as f64 * 0.90).round() as usize]; + let p99 = sorted[((len - 1) as f64 * 0.99).round() as usize]; + + let mean_secs = mean.as_secs_f64(); + let throughput_mib_per_sec = if mean_secs > 0.0 { + (object_size_bytes as f64 / (1024.0 * 1024.0)) / mean_secs + } else { + 0.0 + }; + + Some(Metrics { + mean, + p50, + p90, + p99, + throughput_mib_per_sec, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_latencies() { + assert!(compute_metrics(&[], 1024).is_none()); + } + + #[test] + fn test_compute_metrics_single_element() { + let metrics = compute_metrics(&[Duration::from_millis(1000)], 1024 * 1024).unwrap(); + assert_eq!(metrics.mean, Duration::from_millis(1000)); + assert_eq!(metrics.p50, Duration::from_millis(1000)); + assert_eq!(metrics.p90, Duration::from_millis(1000)); + assert_eq!(metrics.p99, Duration::from_millis(1000)); + assert!((metrics.throughput_mib_per_sec - 1.0).abs() < 1e-6); + } +} diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs new file mode 100644 index 0000000000..f29b8f1831 --- /dev/null +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -0,0 +1,177 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::args::Args; +use super::metrics::Metrics; +use super::scenarios::IterationResult; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +/// Holds the structured results and parameters for a benchmark run. +#[derive(Debug)] +pub struct BenchmarkReport<'a> { + /// Scenario name (e.g. Option A, Option B, Option C). + pub scenario: &'a str, + /// Object size in bytes. + pub object_size: usize, + /// Number of warmup iterations. + pub warmup_iterations: usize, + /// Number of measured iterations. + pub measured_iterations: usize, + /// Number of errors encountered. + pub errors: usize, + /// Calculated latency metrics. + pub metrics: Option, + /// Average precomputation duration if applicable (Option B). + pub mean_precompute_ms: Option, +} + +impl BenchmarkReport<'_> { + /// Prints the report summary to standard output. + pub fn print_stdout(&self) { + println!("-----------------------------------------"); + println!("Scenario: {}", self.scenario); + println!( + "Object Size: {} bytes ({:.2} MiB)", + self.object_size, + self.object_size as f64 / (1024.0 * 1024.0) + ); + println!("Warmup Iterations: {}", self.warmup_iterations); + println!("Measured Iterations: {}", self.measured_iterations); + println!("Errors Recorded: {}", self.errors); + if let Some(m) = &self.metrics { + println!("Mean Latency: {:?}", m.mean); + println!("P50 (Median) Lat: {:?}", m.p50); + println!("P90 Latency: {:?}", m.p90); + println!("P99 Latency: {:?}", m.p99); + println!("Throughput: {:.2} MiB/s", m.throughput_mib_per_sec); + } + if let Some(precompute_ms) = self.mean_precompute_ms { + println!("Pass 1 Hash Time: {:.2} ms", precompute_ms); + } + println!("-----------------------------------------"); + } + + /// Writes the report summary in JSON format to a writer. + pub fn write_json(&self, writer: &mut W) -> std::io::Result<()> { + writeln!(writer, "{{")?; + writeln!(writer, " \"scenario\": \"{}\",", self.scenario)?; + writeln!(writer, " \"object_size_bytes\": {},", self.object_size)?; + writeln!( + writer, + " \"warmup_iterations\": {},", + self.warmup_iterations + )?; + writeln!( + writer, + " \"measured_iterations\": {},", + self.measured_iterations + )?; + writeln!(writer, " \"errors\": {},", self.errors)?; + if let Some(m) = &self.metrics { + writeln!(writer, " \"mean_latency_ms\": {},", m.mean.as_millis())?; + writeln!(writer, " \"p50_latency_ms\": {},", m.p50.as_millis())?; + writeln!(writer, " \"p90_latency_ms\": {},", m.p90.as_millis())?; + writeln!(writer, " \"p99_latency_ms\": {},", m.p99.as_millis())?; + writeln!( + writer, + " \"throughput_mib_s\": {:.2},", + m.throughput_mib_per_sec + )?; + } else { + writeln!(writer, " \"metrics\": null,")?; + } + if let Some(precompute_ms) = self.mean_precompute_ms { + writeln!(writer, " \"mean_precompute_ms\": {:.2}", precompute_ms)?; + } else { + writeln!(writer, " \"mean_precompute_ms\": null")?; + } + writeln!(writer, "}}") + } +} + +/// Formats and outputs the benchmark metrics to stdout and optionally to output files. +pub fn report( + scenario_name: &str, + metrics: Option, + results: &[IterationResult], + errors: usize, + args: &Args, +) -> anyhow::Result<()> { + let mean_precompute_ms = if results.is_empty() { + None + } else { + let precomputes: Vec<_> = results + .iter() + .filter_map(|r| r.precompute_duration) + .collect(); + if precomputes.is_empty() { + None + } else { + let sum_ms: f64 = precomputes.iter().map(|d| d.as_secs_f64() * 1000.0).sum(); + Some(sum_ms / precomputes.len() as f64) + } + }; + + let report = BenchmarkReport { + scenario: scenario_name, + object_size: args.object_size, + warmup_iterations: args.warmup_iterations, + measured_iterations: args.measured_iterations, + errors, + metrics, + mean_precompute_ms, + }; + + report.print_stdout(); + + if let Some(dir_str) = args + .output_dir + .as_deref() + .filter(|s| !s.is_empty() && !results.is_empty()) + { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let output_dir = Path::new(dir_str); + std::fs::create_dir_all(output_dir)?; + + let csv_path = output_dir.join(format!( + "{}_s{}_{}_raw.csv", + scenario_name, args.object_size, timestamp + )); + let mut csv_file = File::create(&csv_path)?; + writeln!(csv_file, "iteration,total_latency_ms,precompute_ms")?; + for (i, r) in results.iter().enumerate() { + let pre_ms = r + .precompute_duration + .map(|d| format!("{:.2}", d.as_secs_f64() * 1000.0)) + .unwrap_or_else(|| "0.0".to_string()); + writeln!(csv_file, "{},{},{}", i, r.total_elapsed.as_millis(), pre_ms)?; + } + println!("Raw latencies written to {}", csv_path.display()); + + let json_path = output_dir.join(format!( + "{}_s{}_{}_summary.json", + scenario_name, args.object_size, timestamp + )); + let mut json_file = File::create(&json_path)?; + report.write_json(&mut json_file)?; + println!("Metrics summary written to {}", json_path.display()); + } + + Ok(()) +} diff --git a/src/storage/benchmarks/write_object/src/scenarios.rs b/src/storage/benchmarks/write_object/src/scenarios.rs new file mode 100644 index 0000000000..3acfde0c04 --- /dev/null +++ b/src/storage/benchmarks/write_object/src/scenarios.rs @@ -0,0 +1,147 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use google_cloud_storage::client::{Storage, StorageControl}; +use std::path::Path; +use std::time::{Duration, Instant}; +use tokio::fs::File; + +/// Result of a single benchmark run iteration. +#[derive(Debug, Clone)] +pub struct IterationResult { + /// Total elapsed time for the upload operation. + pub total_elapsed: Duration, + /// Precomputation duration (Option B only). + pub precompute_duration: Option, +} + +/// Scenario A: Baseline 1-Pass Unbuffered Stream (Option A) +/// - Continuous stream without precomputed checksum. +/// - Client-side validation only, 0 RAM buffer. +pub async fn scenario_option_a( + client: &Storage, + bucket_name: &str, + object_name: &str, + file_path: &Path, + object_size: usize, +) -> anyhow::Result { + let file = File::open(file_path).await?; + let start_time = Instant::now(); + + let object = client + .write_object(bucket_name, object_name, file) + .send_unbuffered() + .await?; + + let total_elapsed = start_time.elapsed(); + + if object.size as usize != object_size { + anyhow::bail!( + "persisted size mismatch: expected {}, got {}", + object_size, + object.size + ); + } + + Ok(IterationResult { + total_elapsed, + precompute_duration: None, + }) +} + +/// Scenario B: 2-Pass Unbuffered Stream (Option B) +/// - Pass 1: Local hash computation (`precompute_checksums()`). +/// - Pass 2: Continuous stream with server-side validation, 0 RAM buffer. +pub async fn scenario_option_b( + client: &Storage, + bucket_name: &str, + object_name: &str, + file_path: &Path, + object_size: usize, +) -> anyhow::Result { + let file = File::open(file_path).await?; + let total_start = Instant::now(); + + let precompute_start = Instant::now(); + let write_builder = client + .write_object(bucket_name, object_name, file) + .precompute_checksums() + .await?; + let precompute_duration = precompute_start.elapsed(); + + let object = write_builder.send_unbuffered().await?; + let total_elapsed = total_start.elapsed(); + + if object.size as usize != object_size { + anyhow::bail!( + "persisted size mismatch: expected {}, got {}", + object_size, + object.size + ); + } + + Ok(IterationResult { + total_elapsed, + precompute_duration: Some(precompute_duration), + }) +} + +/// Scenario C: 1-Pass Chunked Buffered Upload (Option C) +/// - Sequential 8 MiB chunk PUT requests. +/// - Server-side validation attached to final chunk, 8 MiB RAM buffer. +pub async fn scenario_option_c( + client: &Storage, + bucket_name: &str, + object_name: &str, + file_path: &Path, + object_size: usize, +) -> anyhow::Result { + let file = File::open(file_path).await?; + let start_time = Instant::now(); + + let object = client + .write_object(bucket_name, object_name, file) + .send_buffered() + .await?; + + let total_elapsed = start_time.elapsed(); + + if object.size as usize != object_size { + anyhow::bail!( + "persisted size mismatch: expected {}, got {}", + object_size, + object.size + ); + } + + Ok(IterationResult { + total_elapsed, + precompute_duration: None, + }) +} + +/// Cleans up a test object from GCS. +pub async fn cleanup_object( + control: &StorageControl, + bucket_name: &str, + object_name: &str, +) -> anyhow::Result<()> { + control + .delete_object() + .set_bucket(bucket_name) + .set_object(object_name) + .send() + .await?; + Ok(()) +} diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs new file mode 100644 index 0000000000..e1a655bd44 --- /dev/null +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -0,0 +1,43 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use rand::rngs::StdRng; +use rand::{RngExt, SeedableRng}; +use std::path::PathBuf; +use tempfile::NamedTempFile; +use tokio::io::AsyncWriteExt; + +/// Creates a temporary file of the given size populated with pseudo-random bytes. +/// Returns the path to the temporary file and the NamedTempFile handle. +pub async fn create_temp_test_file(size_bytes: usize) -> anyhow::Result<(NamedTempFile, PathBuf)> { + let temp_file = NamedTempFile::new()?; + let path = temp_file.path().to_path_buf(); + + // Use a 1 MiB chunk of pseudo-random data written repeatedly to disk + let chunk_size = 1024 * 1024; // 1 MiB + let mut rng = StdRng::seed_from_u64(42); + let mut pattern = vec![0u8; chunk_size.min(size_bytes)]; + rng.fill(&mut pattern[..]); + + let mut async_file = tokio::fs::File::create(&path).await?; + let mut remaining = size_bytes; + while remaining > 0 { + let to_write = remaining.min(pattern.len()); + async_file.write_all(&pattern[..to_write]).await?; + remaining -= to_write; + } + async_file.flush().await?; + + Ok((temp_file, path)) +} From 6bb1e17c4a5176b029dd8564cc38c03f16f49dd5 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 05:02:29 +0000 Subject: [PATCH 02/15] Add cold cache start for more realistic simulation --- Cargo.lock | 1 + .../benchmarks/write_object/Cargo.toml | 1 + src/storage/benchmarks/write_object/README.md | 14 ++++++-- .../benchmarks/write_object/src/args.rs | 10 ++++++ .../benchmarks/write_object/src/main.rs | 12 ++++++- .../benchmarks/write_object/src/reporter.rs | 5 +++ .../benchmarks/write_object/src/source.rs | 34 +++++++++++++++++-- 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe1ed2ee45..8f9582500d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9544,6 +9544,7 @@ dependencies = [ "crc32c", "google-cloud-auth", "google-cloud-storage", + "libc", "rand 0.10.2", "tempfile", "tokio", diff --git a/src/storage/benchmarks/write_object/Cargo.toml b/src/storage/benchmarks/write_object/Cargo.toml index af711b1a49..fce1ab45ac 100644 --- a/src/storage/benchmarks/write_object/Cargo.toml +++ b/src/storage/benchmarks/write_object/Cargo.toml @@ -30,6 +30,7 @@ clap = { workspace = true, features = ["derive", "env", " crc32c.workspace = true google-cloud-auth.workspace = true google-cloud-storage = { workspace = true, features = ["default-rustls-provider", "unstable-stream"] } +libc = "0.2" rand.workspace = true tempfile.workspace = true tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread"] } diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md index 55ddd9e1ca..6c2ec02da7 100644 --- a/src/storage/benchmarks/write_object/README.md +++ b/src/storage/benchmarks/write_object/README.md @@ -12,8 +12,8 @@ When uploading seekable data (such as local disk files), SDKs face a protocol tr * Checksum calculated on the fly; verified client-side upon completion. * **Option B (`Option_B_Unbuffered_2Pass`)**: * Code: `.precompute_checksums().await?.send_unbuffered()` - * **Pass 1:** Scans the local file to compute SIMD CRC32C checksum. - * **Pass 2:** Streams data via a single continuous PUT request with server-side validation header (`x-goog-hash`). + * **Pass 1:** Scans the local file to compute SIMD CRC32C checksum (reads from physical disk if cold, populates page cache). + * **Pass 2:** Streams data via a single continuous PUT request with server-side validation header (`x-goog-hash`). Reads from OS page cache. * 0 application RAM buffer. * **Option C (`Option_C_Buffered_Chunked`)**: * Code: `.send_buffered()` @@ -29,6 +29,15 @@ When uploading seekable data (such as local disk files), SDKs face a protocol tr 4. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream in Option B. 5. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous stream in Option B. +## Page Cache & Cold Disk Simulation + +By default, `--cold-cache=true` is enabled. Before every iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM). This ensures that: +- **Option A** performs 1 cold disk read (streamed to network). +- **Option B** performs 1 cold disk read in Pass 1 (local SIMD checksumming), and Pass 2 naturally streams from the hot page cache populated by Pass 1. +- **Option C** performs 1 cold disk read (in 8 MiB chunks streamed to network). + +To place the test file on a specific physical SSD/HDD mount instead of `/tmp`, pass `--temp-dir=/path/to/mount`. + ## Pre-requisites - Authenticate with GCP credentials: @@ -53,6 +62,7 @@ chmod +x run_all.sh cargo run --release -p storage-benchmark-write-object -- \ --object-size 67108864 \ --scenario all \ + --cold-cache true \ --warmup-iterations 1 \ --measured-iterations 5 ``` diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index d1811f7d8a..01d125558f 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -49,6 +49,16 @@ pub struct Args { #[arg(long, value_enum, default_value_t = UploadScenario::All)] pub scenario: UploadScenario, + /// Whether to evict the test file from the OS page cache (RAM) before each iteration to + /// simulate a cold physical disk read. + #[arg(long, default_value_t = true)] + pub cold_cache: bool, + + /// Custom directory for creating the temporary test file (e.g., on a real physical SSD/HDD + /// mount instead of `/tmp`). If not specified, the system default temporary directory is used. + #[arg(long)] + pub temp_dir: Option, + /// Directory for output artifacts (raw CSV latencies and summary JSON). If not provided, file /// reporting is skipped. #[arg(long)] diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index a3e130ef9f..c18804ad21 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -52,12 +52,14 @@ async fn main() -> anyhow::Result<()> { args.object_size, args.object_size as f64 / (1024.0 * 1024.0) ); + println!("Cold Cache Eviction: {}", args.cold_cache); println!("Warmup Iterations: {}", args.warmup_iterations); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); println!("Generating local test file on disk..."); - let (_temp_handle, temp_file_path) = source::create_temp_test_file(args.object_size).await?; + let (_temp_handle, temp_file_path) = + source::create_temp_test_file(args.object_size, args.temp_dir.as_deref()).await?; println!("Test file created at: {}", temp_file_path.display()); let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); @@ -153,6 +155,14 @@ async fn run_single_scenario( let total_iterations = args.warmup_iterations + args.measured_iterations; for i in 0..total_iterations { + // If cold_cache is enabled, evict the test file from OS page cache once before the + // iteration starts to ensure a cold physical disk read. + if args.cold_cache + && let Err(e) = source::drop_file_from_page_cache(file_path) + { + eprintln!("Warning: Failed to drop file from page cache: {e}"); + } + let object_name = format!("bench-write-object-{}", Uuid::new_v4()); let res = match scenario { UploadScenario::OptionA => { diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs index f29b8f1831..ed4ebe76a0 100644 --- a/src/storage/benchmarks/write_object/src/reporter.rs +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -30,6 +30,8 @@ pub struct BenchmarkReport<'a> { pub warmup_iterations: usize, /// Number of measured iterations. pub measured_iterations: usize, + /// Whether cold-cache eviction was enabled before each iteration. + pub cold_cache: bool, /// Number of errors encountered. pub errors: usize, /// Calculated latency metrics. @@ -48,6 +50,7 @@ impl BenchmarkReport<'_> { self.object_size, self.object_size as f64 / (1024.0 * 1024.0) ); + println!("Cold Cache Eviction: {}", self.cold_cache); println!("Warmup Iterations: {}", self.warmup_iterations); println!("Measured Iterations: {}", self.measured_iterations); println!("Errors Recorded: {}", self.errors); @@ -69,6 +72,7 @@ impl BenchmarkReport<'_> { writeln!(writer, "{{")?; writeln!(writer, " \"scenario\": \"{}\",", self.scenario)?; writeln!(writer, " \"object_size_bytes\": {},", self.object_size)?; + writeln!(writer, " \"cold_cache\": {},", self.cold_cache)?; writeln!( writer, " \"warmup_iterations\": {},", @@ -130,6 +134,7 @@ pub fn report( object_size: args.object_size, warmup_iterations: args.warmup_iterations, measured_iterations: args.measured_iterations, + cold_cache: args.cold_cache, errors, metrics, mean_precompute_ms, diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index e1a655bd44..2d080bf7a3 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -14,14 +14,25 @@ use rand::rngs::StdRng; use rand::{RngExt, SeedableRng}; -use std::path::PathBuf; +use std::fs::File; +use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use tokio::io::AsyncWriteExt; +#[cfg(unix)] +use std::os::unix::io::AsRawFd; + /// Creates a temporary file of the given size populated with pseudo-random bytes. +/// If `temp_dir` is provided, the file is created in that directory. /// Returns the path to the temporary file and the NamedTempFile handle. -pub async fn create_temp_test_file(size_bytes: usize) -> anyhow::Result<(NamedTempFile, PathBuf)> { - let temp_file = NamedTempFile::new()?; +pub async fn create_temp_test_file( + size_bytes: usize, + temp_dir: Option<&str>, +) -> anyhow::Result<(NamedTempFile, PathBuf)> { + let temp_file = match temp_dir { + Some(dir) => NamedTempFile::new_in(dir)?, + None => NamedTempFile::new()?, + }; let path = temp_file.path().to_path_buf(); // Use a 1 MiB chunk of pseudo-random data written repeatedly to disk @@ -41,3 +52,20 @@ pub async fn create_temp_test_file(size_bytes: usize) -> anyhow::Result<(NamedTe Ok((temp_file, path)) } + +/// Evicts the given file's data from the OS page cache (RAM) to simulate a cold physical disk read. +pub fn drop_file_from_page_cache(path: &Path) -> std::io::Result<()> { + #[cfg(unix)] + { + let std_file = File::open(path)?; + let fd = std_file.as_raw_fd(); + // Sync dirty pages to disk first. + let _ = unsafe { libc::fdatasync(fd) }; + // Tell the OS kernel to discard cached pages for the entire file range. + let ret = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) }; + if ret != 0 { + return Err(std::io::Error::from_raw_os_error(ret)); + } + } + Ok(()) +} From d00fb66eba212e69ff51c3b0c011141ccacbbbc4 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:11:50 +0000 Subject: [PATCH 03/15] Update test file directory to use physical SSD storage --- src/storage/benchmarks/write_object/README.md | 12 +++++------- src/storage/benchmarks/write_object/src/args.rs | 10 ++++++---- src/storage/benchmarks/write_object/src/main.rs | 14 +++++++++++--- src/storage/benchmarks/write_object/src/source.rs | 12 ++++++------ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md index 6c2ec02da7..498cd2c13f 100644 --- a/src/storage/benchmarks/write_object/README.md +++ b/src/storage/benchmarks/write_object/README.md @@ -29,14 +29,11 @@ When uploading seekable data (such as local disk files), SDKs face a protocol tr 4. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream in Option B. 5. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous stream in Option B. -## Page Cache & Cold Disk Simulation +## Page Cache, Storage Medium & Disk Cleanup -By default, `--cold-cache=true` is enabled. Before every iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM). This ensures that: -- **Option A** performs 1 cold disk read (streamed to network). -- **Option B** performs 1 cold disk read in Pass 1 (local SIMD checksumming), and Pass 2 naturally streams from the hot page cache populated by Pass 1. -- **Option C** performs 1 cold disk read (in 8 MiB chunks streamed to network). - -To place the test file on a specific physical SSD/HDD mount instead of `/tmp`, pass `--temp-dir=/path/to/mount`. +- **Physical SSD Storage:** By default, test files are created under `/usr/local/google/tmp/rust-write-object-benchmarking-data` on the physical SSD drive (avoiding RAM-backed `/tmp`). This can be customized via `--temp-dir=/path/to/dir`. +- **Cold Cache Eviction:** By default, `--cold-cache=true` is enabled. Before every iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM), ensuring a cold physical disk read. +- **Disk Cleanup:** Upon benchmark completion, the temporary file is automatically removed from physical disk. ## Pre-requisites @@ -63,6 +60,7 @@ cargo run --release -p storage-benchmark-write-object -- \ --object-size 67108864 \ --scenario all \ --cold-cache true \ + --temp-dir /usr/local/google/tmp/rust-write-object-benchmarking-data \ --warmup-iterations 1 \ --measured-iterations 5 ``` diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index 01d125558f..5186c047d5 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -54,10 +54,12 @@ pub struct Args { #[arg(long, default_value_t = true)] pub cold_cache: bool, - /// Custom directory for creating the temporary test file (e.g., on a real physical SSD/HDD - /// mount instead of `/tmp`). If not specified, the system default temporary directory is used. - #[arg(long)] - pub temp_dir: Option, + /// Directory on physical SSD storage for creating the temporary test file. + #[arg( + long, + default_value = "/usr/local/google/tmp/rust-write-object-benchmarking-data" + )] + pub temp_dir: String, /// Directory for output artifacts (raw CSV latencies and summary JSON). If not provided, file /// reporting is skipped. diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index c18804ad21..0d299376bf 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -53,13 +53,14 @@ async fn main() -> anyhow::Result<()> { args.object_size as f64 / (1024.0 * 1024.0) ); println!("Cold Cache Eviction: {}", args.cold_cache); + println!("Temp Directory: {}", args.temp_dir); println!("Warmup Iterations: {}", args.warmup_iterations); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); - println!("Generating local test file on disk..."); - let (_temp_handle, temp_file_path) = - source::create_temp_test_file(args.object_size, args.temp_dir.as_deref()).await?; + println!("Generating local test file on physical SSD..."); + let (temp_handle, temp_file_path) = + source::create_temp_test_file(args.object_size, &args.temp_dir).await?; println!("Test file created at: {}", temp_file_path.display()); let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); @@ -131,6 +132,13 @@ async fn main() -> anyhow::Result<()> { } } + println!( + "\nDeleting local benchmark test file on disk: {}", + temp_file_path.display() + ); + drop(temp_handle); // Automatically removes the temporary file from physical disk. + println!("Local test file successfully deleted."); + Ok(()) } diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index 2d080bf7a3..6c737dde93 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -23,16 +23,16 @@ use tokio::io::AsyncWriteExt; use std::os::unix::io::AsRawFd; /// Creates a temporary file of the given size populated with pseudo-random bytes. -/// If `temp_dir` is provided, the file is created in that directory. +/// The file is created in `temp_dir` on physical SSD storage. /// Returns the path to the temporary file and the NamedTempFile handle. pub async fn create_temp_test_file( size_bytes: usize, - temp_dir: Option<&str>, + temp_dir: &str, ) -> anyhow::Result<(NamedTempFile, PathBuf)> { - let temp_file = match temp_dir { - Some(dir) => NamedTempFile::new_in(dir)?, - None => NamedTempFile::new()?, - }; + // Ensure parent directory exists + std::fs::create_dir_all(temp_dir)?; + + let temp_file = NamedTempFile::new_in(temp_dir)?; let path = temp_file.path().to_path_buf(); // Use a 1 MiB chunk of pseudo-random data written repeatedly to disk From 1614d7e207b3a6a1731f3326433895686b314d39 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:35:26 +0000 Subject: [PATCH 04/15] Fix warmup iteration to run once on half MiB object --- src/storage/benchmarks/write_object/README.md | 12 +++-- .../benchmarks/write_object/src/args.rs | 6 +-- .../benchmarks/write_object/src/main.rs | 45 +++++++++-------- .../benchmarks/write_object/src/reporter.rs | 9 ---- .../benchmarks/write_object/src/source.rs | 50 +++++++++++++++++++ 5 files changed, 84 insertions(+), 38 deletions(-) diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md index 498cd2c13f..3a959b0868 100644 --- a/src/storage/benchmarks/write_object/README.md +++ b/src/storage/benchmarks/write_object/README.md @@ -29,10 +29,17 @@ When uploading seekable data (such as local disk files), SDKs face a protocol tr 4. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream in Option B. 5. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous stream in Option B. +## Pre-flight Check & 512 KiB Global Warmup + +Before creating large test files or running measured iterations, the benchmark performs a single **512 KiB pre-flight warmup check**: +- Validates Google Cloud authentication and bucket write/delete permissions. If authentication or permissions fail, the benchmark aborts immediately with remediation guidance. +- Primes DNS resolution and the TLS 1.3 keep-alive connection pool. +- Eliminates the need for redundant multi-gigabyte warmup uploads during actual scenario testing. + ## Page Cache, Storage Medium & Disk Cleanup -- **Physical SSD Storage:** By default, test files are created under `/usr/local/google/tmp/rust-write-object-benchmarking-data` on the physical SSD drive (avoiding RAM-backed `/tmp`). This can be customized via `--temp-dir=/path/to/dir`. -- **Cold Cache Eviction:** By default, `--cold-cache=true` is enabled. Before every iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM), ensuring a cold physical disk read. +- **Physical SSD Storage:** By default, test files are created under `/usr/local/google/tmp/rust-write-object-benchmarking-data` on physical SSD storage. This can be overridden via `--temp-dir=/path/to/dir`. +- **Cold Cache Eviction:** By default, `--cold-cache=true` is enabled. Before every measured iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM), ensuring a cold physical disk read. - **Disk Cleanup:** Upon benchmark completion, the temporary file is automatically removed from physical disk. ## Pre-requisites @@ -61,7 +68,6 @@ cargo run --release -p storage-benchmark-write-object -- \ --scenario all \ --cold-cache true \ --temp-dir /usr/local/google/tmp/rust-write-object-benchmarking-data \ - --warmup-iterations 1 \ --measured-iterations 5 ``` diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index 5186c047d5..c1cee73b9c 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -33,11 +33,7 @@ pub struct Args { #[arg(long, env = "GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET")] pub bucket_name: String, - /// Number of warmup iterations. - #[arg(long, default_value_t = 1)] - pub warmup_iterations: usize, - - /// Number of measured iterations. + /// Number of measured iterations per scenario. #[arg(long, default_value_t = 5)] pub measured_iterations: usize, diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index 0d299376bf..59b9fc66b5 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -54,17 +54,24 @@ async fn main() -> anyhow::Result<()> { ); println!("Cold Cache Eviction: {}", args.cold_cache); println!("Temp Directory: {}", args.temp_dir); - println!("Warmup Iterations: {}", args.warmup_iterations); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); - println!("Generating local test file on physical SSD..."); + let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); + + // Pre-flight check: 512 KiB global warmup to verify auth & prime TLS connection pool + println!("\n[1/3] Running pre-flight warmup check (512 KiB payload)..."); + source::perform_global_warmup(&client, &control, &formatted_bucket).await?; + println!("Pre-flight warmup check succeeded: Authentication verified & TLS pool primed."); + + // Generate local test file on physical SSD + println!("\n[2/3] Generating local test file on physical SSD..."); let (temp_handle, temp_file_path) = source::create_temp_test_file(args.object_size, &args.temp_dir).await?; println!("Test file created at: {}", temp_file_path.display()); - let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); - + // Execute upload benchmark scenarios + println!("\n[3/3] Executing benchmark scenarios..."); match args.scenario { UploadScenario::OptionA => { run_single_scenario( @@ -132,11 +139,12 @@ async fn main() -> anyhow::Result<()> { } } + // Clean up local physical disk file println!( - "\nDeleting local benchmark test file on disk: {}", + "\nCleaning up local test file on disk: {}", temp_file_path.display() ); - drop(temp_handle); // Automatically removes the temporary file from physical disk. + drop(temp_handle); println!("Local test file successfully deleted."); Ok(()) @@ -160,9 +168,8 @@ async fn run_single_scenario( println!("\n>>> Running Scenario: {} <<<", scenario_name); let mut results = Vec::new(); let mut errors = 0; - let total_iterations = args.warmup_iterations + args.measured_iterations; - for i in 0..total_iterations { + for i in 0..args.measured_iterations { // If cold_cache is enabled, evict the test file from OS page cache once before the // iteration starts to ensure a cold physical disk read. if args.cold_cache @@ -208,19 +215,15 @@ async fn run_single_scenario( match res { Ok(r) => { - if i < args.warmup_iterations { - println!("Warmup {:>2}: {:?}", i + 1, r.total_elapsed); - } else { - println!( - "Measured {:>2}: {:?}{}", - i - args.warmup_iterations + 1, - r.total_elapsed, - r.precompute_duration - .map(|d| format!(" (Pass 1 Hash: {:?})", d)) - .unwrap_or_default() - ); - results.push(r); - } + println!( + "Measured {:>2}: {:?}{}", + i + 1, + r.total_elapsed, + r.precompute_duration + .map(|d| format!(" (Pass 1 Hash: {:?})", d)) + .unwrap_or_default() + ); + results.push(r); } Err(err) => { eprintln!("Error during iteration {}: {err:#}", i + 1); diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs index ed4ebe76a0..ede61433bb 100644 --- a/src/storage/benchmarks/write_object/src/reporter.rs +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -26,8 +26,6 @@ pub struct BenchmarkReport<'a> { pub scenario: &'a str, /// Object size in bytes. pub object_size: usize, - /// Number of warmup iterations. - pub warmup_iterations: usize, /// Number of measured iterations. pub measured_iterations: usize, /// Whether cold-cache eviction was enabled before each iteration. @@ -51,7 +49,6 @@ impl BenchmarkReport<'_> { self.object_size as f64 / (1024.0 * 1024.0) ); println!("Cold Cache Eviction: {}", self.cold_cache); - println!("Warmup Iterations: {}", self.warmup_iterations); println!("Measured Iterations: {}", self.measured_iterations); println!("Errors Recorded: {}", self.errors); if let Some(m) = &self.metrics { @@ -73,11 +70,6 @@ impl BenchmarkReport<'_> { writeln!(writer, " \"scenario\": \"{}\",", self.scenario)?; writeln!(writer, " \"object_size_bytes\": {},", self.object_size)?; writeln!(writer, " \"cold_cache\": {},", self.cold_cache)?; - writeln!( - writer, - " \"warmup_iterations\": {},", - self.warmup_iterations - )?; writeln!( writer, " \"measured_iterations\": {},", @@ -132,7 +124,6 @@ pub fn report( let report = BenchmarkReport { scenario: scenario_name, object_size: args.object_size, - warmup_iterations: args.warmup_iterations, measured_iterations: args.measured_iterations, cold_cache: args.cold_cache, errors, diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index 6c737dde93..770111df97 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -12,16 +12,66 @@ // See the License for the specific language governing permissions and // limitations under the License. +use bytes::Bytes; +use google_cloud_storage::client::{Storage, StorageControl}; use rand::rngs::StdRng; use rand::{RngExt, SeedableRng}; use std::fs::File; use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use tokio::io::AsyncWriteExt; +use uuid::Uuid; #[cfg(unix)] use std::os::unix::io::AsRawFd; +const WARMUP_PAYLOAD_SIZE: usize = 512 * 1024; // 512 KiB + +/// Performs a one-time global warmup by uploading and deleting a 512 KiB payload. +/// Primes OAuth authentication tokens, DNS resolution, and the TLS connection pool. +/// If an authentication or permission error occurs, returns a fatal error with remediation advice. +pub async fn perform_global_warmup( + client: &Storage, + control: &StorageControl, + bucket: &str, +) -> anyhow::Result<()> { + let warmup_object_name = format!("bench-warmup-{}", Uuid::new_v4()); + let warmup_data = Bytes::from(vec![0u8; WARMUP_PAYLOAD_SIZE]); + + let upload_res = client + .write_object(bucket, &warmup_object_name, warmup_data) + .send_unbuffered() + .await; + + if let Err(e) = upload_res { + eprintln!("\n============================================================"); + eprintln!("FATAL ERROR: Pre-flight warmup check failed!"); + eprintln!("Failed to upload warmup payload (512 KiB) to bucket: {bucket}"); + eprintln!("Error details: {e:#}"); + eprintln!("------------------------------------------------------------"); + eprintln!("Troubleshooting Suggestions:"); + eprintln!("1. Authentication: Ensure your credentials are valid by running:"); + eprintln!(" gcloud auth application-default login"); + eprintln!("2. Bucket Access: Ensure the target bucket exists and your account has"); + eprintln!(" 'Storage Object Admin' (or 'Storage Object Creator') permissions:"); + eprintln!(" export GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET=\"\""); + eprintln!("============================================================\n"); + anyhow::bail!("Warmup pre-flight check failed: {e}"); + } + + if let Err(e) = control + .delete_object() + .set_bucket(bucket) + .set_object(&warmup_object_name) + .send() + .await + { + eprintln!("Warning: Failed to delete warmup object {warmup_object_name}: {e}"); + } + + Ok(()) +} + /// Creates a temporary file of the given size populated with pseudo-random bytes. /// The file is created in `temp_dir` on physical SSD storage. /// Returns the path to the temporary file and the NamedTempFile handle. From 1459f60f323982597bad296fa2f4fe1b87163344 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:49:07 +0000 Subject: [PATCH 05/15] fix output dir and object name --- .../benchmarks/write_object/src/args.rs | 10 ++- .../benchmarks/write_object/src/main.rs | 3 +- .../benchmarks/write_object/src/reporter.rs | 78 +++++++++++-------- 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index c1cee73b9c..2ad054ae95 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -57,10 +57,12 @@ pub struct Args { )] pub temp_dir: String, - /// Directory for output artifacts (raw CSV latencies and summary JSON). If not provided, file - /// reporting is skipped. - #[arg(long)] - pub output_dir: Option, + /// Directory for saving output artifacts (raw CSV latencies and summary JSON). + #[arg( + long, + default_value = "/usr/local/google/tmp/rust-write-object-benchmarking-data/results" + )] + pub output_dir: String, /// Whether to delete test objects from GCS after each iteration. #[arg(long, default_value_t = true)] diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index 59b9fc66b5..853af3cd44 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -54,6 +54,7 @@ async fn main() -> anyhow::Result<()> { ); println!("Cold Cache Eviction: {}", args.cold_cache); println!("Temp Directory: {}", args.temp_dir); + println!("Output Directory: {}", args.output_dir); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); @@ -178,7 +179,7 @@ async fn run_single_scenario( eprintln!("Warning: Failed to drop file from page cache: {e}"); } - let object_name = format!("bench-write-object-{}", Uuid::new_v4()); + let object_name = format!("bench-write-object-{}-{}", scenario_name, Uuid::new_v4()); let res = match scenario { UploadScenario::OptionA => { scenarios::scenario_option_a( diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs index ede61433bb..2c1c97a4cc 100644 --- a/src/storage/benchmarks/write_object/src/reporter.rs +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -98,7 +98,7 @@ impl BenchmarkReport<'_> { } } -/// Formats and outputs the benchmark metrics to stdout and optionally to output files. +/// Formats and outputs the benchmark metrics to stdout and always persists CSV and JSON to disk. pub fn report( scenario_name: &str, metrics: Option, @@ -131,43 +131,55 @@ pub fn report( mean_precompute_ms, }; + // 1. Output summary table to terminal report.print_stdout(); - if let Some(dir_str) = args - .output_dir - .as_deref() - .filter(|s| !s.is_empty() && !results.is_empty()) - { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let output_dir = Path::new(dir_str); - std::fs::create_dir_all(output_dir)?; + // 2. Always persist raw CSV and summary JSON to output_dir + let output_dir = Path::new(&args.output_dir); + std::fs::create_dir_all(output_dir).map_err(|e| { + anyhow::anyhow!( + "Failed to create output directory '{}'. Check write permissions: {e}", + output_dir.display() + ) + })?; - let csv_path = output_dir.join(format!( - "{}_s{}_{}_raw.csv", - scenario_name, args.object_size, timestamp - )); - let mut csv_file = File::create(&csv_path)?; - writeln!(csv_file, "iteration,total_latency_ms,precompute_ms")?; - for (i, r) in results.iter().enumerate() { - let pre_ms = r - .precompute_duration - .map(|d| format!("{:.2}", d.as_secs_f64() * 1000.0)) - .unwrap_or_else(|| "0.0".to_string()); - writeln!(csv_file, "{},{},{}", i, r.total_elapsed.as_millis(), pre_ms)?; - } - println!("Raw latencies written to {}", csv_path.display()); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); - let json_path = output_dir.join(format!( - "{}_s{}_{}_summary.json", - scenario_name, args.object_size, timestamp - )); - let mut json_file = File::create(&json_path)?; - report.write_json(&mut json_file)?; - println!("Metrics summary written to {}", json_path.display()); + let csv_path = output_dir.join(format!( + "{}_s{}_{}_raw.csv", + scenario_name, args.object_size, timestamp + )); + let mut csv_file = File::create(&csv_path).map_err(|e| { + anyhow::anyhow!( + "Failed to create raw CSV file '{}'. Check permissions: {e}", + csv_path.display() + ) + })?; + writeln!(csv_file, "iteration,total_latency_ms,precompute_ms")?; + for (i, r) in results.iter().enumerate() { + let pre_ms = r + .precompute_duration + .map(|d| format!("{:.2}", d.as_secs_f64() * 1000.0)) + .unwrap_or_else(|| "0.0".to_string()); + writeln!(csv_file, "{},{},{}", i, r.total_elapsed.as_millis(), pre_ms)?; } + println!("Raw latencies saved to: {}", csv_path.display()); + + let json_path = output_dir.join(format!( + "{}_s{}_{}_summary.json", + scenario_name, args.object_size, timestamp + )); + let mut json_file = File::create(&json_path).map_err(|e| { + anyhow::anyhow!( + "Failed to create summary JSON file '{}'. Check permissions: {e}", + json_path.display() + ) + })?; + report.write_json(&mut json_file)?; + println!("Summary metrics saved to: {}", json_path.display()); Ok(()) } From 4c23fe414a15110eebe91afe0407ed3188948afa Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:24:00 +0000 Subject: [PATCH 06/15] fix position argument of flag --- .../benchmarks/write_object/src/args.rs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index 2ad054ae95..ff2b998e4b 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -47,7 +47,7 @@ pub struct Args { /// Whether to evict the test file from the OS page cache (RAM) before each iteration to /// simulate a cold physical disk read. - #[arg(long, default_value_t = true)] + #[arg(long, action = clap::ArgAction::Set, default_value_t = true)] pub cold_cache: bool, /// Directory on physical SSD storage for creating the temporary test file. @@ -65,6 +65,38 @@ pub struct Args { pub output_dir: String, /// Whether to delete test objects from GCS after each iteration. - #[arg(long, default_value_t = true)] + #[arg(long, action = clap::ArgAction::Set, default_value_t = true)] pub cleanup: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_args_parsing_with_boolean_values() { + let args = Args::try_parse_from([ + "benchmark", + "--bucket-name", + "test-bucket", + "--object-size", + "12582912", + "--scenario", + "option-a", + "--cleanup", + "false", + "--cold-cache", + "false", + "--measured-iterations", + "1", + ]) + .unwrap(); + + assert_eq!(args.bucket_name, "test-bucket"); + assert_eq!(args.object_size, 12_582_912); + assert_eq!(args.scenario, UploadScenario::OptionA); + assert!(!args.cleanup); + assert!(!args.cold_cache); + assert_eq!(args.measured_iterations, 1); + } +} From 16bf13e983a37d321a7e91f287531bd01eb72632 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:42:35 +0000 Subject: [PATCH 07/15] configure the proper default values for bucket, input, output paths --- src/storage/benchmarks/write_object/README.md | 35 +++++++++---- .../benchmarks/write_object/src/args.rs | 22 ++++---- .../benchmarks/write_object/src/main.rs | 50 +++++++++++++++++-- .../benchmarks/write_object/src/reporter.rs | 9 +++- 4 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md index 3a959b0868..8e85b30e9e 100644 --- a/src/storage/benchmarks/write_object/README.md +++ b/src/storage/benchmarks/write_object/README.md @@ -48,14 +48,22 @@ Before creating large test files or running measured iterations, the benchmark p ```bash gcloud auth application-default login ``` -- Set target bucket environment variable: +- Set required benchmark environment variables: ```bash - export GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET="my-benchmark-bucket" + # Target GCS bucket for uploads (required) + export GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET="rust-write-object-benchmark-bucket" + + # Directory on physical SSD for temporary test file generation (required) + export GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH="/usr/local/google/tmp/rust-write-object-benchmarking-data" + + # Directory for saving CSV latencies and JSON summary metrics (optional) + # If omitted, metrics are printed to the terminal and file writing is skipped. + export GOOGLE_CLOUD_RUST_BENCHMARKS_STATS_OUTPUT_PATH="/usr/local/google/tmp/rust-write-object-benchmarking-data/results" ``` ## Running the Benchmark -### Run All 5 Tiers +### Run All 5 Tiers (12 MiB, 64 MiB, 512 MiB, 2 GiB, 8 GiB) ```bash chmod +x run_all.sh ./run_all.sh @@ -64,14 +72,21 @@ chmod +x run_all.sh ### Run a Single Configuration ```bash cargo run --release -p storage-benchmark-write-object -- \ - --object-size 67108864 \ + --object-size 12582912 \ --scenario all \ - --cold-cache true \ - --temp-dir /usr/local/google/tmp/rust-write-object-benchmarking-data \ --measured-iterations 5 ``` -### Save Output Metrics (CSV & JSON) -```bash -./run_all.sh --output-dir=/path/to/results -``` +### CLI Options Reference + +| Flag | Env Variable | Default | Description | +| :--- | :--- | :--- | :--- | +| `--bucket-name` | `GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET` | *(none, required)* | Target bucket name | +| `--temp-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH` | *(none, required)* | Physical SSD directory for scratch file | +| `--output-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_STATS_OUTPUT_PATH` | *(none, optional)* | Output folder for CSV and JSON reports | +| `--object-size` | | `67108864` (64 MiB) | Object size in bytes | +| `--scenario` | | `all` | Scenario (`option-a`, `option-b`, `option-c`, `all`) | +| `--cold-cache` | | `true` | Evict page cache between iterations | +| `--cleanup` | | `true` | Delete uploaded object after iteration | +| `--measured-iterations`| | `5` | Measured iterations per scenario | + diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index ff2b998e4b..b2da393c30 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -31,7 +31,7 @@ pub enum UploadScenario { pub struct Args { /// The name of the bucket to use for the benchmark. #[arg(long, env = "GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET")] - pub bucket_name: String, + pub bucket_name: Option, /// Number of measured iterations per scenario. #[arg(long, default_value_t = 5)] @@ -51,18 +51,13 @@ pub struct Args { pub cold_cache: bool, /// Directory on physical SSD storage for creating the temporary test file. - #[arg( - long, - default_value = "/usr/local/google/tmp/rust-write-object-benchmarking-data" - )] - pub temp_dir: String, + #[arg(long, env = "GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH")] + pub temp_dir: Option, /// Directory for saving output artifacts (raw CSV latencies and summary JSON). - #[arg( - long, - default_value = "/usr/local/google/tmp/rust-write-object-benchmarking-data/results" - )] - pub output_dir: String, + /// If omitted or empty, file reporting is skipped and results are printed to the terminal. + #[arg(long, env = "GOOGLE_CLOUD_RUST_BENCHMARKS_STATS_OUTPUT_PATH")] + pub output_dir: Option, /// Whether to delete test objects from GCS after each iteration. #[arg(long, action = clap::ArgAction::Set, default_value_t = true)] @@ -79,6 +74,8 @@ mod tests { "benchmark", "--bucket-name", "test-bucket", + "--temp-dir", + "/tmp/test-data", "--object-size", "12582912", "--scenario", @@ -92,7 +89,8 @@ mod tests { ]) .unwrap(); - assert_eq!(args.bucket_name, "test-bucket"); + assert_eq!(args.bucket_name.as_deref(), Some("test-bucket")); + assert_eq!(args.temp_dir.as_deref(), Some("/tmp/test-data")); assert_eq!(args.object_size, 12_582_912); assert_eq!(args.scenario, UploadScenario::OptionA); assert!(!args.cleanup); diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index 853af3cd44..2940b32f99 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -34,6 +34,46 @@ async fn main() -> anyhow::Result<()> { anyhow::bail!("Measured iterations must be greater than 0"); } + let bucket_name = match args.bucket_name.as_deref().filter(|s| !s.trim().is_empty()) { + Some(b) => b.to_string(), + None => { + eprintln!("\n============================================================"); + eprintln!("ERROR: Target GCS bucket name is missing!"); + eprintln!("Please specify the bucket name via one of the following:"); + eprintln!(" 1. Flag: --bucket-name "); + eprintln!( + " 2. Environment variable: export GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET=\"\"" + ); + eprintln!("============================================================\n"); + anyhow::bail!( + "Missing bucket name. Set GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET or pass --bucket-name." + ); + } + }; + + let temp_dir = match args.temp_dir.as_deref().filter(|s| !s.trim().is_empty()) { + Some(d) => d.to_string(), + None => { + eprintln!("\n============================================================"); + eprintln!("ERROR: Temporary data directory path is missing!"); + eprintln!("Please specify the temporary directory on physical storage via:"); + eprintln!(" 1. Flag: --temp-dir "); + eprintln!( + " 2. Environment variable: export GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH=\"/usr/local/google/tmp/rust-write-object-benchmarking-data\"" + ); + eprintln!("============================================================\n"); + anyhow::bail!( + "Missing temporary directory. Set GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH or pass --temp-dir." + ); + } + }; + + let output_dir_display = args + .output_dir + .as_deref() + .filter(|s| !s.trim().is_empty()) + .unwrap_or(""); + let credentials = google_cloud_auth::credentials::Builder::default().build()?; let client = Storage::builder() .with_credentials(credentials.clone()) @@ -46,19 +86,19 @@ async fn main() -> anyhow::Result<()> { println!("============================================================"); println!("GCS write_object Benchmark Suite"); - println!("Target Bucket: {}", args.bucket_name); + println!("Target Bucket: {}", bucket_name); println!( "Object Size: {} bytes ({:.2} MiB)", args.object_size, args.object_size as f64 / (1024.0 * 1024.0) ); println!("Cold Cache Eviction: {}", args.cold_cache); - println!("Temp Directory: {}", args.temp_dir); - println!("Output Directory: {}", args.output_dir); + println!("Temp Directory: {}", temp_dir); + println!("Output Directory: {}", output_dir_display); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); - let formatted_bucket = format!("projects/_/buckets/{}", args.bucket_name); + let formatted_bucket = format!("projects/_/buckets/{}", bucket_name); // Pre-flight check: 512 KiB global warmup to verify auth & prime TLS connection pool println!("\n[1/3] Running pre-flight warmup check (512 KiB payload)..."); @@ -68,7 +108,7 @@ async fn main() -> anyhow::Result<()> { // Generate local test file on physical SSD println!("\n[2/3] Generating local test file on physical SSD..."); let (temp_handle, temp_file_path) = - source::create_temp_test_file(args.object_size, &args.temp_dir).await?; + source::create_temp_test_file(args.object_size, &temp_dir).await?; println!("Test file created at: {}", temp_file_path.display()); // Execute upload benchmark scenarios diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs index 2c1c97a4cc..6a96f3e3c7 100644 --- a/src/storage/benchmarks/write_object/src/reporter.rs +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -134,8 +134,13 @@ pub fn report( // 1. Output summary table to terminal report.print_stdout(); - // 2. Always persist raw CSV and summary JSON to output_dir - let output_dir = Path::new(&args.output_dir); + // 2. Persist raw CSV and summary JSON to output_dir if provided + let Some(output_dir_str) = args.output_dir.as_deref().filter(|s| !s.trim().is_empty()) else { + println!("Output directory not specified; file reporting skipped (terminal output only)."); + return Ok(()); + }; + + let output_dir = Path::new(output_dir_str); std::fs::create_dir_all(output_dir).map_err(|e| { anyhow::anyhow!( "Failed to create output directory '{}'. Check write permissions: {e}", From 2f0feda202b15a061328868292c2b3389692afa5 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:45:15 +0000 Subject: [PATCH 08/15] make main modular --- .../benchmarks/write_object/src/main.rs | 193 ++++++++---------- 1 file changed, 81 insertions(+), 112 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/main.rs b/src/storage/benchmarks/write_object/src/main.rs index 2940b32f99..12963ef7a2 100644 --- a/src/storage/benchmarks/write_object/src/main.rs +++ b/src/storage/benchmarks/write_object/src/main.rs @@ -34,8 +34,57 @@ async fn main() -> anyhow::Result<()> { anyhow::bail!("Measured iterations must be greater than 0"); } - let bucket_name = match args.bucket_name.as_deref().filter(|s| !s.trim().is_empty()) { - Some(b) => b.to_string(), + let bucket_name = resolve_bucket_name(&args)?; + let temp_dir = resolve_temp_dir(&args)?; + let output_dir_display = args + .output_dir + .as_deref() + .filter(|s| !s.trim().is_empty()) + .unwrap_or(""); + + print_benchmark_banner(&bucket_name, &temp_dir, output_dir_display, &args); + + let credentials = google_cloud_auth::credentials::Builder::default().build()?; + let client = Storage::builder() + .with_credentials(credentials.clone()) + .build() + .await?; + let control = StorageControl::builder() + .with_credentials(credentials) + .build() + .await?; + + let formatted_bucket = format!("projects/_/buckets/{}", bucket_name); + + // [1/3] Pre-flight check: 512 KiB warmup to verify auth & prime TLS connection pool + println!("\n[1/3] Running pre-flight warmup check (512 KiB payload)..."); + source::perform_global_warmup(&client, &control, &formatted_bucket).await?; + println!("Pre-flight warmup check succeeded: Authentication verified & TLS pool primed."); + + // [2/3] Generate local test file on physical SSD + println!("\n[2/3] Generating local test file on physical SSD..."); + let (temp_handle, temp_file_path) = + source::create_temp_test_file(args.object_size, &temp_dir).await?; + println!("Test file created at: {}", temp_file_path.display()); + + // [3/3] Execute upload benchmark scenarios + println!("\n[3/3] Executing benchmark scenarios..."); + run_scenarios(&client, &control, &formatted_bucket, &temp_file_path, &args).await?; + + // Clean up local physical disk file + println!( + "\nCleaning up local test file on disk: {}", + temp_file_path.display() + ); + drop(temp_handle); + println!("Local test file successfully deleted."); + + Ok(()) +} + +fn resolve_bucket_name(args: &Args) -> anyhow::Result { + match args.bucket_name.as_deref().filter(|s| !s.trim().is_empty()) { + Some(b) => Ok(b.to_string()), None => { eprintln!("\n============================================================"); eprintln!("ERROR: Target GCS bucket name is missing!"); @@ -49,10 +98,12 @@ async fn main() -> anyhow::Result<()> { "Missing bucket name. Set GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET or pass --bucket-name." ); } - }; + } +} - let temp_dir = match args.temp_dir.as_deref().filter(|s| !s.trim().is_empty()) { - Some(d) => d.to_string(), +fn resolve_temp_dir(args: &Args) -> anyhow::Result { + match args.temp_dir.as_deref().filter(|s| !s.trim().is_empty()) { + Some(d) => Ok(d.to_string()), None => { eprintln!("\n============================================================"); eprintln!("ERROR: Temporary data directory path is missing!"); @@ -66,27 +117,13 @@ async fn main() -> anyhow::Result<()> { "Missing temporary directory. Set GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH or pass --temp-dir." ); } - }; - - let output_dir_display = args - .output_dir - .as_deref() - .filter(|s| !s.trim().is_empty()) - .unwrap_or(""); - - let credentials = google_cloud_auth::credentials::Builder::default().build()?; - let client = Storage::builder() - .with_credentials(credentials.clone()) - .build() - .await?; - let control = StorageControl::builder() - .with_credentials(credentials) - .build() - .await?; + } +} +fn print_benchmark_banner(bucket: &str, temp_dir: &str, output_dir: &str, args: &Args) { println!("============================================================"); println!("GCS write_object Benchmark Suite"); - println!("Target Bucket: {}", bucket_name); + println!("Target Bucket: {}", bucket); println!( "Object Size: {} bytes ({:.2} MiB)", args.object_size, @@ -94,100 +131,32 @@ async fn main() -> anyhow::Result<()> { ); println!("Cold Cache Eviction: {}", args.cold_cache); println!("Temp Directory: {}", temp_dir); - println!("Output Directory: {}", output_dir_display); + println!("Output Directory: {}", output_dir); println!("Measured Iterations: {}", args.measured_iterations); println!("============================================================"); +} - let formatted_bucket = format!("projects/_/buckets/{}", bucket_name); - - // Pre-flight check: 512 KiB global warmup to verify auth & prime TLS connection pool - println!("\n[1/3] Running pre-flight warmup check (512 KiB payload)..."); - source::perform_global_warmup(&client, &control, &formatted_bucket).await?; - println!("Pre-flight warmup check succeeded: Authentication verified & TLS pool primed."); - - // Generate local test file on physical SSD - println!("\n[2/3] Generating local test file on physical SSD..."); - let (temp_handle, temp_file_path) = - source::create_temp_test_file(args.object_size, &temp_dir).await?; - println!("Test file created at: {}", temp_file_path.display()); - - // Execute upload benchmark scenarios - println!("\n[3/3] Executing benchmark scenarios..."); - match args.scenario { - UploadScenario::OptionA => { - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionA, - ) - .await?; - } - UploadScenario::OptionB => { - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionB, - ) - .await?; - } - UploadScenario::OptionC => { - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionC, - ) - .await?; - } - UploadScenario::All => { - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionA, - ) - .await?; - - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionB, - ) - .await?; +async fn run_scenarios( + client: &Storage, + control: &StorageControl, + bucket: &str, + file_path: &Path, + args: &Args, +) -> anyhow::Result<()> { + let scenarios = match args.scenario { + UploadScenario::OptionA => vec![UploadScenario::OptionA], + UploadScenario::OptionB => vec![UploadScenario::OptionB], + UploadScenario::OptionC => vec![UploadScenario::OptionC], + UploadScenario::All => vec![ + UploadScenario::OptionA, + UploadScenario::OptionB, + UploadScenario::OptionC, + ], + }; - run_single_scenario( - &client, - &control, - &formatted_bucket, - &temp_file_path, - &args, - UploadScenario::OptionC, - ) - .await?; - } + for scenario in scenarios { + run_single_scenario(client, control, bucket, file_path, args, scenario).await?; } - - // Clean up local physical disk file - println!( - "\nCleaning up local test file on disk: {}", - temp_file_path.display() - ); - drop(temp_handle); - println!("Local test file successfully deleted."); - Ok(()) } From b3396960003f1eefddb454d73a9c944e723ca660 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:42:37 +0000 Subject: [PATCH 09/15] format --- Cargo.toml | 1 + .../benchmarks/write_object/Cargo.toml | 2 +- src/storage/benchmarks/write_object/README.md | 100 +++++++++++------- 3 files changed, 63 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1186152b11..fdf2a16ac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,6 +442,7 @@ humantime = { default-features = false, version = "2" } hyper = { default-features = false, version = "1.10" } jiff = { default-features = false, version = "0.2.32" } jsonwebtoken = { default-features = false, version = "11" } +libc = { default-features = false, version = "0.2.168" } markdown = { default-features = false, version = "1.0" } opentelemetry = { default-features = false, version = "0.32" } opentelemetry-proto = { default-features = false, version = "0.32" } diff --git a/src/storage/benchmarks/write_object/Cargo.toml b/src/storage/benchmarks/write_object/Cargo.toml index fce1ab45ac..b3c49edb6a 100644 --- a/src/storage/benchmarks/write_object/Cargo.toml +++ b/src/storage/benchmarks/write_object/Cargo.toml @@ -30,7 +30,7 @@ clap = { workspace = true, features = ["derive", "env", " crc32c.workspace = true google-cloud-auth.workspace = true google-cloud-storage = { workspace = true, features = ["default-rustls-provider", "unstable-stream"] } -libc = "0.2" +libc.workspace = true rand.workspace = true tempfile.workspace = true tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt-multi-thread"] } diff --git a/src/storage/benchmarks/write_object/README.md b/src/storage/benchmarks/write_object/README.md index 8e85b30e9e..0368a0822b 100644 --- a/src/storage/benchmarks/write_object/README.md +++ b/src/storage/benchmarks/write_object/README.md @@ -1,46 +1,67 @@ # Cloud Storage WriteObject Upload Benchmark -Benchmarks and compares upload strategies and checksum modes for [`write_object`](crate::client::Storage::write_object) in the Google Cloud Storage (GCS) Rust client library. +Benchmarks and compares upload strategies and checksum modes for +[`write_object`](crate::client::Storage::write_object) in the Google Cloud +Storage (GCS) Rust client library. ## Motivation & Scenarios -When uploading seekable data (such as local disk files), SDKs face a protocol trade-off regarding data integrity validation and throughput: - -* **Option A (`Option_A_Unbuffered_Baseline`)**: - * Code: `.send_unbuffered()` - * Single continuous stream, 0 application RAM buffer. - * Checksum calculated on the fly; verified client-side upon completion. -* **Option B (`Option_B_Unbuffered_2Pass`)**: - * Code: `.precompute_checksums().await?.send_unbuffered()` - * **Pass 1:** Scans the local file to compute SIMD CRC32C checksum (reads from physical disk if cold, populates page cache). - * **Pass 2:** Streams data via a single continuous PUT request with server-side validation header (`x-goog-hash`). Reads from OS page cache. - * 0 application RAM buffer. -* **Option C (`Option_C_Buffered_Chunked`)**: - * Code: `.send_buffered()` - * 1-pass chunked upload using sequential 8 MiB HTTP PUT requests. - * Server-side validation enforced by attaching checksum to the final chunk. - * Requires 8 MiB in-memory buffer per upload. +When uploading seekable data (such as local disk files), SDKs face a protocol +trade-off regarding data integrity validation and throughput: + +- **Option A (`Option_A_Unbuffered_Baseline`)**: + - Code: `.send_unbuffered()` + - Single continuous stream, 0 application RAM buffer. + - Checksum calculated on the fly; verified client-side upon completion. +- **Option B (`Option_B_Unbuffered_2Pass`)**: + - Code: `.precompute_checksums().await?.send_unbuffered()` + - **Pass 1:** Scans the local file to compute SIMD CRC32C checksum (reads from + physical disk if cold, populates page cache). + - **Pass 2:** Streams data via a single continuous PUT request with + server-side validation header (`x-goog-hash`). Reads from OS page cache. + - 0 application RAM buffer. +- **Option C (`Option_C_Buffered_Chunked`)**: + - Code: `.send_buffered()` + - 1-pass chunked upload using sequential 8 MiB HTTP PUT requests. + - Server-side validation enforced by attaching checksum to the final chunk. + - Requires 8 MiB in-memory buffer per upload. ## Benchmark Matrix (5 Size Tiers) -1. **12 MiB (Single-Shot Multipart)**: Exercises single-shot multipart upload path (below default 16 MiB threshold). -2. **64 MiB (Small Resumable)**: 8 chunks in Option C vs. 1 continuous stream in Option B. -3. **512 MiB (Medium Resumable)**: 64 chunks in Option C vs. 1 continuous stream in Option B. -4. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream in Option B. -5. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous stream in Option B. +1. **12 MiB (Single-Shot Multipart)**: Exercises single-shot multipart upload + path (below default 16 MiB threshold). +1. **64 MiB (Small Resumable)**: 8 chunks in Option C vs. 1 continuous stream in + Option B. +1. **512 MiB (Medium Resumable)**: 64 chunks in Option C vs. 1 continuous stream + in Option B. +1. **2 GiB (Large Resumable)**: 256 chunks in Option C vs. 1 continuous stream + in Option B. +1. **8 GiB (Stress Resumable)**: 1,024 chunks in Option C vs. 1 continuous + stream in Option B. ## Pre-flight Check & 512 KiB Global Warmup -Before creating large test files or running measured iterations, the benchmark performs a single **512 KiB pre-flight warmup check**: -- Validates Google Cloud authentication and bucket write/delete permissions. If authentication or permissions fail, the benchmark aborts immediately with remediation guidance. +Before creating large test files or running measured iterations, the benchmark +performs a single **512 KiB pre-flight warmup check**: + +- Validates Google Cloud authentication and bucket write/delete permissions. If + authentication or permissions fail, the benchmark aborts immediately with + remediation guidance. - Primes DNS resolution and the TLS 1.3 keep-alive connection pool. -- Eliminates the need for redundant multi-gigabyte warmup uploads during actual scenario testing. +- Eliminates the need for redundant multi-gigabyte warmup uploads during actual + scenario testing. ## Page Cache, Storage Medium & Disk Cleanup -- **Physical SSD Storage:** By default, test files are created under `/usr/local/google/tmp/rust-write-object-benchmarking-data` on physical SSD storage. This can be overridden via `--temp-dir=/path/to/dir`. -- **Cold Cache Eviction:** By default, `--cold-cache=true` is enabled. Before every measured iteration, the benchmark calls `posix_fadvise(DONTNEED)` once to evict the test file from the OS page cache (RAM), ensuring a cold physical disk read. -- **Disk Cleanup:** Upon benchmark completion, the temporary file is automatically removed from physical disk. +- **Physical SSD Storage:** By default, test files are created under + `/usr/local/google/tmp/rust-write-object-benchmarking-data` on physical SSD + storage. This can be overridden via `--temp-dir=/path/to/dir`. +- **Cold Cache Eviction:** By default, `--cold-cache=true` is enabled. Before + every measured iteration, the benchmark calls `posix_fadvise(DONTNEED)` once + to evict the test file from the OS page cache (RAM), ensuring a cold physical + disk read. +- **Disk Cleanup:** Upon benchmark completion, the temporary file is + automatically removed from physical disk. ## Pre-requisites @@ -64,12 +85,14 @@ Before creating large test files or running measured iterations, the benchmark p ## Running the Benchmark ### Run All 5 Tiers (12 MiB, 64 MiB, 512 MiB, 2 GiB, 8 GiB) + ```bash chmod +x run_all.sh ./run_all.sh ``` ### Run a Single Configuration + ```bash cargo run --release -p storage-benchmark-write-object -- \ --object-size 12582912 \ @@ -79,14 +102,13 @@ cargo run --release -p storage-benchmark-write-object -- \ ### CLI Options Reference -| Flag | Env Variable | Default | Description | -| :--- | :--- | :--- | :--- | -| `--bucket-name` | `GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET` | *(none, required)* | Target bucket name | -| `--temp-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH` | *(none, required)* | Physical SSD directory for scratch file | -| `--output-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_STATS_OUTPUT_PATH` | *(none, optional)* | Output folder for CSV and JSON reports | -| `--object-size` | | `67108864` (64 MiB) | Object size in bytes | -| `--scenario` | | `all` | Scenario (`option-a`, `option-b`, `option-c`, `all`) | -| `--cold-cache` | | `true` | Evict page cache between iterations | -| `--cleanup` | | `true` | Delete uploaded object after iteration | -| `--measured-iterations`| | `5` | Measured iterations per scenario | - +| Flag | Env Variable | Default | Description | +| :---------------------- | :----------------------------------------------- | :------------------ | :--------------------------------------------------- | +| `--bucket-name` | `GOOGLE_CLOUD_RUST_BENCHMARKS_BUCKET` | *(none, required)* | Target bucket name | +| `--temp-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_DATA_PATH` | *(none, required)* | Physical SSD directory for scratch file | +| `--output-dir` | `GOOGLE_CLOUD_RUST_BENCHMARKS_STATS_OUTPUT_PATH` | *(none, optional)* | Output folder for CSV and JSON reports | +| `--object-size` | | `67108864` (64 MiB) | Object size in bytes | +| `--scenario` | | `all` | Scenario (`option-a`, `option-b`, `option-c`, `all`) | +| `--cold-cache` | | `true` | Evict page cache between iterations | +| `--cleanup` | | `true` | Delete uploaded object after iteration | +| `--measured-iterations` | | `5` | Measured iterations per scenario | From bdf13251ccf430d4cb8dc88b839d2957bc6ab110 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:47:16 +0000 Subject: [PATCH 10/15] fix(storage): use non-blocking tokio::fs::create_dir_all in async fn --- src/storage/benchmarks/write_object/src/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index 770111df97..652ccb1ee7 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -80,7 +80,7 @@ pub async fn create_temp_test_file( temp_dir: &str, ) -> anyhow::Result<(NamedTempFile, PathBuf)> { // Ensure parent directory exists - std::fs::create_dir_all(temp_dir)?; + tokio::fs::create_dir_all(temp_dir).await?; let temp_file = NamedTempFile::new_in(temp_dir)?; let path = temp_file.path().to_path_buf(); From 5b1aa4a4ba3d398ad6c8cdee739396a60c4de6f3 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:49:22 +0000 Subject: [PATCH 11/15] chore(storage): add SAFETY comments for unsafe libc calls --- src/storage/benchmarks/write_object/src/source.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index 652ccb1ee7..f28857225c 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -110,8 +110,10 @@ pub fn drop_file_from_page_cache(path: &Path) -> std::io::Result<()> { let std_file = File::open(path)?; let fd = std_file.as_raw_fd(); // Sync dirty pages to disk first. + // SAFETY: `fdatasync` is called with a valid open file descriptor owned by `std_file`. let _ = unsafe { libc::fdatasync(fd) }; // Tell the OS kernel to discard cached pages for the entire file range. + // SAFETY: `posix_fadvise` is called with a valid open file descriptor owned by `std_file` and valid offset/len arguments. let ret = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) }; if ret != 0 { return Err(std::io::Error::from_raw_os_error(ret)); From 27459d011c333df1fe485d887b55f0966657bbf1 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:53:31 +0000 Subject: [PATCH 12/15] chore: bump workspace minimum libc requirement to 0.2.183 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fdf2a16ac1..4debb52eaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -442,7 +442,7 @@ humantime = { default-features = false, version = "2" } hyper = { default-features = false, version = "1.10" } jiff = { default-features = false, version = "0.2.32" } jsonwebtoken = { default-features = false, version = "11" } -libc = { default-features = false, version = "0.2.168" } +libc = { default-features = false, version = "0.2.183" } markdown = { default-features = false, version = "1.0" } opentelemetry = { default-features = false, version = "0.32" } opentelemetry-proto = { default-features = false, version = "0.32" } From d6cb03e597209866034135b76386c9941dbace93 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:55:07 +0000 Subject: [PATCH 13/15] fix(storage): use u64 for object_size in write_object benchmark --- src/storage/benchmarks/write_object/src/args.rs | 2 +- src/storage/benchmarks/write_object/src/metrics.rs | 2 +- src/storage/benchmarks/write_object/src/reporter.rs | 2 +- src/storage/benchmarks/write_object/src/scenarios.rs | 12 ++++++------ src/storage/benchmarks/write_object/src/source.rs | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/args.rs b/src/storage/benchmarks/write_object/src/args.rs index b2da393c30..07c7fee6ae 100644 --- a/src/storage/benchmarks/write_object/src/args.rs +++ b/src/storage/benchmarks/write_object/src/args.rs @@ -39,7 +39,7 @@ pub struct Args { /// The size of the object to upload in bytes. #[arg(long, default_value_t = 67_108_864)] // 64 MiB default - pub object_size: usize, + pub object_size: u64, /// Upload scenario / strategy to benchmark. #[arg(long, value_enum, default_value_t = UploadScenario::All)] diff --git a/src/storage/benchmarks/write_object/src/metrics.rs b/src/storage/benchmarks/write_object/src/metrics.rs index 7ce09b9846..91255df172 100644 --- a/src/storage/benchmarks/write_object/src/metrics.rs +++ b/src/storage/benchmarks/write_object/src/metrics.rs @@ -30,7 +30,7 @@ pub struct Metrics { } /// Computes statistical metrics (mean, p50, p90, p99, throughput) from latencies. -pub fn compute_metrics(latencies: &[Duration], object_size_bytes: usize) -> Option { +pub fn compute_metrics(latencies: &[Duration], object_size_bytes: u64) -> Option { if latencies.is_empty() { return None; } diff --git a/src/storage/benchmarks/write_object/src/reporter.rs b/src/storage/benchmarks/write_object/src/reporter.rs index 6a96f3e3c7..783a995773 100644 --- a/src/storage/benchmarks/write_object/src/reporter.rs +++ b/src/storage/benchmarks/write_object/src/reporter.rs @@ -25,7 +25,7 @@ pub struct BenchmarkReport<'a> { /// Scenario name (e.g. Option A, Option B, Option C). pub scenario: &'a str, /// Object size in bytes. - pub object_size: usize, + pub object_size: u64, /// Number of measured iterations. pub measured_iterations: usize, /// Whether cold-cache eviction was enabled before each iteration. diff --git a/src/storage/benchmarks/write_object/src/scenarios.rs b/src/storage/benchmarks/write_object/src/scenarios.rs index 3acfde0c04..44531274ad 100644 --- a/src/storage/benchmarks/write_object/src/scenarios.rs +++ b/src/storage/benchmarks/write_object/src/scenarios.rs @@ -34,7 +34,7 @@ pub async fn scenario_option_a( bucket_name: &str, object_name: &str, file_path: &Path, - object_size: usize, + object_size: u64, ) -> anyhow::Result { let file = File::open(file_path).await?; let start_time = Instant::now(); @@ -46,7 +46,7 @@ pub async fn scenario_option_a( let total_elapsed = start_time.elapsed(); - if object.size as usize != object_size { + if object.size as u64 != object_size { anyhow::bail!( "persisted size mismatch: expected {}, got {}", object_size, @@ -68,7 +68,7 @@ pub async fn scenario_option_b( bucket_name: &str, object_name: &str, file_path: &Path, - object_size: usize, + object_size: u64, ) -> anyhow::Result { let file = File::open(file_path).await?; let total_start = Instant::now(); @@ -83,7 +83,7 @@ pub async fn scenario_option_b( let object = write_builder.send_unbuffered().await?; let total_elapsed = total_start.elapsed(); - if object.size as usize != object_size { + if object.size as u64 != object_size { anyhow::bail!( "persisted size mismatch: expected {}, got {}", object_size, @@ -105,7 +105,7 @@ pub async fn scenario_option_c( bucket_name: &str, object_name: &str, file_path: &Path, - object_size: usize, + object_size: u64, ) -> anyhow::Result { let file = File::open(file_path).await?; let start_time = Instant::now(); @@ -117,7 +117,7 @@ pub async fn scenario_option_c( let total_elapsed = start_time.elapsed(); - if object.size as usize != object_size { + if object.size as u64 != object_size { anyhow::bail!( "persisted size mismatch: expected {}, got {}", object_size, diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index f28857225c..7df467053a 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -76,7 +76,7 @@ pub async fn perform_global_warmup( /// The file is created in `temp_dir` on physical SSD storage. /// Returns the path to the temporary file and the NamedTempFile handle. pub async fn create_temp_test_file( - size_bytes: usize, + size_bytes: u64, temp_dir: &str, ) -> anyhow::Result<(NamedTempFile, PathBuf)> { // Ensure parent directory exists @@ -88,15 +88,15 @@ pub async fn create_temp_test_file( // Use a 1 MiB chunk of pseudo-random data written repeatedly to disk let chunk_size = 1024 * 1024; // 1 MiB let mut rng = StdRng::seed_from_u64(42); - let mut pattern = vec![0u8; chunk_size.min(size_bytes)]; + let mut pattern = vec![0u8; (chunk_size as u64).min(size_bytes) as usize]; rng.fill(&mut pattern[..]); let mut async_file = tokio::fs::File::create(&path).await?; let mut remaining = size_bytes; while remaining > 0 { - let to_write = remaining.min(pattern.len()); + let to_write = (remaining.min(pattern.len() as u64)) as usize; async_file.write_all(&pattern[..to_write]).await?; - remaining -= to_write; + remaining -= to_write as u64; } async_file.flush().await?; From 75f78e023ba766789cf7d559c0b6fa449756a69c Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:03:27 +0000 Subject: [PATCH 14/15] fix(storage): gate posix_fadvise with target_os = linux and provide non-Linux fallback --- src/storage/benchmarks/write_object/src/source.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index 7df467053a..d624ae7a3f 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -16,15 +16,15 @@ use bytes::Bytes; use google_cloud_storage::client::{Storage, StorageControl}; use rand::rngs::StdRng; use rand::{RngExt, SeedableRng}; +#[cfg(target_os = "linux")] use std::fs::File; +#[cfg(target_os = "linux")] +use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use tokio::io::AsyncWriteExt; use uuid::Uuid; -#[cfg(unix)] -use std::os::unix::io::AsRawFd; - const WARMUP_PAYLOAD_SIZE: usize = 512 * 1024; // 512 KiB /// Performs a one-time global warmup by uploading and deleting a 512 KiB payload. @@ -105,7 +105,7 @@ pub async fn create_temp_test_file( /// Evicts the given file's data from the OS page cache (RAM) to simulate a cold physical disk read. pub fn drop_file_from_page_cache(path: &Path) -> std::io::Result<()> { - #[cfg(unix)] + #[cfg(target_os = "linux")] { let std_file = File::open(path)?; let fd = std_file.as_raw_fd(); @@ -119,5 +119,9 @@ pub fn drop_file_from_page_cache(path: &Path) -> std::io::Result<()> { return Err(std::io::Error::from_raw_os_error(ret)); } } + #[cfg(not(target_os = "linux"))] + { + let _ = path; + } Ok(()) } From 4bf7daad7c3f448c12464cacd17024ffc7acbef6 Mon Sep 17 00:00:00 2001 From: Olivia Xiaoni Lai <5503815+xlai20@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:25:07 +0000 Subject: [PATCH 15/15] fix(storage): check fdatasync return code and clarify pattern docs in write_object benchmark --- src/storage/benchmarks/write_object/src/source.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/storage/benchmarks/write_object/src/source.rs b/src/storage/benchmarks/write_object/src/source.rs index d624ae7a3f..fee1921e36 100644 --- a/src/storage/benchmarks/write_object/src/source.rs +++ b/src/storage/benchmarks/write_object/src/source.rs @@ -73,7 +73,10 @@ pub async fn perform_global_warmup( } /// Creates a temporary file of the given size populated with pseudo-random bytes. -/// The file is created in `temp_dir` on physical SSD storage. +/// The file is created in `temp_dir` on physical storage. +/// +/// To minimize setup overhead and memory consumption, a 1 MiB pseudo-random block +/// is generated once and written repeatedly to disk until the target size is reached. /// Returns the path to the temporary file and the NamedTempFile handle. pub async fn create_temp_test_file( size_bytes: u64, @@ -109,9 +112,12 @@ pub fn drop_file_from_page_cache(path: &Path) -> std::io::Result<()> { { let std_file = File::open(path)?; let fd = std_file.as_raw_fd(); - // Sync dirty pages to disk first. + // Sync dirty pages to disk first. Dirty pages cannot be discarded by posix_fadvise. // SAFETY: `fdatasync` is called with a valid open file descriptor owned by `std_file`. - let _ = unsafe { libc::fdatasync(fd) }; + let ret = unsafe { libc::fdatasync(fd) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } // Tell the OS kernel to discard cached pages for the entire file range. // SAFETY: `posix_fadvise` is called with a valid open file descriptor owned by `std_file` and valid offset/len arguments. let ret = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED) };