Skip to content

Conversation

@chandra-siri
Copy link
Collaborator

feat: add time based benchmarks

@product-auto-label product-auto-label bot added size: l Pull request size is large. api: storage Issues related to the googleapis/python-storage API. labels Feb 10, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @chandra-siri, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request establishes a foundational framework for time-based performance microbenchmarks within the project. The primary goal is to enable more accurate and continuous measurement of Google Cloud Storage read throughput under various conditions, moving beyond operation-count-based benchmarks to duration-based evaluations. This enhancement will provide deeper insights into the system's sustained performance characteristics.

Highlights

  • New Time-Based Benchmarking Framework: Introduced a new set of microbenchmarks designed to measure performance over a specified duration, including a warmup period, for Google Cloud Storage read operations.
  • Configurable Benchmark Parameters: Implemented a flexible configuration system using YAML to define various benchmark parameters such as bucket types, file sizes, chunk sizes, duration, and workload patterns (sequential and random reads).
  • Multi-Process and Asynchronous Read Operations: Developed a pytest-based test suite that leverages multiprocessing and asyncio to simulate concurrent read operations, allowing for realistic performance evaluation under various concurrency levels.
  • CPU Affinity for Worker Processes: Configured worker processes to set CPU affinity, pinning them to specific cores to potentially reduce context switching overhead and improve benchmark consistency.
Changelog
  • tests/perf/microbenchmarks/time_based/conftest.py
    • Added a pytest fixture to provide workload parameters and dynamically generated filenames for time-based benchmarks.
  • tests/perf/microbenchmarks/time_based/reads/config.py
    • Added a module to parse YAML configuration and generate a comprehensive list of benchmark parameters for time-based read operations.
  • tests/perf/microbenchmarks/time_based/reads/config.yaml
    • Added a YAML configuration file defining common and workload-specific parameters for time-based sequential and random read benchmarks.
  • tests/perf/microbenchmarks/time_based/reads/parameters.py
    • Added a dataclass to encapsulate parameters specific to time-based read benchmarks, including pattern, duration, and warmup duration.
  • tests/perf/microbenchmarks/time_based/reads/test_reads.py
    • Added the main pytest file implementing time-based microbenchmarks for Google Cloud Storage read operations, featuring multi-process, asynchronous execution, and throughput calculation.
Activity
  • No human activity (comments, reviews, etc.) has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces time-based benchmarks for read operations. The changes include new configuration files (yaml and .py), a conftest.py for test setup, and the benchmark test logic itself. The implementation uses multiprocessing to run downloads in parallel.

My review focuses on improving the maintainability, portability, and performance of the benchmark code. Key suggestions include removing commented-out code, making benchmark names more descriptive, avoiding hardcoded values for CPU affinity to improve portability, and optimizing the download loop to reduce memory allocations and redundant calls, which is crucial for accurate performance measurement.


def _worker_init(bucket_type):
"""Initializes a persistent event loop and client for each worker process."""
os.sched_setaffinity(0, {i for i in range(20, 180)}) # Pin to cores 20-189
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The CPU affinity is hardcoded to cores 20-179. This makes the benchmark non-portable and likely to fail on machines with fewer than 180 cores. Consider making this dynamic by querying the available cores using os.sched_getaffinity(0) and selecting a subset. This will make the benchmark more robust and runnable on different environments.

Comment on lines 72 to 97
offset = 0
is_warming_up = True
start_time = time.monotonic()
warmup_end_time = start_time + params.warmup_duration
test_end_time = warmup_end_time + params.duration

while time.monotonic() < test_end_time:
current_time = time.monotonic()
if is_warming_up and current_time >= warmup_end_time:
is_warming_up = False
total_bytes_downloaded = 0 # Reset counter after warmup

if params.pattern == "rand":
offset = random.randint(0, params.file_size_bytes - params.chunk_size_bytes)

buffer = BytesIO()
await mrd.download_ranges([(offset, params.chunk_size_bytes, buffer)])

if not is_warming_up:
total_bytes_downloaded += params.chunk_size_bytes

if params.pattern == "seq":
offset += params.chunk_size_bytes
if offset + params.chunk_size_bytes > params.file_size_bytes:
offset = 0 # Reset offset if end of file is reached

Copy link
Contributor

Choose a reason for hiding this comment

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

high

This while loop has a couple of inefficiencies that could affect benchmark accuracy:

  1. A new BytesIO buffer is created on every iteration, which can cause significant memory allocation and garbage collection overhead. It's better to create it once outside the loop and reuse it.
  2. time.monotonic() is called twice per iteration. This can be reduced to one call.

The suggested change refactors the loop to address both issues.

    offset = 0
    is_warming_up = True
    start_time = time.monotonic()
    warmup_end_time = start_time + params.warmup_duration
    test_end_time = warmup_end_time + params.duration
    buffer = BytesIO()

    while (current_time := time.monotonic()) < test_end_time:
        if is_warming_up and current_time >= warmup_end_time:
            is_warming_up = False
            total_bytes_downloaded = 0  # Reset counter after warmup

        if params.pattern == "rand":
            offset = random.randint(0, params.file_size_bytes - params.chunk_size_bytes)

        buffer.seek(0)
        buffer.truncate(0)
        await mrd.download_ranges([(offset, params.chunk_size_bytes, buffer)])

        if not is_warming_up:
            total_bytes_downloaded += params.chunk_size_bytes

        if params.pattern == "seq":
            offset += params.chunk_size_bytes
            if offset + params.chunk_size_bytes > params.file_size_bytes:
                offset = 0  # Reset offset if end of file is reached

Comment on lines 23 to 74
# def _upload_worker(args):
# bucket_name, object_name, object_size = args
# storage_client = storage.Client()
# bucket = storage_client.bucket(bucket_name)
# blob = bucket.blob(object_name)

# try:
# blob.reload()
# if blob.size >= object_size:
# logging.info(f"Object {object_name} already exists and has the required size.")
# return object_name, object_size
# except Exception:
# pass

# logging.info(f"Creating object {object_name} of size {object_size} bytes.")
# # For large objects, it's better to upload in chunks.
# # Using urandom is slow, so for large objects, we will write the same chunk over and over.
# chunk_size = 100 * 1024 * 1024 # 100 MiB
# data_chunk = os.urandom(chunk_size)
# num_chunks = object_size // chunk_size
# remaining_bytes = object_size % chunk_size

# from io import BytesIO
# with BytesIO() as f:
# for _ in range(num_chunks):
# f.write(data_chunk)
# if remaining_bytes > 0:
# f.write(data_chunk[:remaining_bytes])

# f.seek(0)
# blob.upload_from_file(f, size=object_size)

# logging.info(f"Finished creating object {object_name}.")
# return object_name, object_size


# def _create_files(num_files, bucket_name, object_size):
# """
# Create/Upload objects for benchmarking and return a list of their names.
# """
# object_names = [f"{_OBJECT_NAME_PREFIX}_{i}" for i in range(num_files)]

# args_list = [
# (bucket_name, object_names[i], object_size) for i in range(num_files)
# ]

# # Don't use a pool to avoid contention writing the same objects.
# # The check for existence should make this fast on subsequent runs.
# results = [_upload_worker(arg) for arg in args_list]

# return [r[0] for r in results]

Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This file contains a large block of commented-out code. To improve readability and maintainability, this unused code should be removed. If it's intended for future use, consider moving it to a separate file or adding a TODO with an explanation.

num_files = num_processes * num_coros

# Create a descriptive name for the parameter set
name = f"{pattern}_{bucket_type}_{num_processes}p_{file_size_mib}MiB_{chunk_size_mib}MiB"
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The generated benchmark name does not include num_coros. While the current configuration only uses one coroutine, including this parameter in the name will make the benchmark results clearer and more robust if the number of coroutines is varied in the future.

Suggested change
name = f"{pattern}_{bucket_type}_{num_processes}p_{file_size_mib}MiB_{chunk_size_mib}MiB"
name = f"{pattern}_{bucket_type}_{num_processes}p_{num_coros}c_{file_size_mib}MiB_{chunk_size_mib}MiB"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: storage Issues related to the googleapis/python-storage API. size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant