-
Notifications
You must be signed in to change notification settings - Fork 13.4k
rustdoc: Add DocVisitor
and use it where possible
#90475
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
Conversation
Also, contrary to the comment, the clone is not that small, since `Variant` contains `Item`s, which are quite large when you factor in both stack- and heap-allocated memory.
This simplifies the code and future-proofs it against changes to `Variant`.
`DocFolder` allows transforming the docs, accomplished by making its methods take and return types by-value. However, several of the rustdoc `DocFolder` impls only *visit* the docs; they don't change anything. Passing around types by-value is thus unnecessary, confusing, and potentially inefficient for those impls. `DocVisitor` is very similar to `DocFolder`, except that its methods take shared references and return nothing (i.e., the unit type). This should both be more efficient and make the code clearer. There is an additional reason to add `DocVisitor`, too. As part of my cleanup of `external_traits`, I'm planning to add a `fn cache(&mut self) -> &mut Cache` method to `DocFolder` so that `external_traits` can be retrieved explicitly from the `Cache`, rather than implicitly via `Crate.external_traits` (which is an `Rc<RefCell<...>>`). However, some of the `DocFolder` impls that could be turned into `DocVisitor` impls only have a shared reference to the `Cache`, because they are used during rendering. (They have to access the `Cache` via `html::render::Context.shared.cache`, which involves an `Rc`.) Since `DocVisitor` does not mutate any of the types it's visiting, its equivalent `cache()` method will only need a shared reference to the `Cache`, avoiding the problem described above.
One of the FIXMEs is irrelevant since that code is only run if `include_sources` is set. I fixed the other FIXME.
* Flip conjuncts of `&&` in rustdoc The `CrateNum` comparison should be very cheap, while `span.filename()` fetches and clones a `FileName`. * Use `into_local_path()` instead of `local_path().clone()`
Many of `DocFolder`'s impls didn't actually transform the syntax tree, so they can be visitors instead.
I think these are the last of the impls that can be easily converted to visitors.
Until `external_traits` is cleaned up (i.e., no longer behind a `RefCell`), `DocVisitor` will have to `take` `external_traits` -- just like `DocFolder` -- to prevent `RefCell` runtime errors.
Some changes occurred in intra-doc-links. cc @jyn514 |
@bors try @rust-timer queue |
Awaiting bors try build completion. @rustbot label: +S-waiting-on-perf |
⌛ Trying commit c03cab3 with merge d9a63bc4a59bd86b29b0070be1dbfe3ace5c67b1... |
/// don't override! | ||
fn visit_item_recur(&mut self, item: &Item) { | ||
match &*item.kind { | ||
StrippedItem(i) => self.visit_inner_recur(i), |
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.
This should probably be moved into visit_inner_recur
instead of having the unreachable!
. (This is also the case in DocFolder
.)
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.
is there a reason for visit_inner_recur
to not be merged into visit_item_recur
?
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 don't know of one, but it could be some of the code relies on them being separate.
☀️ Try build successful - checks-actions |
Queued d9a63bc4a59bd86b29b0070be1dbfe3ace5c67b1 with parent db14a17, future comparison URL. |
Finished benchmarking commit (d9a63bc4a59bd86b29b0070be1dbfe3ace5c67b1): comparison url. Summary: This change led to small relevant mixed results 🤷 in compiler performance.
If you disagree with this performance assessment, please file an issue in rust-lang/rustc-perf. Benchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR led to changes in compiler perf. Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @bors rollup=never |
Hmm, not sure why there are regressions, but the improvements outweigh the regressions, and this is a good cleanup regardless. Perhaps I can look into if there's a way to avoid the regression; it's a bit surprising, but it could just be the optimizer behaving differently. |
@notriddle Guillaume is busy right now so can't do an in-depth review. Would you be interested in reviewing this? You don't have to do a full review, but I'd appreciate some feedback if you have time :) |
It looks like the largest regression (webrender) only regressed by 0.01 seconds of wall-clock time. |
let num_fields = j.fields.len(); | ||
j.fields = j.fields.into_iter().filter_map(|x| self.fold_item(x)).collect(); | ||
j.fields_stripped |= | ||
num_fields != j.fields.len() || j.fields.iter().any(|f| f.is_stripped()); |
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 know it was done this way before, but any idea why it isn't using a conditional, or a short-circuiting OR, for this?
if !j.fields_stripped {
j.fields_stripped =
num_fields != j.fields.len() || j.fields.iter().any(|f| f.is_stripped());
}
The length comparison probably isn't a problem, but unconditionally iterating over all of the fields seems like leaving performance on the table (or gambling on a compiler optimization pass that may or may not fire).
From a dive through Blame, this use of the |=
operator dates back to a version of the code that was only doing the length comparison:
d6d31d7#diff-3ba434e74829d360308543dff0f880edbd86bdc9fd90f025d990d91319b7002cR82
The code that started doing the any()
loop doesn't have any documentation that justifies why it isn't a perf problem, either. It looks like they just added the any()
loop without reconsidering the use of a non-short-circuiting OR.
b1543a1#diff-3ba434e74829d360308543dff0f880edbd86bdc9fd90f025d990d91319b7002cR48-R49
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.
Probably the reason it doesn't use a conditional is that no one thought of it ;)
It's a good idea; do you want to open a followup PR for it? Honestly, I think we should just remove the fields_stripped
fields altogether and compute this information on-demand, but that'll take some time.
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.
Sure, I'll do that.
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 assign me on it if you like.
impl DocVisitor for SourceCollector<'_, '_> { | ||
fn visit_item(&mut self, item: &clean::Item) { | ||
if !self.cx.include_sources { | ||
return; |
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.
This line of code cries out for a more explicit way to do control flow than this. This conditional seems to stop it from trying to recurse into child nodes, but it can't stop the parent from trying to visit all of the siblings (and that seems wasteful), and I really only figured that out by carefully reading the implementation.
The API used by TypeVisitor
seems like a good source for plagiarizing, since ControlFlow::Break
provides a way to "just bail out" like we want here.
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.
Yeah, ControlFlow
makes everything better 🙃
I'd be happy to try out switching DocVisitor
to use ControlFlow
, but that'd make this change even bigger, so I think let's save it for a future PR.
self.visit_item_recur(item) | ||
} | ||
|
||
/// don't override! |
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.
Like how TypeFoldable provides this method for types, could an extension trait be attached to ItemKind to take care of this?
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.
Hmm, could you elaborate? I'm not sure why that would be better. I'm generally not a fan of too many extension traits since they add complexity.
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.
If you don't like the idea, then it's fine, let's not do that. I brought it up because it would enforce the "don't override" rule instead of just making it a comment.
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 think I see what you mean. We could do this without an extension trait since ItemKind
is local. But I'm not sure if the extra complexity of not having all the visitor code in one place is worth it. Also, the current way DocVisitor
(and DocFolder
) works is consistent with similar visitors in rustc (like mir::Visitor
). But I'd still be open to us trying it.
@@ -16,23 +16,25 @@ use std::ffi::OsStr; | |||
use std::fs; | |||
use std::path::{Component, Path, PathBuf}; | |||
|
|||
crate fn render(cx: &mut Context<'_>, krate: clean::Crate) -> Result<clean::Crate, Error> { |
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.
👍 That's way cleaner. I like this change; the other comments really are just nitpicking, and the PR could easily be merged as-is.
@notriddle does that mean it's okay to And thanks for the review! ❤️ |
Yeah, that's what it means. |
Thanks again! @bors r=notriddle rollup=never |
📌 Commit 8e4bcdf has been approved by |
Marking the perf regression as triaged:
@rustbot label: +perf-regression-triaged |
☀️ Test successful - checks-actions |
Finished benchmarking commit (4ff9023): comparison url. Summary: This change led to moderate relevant mixed results 🤷 in compiler performance.
If you disagree with this performance assessment, please file an issue in rust-lang/rustc-perf. Next Steps: If you can justify the regressions found in this perf run, please indicate this with @rustbot label: +perf-regression |
…-conditional, r=camelid,GuillaumeGomez rustdoc: Use conditional for _stripped fold Followup: rust-lang#90475 (comment)
Going to mark this as triaged as the regressions are way smaller than the improvements and confined to the @rustbot label: +perf-regression-triaged |
I agree that this is really an improvement, but AFAICT |
DocFolder
allows transforming the docs, accomplished by making its methods take and return types by-value. However, several of the rustdocDocFolder
impls only visit the docs; they don't change anything. Passing around types by-value is thus unnecessary, confusing, and potentially inefficient for those impls.DocVisitor
is very similar toDocFolder
, except that its methods take shared references and return nothing (i.e., the unit type). This should both be more efficient and make the code clearer.There is an additional reason to add
DocVisitor
, too. As part of my cleanup ofexternal_traits
, I'm planning to add afn cache(&mut self) -> &mut Cache
method toDocFolder
so thatexternal_traits
can be retrieved explicitly from theCache
, rather than implicitly viaCrate.external_traits
(which is anRc<RefCell<...>>
). However, some of theDocFolder
impls that could be turned intoDocVisitor
impls only have a shared reference to theCache
, because they are used during rendering. (They have to access theCache
viahtml::render::Context.shared.cache
, which involves anRc
.)Since
DocVisitor
does not mutate any of the types it's visiting, its equivalentcache()
method will only need a shared reference to theCache
, avoiding the problem described above.r? @GuillaumeGomez
cc @jyn514