Skip to content

Commit 885de7f

Browse files
committed
feat: implement dynamic import profile diff check against baseline
1 parent 705b188 commit 885de7f

4 files changed

Lines changed: 146 additions & 9 deletions

File tree

ci/run_single_test.sh

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,27 @@ case ${TEST_TYPE} in
107107
;;
108108
import_profile)
109109
if nox --list-sessions | grep -q "import_profile"; then
110-
nox -s import_profile
110+
rm -f /tmp/baseline.csv
111+
if [ -n "${TARGET_BRANCH}" ]; then
112+
if git rev-parse HEAD^1 >/dev/null 2>&1; then
113+
echo "Checking out HEAD^1 for baseline..."
114+
git checkout HEAD^1
115+
if nox --list-sessions | grep -q "import_profile"; then
116+
nox -s import_profile -- --csv /tmp/baseline.csv
117+
else
118+
echo "No import_profile session on baseline."
119+
fi
120+
git checkout -
121+
else
122+
echo "Could not find HEAD^1. Skipping baseline generation."
123+
fi
124+
fi
125+
126+
if [ -f /tmp/baseline.csv ]; then
127+
nox -s import_profile -- --diff-baseline /tmp/baseline.csv --diff-threshold 100
128+
else
129+
nox -s import_profile
130+
fi
111131
retval=$?
112132
else
113133
echo "Skipping import_profile as it is not supported by this package yet."

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,5 +667,6 @@ def import_profile(session):
667667
"10",
668668
"--fail-threshold",
669669
"5000",
670+
*session.posargs,
670671
)
671672
{% endblock %}

patch_goldens.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import os
2+
import glob
3+
import re
4+
5+
def get_module_name(golden_dir):
6+
setup_py = os.path.join(golden_dir, "setup.py")
7+
if not os.path.exists(setup_py):
8+
return "google"
9+
10+
with open(setup_py, "r", encoding="utf-8") as f:
11+
content = f.read()
12+
13+
match = re.search(r"'(google/[^']+)/gapic_version.py'", content)
14+
if match:
15+
return match.group(1).replace("/", ".")
16+
17+
match = re.search(r"\"(google/[^\"]+)/gapic_version.py\"", content)
18+
if match:
19+
return match.group(1).replace("/", ".")
20+
21+
return "google"
22+
23+
def patch_noxfile(golden_dir):
24+
path = os.path.join(golden_dir, "noxfile.py")
25+
if not os.path.exists(path):
26+
return False
27+
28+
try:
29+
with open(path, 'r', encoding='utf-8') as f:
30+
content = f.read()
31+
except UnicodeDecodeError:
32+
return False
33+
34+
module_name = get_module_name(golden_dir)
35+
36+
idx = content.find("@nox.session(python=\"3.15\")\ndef import_profile(session):")
37+
if idx != -1:
38+
content = content[:idx].strip() + "\n"
39+
else:
40+
idx = content.find("@nox.session\ndef import_profile")
41+
if idx != -1:
42+
content = content[:idx].strip() + "\n"
43+
44+
if "nox.options.sessions =" in content and "\"import_profile\"" not in content:
45+
content = re.sub(
46+
r'(\n\s*"docs",\n)(\s*\])',
47+
r'\1 "import_profile",\n\2',
48+
content
49+
)
50+
51+
session_text = f"""
52+
@nox.session(python="3.15")
53+
def import_profile(session):
54+
\"\"\"Ensure import times remain below defined thresholds.\"\"\"
55+
profiler_script = (
56+
CURRENT_DIRECTORY.parent.parent / "scripts" / "import_profiler" / "profiler.py"
57+
)
58+
if not profiler_script.exists():
59+
session.skip("The import profiler script was not found.")
60+
61+
session.install(".")
62+
session.run(
63+
"python",
64+
str(profiler_script),
65+
"--package",
66+
"{module_name}",
67+
"--fail-threshold",
68+
"5000",
69+
"--iterations",
70+
"10",
71+
*session.posargs,
72+
)
73+
"""
74+
75+
content += "\n" + session_text
76+
77+
with open(path, 'w', encoding='utf-8') as f:
78+
f.write(content)
79+
return True
80+
81+
count = 0
82+
for golden_dir in glob.glob('packages/gapic-generator/tests/integration/goldens/*'):
83+
if os.path.isdir(golden_dir):
84+
if patch_noxfile(golden_dir):
85+
count += 1
86+
87+
print(f"Patched {count} goldens.")

scripts/import_profiler/profiler.py

Lines changed: 37 additions & 8 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, fail_threshold=None):
157+
def run_master(iterations, target_module, cpu=0, csv_path=None, clear_cache=True, fail_threshold=None, diff_baseline=None, diff_threshold=None):
158158
"""Orchestrates the benchmark."""
159159
if iterations < 1:
160160
raise ValueError("Number of iterations must be at least 1.")
@@ -229,12 +229,39 @@ 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).")
232+
if fail_threshold is not None:
233+
if p99_time > fail_threshold:
234+
print(f"
235+
FAILURE: P99 import time ({p99_time:.2f} ms) exceeds the failure threshold ({fail_threshold} ms).", file=sys.stderr)
236+
sys.exit(1)
237+
else:
238+
print(f"
239+
SUCCESS: P99 import time ({p99_time:.2f} ms) is within the failure threshold ({fail_threshold} ms).")
240+
241+
if diff_baseline:
242+
if os.path.exists(diff_baseline):
243+
baseline_times = []
244+
with open(diff_baseline, "r", encoding="utf-8") as f:
245+
reader = csv.reader(f)
246+
next(reader) # skip header
247+
for row in reader:
248+
baseline_times.append(float(row[1]))
249+
_, _, baseline_p99 = _calculate_percentiles(baseline_times)
250+
diff = p99_time - baseline_p99
251+
252+
print(f"
253+
--- Diff vs Baseline ---")
254+
print(f"Baseline P99: {baseline_p99:.2f} ms")
255+
print(f"Current P99: {p99_time:.2f} ms")
256+
print(f"Difference: {diff:+.2f} ms")
257+
258+
if diff > diff_threshold:
259+
print(f"FAILURE: Import time regression of {diff:.2f} ms exceeds the allowed threshold of {diff_threshold} ms.", file=sys.stderr)
260+
sys.exit(1)
261+
else:
262+
print("SUCCESS: Import time diff is within acceptable thresholds.")
263+
else:
264+
print(f"WARNING: Baseline CSV {diff_baseline} not found. Skipping diff check.")
238265

239266

240267
def run_trace(target_module):
@@ -342,6 +369,8 @@ def find_module_from_package(pkg):
342369
parser.add_argument("--mprofile", action="store_true", help="Run tracemalloc memory snapshot")
343370
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)")
344371
parser.add_argument("--fail-threshold", type=float, help="Fail the profiling if the P99 time exceeds this threshold (in ms).")
372+
parser.add_argument("--diff-baseline", help="Path to a baseline CSV file to compare against.")
373+
parser.add_argument("--diff-threshold", type=float, default=100.0, help="Fail if P99 time exceeds baseline P99 by this many ms.")
345374
parser.add_argument("--worker", action="store_true", help=argparse.SUPPRESS)
346375

347376
args = parser.parse_args()
@@ -362,4 +391,4 @@ def find_module_from_package(pkg):
362391
if not args.keep_pycache: clean_bytecode()
363392
run_mprofile(target_module)
364393
else:
365-
run_master(args.iterations, target_module, args.cpu, args.csv, not args.keep_pycache, args.fail_threshold)
394+
run_master(args.iterations, target_module, args.cpu, args.csv, not args.keep_pycache, args.fail_threshold, args.diff_baseline, args.diff_threshold)

0 commit comments

Comments
 (0)