From b4e4ba4681e6bb321349ebf4667bc6d0d008143e Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Sun, 5 Apr 2015 14:30:36 +0200 Subject: [PATCH] Collect more statistics from the installability tester Signed-off-by: Niels Thykier --- britney.py | 4 ++ installability/tester.py | 83 ++++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/britney.py b/britney.py index a396ba5..63ded46 100755 --- a/britney.py +++ b/britney.py @@ -2888,6 +2888,10 @@ class Britney(object): else: self.upgrade_testing() + self.__log('> Stats from the installability tester', type="I") + for stat in self._inst_tester.stats.stats(): + self.__log('> %s' % stat, type="I") + def _installability_test(self, pkg_name, pkg_version, pkg_arch, broken, to_check, nuninst_arch): """Test for installability of a package on an architecture diff --git a/installability/tester.py b/installability/tester.py index f38034c..37c200b 100644 --- a/installability/tester.py +++ b/installability/tester.py @@ -18,6 +18,7 @@ from itertools import chain, filterfalse from britney_util import iter_except + class InstallabilityTester(object): def __init__(self, universe, revuniverse, testing, broken, essentials, @@ -53,6 +54,7 @@ class InstallabilityTester(object): self._revuniverse = revuniverse self._safe_set = safe_set self._eqv_table = eqv_table + self._stats = InstallabilityStats() # Cache of packages known to be broken - we deliberately do not # include "broken" in it. See _optimize for more info. @@ -98,13 +100,16 @@ class InstallabilityTester(object): testing -= eqv_set cbroken |= eqv_set + @property + def stats(self): + return self._stats def are_equivalent(self, p1, p2): """Test if p1 and p2 are equivalent Returns True if p1 and p2 have the same "signature" in the package dependency graph (i.e. relations can not tell - them appart sematically except for their name) + them apart semantically except for their name) """ eqv_table = self._eqv_table return p1 in eqv_table and p2 in eqv_table[p1] @@ -114,7 +119,7 @@ class InstallabilityTester(object): """Add a binary package to "testing" If the package is not known, this method will throw an - Keyrror. + KeyError. """ t = (pkg_name, pkg_version, pkg_arch) @@ -126,6 +131,8 @@ class InstallabilityTester(object): self._testing.add(t) elif t not in self._testing: self._testing.add(t) + if self._cache_inst: + self._stats.cache_drops += 1 self._cache_inst = set() if self._cache_broken: # Re-add broken packages as some of them may now be installable @@ -164,6 +171,7 @@ class InstallabilityTester(object): if t not in self._broken and t in self._cache_inst: # It is in our cache (and not guaranteed to be broken) - throw out the cache self._cache_inst = set() + self._stats.cache_drops += 1 return True @@ -177,17 +185,21 @@ class InstallabilityTester(object): Returns False otherwise. """ + self._stats.is_installable_calls += 1 t = (pkg_name, pkg_version, pkg_arch) if t not in self._universe: raise KeyError(str(t)) if t not in self._testing or t in self._broken: + self._stats.cache_hits += 1 return False if t in self._cache_inst: + self._stats.cache_hits += 1 return True + self._stats.cache_misses += 1 return self._check_inst(t) @@ -195,8 +207,9 @@ class InstallabilityTester(object): # See the explanation of musts, never and choices below. cache_inst = self._cache_inst + stats = self._stats - if t in cache_inst and not never: + if musts and t in cache_inst and not never: # use the inst cache only for direct queries/simple queries. cache = True if choices: @@ -214,7 +227,6 @@ class InstallabilityTester(object): if cache: return True - universe = self._universe testing = self._testing cbroken = self._cache_broken @@ -255,16 +267,16 @@ class InstallabilityTester(object): # set conflicts with t - either way, t is f***ed cbroken.add(t) testing.remove(t) + stats.conflicts_essential += 1 return False musts.update(start) never.update(ess_never) # curry check_loop check_loop = partial(self._check_loop, universe, testing, - eqv_table, musts, never, choices, + eqv_table, stats, musts, never, choices, cbroken) - # Useful things to remember: # # * musts and never are disjointed at all times @@ -278,7 +290,7 @@ class InstallabilityTester(object): # # * check never includes choices (these are always in choices) # - # * A package is installable if never and musts are disjoined + # * A package is installable if never and musts are disjointed # and both check and choices are empty. # - exception: _pick_choice may determine the installability # of t via recursion (calls _check_inst). In this case @@ -318,6 +330,7 @@ class InstallabilityTester(object): if first: musts.add(first) check.add(first) + stats.choice_resolved_using_safe_set += 1 continue # None of the safe set choices are installable, so drop them remain -= safe_set @@ -326,11 +339,13 @@ class InstallabilityTester(object): # the choice was reduced to one package we haven't checked - check that check.update(remain) musts.update(remain) + stats.choice_presolved += 1 continue if not remain: # all alternatives would violate the conflicts or are uninstallable # => package is not installable + stats.choice_presolved += 1 return None # The choice is still deferred @@ -347,7 +362,7 @@ class InstallabilityTester(object): choices_tmp = set() check_tmp = set([p]) if not self._check_loop(universe, testing, eqv_table, - musts_copy, never_tmp, + stats, musts_copy, never_tmp, choices_tmp, cbroken, check_tmp): # p cannot be chosen/is broken (unlikely, but ...) @@ -364,6 +379,7 @@ class InstallabilityTester(object): # routine, but to conserve stack-space, we return # and expect to be called again later. musts.update(musts_copy) + stats.choice_resolved_without_restore_point += 1 return False if not musts.isdisjoint(never_tmp): @@ -371,6 +387,7 @@ class InstallabilityTester(object): # t uninstallable, so p is a no-go. continue + stats.backtrace_restore_point_created += 1 # We are not sure that p is safe, setup a backtrack # point and recurse. never_tmp |= never @@ -387,6 +404,7 @@ class InstallabilityTester(object): # to satisfy the dependencies, so pretend to conflict # with it - hopefully it will reduce future choices. never.add(p) + stats.backtrace_restore_point_used += 1 # Optimization for the last case; avoid the recursive call # and just assume the last will lead to a solution. If it @@ -394,6 +412,7 @@ class InstallabilityTester(object): # have to back-track anyway. check.add(last) musts.add(last) + stats.backtrace_last_option += 1 return False # END _pick_choice @@ -418,11 +437,13 @@ class InstallabilityTester(object): if verdict: # if t is installable, then so are all packages in musts self._cache_inst.update(musts) + stats.solved_installable += 1 + else: + stats.solved_uninstallable += 1 return verdict - - def _check_loop(self, universe, testing, eqv_table, musts, never, + def _check_loop(self, universe, testing, eqv_table, stats, musts, never, choices, cbroken, check, len=len, frozenset=frozenset): """Finds all guaranteed dependencies via "check". @@ -493,13 +514,19 @@ class InstallabilityTester(object): # _build_eqv_packages_table method for more # information on how this works. new_cand = set(x for x in candidates if x not in possible_eqv) + stats.eqv_table_times_used += 1 + for chosen in iter_except(possible_eqv.pop, KeyError): new_cand.add(chosen) possible_eqv -= eqv_table[chosen] + stats.eqv_table_total_number_of_alternatives_eliminated += len(candidates) - len(new_cand) if len(new_cand) == 1: check.update(new_cand) musts.update(new_cand) + stats.eqv_table_reduced_to_one += 1 continue + elif len(candidates) == len(new_cand): + stats.eqv_table_reduced_by_zero += 1 candidates = frozenset(new_cand) # defer this choice till later choices.add(candidates) @@ -514,6 +541,7 @@ class InstallabilityTester(object): eqv_table = self._eqv_table cbroken = self._cache_broken universe = self._universe + stats = self._stats safe_set = self._safe_set ess_base = set(x for x in self._essentials if x[2] == arch and x in testing) @@ -523,7 +551,7 @@ class InstallabilityTester(object): not_satisified = partial(filter, start.isdisjoint) while ess_base: - self._check_loop(universe, testing, eqv_table, + self._check_loop(universe, testing, eqv_table, stats, start, ess_never, ess_choices, cbroken, ess_base) if ess_choices: @@ -575,6 +603,39 @@ class InstallabilityTester(object): return graph_stats +class InstallabilityStats(object): + + def __init__(self): + self.cache_hits = 0 + self.cache_misses = 0 + self.cache_drops = 0 + self.backtrace_restore_point_created = 0 + self.backtrace_restore_point_used = 0 + self.backtrace_last_option = 0 + self.choice_presolved = 0 + self.choice_resolved_using_safe_set = 0 + self.choice_resolved_without_restore_point = 0 + self.is_installable_calls = 0 + self.solved_installable = 0 + self.solved_uninstallable = 0 + self.conflicts_essential = 0 + self.eqv_table_times_used = 0 + self.eqv_table_reduced_to_one = 0 + self.eqv_table_reduced_by_zero = 0 + self.eqv_table_total_number_of_alternatives_eliminated = 0 + + def stats(self): + formats = [ + "Requests - is_installable: {is_installable_calls}", + "Cache - hits: {cache_hits}, misses: {cache_misses}, drops: {cache_drops}", + "Choices - pre-solved: {choice_presolved}, safe-set: {choice_resolved_using_safe_set}, No RP: {choice_resolved_without_restore_point}", + "Backtrace - RP created: {backtrace_restore_point_created}, RP used: {backtrace_restore_point_used}, reached last option: {backtrace_last_option}", + "Solved - installable: {solved_installable}, uninstallable: {solved_uninstallable}, conflicts essential: {conflicts_essential}", + "Eqv - times used: {eqv_table_times_used}, perfect reductions: {eqv_table_reduced_to_one}, failed reductions: {eqv_table_reduced_by_zero}, total no. of alternatives pruned: {eqv_table_total_number_of_alternatives_eliminated}", + ] + return [x.format(**self.__dict__) for x in formats] + + class ArchStats(object): def __init__(self):