Skip to content

Commit c749ca9

Browse files
carlobaldassiKristofferC
authored andcommitted
Improve the local optimizer at the end of resolve (#2715)
* Improve the local optimizer at the end of resolve For each package, instead of trying to bump just one package, try to propagate the effect of the bump outward through the graph. Simple example: if package Av1 depends on Bv1 and Av2 depends on Bv2, and we are considering bumping Av1 to Av2, with the previous code we would just give up. With this change, we try to bump Bv1 to Bv2 and see if that works. Still todo: polishing, tests, improve logging (?) * Fix a bug, further improve the optimizer, add test Bugfix: in propagate_constraints, actually propagate the constraints throughout the reachable graph. Otherwise, inconsistent situations might arise (under peculiar circumstances). Optimizer improvements: allow the local optimizer to also install new packages, if this allows other packages to be bumped. The test is a reduced form that fails with the previous optimizer (even with the bug fix) and succeeds with the new one. (cherry picked from commit 0f66046)
1 parent 91e2c17 commit c749ca9

File tree

5 files changed

+1203
-49
lines changed

5 files changed

+1203
-49
lines changed

src/Resolve/Resolve.jl

Lines changed: 172 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -341,57 +341,18 @@ function verify_solution(sol::Vector{Int}, graph::Graph)
341341
return true
342342
end
343343

344+
344345
"""
345-
Push the given solution to a local optimium if needed: keeps increasing
346-
the states of the given solution as long as no constraints are violated.
347-
It also removes unnecessary parts of the solution which are unconnected
348-
to the required packages.
346+
Uninstall unreachable packages:
347+
start from the required ones and keep only the packages reachable from them along the graph.
349348
"""
350-
function enforce_optimality!(sol::Vector{Int}, graph::Graph)
349+
function _uninstall_unreachable!(sol::Vector{Int}, why::Vector{Union{Symbol,Int}}, graph::Graph)
351350
np = graph.np
352351
spp = graph.spp
353352
gadj = graph.gadj
354353
gmsk = graph.gmsk
355354
gconstr = graph.gconstr
356-
pkgs = graph.data.pkgs
357355

358-
# keep a track for the log
359-
why = Union{Symbol,Int}[0 for p0 = 1:np]
360-
361-
restart = true
362-
while restart
363-
restart = false
364-
for p0 = 1:np
365-
s0 = sol[p0]
366-
s0 == spp[p0] && (why[p0] = :uninst; continue) # the package is not installed
367-
368-
# check if bumping to the higher version would violate a constraint
369-
gconstr[p0][s0+1] || (why[p0] = :constr; continue)
370-
371-
# check if bumping to the higher version would violate a constraint
372-
viol = false
373-
for (j1,p1) in enumerate(gadj[p0])
374-
s1 = sol[p1]
375-
msk = gmsk[p0][j1]
376-
if !msk[s1, s0+1]
377-
viol = true
378-
why[p0] = p1
379-
break
380-
end
381-
end
382-
viol && continue
383-
384-
# So the solution is non-optimal: we bump it manually
385-
sol[p0] += 1
386-
restart = true
387-
end
388-
end
389-
390-
# Finally uninstall unneeded packages:
391-
# start from the required ones and keep only
392-
# the packages reachable from them along the graph.
393-
# (These should have been removed in the previous step, but in principle
394-
# an unconnected yet self-sustaining cycle may have survived.)
395356
uninst = trues(np)
396357
staged = Set{Int}(p0 for p0 = 1:np if !gconstr[p0][end])
397358
seen = copy(staged)
@@ -400,7 +361,7 @@ function enforce_optimality!(sol::Vector{Int}, graph::Graph)
400361
staged_next = Set{Int}()
401362
for p0 in staged
402363
s0 = sol[p0]
403-
@assert s0 < spp[p0]
364+
s0 == spp[p0] && continue
404365
uninst[p0] = false
405366
for (j1,p1) in enumerate(gadj[p0])
406367
gmsk[p0][j1][end,s0] && continue # the package is not required by p0 at version s0
@@ -415,6 +376,173 @@ function enforce_optimality!(sol::Vector{Int}, graph::Graph)
415376
sol[p0] = spp[p0]
416377
why[p0] = :uninst
417378
end
379+
end
380+
381+
"""
382+
Push the given solution to a local optimum if needed: keeps increasing
383+
the states of the given solution as long as no constraints are violated.
384+
It might also install additional packages, if needed to bump the ones already
385+
installed.
386+
It also removes unnecessary parts of the solution which are unconnected
387+
to the required packages.
388+
"""
389+
function enforce_optimality!(sol::Vector{Int}, graph::Graph)
390+
np = graph.np
391+
spp = graph.spp
392+
gadj = graph.gadj
393+
gmsk = graph.gmsk
394+
gconstr = graph.gconstr
395+
pkgs = graph.data.pkgs
396+
397+
# keep a track for the log
398+
why = Union{Symbol,Int}[0 for p0 = 1:np]
399+
400+
# Strategy:
401+
# There's a cycle in which first the unnecessary (unconnected) packages are removed,
402+
# then we make a pass over the whole packages trying to bump each of them.
403+
# We repeat the above two steps until no further action is allowed.
404+
# When attempting to bump a package, we may attempt to bump or install other packages
405+
# if needed. Except if the bump would uninstall a package, in which cases we don't
406+
# touch anything else: we do it only if it has no consequence at all. This strategy
407+
# favors installing packages as needed.
408+
# During the bumping pass, we keep an upper and lower bound for each package, which
409+
# progressively shrink. These are used when adjusting for the effect of an attempted bump.
410+
# The way it's written should ensure that no package is ever downgraded (unless it was
411+
# originally unneeded, and then got removed, and later reinstalled to a lower version as
412+
# a consequence of a bump of some other package).
413+
414+
# move_up is used to keep track of which packages can move up
415+
# (they start installed and can be bumped) and which down (they start uninstalled and
416+
# can be installed)
417+
move_up = BitVector(undef, length(sol))
418+
# lower and upper bounds on the valid range of each package
419+
upperbound = similar(spp)
420+
lowerbound = similar(spp)
421+
# backup space for restoring the state if an attempted bump fails
422+
bk_sol = similar(sol)
423+
bk_lowerbound = similar(lowerbound)
424+
bk_upperbound = similar(upperbound)
425+
426+
# auxiliary sets to perform breadth-first search on the graph
427+
seen = Set{Int}()
428+
staged = Set{Int}()
429+
staged_next = Set{Int}()
430+
431+
old_sol = similar(sol) # to detect if we made any changes
432+
allsols = Set{Vector{Int}}() # used to make 100% sure we avoid infinite loops
433+
434+
while true
435+
copy!(old_sol, sol)
436+
push!(allsols, copy(sol))
437+
438+
# step 1: uninstall unneded packages
439+
_uninstall_unreachable!(sol, why, graph)
440+
441+
# setp 2: try to bump each installed package in turn
442+
443+
move_up .= sol .≠ spp
444+
copy!(upperbound, spp)
445+
let move_up = move_up
446+
lowerbound .= [move_up[p0] ? sol[p0] : 1 for p0 = 1:np]
447+
end
448+
449+
for p0 = 1:np
450+
s0 = sol[p0]
451+
s0 == spp[p0] && (why[p0] = :uninst; continue) # the package is not installed
452+
move_up[p0] || continue # the package is only installed as a result of a previous bump, skip it
453+
454+
@assert upperbound[p0] == spp[p0]
455+
456+
# pick the next version that doesn't violate a constraint (if any)
457+
bump_range = collect(s0+1:spp[p0])
458+
bump = let gconstr = gconstr
459+
findfirst(v0->gconstr[p0][v0], bump_range)
460+
end
461+
462+
# no such version was found, skip this package
463+
bump nothing && (why[p0] = :constr; continue)
464+
465+
# assume that we will succeed in bumping the version (otherwise we
466+
# roll-back at the end)
467+
468+
new_s0 = bump_range[bump]
469+
try_uninstall = new_s0 == spp[p0] # are we trying to uninstall a package?
470+
471+
copy!(bk_sol, sol)
472+
copy!(bk_lowerbound, lowerbound)
473+
copy!(bk_upperbound, upperbound)
474+
sol[p0] = new_s0
475+
476+
# if we're trying to uninstall, the bump is "soft": we don't update the
477+
# lower bound so that the package can be reinstalled later in the pass
478+
# if needed by another package
479+
try_uninstall || (lowerbound[p0] = new_s0) # note that we're in the move_up case
480+
481+
empty!(seen)
482+
empty!(staged)
483+
empty!(staged_next)
484+
push!(staged, p0)
485+
486+
while !isempty(staged)
487+
for f0 in staged
488+
for (j1,f1) in enumerate(gadj[f0])
489+
f1 == p0 && continue
490+
f1 seen && continue
491+
s1 = sol[f1]
492+
msk = gmsk[f0][j1]
493+
if try_uninstall
494+
bump_range = [s1] # when uninstalling, no further changes are allowed
495+
else
496+
lb1 = lowerbound[f1]
497+
ub1 = upperbound[f1]
498+
@assert lb1 s1 ub1
499+
if move_up[f1]
500+
s1 > lb1 && @assert s1 == spp[f1]
501+
# the arrangement of the range gives precedence to improving the
502+
# current situation, but allows reinstalling a package if needed
503+
bump_range = vcat(s1:ub1, s1-1:-1:lb1)
504+
else
505+
bump_range = collect(ub1:-1:lb1)
506+
end
507+
end
508+
bump = let gconstr = gconstr
509+
findfirst(v1->(gconstr[f1][v1] && msk[v1, sol[f0]]), bump_range)
510+
end
511+
if bump nothing
512+
why[p0] = f1 # TODO: improve this? (ideally we might want the path from p0 to f1)
513+
@goto abort
514+
end
515+
new_s1 = bump_range[bump]
516+
sol[f1] = new_s1
517+
new_s1 == s1 && continue
518+
push!(staged_next, f1)
519+
if move_up[f1]
520+
lowerbound[f1] = new_s1
521+
else
522+
upperbound[f1] = new_s1
523+
end
524+
end
525+
end
526+
union!(seen, staged)
527+
staged, staged_next = staged_next, staged
528+
empty!(staged_next)
529+
end
530+
531+
# if we're here the bump was successful, there's nothing more to do
532+
continue
533+
534+
## abort the bumping: restore the solution
535+
@label abort
536+
537+
copy!(sol, bk_sol)
538+
copy!(lowerbound, bk_lowerbound)
539+
copy!(upperbound, bk_upperbound)
540+
end
541+
sol old_sol || break
542+
# It might be possible in principle to contrive a situation in which
543+
# the solutions oscillate
544+
sol allsols && break
545+
end
418546

419547
@assert verify_solution(sol, graph)
420548

src/Resolve/graphtype.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mutable struct ResolveLog
4444
pool::Dict{UUID,ResolveLogEntry}
4545

4646
# journal: record all messages in order (shared between all entries)
47-
journal::Vector{Tuple{UUID,String}}
47+
journal::ResolveJournal
4848

4949
# exact: keeps track of whether the resolve process is still exact, or
5050
# heuristics have been employed
@@ -1019,6 +1019,8 @@ function propagate_constraints!(graph::Graph, sources::Set{Int} = Set{Int}(); lo
10191019
Set{Int}(p0 for p0 = 1:np if !gconstr[p0][end]) :
10201020
sources
10211021

1022+
seen = copy(staged)
1023+
10221024
while !isempty(staged)
10231025
staged_next = Set{Int}()
10241026
for p0 in staged
@@ -1048,6 +1050,8 @@ function propagate_constraints!(graph::Graph, sources::Set{Int} = Set{Int}(); lo
10481050
if gconstr1 old_gconstr1
10491051
push!(staged_next, p1)
10501052
log_events && log_event_implicit_req!(graph, p1, added_constr1, p0)
1053+
elseif p1 seen
1054+
push!(staged_next, p1)
10511055
end
10521056
if !any(gconstr1)
10531057
if exact
@@ -1060,6 +1064,7 @@ function propagate_constraints!(graph::Graph, sources::Set{Int} = Set{Int}(); lo
10601064
end
10611065
end
10621066
end
1067+
union!(seen, staged_next)
10631068
staged = staged_next
10641069
end
10651070
return graph

test/resolve.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,11 +407,19 @@ end
407407

408408
@test sanity_tst(ResolveData.deps_data, ResolveData.problematic_data)
409409
@test resolve_tst(ResolveData.deps_data, ResolveData.reqs_data, ResolveData.want_data)
410+
411+
## DEPENDENCY SCHEME 12: A LARGER, MORE DIFFICULT REALISTIC EXAMPLE
412+
## ref Pkg.jl issue #1949
413+
414+
include("resolvedata2.jl")
415+
416+
@test sanity_tst(ResolveData2.deps_data, ResolveData2.problematic_data)
417+
@test resolve_tst(ResolveData2.deps_data, ResolveData2.reqs_data, ResolveData2.want_data)
410418
end
411419

412420
@testset "nasty" begin
413421
VERBOSE && @info("SCHEME NASTY")
414-
## DEPENDENCY SCHEME 12: A NASTY CASE
422+
## DEPENDENCY SCHEME 13: A NASTY CASE
415423

416424
include("NastyGenerator.jl")
417425
deps_data, reqs_data, want_data, problematic_data = NastyGenerator.generate_nasty(5, 20, q=20, d=4, sat = true)

test/resolve_utils.jl

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,11 @@ function graph_from_data(deps_data)
5252
end
5353
isempty(r) && continue
5454
rp = r[1]
55-
rvs = VersionSpec(r[2:end])
55+
rvs = VersionSpec(r[2:end]...)
5656
deps[p][vn][rp] = rvs
5757
end
5858
for (p,preq) in deps
5959
u = uuid(p)
60-
6160
deps_pkgs = Dict{String,Set{VersionNumber}}()
6261
for (vn,vreq) in deps[p], rp in keys(vreq)
6362
push!(get!(Set{VersionNumber}, deps_pkgs, rp), vn)
@@ -117,7 +116,26 @@ function resolve_tst(deps_data, reqs_data, want_data = nothing; clean_graph = fa
117116
add_reqs!(graph, reqs)
118117
simplify_graph!(graph, clean_graph = clean_graph)
119118
want = resolve(graph)
120-
return want == wantuuids(want_data)
119+
120+
id(u) = pkgID(u, graph)
121+
wd = wantuuids(want_data)
122+
if want wd
123+
for (u,vn) in want
124+
if u keys(wd)
125+
@info "resolver decided to install $(id(u)) (v$vn), package wasn't expected"
126+
elseif vn wd[u]
127+
@info "version mismatch for $(id(u)), resolver wants v$vn, expected v$(wd[u])"
128+
end
129+
end
130+
for (u,vn) in wd
131+
if u keys(want)
132+
@info "was expecting the resolver to install $(id(u)) (v$vn)"
133+
end
134+
end
135+
return false
136+
else
137+
return true
138+
end
121139
end
122140

123141
end

0 commit comments

Comments
 (0)