Skip to content

Limit how deep the rule index looks into :is() - #261

Merged
FlorianRappl merged 2 commits into
AngleSharp:develfrom
michaelthyregod:feature/is-index-cap
Nov 2, 2026
Merged

FlorianRappl merged 2 commits into
AngleSharp:develfrom
michaelthyregod:feature/is-index-cap

Conversation

@michaelthyregod

Copy link
Copy Markdown
Contributor

Types of Changes

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

A performance fix in the rule index of the cascade. There is no separate issue.

To group a rule by its subject, StyleRuleIndex looks into the arguments of :is(), :where() and :matches(). It parses the arguments' text again at every level, so a selector that nests them deeply costs quadratic time (and stack) to index, every time a document's rules are indexed.

The change

  • The index looks into at most 8 levels. A rule nested deeper stays ungrouped, i.e., it is a candidate for every element and is matched as before, which is always correct. Real selectors nest a level or two.
  • Tests in StyleRuleIndexTests: 8 levels are still grouped and 9 are not (this case fails without the change), and a rule nested 9 deep still applies to its elements only.
  • Benchmark DeepSelectorBenchmarks: computing a style with a rule nesting :is() 10, 50 and 75 levels deep. 75 is the limit AngleSharp #1402 proposes, so the shapes stay valid with it.

Benchmarks

Paired builds of the same benchmark code against devel (342fa84), alternating which goes first each round. BenchmarkDotNet 0.15.8, net10.0, AngleSharp 1.9.0 for both, pinned to the performance cores of an Intel Core Ultra 7 165H. Median of 3 rounds:

Case devel this PR Change Per round Allocated
DeepSelectorBenchmarks, 10 levels 29.9 μs 31.1 μs +4.0% +4.8 / +5.1 / −3.9 69.5 → 69.0 KB
DeepSelectorBenchmarks, 50 levels 38.1 μs 32.9 μs −13.8% −12.3 / −13.3 / −19.7 98.4 → 77.3 KB
DeepSelectorBenchmarks, 75 levels 45.2 μs 35.2 μs −22.2% −19.3 / −19.8 / −26.9 132.1 → 82.2 KB
CssCascadeBenchmarks.ComputedStyle 4,155 μs 4,130 μs −0.6% −2.4 / +7.2 / −1.3 same
CssCascadeBenchmarks.RenderTree 5,328 μs 5,290 μs −0.7% −1.1 / +3.3 / −3.0 same

The gain is modest: AngleSharp 1.9.0 already made the selector parsing behind it much cheaper (with AngleSharp 1.5.0, 50 levels took 442 μs on devel). The 10-level row changes sign between rounds, and single rounds vary by up to ±5% on this machine; real style sheets are unaffected.

Suggested CHANGELOG line

I left CHANGELOG.md out so you can word it; suggestion:

  • Improved indexing rules with deeply nested :is(), :where(), or :matches() (#PR) @michaelthyregod

🤖 Generated with Claude Code

Indexing a rule for the cascade looks into the arguments of :is(),
:where() and :matches(), parsing them again at every level, so the cost
grows quadratically with the nesting: computing a style with 75 nested
:is() took 45 us, 35 us with the cap (allocating 132 KB, 82 KB with it).

- Look into at most 8 levels; a rule nested deeper stays ungrouped, i.e.,
  it is a candidate for every element, which is always correct
- Add tests for the boundary and for matching beyond it
- Add DeepSelectorBenchmarks with 10, 50 and 75 levels

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM - please add the CHANGELOG entry.

@FlorianRappl FlorianRappl added this to the v1.3.2 milestone Nov 2, 2026

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@FlorianRappl
FlorianRappl merged commit d0c4329 into AngleSharp:devel Nov 2, 2026
5 checks passed
@michaelthyregod
michaelthyregod deleted the feature/is-index-cap branch November 2, 2026 15:59
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.

2 participants