[efficiency-improver] perf: eliminate GetRawText() string allocation in STJ deserializer converters#16210
Conversation
…nverters Replace StjSafe.Deserialize<T>(element.GetRawText(), options) with StjSafe.Deserialize<T>(element, options) across 9 serialization converters in the IPC hot path. JsonElement.GetRawText() allocates a new string (the raw JSON fragment) which is then immediately reparsed by JsonSerializer.Deserialize<T>(string,…). The JsonElement overload of Deserialize<T> reads directly from the element, skipping both the string allocation and the re-parse. Affected converters (all inside #if NETCOREAPP): - AttachmentConverters.cs - TestObjectBaseConverter.cs - ExceptionConverter.cs - TestRunChangedEventArgsConverter.cs - TestCaseConverter.cs - TestRunCompleteEventArgsConverter.cs - TestExecutionContextConverter.cs - AfterTestRunEndResultConverter.cs - DiscoveryCriteriaConverter.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves IPC deserialization efficiency in Microsoft.TestPlatform.CommunicationUtilities by removing JsonElement.GetRawText() string allocations and avoiding the string-based deserialize path in multiple System.Text.Json converter implementations.
Changes:
- Replaced
StjSafe.Deserialize<T>(element.GetRawText(), options)withStjSafe.Deserialize<T>(element, options)across 9 NETCOREAPP-only converters. - Centralized the improvement via the existing
DeserializeProperty<T>helper pattern where present, keeping behavior consistent while reducing allocations.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestRunCompleteEventArgsConverter.cs | Uses JsonElement overload to avoid GetRawText() allocation when deserializing optional properties. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestRunChangedEventArgsConverter.cs | Same substitution in shared property-deserialization helper to reduce allocations. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestObjectBaseConverter.cs | Deserializes TestProperty directly from JsonElement for property bag keys. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestExecutionContextConverter.cs | Deserializes FilterOptions from JsonElement without intermediate string creation. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/TestCaseConverter.cs | Deserializes TestProperty keys directly from JsonElement to reduce per-test allocations. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/ExceptionConverter.cs | Deserializes nested InnerException directly from JsonElement. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/DiscoveryCriteriaConverter.cs | Updates helper to deserialize properties from JsonElement instead of raw text. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/AttachmentConverters.cs | Deserializes UriDataAttachment directly from JsonElement when enumerating attachments. |
| src/Microsoft.TestPlatform.CommunicationUtilities/Serialization/AfterTestRunEndResultConverter.cs | Updates helper to use JsonElement deserialize overload and avoid GetRawText() allocation. |
nohwnd
left a comment
There was a problem hiding this comment.
🧠 Expert Review — PR #16210
Dimensions activated: IPC Transport & Protocol Stability · Cross-TFM & Framework Resolution · Performance & Allocations · Resource & IDisposable Management · Algorithmic Correctness
Summary
No issues found. This is a correct and clean performance optimization.
What was verified:
StjSafeoverload exists —StjSafe.Deserialize<T>(JsonElement, JsonSerializerOptions)is already present inStjSafe.cs(lines 41–47) with the same trim/AOT suppression as the string overload.- JsonElement lifetime is safe in all 9 cases — Every changed
JsonElement(prop,attachment,keyElement,innerProp,filterOptions) is used while its parentJsonDocumentis still inside itsusingblock. No use-after-dispose risk. - Behavioral equivalence —
JsonSerializer.Deserialize<T>(JsonElement, options)andDeserialize<T>(element.GetRawText(), options)produce identical deserialized objects. TheJsonElementpath reads the same already-parsed token tree thatGetRawText()would serialize back to UTF-16 and re-parse. - No wire format change — This only affects the deserialization read path; serialization is untouched.
- Cross-TFM safety — All 9 changed call sites are inside
#if NETCOREAPPblocks.JsonSerializer.Deserialize<T>(JsonElement, ...)has been available since .NET Core 3.0. - Scope completeness — The one remaining
GetRawText()call inTestCaseConverter.cs(line 68:token.GetRawText().Trim('"')) is intentionally untouched — it is a string-coercion for non-string JSON values, not aDeserialize+GetRawTextpattern. - PR description accuracy — Title, description, and the 9-converter table match the diff exactly.
🧠 Reviewed by expert-reviewing workflow · PR #16210
🧠 Reviewed by Expert Code Reviewer 🧠
Goal and Rationale
Eliminate redundant string allocation and re-parse on the IPC deserialization path by replacing
StjSafe.Deserialize<T>(element.GetRawText(), options)withStjSafe.Deserialize<T>(element, options)across 9 STJ converter classes.Focus area: Network & I/O Efficiency (IPC deserialization path)
Problem
JsonElement.GetRawText()allocates a new heap string containing the raw JSON fragment. Passing this string toJsonSerializer.Deserialize<T>(string, ...)then causes a second parse — the serializer must re-tokenize the string from scratch.JsonSerializer.Deserialize<T>(JsonElement, ...)reads directly from the already-parsedJsonElement, skipping both the string allocation and the re-parse entirely.StjSafealready exposes this overload.Approach
Single-line substitution in 9 converters (all inside
#if NETCOREAPP):AttachmentConverters.csattachmentTestObjectBaseConverter.cskeyElementExceptionConverter.csinnerPropTestRunChangedEventArgsConverter.cspropTestCaseConverter.cskeyElementTestRunCompleteEventArgsConverter.cspropTestExecutionContextConverter.csfilterOptionsAfterTestRunEndResultConverter.cspropDiscoveryCriteriaConverter.cspropThe shared
DeserializeProperty<T>helper pattern (used by 4 converters) makes each replacement a one-line change.Energy Efficiency Evidence
Proxy metric: Memory allocation (GC pressure on the IPC deserialization path)
Each
GetRawText()call allocates a heap string proportional to the JSON fragment size. Typical fragment sizes for the affected types:TestPropertykey object: ~60–150 bytes of JSON → ~120–300 bytes UTF-16 stringUriDataAttachment: ~80–200 bytes → ~160–400 bytesFilterOptions/TestExecutionContextsub-objects: ~20–100 bytesThese allocations occur on every
TestCaseandTestResultdeserialization, and on everyDiscoveryCriteria/ execution context message receive. Eliminating all 9 sites reduces Gen0 heap pressure on the IPC receive thread.Green Software Foundation context:
dotnet testinvocation, proportional to test count and run frequencyTrade-offs
None.
Deserialize<T>(JsonElement, ...)produces identical output for every case — it reads the same token tree thatGetRawText()serialises back to a string. Change is fully covered by existing round-trip tests.Reproducibility
Test Status
Microsoft.TestPlatform.CommunicationUtilities.UnitTests: 632/632 passed, 0 failures