Skip to content

Use generated regexes for gRPC JSON parsing#67758

Draft
artl93 wants to merge 4 commits into
dotnet:mainfrom
artl93:artl93-grpc-generated-regex
Draft

Use generated regexes for gRPC JSON parsing#67758
artl93 wants to merge 4 commits into
dotnet:mainfrom
artl93:artl93-grpc-generated-regex

Conversation

@artl93

@artl93 artl93 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Motivation

gRPC JSON transcoding currently creates two process-wide RegexOptions.Compiled regexes for protobuf timestamp and duration parsing. Their first use performs runtime regex parsing/code-generation work and allocates about 52-58 KiB on the measured thread. This is avoidable for static patterns and is most relevant to cold/serverless/scale-to-zero processes and first-request latency.

Change

  • Replace the timestamp and duration fields with [GeneratedRegex] accessors while preserving the exact patterns and RegexOptions.Compiled value.
  • Add direct old-compiled-vs-generated differential tests for matches, groups, captures, options, timeout, culture, malformed input, and downstream parse/error behavior.
  • Add committed BenchmarkDotNet coverage for parsing/deserialization plus timestamp-write and field-mask controls.

There is no public API change. The accessors are internal only so the friend test assembly can compare the generated regexes directly.

BenchmarkDotNet and process-start evidence

Exact current comparison:

  • Parent: d516a8d1efd70e1f6b0dd023407b100622adc16e
  • PR head: 1aed9b4ab810cf1d1a329e730fc89001c11d2705
  • Identical benchmark source in both trees: SHA-256 eed49381a6f7d65558d63acbb4b940e67c42bfdf2b659f8ed8a7c4d0e075cedf
  • macOS 26.5.2, Apple M5 Pro, .NET 11 Arm64, out-of-process BDN, server GC

Warm BDN

Method Parent PR Allocation
Parse timestamp 277.736 ns 269.190 ns 928 B / 928 B
Parse duration 140.207 ns 139.995 ns 680 B / 680 B
Deserialize timestamp 337.738 ns 319.735 ns 1,064 B / 1,064 B
Deserialize duration 196.936 ns 182.924 ns 784 B / 784 B
Write timestamp control 85.871 ns 95.462 ns 400 B / 400 B
Field-mask control 6.042 ns 5.756 ns 0 B / 0 B

The controls move in opposite directions and by more than most parser deltas, so the correct conclusion is warm throughput and allocation are neutral within run-to-run noise. This PR does not claim a steady-state throughput win.

Cold first use

100 interleaved fresh processes per implementation/path. Complete-process time is measured outside the child; the inner first invocation is timed directly around the unchanged reflected method call.

Path Complete process parent -> PR Delta First-invocation allocation parent -> PR
Timestamp parse 38.482 -> 36.350 ms -5.54% 66,312 -> 13,880 B
Duration parse 33.373 -> 29.679 ms -11.07% 61,968 -> 9,736 B
Timestamp write 31.296 -> 28.322 ms -9.50% 60,448 -> 2,392 B
Field-mask control 27.687 -> 27.550 ms -0.49% 376 -> 376 B

The robust result is removal of 51-57 KiB of one-time current-thread allocation and a 2-4 ms reduction in this focused fresh-process harness. Real application startup includes descriptor registration, DI, hosting, and Kestrel, so the end-to-end impact in a full app will be smaller.

The product DLL grows from 142,848 to 147,456 bytes (+4,608 bytes on disk).

Raw current evidence:

  • Parent BDN CSV SHA-256: be503aacf41dc0d9836e87d54baf35285b508ba8dedf6947dc44ff6e4ae009e8
  • PR BDN CSV SHA-256: c345db63fd140b7a95d83e8cf8b8e352eb63474f1b6b4711acbfceb01ff99b
  • Cold samples CSV (800 rows) SHA-256: 8dd28c535449cc4552028ace182c26cea44298dc7b4b760f13eef80206a5f713
  • Parent product DLL SHA-256: 2f32f83145cf432f35308872759a60128dc54cee23a13eb2670e5cd79d36501f
  • PR product DLL SHA-256: a217ae8713a64a98abc30db42839a606b3f69e5a0bf6d6d1fa095db8553eea15

EgorBot v2 is documented as a dotnet/runtime service and does not build dotnet/aspnetcore; it also does not support NativeAOT. A Linux Arm64 Docker BDN campaign with identical detached parent/final worktrees is prepared as the cross-platform substitute, but remains pending because the local Docker daemon is unavailable.

Validation

  • NuGetAudit=false ./src/Grpc/build.sh -test: succeeded, 0 warnings, 0 errors.
  • Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests: 340 passed.
  • Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests: 24 passed.
  • Both consuming projects build through the full gRPC build (the shared Legacy.cs is compiled into JsonTranscoding and Swagger).
  • No review threads are open.

Current PR CI is blocked by a repository-wide NU1902 for AngleSharp 0.9.9 / GHSA-pgww-w46g-26qg in unrelated MVC, Identity, and Security projects. The exact-base main build and unrelated PRs fail identically; platform builds still restore/build/package this gRPC product successfully. This PR intentionally does not modify or suppress that unrelated dependency issue.

Risks and limits

  • Semantics are protected by direct differential tests covering match/capture structure, trailing-newline $ behavior, fractions, offset boundaries, negative/invalid durations, cultures, Unicode digits, exact exceptions/messages, and 100,000-character malformed inputs.
  • Generated source emits sealed specialized regex runners/singletons, removing runtime pattern compilation for these two regexes. This is structurally friendlier to trimming/AOT, but does not make the full reflection-heavy transcoding library AOT-compatible.
  • The exact-pattern probe passed .NET 10 NativeAOT. Full net11 package NativeAOT was not validated because the exact preview runtime/AOT packs are unavailable.
  • A manual parser could improve warm performance further but would be a substantially larger semantic rewrite and is intentionally out of scope.

AI disclosure

The implementation, tests, benchmark harness, measurements, and PR analysis were AI-assisted. Two independent code reviews and two skeptical performance/AOT reviews found no correctness issue and required the conservative warm-neutral/AOT-scoped framing above. Human owner review is still required before this draft is marked ready.

Art Leonard added 2 commits July 12, 2026 14:06
Replace the runtime-compiled timestamp and duration regexes with source-generated equivalents, add semantic differential coverage, and benchmark parser and JSON deserialization paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 61f16215-869e-496a-bb38-7175bebdd2db
Benchmark timestamp writing and field-mask validation alongside timestamp and duration parsing so generated-regex measurements include unaffected control paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 61f16215-869e-496a-bb38-7175bebdd2db
Art Leonard added 2 commits July 19, 2026 10:55
Refresh the draft PR on the current ASP.NET Core baseline before readiness validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 61f16215-869e-496a-bb38-7175bebdd2db
Update the draft PR to the current ASP.NET Core baseline for readiness validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 61f16215-869e-496a-bb38-7175bebdd2db
@artl93 artl93 changed the title WIP: DO NOT REVIEW Use generated regexes for gRPC JSON parsing Use generated regexes for gRPC JSON parsing Jul 20, 2026
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