[efficiency-improver] Make Jsonite JsonWriter's type-dispatch table static (avoid per-call Dictionary+closures)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 15/100
- Issue type
- Refactor
- Clarity
- Clearly specified
- Activity status
- Stale
- Tech stack
- csharp
- Domain
- backend, performance
Research direction
The change is in src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Jsonite/JsonWriter.cs, with coverage in test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/JsoniteTests.cs. Review the existing branch and patch, then run the listed build and server-mode tests; done means the build passes and serialization behavior remains unchanged without per-call dispatch-table allocation.
Written by the indexing model from the issue text.
Description
[!TIP]
Your pull request is ready to create! 🎉 ✅Everything is OK—the changes have been pushed to branch
efficiency/jsonwriter-static-dispatch-table-634c00ef659ffb9d. Please review the changes, including any protected files, before creating the pull request.The original pull request description is below.
Goal and rationale
Jsonite.JsonWriter — the JSON serializer used on the net462/netstandard2.0 server-mode code path — rebuilt an 18-entry Dictionary<Type, Action<object>>, with 18 closures each capturing the per-call TextWriter, inside its constructor on every single Serialize() call. This code runs on every RPC message and test-node update sent by a server-mode host running on .NET Framework (e.g. classic MSTest test projects talking JSON-RPC to an IDE or dotnet test), so the wasted allocation scales with test count.
Focus area
Code-Level Efficiency — redundant per-call allocation of a dispatch table that has no per-instance state and can be built once.
Approach
Hoisted the dispatch table to a private static readonly Dictionary<Type, Action<JsonWriter, object>>, using delegates that take the owning JsonWriter as an explicit first parameter (instead of capturing writer/this in a closure per instance). The instance-scoped WriteString calls now go through w.WriteString(...). No behavior change: the table is populated once per process and shared across all JsonWriter instances/calls.
Energy efficiency evidence
Proxy metrics used: wall-clock execution time and GC.GetAllocatedBytesForCurrentThread() (CPU time and memory allocation are both direct proxies for energy: fewer CPU cycles and less GC/DRAM churn per call).
Benchmarked using the actual repository source (Jsonite/*.cs copied into a standalone console harness, !NETCOREAPP forced so the changed code path runs), serializing a small mix of representative values (string, int, double, bool, Guid, DateTime) 200,000 times:
| Old (per-call dictionary+closures) | New (static dispatch table) | Improvement | |
|---|---|---|---|
| Time | 464 ms | 141 ms | ~3.3x faster |
| Allocated | 740,000 KB (740 MB) | 73,000 KB (73 MB) | ~90% less |
I also verified serialization output is byte-for-byte identical before/after for DateTimeOffset, an anonymous object, an int[] array, a string with escapes, a long, and a float, using the same standalone harness.
Green Software Foundation context
- Energy Proportionality: the fix removes work that scaled with call count but had zero per-call variability in its result — the dispatch table is process-invariant, so building it once instead of once-per-call makes resource use proportional to actual demand (unique types encountered), not call volume.
- Hardware Efficiency: fewer heap allocations mean less GC pressure and better cache locality per call, reducing CPU cycles per RPC message.
Trade-offs
None of note. Delegates now take an extra JsonWriter parameter instead of closing over instance fields, which is a standard pattern for static dispatch tables and does not reduce readability — the intent ("static, allocation-free per-type dispatch") is documented in a comment above the field.
Reproducibility
# Build to confirm the netstandard2.0/net462-only code path still compiles:
./build.sh
.dotnet/dotnet build src/Platform/Microsoft.Testing.Platform -c Debug -f netstandard2.0 --no-restore
# Run the .NET-hosted server-mode tests (System.Text.Json path, unaffected but confirms no regression):
.dotnet/dotnet build test/UnitTests/Microsoft.Testing.Platform.UnitTests -c Debug -f net8.0 --no-restore
artifacts/bin/Microsoft.Testing.Platform.UnitTests/Debug/net8.0/Microsoft.Testing.Platform.UnitTests --filter "FullyQualifiedName~ServerMode"
The Jsonite-specific test file (test/UnitTests/Microsoft.Testing.Platform.UnitTests/ServerMode/JsoniteTests.cs) is net462-only and only compiles on Windows CI (net462 is conditional on $(OS) == 'Windows_NT' and this sandbox is Linux), so it could not be run directly here. I validated correctness by copying the actual Jsonite/*.cs sources (old vs. new) into a standalone console harness with !NETCOREAPP forced, compiling and running both versions, and diffing their output for representative values — all outputs matched exactly.
Test Status
./build.sh(full repo restore + build, all target frameworks includingnetstandard2.0/net462): 0 warnings, 0 errors.dotnet format whitespace TestFx.slnx --verify-no-changes --include <file>: clean (after restoring the file's UTF-8 BOM per repo convention).Microsoft.Testing.Platform.UnitTests(net8.0, full suite): 2564 passed, 0 failed, 21 skipped (pre-existing skips).Microsoft.Testing.Platform.ServerMode.Client.Sources.UnitTests(net8.0, full suite): 105 passed, 0 failed, 2 skipped (pre-existing skips).JsoniteTests.cs(net462-only, Windows-only in CI): not runnable in this Linux sandbox; behavior equivalence independently verified via the standalone harness described above.
[!NOTE]
GitHub Actions is not permitted to create or approve pull requests in this repository.
The changes have been pushed to branch
efficiency/jsonwriter-static-dispatch-table-634c00ef659ffb9dand are ready to review.
To fix the permissions issue, go to Settings → Actions → General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ
Show patch preview (34 of 131 lines)
From f6c7170a408f66a3ed94a21cf97c5ddd36aae9d3 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: acf0a077f37c1a4c23500f05fb6f90c526694c3f
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 24 Sep 2026 22:03:58 +0000
Subject: [PATCH] Make Jsonite JsonWriter's type-dispatch table static
Jsonite.JsonWriter (the JSON serializer used on the net462/netstandard2.0
server-mode code path) rebuilt an 18-entry Dictionary<Type, Action<object>>,
with 18 closures each capturing the per-call TextWriter, inside its
constructor on every single Serialize() call. This runs on every RPC message
and test-node update sent by a server-mode host running on .NET Framework.
Hoist the dispatch table to a static readonly field keyed by delegates that
take the owning JsonWriter as an explicit first parameter, so no per-call
allocation or closure capture is needed. Serialization output is unchanged
(verified via a standalone harness compiling the actual repo source with the
!NETCOREAPP path forced, comparing old vs. new output byte-for-byte for
strings, numbers, Guid, DateTime(Offset), arrays, and objects).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../JsonRpc/Json/Jsonite/JsonWriter.cs | 65 ++++++++++---------
1 file changed, 36 insertions(+), 29 deletions(-)
diff --git a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Jsonite/JsonWriter.cs b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Jsonite/JsonWriter.cs
index 6a77ebc..f5bed06 100644
--- a/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Jsonite/JsonWriter.cs
+++ b/src/Platform/Microsoft.Testing.Platform/ServerMode/JsonRpc/Json/Jsonite/JsonWriter.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// Copyright(c) 2016, Alexa
... (truncated)
[!WARNING]
Firewall blocked 94 domains
The following domains were blocked by the firewall during workflow execution:
0t3vsblobprodcus362.vsblob.vsassets.io11vvsblobprodcus336.vsblob.vsassets.io1javsblobprodcus364.vsblob.vsassets.io1k9vsblobprodcus379.vsblob.vsassets.io1oavsblobprodcus350.vsblob.vsassets.io1p7vsblobprodcus324.vsblob.vsassets.io1s1vsblobprodcus386.vsblob.vsassets.io2kmvsblobprodcus39.vsblob.vsassets.io2zrvsblobprodcus388.vsblob.vsassets.io37bvsblobprodcus311.vsblob.vsassets.io37cvsblobprodcus359.vsblob.vsassets.io4m6vsblobprodcus384.vsblob.vsassets.io4myvsblobprodcus32.vsblob.vsassets.io4vyvsblobprodcus361.vsblob.vsassets.io4zjvsblobprodcus390.vsblob.vsassets.io51yvsblobprodcus36.vsblob.vsassets.io5dkvsblobprodcus355.vsblob.vsassets.io5rqvsblobprodcus385.vsblob.vsassets.io6s7vsblobprodcus313.vsblob.vsassets.io7devsblobprodcus323.vsblob.vsassets.io7k6vsblobprodcus337.vsblob.vsassets.io7tjvsblobprodcus341.vsblob.vsassets.io80zvsblobprodcus35.vsblob.vsassets.io8xbvsblobprodcus382.vsblob.vsassets.io96bvsblobprodcus338.vsblob.vsassets.ioajhvsblobprodcus363.vsblob.vsassets.iobcnvsblobprodcus378.vsblob.vsassets.iobuilds.dotnet.microsoft.comc50vsblobprodcus330.vsblob.vsassets.ioc78vsblobprodcus322.vsblob.vsassets.iocflvsblobprodcus383.vsblob.vsassets.ioci.dot.netckzvsblobprodcus347.vsblob.vsassets.iod0svsblobprodcus381.vsblob.vsassets.iodata.nuget.orgdc.services.visualstudio.comdlbvsblobprodcus316.vsblob.vsassets.ioe7bvsblobprodcus348.vsblob.vsassets.iofdpvsblobprodcus345.vsblob.vsassets.iofrdvsblobprodcus327.vsblob.vsassets.iogbsvsblobprodcus365.vsblob.vsassets.iogervsblobprodcus329.vsblob.vsassets.ioh6tvsblobprodcus346.vsblob.vsassets.ioi1qvsblobprodcus353.vsblob.vsassets.ioibzvsblobprodcus369.vsblob.vsassets.ioimzvsblobprodcus368.vsblob.vsassets.iojd4vsblobprodcus366.vsblob.vsassets.iojosvsblobprodcus372.vsblob.vsassets.iojrqvsblobprodcus343.vsblob.vsassets.iok0ivsblobprodcus356.vsblob.vsassets.iok4kvsblobprodcus344.vsblob.vsassets.iokgfvsblobprodcus314.vsblob.vsassets.iokh4vsblobprodcus325.vsblob.vsassets.iokijvsblobprodcus387.vsblob.vsassets.iokmuvsblobprodcus389.vsblob.vsassets.iokxqvsblobprodcus376.vsblob.vsassets.iol49vsblobprodcus358.vsblob.vsassets.iol7avsblobprodcus319.vsblob.vsassets.ioljcvsblobprodcus317.vsblob.vsassets.iolylvsblobprodcus31.vsblob.vsassets.iom16vsblobprodcus374.vsblob.vsassets.iom6xvsblobprodcus342.vsblob.vsassets.iom8dvsblobprodcus37.vsblob.vsassets.iomfjvsblobprodcus373.vsblob.vsassets.ion3kvsblobprodcus335.vsblob.vsassets.ionn8vsblobprodcus340.vsblob.vsassets.ioo3svsblobprodcus318.vsblob.vsassets.ioofvvsblobprodcus315.vsblob.vsassets.iop2ovsblobprodcus312.vsblob.vsassets.iopc2vsblobprodcus360.vsblob.vsassets.iopdfvsblobprodcus380.vsblob.vsassets.iope3vsblobprodcus354.vsblob.vsassets.iope4vsblobprodcus351.vsblob.vsassets.iopkgs.dev.azure.comrcxvsblobprodcus328.vsblob.vsassets.ios4uvsblobprodcus326.vsblob.vsassets.ios8mvsblobprodcus38.vsblob.vsassets.iosc4vsblobprodcus331.vsblob.vsassets.iose1vsblobprodcus349.vsblob.vsassets.iosouthcentralus0.in.applicationinsights.azure.comsqdvsblobprodcus333.vsblob.vsassets.iost8vsblobprodcus339.vsblob.vsassets.iotphvsblobprodcus375.vsblob.vsassets.iou3hvsblobprodcus371.vsblob.vsassets.iou6ovsblobprodcus377.vsblob.vsassets.ioukkvsblobprodcus352.vsblob.vsassets.iouy6vsblobprodcus34.vsblob.vsassets.iov53vsblobprodcus320.vsblob.vsassets.iovb4vsblobprodcus33.vsblob.vsassets.iovwvvsblobprodcus334.vsblob.vsassets.iox3yvsblobprodcus370.vsblob.vsassets.ioxupvsblobprodcus332.vsblob.vsassets.ioyluvsblobprodcus367.vsblob.vsassets.ioyttvsblobprodcus357.vsblob.vsassets.ioTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:network: allowed: - defaults - "0t3vsblobprodcus362.vsblob.vsassets.io" - "11vvsblobprodcus336.vsblob.vsassets.io" - "1javsblobprodcus364.vsblob.vsassets.io" - "1k9vsblobprodcus379.vsblob.vsassets.io" - "1oavsblobprodcus350.vsblob.vsassets.io" - "1p7vsblobprodcus324.vsblob.vsassets.io" - "1s1vsblobprodcus386.vsblob.vsassets.io" - "2kmvsblobprodcus39.vsblob.vsassets.io" - "2zrvsblobprodcus388.vsblob.vsassets.io" - "37bvsblobprodcus311.vsblob.vsassets.io" - "37cvsblobprodcus359.vsblob.vsassets.io" - "4m6vsblobprodcus384.vsblob.vsassets.io" - "4myvsblobprodcus32.vsblob.vsassets.io" - "4vyvsblobprodcus361.vsblob.vsassets.io" - "4zjvsblobprodcus390.vsblob.vsassets.io" - "51yvsblobprodcus36.vsblob.vsassets.io" - "5dkvsblobprodcus355.vsblob.vsassets.io" - "5rqvsblobprodcus385.vsblob.vsassets.io" - "6s7vsblobprodcus313.vsblob.vsassets.io" - "7devsblobprodcus323.vsblob.vsassets.io" - "7k6vsblobprodcus337.vsblob.vsassets.io" - "7tjvsblobprodcus341.vsblob.vsassets.io" - "80zvsblobprodcus35.vsblob.vsassets.io" - "8xbvsblobprodcus382.vsblob.vsassets.io" - "96bvsblobprodcus338.vsblob.vsassets.io" - "ajhvsblobprodcus363.vsblob.vsassets.io" - "bcnvsblobprodcus378.vsblob.vsassets.io" - "builds.dotnet.microsoft.com" - "c50vsblobprodcus330.vsblob.vsassets.io" - "c78vsblobprodcus322.vsblob.vsassets.io" - "cflvsblobprodcus383.vsblob.vsassets.io" - "ci.dot.net" - "ckzvsblobprodcus347.vsblob.vsassets.io" - "d0svsblobprodcus381.vsblob.vsassets.io" - "data.nuget.org" - "dc.services.visualstudio.com" - "dlbvsblobprodcus316.vsblob.vsassets.io" - "e7bvsblobprodcus348.vsblob.vsassets.io" - "fdpvsblobprodcus345.vsblob.vsassets.io" - "frdvsblobprodcus327.vsblob.vsassets.io" - "gbsvsblobprodcus365.vsblob.vsassets.io" - "gervsblobprodcus329.vsblob.vsassets.io" - "h6tvsblobprodcus346.vsblob.vsassets.io" - "i1qvsblobprodcus353.vsblob.vsassets.io" - "ibzvsblobprodcus369.vsblob.vsassets.io" - "imzvsblobprodcus368.vsblob.vsassets.io" - "jd4vsblobprodcus366.vsblob.vsassets.io" - "josvsblobprodcus372.vsblob.vsassets.io" - "jrqvsblobprodcus343.vsblob.vsassets.io" - "k0ivsblobprodcus356.vsblob.vsassets.io" - "k4kvsblobprodcus344.vsblob.vsassets.io" - "kgfvsblobprodcus314.vsblob.vsassets.io" - "kh4vsblobprodcus325.vsblob.vsassets.io" - "kijvsblobprodcus387.vsblob.vsassets.io" - "kmuvsblobprodcus389.vsblob.vsassets.io" - "kxqvsblobprodcus376.vsblob.vsassets.io" - "l49vsblobprodcus358.vsblob.vsassets.io" - "l7avsblobprodcus319.vsblob.vsassets.io" - "ljcvsblobprodcus317.vsblob.vsassets.io" - "lylvsblobprodcus31.vsblob.vsassets.io" - "m16vsblobprodcus374.vsblob.vsassets.io" - "m6xvsblobprodcus342.vsblob.vsassets.io" - "m8dvsblobprodcus37.vsblob.vsassets.io" - "mfjvsblobprodcus373.vsblob.vsassets.io" - "n3kvsblobprodcus335.vsblob.vsassets.io" - "nn8vsblobprodcus340.vsblob.vsassets.io" - "o3svsblobprodcus318.vsblob.vsassets.io" - "ofvvsblobprodcus315.vsblob.vsassets.io" - "p2ovsblobprodcus312.vsblob.vsassets.io" - "pc2vsblobprodcus360.vsblob.vsassets.io" - "pdfvsblobprodcus380.vsblob.vsassets.io" - "pe3vsblobprodcus354.vsblob.vsassets.io" - "pe4vsblobprodcus351.vsblob.vsassets.io" - "pkgs.dev.azure.com" - "rcxvsblobprodcus328.vsblob.vsassets.io" - "s4uvsblobprodcus326.vsblob.vsassets.io" - "s8mvsblobprodcus38.vsblob.vsassets.io" - "sc4vsblobprodcus331.vsblob.vsassets.io" - "se1vsblobprodcus349.vsblob.vsassets.io" - "southcentralus0.in.applicationinsights.azure.com" - "sqdvsblobprodcus333.vsblob.vsassets.io" - "st8vsblobprodcus339.vsblob.vsassets.io" - "tphvsblobprodcus375.vsblob.vsassets.io" - "u3hvsblobprodcus371.vsblob.vsassets.io" - "u6ovsblobprodcus377.vsblob.vsassets.io" - "ukkvsblobprodcus352.vsblob.vsassets.io" - "uy6vsblobprodcus34.vsblob.vsassets.io" - "v53vsblobprodcus320.vsblob.vsassets.io" - "vb4vsblobprodcus33.vsblob.vsassets.io" - "vwvvsblobprodcus334.vsblob.vsassets.io" - "x3yvsblobprodcus370.vsblob.vsassets.io" - "xupvsblobprodcus332.vsblob.vsassets.io" - "yluvsblobprodcus367.vsblob.vsassets.io" - "yttvsblobprodcus357.vsblob.vsassets.io"See Network Configuration for more information.
🤖 Automated content by GitHub Copilot. Generated by the Efficiency Improver workflow. · copilot · auto · 588 AIC · ⌖ 13.9 AIC · ⊞ 21.2K · [◷]( · ◷)
Add this agentic workflow to your repo
To install this agentic workflow, run
gh aw add githubnext/agentics/workflows/efficiency-improver.md@main
- Dominant language
- C#
- Stars
- 1k
- Forks
- 312
- Avg merge
- 8h 14m
- Merged PRs (30d)
- 497
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/testfx
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
[file-diet] Refactor AzureDevOpsTestResultsPublisher.cs (578 lines) into focused partial-class files Opentype/automation type/tech-debt
-
area/mtp needs/triage
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
area/winui
-
type/automation type/tech-debt
Difficulty 5/5 Over a week Newbie friendliness 10/100
All issues in microsoft/testfx
Similar issues
-
untriaged
Difficulty 1/5 Under an hour Newbie friendliness 88/100
dotnet/dotnet-api-docs#13095 ·
-
area-deployment area-integrations triage:bot-seen
Difficulty 2/5 Half a day Newbie friendliness 86/100
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 90/100
newrelic/newrelic-dotnet-agent#3850 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
LuckyPennySoftware/AutoMapper#4660 ·
-
clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr issue-rating: 🌊 off-meta tidepool P2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
openclaw/openclaw-windows-packaging#116 · 1 comment · 1 reaction ·