Skip to content

Build the parts of the render tree only when needed - #266

Open
michaelthyregod wants to merge 1 commit into
AngleSharp:develfrom
michaelthyregod:feature/partial-render-tree
Open

michaelthyregod wants to merge 1 commit into
AngleSharp:develfrom
michaelthyregod:feature/partial-render-tree

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

The first step of the partial render tree from #262: the parts of a render node that are rarely needed are only built when they are. Render() still returns the complete tree, with the same styles. GetInnerText on the render tree (and building children on demand) follows in a separate PR.

The change

  • ::before / ::after: a pseudo-element is only cascaded if a rule that may match it declares content. Checking for any matching rule would not help: the default sheet's *::before, *::after { display: inline } matches every element. StyleRuleIndex keeps the pseudo-element rules declaring content (and those with nested rules, which may) in their own group, keyed like the others.
  • Specified style: a render node keeps its explicit style and builds SpecifiedStyle on first read, top-down without recursion. Children inherit from a copy-on-write copy of their parent's style as it was built, so modifying a parent's SpecifiedStyle still does not reach its children. Render(element, parent) inherits the parent's current one, as before. The style is published with Interlocked.CompareExchange, so concurrent first reads get the same instance. GetCascadedStyle() of a rendered pseudo-element still returns its node's specified style.
  • No public API changes (an internal constructor on ElementRenderNode; the public one is unchanged).
  • Tests: generated content from universal, keyed, nested and @media rules, and content added through the CSSOM; specified styles in any reading order and after modifying the parent; rendering again below a modified parent; concurrent first reads (synchronised with a Barrier); the cascaded style of a pseudo-element.

Benchmarks

Paired builds of the same benchmark code against devel (0d5ea08), 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 6 rounds:

Case devel this PR Change Allocated
CssCascadeBenchmarks.RenderTree 5,262 μs 4,486 μs −14.8% 6,629 → 4,994 KB (−24.7%)
CssCascadeBenchmarks.ComputedStyle 4,170 μs 4,232 μs +1.5% 6,701 → 6,754 KB
CssCascadeBenchmarks.ParseInlineDeclarations 408 μs 411 μs +0.8% same
DeepTreeBenchmarks.InnerText, 100 / 500 levels 346 / 1,772 μs 351 / 1,804 μs +1.4% / +1.9% same
DeepTreeBenchmarks.IsVisible, 100 / 500 levels 10.2 / 49.0 μs 10.4 / 50.1 μs +2.6% / +2.3% same

RenderTree is faster in every round (−10.8% to −18.3%). The other cases do not build a render tree; their rounds change sign and stay within the ±5% that single rounds of the same build differ by. ComputedStyle builds the rule index on every call, which now has one more group: 0.8% more allocation.

The CHANGELOG has no open version yet. Shall I add a line (e.g. "Improved rendering by building pseudo-elements and specified styles only when needed") once you open one, or do you prefer to write it?

🤖 Generated with Claude Code

Every element of a render tree got its ::before and ::after cascaded and
its specified style built, though most have no generated content and the
specified style is rarely read (AngleSharp#262).

- Cascade a pseudo-element only if a rule that may match it declares its
  content; rules such as *::before { display: inline } of the default
  sheet alone do not give it a box
- Build the specified style of a render node on first read, from the one
  its parent had when it was built; it is published atomically
- Add tests for the declared content, the reading order, rendering again
  below a parent, and concurrent first reads
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.

1 participant