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
RSpec/SubjectStub. Refactor and decrease complexity #925
Conversation
find_subject_stub(node) do |stub| | ||
# skip already processed example group | ||
# it's processed if is nested in one of the processed example groups | ||
return unless (processed_example_groups & node.ancestors).empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How big are your specs so this makes a significant difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran Rubocop on its own source code so the test suite isn't large enough.
Regarding significance... This check avoids repeated analyzing of nested context
/describe
and I assume (I didn't test this TBH) it decrease the execution time by a factor of 2/3/4/... it depends on how many levels of nesting there are in the spec file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to avoid optimizations while sacrificing simplicity if we don't have enough proof that it gets us significant performance benefits. Also, long specs are hard to read and change. I'm not saying that we should deliberately make our tools slow to induce frustration when working with such specs, but primarily aiming to work with that kind of specs is not our aim.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the performance testing in the wild - you can pick some projects out of this list, I find https://github.com/discourse/discourse/ specifically good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checked this optimization on Gitlab source code and got the following results:
- it speeds up the cope from 13% to 5.8% of samples
- the whole execution time decreased by ~10s
Command to reproduce:
time bundle exec exe/rubocop --cache false --out gitlab.out --force-default-config --require rubocop-rspec --only RSpec/SubjectStub ../rubocop-profiling-examples/gitlabhq/spec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use on_top_level describe
and then search for example groups, in order to avoid double lookups. E.g. once you are in an example group, you can break from going to nested example groups,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Darhazer This is a truly amazing proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then it turns out that RuboCop::RSpec::TopLevelDescribe#on_send which triggers on_top_level_descrive, is one of the slowest methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, fair enough. Never looked closely at TopLevelDescribe
. Frankly, I'm having a hard time understanding why it's implemented that way. Should be: "call on_top_level_describe
if method name is :describe
, and there are no ancestors of the type :block
with a method name :describe
". It'd better keep a set of already detected TLDs to quickly return from on_send
if (node.ancestors & top_level_describes).any?
just like Andrew has implemented in this pull request.
Luckily, it doesn't fit our purposes in this pull request anyway, since it's only for describe
:
return false unless node.method_name == :describe
while we need something like TopLevelExampleGroup
, probably implemented in a way I suggest above, but focusing on all example group methods, not only :describe
.
Would you like to give this idea a shot in the scope of this pull request, or do you feel this improvement that you've done is good on its own already, @andrykonchin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love to optimize TopLevelDescribe
but think it deserves a separate PR.
Currently I am working on optimizing cops from the main rubocop
gem but will definitely look at rubocop-rspec
after that.
P.S.
But then it turns out that RuboCop::RSpec::TopLevelDescribe#on_send which triggers on_top_level_descrive, is one of the slowest methods
Switching from manual caching to #on_top_level_describe
callback decreased profiler samples for SubjectStub
cop from 0.9% to 0% so it isn't as bad as you may think 😉.
I guess if you rebase your code, the build will pass. Fix for those has been merged #914 |
373acbe
to
20e37fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I highly appreciate your effort of improving this cop, but please make correctness and simplicity your goals, not performance.
find_subject_stub(node) do |stub| | ||
# skip already processed example group | ||
# it's processed if is nested in one of the processed example groups | ||
return unless (processed_example_groups & node.ancestors).empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to avoid optimizations while sacrificing simplicity if we don't have enough proof that it gets us significant performance benefits. Also, long specs are hard to read and change. I'm not saying that we should deliberately make our tools slow to induce frustration when working with such specs, but primarily aiming to work with that kind of specs is not our aim.
Oh, sorry, I've completely missed your update in the PR description.
This is truly impressive 👍 |
…jects in example group
@pirj Fixed all the issues. Please let me know to squash all the "code review" commits. |
I have never thought we'll have to optimize anytime soon, but it is happening :D |
… #on_block callback
592f992
to
341ae44
Compare
Addressed the issue with |
But it only detects subject stubbing inside a |
node.each_descendant(:block).each_with_object({}) do |child, h| | ||
name = subject(child) | ||
if name | ||
h[child.parent.parent] ||= [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the meaning of parent.parent
? isn't it node.parent? while you are attaching to the parent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parent should be a block
node for outer describe
/context
/...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it work properly for contexts defined in iterators? (yes, another funny concept from my current project)
RSpec.describe do
%i[one two].each do |count|
context "for #{count}" do
specify do
expect(subject).to receive(count).and_return(0)
There's a parent block, but not an example group block.
I'm not sure this is the example to fail, hope you can figure out a better example to fail .parent.parent
.
node.each_child_node do |child| | ||
find_subject_expectation(child, subject_name, &block) | ||
def find_subject_expectations(node, subject_names = [], &block) | ||
if example_group?(node) && @explicit_subjects[node] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well I guess you can just check if there are explicit_subjects for this node, as if it is not an example group, there won't be any anyway. That could simplify the code
Maybe you got the definition of top-level describe wrong. Isn't your spec like:
As that is a top-level describe, and I never saw RSpec tests that begin directly with a context. If there are, other cops may not work as well |
@Darhazer Is there any similar helper to detect any top-level example group? Not only |
https://relishapp.com/rspec/rspec-core/docs/example-groups/basic-structure-describe - it surely hints that the basic structure is: RSpec.describe "something" do
context "in one context" do
it "does one thing" do
end
end
end But how about: RSpec.shared_examples_for "it sabotages SubjectStub" do
subject { haha }
specify do
expect(subject).to receive(:yes).and_return("no")
end
end We should detect this as well. What about
I've never seen it before as well. But it is how it is. |
My point is not to limit Here's the default list. https://github.com/rspec/rspec-core/blob/2b913e1e36904227e3ecc8b4af0b7c622d3cb50d/lib/rspec/core/example_group.rb#L280
For some of the cops, I don't see a good reason though to leave out those https://github.com/rspec/rspec-core/blob/ddbb43674be2a02c6cc839eba15f58265abcd8ca/lib/rspec/core/shared_example_group.rb#L100:
|
…arching of outer example group
d2430ae
to
956efea
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks rock-solid. No changes in detection on 3500 files repo (~600 offences).
Before: 120s
After: 6.5s
Bravo! 👏
Running |
Thanks a lot @andrykonchin ! |
I think this change warrants a mention in the Changelog. |
For his incredible work on rubocop#925
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
For his incredible work on rubocop/rubocop-rspec#925
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
For his incredible work on rubocop/rubocop-rspec#925
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
For his incredible work on rubocop/rubocop-rspec#925
For his incredible work on rubocop/rubocop-rspec#925
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
RSpec/SubjectStub
takes about 35% of all the time spent in cops if run Rubocop on Rubocop's own source code.PR addresses the performance problem mentioned in the rubocop/rubocop#8022 issue.
Profiler reports
Profiler report (
stackprof
) before optimizaition:Profiler report (
stackprof
) afeter optimizaition:Samples number decreased from 65.3% to 56.9% and the cop samples (relative to
RuboCop::Cop::Commissioner#trigger_responding_cops
method execution) decreased from 35.7% to 0.9%.Total execution time
Before:
After:
Total execution time decreased from 1:24.40 to 1:03.21.
Rubocop version
In performance tests was used the following Rubocop master commit:
Before submitting the PR make sure the following are checked:
master
(if not - rebase it).bundle exec rake
) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).