Skip to content

Commit 113b3d5

Browse files
committed
address PR comments for import profiler CI
1 parent a6491b5 commit 113b3d5

7 files changed

Lines changed: 183 additions & 29 deletions

File tree

.github/workflows/import-profiler.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
python -m pip install nox
3232
- name: Run import profiler
3333
env:
34-
BUILD_TYPE: presubmit
34+
BUILD_TYPE: ${{ contains(github.event.pull_request.labels.*.name, 'import_profile:all_packages') && 'all' || 'presubmit' }}
3535
TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }}
3636
TEST_TYPE: import_profile
3737
PY_VERSION: "3.15"

ci/run_single_test.sh

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,27 +106,13 @@ case ${TEST_TYPE} in
106106
esac
107107
;;
108108
import_profile)
109-
python -m pip install .
110-
PKG_NAME=$(python setup.py --name)
111-
MODULE=$(python -c "
112-
import sys, importlib.util
113-
pkg = '${PKG_NAME}'
114-
candidates = [
115-
pkg.replace('-', '.'),
116-
'.'.join(pkg.split('-')[:-1]) + '_' + pkg.split('-')[-1] if '-' in pkg else pkg,
117-
pkg.replace('-', '_')
118-
]
119-
for mod in candidates:
120-
try:
121-
if importlib.util.find_spec(mod):
122-
print(mod)
123-
sys.exit(0)
124-
except Exception:
125-
pass
126-
print(candidates[0])
127-
")
128-
python ${PROJECT_ROOT}/scripts/import_profiler/profiler.py --module "${MODULE}" --iterations 10
129-
retval=$?
109+
if nox --list-sessions | grep -q "import_profile"; then
110+
nox -s import_profile
111+
retval=$?
112+
else
113+
echo "Skipping import_profile as it is not supported by this package yet."
114+
retval=0
115+
fi
130116
;;
131117
*)
132118
nox -s ${TEST_TYPE}

inject_import_profile.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
import glob
3+
import re
4+
5+
def patch_noxfile(path):
6+
try:
7+
with open(path, 'r', encoding='utf-8') as f:
8+
content = f.read()
9+
except UnicodeDecodeError:
10+
return False
11+
12+
if "import_profile" in content:
13+
return False
14+
15+
# 1. Add "import_profile" to nox.options.sessions
16+
# It looks like:
17+
# nox.options.sessions = [
18+
# "unit",
19+
# ...
20+
# "docs",
21+
# ]
22+
# We want to add it right before the closing bracket.
23+
if "nox.options.sessions =" in content:
24+
content = re.sub(
25+
r'(\n\s*"docs",\n)(\s*\])',
26+
r'\1 "import_profile",\n\2',
27+
content
28+
)
29+
# If "docs", is not the last one, maybe just add it before the closing bracket
30+
if "import_profile" not in content:
31+
content = re.sub(
32+
r'(\n)(\s*\]\n)',
33+
r'\1 "import_profile",\n\2',
34+
content,
35+
count=1,
36+
flags=re.DOTALL
37+
)
38+
# Make sure it only replaced the first matching closing bracket for sessions
39+
# wait, it's safer to just replace:
40+
# content = content.replace('"docs",\n', '"docs",\n "import_profile",\n')
41+
42+
# 2. Append the import_profile session at the end
43+
# We need to extract the module namespace. It's usually "google".
44+
# Let's extract python-gapic-namespace or python-gapic-name from somewhere,
45+
# or just hardcode "google" like the previous agent, which worked for CI.
46+
47+
session_text = """
48+
49+
@nox.session(python="3.15")
50+
def import_profile(session):
51+
\"\"\"Ensure import times remain below defined thresholds.\"\"\"
52+
profiler_script = (
53+
CURRENT_DIRECTORY.parent.parent / "scripts" / "import_profiler" / "profiler.py"
54+
)
55+
if not profiler_script.exists():
56+
session.skip("The import profiler script was not found.")
57+
58+
session.install(".")
59+
session.run(
60+
"python",
61+
str(profiler_script),
62+
"--package",
63+
"{pkg}",
64+
"--iterations",
65+
"10",
66+
"--fail-threshold",
67+
"5000",
68+
)
69+
"""
70+
71+
package_name = path.split('/')[1]
72+
session_text = session_text.replace("{pkg}", package_name)
73+
74+
content += session_text
75+
76+
with open(path, 'w', encoding='utf-8') as f:
77+
f.write(content)
78+
return True
79+
80+
if __name__ == "__main__":
81+
count = 0
82+
for path in glob.glob('packages/*/noxfile.py'):
83+
# don't modify the goldens here, we use bazel for them
84+
if "goldens" in path:
85+
continue
86+
if patch_noxfile(path):
87+
count += 1
88+
89+
print(f"Patched {count} noxfiles.")

packages/gapic-generator/gapic/templates/noxfile.py.j2

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ nox.options.sessions = [
8686
"lint_setup_py",
8787
"blacken",
8888
"docs",
89+
"import_profile",
8990
]
9091

9192
# Error if a python version is missing
@@ -641,4 +642,30 @@ def core_deps_from_source(session, protobuf_implementation):
641642
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
642643
},
643644
)
645+
646+
647+
@nox.session(python=DEFAULT_PYTHON_VERSION)
648+
def import_profile(session):
649+
"""Ensure import times remain below defined thresholds."""
650+
profiler_script = (
651+
CURRENT_DIRECTORY.parent.parent / "scripts" / "import_profiler" / "profiler.py"
652+
)
653+
if not profiler_script.exists():
654+
session.skip("The import profiler script was not found.")
655+
656+
session.install(".")
657+
session.run(
658+
"python",
659+
str(profiler_script),
660+
"--module",
661+
{% if api.naming.module_namespace %}
662+
"{{ api.naming.module_namespace[0] }}",
663+
{% else %}
664+
"{{ api.naming.versioned_module_name }}",
665+
{% endif %}
666+
"--iterations",
667+
"10",
668+
"--fail-threshold",
669+
"5000",
670+
)
644671
{% endblock %}

packages/google-cloud-asset/noxfile.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"lint_setup_py",
8989
"blacken",
9090
"docs",
91+
"import_profile",
9192
]
9293

9394
# Error if a python version is missing
@@ -637,3 +638,25 @@ def core_deps_from_source(session, protobuf_implementation):
637638
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
638639
},
639640
)
641+
642+
643+
@nox.session(python="3.15")
644+
def import_profile(session):
645+
"""Ensure import times remain below defined thresholds."""
646+
profiler_script = (
647+
CURRENT_DIRECTORY.parent.parent / "scripts" / "import_profiler" / "profiler.py"
648+
)
649+
if not profiler_script.exists():
650+
session.skip("The import profiler script was not found.")
651+
652+
session.install(".")
653+
session.run(
654+
"python",
655+
str(profiler_script),
656+
"--package",
657+
"google-cloud-asset",
658+
"--iterations",
659+
"10",
660+
"--fail-threshold",
661+
"5000",
662+
)

packages/google-cloud-asset/test_trigger.txt

Whitespace-only changes.

scripts/import_profiler/profiler.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def _format_stats(title, data, p50, p90, p99, fmt):
154154
{_format_stats("Physical RSS RAM (MB)", rss_memories, p50_rss, p90_rss, p99_rss, ".4f")}"""
155155
print(final_output.strip())
156156

157-
def run_master(iterations, target_module, cpu=0, csv_path=None, clear_cache=True):
157+
def run_master(iterations, target_module, cpu=0, csv_path=None, clear_cache=True, fail_threshold=None):
158158
"""Orchestrates the benchmark."""
159159
if iterations < 1:
160160
raise ValueError("Number of iterations must be at least 1.")
@@ -229,6 +229,14 @@ def run_master(iterations, target_module, cpu=0, csv_path=None, clear_cache=True
229229
rss_memories, p50_rss, p90_rss, p99_rss
230230
)
231231

232+
if fail_threshold is not None:
233+
if p99_time > fail_threshold:
234+
print(f"\nFAILURE: P99 import time ({p99_time:.2f} ms) exceeds the failure threshold ({fail_threshold} ms).", file=sys.stderr)
235+
sys.exit(1)
236+
else:
237+
print(f"\nSUCCESS: P99 import time ({p99_time:.2f} ms) is within the failure threshold ({fail_threshold} ms).")
238+
239+
232240
def run_trace(target_module):
233241
"""Generates importtime trace log and writes it to a file."""
234242
trace_file = f"import_trace_{target_module.replace('.', '_')}.log"
@@ -307,8 +315,24 @@ def validate_module_name(module_name):
307315
raise argparse.ArgumentTypeError(f"'{module_name}' is not a valid Python module identifier.")
308316
return module_name
309317

318+
def find_module_from_package(pkg):
319+
candidates = [
320+
pkg.replace('-', '.'),
321+
'.'.join(pkg.split('-')[:-1]) + '_' + pkg.split('-')[-1] if '-' in pkg else pkg,
322+
pkg.replace('-', '_')
323+
]
324+
for mod in candidates:
325+
try:
326+
if importlib.util.find_spec(mod):
327+
return mod
328+
except Exception:
329+
pass
330+
return candidates[0]
331+
310332
parser = argparse.ArgumentParser(description="Python SDK Import Profiler")
311-
parser.add_argument("--module", type=validate_module_name, default="google.cloud.compute_v1", help="Target module to profile")
333+
group = parser.add_mutually_exclusive_group(required=True)
334+
group.add_argument("--module", type=validate_module_name, help="Target module to profile")
335+
group.add_argument("--package", help="Target package name to profile (auto-detects module)")
312336
parser.add_argument("--iterations", type=int, default=50, help="Number of iterations")
313337
default_cpu = 0 if sys.platform.startswith("linux") else NO_CPU_PINNING
314338
parser.add_argument("--cpu", type=int, default=default_cpu, help="CPU core to pin to (or -1 for no pinning)")
@@ -317,20 +341,25 @@ def validate_module_name(module_name):
317341
parser.add_argument("--cprofile", action="store_true", help="Run cProfile")
318342
parser.add_argument("--mprofile", action="store_true", help="Run tracemalloc memory snapshot")
319343
parser.add_argument("--keep-pycache", action="store_true", help="Preserve __pycache__ and allow bytecode execution (Default: False, script automatically sweeps __pycache__ for true cold-starts)")
344+
parser.add_argument("--fail-threshold", type=float, help="Fail the profiling if the P99 time exceeds this threshold (in ms).")
320345
parser.add_argument("--worker", action="store_true", help=argparse.SUPPRESS)
321346

322347
args = parser.parse_args()
323348

349+
target_module = args.module
350+
if args.package:
351+
target_module = find_module_from_package(args.package)
352+
324353
if args.worker:
325-
run_worker(args.module)
354+
run_worker(target_module)
326355
elif args.trace:
327356
if not args.keep_pycache: clean_bytecode()
328-
run_trace(args.module)
357+
run_trace(target_module)
329358
elif args.cprofile:
330359
if not args.keep_pycache: clean_bytecode()
331-
run_cprofile(args.module)
360+
run_cprofile(target_module)
332361
elif args.mprofile:
333362
if not args.keep_pycache: clean_bytecode()
334-
run_mprofile(args.module)
363+
run_mprofile(target_module)
335364
else:
336-
run_master(args.iterations, args.module, args.cpu, args.csv, not args.keep_pycache)
365+
run_master(args.iterations, target_module, args.cpu, args.csv, not args.keep_pycache, args.fail_threshold)

0 commit comments

Comments
 (0)