Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions packages/google-cloud-storage/cloudbuild/benchmarks-cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
shradhakatyal marked this conversation as resolved.
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"
Original file line number Diff line number Diff line change
@@ -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)
152 changes: 152 additions & 0 deletions packages/google-cloud-storage/cloudbuild/run_benchmark_tests.sh
Original file line number Diff line number Diff line change
@@ -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}')
"
Comment thread
shradhakatyal marked this conversation as resolved.
Comment on lines +82 to +130

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole thing, editing on-the-fly is brittle.

one whitespace mismatch can break the code. ( and this usually happens due to mismatch in linters/formatters)

consider using env variables. config.yaml is actually parsed by config.py , where you can override by env variables.


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