2323import subprocess
2424import sys
2525import tempfile
26+ import time
2627import yaml
2728from datetime import date , datetime
2829from functools import lru_cache
3132import build .util
3233import parse_googleapis_content
3334
35+ logging .basicConfig (stream = sys .stdout , level = logging .INFO )
36+
37+ import functools
38+
39+ PERF_LOGGING_ENABLED = os .environ .get ("ENABLE_PERF_LOGS" ) == "1"
40+
41+ if PERF_LOGGING_ENABLED :
42+ perf_logger = logging .getLogger ("performance_metrics" )
43+ perf_logger .setLevel (logging .INFO )
44+ perf_handler = logging .FileHandler ("/tmp/performance_metrics.log" , mode = 'w' )
45+ perf_formatter = logging .Formatter ('%(asctime)s | %(message)s' , datefmt = '%H:%M:%S' )
46+ perf_handler .setFormatter (perf_formatter )
47+ perf_logger .addHandler (perf_handler )
48+ perf_logger .propagate = False
49+
50+ def track_time (func ):
51+ """
52+ Decorator. Usage: @track_time
53+ If logging is OFF, it returns the original function (Zero Overhead).
54+ If logging is ON, it wraps the function to measure execution time.
55+ """
56+ if not PERF_LOGGING_ENABLED :
57+ return func
58+
59+ @functools .wraps (func )
60+ def wrapper (* args , ** kwargs ):
61+ start_time = time .perf_counter ()
62+ try :
63+ return func (* args , ** kwargs )
64+ finally :
65+ duration = time .perf_counter () - start_time
66+ perf_logger .info (f"{ func .__name__ :<30} | { duration :.4f} seconds" )
67+
68+ return wrapper
3469
3570try :
3671 import synthtool
@@ -323,8 +358,9 @@ def _get_library_id(request_data: Dict) -> str:
323358 return library_id
324359
325360
361+ @track_time
326362def _run_post_processor (output : str , library_id : str , is_mono_repo : bool ):
327- """Runs the synthtool post-processor on the output directory .
363+ """Runs the synthtool post-processor (templates) and Ruff formatter (lint/format) .
328364
329365 Args:
330366 output(str): Path to the directory in the container where code
@@ -334,25 +370,58 @@ def _run_post_processor(output: str, library_id: str, is_mono_repo: bool):
334370 """
335371 os .chdir (output )
336372 path_to_library = f"packages/{ library_id } " if is_mono_repo else "."
337- logger .info ("Running Python post-processor..." )
373+
374+ # 1. Run Synthtool (Templates & Fixers only)
375+ # Note: This relies on 'nox' being disabled in your environment (via run_fast.sh shim)
376+ # to avoid the slow formatting step inside owlbot.
377+ logger .info ("Running Python post-processor (Templates & Fixers)..." )
338378 if SYNTHTOOL_INSTALLED :
339- if is_mono_repo :
340- python_mono_repo .owlbot_main (path_to_library )
341- else :
342- # Some repositories have customizations in `librarian.py`.
343- # If this file exists, run those customizations instead of `owlbot_main`
344- if Path (f"{ output } /librarian.py" ).exists ():
345- subprocess .run (["python3.14" , f"{ output } /librarian.py" ])
379+ try :
380+ if is_mono_repo :
381+ python_mono_repo .owlbot_main (path_to_library )
346382 else :
347- python .owlbot_main ()
348- else :
349- raise SYNTHTOOL_IMPORT_ERROR # pragma: NO COVER
383+ # Handle custom librarian scripts if present
384+ if Path (f"{ output } /librarian.py" ).exists ():
385+ subprocess .run (["python3.14" , f"{ output } /librarian.py" ])
386+ else :
387+ python .owlbot_main ()
388+ except Exception as e :
389+ logger .warning (f"Synthtool warning (non-fatal): { e } " )
390+
391+ # 2. Run RUFF (Fast Formatter & Import Sorter)
392+ # This replaces both 'isort' and 'black' and runs in < 1 second.
393+ # We hardcode flags here to match Black defaults so you don't need config files.
394+ # logger.info("🚀 Running Ruff (Fast Formatter)...")
395+ try :
396+ # STEP A: Fix Imports (like isort)
397+ subprocess .run (
398+ [
399+ "ruff" , "check" ,
400+ "--select" , "I" , # Only run Import sorting rules
401+ "--fix" , # Auto-fix them
402+ "--line-length=88" , # Match Black default
403+ "--known-first-party=google" , # Prevent 'google' moving to 3rd party block
404+ output
405+ ],
406+ check = False ,
407+ stdout = subprocess .DEVNULL ,
408+ stderr = subprocess .DEVNULL
409+ )
350410
351- # If there is no noxfile, run `isort`` and `black` on the output.
352- # This is required for proto-only libraries which are not GAPIC.
353- if not Path (f"{ output } /{ path_to_library } /noxfile.py" ).exists ():
354- subprocess .run (["isort" , output ])
355- subprocess .run (["black" , output ])
411+ # STEP B: Format Code (like black)
412+ subprocess .run (
413+ [
414+ "ruff" , "format" ,
415+ "--line-length=88" , # Match Black default
416+ output
417+ ],
418+ check = False ,
419+ stdout = subprocess .DEVNULL ,
420+ stderr = subprocess .DEVNULL
421+ )
422+ except FileNotFoundError :
423+ logger .warning ("⚠️ Ruff binary not found. Code will be unformatted." )
424+ logger .warning (" Please run: pip install ruff" )
356425
357426 logger .info ("Python post-processor ran successfully." )
358427
@@ -392,6 +461,7 @@ def _add_header_to_files(directory: str) -> None:
392461 f .writelines (lines )
393462
394463
464+ @track_time
395465def _copy_files_needed_for_post_processing (
396466 output : str , input : str , library_id : str , is_mono_repo : bool
397467):
@@ -444,6 +514,7 @@ def _copy_files_needed_for_post_processing(
444514 )
445515
446516
517+ @track_time
447518def _clean_up_files_after_post_processing (
448519 output : str , library_id : str , is_mono_repo : bool
449520):
@@ -590,6 +661,7 @@ def _get_repo_name_from_repo_metadata(base: str, library_id: str, is_mono_repo:
590661 return repo_name
591662
592663
664+ @track_time
593665def _generate_repo_metadata_file (
594666 output : str , library_id : str , source : str , apis : List [Dict ], is_mono_repo : bool
595667):
@@ -631,6 +703,7 @@ def _generate_repo_metadata_file(
631703 _write_json_file (output_repo_metadata , metadata_content )
632704
633705
706+ @track_time
634707def _copy_readme_to_docs (output : str , library_id : str , is_mono_repo : bool ):
635708 """Copies the README.rst file for a generated library to docs/README.rst.
636709
@@ -672,6 +745,7 @@ def _copy_readme_to_docs(output: str, library_id: str, is_mono_repo: bool):
672745 f .write (content )
673746
674747
748+ @track_time
675749def handle_generate (
676750 librarian : str = LIBRARIAN_DIR ,
677751 source : str = SOURCE_DIR ,
@@ -933,6 +1007,7 @@ def _stage_gapic_library(tmp_dir: str, staging_dir: str) -> None:
9331007 shutil .copytree (tmp_dir , staging_dir , dirs_exist_ok = True )
9341008
9351009
1010+ @track_time
9361011def _generate_api (
9371012 api_path : str ,
9381013 library_id : str ,
@@ -1748,6 +1823,7 @@ def handle_release_stage(
17481823 output = args .output ,
17491824 input = args .input ,
17501825 )
1826+
17511827 elif args .command == "build" :
17521828 args .func (librarian = args .librarian , repo = args .repo )
17531829 elif args .command == "release-stage" :
0 commit comments