2424
2525from __future__ import print_function
2626
27+ import ast
2728import contextlib
2829import datetime
2930import glob
@@ -109,6 +110,8 @@ def get_boolean_env(name, default=False):
109110os .environ ["GRAALPY_VERSION" ] = GRAAL_VERSION
110111
111112MAIN_BRANCH = 'master'
113+ GRAALPY_PGO_PROFILE_ARTIFACT_GROUP = "graalpy"
114+ GRAALPY_PGO_PROFILE_ARTIFACT_PREFIX = "pgo-"
112115
113116GRAALPYTHON_MAIN_CLASS = "com.oracle.graal.python.shell.GraalPythonMain"
114117
@@ -293,6 +296,98 @@ def _is_overridden_native_image_arg(prefix):
293296 return any (arg .startswith (prefix ) for arg in extras )
294297
295298
299+ def _normalize_branch_name (branch ):
300+ if not branch :
301+ return ""
302+ branch = branch .strip ()
303+ for prefix in ("refs/heads/" , "origin/" ):
304+ if branch .startswith (prefix ):
305+ return branch [len (prefix ):]
306+ return branch
307+
308+
309+ def _graalpython_target_import_commit (target_branch ):
310+ """Return the GraalPy commit imported by the target branch's VM suite.
311+
312+ Product PGO profiles are produced by the GraalPy post-merge profile job and
313+ keyed by GraalPy commit. In linked cross-repo PR gates, CI can auto-bump the
314+ VM suite's GraalPy import to the GraalPython PR merge commit, which normally
315+ has no post-merge product profile. For feature branches, product-ee therefore
316+ uses the GraalPy import from the target VM branch instead, bypassing that
317+ CI-generated import bump.
318+
319+ Prefer origin/<target>. Some CI workspaces only have the PR merge commit; in
320+ that case HEAD^1 is the target-side parent and still contains the target
321+ branch import. Local workspaces can fall back to the checked-out VM suite.
322+ The suite file is a literal dict, so parse it instead of executing suite.py.
323+ """
324+ vm_suite = mx .suite ("vm" , fatalIfMissing = False )
325+ if not vm_suite or not vm_suite .vc :
326+ mx .warn ("Cannot resolve target-branch GraalPy import: mx suite 'vm' is not available" )
327+ return None
328+
329+ target_branch = _normalize_branch_name (target_branch )
330+ suite_path = "vm/mx.vm/suite.py"
331+
332+ # Some CI checkouts do not keep origin/<target>. In PR merge checkouts, the
333+ # first parent is the target branch side of the merge and therefore still
334+ # gives us the target branch's imported GraalPy commit without fetching.
335+ suite_text = None
336+ refs = [f"origin/{ target_branch } " ]
337+ parents = vm_suite .vc .git_command (
338+ vm_suite .vc_dir ,
339+ ["rev-list" , "--parents" , "-n" , "1" , "HEAD" ],
340+ abortOnError = False ,
341+ )
342+ if parents and len (parents .split ()) > 2 :
343+ refs .append ("HEAD^1" )
344+ for ref in refs :
345+ if ref :
346+ suite_text = vm_suite .vc .git_command (
347+ vm_suite .vc_dir ,
348+ ["show" , f"{ ref } :{ suite_path } " ],
349+ abortOnError = False ,
350+ )
351+ if suite_text :
352+ break
353+
354+ if not suite_text :
355+ suite_file = os .path .join (vm_suite .dir , f"mx.{ vm_suite .name } " , "suite.py" )
356+ try :
357+ with open (suite_file , encoding = "utf-8" ) as f :
358+ suite_text = f .read ()
359+ except OSError :
360+ mx .warn (f"Cannot read { suite_path } to resolve the GraalPy profile source" )
361+ return None
362+
363+ try :
364+ suite_node = next (
365+ node .value
366+ for node in ast .parse (suite_text , filename = suite_path ).body
367+ if isinstance (node , ast .Assign )
368+ and any (isinstance (target , ast .Name ) and target .id == "suite" for target in node .targets )
369+ )
370+ suite = ast .literal_eval (suite_node )
371+ imports = suite .get ("imports" , {}).get ("suites" , [])
372+ matches = []
373+ for imported_suite in imports :
374+ version = imported_suite .get ("version" )
375+ if (
376+ imported_suite .get ("name" ) == "graalpython"
377+ and isinstance (version , str )
378+ and re .match (r"^[0-9a-f]{40}$" , version )
379+ ):
380+ matches .append (version )
381+ except (StopIteration , SyntaxError , ValueError , TypeError ) as e :
382+ mx .warn (f"Cannot evaluate { suite_path } to resolve the GraalPy profile source: { e } " )
383+ return None
384+
385+ if len (matches ) == 1 :
386+ return matches [0 ]
387+ mx .warn (f"Expected exactly one graalpython import in target vm suite, found { len (matches )} " )
388+ return None
389+
390+
296391def github_ci_build_args ():
297392 # Determine memory and parallelism for GitHub CI builds
298393 # Use 90% of available memory up to 14GB, but at least 8GB
@@ -347,15 +442,57 @@ def libpythonvm_build_args():
347442 build_args += ['-H:-ProtectionKeys' ]
348443
349444 profile = None
445+ require_profile = get_boolean_env ("GRAALPY_REQUIRE_PGO_PROFILE" )
350446 if (
351447 "GRAALPY_PGO_PROFILE" not in os .environ
352448 and mx .suite ('graalpython-enterprise' , fatalIfMissing = False )
353449 and mx_sdk_vm_ng .get_bootstrap_graalvm_version () >= mx .VersionSpec ("25.0" )
354450 and not _is_overridden_native_image_arg ("--pgo" )
355451 ):
356452 vc = SUITE .vc
357- commit = str (vc .tip (SUITE .dir )).strip ()
358- branch = str (vc .active_branch (SUITE .dir , abortOnError = False ) or 'master' ).strip ()
453+ source_commit = str (vc .tip (SUITE .dir )).strip ()
454+ source_branch = _normalize_branch_name (
455+ os .environ .get ("FROM_BRANCH" ) or vc .active_branch (SUITE .dir , abortOnError = False ) or 'master'
456+ )
457+ target_branch = _normalize_branch_name (os .environ .get ("TO_BRANCH" ))
458+ profile_source_commit = source_commit
459+ profile_source_branch = source_branch
460+ profile_source_reason = "current GraalPy commit"
461+ override = os .environ .get ("GRAALPY_PGO_PROFILE_SOURCE_COMMIT" )
462+ if override :
463+ profile_source_commit = override .strip ()
464+ if not re .match (r"^[0-9a-f]{40}$" , profile_source_commit ):
465+ mx .abort (f"GRAALPY_PGO_PROFILE_SOURCE_COMMIT must be a 40-character lowercase git commit, got: { override } " )
466+ profile_source_reason = "GRAALPY_PGO_PROFILE_SOURCE_COMMIT"
467+ elif (
468+ target_branch
469+ and source_branch
470+ and source_branch != target_branch
471+ and not (source_branch == MAIN_BRANCH or source_branch .startswith (("release/" , "cpu/" )))
472+ ):
473+ # Feature branch merge commits can import a fresh GraalPy revision that
474+ # has no released-product profile yet. Use the target branch's imported
475+ # GraalPy commit so product-ee consumes the profile that already belongs
476+ # to the baseline product launcher.
477+ target_import_commit = _graalpython_target_import_commit (target_branch )
478+ if target_import_commit :
479+ profile_source_commit = target_import_commit
480+ profile_source_branch = target_branch
481+ profile_source_reason = f"target branch import from { target_branch } "
482+ elif require_profile :
483+ mx .abort ("\n " .join ([
484+ "Could not resolve the GraalPy PGO profile source commit from the target branch import." ,
485+ f"GraalPy source commit: { source_commit } " ,
486+ f"Source branch: { source_branch or '<unknown>' } " ,
487+ f"Target branch: { target_branch or '<unknown>' } " ,
488+ "Expected to read vm/mx.vm/suite.py from the target branch and find the graalpython import version." ,
489+ "Set GRAALPY_PGO_PROFILE_SOURCE_COMMIT=<40-character-commit> to override this lookup for debugging." ,
490+ ]))
491+ else :
492+ mx .warn ("Falling back to the current GraalPy commit for PGO profile lookup because the target branch import could not be resolved" )
493+ artifact_name = f"{ GRAALPY_PGO_PROFILE_ARTIFACT_GROUP } /{ GRAALPY_PGO_PROFILE_ARTIFACT_PREFIX } { profile_source_commit } "
494+ mx .log (f"GraalPy source commit for PGO profile lookup: { source_commit } " )
495+ mx .log (f"GraalPy PGO profile source commit: { profile_source_commit } ({ profile_source_reason } )" )
359496
360497 if script := os .environ .get ("ARTIFACT_DOWNLOAD_SCRIPT" ):
361498 # This is always available in the GraalPy CI
@@ -364,29 +501,50 @@ def libpythonvm_build_args():
364501 [
365502 sys .executable ,
366503 script ,
367- f"graalpy/pgo- { commit } " ,
504+ artifact_name ,
368505 profile ,
369506 ],
370507 nonZeroIsFatal = False ,
371508 )
372- else :
509+ elif not require_profile :
373510 # Locally, we try to get a reasonable profile
374511 get_profile = mx .command_function ('python-get-latest-profile' , fatalIfMissing = False )
375512 if get_profile :
376- for b in [branch , "master" ]:
513+ seen_branches = set ()
514+ for b in [profile_source_branch , source_branch , MAIN_BRANCH ]:
515+ b = _normalize_branch_name (b )
516+ if not b or b in seen_branches :
517+ continue
518+ seen_branches .add (b )
377519 if not profile :
378520 try :
379521 profile = get_profile (["--branch" , b ])
380522 except BaseException :
381523 pass
382524
383- if CI and (not profile or not os .path .isfile (profile )):
525+ profile_missing = not profile or not os .path .isfile (profile )
526+ if require_profile and profile_missing :
527+ mx .abort ("\n " .join ([
528+ "GRAALPY_REQUIRE_PGO_PROFILE is set, but no CI generated GraalPy PGO profile was found." ,
529+ f"GraalPy source commit: { source_commit } " ,
530+ f"Source branch: { source_branch or '<unknown>' } " ,
531+ f"Target branch: { target_branch or '<unset>' } " ,
532+ f"PGO profile source commit: { profile_source_commit } ({ profile_source_reason } )" ,
533+ f"Expected artifact: { artifact_name } " ,
534+ "The product profile configuration does not fall back to benchmark-local PGO." ,
535+ "Run the GraalPy CI job python-pgo-profile-post_merge-linux-amd64-jdk-latest for the PGO profile source commit, then retry the product-ee benchmark." ,
536+ ]))
537+
538+ if CI and profile_missing :
384539 mx .log ("No profile in CI job" )
385540 # When running on a release branch or attempting to merge into
386- # a release branch, make sure we can use a PGO profile, and
387- # when running in the CI on a bench runner, ensure a PGO profile
541+ # a release/CPU branch, make sure we can use a PGO profile, and
542+ # when running in the CI on a bench runner, ensure a PGO profile.
388543 if (
389- any (b .startswith ("release/" ) for b in [branch , os .environ .get ("TO_BRANCH" , "" )])
544+ any (
545+ _normalize_branch_name (b ).startswith (("release/" , "cpu/" ))
546+ for b in [source_branch , target_branch ]
547+ )
390548 or ("bench" in os .environ .get ('BUILD_NAME' , '' ))
391549 ):
392550 mx .warn ("PGO profile must exist for benchmarking and release, creating one now..." )
@@ -505,8 +663,8 @@ def graalpy_native_pgo_build_and_test(args=None):
505663 sys .executable ,
506664 script ,
507665 iprof_gz_path ,
508- f"pgo- { commit } " ,
509- "graalpy" ,
666+ f"{ GRAALPY_PGO_PROFILE_ARTIFACT_PREFIX } { commit } " ,
667+ GRAALPY_PGO_PROFILE_ARTIFACT_GROUP ,
510668 "--lifecycle" ,
511669 "cache" ,
512670 "--artifact-repo-key" ,
0 commit comments