Skip to content

Commit a3483e4

Browse files
authored
perf(linter): performance improvement for css semantic model (#4044)
1 parent 20a726c commit a3483e4

6 files changed

Lines changed: 56 additions & 43 deletions

File tree

crates/biome_css_analyze/src/services/semantic.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ impl Visitor for SemanticModelBuilderVisitor {
9999
fn visit(&mut self, event: &WalkEvent<CssSyntaxNode>, _ctx: VisitorContext<CssLanguage>) {
100100
match event {
101101
WalkEvent::Enter(node) => {
102-
self.builder.push_node(node);
103102
self.extractor.enter(node);
104103
}
105104
WalkEvent::Leave(node) => {

crates/biome_css_semantic/src/events.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::VecDeque;
1+
use std::{borrow::Cow, collections::VecDeque};
22

33
use biome_css_syntax::{
44
AnyCssSelector, CssDeclarationBlock, CssRelativeSelector, CssSyntaxKind::*,
@@ -10,6 +10,8 @@ use crate::{
1010
semantic_model::model::Specificity,
1111
};
1212

13+
const ROOT_SELECTOR: &str = ":root";
14+
1315
#[derive(Debug)]
1416
pub enum SemanticEvent {
1517
RuleStart(TextRange),
@@ -102,22 +104,24 @@ impl SemanticEventExtractor {
102104
}
103105
}
104106

107+
#[inline]
105108
fn process_selector(&mut self, selector: AnyCssSelector) {
106109
match selector {
107110
AnyCssSelector::CssComplexSelector(s) => {
108111
if let Ok(l) = s.left() {
109-
self.add_selector_event(l.text(), l.range());
112+
self.add_selector_event(Cow::Borrowed(&l.text()), l.range());
110113
}
111114
if let Ok(r) = s.right() {
112-
self.add_selector_event(r.text(), r.range());
115+
self.add_selector_event(Cow::Borrowed(&r.text()), r.range());
113116
}
114117
}
115118
AnyCssSelector::CssCompoundSelector(selector) => {
116-
if selector.text() == ":root" {
119+
let selector_text = selector.text();
120+
if selector_text == ROOT_SELECTOR {
117121
self.stash.push_back(SemanticEvent::RootSelectorStart);
118122
self.is_in_root_selector = true;
119123
}
120-
self.add_selector_event(selector.text().to_string(), selector.range())
124+
self.add_selector_event(Cow::Borrowed(&selector_text), selector.range())
121125
}
122126
_ => {}
123127
}
@@ -194,9 +198,9 @@ impl SemanticEventExtractor {
194198
});
195199
}
196200

197-
fn add_selector_event(&mut self, name: String, range: TextRange) {
201+
fn add_selector_event(&mut self, name: Cow<str>, range: TextRange) {
198202
self.stash.push_back(SemanticEvent::SelectorDeclaration {
199-
name,
203+
name: name.into_owned(),
200204
range,
201205
specificity: Specificity(0, 0, 0), // TODO: Implement this
202206
});

crates/biome_css_semantic/src/semantic_model/builder.rs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use biome_css_syntax::{CssRoot, CssSyntaxKind, CssSyntaxNode};
1+
use std::collections::BTreeMap;
2+
3+
use biome_css_syntax::CssRoot;
24
use biome_rowan::TextRange;
35
use rustc_hash::FxHashMap;
46

@@ -10,15 +12,14 @@ use crate::events::SemanticEvent;
1012

1113
pub struct SemanticModelBuilder {
1214
root: CssRoot,
13-
node_by_range: FxHashMap<TextRange, CssSyntaxNode>,
1415
/// List of all top-level rules in the CSS file
1516
rules: Vec<Rule>,
1617
global_custom_variables: FxHashMap<String, CssGlobalCustomVariable>,
1718
/// Stack of rule IDs to keep track of the current rule hierarchy
1819
current_rule_stack: Vec<RuleId>,
1920
next_rule_id: RuleId,
2021
/// Map to get the rule containing the given range of CST nodes
21-
range_to_rule: FxHashMap<TextRange, Rule>,
22+
range_to_rule: BTreeMap<TextRange, Rule>,
2223
rules_by_id: FxHashMap<RuleId, Rule>,
2324
/// Indicates if the current node is within a `:root` selector
2425
is_in_root_selector: bool,
@@ -28,11 +29,10 @@ impl SemanticModelBuilder {
2829
pub fn new(root: CssRoot) -> Self {
2930
Self {
3031
root,
31-
node_by_range: FxHashMap::default(),
3232
rules: Vec::new(),
3333
current_rule_stack: Vec::new(),
3434
global_custom_variables: FxHashMap::default(),
35-
range_to_rule: FxHashMap::default(),
35+
range_to_rule: BTreeMap::default(),
3636
is_in_root_selector: false,
3737
next_rule_id: RuleId::default(),
3838
rules_by_id: FxHashMap::default(),
@@ -42,7 +42,6 @@ impl SemanticModelBuilder {
4242
pub fn build(self) -> SemanticModel {
4343
let data = SemanticModelData {
4444
root: self.root,
45-
node_by_range: self.node_by_range,
4645
rules: self.rules,
4746
global_custom_variables: self.global_custom_variables,
4847
range_to_rule: self.range_to_rule,
@@ -51,17 +50,6 @@ impl SemanticModelBuilder {
5150
SemanticModel::new(data)
5251
}
5352

54-
#[inline]
55-
pub fn push_node(&mut self, node: &CssSyntaxNode) {
56-
use CssSyntaxKind::*;
57-
if matches!(
58-
node.kind(),
59-
CSS_SELECTOR_LIST | CSS_DECLARATION | CSS_DECLARATION_OR_RULE_LIST | CSS_QUALIFIED_RULE
60-
) {
61-
self.node_by_range.insert(node.text_range(), node.clone());
62-
}
63-
}
64-
6553
#[inline]
6654
pub fn push_event(&mut self, event: SemanticEvent) {
6755
match event {

crates/biome_css_semantic/src/semantic_model/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ pub fn semantic_model(root: &CssRoot) -> SemanticModel {
1616
for node in root.preorder() {
1717
match node {
1818
biome_css_syntax::WalkEvent::Enter(node) => {
19-
builder.push_node(&node);
2019
extractor.enter(&node);
2120
}
2221
biome_css_syntax::WalkEvent::Leave(node) => extractor.leave(&node),

crates/biome_css_semantic/src/semantic_model/model.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::rc::Rc;
1+
use std::{collections::BTreeMap, rc::Rc};
22

3-
use biome_css_syntax::{CssRoot, CssSyntaxNode};
4-
use biome_rowan::TextRange;
3+
use biome_css_syntax::CssRoot;
4+
use biome_rowan::{TextRange, TextSize};
55
use rustc_hash::FxHashMap;
66

77
/// The façade for all semantic information of a CSS document.
@@ -24,11 +24,6 @@ impl SemanticModel {
2424
&self.data.root
2525
}
2626

27-
/// Retrieves a node by its text range.
28-
pub fn node_by_range(&self, range: TextRange) -> Option<&CssSyntaxNode> {
29-
self.data.node_by_range.get(&range)
30-
}
31-
3227
/// Returns a slice of all rules in the CSS document.
3328
pub fn rules(&self) -> &[Rule] {
3429
&self.data.rules
@@ -44,12 +39,26 @@ impl SemanticModel {
4439

4540
/// Returns the rule that contains the given range.
4641
pub fn get_rule_by_range(&self, target_range: TextRange) -> Option<&Rule> {
47-
self.data
48-
.range_to_rule
49-
.iter()
50-
.filter(|(rule_range, _)| rule_range.contains_range(target_range))
51-
.min_by_key(|(rule_range, _)| rule_range.len())
52-
.map(|(_, rule)| rule)
42+
// Generally, this function narrows down the search before finding the most specific rule for better performance.
43+
// But when the target range starts from 0, the BTreeMap's range method may not work as expected due to
44+
// the comparison semantics of TextRange.
45+
46+
// Handle the edge case where the target range starts from 0.
47+
if target_range.start() == TextSize::from(0) {
48+
self.data
49+
.range_to_rule
50+
.iter()
51+
.rev()
52+
.find(|(&range, _)| range.contains_range(target_range))
53+
.map(|(_, rule)| rule)
54+
} else {
55+
self.data
56+
.range_to_rule
57+
.range(..=target_range)
58+
.rev()
59+
.find(|(&range, _)| range.contains_range(target_range))
60+
.map(|(_, rule)| rule)
61+
}
5362
}
5463
}
5564

@@ -60,16 +69,14 @@ impl SemanticModel {
6069
#[derive(Debug)]
6170
pub(crate) struct SemanticModelData {
6271
pub(crate) root: CssRoot,
63-
/// Map to each by its range
64-
pub(crate) node_by_range: FxHashMap<TextRange, CssSyntaxNode>,
6572
/// List of all top-level rules in the CSS document
6673
pub(crate) rules: Vec<Rule>,
6774
/// Map of CSS variables declared in the `:root` selector or using the @property rule.
6875
pub(crate) global_custom_variables: FxHashMap<String, CssGlobalCustomVariable>,
6976
/// Map of all the rules by their id
7077
pub(crate) rules_by_id: FxHashMap<RuleId, Rule>,
7178
/// Map of the range of each rule to the rule itself
72-
pub(crate) range_to_rule: FxHashMap<TextRange, Rule>,
79+
pub(crate) range_to_rule: BTreeMap<TextRange, Rule>,
7380
}
7481

7582
/// Represents a CSS rule set, including its selectors, declarations, and nested rules.

crates/biome_text_size/src/range.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ impl fmt::Debug for TextRange {
2626
}
2727
}
2828

29+
impl PartialOrd for TextRange {
30+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
31+
Some(self.cmp(other))
32+
}
33+
}
34+
35+
impl Ord for TextRange {
36+
fn cmp(&self, other: &Self) -> Ordering {
37+
match self.start.cmp(&other.start) {
38+
Ordering::Less => Ordering::Less,
39+
Ordering::Greater => Ordering::Greater,
40+
Ordering::Equal => self.end.cmp(&other.end),
41+
}
42+
}
43+
}
44+
2945
impl TextRange {
3046
/// Creates a new `TextRange` with the given `start` and `end` (`start..end`).
3147
///

0 commit comments

Comments
 (0)