diff --git a/packages/google-cloud-storage/cloudbuild/benchmarks-cloudbuild.yaml b/packages/google-cloud-storage/cloudbuild/benchmarks-cloudbuild.yaml new file mode 100644 index 000000000000..2085762cbdc5 --- /dev/null +++ b/packages/google-cloud-storage/cloudbuild/benchmarks-cloudbuild.yaml @@ -0,0 +1,140 @@ +substitutions: + _ZONE: "us-west4-a" + _VM_NAME: "gcs-benchmark-runner-us-west4-a" + _ULIMIT: "65536" + _PROCESSES: "48" + _COROS: "1" + _FILE_SIZE_MIB: "10240" + _CHUNK_SIZE_KIB: "102400" + _ROUNDS: "3" + _BUCKET_TYPE: "zonal" + _ZONAL_BUCKET: "gcs-read-bench-zb-us-west4-a" + _REGIONAL_BUCKET: "gcs-read-bench-rb-us-west4" + _PR_NUMBER: "" + _REPO: "googleapis/google-cloud-python" + +steps: + # Step 0: Generate a temporary SSH key for this build run and register with OS Login + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "generate-ssh-key" + entrypoint: "bash" + args: + - "-c" + - | + mkdir -p /workspace/.ssh + ssh-keygen -t rsa -f /workspace/.ssh/google_compute_engine -N '' -C gcb + cat /workspace/.ssh/google_compute_engine.pub > /workspace/gcb_ssh_key.pub + gcloud compute os-login ssh-keys add \ + --key-file=/workspace/.ssh/google_compute_engine.pub \ + --ttl=1h + waitFor: ["-"] + + # Step 1: Package google-cloud-storage directory for direct transfer to VM + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "package-code" + entrypoint: "bash" + args: + - "-c" + - | + tar --exclude='.nox' --exclude='venv_*' --exclude='.pytest_cache' --exclude='__pycache__' --exclude='.git' \ + -czf /workspace/google-cloud-storage.tar.gz -C /workspace/packages google-cloud-storage + waitFor: ["-"] + + # Step 2: Start the standing high-bandwidth VM + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "start-vm" + entrypoint: "bash" + args: + - "-c" + - | + echo "Starting standing VM ${_VM_NAME} in zone ${_ZONE}..." + gcloud compute instances start "${_VM_NAME}" --zone="${_ZONE}" + waitFor: ["-"] + + # Step 3: Run the benchmark directly on the VM via private internal IP SSH, fetch results, and stop the VM + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "run-benchmark-on-vm" + entrypoint: "bash" + args: + - "-c" + - | + set -e + cleanup() { + set +e + echo "Stopping VM ${_VM_NAME}..." + gcloud compute instances stop "${_VM_NAME}" --zone="${_ZONE}" --quiet + } + trap cleanup EXIT + + echo "Waiting for VM ${_VM_NAME} to become accessible over internal SSH..." + SSH_READY=0 + for i in $(seq 1 20); do + if gcloud compute ssh "${_VM_NAME}" --zone="${_ZONE}" --internal-ip --ssh-key-file=/workspace/.ssh/google_compute_engine --command="echo VM is ready" 2>/dev/null; then + echo "VM internal SSH connection established successfully." + SSH_READY=1 + break + fi + echo "Waiting for VM internal SSH availability... (attempt $$i/20)" + sleep 10 + done + + if [ $$SSH_READY -ne 1 ]; then + echo "ERROR: VM internal SSH connection could not be established." >&2 + exit 1 + fi + + echo "Copying package archive and runner scripts to VM over internal IP..." + gcloud compute scp /workspace/google-cloud-storage.tar.gz \ + packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh \ + packages/google-cloud-storage/cloudbuild/display_benchmark_results.py \ + "${_VM_NAME}":~ --zone="${_ZONE}" --internal-ip --ssh-key-file=/workspace/.ssh/google_compute_engine + + echo "Executing benchmark test suite directly on VM via SSH..." + set +e + gcloud compute ssh "${_VM_NAME}" --zone="${_ZONE}" --internal-ip --ssh-key-file=/workspace/.ssh/google_compute_engine \ + --command="tar -xzf google-cloud-storage.tar.gz && cd google-cloud-storage && ulimit -n ${_ULIMIT}; PROCESSES=${_PROCESSES} COROS=${_COROS} FILE_SIZE_MIB=${_FILE_SIZE_MIB} CHUNK_SIZE_KIB=${_CHUNK_SIZE_KIB} ROUNDS=${_ROUNDS} BUCKET_TYPE=${_BUCKET_TYPE} ZONAL_BUCKET=${_ZONAL_BUCKET} REGIONAL_BUCKET=${_REGIONAL_BUCKET} bash cloudbuild/run_benchmark_tests.sh" + TEST_EXIT_CODE=$? + set -e + + # Copy JSON report back from VM to Cloud Build workspace + mkdir -p /workspace/report + echo "Fetching benchmark result JSON from VM..." + gcloud compute scp "${_VM_NAME}":~/bench_result.json /workspace/report/bench_result.json \ + --zone="${_ZONE}" --internal-ip --ssh-key-file=/workspace/.ssh/google_compute_engine 2>/dev/null || true + + exit $$TEST_EXIT_CODE + waitFor: + - "start-vm" + - "generate-ssh-key" + - "package-code" + + # Step 4: Display benchmark performance summary table in Cloud Build logs + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "display-benchmark-results" + entrypoint: "python3" + args: + - "packages/google-cloud-storage/cloudbuild/display_benchmark_results.py" + - "/workspace/report/bench_result.json" + waitFor: + - "run-benchmark-on-vm" + + # Step 5: Clean up SSH key from OS Login profile + - name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + id: "cleanup-ssh-key" + entrypoint: "bash" + args: + - "-c" + - | + echo "Removing temporary build SSH key from OS Login profile..." + gcloud compute os-login ssh-keys remove \ + --key-file=/workspace/gcb_ssh_key.pub || true + waitFor: + - "display-benchmark-results" + +timeout: "3600s" + +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true + pool: + name: "projects/${PROJECT_ID}/locations/us-west4/workerPools/benchmark-worker-pool" diff --git a/packages/google-cloud-storage/cloudbuild/display_benchmark_results.py b/packages/google-cloud-storage/cloudbuild/display_benchmark_results.py new file mode 100644 index 000000000000..031dc1ce2fa9 --- /dev/null +++ b/packages/google-cloud-storage/cloudbuild/display_benchmark_results.py @@ -0,0 +1,69 @@ +# 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 +# +# http://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. + +"""Helper script to format and display GCS benchmark performance results.""" + +import json +import os +import sys + + +def display_results(result_path: str) -> None: + """Reads benchmark JSON result and prints a formatted summary table.""" + if not os.path.exists(result_path): + print(f"ERROR: Benchmark result file not found at {result_path}", file=sys.stderr) + sys.exit(1) + + with open(result_path) as f: + data = json.load(f) + + if not isinstance(data, dict): + print("ERROR: Invalid JSON structure in benchmark result file.", file=sys.stderr) + sys.exit(1) + + benchmarks = data.get("benchmarks", []) + if not isinstance(benchmarks, list) or not benchmarks: + print("No benchmarks found in result file.") + sys.exit(0) + + print("\n" + "=" * 88) + print(" GCS DIRECTPATH READ BENCHMARK PERFORMANCE RESULTS") + print("=" * 88) + header = f"| {'Workload Pattern':<36} | {'Avg Throughput':<17} | {'Network Bandwidth':<22} | {'CPU Usage':<9} |" + print(header) + print("|" + "-" * 38 + "|" + "-" * 19 + "|" + "-" * 24 + "|" + "-" * 11 + "|") + for b in benchmarks: + if not isinstance(b, dict): + continue + name = b.get("name", "").replace("test_downloads_multi_proc_multi_coro[", "").replace("]", "") + extra = b.get("extra_info", {}) + if not isinstance(extra, dict): + extra = {} + avg_mib = extra.get("avg_throughput_mib_s", "N/A") + net_mb = extra.get("net_throughput_mb_s") + if net_mb: + try: + net_str = f"{float(net_mb):,.1f} MB/s ({float(net_mb)*0.008:.1f} Gbps)" + except Exception: + net_str = str(net_mb) + else: + net_str = "N/A" + cpu = extra.get("cpu_max_global", "N/A") + print(f"| {name:<36} | {str(avg_mib) + ' MiB/s':<17} | {net_str:<22} | {str(cpu):<9} |") + print("=" * 88 + "\n") + + +if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "/workspace/report/bench_result.json" + display_results(path) diff --git a/packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh b/packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh new file mode 100755 index 000000000000..0d1be176f0cd --- /dev/null +++ b/packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# ============================================================================== +# Automated Google Cloud Storage Read Microbenchmark Runner +# Intended for GitHub CI/CD & GCE High-Bandwidth Tier-1 VMs (C4/N2/C3 series) +# Location: packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh +# ============================================================================== + +set -eo pipefail + +# Configurable defaults +PROCESSES="${PROCESSES:-48}" +COROS="${COROS:-1}" +FILE_SIZE_MIB="${FILE_SIZE_MIB:-10240}" # 10 GiB files by default +CHUNK_SIZE_KIB="${CHUNK_SIZE_KIB:-102400}" # ~100 MiB read chunks by default +ROUNDS="${ROUNDS:-3}" # Run benchmark 3 rounds by default +BUCKET_TYPE="${BUCKET_TYPE:-zonal}" # "zonal" uses BidiReadObject gRPC DirectPath, "regional" uses REST/gRPC standard +ZONAL_BUCKET="${ZONAL_BUCKET:-${DEFAULT_RAPID_ZONAL_BUCKET:-gcs-read-bench-zb-us-west4-a}}" +REGIONAL_BUCKET="${REGIONAL_BUCKET:-${DEFAULT_STANDARD_BUCKET:-gcs-read-bench-rb-us-west4}}" +if [ -n "${TARGET_BUCKET:-}" ]; then + if [ "${BUCKET_TYPE}" = "regional" ]; then + REGIONAL_BUCKET="${TARGET_BUCKET}" + else + ZONAL_BUCKET="${TARGET_BUCKET}" + fi +fi +OUTPUT_JSON_PATH="${OUTPUT_JSON_PATH:-${OUT_JSON:-${HOME:-/tmp}/bench_result.json}}" +UPLOAD_GCS_PREFIX="${UPLOAD_GCS_PREFIX:-}" + +echo "========================================================================" +echo " GCS Read Microbenchmark Runner (gRPC BidiReadObject / REST)" +echo " Processes: ${PROCESSES}" +echo " Coroutines/proc: ${COROS}" +echo " File Size: ${FILE_SIZE_MIB} MiB" +echo " Chunk Size: ${CHUNK_SIZE_KIB} KiB" +echo " Rounds: ${ROUNDS}" +echo " Bucket Type: ${BUCKET_TYPE}" +echo " Zonal Bucket: gs://${ZONAL_BUCKET}" +echo " Regional Bucket: gs://${REGIONAL_BUCKET}" +echo " Output JSON Path: ${OUTPUT_JSON_PATH}" +echo " Upload GCS Path: ${UPLOAD_GCS_PREFIX:-None}" +echo "========================================================================" + +# Ensure HOME is exported for gRPC / ALTS Application Default Credentials +export HOME="${HOME:-/root}" +export DEFAULT_RAPID_ZONAL_BUCKET="${ZONAL_BUCKET}" +export DEFAULT_STANDARD_BUCKET="${REGIONAL_BUCKET}" +export USE_PRESEEDED_BENCHMARK_OBJECTS="1" + +# Determine script directory and repository root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "${REPO_ROOT}/packages/google-cloud-storage" 2>/dev/null || cd "$(pwd)" + +echo "--- 1. Setting up Python environment ---" +# Ensure python3-pip and python3-venv are present on the VM +if ! command -v pip3 &>/dev/null || ! python3 -c "import venv" 2>/dev/null; then + echo "Installing python3-pip and python3-venv on VM..." + sudo apt-get update && sudo apt-get install -y python3-pip python3-venv +fi + +# Ensure persistent virtual environment exists and is activated +BENCH_VENV="${HOME}/bench_env" +if [ ! -d "${BENCH_VENV}" ]; then + echo "Creating virtual environment at ${BENCH_VENV}..." + python3 -m venv "${BENCH_VENV}" +fi +source "${BENCH_VENV}/bin/activate" + +# Check and install all dependencies into virtual environment +if ! python3 -c "import pytest, psutil, yaml, google.cloud.storage" 2>/dev/null; then + echo "Installing dependencies into virtual environment..." + pip install --upgrade pip + pip install -e ".[grpc,testing]" +fi + +CONFIG_PATH="tests/perf/microbenchmarks/time_based/reads/config.yaml" +if [ ! -f "${CONFIG_PATH}" ]; then + echo "ERROR: Could not find ${CONFIG_PATH}. Please run from google-cloud-storage root." + exit 1 +fi + +echo "--- 2. Updating ${CONFIG_PATH} parameters (rounds=${ROUNDS}) ---" +python3 -c " +import yaml +path = '${CONFIG_PATH}' +with open(path) as f: + d = yaml.safe_load(f) +if isinstance(d, dict): + defaults = d.get('defaults') + if isinstance(defaults, dict): + defaults['DEFAULT_RAPID_ZONAL_BUCKET'] = '${ZONAL_BUCKET}' + defaults['DEFAULT_STANDARD_BUCKET'] = '${REGIONAL_BUCKET}' + common = d.get('common') + if isinstance(common, dict): + common['file_sizes_mib'] = [${FILE_SIZE_MIB}] + common['chunk_sizes_kib'] = [${CHUNK_SIZE_KIB}] + b_types = [b.strip() for b in '${BUCKET_TYPE}'.split(',') if b.strip()] + common['bucket_types'] = b_types if b_types else ['zonal'] + common['rounds'] = int('${ROUNDS}') + workloads = d.get('workload') + if isinstance(workloads, list): + for w in workloads: + if isinstance(w, dict): + w['processes'] = [${PROCESSES}] + w['coros'] = [${COROS}] +with open(path, 'w') as f: + yaml.dump(d, f) +" + +# Patch config.py so 1-to-1 process-to-file indexing prevents 404 on multi-coroutine runs +sed -i 's/num_files = num_processes \* num_coros/num_files = num_processes/g' tests/perf/microbenchmarks/time_based/reads/config.py || true +sed -i 's/num_files = num_processes \* num_coros/num_files = num_processes/g' tests/perf/microbenchmarks/reads/config.py || true + +# Patch conftest.py at runtime on VM to use pre-seeded test objects and bypass 480GB re-upload +python3 -c " +path = 'tests/perf/microbenchmarks/conftest.py' +try: + with open(path) as f: + s = f.read() + if '_create_files(' in s: + target = 'files_names = _create_files(\n params.num_files,\n params.bucket_name,\n params.bucket_type,\n params.file_size_bytes,\n)' + replacement = 'files_names = [f\"fio-go_storage_fio.0.{i}\" for i in range(params.num_files)]' + if target not in s: + raise ValueError('Exact _create_files call signature not found in conftest.py') + s = s.replace(target, replacement) + with open(path, 'w') as f: + f.write(s) +except Exception as e: + print(f'Warning patching conftest.py: {e}') +" + +echo "--- 3. Executing pytest benchmark suite (${ROUNDS} rounds) ---" +rm -f "${OUTPUT_JSON_PATH}" 2>/dev/null || true +python3 -m pytest --benchmark-json="${OUTPUT_JSON_PATH}" \ + -rA \ + tests/perf/microbenchmarks/time_based/reads/test_reads.py + +if [ -s "${OUTPUT_JSON_PATH}" ]; then + DISPLAY_SCRIPT="${SCRIPT_DIR}/display_benchmark_results.py" + if [ ! -f "${DISPLAY_SCRIPT}" ]; then + DISPLAY_SCRIPT="cloudbuild/display_benchmark_results.py" + fi + python3 "${DISPLAY_SCRIPT}" "${OUTPUT_JSON_PATH}" + + if [ -n "${UPLOAD_GCS_PREFIX}" ]; then + GCS_DEST="${UPLOAD_GCS_PREFIX}/test_result_$(hostname)_$(date +%s).json" + echo "Uploading JSON report to ${GCS_DEST}..." + gcloud storage cp "${OUTPUT_JSON_PATH}" "${GCS_DEST}" + fi +fi + +echo "--- Benchmark Run Complete ---"