Skip to content

Conversation

@lunacookies
Copy link
Contributor

Closes #8943.

@Veykril
Copy link
Member

Veykril commented May 27, 2021

@Veykril
Copy link
Member

Veykril commented May 27, 2021

CI tests are complaining about non-linearity here(all three platforms) but I don't see where this change would be non-linear? 🤔

@lunacookies
Copy link
Contributor Author

Can you also add the modifier and a short description to the list here?

Done!

CI tests are complaining about non-linearity here(all three platforms) but I don't see where this change would be non-linear? 🤔

Any ideas how to fix this? Would it be worth rerunning to make sure it wasn’t a fluke? Then again, it isn’t good to have tests that fail on a whim.

@lnicola
Copy link
Member

lnicola commented May 28, 2021

Looks linear enough to me.

@lnicola
Copy link
Member

lnicola commented May 28, 2021

bors try

bors bot added a commit that referenced this pull request May 28, 2021
@bors
Copy link
Contributor

bors bot commented May 28, 2021

try

Build failed:

@lnicola
Copy link
Member

lnicola commented May 28, 2021

bors try

bors bot added a commit that referenced this pull request May 28, 2021
@bors
Copy link
Contributor

bors bot commented May 28, 2021

try

Build failed:

@matklad
Copy link
Contributor

matklad commented May 29, 2021

Looks rather quadratic here:

plot

diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs
index bb68181fd..4edda0501 100644
--- a/crates/ide/src/syntax_highlighting/tests.rs
+++ b/crates/ide/src/syntax_highlighting/tests.rs
@@ -327,32 +327,33 @@ fn benchmark_syntax_highlighting_long_struct() {
 
 #[test]
 fn syntax_highlighting_not_quadratic() {
-    if skip_slow_tests() {
-        return;
-    }
+    // if skip_slow_tests() {
+    //     return;
+    // }
 
     let mut al = AssertLinear::default();
-    while al.next_round() {
-        for i in 6..=10 {
-            let n = 1 << i;
+    // while al.next_round() {
+    for i in 1..=45 {
+        let n = i * 100;
 
-            let fixture = bench_fixture::big_struct_n(n);
-            let (analysis, file_id) = fixture::file(&fixture);
+        let fixture = bench_fixture::big_struct_n(n);
+        let (analysis, file_id) = fixture::file(&fixture);
 
-            let time = Instant::now();
+        let time = Instant::now();
 
-            let hash = analysis
-                .highlight(file_id)
-                .unwrap()
-                .iter()
-                .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
-                .count();
-            assert!(hash > n as usize);
+        let hash = analysis
+            .highlight(file_id)
+            .unwrap()
+            .iter()
+            .filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
+            .count();
+        assert!(hash > n as usize);
+        println!("P! {:>4?} {:?}", n, time.elapsed().as_millis());
 
-            let elapsed = time.elapsed();
-            al.sample(n as f64, elapsed.as_millis() as f64);
-        }
+        let elapsed = time.elapsed();
+        // al.sample(n as f64, elapsed.as_millis() as f64);
     }
+    // }
 }
 
 #[test]

@lunacookies
Copy link
Contributor Author

Thanks for making the effort to create that graph! Would you have any idea what about this change makes highlighting quadratic?

@matklad
Copy link
Contributor

matklad commented May 31, 2021

Huh, looks like we are also quadratic on master...

@matklad
Copy link
Contributor

matklad commented May 31, 2021

Marking as broken window:

  • we should fix the fact that we are quadratic
  • we should fix our is quadratic detector

Not sure if I'll be able to look into this myself today though

@matklad matklad added the Broken Window Bugs / technical debt to be addressed immediately label May 31, 2021
@iDawer
Copy link
Contributor

iDawer commented May 31, 2021

I suppose this one
https://github.com/rust-analyzer/rust-analyzer/blob/3cb3f1d17b54a7e144db495e073551049c9c482c/crates/hir_def/src/adt.rs#L227-L229
along with field traverse causes such behavior:
Used in SourceAnalyzer::resolve_record_field https://github.com/rust-analyzer/rust-analyzer/blob/3cb3f1d17b54a7e144db495e073551049c9c482c/crates/hir/src/source_analyzer.rs#L183-L188

Caller hierarchy leads to higlight::traverse which is a huge loop over all the fields
https://github.com/rust-analyzer/rust-analyzer/blob/3cb3f1d17b54a7e144db495e073551049c9c482c/crates/ide/src/syntax_highlighting.rs#L306-L312

matklad added a commit to matklad/rust-analyzer that referenced this pull request Jun 21, 2021
This story begins in rust-lang#8384, where we added a smart test for our syntax
highting, which run the algorithm on synthetic files of varying length
in order to guesstimate if the complexity is O(N^2) or O(N)-ish.

The test turned out to be pretty effective, and flagged rust-lang#9031 as a
change that makes syntax highlighting accidentally quadratic. There was
much rejoicing, for the time being.

Then, lnicola asked an ominous question[1]: "Are we sure that the time
is linear right now?"

Of course it turned out that our sophisticated non-linearity detector
*was* broken, and that our syntax highlighting *was* quadratic.

Investigating that, many brave hearts dug deeper and deeper into the
guts of rust-analyzer, only to get lost in a maze of traits delegating
to traits delegating to macros.

Eventually, matklad managed to peel off all layers of abstraction one by
one, until almost nothing was left. In fact, the issue was discovered in
the very foundation of the rust-analyzer -- in the syntax trees.

Worse, it was not a new problem, but rather a well-know, well-understood
and event (almost) well-fixed (!) performance bug.

The problem lies within `SyntaxNodePtr` type -- a light-weight "address"
of a node in a syntax tree [3]. Such pointers are used by rust-analyzer all
other the place to record relationships between IR nodes and the
original syntax.

Internally, the pointer to a syntax node is represented by node's range.
To "dereference" the pointer, you traverse the syntax tree from the
root, looking for the node with the right range. The inner loop of this
search is finding a node's child whose range contains the specified
range. This inner loop was implemented by naive linear search over all
the children. For wide trees, dereferencing a single `SyntaxNodePtr` was
linear. The problem with wide trees though is that they contain a lot of
nodes! And dereferencing pointers to all the nodes is quadratic in the
size of the file!

The solution to this problem is to speed up the children search --
rather than doing a linear lookup, we can use binary search to locate
the child with the desired interval.

Doing this optimization was one of the motivations (or rather, side
effects) of rust-lang#6857. That's why `rowan` grew the useful
`child_or_token_at_range` method which does exactly this binary search.

But looks like we've never actually switch to this method? Oups.

Lesson learned: do not leave broken windows in the fundamental infra.
Otherwise, you'll have to repeatedly re-investigate the issue, by
digging from the top of the Everest down to the foundation!

[1]: https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/.60syntax_highlighting_not_quadratic.60.20failure/near/240811501
[2]: https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/Syntax.20highlighting.20is.20quadratic
[3]: https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/Syntax.20highlighting.20is.20quadratic/near/243412392
@matklad
Copy link
Contributor

matklad commented Jun 21, 2021

@arzg could you rebase? #9362 Should’ve fixed the issue!

@lunacookies
Copy link
Contributor Author

Done!

@lnicola
Copy link
Member

lnicola commented Jun 22, 2021

No luck, it seems.

@matklad
Copy link
Contributor

matklad commented Jun 22, 2021

Aha, it seems that this PR does introduce O(N^2) in the end. Running this in release:

#[test]
fn benchmark_syntax_highlighting_long_struct() {
    for i in 1..=20 {
        let n = i * 500;
        let fixture = bench_fixture::big_struct_n(n);
        let (analysis, file_id) = fixture::file(&fixture);
        let t = std::time::Instant::now();
        analysis.highlight(file_id).unwrap();
        eprintln!("{:3}: {:?}", i, t.elapsed());
    }
}

I get

  1: 21.157851ms
  2: 39.387636ms
  3: 57.04733ms
  4: 76.089806ms
  5: 94.733162ms
  6: 113.956429ms
  7: 131.232812ms
  8: 153.221424ms
  9: 173.878544ms
 10: 191.949289ms
 11: 215.087703ms
 12: 231.287055ms
 13: 251.732164ms
 14: 269.931029ms
 15: 293.694245ms
 16: 316.968239ms
 17: 335.799605ms
 18: 355.772114ms
 19: 389.92803ms
 20: 412.518353ms

with master and

  1: 33.046146ms
  2: 90.745823ms
  3: 168.102535ms
  4: 273.206792ms
  5: 394.522778ms
  6: 552.906436ms
  7: 750.60107ms
  8: 925.746766ms
  9: 1.148798134s
 10: 1.431866023s
 11: 1.726566724s
 12: 2.07207502s
 13: 2.445697905s
 14: 3.007470561s
 15: 2.98714759s
 16: 3.565271835s
 17: 3.975792518s
 18: 4.369784765s
 19: 4.776471804s
 20: 5.336346217s

with this PR. Let me take a closer look here....

@matklad
Copy link
Contributor

matklad commented Jun 22, 2021

Yeah, it's pertty clear why this is quadratic:

impl ItemScope {
    pub fn entries<'a>(&'a self) -> impl Iterator<Item = (&'a Name, PerNs)> + 'a { ... }
    pub fn visibility_of(&self, def: ModuleDefId) -> Option<Visibility> {
        self.name_of(ItemInNs::Types(def))
            .or_else(|| self.name_of(ItemInNs::Values(def)))
            .map(|(_, v)| v)
    }
    pub(crate) fn name_of(&self, item: ItemInNs) -> Option<(&Name, Visibility)> {
        for (name, per_ns) in self.entries() {
            if let Some(vis) = item.match_with(per_ns) {
                return Some((name, vis));
            }
        }
        None
    }

@matklad matklad removed the Broken Window Bugs / technical debt to be addressed immediately label Jun 22, 2021
@matklad
Copy link
Contributor

matklad commented Jun 22, 2021

Fixed the problem in #9379.

@arzg could you rebase this again? Also, thank you so much for uncovering so many performance issues :-)

@lunacookies
Copy link
Contributor Author

Thank you for the thorough analysis. Finding performance issues wasn’t my intention, but I guess it worked out :)

@lunacookies
Copy link
Contributor Author

bors try

@bors
Copy link
Contributor

bors bot commented Jun 22, 2021

🔒 Permission denied

Existing reviewers: click here to make arzg a reviewer

@Veykril
Copy link
Member

Veykril commented Jun 22, 2021

odd, the workflow isnt starting for some reason
bors try

bors bot added a commit that referenced this pull request Jun 22, 2021
@bors
Copy link
Contributor

bors bot commented Jun 22, 2021

try

Build succeeded:

@Veykril
Copy link
Member

Veykril commented Jun 22, 2021

bors r+

@Veykril Veykril changed the title Add public semantic token modifier for public items feat: Add public semantic token modifier for public items Jun 22, 2021
@bors
Copy link
Contributor

bors bot commented Jun 23, 2021

@bors bors bot merged commit 3381c2e into rust-lang:master Jun 23, 2021
@lunacookies lunacookies deleted the pub-modifier branch June 23, 2021 00:05
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.

Add semantic token modifier for pub items

5 participants