gitlab: Prune untouched specs less aggressively (#33669)

Untouched spec pruning was added to reduce the number of specs
developers see getting rebuilt in their PR pipelines that they
don't understand.  Because the state of the develop mirror lags
quite far behind the tip of the develop branch, PRs often find
they need to rebuild things untouched by their PR.

Untouched spec pruning was previously implemented by finding all
specs in the environment with names of packages touched by the PR,
traversing in both directions the DAGS of those specs, and adding
all dependencies as well as dependents to a list of concrete specs
that should not be considered for pruning.

We found that this heuristic results in too many pruned specs, and
that dependents of touched specs must have all their dependencies
added to the list of specs that should not be considered for pruning.
This commit is contained in:
Scott Wittenburg 2022-11-03 13:33:52 -06:00 committed by GitHub
parent 08bee718a2
commit b55509ffa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 31 additions and 26 deletions

View File

@ -515,38 +515,36 @@ def compute_affected_packages(rev1="HEAD^", rev2="HEAD"):
return spack.repo.get_all_package_diffs("ARC", rev1=rev1, rev2=rev2) return spack.repo.get_all_package_diffs("ARC", rev1=rev1, rev2=rev2)
def get_spec_filter_list(env, affected_pkgs, dependencies=True, dependents=True): def get_spec_filter_list(env, affected_pkgs):
"""Given a list of package names, and assuming an active and """Given a list of package names and an active/concretized
concretized environment, return a set of concrete specs from environment, return the set of all concrete specs from the
the environment corresponding to any of the affected pkgs (or environment that could have been affected by changing the
optionally to any of their dependencies/dependents). list of packages.
Arguments: Arguments:
env (spack.environment.Environment): Active concrete environment env (spack.environment.Environment): Active concrete environment
affected_pkgs (List[str]): Affected package names affected_pkgs (List[str]): Affected package names
dependencies (bool): Include dependencies of affected packages
dependents (bool): Include dependents of affected pacakges
Returns: Returns:
A list of concrete specs from the active environment including A set of concrete specs from the active environment including
those associated with affected packages, and possible their those associated with affected packages, their dependencies and
dependencies and dependents as well. dependents, as well as their dependents dependencies.
""" """
affected_specs = set() affected_specs = set()
all_concrete_specs = env.all_specs() all_concrete_specs = env.all_specs()
tty.debug("All concrete environment specs:") tty.debug("All concrete environment specs:")
for s in all_concrete_specs: for s in all_concrete_specs:
tty.debug(" {0}/{1}".format(s.name, s.dag_hash()[:7])) tty.debug(" {0}/{1}".format(s.name, s.dag_hash()[:7]))
for pkg in affected_pkgs: env_matches = [s for s in all_concrete_specs if s.name in frozenset(affected_pkgs)]
env_matches = [s for s in all_concrete_specs if s.name == pkg] visited = set()
dag_hash = lambda s: s.dag_hash()
for match in env_matches: for match in env_matches:
affected_specs.add(match) for parent in match.traverse(direction="parents", key=dag_hash):
if dependencies: affected_specs.update(
affected_specs.update(match.traverse(direction="children", root=False)) parent.traverse(direction="children", visited=visited, key=dag_hash)
if dependents: )
affected_specs.update(match.traverse(direction="parents", root=False))
return affected_specs return affected_specs
@ -625,7 +623,7 @@ def generate_gitlab_ci_yaml(
affected_specs = get_spec_filter_list(env, affected_pkgs) affected_specs = get_spec_filter_list(env, affected_pkgs)
tty.debug("all affected specs:") tty.debug("all affected specs:")
for s in affected_specs: for s in affected_specs:
tty.debug(" {0}".format(s.name)) tty.debug(" {0}/{1}".format(s.name, s.dag_hash()[:7]))
# Allow overriding --prune-dag cli opt with environment variable # Allow overriding --prune-dag cli opt with environment variable
prune_dag_override = os.environ.get("SPACK_PRUNE_UP_TO_DATE", None) prune_dag_override = os.environ.get("SPACK_PRUNE_UP_TO_DATE", None)
@ -848,7 +846,11 @@ def generate_gitlab_ci_yaml(
if prune_untouched_packages: if prune_untouched_packages:
if release_spec not in affected_specs: if release_spec not in affected_specs:
tty.debug("Pruning {0}, untouched by change.".format(release_spec.name)) tty.debug(
"Pruning {0}/{1}, untouched by change.".format(
release_spec.name, release_spec.dag_hash()[:7]
)
)
spec_record["needs_rebuild"] = False spec_record["needs_rebuild"] = False
continue continue

View File

@ -442,13 +442,16 @@ def test_get_spec_filter_list(mutable_mock_env_path, config, mutable_mock_repo):
touched = ["libdwarf"] touched = ["libdwarf"]
# traversing both directions from libdwarf in the graphs depicted # traversing both directions from libdwarf in the graphs depicted
# above results in the following possibly affected env specs: # above (and additionally including dependencies of dependents of
# mpileaks, callpath, dyninst, libdwarf, and libelf. Unaffected # libdwarf) results in the following possibly affected env specs:
# specs are mpich, plus hypre and it's dependencies. # mpileaks, callpath, dyninst, libdwarf, libelf, and mpich.
# Unaffected specs are hypre and it's dependencies.
affected_specs = ci.get_spec_filter_list(e1, touched) affected_specs = ci.get_spec_filter_list(e1, touched)
affected_pkg_names = set([s.name for s in affected_specs]) affected_pkg_names = set([s.name for s in affected_specs])
expected_affected_pkg_names = set(["mpileaks", "callpath", "dyninst", "libdwarf", "libelf"]) expected_affected_pkg_names = set(
["mpileaks", "mpich", "callpath", "dyninst", "libdwarf", "libelf"]
)
assert affected_pkg_names == expected_affected_pkg_names assert affected_pkg_names == expected_affected_pkg_names

View File

@ -1837,8 +1837,8 @@ def fake_stack_changed(env_path, rev1="HEAD^", rev2="HEAD"):
yaml_contents = syaml.load(contents) yaml_contents = syaml.load(contents)
for ci_key in yaml_contents.keys(): for ci_key in yaml_contents.keys():
if "archive-files" in ci_key or "mpich" in ci_key: if "archive-files" in ci_key:
print("Error: archive-files and mpich should have been pruned") print("Error: archive-files should have been pruned")
assert False assert False