Implement caching for dependency expansion

This commit is contained in:
Bo Anderson 2021-03-19 03:21:27 +00:00
parent 3c3bf1c74d
commit e49a338896
No known key found for this signature in database
GPG Key ID: 3DB94E204E137D65
10 changed files with 79 additions and 25 deletions

View File

@ -0,0 +1,9 @@
# typed: strict
class Dependencies < SimpleDelegator
include Kernel
end
class Requirements < SimpleDelegator
include Kernel
end

View File

@ -35,15 +35,11 @@ module DependenciesHelpers
end end
def recursive_includes(klass, root_dependent, includes, ignores) def recursive_includes(klass, root_dependent, includes, ignores)
type = if klass == Dependency raise ArgumentError, "Invalid class argument: #{klass}" if klass != Dependency && klass != Requirement
:dependencies
elsif klass == Requirement
:requirements
else
raise ArgumentError, "Invalid class argument: #{klass}"
end
root_dependent.send("recursive_#{type}") do |dependent, dep| cache_key = "recursive_includes_#{includes}_#{ignores}"
klass.expand(root_dependent, cache_key: cache_key) do |dependent, dep|
if dep.recommended? if dep.recommended?
klass.prune if ignores.include?("recommended?") || dependent.build.without?(dep) klass.prune if ignores.include?("recommended?") || dependent.build.without?(dep)
elsif dep.optional? elsif dep.optional?
@ -57,7 +53,7 @@ module DependenciesHelpers
# If a tap isn't installed, we can't find the dependencies of one of # If a tap isn't installed, we can't find the dependencies of one of
# its formulae, and an exception will be thrown if we try. # its formulae, and an exception will be thrown if we try.
if type == :dependencies && if klass == Dependency &&
dep.is_a?(TapDependency) && dep.is_a?(TapDependency) &&
!dep.tap.installed? !dep.tap.installed?
Dependency.keep_but_prune_recursive_deps Dependency.keep_but_prune_recursive_deps

View File

@ -11,6 +11,7 @@ class Dependency
extend Forwardable extend Forwardable
include Dependable include Dependable
extend Cachable
attr_reader :name, :tags, :env_proc, :option_names attr_reader :name, :tags, :env_proc, :option_names
@ -87,12 +88,17 @@ class Dependency
# `[dependent, dep]` pairs to allow callers to apply arbitrary filters to # `[dependent, dep]` pairs to allow callers to apply arbitrary filters to
# the list. # the list.
# The default filter, which is applied when a block is not given, omits # The default filter, which is applied when a block is not given, omits
# optionals and recommendeds based on what the dependent has asked for. # optionals and recommendeds based on what the dependent has asked for
def expand(dependent, deps = dependent.deps, &block) def expand(dependent, deps = dependent.deps, cache_key: nil, &block)
# Keep track dependencies to avoid infinite cyclic dependency recursion. # Keep track dependencies to avoid infinite cyclic dependency recursion.
@expand_stack ||= [] @expand_stack ||= []
@expand_stack.push dependent.name @expand_stack.push dependent.name
if cache_key.present?
cache[cache_key] ||= {}
return cache[cache_key][dependent.full_name].dup if cache[cache_key][dependent.full_name]
end
expanded_deps = [] expanded_deps = []
deps.each do |dep| deps.each do |dep|
@ -104,18 +110,20 @@ class Dependency
when :skip when :skip
next if @expand_stack.include? dep.name next if @expand_stack.include? dep.name
expanded_deps.concat(expand(dep.to_formula, &block)) expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, &block))
when :keep_but_prune_recursive_deps when :keep_but_prune_recursive_deps
expanded_deps << dep expanded_deps << dep
else else
next if @expand_stack.include? dep.name next if @expand_stack.include? dep.name
expanded_deps.concat(expand(dep.to_formula, &block)) expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, &block))
expanded_deps << dep expanded_deps << dep
end end
end end
merge_repeats(expanded_deps) expanded_deps = merge_repeats(expanded_deps)
cache[cache_key][dependent.full_name] = expanded_deps.dup if cache_key.present?
expanded_deps
ensure ensure
@expand_stack.pop @expand_stack.pop
end end

View File

@ -310,6 +310,8 @@ module Homebrew
Formula.clear_cache Formula.clear_cache
Keg.clear_cache Keg.clear_cache
Tab.clear_cache Tab.clear_cache
Dependency.clear_cache
Requirement.clear_cache
tab = Tab.for_keg(keg) tab = Tab.for_keg(keg)
original_tab = tab.dup original_tab = tab.dup
tab.poured_from_bottle = false tab.poured_from_bottle = false

View File

@ -165,7 +165,7 @@ class Formula
# during the installation of a {Formula}. This is annoying but the result of # during the installation of a {Formula}. This is annoying but the result of
# state that we're trying to eliminate. # state that we're trying to eliminate.
# @return [BuildOptions] # @return [BuildOptions]
attr_accessor :build attr_reader :build
# Whether this formula should be considered outdated # Whether this formula should be considered outdated
# if the target of the alias it was installed with has since changed. # if the target of the alias it was installed with has since changed.
@ -222,10 +222,28 @@ class Formula
spec = send(spec_sym) spec = send(spec_sym)
raise FormulaSpecificationError, "#{spec_sym} spec is not available for #{full_name}" unless spec raise FormulaSpecificationError, "#{spec_sym} spec is not available for #{full_name}" unless spec
old_spec_sym = @active_spec_sym
@active_spec = spec @active_spec = spec
@active_spec_sym = spec_sym @active_spec_sym = spec_sym
validate_attributes! validate_attributes!
@build = active_spec.build @build = active_spec.build
return if spec_sym == old_spec_sym
Dependency.clear_cache
Requirement.clear_cache
end
# @private
def build=(build_options)
old_options = @build
@build = build_options
return if old_options.used_options == build_options.used_options &&
old_options.unused_options == build_options.unused_options
Dependency.clear_cache
Requirement.clear_cache
end end
private private
@ -1657,13 +1675,15 @@ class Formula
# means if a depends on b then b will be ordered before a in this list # means if a depends on b then b will be ordered before a in this list
# @private # @private
def recursive_dependencies(&block) def recursive_dependencies(&block)
Dependency.expand(self, &block) cache_key = "Formula#recursive_dependencies" unless block
Dependency.expand(self, cache_key: cache_key, &block)
end end
# The full set of Requirements for this formula's dependency tree. # The full set of Requirements for this formula's dependency tree.
# @private # @private
def recursive_requirements(&block) def recursive_requirements(&block)
Requirement.expand(self, &block) cache_key = "Formula#recursive_requirements" unless block
Requirement.expand(self, cache_key: cache_key, &block)
end end
# Returns a Keg for the opt_prefix or installed_prefix if they exist. # Returns a Keg for the opt_prefix or installed_prefix if they exist.

View File

@ -572,8 +572,8 @@ class FormulaInstaller
unsatisfied_reqs = Hash.new { |h, k| h[k] = [] } unsatisfied_reqs = Hash.new { |h, k| h[k] = [] }
req_deps = [] req_deps = []
formulae = [formula] formulae = [formula]
formula_deps_map = Dependency.expand(formula) formula_deps_map = formula.recursive_dependencies
.index_by(&:name) .index_by(&:name)
while (f = formulae.pop) while (f = formulae.pop)
runtime_requirements = runtime_requirements(f) runtime_requirements = runtime_requirements(f)

View File

@ -114,6 +114,12 @@ class Options
@options.to_a * other @options.to_a * other
end end
def ==(other)
instance_of?(other.class) &&
to_a == other.to_a
end
alias eql? ==
def empty? def empty?
@options.empty? @options.empty?
end end

View File

@ -15,6 +15,7 @@ class Requirement
extend T::Sig extend T::Sig
include Dependable include Dependable
extend Cachable
attr_reader :tags, :name, :cask, :download attr_reader :tags, :name, :cask, :download
@ -219,7 +220,12 @@ class Requirement
# the list. # the list.
# The default filter, which is applied when a block is not given, omits # The default filter, which is applied when a block is not given, omits
# optionals and recommendeds based on what the dependent has asked for. # optionals and recommendeds based on what the dependent has asked for.
def expand(dependent, &block) def expand(dependent, cache_key: nil, &block)
if cache_key.present?
cache[cache_key] ||= {}
return cache[cache_key][dependent.full_name].dup if cache[cache_key][dependent.full_name]
end
reqs = Requirements.new reqs = Requirements.new
formulae = dependent.recursive_dependencies.map(&:to_formula) formulae = dependent.recursive_dependencies.map(&:to_formula)
@ -233,6 +239,7 @@ class Requirement
end end
end end
cache[cache_key][dependent.full_name] = reqs.dup if cache_key.present?
reqs reqs
end end

View File

@ -166,8 +166,7 @@ describe Formula do
end end
build_values_with_no_installed_alias = [ build_values_with_no_installed_alias = [
nil, BuildOptions.new(Options.new, f.options),
BuildOptions.new({}, {}),
Tab.new(source: { "path" => f.path.to_s }), Tab.new(source: { "path" => f.path.to_s }),
] ]
build_values_with_no_installed_alias.each do |build| build_values_with_no_installed_alias.each do |build|
@ -201,7 +200,10 @@ describe Formula do
url "foo-1.0" url "foo-1.0"
end end
build_values_with_no_installed_alias = [nil, BuildOptions.new({}, {}), Tab.new(source: { "path" => f.path })] build_values_with_no_installed_alias = [
BuildOptions.new(Options.new, f.options),
Tab.new(source: { "path" => f.path }),
]
build_values_with_no_installed_alias.each do |build| build_values_with_no_installed_alias.each do |build|
f.build = build f.build = build
expect(f.installed_alias_path).to be nil expect(f.installed_alias_path).to be nil
@ -405,7 +407,7 @@ describe Formula do
f = formula alias_path: alias_path do f = formula alias_path: alias_path do
url "foo-1.0" url "foo-1.0"
end end
f.build = BuildOptions.new({}, {}) f.build = BuildOptions.new(Options.new, f.options)
expect(f.alias_path).to eq(alias_path) expect(f.alias_path).to eq(alias_path)
expect(f.installed_alias_path).to be nil expect(f.installed_alias_path).to be nil
@ -802,7 +804,7 @@ describe Formula do
expect(Set.new(f1.recursive_requirements)).to eq(Set[]) expect(Set.new(f1.recursive_requirements)).to eq(Set[])
f1.build = BuildOptions.new(["--with-xcode"], f1.options) f1.build = BuildOptions.new(Options.create(["--with-xcode"]), f1.options)
expect(Set.new(f1.recursive_requirements)).to eq(Set[xcode]) expect(Set.new(f1.recursive_requirements)).to eq(Set[xcode])

View File

@ -185,6 +185,8 @@ RSpec.configure do |config|
Formula.clear_cache Formula.clear_cache
Keg.clear_cache Keg.clear_cache
Tab.clear_cache Tab.clear_cache
Dependency.clear_cache
Requirement.clear_cache
FormulaInstaller.clear_attempted FormulaInstaller.clear_attempted
FormulaInstaller.clear_installed FormulaInstaller.clear_installed
@ -229,6 +231,8 @@ RSpec.configure do |config|
Formula.clear_cache Formula.clear_cache
Keg.clear_cache Keg.clear_cache
Tab.clear_cache Tab.clear_cache
Dependency.clear_cache
Requirement.clear_cache
FileUtils.rm_rf [ FileUtils.rm_rf [
*TEST_DIRECTORIES, *TEST_DIRECTORIES,