Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-enable all the indexes / specs #73

Merged
merged 5 commits into from
Jul 27, 2017
Merged

Conversation

greysteil
Copy link
Collaborator

@greysteil greysteil commented Jul 24, 2017

Gets Molinillo to the point where it can resolve anything that's thrown at it. Includes the following fixes and speedups (which will be pulled out into separate PRs when they're ready):

TODO:

  • Get specs passing in all known cases
    • Testing locally should get no errors when running the RandomSortIndex specs 1000x
  • Improve performance while continuing to track all past conflicts (spec slow down is most noticeable on ReverseBundlerIndex, which is the place to start analysis...)
  • Clean up circular dependency handling (current special-casing feels grim)
  • Document approach in ARCHITECTURE.md where appropriate (speedup is undoubtedly going to require some hard thinking about how we loop through conflicts)
  • Spec thorny cases (circular dependencies, multiple states for a single requirement, rewinding on previous conflicts)

@greysteil greysteil force-pushed the more-unwinding-improvements branch 2 times, most recently from ed8a9ca to 2ab704c Compare July 24, 2017 19:39
@@ -185,6 +195,9 @@ def process_topmost_state
create_conflict if state.is_a? PossibilityState
unwind_for_conflict until possibility && state.is_a?(DependencyState)
end
rescue CircularDependencyError => root_error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this new behavior will require a test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't pass on every index

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does now. Still don't love the way I've implemented rescuing from CircularDependency errors, though. I'll have a think about it once I get properly handling previous conflicts working fast.

@@ -440,7 +467,7 @@ def parent_of(requirement)
# @return [Object] the requirement that led to a version of a possibility
# with the given name being activated.
def requirement_for_existing_name(name)
return nil unless activated.vertex_named(name).payload
return nil unless activated.vertex_named(name) && activated.vertex_named(name).payload
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's store the vertex into a local to avoid the second lookup

@@ -391,6 +417,7 @@ def binding_requirements_for_conflict(conflict)
# then the only two requirements we need to consider are the initial one
# (where the dependency's version was first chosen) and the last
if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities)
return possible_binding_requirements if conflict.root_error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a comment on this would be 🚀

@@ -453,7 +480,7 @@ def find_state_for(requirement)

# @return [Conflict] a {Conflict} that reflects the failure to activate
# the {#possibility} in conjunction with the current {#state}
def create_conflict
def create_conflict(root_error = nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document the param

@greysteil greysteil force-pushed the more-unwinding-improvements branch from df3c750 to b2950e0 Compare July 25, 2017 19:39
@@ -241,82 +259,78 @@ def initial_state
# @return [void]
def unwind_for_conflict
details_for_unwind = build_details_for_unwind
tmp = state.unused_unwind_options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the state. is redundant

# for primary / parent elements of the tree?
alternative = unused_unwind_options.select do |option_set|
next false unless option_set.first > unwind_details.first
(option_set.map(&:requirement).flatten & unwind_details.map(&:requirement).flatten).any?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compatibility.flat_map


binding_requirements.reverse_each do |r|
unwind_details << UnwindDetails.new(-1, requirement_tree_for(r), nil, binding_requirements)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think calculating the full requirement tree is rather slow... is it really necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the approach I'm experimenting with it is. Might be able to speed things up by doing it lazily, but I'm not sure yet.

High level, the idea here is that all previous unwind options remain possible later, even if they're not selected at first. To make that feasible, we need to:

  1. Store them all (hence the removal of an early-exits from this method)
  2. Pick the relevant ones when rewinding from a future conflict

It's part (2) that needs the requirement trees - to know whether a previous, unused rewind would avoid the current conflict, we want to check whether there's any overlap between that previous conflict's requirement tree (the amalgamation of the requirement trees for all the requirements that caused it) and the current conflict's tree.

Part I'm hazy about is exactly how much filtering we can do on the past rewinds, but I'm more comfortable working back from the above, which I'm relatively certain is formally correct, than trying to get to correctness from an "efficient" solution.

🤞

end
grandparent_r = parent_of(grandparent_r)
end
end

unwind_details
unwind_details.sort.reverse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might it be faster to do unwind_details.sort { |l ,r| r <=> l } to avoid the intermediary array?

@greysteil greysteil force-pushed the more-unwinding-improvements branch 5 times, most recently from 46b50a8 to 6135941 Compare July 25, 2017 20:52
@@ -324,18 +327,23 @@ def build_details_for_unwind
# resolve the given conflict conflict
def unwind_details_for_requirements(binding_requirements)
unwind_details = []

full_requirement_tree = binding_requirements.map { |r| requirement_tree_for(r) }.flatten
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please either flat map or flatten a specific amount

@greysteil greysteil force-pushed the more-unwinding-improvements branch 3 times, most recently from 525f8c3 to 811cef5 Compare July 26, 2017 19:19
@@ -460,8 +461,12 @@ def filter_possibilities_for_primary_unwind(unwind_details)
# @param [UnwindDetails] details of the conflict just unwound from
# @return [void]
def filter_possibilities_for_parent_unwind(unwind_details)
unwinds = [unwind_details] + unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.select << unwind_details to save an array allocation

end
parent_r = grandparent_r
grandparent_r = parent_of(parent_r)
end
end

unwind_details
unwind_details.sort
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible this array won't be used, and we could maybe sort! it when first accessed?

next unless alternative > current_detail
intersecting_requirements =
alternative.requirement_trees.flatten(1) &
last_detail_for_current_unwind.requirement_trees.flatten(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be pulled out into a local, but it might be worth memoizing this in the details?

@greysteil greysteil force-pushed the more-unwinding-improvements branch 5 times, most recently from 9d67ace to c92a601 Compare July 27, 2017 10:24
@greysteil
Copy link
Collaborator Author

greysteil commented Jul 27, 2017

Crude benchmarking:

  • Running all specs on master 100 times: 329 seconds
  • This branch (with only masters' specs): 335 seconds

That should be well within any allowable margin of error.

🎉

Copy link
Member

@segiddins segiddins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so indescribably excited for this to land. Thank you so so much @greysteil, you've managed to push Molinillo forward when I thought I'd hit a wall with it.

intersecting_requirements =
last_detail_for_current_unwind.all_requirements &
alternative.requirements_unwound_to_instead
next unless intersecting_requirements.any?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next if empty?

# @param [Array] requirements an array of requirements
# @return [Boolean] whether the possibility satisfies all of the
# given requirements
def possibility_satisfies_requirements?(possibility, requirements)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YASSSS

# These indexes don't do a great job sorting, and segiddins has been
# unable to get the test passing with the bad sort without breaking
# other specs
# This index occassionally finds orders that are *incredibly* slow to resolver,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve

@@ -198,7 +198,7 @@ def versions_of(dependency_name)
BundlerIndex,
ReverseBundlerIndex,
BundlerSingleAllNoPenaltyIndex,
# RandomSortIndex,
RandomSortIndex,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

Previously, we kept a hash of previous conflicts in `DependencyState#conflicts`,
and considered unwinds from those previous considering when determining which
state to unwind to.

Unfortunately, there were a couple of problems with that approach:
- A maximum of one conflict per dependency was stored. When a conflict was
  "resolved" and a new version of that dependency was activated, the previous
  conflict was deleted. Doing so deleted unwind options that may have been used
  in the future.
- The `DependencyState#conflicts` method was also used when constructing error
  messages, so our ability to tweak it's behaviour was limited.
- Full requirement trees for the conflicting requirements, and the associated
  unwind options, weren't stored, and could not be recreated at a later date
  (since the DependencyGraph would have changed since the time when the conflict
  was created)

This PR introduces a new store on each `DependencyState` which contains details
of all previously unused unwinds. Those old, previously unused unwinds are then
considered when deciding whether to unwind from a new conflict. In theory, an
old unwind may allow us to avoid the conflict we've just encountered, if it
would affect the conflict's requirement trees in any way.
@greysteil greysteil force-pushed the more-unwinding-improvements branch from 8e0314c to 5a58e39 Compare July 27, 2017 16:00
@greysteil
Copy link
Collaborator Author

My pleasure @segiddins - it's an awesome project, and I've enjoyed every minute of working on it. (Well, maybe not all of the minutes banging my head against the keyboard...!)

@greysteil greysteil changed the title [WIP] Re-enable all the indexes / specs Re-enable all the indexes / specs Jul 27, 2017
@segiddins segiddins merged commit a5b9a73 into master Jul 27, 2017
@segiddins segiddins deleted the more-unwinding-improvements branch July 27, 2017 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants