Skip to content

[Perf] Optimize documentation lints **a lot** (1/2) (18% -> 10%) #14693

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

Merged
merged 2 commits into from
May 21, 2025

Conversation

blyxyas
Copy link
Member

@blyxyas blyxyas commented Apr 25, 2025

Turns out that doc_markdown uses a non-cheap rustdoc function to convert from markdown ranges into source spans. And it was using it a lot (about once every 17 lines of documentation on tokio, which ends up being about 2000 times).

This ended up being about 18% of the total Clippy runtime as discovered by lintcheck --perf in docs-heavy crates. This PR optimizes one of the cases in which Clippy calls the function, and a future PR once pulldown-cmark/pulldown-cmark#1034 is merged will be opened. This PR lands the use of the function into the single-digit zone.

Note that not all crates were affected by this crate equally, those with more docs are affected far more than those light ones.

changelog:[clippy::doc_markdown] has been optimized by 50%

@blyxyas blyxyas added the performance-project For issues and PRs related to the Clippy Performance Project label Apr 25, 2025
@rustbot
Copy link
Collaborator

rustbot commented Apr 25, 2025

r? @Jarcho

rustbot has assigned @Jarcho.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Apr 25, 2025
@rustbot

This comment has been minimized.

Turns out that `doc_markdown` uses a non-cheap rustdoc function
to convert from markdown ranges into source spans. And it was using it
a lot (about once every 18 lines of documentation on `tokio`, which ends
up being about 1800 times).

This ended up being about 18% of the total Clippy runtime as discovered by
lintcheck --perf in docs-heavy crates. This PR optimizes one of the cases
in which Clippy calls the function, and a future PR once
pulldown-cmark/pulldown-cmark issue number 1034 is merged will be open.

Note that not all crates were affected by this crate equally, those with
more docs are affected far more than those light ones.
@blyxyas blyxyas force-pushed the optimize-doc-lints branch from fe7ec9b to 565cf5a Compare April 25, 2025 19:15
Comment on lines 137 to 147
let Some(fragment_span) = fragments.span(cx, range.clone()) else {
return ControlFlow::Break(());
};

let span = Span::new(
fragment_span.lo() + BytePos::from_usize(fragment_offset),
fragment_span.lo() + BytePos::from_usize(fragment_offset + word.len()),
fragment_span.ctxt(),
fragment_span.parent(),
);

Copy link
Contributor

Choose a reason for hiding this comment

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

Should you not be adjusting the range before creating the span? fragment_offset looks like it's an offset in the markdown text.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure if I understand this comment correctly. This snippet is taken as-is from check with variable names fixed, check->offset didn't really care about the markdown text.

Copy link
Contributor

@Jarcho Jarcho May 10, 2025

Choose a reason for hiding this comment

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

fragment_offset looks like it's an offset in the cooked doc string. It can't be used as an offset for a span since that doesn't always line up perfectly with the source text.

Copy link
Member Author

Choose a reason for hiding this comment

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

After testing this out, text_to_check only contains text, it doesn't contain links, or bold text, etc. And fragment_offset is resetted for each one of those texts. I can add a debug assertion for future proofing this though.

Copy link
Contributor

Choose a reason for hiding this comment

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

A text fragment can still contain escape sequences e.g. #[doc = "docs with unicode \u{xxxxxx}"]. The string the fragments work on is the cooked version of the doc string, not the source form. Multiline comments (/** */) might also have issues, don't know how the those are presented.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just tested this, #[doc = "*"] does not lint, at all. Even for the test cases documented, if you change the /// * for #[doc = "*"] they will just not lint.

For the case of /** XXX */, seems that everything works correctly (or that I'm testing for the wrong thing), either way, I've added some tests for this along with some other weird escaped sequences.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't really want to stall the PR on this and it's not a new thing added. I'll try to make it break later.

@@ -117,6 +134,17 @@ fn check_word(cx: &LateContext<'_>, word: &str, span: Span, code_level: isize, b
// try to get around the fact that `foo::bar` parses as a valid URL
&& !url.cannot_be_a_base()
{
let Some(fragment_span) = fragments.span(cx, range.clone()) else {
return ControlFlow::Break(());
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems wrong. One spot failing to get a span doesn't mean all the others will.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed!

Comment on lines 168 to 177
let Some(fragment_span) = fragments.span(cx, range.clone()) else {
return ControlFlow::Break(());
};

let span = Span::new(
fragment_span.lo() + BytePos::from_usize(fragment_offset),
fragment_span.lo() + BytePos::from_usize(fragment_offset + word.len()),
fragment_span.ctxt(),
fragment_span.parent(),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as the previous two comments.

@blyxyas blyxyas force-pushed the optimize-doc-lints branch from 4a99a06 to acff5d3 Compare May 21, 2025 21:44
Copy link
Contributor

@Jarcho Jarcho left a comment

Choose a reason for hiding this comment

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

Thank you.

@Jarcho Jarcho added this pull request to the merge queue May 21, 2025
Merged via the queue into rust-lang:master with commit a6e40fa May 21, 2025
11 checks passed
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 22, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

@rustbot label +performance-project

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 22, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

@rustbot label +performance-project

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 22, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 22, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates

Co-authored-by: Roope Salmi <[email protected]>
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 23, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates

Co-authored-by: Roope Salmi <[email protected]>
blyxyas added a commit to blyxyas/rust-clippy that referenced this pull request May 23, 2025
So, after rust-lang#14693 was merged,
this is the continuation. It performs some optimizations on `Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease (7.51% -> 10.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% -> 0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

This means that currently our heaviest function is `rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates

Co-authored-by: Roope Salmi <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request May 24, 2025
So, after #14693 was
merged,
this is the continuation. It performs some optimizations on
`Fragments::span`
, makes it so we don't call it so much, and makes a 85.75% decrease
(7.51% -> 1.07%)
in execution samples of `source_span_for_markdown_range` and a 6.39% ->
0.88%
for `core::StrSearcher::new`. Overall a 13.11% icount decrase on
docs-heavy crates.

Benchmarked mainly on `regex-1.10.5`.

@rustbot label +performance-project

This means that currently our heaviest function is
`rustc_middle::Interners::intern_ty`, even
for documentation-heavy crates

Along with #14693, this makes the lint a 7% of what
it was before and makes it so that even in the most doc-heavy of crates
it's not an issue.

changelog:Optimize documentation lints by a further 85%

r? @Jarcho
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance-project For issues and PRs related to the Clippy Performance Project S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants