Merge ~juliank/ubuntu-release-upgrader:improve-obsoletes into ubuntu-release-upgrader:ubuntu/main

Proposed by Julian Andres Klode
Status: Needs review
Proposed branch: ~juliank/ubuntu-release-upgrader:improve-obsoletes
Merge into: ubuntu-release-upgrader:ubuntu/main
Diff against target: 110 lines (+53/-25)
2 files modified
DistUpgrade/DistUpgradeCache.py (+18/-19)
DistUpgrade/DistUpgradeController.py (+35/-6)
Reviewer Review Type Date Requested Status
Nick Rosbrook Pending
Review via email: mp+465148@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Julian Andres Klode (juliank) wrote :

I think we likely want to ship this _after_ the current fixes in ubuntu/main have landed in -updates, as this is a bigger change for performance, but ymmv.

Unmerged commits

ae0c35f... by Julian Andres Klode

Reimplement obsolete removal with resolve_by_keep

Basically the idea is that we pass auto_fix=False to the cache
and then use resolve_by_keep() to revert any removals that cause
additional removals.

This is easier than it sounds however, as we need to pay special
attention to forced_obsoletes. As can be seen in the code, forced
obsoletes also remove additional dependencies (they do not check
the changes made by mark_delete() and revert it).

So we solve this with a two stage approach:

1. We mark the forced obsoletes for removal with auto_fix turned
   on. This will keep the behavior for them the same. If one of
   them can't be removed, we end up reverting it.

   For this, we need to keep the code to create and restore the
   snapshot.

2. We mark the other packages for removal without auto_fix and
   then call ProblemResolver.resolve_by_keep() to fix any broken
   packages by keeping them back.

This brings runtime from multiple seconds down to half a second
for a bare lxd container:

2024-04-29 10:34:35,549 DEBUG Start checking for obsolete pkgs
2024-04-29 10:34:35,912 DEBUG Finish checking for obsolete pkgs

It used to be over 11s before:

2024-04-29 10:28:59,382 DEBUG Start checking for obsolete pkgs
2024-04-29 10:29:11,855 DEBUG Finish checking for obsolete pkgs

LP: #1874272

11047fa... by Julian Andres Klode

Move the actiongroup out of the obsoletes loop

This improves performance as we do not need to check for autoremovable
packages after each removal.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/DistUpgrade/DistUpgradeCache.py b/DistUpgrade/DistUpgradeCache.py
2index 0692ef9..77ffb64 100644
3--- a/DistUpgrade/DistUpgradeCache.py
4+++ b/DistUpgrade/DistUpgradeCache.py
5@@ -940,7 +940,7 @@ class MyCache(apt.Cache):
6 return False
7
8 @withResolverLog
9- def tryMarkObsoleteForRemoval(self, pkgname, remove_candidates, forced_obsoletes, foreign_pkgs):
10+ def tryMarkObsoleteForRemoval(self, pkgname, remove_candidates, forced_obsoletes, foreign_pkgs, auto_fix):
11 #logging.debug("tryMarkObsoleteForRemoval(): %s" % pkgname)
12 # coherence check, first see if it looks like a running kernel pkg
13 if pkgname.endswith(self.uname):
14@@ -972,28 +972,27 @@ class MyCache(apt.Cache):
15 # if this package has not been forced obsolete, only
16 # delete it if it doesn't remove other dependents
17 # that are not obsolete as well
18- actiongroup = apt_pkg.ActionGroup(self._depcache)
19- # just make pyflakes shut up, later we should use
20- # with self.actiongroup():
21- actiongroup
22- self.create_snapshot()
23+ if auto_fix:
24+ self.create_snapshot()
25 try:
26- self[pkgname].mark_delete(purge=purge)
27+ self[pkgname].mark_delete(purge=purge, auto_fix=auto_fix)
28 self.view.processEvents()
29- if pkgname in forced_obsoletes:
30- return True
31- #logging.debug("marking '%s' for removal" % pkgname)
32- for pkg in self.get_changes():
33- if (pkg.name not in remove_candidates or
34- pkg.name in foreign_pkgs or
35- self._inRemovalDenylist(pkg.name) or
36- pkg.name == self.linux_metapackage):
37- logging.debug("package '%s' produces an unwanted removal '%s', skipping" % (pkgname, pkg.name))
38- self.restore_snapshot()
39- return False
40+ if auto_fix:
41+ if pkgname in forced_obsoletes:
42+ return True
43+ #logging.debug("marking '%s' for removal" % pkgname)
44+ for pkg in self.get_changes():
45+ if (pkg.name not in remove_candidates or
46+ pkg.name in foreign_pkgs or
47+ self._inRemovalDenylist(pkg.name) or
48+ pkg.name == self.linux_metapackage):
49+ logging.debug("package '%s' produces an unwanted removal '%s', skipping" % (pkgname, pkg.name))
50+ self.restore_snapshot()
51+ return False
52 except (SystemError, KeyError) as e:
53 logging.warning("_tryMarkObsoleteForRemoval failed for '%s' (%s: %s)" % (pkgname, repr(e), e))
54- self.restore_snapshot()
55+ if auto_fix:
56+ self.restore_snapshot()
57 return False
58 return True
59
60diff --git a/DistUpgrade/DistUpgradeController.py b/DistUpgrade/DistUpgradeController.py
61index eebe281..f7853ae 100644
62--- a/DistUpgrade/DistUpgradeController.py
63+++ b/DistUpgrade/DistUpgradeController.py
64@@ -1995,12 +1995,41 @@ class DistUpgradeController(object):
65 logging.debug("remove_candidates: '%s'" % remove_candidates)
66 logging.debug("Start checking for obsolete pkgs")
67 progress = self._view.getOpCacheProgress()
68- for (i, pkgname) in enumerate(remove_candidates):
69- progress.update((i/float(len(remove_candidates)))*100.0)
70- if pkgname not in self.foreign_pkgs:
71- self._view.processEvents()
72- if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, self.foreign_pkgs):
73- logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
74+ scheduled_remove = set()
75+
76+ with self.cache.actiongroup():
77+ # Forced obsoletes we remove, removing any of their dependencies, hence do a first loop with auto_fix=True
78+ for (i, pkgname) in enumerate(remove_candidates):
79+ progress.update((i/float(len(remove_candidates)))*100.0 / 2)
80+ if pkgname in self.foreign_pkgs and pkgname in self.force_obsoletes:
81+ self._view.processEvents()
82+ if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, self.foreign_pkgs, auto_fix=True):
83+ logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
84+ else:
85+ scheduled_remove.add(pkgname)
86+
87+ # Now let's try to remove other packages
88+ for (i, pkgname) in enumerate(remove_candidates):
89+ progress.update((i/float(len(remove_candidates)))*100.0 / 2 + 50)
90+ if pkgname not in self.foreign_pkgs:
91+ self._view.processEvents()
92+ if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, self.foreign_pkgs, auto_fix=False):
93+ logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
94+ else:
95+ scheduled_remove.add(pkgname)
96+
97+ # We have scheduled their removals, but not any reverse-dependencies. If anything is broken now,
98+ # resolve them by keeping back the obsolete packages.
99+ self.cache._startAptResolverLog()
100+ pr = apt.ProblemResolver(self.cache)
101+ pr.resolve_by_keep()
102+ self.cache._stopAptResolverLog()
103+
104+ # resolve_by_keep() will revert any unsafe removals, so we need to list them here again.
105+ for pkgname in scheduled_remove:
106+ if not self.cache[pkgname].marked_delete:
107+ logging.debug("obsolete package '%s' could not be removed", pkgname)
108+
109 logging.debug("Finish checking for obsolete pkgs")
110 progress.done()
111

Subscribers

People subscribed via source and target branches