Skip to content

Reuse nested trial layouts within a render - #54

Merged
FlorianRappl merged 4 commits into
AngleSharp:develfrom
michaelthyregod:perf/nested-trial-layouts
Nov 12, 2026
Merged

FlorianRappl merged 4 commits into
AngleSharp:develfrom
michaelthyregod:perf/nested-trial-layouts

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

Since 0.8.0, nested tables and inline-blocks take time doubling with every level. Each level measures its content with TrialLayout and then lays it out for real, so level n is laid out 2^n times: 6 nested tables take 63 trial layouts, 12 take 4095.

var device = new DefaultRenderDevice { ViewPortWidth = 800, ViewPortHeight = 600 };
var context = BrowsingContext.New(Configuration.Default.WithCss().WithRenderDevice(device));
var html = String.Concat(Enumerable.Repeat("<table><tr><td>", 18)) + "x";
var document = await context.OpenAsync(r => r.Content(html));
new HtmlRenderer().RenderToPng(document, device); // 7.2 s
nested tables 16 18
devel 5a52786 2.6 s 7.2 s
this PR 0.30 s 0.31 s

A trial is laid out on its own (at the origin, with its own floats, positioned and sticky scopes), so the same inputs give the same result within a render. Trials started inside another trial, the ones the layout around them repeats, are now kept in the LayoutContext for the render, like MeasuredHeights: per element, with their inputs (style map, containing width, inherited text style, forced width, atomic/flex item, maxY, measuring mode). Not inside a line-clamped box (its lines are counted), and incremental rendering drops them on every change. 6 nested tables now take 6 trial layouts, 12 take 12. The lookup is a method of its own, so the frame of TrialLayout (on the stack once per nesting level) does not grow.

NestedTrialLayouts_GrowLinearlyWithTheDepth checks this for tables and inline-blocks (RenderMetrics.TrialLayouts).

Benchmarks (AngleSharp.Renderer.Benchmarks, devel 5a52786 vs this PR, P-cores, 3 alternating rounds, median):

devel this PR
BuildDisplayList, net10.0 209.9 ms, 58.34 MB 215.8 ms, 58.62 MB
RenderToPng, net10.0 2326 ms, 64.34 MB 2264 ms, 64.63 MB
BuildDisplayList, net8.0 192.5 ms, 58.81 MB 189.1 ms, 59.10 MB
RenderToPng, net8.0 2359 ms, 64.82 MB 2264 ms, 65.10 MB

The benchmark page has few nested trials (40 of 966 are reused), so the time differences are within the spread of a single run (StdDev up to 15% of the mean for BuildDisplayList); the kept trials cost 0.29 MB (0.5%) per render there.

A trial layout is laid out on its own, so the same inputs give the same
result within one render. Nested tables and inline-blocks trial-laid out
every level again for each level above it, doubling the work per level
(20 nested tables took 16 s, 22 over a minute). Trials inside a trial,
the ones the layout around them repeats, are now kept per element with
their inputs (style map, width, inherited style, measuring mode) for the
render, not inside a line-clamped box; incremental rendering drops them
on every change.
Without the trial cache, 6 nested tables take 63 trial layouts and 12 take
4095 (2^depth - 1).
@CLAassistant

CLAassistant commented Nov 10, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@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 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.

Tests fail on MacOS. Not a snapshot issue, but rather a full crash.

Laid out only a few times, the layout stays in the first JIT tier, whose
frames are large: 12 nested inline-blocks overflowed the test thread's
stack on macOS. With tiered compilation off, devel and the trial cache
need the same stack.
michaelthyregod added a commit to michaelthyregod/AngleSharp.Renderer that referenced this pull request Nov 12, 2026
Each of their levels lays out the levels below it twice, so up to the
depth the stack allows the test never completed in CI. They come back
once the nested trial layouts are reused (AngleSharp#54).
@michaelthyregod

Copy link
Copy Markdown
Contributor Author

Thanks! The crash is the new test: 12 nested inline-blocks need more stack in a fresh process than the macOS test thread has. With the cache the layout runs so rarely that it stays in the JIT's first tier, whose frames are about 3x larger; devel's 4095 trial layouts get it recompiled along the way. With tiered compilation off, both need the same stack. The test now counts on a thread with a 16 MB stack, so it no longer depends on the platform's thread size.

@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 9aa4a89 into AngleSharp:devel Nov 12, 2026
7 checks passed
michaelthyregod added a commit to michaelthyregod/AngleSharp.Renderer that referenced this pull request Nov 12, 2026
…again

With the nested trial layouts reused (AngleSharp#54), they render in well under a
second.
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.

3 participants