Skip to content

Disable Utf8JsonReader whitespace-skip vectorization on browser/WASM#130484

Merged
eiriktsarpalis merged 6 commits into
mainfrom
eiriktsarpalis-perf-regression-triage-75553
Jul 14, 2026
Merged

Disable Utf8JsonReader whitespace-skip vectorization on browser/WASM#130484
eiriktsarpalis merged 6 commits into
mainfrom
eiriktsarpalis-perf-regression-triage-75553

Conversation

@eiriktsarpalis

@eiriktsarpalis eiriktsarpalis commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

#129701 replaced the scalar whitespace-skip loop in Utf8JsonReader.SkipWhiteSpace with an unconditional vectorized SearchValues scan. That is neutral-to-faster on native, but regressed a large batch of System.Text.Json benchmarks on browser/WASM (Mono AOT) — 76 reported in dotnet/perf-autofiling-issues#75553. On WASM AOT the fixed per-call SIMD entry cost is high relative to the short inter-token whitespace runs typical of JSON, so the vectorized scan is ~2.6× slower per call there.

Fix

Disable the vectorized whitespace scan on browser/WASM and fall back to the original scalar loop; keep the vectorized path on every other target:

if (!OperatingSystem.IsBrowser())
{
    // vectorized SearchValues scan — unchanged from main
    return;
}
// browser/WASM: original scalar loop

OperatingSystem.IsBrowser() folds to a per-target constant, so native codegen is identical to main (the scalar branch is dead-code-eliminated) and only the browser build takes the scalar path. This intentionally leaves SIMD throughput on the table for long whitespace runs on WASM; real JSON inter-token runs are short (~4 bytes) and sit well below the crossover, so realistic documents only benefit.

Scope: IsBrowser() covers browser-WASM only (what #75553 reported), not WASI/iOS/Android Mono AOT; it can be broadened later if needed.

Validation

WASM AOT (net11, node, SIMD on), ns per SkipWhiteSpace call — the scalar loop this PR restores vs the SearchValues scan on main:

Document scalar (this PR) SearchValues (main)
indented 4 KB 16.3 43.2
indented 40 KB 16.4 43.2
indented 400 KB 16.3 43.0
minified 40 KB 10.7 27.5

Native is unchanged by construction (codegen identical to main, confirmed by EgorBot runs on the earlier commits). Correctness verified against the scalar oracle (32.2M differential cases, 0 mismatches) plus the existing reader tests.

Contributes to dotnet/perf-autofiling-issues#75553.

Note

This pull request was authored with the assistance of GitHub Copilot.

PR #129701 replaced the scalar whitespace-skip loop in Utf8JsonReader.SkipWhiteSpace with an unconditional vectorized SearchValues scan. On Mono/WASM AOT the fixed per-call cost of that scan, together with the extra LastIndexOf('\n')/Count('\n') passes needed to recompute the line/byte-position bookkeeping, is not amortized over the short inter-token whitespace runs typical of indented JSON. This regressed 76 System.Text.Json benchmarks (all indented documents; zero minified).

Reinstate the hybrid approach from the first commit of that PR: scan up to JsonConstants.MaxScalarWhiteSpaceScanLength (16) bytes with the scalar loop, which handles the common short-run case in a single fused pass (advance + newline count + byte offset), and only fall back to the vectorized IndexOfFirstNonWhiteSpace + CountNewLines path once the run is still whitespace after the window. This restores parity on short runs while preserving the native vectorization win for genuinely long whitespace runs.

Behavior is identical to the long-shipped scalar oracle: verified with 32M+ differential cases and all published whitespace line/byte-position test vectors, which already exercise runs that cross the 16-byte boundary.

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

Copy link
Copy Markdown
Member Author

Native validation of the impacted scenarios. EgorBot runs on native hardware (not WASM), so this confirms the reinstated scalar pre-scan does not regress the native path that #129701 optimized — short/indented runs, minified input, and genuinely long whitespace runs where vectorization must be preserved. PR mode compares this branch against main (the current unconditional SearchValues scan). The WASM/Mono AOT win itself is covered in the PR description.

@EgorBot -linux_amd -linux_arm64 -osx_arm64

using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);

[MemoryDiagnoser]
public class Bench
{
    private byte[] _indented = default!;
    private byte[] _minified = default!;
    private byte[] _deeplyIndented = default!;
    private byte[] _longRuns = default!;

    [GlobalSetup]
    public void Setup()
    {
        object graph = MakeGraph(depth: 4, breadth: 4);
        _indented = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = true });
        _minified = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = false });
        // Deep nesting => longer indentation runs (2 spaces * depth), straddling the 16-byte promotion boundary.
        _deeplyIndented = JsonSerializer.SerializeToUtf8Bytes(MakeGraph(depth: 12, breadth: 2), new JsonSerializerOptions { WriteIndented = true });
        // Explicit long whitespace runs => the vectorized tail path that must stay competitive on native.
        _longRuns = MakeLongRunDocument(elements: 2000, runLength: 48);
    }

    private static object MakeGraph(int depth, int breadth)
    {
        if (depth == 0)
        {
            return "some string leaf value";
        }

        var node = new Dictionary<string, object>();
        for (int i = 0; i < breadth; i++)
        {
            node["child_" + i] = MakeGraph(depth - 1, breadth);
        }
        node["number"] = 1234567;
        node["flag"] = true;
        node["nullable"] = null!;
        node["array"] = new object[] { 1, 2, 3, "four", false };
        return node;
    }

    private static byte[] MakeLongRunDocument(int elements, int runLength)
    {
        string ws = new string(' ', runLength - 1);
        var sb = new StringBuilder();
        sb.Append('[');
        for (int i = 0; i < elements; i++)
        {
            if (i > 0)
            {
                sb.Append(',');
            }
            sb.Append('\n').Append(ws).Append(i);
        }
        sb.Append('\n').Append(']');
        return Encoding.UTF8.GetBytes(sb.ToString());
    }

    private static long CountTokens(byte[] utf8)
    {
        var reader = new Utf8JsonReader(utf8);
        long tokens = 0;
        while (reader.Read())
        {
            tokens++;
        }
        return tokens;
    }

    [Benchmark]
    public long Indented() => CountTokens(_indented);

    [Benchmark]
    public long Minified() => CountTokens(_minified);

    [Benchmark]
    public long DeeplyIndented() => CountTokens(_deeplyIndented);

    [Benchmark]
    public long LongWhitespaceRuns() => CountTokens(_longRuns);
}

Note

Benchmark authored with the assistance of GitHub Copilot.

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.

Pull request overview

This PR adjusts System.Text.Json’s Utf8JsonReader.SkipWhiteSpace implementation to use a hybrid strategy: a scalar loop for short whitespace runs and a vectorized SearchValues-based scan only after a configurable run-length threshold, aiming to avoid regressions on platforms where SIMD entry is expensive (e.g., Mono/WASM AOT) while still benefiting long runs.

Changes:

  • Replaced the unconditional IndexOfFirstNonWhiteSpace scan in SkipWhiteSpace with a scalar pre-scan that promotes to vectorized scanning after a threshold.
  • Added JsonConstants.MaxScalarWhiteSpaceScanLength (16) to centralize the promotion threshold.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs Reintroduces scalar whitespace scanning and promotes to vectorized scanning after MaxScalarWhiteSpaceScanLength, while preserving line/byte position bookkeeping.
src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs Adds MaxScalarWhiteSpaceScanLength constant and documents its purpose.

Comment thread src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs Outdated
EgorBot native benchmarks (EgorBot/Benchmarks#309) showed the unified scalar

pre-scan regresses CoreCLR/native ~2x on deeply-indented and long-whitespace-run

documents, since for runs longer than the 16-byte window it pays the scalar

compares AND still performs the full vectorized scan on the remainder.

Gate the pre-scan behind !OperatingSystem.IsBrowser(), which folds to a

per-target constant: non-browser codegen is identical to main (zero native

delta), while browser/WASM AOT keeps the scalar pre-scan that fixes the

reported regression (dotnet/perf-autofiling-issues#75553). The vectorized tail

is extracted into a shared SkipWhiteSpaceVectorized helper used by both paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 10, 2026 14:32
@eiriktsarpalis

Copy link
Copy Markdown
Member Author

Follow-up: the first EgorBot run exposed a real native regression — now gated to WASM

The first native run did not come back clean, and I don't want to paper over that. On all three native machines the unified scalar pre-scan regressed the longer-run scenarios, exactly the concern raised in review about trading native perf for a WASM-only win:

Scenario macOS M4 arm64 Ubuntu EPYC x64 Ubuntu N2 arm64
Indented (depth 4) ~16% slower ~19% faster ~6% slower
Minified neutral neutral neutral
DeeplyIndented (depth 12) ~2.1x slower ~45% slower ~2.1x slower
LongWhitespaceRuns (48-byte) ~2.8x slower ~79% slower ~2x slower

Root cause of the native regression: for whitespace runs longer than the 16-byte window, the hybrid pays up to 16 scalar compares and still performs the full vectorized scan (IndexOfFirstNonWhiteSpace + CountNewLines) on the remainder. On native, where SIMD entry is cheap, that pre-scan is pure overhead — native genuinely wants main's unconditional SearchValues scan.

Fix (pushed): gate the scalar pre-scan behind !OperatingSystem.IsBrowser(). It folds to a per-target constant, so:

  • CoreCLR / native — the pre-scan branch is dead-code-eliminated; codegen is identical to mainzero native delta.
  • Browser / WASM AOT — keeps the scalar pre-scan + promotion that fixes the reported regression (dotnet/perf-autofiling-issues#75553).

The vectorized tail is now a shared SkipWhiteSpaceVectorized helper used by both call sites. Correctness re-verified after the refactor: differential harness (both paths vs. the scalar oracle, 32.2M cases, 0 mismatches) + end-to-end reader verifier (90/90).

Scope note: IsBrowser() covers browser-WASM only, not WASI/iOS/Android Mono AOT (same SIMD-entry root cause). That precisely matches what #75553 reported (browser-WASM), and there's no clean "is-Mono-AOT" gate at this layer. Happy to broaden if we'd rather cover mobile AOT too.

Re-running the same four scenarios to confirm native is now neutral (every scenario should return to ~1.00 vs main):

@EgorBot -linux_amd -linux_arm64 -osx_arm64

using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);

[MemoryDiagnoser]
public class Bench
{
    private byte[] _indented = default!;
    private byte[] _minified = default!;
    private byte[] _deeplyIndented = default!;
    private byte[] _longRuns = default!;

    [GlobalSetup]
    public void Setup()
    {
        object graph = MakeGraph(depth: 4, breadth: 4);
        _indented = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = true });
        _minified = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = false });
        // Deep nesting => longer indentation runs (2 spaces * depth), straddling the 16-byte promotion boundary.
        _deeplyIndented = JsonSerializer.SerializeToUtf8Bytes(MakeGraph(depth: 12, breadth: 2), new JsonSerializerOptions { WriteIndented = true });
        // Explicit long whitespace runs => the vectorized tail path that must stay competitive on native.
        _longRuns = MakeLongRunDocument(elements: 2000, runLength: 48);
    }

    private static object MakeGraph(int depth, int breadth)
    {
        if (depth == 0)
        {
            return "some string leaf value";
        }

        var node = new Dictionary<string, object>();
        for (int i = 0; i < breadth; i++)
        {
            node["child_" + i] = MakeGraph(depth - 1, breadth);
        }
        node["number"] = 1234567;
        node["flag"] = true;
        node["nullable"] = null!;
        node["array"] = new object[] { 1, 2, 3, "four", false };
        return node;
    }

    private static byte[] MakeLongRunDocument(int elements, int runLength)
    {
        string ws = new string(' ', runLength - 1);
        var sb = new StringBuilder();
        sb.Append('[');
        for (int i = 0; i < elements; i++)
        {
            if (i > 0)
            {
                sb.Append(',');
            }
            sb.Append('\n').Append(ws).Append(i);
        }
        sb.Append('\n').Append(']');
        return Encoding.UTF8.GetBytes(sb.ToString());
    }

    private static long CountTokens(byte[] utf8)
    {
        var reader = new Utf8JsonReader(utf8);
        long tokens = 0;
        while (reader.Read())
        {
            tokens++;
        }
        return tokens;
    }

    [Benchmark]
    public long Indented() => CountTokens(_indented);

    [Benchmark]
    public long Minified() => CountTokens(_minified);

    [Benchmark]
    public long DeeplyIndented() => CountTokens(_deeplyIndented);

    [Benchmark]
    public long LongWhitespaceRuns() => CountTokens(_longRuns);
}

Note

This comment and benchmark were authored with the assistance of GitHub Copilot.

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs Outdated
Comment thread src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs Outdated
@eiriktsarpalis

Copy link
Copy Markdown
Member Author

Native re-run confirms the gate resolved the regression

The re-run after gating the pre-scan behind !OperatingSystem.IsBrowser() is in — EgorBot/Benchmarks#313, same four scenarios, three native machines. Ratios below are main relative to this PR (PR = baseline 1.00); < 1.00 means main is faster (PR slower), > 1.00 means PR is faster.

Scenario macOS arm64 Ubuntu EPYC x64 Ubuntu N2 arm64
Indented (depth 4) 0.97 0.98 1.01
Minified 1.00 0.97 1.00
DeeplyIndented (depth 12) 1.03 1.02 0.99
LongWhitespaceRuns (48-byte) 1.21 0.92 1.07

For comparison, the ungated hybrid (#309) was regressing the long-run scenarios hard:

Scenario macOS x64 ARM
DeeplyIndented 0.48 (~2.1× slower) 0.69 0.47
LongWhitespaceRuns 0.36 (~2.8× slower) 0.56 0.51

Result: every native scenario is back to parity with main. DeeplyIndented moved from ~2× slower to 0.99–1.03, and LongWhitespaceRuns from ~2–2.8× slower to 0.92–1.21. This is the expected outcome of the gate — on native OperatingSystem.IsBrowser() folds to false, the scalar pre-scan is dead-code-eliminated, and codegen is identical to main.

The one mild negative — x64 LongWhitespaceRuns at 0.92 — is measurement noise, not a systematic regression: the same benchmark is 21% and 7% faster on the other two machines, and DeeplyIndented (also long-run) is neutral across all three. Inconsistent sign on a provably codegen-identical path = run-to-run variance on cloud VMs.

WASM parity is preserved by construction: the gate only wraps the previously-validated browser hybrid in if (IsBrowser()) — it doesn't change the browser code path — so the WASM AOT numbers in the PR description (indented 16.1 ns vs main 41.0 ns) still hold. Correctness was re-verified after the helper extraction: differential harness over both paths vs. the scalar oracle (32.2M cases, 0 mismatches) + 90/90 end-to-end reader assertions.

Net: the reported WASM regression (#75553) is fixed, and native is unaffected.

Note

This comment was authored with the assistance of GitHub Copilot.

@eiriktsarpalis

Copy link
Copy Markdown
Member Author

Supplementary: is SearchValues worth it vs. no SearchValues at all?

The two runs above compare this PR against main, but main already uses SearchValues, so neither answers the underlying question: what does SearchValues actually buy over the pre-#129701 pure-scalar loop — the justification for vectorizing in the first place, and the reason the PR keeps it on native and promotes to it on WASM.

This is a self-contained three-way microbenchmark. All three SkipWhiteSpace strategies are implemented in the harness itself (they're the exact algorithms the correctness harness already proved equivalent over 32.2M cases), so it does not depend on the runtime build — the PR and main toolchain columns will be identical; read either one. Scalar (no SearchValues, the pre-#129701 behavior) is the BDN baseline, so every ratio is relative to not using SearchValues.

@EgorBot -linux_amd -linux_arm64 -osx_arm64

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Bench).Assembly).Run(args);

public class Bench
{
    private const byte Space = (byte)' ';
    private const byte Tab = (byte)'\t';
    private const byte CR = (byte)'\r';
    private const byte LF = (byte)'\n';
    private const int MaxScalarWhiteSpaceScanLength = 16;

    private static readonly SearchValues<byte> s_ws = SearchValues.Create(" \t\r\n"u8);

    private byte[] _indented = default!;
    private byte[] _minified = default!;
    private byte[] _deeplyIndented = default!;
    private byte[] _longRuns = default!;

    public enum Doc { Indented, Minified, DeeplyIndented, LongRuns }

    [Params(Doc.Indented, Doc.Minified, Doc.DeeplyIndented, Doc.LongRuns)]
    public Doc Scenario;

    private byte[] Current => Scenario switch
    {
        Doc.Indented => _indented,
        Doc.Minified => _minified,
        Doc.DeeplyIndented => _deeplyIndented,
        _ => _longRuns,
    };

    [GlobalSetup]
    public void Setup()
    {
        object graph = MakeGraph(depth: 4, breadth: 4);
        _indented = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = true });
        _minified = JsonSerializer.SerializeToUtf8Bytes(graph, new JsonSerializerOptions { WriteIndented = false });
        _deeplyIndented = JsonSerializer.SerializeToUtf8Bytes(MakeGraph(depth: 12, breadth: 2), new JsonSerializerOptions { WriteIndented = true });
        _longRuns = MakeLongRunDocument(elements: 2000, runLength: 48);
    }

    // Baseline: pure scalar loop, i.e. the pre-#129701 implementation (no SearchValues at all).
    [Benchmark(Baseline = true)]
    public long Scalar() => DriveScalar(Current);

    // Unconditional vectorized SearchValues scan = current `main`.
    [Benchmark]
    public long SearchValuesOnly() => DriveSearchValues(Current);

    // Scalar pre-scan promoting to SearchValues after 16 bytes = this PR's browser/WASM path.
    [Benchmark]
    public long Hybrid() => DriveHybrid(Current);

    private static long DriveScalar(ReadOnlySpan<byte> span)
    {
        int pos = 0; long line = 0, bytePos = 0;
        while (pos < span.Length) { pos = SkipScalar(span, pos, ref line, ref bytePos); pos = SkipToken(span, pos); }
        return line + bytePos;
    }

    private static long DriveSearchValues(ReadOnlySpan<byte> span)
    {
        int pos = 0; long line = 0, bytePos = 0;
        while (pos < span.Length) { pos = SkipSearchValues(span, pos, ref line, ref bytePos); pos = SkipToken(span, pos); }
        return line + bytePos;
    }

    private static long DriveHybrid(ReadOnlySpan<byte> span)
    {
        int pos = 0; long line = 0, bytePos = 0;
        while (pos < span.Length) { pos = SkipHybrid(span, pos, ref line, ref bytePos); pos = SkipToken(span, pos); }
        return line + bytePos;
    }

    private static int SkipToken(ReadOnlySpan<byte> span, int pos)
    {
        while (pos < span.Length)
        {
            byte v = span[pos];
            if (v is Space or Tab or CR or LF) break;
            pos++;
        }
        return pos;
    }

    private static int SkipScalar(ReadOnlySpan<byte> span, int pos, ref long line, ref long bytePos)
    {
        for (; pos < span.Length; pos++)
        {
            byte val = span[pos];
            if (val is not Space and not CR and not LF and not Tab) break;
            if (val == LF) { line++; bytePos = 0; } else { bytePos++; }
        }
        return pos;
    }

    private static int SkipSearchValues(ReadOnlySpan<byte> span, int pos, ref long line, ref long bytePos)
    {
        ReadOnlySpan<byte> remaining = span.Slice(pos);
        int idx = IndexOfFirstNonWhiteSpace(remaining);
        if (idx > 0)
        {
            (int newLines, int lastLineFeedIndex) = CountNewLines(remaining.Slice(0, idx));
            line += newLines;
            if (lastLineFeedIndex >= 0) { bytePos = idx - lastLineFeedIndex - 1; } else { bytePos += idx; }
        }
        return pos + idx;
    }

    private static int SkipHybrid(ReadOnlySpan<byte> span, int pos, ref long line, ref long bytePos)
    {
        int run = 0;
        for (; pos < span.Length; pos++)
        {
            byte val = span[pos];
            if (val is not Space and not CR and not LF and not Tab) break;
            if (val == LF) { line++; bytePos = 0; } else { bytePos++; }

            if (++run == MaxScalarWhiteSpaceScanLength)
            {
                return SkipSearchValues(span, pos + 1, ref line, ref bytePos);
            }
        }
        return pos;
    }

    private static int IndexOfFirstNonWhiteSpace(ReadOnlySpan<byte> span)
    {
        int index = span.IndexOfAnyExcept(s_ws);
        return index < 0 ? span.Length : index;
    }

    private static (int, int) CountNewLines(ReadOnlySpan<byte> data)
    {
        int lastLineFeedIndex = data.LastIndexOf(LF);
        int newLines = 0;
        if (lastLineFeedIndex >= 0)
        {
            newLines = 1;
            data = data.Slice(0, lastLineFeedIndex);
            newLines += data.Count(LF);
        }
        return (newLines, lastLineFeedIndex);
    }

    private static object MakeGraph(int depth, int breadth)
    {
        if (depth == 0)
        {
            return "some string leaf value";
        }

        var node = new Dictionary<string, object>();
        for (int i = 0; i < breadth; i++)
        {
            node["child_" + i] = MakeGraph(depth - 1, breadth);
        }
        node["number"] = 1234567;
        node["flag"] = true;
        node["nullable"] = null!;
        node["array"] = new object[] { 1, 2, 3, "four", false };
        return node;
    }

    private static byte[] MakeLongRunDocument(int elements, int runLength)
    {
        string ws = new string(' ', runLength - 1);
        var sb = new StringBuilder();
        sb.Append('[');
        for (int i = 0; i < elements; i++)
        {
            if (i > 0)
            {
                sb.Append(',');
            }
            sb.Append('\n').Append(ws).Append(i);
        }
        sb.Append('\n').Append(']');
        return Encoding.UTF8.GetBytes(sb.ToString());
    }
}

Note

This comment and benchmark were authored with the assistance of GitHub Copilot.

Replace the hybrid scalar-pre-scan + vectorized-promotion approach with a straightforward gate: on browser (Mono/WASM AOT) SkipWhiteSpace uses the plain pre-existing scalar loop; every other target keeps the unconditional vectorized SearchValues scan.

OperatingSystem.IsBrowser() folds to a per-target constant, so exactly one path survives after dead-code elimination. Native codegen stays byte-identical to main (no regression, and we forgo the browser vectorization win by design), while the reported WASM regression (#75553) is removed. Removes the SkipWhiteSpaceVectorized helper and the MaxScalarWhiteSpaceScanLength constant the hybrid required.

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

Copilot-Session: c9c7bcc8-88ac-491c-82d7-1a208f5a438e
Copilot AI review requested due to automatic review settings July 13, 2026 10:55

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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

@eiriktsarpalis
eiriktsarpalis merged commit 6d47555 into main Jul 14, 2026
86 of 89 checks passed
@eiriktsarpalis
eiriktsarpalis deleted the eiriktsarpalis-perf-regression-triage-75553 branch July 14, 2026 16:54
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-System.Text.Json

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants