Prototype hybrid lazy imports#17572
Conversation
- Add fallback to handle single-iteration quantile calculation crashes - Normalize .pyc file paths during line counting and log exceptions - Expose worker process stderr to facilitate debugging CalledProcessError - Fix absolute paths in documentation.md to use relative directory paths
…g, and encodings - Use importlib.util.source_from_cache for robust .pyc resolution - Move importlib.util and logging imports to module level - Refine json.loads to parse only the last line of stdout - Specify UTF-8 encoding when opening files for writing
- Extract worker logic to _run_worker_and_parse for robust JSON parsing - Add early validation for iterations parameter in run_master - Add non-zero exit code check and warning for run_trace failures
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Execute cProfile in a fresh subprocess to ensure cold-start accuracy - Execute tracemalloc in a fresh subprocess to capture all memory allocations - Clean up master process exception handling for missing taskset vs crashed workers
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Use json.dumps to safely escape target_module string in all subprocess commands (mprofile, cprofile, trace)
- Replace detailed implementation architecture and alternative solutions with a concise high-level usage and expected output format per PR review feedback
- Read /proc/self/statm inside worker before and after import - Exposes pure C-extension memory allocations (e.g. grpc/protobuf) missed by Python's internal tracemalloc - Requires zero dependencies, bypassing the need for psutil
- Recursively wipes __pycache__ and .pyc files via pathlib prior to iteration loops. - Employs Python's -B flag on worker subprocesses to prevent re-caching during the benchmark, enforcing raw .py parsing for every execution.
- Replaces --clear-pycache with an opt-out --keep-pycache flag - The profiler now automatically sweeps and enforces disk-level cold starts without requiring explicit flags from the user.
Address PR review comment by moving setup tracking (rss_before, modules_before) outside of the time.perf_counter() window to prevent artificial latency inflation.
Addresses PR feedback to avoid broad exception catching when parsing physical python files for line counts.
Addresses PR review by explicitly documenting the expected PEP 3147/488 error path for irregular .pyc files.
…ssing modules Addresses PR review by replacing null-checks with a cleaner try/except block that assumes normal module behavior and explicitly logs when a C-extension or built-in is skipped due to a missing __file__ attribute.
Addresses PR review by replacing the 'none' string check with a typed NO_CPU_PINNING = -1 constant for cleaner argument parsing and state tracking.
…askset Addresses PR review by removing the fallback unpinned execution logic when taskset is not installed. The script now fails immediately with a clear error message, preventing code duplication.
Addresses PR review by collapsing the repeated statistics.quantiles logic into a reusable _calculate_percentiles helper method.
…terations Addresses PR review by validating that 'loaded_modules' and 'loaded_lines' remain deterministic across cold-start iterations, warning the user if non-deterministic import paths are triggered.
… tracemalloc profiling Addresses PR review by replacing the hard-to-maintain dynamic code string inside run_mprofile with a properly structured multiprocessing 'spawn' context.
There was a problem hiding this comment.
Code Review
This pull request introduces a Python SDK import profiling tool, consisting of a documentation guide, a benchmarking and tracing script (profiler.py), and a utility script (rewrite_init.py) to automate lazy-loading in package init.py files. The review feedback highlights several opportunities to make the scripts more robust, such as handling comments during import parsing, supporting both lists and tuples for all matching, resolving file paths dynamically, adding a dir method to the generated lazy-loaded modules for better IDE support, and removing a redundant local import of tracemalloc.
| while i < len(lines): | ||
| line = lines[i] | ||
| if line.startswith("from .") and " import " in line: | ||
| parts = line.split(" import ") | ||
| mod = parts[0].replace("from ", "").strip() | ||
| names_part = parts[1].strip() | ||
| if "(" in names_part and ")" not in names_part: | ||
| # multi-line import | ||
| import_names_str = names_part.replace("(", "") | ||
| i += 1 | ||
| while ")" not in lines[i]: | ||
| import_names_str += lines[i] | ||
| i += 1 | ||
| import_names_str += lines[i].replace(")", "") | ||
| else: | ||
| import_names_str = names_part.replace("(", "").replace(")", "") | ||
|
|
||
| names = [n.strip() for n in import_names_str.split(",") if n.strip()] | ||
| parsed_imports.append((mod, names)) | ||
| i += 1 |
There was a problem hiding this comment.
The manual import parsing logic is fragile and will fail or produce invalid syntax if there are comments (such as # type: ignore or other annotations) on the import lines. Stripping comments before parsing the imported names makes the script much more robust.
| while i < len(lines): | |
| line = lines[i] | |
| if line.startswith("from .") and " import " in line: | |
| parts = line.split(" import ") | |
| mod = parts[0].replace("from ", "").strip() | |
| names_part = parts[1].strip() | |
| if "(" in names_part and ")" not in names_part: | |
| # multi-line import | |
| import_names_str = names_part.replace("(", "") | |
| i += 1 | |
| while ")" not in lines[i]: | |
| import_names_str += lines[i] | |
| i += 1 | |
| import_names_str += lines[i].replace(")", "") | |
| else: | |
| import_names_str = names_part.replace("(", "").replace(")", "") | |
| names = [n.strip() for n in import_names_str.split(",") if n.strip()] | |
| parsed_imports.append((mod, names)) | |
| i += 1 | |
| while i < len(lines): | |
| line = lines[i] | |
| if line.startswith("from .") and " import " in line: | |
| parts = line.split(" import ") | |
| mod = parts[0].replace("from ", "").strip() | |
| names_part = parts[1].split("#")[0].strip() | |
| if "(" in names_part and ")" not in names_part: | |
| # multi-line import | |
| import_names_str = names_part.replace("(", "") | |
| i += 1 | |
| while ")" not in lines[i]: | |
| import_names_str += lines[i].split("#")[0] | |
| i += 1 | |
| import_names_str += lines[i].split("#")[0].replace(")", "") | |
| else: | |
| import_names_str = names_part.replace("(", "").replace(")", "") | |
| names = [n.strip() for n in import_names_str.split(",") if n.strip()] | |
| parsed_imports.append((mod, names)) | |
| i += 1 |
| def __getattr__(name): | ||
| if name in _LAZY_IMPORTS: | ||
| module = importlib.import_module(_LAZY_IMPORTS[name], package=__name__) | ||
| attr = getattr(module, name) | ||
| globals()[name] = attr | ||
| return attr | ||
| raise AttributeError(f"module {{__name__!r}} has no attribute {{name!r}}") |
There was a problem hiding this comment.
The generated __init__.py defines __getattr__ but is missing a corresponding __dir__ function. Without __dir__, interactive environments (like IPython, Jupyter notebooks, or standard REPLs) and IDEs will not be able to auto-complete or inspect the lazy-loaded attributes until they are explicitly accessed.
def __getattr__(name):
if name in _LAZY_IMPORTS:
module = importlib.import_module(_LAZY_IMPORTS[name], package=__name__)
attr = getattr(module, name)
globals()[name] = attr
return attr
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__():
return sorted(set(globals().keys()) | set(_LAZY_IMPORTS.keys()))| import re | ||
| import os | ||
|
|
||
| INIT_PATH = "packages/google-cloud-compute/google/cloud/compute_v1/__init__.py" |
There was a problem hiding this comment.
The relative path INIT_PATH is hardcoded, which causes the script to fail with a FileNotFoundError if executed from any directory other than the repository root. Resolving the path relative to the script's location makes it much more robust.
| INIT_PATH = "packages/google-cloud-compute/google/cloud/compute_v1/__init__.py" | |
| import pathlib | |
| INIT_PATH = pathlib.Path(__file__).resolve().parents[2] / "packages/google-cloud-compute/google/cloud/compute_v1/__init__.py" |
| all_match = re.search(r'__all__\s*=\s*\((.*?)\)', content, re.DOTALL) | ||
| if not all_match: | ||
| print("Could not find __all__") | ||
| return |
There was a problem hiding this comment.
The regex pattern for __all__ only matches tuples (...). Some packages in the monorepo define __all__ as a list [...]. Supporting both formats makes the script more robust and reusable.
| all_match = re.search(r'__all__\s*=\s*\((.*?)\)', content, re.DOTALL) | |
| if not all_match: | |
| print("Could not find __all__") | |
| return | |
| all_match = re.search(r'__all__\s*=\s*[\(\[](.*?)[\)\]]', content, re.DOTALL) | |
| if not all_match: | |
| print("Could not find __all__") | |
| return |
| def _mprofile_worker(target_module): | ||
| import tracemalloc | ||
| tracemalloc.start() |
There was a problem hiding this comment.
The tracemalloc module is already imported at the module level (line 6). Importing it again inside _mprofile_worker is redundant and violates the general rule against local imports of already module-level imported modules.
| def _mprofile_worker(target_module): | |
| import tracemalloc | |
| tracemalloc.start() | |
| def _mprofile_worker(target_module): | |
| tracemalloc.start() |
References
- Do not import modules or classes inside functions if they are already imported at the module level.
Overview
This PR resolves the severe initialization bottlenecks (~10s-13s) experienced by customers in serverless environments (Cloud Run/Functions) caused by eager loading cascades across generated service clients and protobuf types.
To deliver immediate latency relief to all current users while bridging the gap to native language performance, this PR adopts a Hybrid Lazy Loading Strategy (PEP 0810 + PEP 562).
Implementation Architecture
We utilize a branched architecture in the generated
__init__.pyfiles to provide optimal performance per environment without sacrificing IDE autocompletion or static type checking:__lazy_modules__set containing fully-qualified module paths. For users on Python 3.15+, the interpreter natively intercepts these imports and treats them as fast C-level lazy proxies.if typing.TYPE_CHECKING:guard so that IDEs (IntelliSense) and static analyzers (MyPy) maintain zero-friction support.lazy_imports.attach_module()helper fromgoogle-api-coreto dynamically load clients and types on-demand.Example Structure:
Related Links:
Design Doc: go/sdk-performance-design
Towards googleapis/python-aiplatform#4749