uninstall: fix accidental cubic complexity (#34005)
* uninstall: fix accidental cubic complexity Currently spack uninstall runs in worst case cubic time complexity thanks to traversal during traversal during traversal while collecting the specs to be uninstalled. Also brings down the number of error messages printed to something linear in the amount of matching specs instead of quadratic.
This commit is contained in:
parent
59dd405626
commit
d33c990278
@ -17,6 +17,7 @@
|
|||||||
import spack.package_base
|
import spack.package_base
|
||||||
import spack.repo
|
import spack.repo
|
||||||
import spack.store
|
import spack.store
|
||||||
|
import spack.traverse as traverse
|
||||||
from spack.database import InstallStatuses
|
from spack.database import InstallStatuses
|
||||||
|
|
||||||
description = "remove installed packages"
|
description = "remove installed packages"
|
||||||
@ -144,11 +145,7 @@ def installed_dependents(specs, env):
|
|||||||
active environment, and one from specs to dependent installs outside of
|
active environment, and one from specs to dependent installs outside of
|
||||||
the active environment.
|
the active environment.
|
||||||
|
|
||||||
Any of the input specs may appear in both mappings (if there are
|
Every installed dependent spec is listed once.
|
||||||
dependents both inside and outside the current environment).
|
|
||||||
|
|
||||||
If a dependent spec is used both by the active environment and by
|
|
||||||
an inactive environment, it will only appear in the first mapping.
|
|
||||||
|
|
||||||
If there is not current active environment, the first mapping will be
|
If there is not current active environment, the first mapping will be
|
||||||
empty.
|
empty.
|
||||||
@ -158,19 +155,27 @@ def installed_dependents(specs, env):
|
|||||||
|
|
||||||
env_hashes = set(env.all_hashes()) if env else set()
|
env_hashes = set(env.all_hashes()) if env else set()
|
||||||
|
|
||||||
all_specs_in_db = spack.store.db.query()
|
# Ensure we stop traversal at input specs.
|
||||||
|
visited = set(s.dag_hash() for s in specs)
|
||||||
|
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
installed = [x for x in all_specs_in_db if spec in x]
|
for dpt in traverse.traverse_nodes(
|
||||||
|
spec.dependents(deptype="all"),
|
||||||
# separate installed dependents into dpts in this environment and
|
direction="parents",
|
||||||
# dpts that are outside this environment
|
visited=visited,
|
||||||
for dpt in installed:
|
deptype="all",
|
||||||
if dpt not in specs:
|
root=True,
|
||||||
if dpt.dag_hash() in env_hashes:
|
key=lambda s: s.dag_hash(),
|
||||||
active_dpts.setdefault(spec, set()).add(dpt)
|
):
|
||||||
else:
|
hash = dpt.dag_hash()
|
||||||
outside_dpts.setdefault(spec, set()).add(dpt)
|
# Ensure that all the specs we get are installed
|
||||||
|
record = spack.store.db.query_local_by_spec_hash(hash)
|
||||||
|
if record is None or not record.installed:
|
||||||
|
continue
|
||||||
|
if hash in env_hashes:
|
||||||
|
active_dpts.setdefault(spec, set()).add(dpt)
|
||||||
|
else:
|
||||||
|
outside_dpts.setdefault(spec, set()).add(dpt)
|
||||||
|
|
||||||
return active_dpts, outside_dpts
|
return active_dpts, outside_dpts
|
||||||
|
|
||||||
@ -250,7 +255,7 @@ def is_ready(dag_hash):
|
|||||||
if force:
|
if force:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_, record = spack.store.db.query_by_spec_hash(dag_hash)
|
record = spack.store.db.query_local_by_spec_hash(dag_hash)
|
||||||
if not record.ref_count:
|
if not record.ref_count:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -725,6 +725,15 @@ def query_by_spec_hash(self, hash_key, data=None):
|
|||||||
return True, db._data[hash_key]
|
return True, db._data[hash_key]
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
def query_local_by_spec_hash(self, hash_key):
|
||||||
|
"""Get a spec by hash in the local database
|
||||||
|
|
||||||
|
Return:
|
||||||
|
(InstallRecord or None): InstallRecord when installed
|
||||||
|
locally, otherwise None."""
|
||||||
|
with self.read_transaction():
|
||||||
|
return self._data.get(hash_key, None)
|
||||||
|
|
||||||
def _assign_dependencies(self, hash_key, installs, data):
|
def _assign_dependencies(self, hash_key, installs, data):
|
||||||
# Add dependencies from other records in the install DB to
|
# Add dependencies from other records in the install DB to
|
||||||
# form a full spec.
|
# form a full spec.
|
||||||
|
@ -3,12 +3,14 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
|
||||||
|
import spack.cmd.uninstall
|
||||||
import spack.environment
|
import spack.environment
|
||||||
import spack.store
|
import spack.store
|
||||||
from spack.main import SpackCommand, SpackCommandError
|
from spack.main import SpackCommand, SpackCommandError
|
||||||
@ -40,6 +42,39 @@ def test_installed_dependents(mutable_database):
|
|||||||
uninstall("-y", "libelf")
|
uninstall("-y", "libelf")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.db
|
||||||
|
def test_correct_installed_dependents(mutable_database):
|
||||||
|
# Test whether we return the right dependents.
|
||||||
|
|
||||||
|
# Take callpath from the database
|
||||||
|
callpath = spack.store.db.query_local("callpath")[0]
|
||||||
|
|
||||||
|
# Ensure it still has dependents and dependencies
|
||||||
|
dependents = callpath.dependents(deptype="all")
|
||||||
|
dependencies = callpath.dependencies(deptype="all")
|
||||||
|
assert dependents and dependencies
|
||||||
|
|
||||||
|
# Uninstall it, so it's missing.
|
||||||
|
callpath.package.do_uninstall(force=True)
|
||||||
|
|
||||||
|
# Retrieve all dependent hashes
|
||||||
|
inside_dpts, outside_dpts = spack.cmd.uninstall.installed_dependents(dependencies, None)
|
||||||
|
dependent_hashes = [s.dag_hash() for s in itertools.chain(*outside_dpts.values())]
|
||||||
|
set_dependent_hashes = set(dependent_hashes)
|
||||||
|
|
||||||
|
# We dont have an env, so this should be empty.
|
||||||
|
assert not inside_dpts
|
||||||
|
|
||||||
|
# Assert uniqueness
|
||||||
|
assert len(dependent_hashes) == len(set_dependent_hashes)
|
||||||
|
|
||||||
|
# Ensure parents of callpath are listed
|
||||||
|
assert all(s.dag_hash() in set_dependent_hashes for s in dependents)
|
||||||
|
|
||||||
|
# Ensure callpath itself is not, since it was missing.
|
||||||
|
assert callpath.dag_hash() not in set_dependent_hashes
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.db
|
@pytest.mark.db
|
||||||
def test_recursive_uninstall(mutable_database):
|
def test_recursive_uninstall(mutable_database):
|
||||||
"""Test recursive uninstall."""
|
"""Test recursive uninstall."""
|
||||||
|
Loading…
Reference in New Issue
Block a user