Skip to content

release: .NET 11 GA SDK, Runtime Async for the whole line, opt-in unions, capability gate - #498

Closed
MPCoreDeveloper wants to merge 7 commits into
masterfrom
release/net11-ga-20261112
Closed

MPCoreDeveloper wants to merge 7 commits into
masterfrom
release/net11-ga-20261112

Conversation

@MPCoreDeveloper

Copy link
Copy Markdown
Owner

What this is

master moves from the .NET 11 RC to the .NET 11 GA SDK (11.0.100), Runtime Async becomes one definition for the whole line instead of three hand-copied project lines, SharpCoreDB.Unions ships as an additive opt-in surface, and the write-path gate learns to measure what it can actually certify. No storage-format, encryption, durability or WAL change.

Measured (GA runtime 11.0.0, same binary per A/B)

before after
awaited, synchronously-completing async Task<T> 71,4 B/call 0 B/call
IAsyncStateMachine types in SharpCoreDB.dll 163 2 (async iterators)
INSERT with DOTNET_gcServer=1 (host lever) ~200.000 ops/s ~359.000 ops/s (≈1,75×)
batch INSERT allocation (object[] overload) 919 B/row 894 B/row (−2,7 %, canary-pinned)
gate vs the previous release's baseline (2026-09-27) — PASSED, worst cell 1,08×, UPDATE faster on both arms (0,85× / 0,94×)

Why the gate changed shape

The UPDATE cell is bimodal (~215k vs ~420k ops/sec) and seven dials were tested and refuted for it: machine load, warm-up depth, GC frequency, GC mode, tiering, hash-index maintenance, compaction. Inside every run the fastest rep is 50–70 % above the median and agrees across runs to ~1 %; a ~50 ms GC pause lands in 2 of 54 measured phases. That is a per-rep disturbance in a 22–35 ms window, so the median cannot certify it. The gate now prints a best column, --write-baseline records rawBest/defaultBest, and when a baseline carries them the verdict is a best-to-best capability comparison. Recording is no longer blocked by the rep spread (it is not a verdict — on this box that made a baseline impossible) but refuses a capability more than 25 % below the one on disk.

Verified on this branch

  • SharpCoreDB.CI.slnf with the GA SDK: Build succeeded, 0 errors
  • core suite: 2124 / 0 failed / 0 errors / 0 skipped (81,3 s)
  • the eight standing canaries: 57 / 0 / 0 / 0
  • write-path gate against the recorded capability baseline: GATE PASSED (exit 0)
  • documentation consistency: 9/9
  • server image rebuilt on the GA base images: sharpcoredb-server:net11-ga (604 MB) reports runtime 11.0.0

Owner actions — deliberately not in this PR

Merging to master runs pack and Publish to NuGet.org, so landing is the owner'''s step. The same goes for the version bump (Version/AssemblyVersion/FileVersion, <PackageReleaseNotes>, tags) and the tag itself — docs/net11/GA_RELEASE_NOTES.md §4 lists all six steps, and §5 states why they are untouched.

Review the baseline diff before landing: baselines/dual-mode-baseline.json now carries both statistics, and a baseline recorded during a regression would silently accept it for every later run.

…hole line

.NET 11 GA shipped 2026-11-10, so master moves off the RC: global.json (both copies) pins 11.0.100 with
rollForward latestPatch and no allowPrerelease, the five net11 workflows install 11.0.x without
include-prerelease (ci-net10.yml untouched), and the server Dockerfile comment names the GA images.

While doing it, Features=runtime-async=on turned out to be three hand-copied project lines (core,
EventSourcing, its tests) while the other 72 projects compiled classic - although docs/net11/UPGRADE.md
already described it as a property of the whole line. It is now defined once, in a new
Directory.Build.targets, conditioned on net11.0, with <UseRuntimeAsync>false</UseRuntimeAsync> as the
per-project opt-out.

Why targets and not props: a TFM-conditioned switch in Directory.Build.props is evaluated before the project
body, where $(TargetFramework) is still empty, so it silently never applies - measured, not reasoned (163
state machines and 71,4 B/call survived with the block in props; the same block in targets gave 2 and 0).

Measured on the GA runtime, 1.000-call loop: an awaited, synchronously-completing async Task<int> allocated
71.424 bytes before and 0 after; SharpCoreDB.dll went from 163 compiler-generated IAsyncStateMachine types
to 2 (the async iterators, which runtime async does not cover at GA).

Verified: SharpCoreDB.CI.slnf builds with the GA SDK (0 errors); core suite 2124 / 0 / 0 / 0.
…async codegen

SharpCoreDB.Unions (net11.0-only, additive) exposes QueryOutcome (rows / not found / failure),
ExecuteOutcome (rows affected / failure) and TableLookup (found / missing) as C# 15 union declarations with
seven record class cases, so a switch over them is exhaustive at compile time. Nothing is retrofitted:
QueryResult, ExecuteResult and every ExecuteSQL signature stay byte-identical, and BuildResult deliberately
stays a record hierarchy (a union there would break a shipped type). The union keyword form was chosen over
the [Union] class shape in the design doc because the GA compiler generates that shape itself.

Two canaries pin facts that would otherwise drift silently:
- RuntimeAsyncCodegenTests fails when the runtime-async flag is dropped (-p:UseRuntimeAsync=false: 163 state
  machines and 71,4 B/call) and passes on the GA build (2 state machines, 0 B/call on an awaited
  synchronously-completing call).
- UnionOutcomeTests carries no discard arm in any switch, so exhaustiveness is enforced by the compiler.

Docs: UPGRADE.md gains the GA section and the measured Runtime Async limits (the awaited shape is the win; a
blocking GetResult still materialises the task; async iterators keep their compiler state machine),
UNION_TYPES_DESIGN.md records that the design shipped, and ROADMAP/INDEX are updated to match.

Verified: union canaries 3/3, async canaries 2/2, core suite 2124 / 0 / 0 / 0.
…location on batch INSERT

The write-path gate's UPDATE cell is bimodal (~215k vs ~420k ops/sec) and six dials were tested and refuted
for it: machine load (INSERT/READ stay inside 1,07x in the same reps), warm-up depth (8/16/2 passes), GC
frequency (2 MB gen0), GC mode (DOTNET_gcServer=1 keeps the split at higher absolutes), tiering
(DOTNET_TieredCompilation=0), hash-index maintenance (SHARPCOREDB_HASH_INDEXES=0) and compaction
(SHARPCOREDB_COMPACTION_THRESHOLD=1e9). What the data says instead: inside every run the fastest rep is
50-70% above the median and the fastest rep agrees across runs to ~1%, a ~50 ms GC pause lands in 2 of 54
measured phases, and most slow passes carry one single-stage stall whose stage varies per pass. The mode is
a per-rep disturbance, and the median is the wrong statistic for a 22-35 ms window.

So the gate now reports and compares capability:
- GateRow prints a `best` column (fastest of the reps); the verdict still uses the median.
- --write-baseline records medians in raw/default and bests in rawBest/defaultBest.
- When a baseline carries the bests, a capability table (best-to-best) decides the verdict and the spread is
  reported as information; median-only baselines keep the old behaviour exactly.
- Recording is no longer gated on the rep spread (it is not a verdict - and on this box that gate made a
  baseline impossible: every attempt exited 2 with 2,63x-3,29x spreads), and instead refuses to write when any
  metric's best is more than 25% below the one on disk, which is what "a baseline recorded during a
  regression silently accepts that regression" is about.
- SHARPCOREDB_PHASE_DIAG=1 adds wall vs process CPU vs GC counts/pause per phase.

With that instrument the INSERT path got its first measured cut: NormalizeInsertRow allocated a
bool[Columns.Count] per row and rebuilt it from the same columnIndexMap every time, but which columns a
statement names is a property of the statement. Hoisting it to one mask per batch took the dictionary-free
object[] overload from 1.838.776 to 1.787.608 bytes for 2.000 rows - 919 -> 894 B/row (-2,7%) - with identical
behaviour, pinned by the new InsertBatchAllocationTests canary at a 900 B/row ceiling.

Refuted in the same session and reverted, so nobody re-derives it: the identity-remap fast path (skip the
normalized object[] when the column order matches) saves 3 B/row, because an insert never names the hidden
AUTO column _rowid and the normalized row is required to carry it.

Docs: docs/manual/performance.md documents server GC as the measured host lever (~1,75x INSERT, +20% READ,
costs a heap per core) and records that GC frequency and tiering are not levers for this engine.

The baseline file is re-recorded on the GA runtime with both statistics; review its diff before landing.

Verified: gate PASSED on the capability comparison (raw INSERT 0,98x, default UPDATE 1,03x), suite 2124 /
0 / 0 / 0, canaries 57 / 0 / 0 / 0.
… session record

CHANGELOG gains the GA upgrade, the opt-in union surface, the GA-runtime gate evidence and the batch-INSERT
allocation cut. PROMOTION_TO_MAIN's GA checklist is closed item by item - SDK pin, CI pins, unions shipped,
gate run (INCONCLUSIVE then a documented PASSED, plus the capability statistic that made it certifiable), the
baseline recorded by the owner, the image rebuilt on the GA base images - and item 5 is stated as owner-only
rather than started, because version metadata, packing and publishing are the rail.

docs/net11/GA_RELEASE_NOTES.md is the release note draft plus the checklist: what the repository verified
(slnf 0 errors, 2124/0/0/0, canaries 57/0/0/0, gate PASSED, docs 9/9, image reports 11.0.0) and the six owner
steps (version bump, tag, merge that packs and publishes, GitHub release, image push, optional quiet-box
re-record).

docs/performance/NET11_C15_PERFORMANCE_PLAN.md is the standing plan: what is already in place, where the time
actually goes per UPDATE stage, the refuted-dial list (including the identity-remap fast path), the UPDATE
mode resolved as a statistic, phases P0-P3 and the zero-regression rails.

WORKLOG carries session 87 and its eight addenda: the GA upgrade, the runtime-async measurement, the union
implementation, the UPDATE mode hunt (seven refutations), the capability gate, the INSERT cut, the refuted
identity path, and the "no regression against the previous version" check (gate against the 2026-09-27
baseline: PASSED, worst cell 1,08x, UPDATE faster on both arms).
@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/SharpCoreDB/Net11/Unions/UnionOutcomes.cs 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

The first CI run on this branch was red in two places. One is mine and is fixed here: the coverage job runs the
suites a second time under `dotnet-coverage collect`, and instrumentation changes what the JIT is allowed to
do, so both allocation canaries stopped measuring the engine and started measuring the collector. Evidence from
job 131142938452: RuntimeAsyncCodegenTests reported 71.424 bytes over 1.000 awaited calls - exactly the classic
(non-runtime-async) figure, i.e. the elision is gone under instrumentation, so asserting 0 was never true there -
and InsertBatchAllocationTests read 902 B/row against its 900 ceiling.

Both are now `[Trait("Category","Allocation")]`, and the coverage job passes -Filter
"...&Category!=Allocation". They still run and still gate in the uninstrumented Build and Test jobs: verified
locally, `Category=Allocation` selects exactly those 2 tests (green) and the coverage filter runs 2122 (green).
AGENTS.md documents the convention so the next allocation canary is tagged the same way.

The second failure is not this branch's: CI's "Fail on vulnerable NuGet packages" step fails on NU1903 by Aspire.Hosting
13.6.4 (GHSA-f7vh-h5g3-7jrh / CVE-2026-120006, high, published 2026-11-10, patched in 13.6.5) in
src/SharpCoreDB.Aspire.Hosting. No package moved in this branch. Bumping a package version is the owner's under the
standing rail, so it is recorded as a release blocker in docs/net11/GA_RELEASE_NOTES.md with the advisory, the
patched version and the file to change - not patched here.
… CI reds

Records the pre-flight evidence (slnf 0 errors, suite 2124/0, canaries 57/0, gate PASSED, docs 9/9, image
reports 11.0.0), the five commits on this branch, and the two CI failures separated by cause: the coverage job
was mine (allocation canaries measure the collector under instrumentation - 71.424 bytes per 1.000 awaited calls
is exactly the classic figure - fixed with the Allocation trait and the coverage filter), and the three Build and
Test jobs are one dependency advisory (NU1903 Aspire.Hosting 13.6.4, GHSA-f7vh-h5g3-7jrh, patched in 13.6.5)
which is a package-version change the rail keeps with the owner.
@MPCoreDeveloper

Copy link
Copy Markdown
Owner Author

Status after the first CI round.

Green: Coverage (net11.0) (runs the full suite on ubuntu under instrumentation - 2122 tests, TOTAL 46,37 % against the 18 % floor), Server Compatibility Smoke, codecov/patch, the CodeQL analyses.

Fixed in c8a28519: the coverage job was red because both allocation canaries measure the JIT, and dotnet-coverage changes what the JIT may do - the Runtime Async canary reported 71.424 bytes per 1.000 awaited calls (exactly the classic, non-runtime-async figure) and the INSERT canary 902 vs its 900 B/row ceiling. They are now [Trait("Category","Allocation")] and the coverage job passes -Filter "...&Category!=Allocation"; they still run and gate in the uninstrumented jobs (verified locally: 2 selected by the trait, 2122 by the coverage filter).

Blocked on an owner decision: all three Build and Test jobs fail on Fail on vulnerable NuGet packages - NU1903, Aspire.Hosting 13.6.4, GHSA-f7vh-h5g3-7jrh / CVE-2026-120006 (high, published 2026-11-10), patched in 13.6.5 (same advisory covers Aspire.Hosting.AppHost). No package moved in this PR, so the advisory is new rather than caused here. Bumping it is a package-version change, which the standing rail keeps with the owner - recorded in docs/net11/GA_RELEASE_NOTES.md §4 instead of patched. Because that gate sits ahead of the test steps, those three jobs never reach them; the code itself is validated on the runners by the coverage and smoke jobs.

After the bump the three jobs become full build+test runs, and the remaining release steps are the ones in GA_RELEASE_NOTES.md §4: version metadata, tag, merge (packs + publishes), GitHub release, image push.

The status page still described 2.1.2 as the whole story, with no mention of the GA upgrade. Adds the factual
line: GA SDK pin, Runtime Async once for every project (0 B/call on an awaited synchronously-completing call,
2 state machines instead of 163), the opt-in union surface, the capability gate, and the validated numbers -
slnf 0 errors, core suite 2,124/0, canaries 57/0, gate PASSED (worst cell 1,08x, UPDATE faster on both arms),
docs 9/9, image reporting 11.0.0. Points at net11/GA_RELEASE_NOTES.md for the checklist and the one open
blocker, and states that version metadata, the tag and the publishing merge stay owner actions.
@sonarqubecloud

Copy link
Copy Markdown

@MPCoreDeveloper
MPCoreDeveloper deleted the release/net11-ga-20261112 branch November 12, 2026 21:52
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