Skip to content

Commit a4f5973

Browse files
committed
Cache package version selection:
- ### Problem Selecting a version for a given package is a extremelly hot path. In a very large gemfile and a deep dependency tree (like the one we have in our monolith at Shopify), this codepath is hit around 3.2 million times in total during the resolution phase. ### Context When the resolution starts, Bundler fetch and turn every possible versions of a gem that was ever released on Rubygems.org into a possible candidate. We end up with a massive matrix of possibilites that PubGrub has to go through. In the case of a large Gemfile like we have, we end up with ~55,000 candidates. Many of this candidate have conflicting dependencies requirements and as pubgrub progress, it will continously ask over and over the same things: "Return the possible candidates given this version constraint" (`range.select_versions(@sorted_versions[package])`). Since this path is called so frequently (sometimes more than 8000 times for a single candidate and the same constraint), the returned value can be cached for faster access. ### Solution Cache the selected versions for a given constraint in a hash. The key being an array where the first element is the package we want to resolve and the second element is the constraint. The associated value is all possible candidates matching. If the resolver end up not finding a candidate (in example you run `bundle install --prefer-local`) then Bundler will allow finding candidates on the remote. In this case we need to invalidate the cache as otherwise the candidates from the remotes will not be considered. ### Benchmark This change has a huge impact on resolution time. Those measures were taken on Shopify monolith by removing the lockfile and measuring only resolution time (no network or external factors affect these results.) ┌─────┬──────────┬───────────┬─────────┐ │ Run │ Original │ Optimized │ Speedup │ ├─────┼──────────┼───────────┼─────────┤ │ 1 │ 19.20s │ 10.32s │ 46.3% │ ├─────┼──────────┼───────────┼─────────┤ │ 2 │ 19.17s │ 10.46s │ 45.4% │ ├─────┼──────────┼───────────┼─────────┤ │ 3 │ 18.95s │ 10.29s │ 45.7% │ ├─────┼──────────┼───────────┼─────────┤ │ 4 │ 19.17s │ 10.37s │ 45.9% │ ├─────┼──────────┼───────────┼─────────┤ │ 5 │ 19.26s │ 10.30s │ 46.5% │ ├─────┼──────────┼───────────┼─────────┤ │ Avg │ 19.15s │ 10.35s │ 46.0% │ └─────┴──────────┴───────────┴─────────┘
1 parent edf0012 commit a4f5973

1 file changed

Lines changed: 6 additions & 3 deletions

File tree

bundler/lib/bundler/resolver/strategy.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class Resolver
55
class Strategy
66
def initialize(source)
77
@source = source
8+
@package_priority_cache = {}
89
end
910

1011
def next_package_and_version(unsatisfied)
@@ -17,10 +18,12 @@ def next_package_and_version(unsatisfied)
1718

1819
def next_term_to_try_from(unsatisfied)
1920
unsatisfied.min_by do |package, range|
20-
matching_versions = @source.versions_for(package, range)
21-
higher_versions = @source.versions_for(package, range.upper_invert)
21+
@package_priority_cache[[package, range]] ||= begin
22+
matching_versions = @source.versions_for(package, range)
23+
higher_versions = @source.versions_for(package, range.upper_invert)
2224

23-
[matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
25+
[matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
26+
end
2427
end
2528
end
2629

0 commit comments

Comments
 (0)