Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solver performance #5335

Merged
merged 4 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/poetry/mixology/partial_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ def _register(self, assignment: Assignment) -> None:
name = assignment.dependency.complete_name
old_positive = self._positive.get(name)
if old_positive is not None:
self._positive[name] = old_positive.intersect(assignment)
value = old_positive.intersect(assignment)
assert value is not None
self._positive[name] = value

return

Expand Down
4 changes: 4 additions & 0 deletions src/poetry/mixology/term.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import functools

from typing import TYPE_CHECKING

from poetry.mixology.set_relation import SetRelation
Expand Down Expand Up @@ -46,6 +48,7 @@ def satisfies(self, other: Term) -> bool:
and self.relation(other) == SetRelation.SUBSET
)

@functools.lru_cache(maxsize=None)
def relation(self, other: Term) -> int:
"""
Returns the relationship between the package versions
Expand Down Expand Up @@ -108,6 +111,7 @@ def relation(self, other: Term) -> int:
# not foo ^1.5.0 is a superset of not foo ^1.0.0
return SetRelation.OVERLAPPING

@functools.lru_cache(maxsize=None)
def intersect(self, other: Term) -> Term | None:
"""
Returns a Term that represents the packages
Expand Down
46 changes: 44 additions & 2 deletions src/poetry/mixology/version_solver.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import functools
import time

from contextlib import suppress
Expand All @@ -23,12 +24,43 @@
from poetry.core.packages.package import Package
from poetry.core.packages.project_package import ProjectPackage

from poetry.packages import DependencyPackage
from poetry.puzzle.provider import Provider


_conflict = object()


class DependencyCache:
abn marked this conversation as resolved.
Show resolved Hide resolved
"""
A cache of the valid dependencies.

The key observation here is that during the search - except at backtracking
- once we have decided that a dependency is invalid, we never need check it
again.
"""

def __init__(self, provider: Provider):
self.provider = provider
self.cache: dict[str, list[Package]] = {}

@functools.lru_cache(maxsize=128)
abn marked this conversation as resolved.
Show resolved Hide resolved
def search_for(self, dependency: Dependency) -> list[DependencyPackage]:
complete_name = dependency.complete_name
packages = self.cache.get(complete_name)
if packages is None:
packages = self.provider.search_for(dependency)
else:
packages = [p for p in packages if dependency.constraint.allows(p.version)]

self.cache[complete_name] = packages

return packages

def clear(self) -> None:
self.cache.clear()


class VersionSolver:
"""
The version solver that finds a set of package versions that satisfy the
Expand All @@ -47,6 +79,7 @@ def __init__(
):
self._root = root
self._provider = provider
self._dependency_cache = DependencyCache(provider)
self._locked = locked or {}

if use_latest is None:
Expand All @@ -55,6 +88,7 @@ def __init__(
self._use_latest = use_latest

self._incompatibilities: dict[str, list[Incompatibility]] = {}
self._contradicted_incompatibilities: set[Incompatibility] = set()
self._solution = PartialSolution()

@property
Expand Down Expand Up @@ -103,6 +137,9 @@ def _propagate(self, package: str) -> None:
# we can derive stronger assignments sooner and more eagerly find
# conflicts.
for incompatibility in reversed(self._incompatibilities[package]):
if incompatibility in self._contradicted_incompatibilities:
continue

result = self._propagate_incompatibility(incompatibility)

if result is _conflict:
Expand Down Expand Up @@ -149,6 +186,7 @@ def _propagate_incompatibility(
# If term is already contradicted by _solution, then
# incompatibility is contradicted as well and there's nothing new we
# can deduce from it.
self._contradicted_incompatibilities.add(incompatibility)
return None
elif relation == SetRelation.OVERLAPPING:
# If more than one term is inconclusive, we can't deduce anything about
Expand All @@ -166,6 +204,8 @@ def _propagate_incompatibility(
if unsatisfied is None:
return _conflict

self._contradicted_incompatibilities.add(incompatibility)

adverb = "not " if unsatisfied.is_positive() else ""
self._log(f"derived: {adverb}{unsatisfied.dependency}")

Expand Down Expand Up @@ -255,6 +295,8 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility
or most_recent_satisfier.cause is None
):
self._solution.backtrack(previous_satisfier_level)
self._contradicted_incompatibilities.clear()
self._dependency_cache.clear()
if new_incompatibility:
self._add_incompatibility(incompatibility)

Expand Down Expand Up @@ -346,7 +388,7 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]:
try:
return (
not dependency.marker.is_any(),
len(self._provider.search_for(dependency)),
len(self._dependency_cache.search_for(dependency)),
)
except ValueError:
return not dependency.marker.is_any(), 0
Expand All @@ -359,7 +401,7 @@ def _get_min(dependency: Dependency) -> tuple[bool, int]:
locked = self._get_locked(dependency)
if locked is None or not dependency.constraint.allows(locked.version):
try:
packages = self._provider.search_for(dependency)
packages = self._dependency_cache.search_for(dependency)
except ValueError as e:
self._add_incompatibility(
Incompatibility([Term(dependency, True)], PackageNotFoundCause(e))
Expand Down
25 changes: 0 additions & 25 deletions src/poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def __init__(
self._io = io
self._env = env
self._python_constraint = package.python_constraint
self._search_for: dict[Dependency, list[Package]] = {}
self._is_debugging = self._io.is_debug() or self._io.is_very_verbose()
self._in_progress = False
self._overrides: dict = {}
Expand Down Expand Up @@ -119,28 +118,6 @@ def search_for(
if dependency.is_root:
return PackageCollection(dependency, [self._package])

for constraint in self._search_for:
if (
constraint.is_same_package_as(dependency)
and constraint.constraint.intersect(dependency.constraint)
== dependency.constraint
):
packages = [
p
for p in self._search_for[constraint]
if dependency.constraint.allows(p.version)
]

packages.sort(
key=lambda p: (
not p.is_prerelease() and not dependency.allows_prereleases(),
p.version,
),
reverse=True,
)

return PackageCollection(dependency, packages)

if dependency.is_vcs():
packages = self.search_for_vcs(dependency)
elif dependency.is_file():
Expand All @@ -160,8 +137,6 @@ def search_for(
reverse=True,
)

self._search_for[dependency] = packages

return PackageCollection(dependency, packages)

def search_for_vcs(self, dependency: VCSDependency) -> list[Package]:
Expand Down
2 changes: 1 addition & 1 deletion tests/console/commands/test_show.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def test_show_outdated_has_prerelease_and_allowed(
):
poetry.package.add_dependency(
Factory.create_dependency(
"cachy", {"version": "^0.1.0", "allow-prereleases": True}
"cachy", {"version": ">=0.0.1", "allow-prereleases": True}
)
)
poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0"))
Expand Down