Skip to content

Write RenderBatchWriter frames and edits as single fixed-size records - #70125

Open
davidfowl wants to merge 3 commits into
mainfrom
perf/components-renderbatchwriter-spans
Open

davidfowl wants to merge 3 commits into
mainfrom
perf/components-renderbatchwriter-spans

Conversation

@davidfowl

@davidfowl davidfowl commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Write RenderBatchWriter frames and edits as single fixed-size records

Serialize each render batch frame and edit with a single stream write (~1.8× faster for large batches)

Description

RenderBatchWriter is shared by Blazor Server, WebView and WebAssembly out-of-process rendering. It serialized every 20-byte RenderTreeFrame through 4–7 separate BinaryWriter.Write calls, and every 16-byte RenderTreeEdit through 4. Each call is a virtual Stream.Write into ArrayBuilderMemoryStream. The disposed component ID and event handler ID arrays were written one element at a time.

This change:

  • assembles each frame and edit with BinaryPrimitives in a private scratch buffer, then writes it with one call. The buffer is rented from ArrayPool<byte>.Shared when the writer is created and returned (and the field nulled) in Dispose. It is never exposed to callers. Every record overwrites all the bytes it emits (frames are cleared first), so no data carries over between records or batches.
  • encodes the int/ulong arrays in chunks through the same scratch buffer, using BinaryPrimitives.Write*LittleEndian.
  • replaces WriteString with GetStringIndex, which returns the string-table index so it can be placed in the buffer, and removes the now-unused WritePadding.

There is no unsafe code, no stackalloc, no MemoryMarshal/Unsafe, and no public API change. The wire format is unchanged byte for byte. I temporarily instrumented every RenderBatchWriterTest case to dump its serialized bytes, ran them on main and on this branch, and the SHA-256 of all outputs is identical. That includes the new multi-chunk test.

Benchmark

This adds RenderBatchWriterBenchmark in src/Components/Components/perf:

  • WriteLargeBatch: initial render of a 200-row table, about 2,200 frames and 200 edits.
  • WriteSmallBatch: a counter-style update with 5 edits and 3 frames.

Setup: BenchmarkDotNet --config perflab (InProcess) on a 2 vCPU Linux VM, 3 interleaved rounds.

Method Round main PR Allocated main / PR
WriteLargeBatch 1 116.9 µs 51.7 µs 4,720 B / 4,728 B
WriteLargeBatch 2 92.8 µs 53.5 µs 4,720 B / 4,728 B
WriteLargeBatch 3 94.9 µs 51.4 µs 4,720 B / 4,728 B
WriteSmallBatch 1 591.1 ns 394.3 ns 464 B / 472 B
WriteSmallBatch 2 499.9 ns 403.0 ns 464 B / 472 B
WriteSmallBatch 3 507.3 ns 394.2 ns 464 B / 472 B

The +8 B per batch is the scratch-buffer field on the writer object; the buffer itself is pooled.

Tests

  • Microsoft.AspNetCore.Components.Server.Tests passes (395 tests). This includes the byte-exact RenderBatchWriterTest cases that cover every frame type, edits, disposed IDs and string deduplication, plus the new CanIncludeLargeDisposedIdArrays, which spans multiple chunks.
  • Microsoft.AspNetCore.Components.WebView and Microsoft.AspNetCore.Components.WebAssembly, which also compile this file, build cleanly.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

Each RenderTreeFrame (20 bytes) and RenderTreeEdit (16 bytes) was previously
serialized through 4-7 separate BinaryWriter.Write calls (each a virtual
Stream.Write), plus per-element writes for the disposed component/event
handler ID arrays. Assemble each frame/edit in a stack buffer and write it
once, and bulk-write the int/ulong arrays on little-endian platforms.
The output bytes are unchanged.

Adds RenderBatchWriterBenchmark to the Components perf project.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 20:57
@davidfowl
davidfowl requested a review from a team as a code owner October 30, 2026 20:57
@github-actions github-actions Bot added the area-blazor Includes: Blazor, Razor Components label Oct 30, 2026

Copilot AI 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.

🔵 Needs a closer look

The missing frozen review bundle and existing-feedback snapshot prevent completion of revision-provenance checks and prior-feedback deduplication.

No open issues 🎉

What changed in this PR

This PR reduces stream writes in the render-batch serializer shared by Blazor Server, WebView, and WebAssembly out-of-process rendering, while preserving the existing wire format.

Changes:

  • Assemble each frame and edit in a fixed-size stack buffer for a single write.
  • Bulk-write disposed ID arrays on little-endian platforms, retaining per-element writes on big-endian platforms.
  • Add large- and small-batch serialization benchmarks and the project configuration needed to compile the shared writer.
File Description
src/​Components/​Shared/​src/​RenderBatchWriter.cs Consolidates frame, edit, and ID-array writes; replaces string writes with index lookup.
src/​Components/​Components/​perf/​RenderBatchWriterBenchmark.cs Adds table-render and counter-style update benchmarks.
src/​Components/​Components/​perf/​Microsoft.AspNetCore.Components.Performance.csproj Includes shared serialization sources and enables their compilation settings.

Review effort: Balanced

Copy link
Copy Markdown
Member Author

/azp run aspnetcore-components-e2e

The only failure is ServerRoutingTest.NavigationLock_CanBlockExternalNavigation on the Mono server leg (1 of 3,785 tests). That test exercises the browser's beforeunload dialog for external navigation; it doesn't depend on render batch encoding, and all other Blazor Server E2E tests, which depend on the batch format, passed. Rerunning it as a likely flake.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

davidfowl commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

/azp run aspnetcore-components-e2e

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

davidfowl commented Oct 31, 2026 •

Copy link
Copy Markdown
Member Author

/azp run aspnetcore-components-e2e

Rerunning: the CoreCLR e2e leg failed only VirtualizationTest.VariableHeight_HtmlTable_DoesNotProduceNestedTrElements. Build Analysis reports an 8.08% historical failure rate for it, and it failed the same way on unrelated PRs #70108 and #70143. That test is WebAssembly-hosted (/subdir), so it doesn't exercise the Server RenderBatchWriter changed here.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

Copy link
Copy Markdown
Member Author

Compliance check: no public API or PublicAPI.*.txt changes. The wire format is byte-identical. I temporarily instrumented every RenderBatchWriterTest case to dump the serialized bytes, then ran them on main and on this branch. The SHA-256 of all outputs is identical (33353782…be0). The writer still emits little-endian on every platform, using the BinaryPrimitives *LittleEndian APIs.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

…rshal in RenderBatchWriter

Edits and frames are assembled in a private ArrayPool-rented scratch buffer
(returned in Dispose) and int/ulong arrays are encoded in chunks with
BinaryPrimitives. Wire format is unchanged; adds a multi-chunk test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl changed the title Write RenderBatchWriter frames and edits as single fixed-size spans Write RenderBatchWriter frames and edits as single fixed-size records Oct 31, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Member Author

CI on b447d02 failed only in aspnetcore-components-e2e (Mono server): ServerRoutingTest.NavigationLock_CanBlockExternalNavigation. Build Analysis reports a 25.44% historical failure rate for this test, and it exercises browser navigation, not the RenderBatchWriter serialization changed here (the wire format is byte-identical to main, verified via SHA-256 of all RenderBatchWriterTest outputs). All other pipelines/legs passed.

Pushed empty commit f7c06cf to rerun CI.

🤖 Posted by ASP.NET Core Perf 2 (AI Agent)

@PureWeen

PureWeen commented Nov 1, 2026

Copy link
Copy Markdown
Member

/review

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants