Add an IArrayPool<char> implementation for vendored Newtonsoft.JSON#8228
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad20bb98cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ad20bb9 to
7c8bf45
Compare
| // Default values for StreamReader, but with leaveOpen:true | ||
| using var streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); |
There was a problem hiding this comment.
Previously this wasn't disposing the StreamReader, presumably because that closes the MemoryStream by default. I think technically that was ok, but seemed better to be explicit here, especially as we now have to dispose the JsonTextReader, and don't want there to be any ambiguity around it
| scopeMembers.Add(result); | ||
|
|
||
| var reader = new JsonTextReader(new StringReader(expressionJson)); | ||
| using var reader = new JsonTextReader(new StringReader(expressionJson)) { ArrayPool = JsonArrayPool.Shared }; |
There was a problem hiding this comment.
I'm pretty sure this was just an accidental miss of the using
| @@ -276,11 +277,11 @@ internal static void WriteValue(JsonWriter writer, object? value) | |||
| } | |||
|
|
|||
| internal static JsonTextWriter GetJsonWriter(StringBuilder builder) | |||
There was a problem hiding this comment.
The callers "own" and handle disposing the returned object
7c8bf45 to
947d1d6
Compare
BenchmarksBenchmark execution time: 2026-03-06 14:56:53 Comparing candidate commit a33aae2 in PR branch Found 7 performance improvements and 13 performance regressions! Performance is the same for 153 metrics, 19 unstable metrics. scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs net472
scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net6.0
scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery net472
scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync netcoreapp3.1
scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync net472
scenario:Benchmarks.Trace.GraphQLBenchmark.ExecuteAsync netcoreapp3.1
scenario:Benchmarks.Trace.HttpClientBenchmark.SendAsync net472
scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatAspectBenchmark netcoreapp3.1
scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog netcoreapp3.1
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan net6.0
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes net6.0
scenario:Benchmarks.Trace.TraceAnnotationsBenchmark.RunOnMethodBegin net6.0
|
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8228) and master. ✅ No regressions detected - check the details below Full Metrics ComparisonFakeDbCommand
HttpMessageHandler
Comparison explanationExecution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard. Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph). Duration chartsFakeDbCommand (.NET Framework 4.8)gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (77ms) : 75, 79
master - mean (75ms) : 73, 77
section Bailout
This PR (8228) - mean (81ms) : 79, 83
master - mean (79ms) : 78, 81
section CallTarget+Inlining+NGEN
This PR (8228) - mean (1,106ms) : 1058, 1154
master - mean (1,089ms) : 1047, 1130
FakeDbCommand (.NET Core 3.1)gantt
title Execution time (ms) FakeDbCommand (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (119ms) : 115, 123
master - mean (117ms) : 113, 120
section Bailout
This PR (8228) - mean (122ms) : 119, 124
master - mean (118ms) : 115, 121
section CallTarget+Inlining+NGEN
This PR (8228) - mean (780ms) : 715, 846
master - mean (766ms) : 708, 823
FakeDbCommand (.NET 6)gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (108ms) : 104, 112
master - mean (104ms) : 101, 107
section Bailout
This PR (8228) - mean (108ms) : 106, 111
master - mean (105ms) : 103, 107
section CallTarget+Inlining+NGEN
This PR (8228) - mean (770ms) : 691, 848
master - mean (768ms) : 707, 828
FakeDbCommand (.NET 8)gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (106ms) : 103, 108
master - mean (103ms) : 100, 105
section Bailout
This PR (8228) - mean (107ms) : 105, 110
master - mean (103ms) : 101, 105
section CallTarget+Inlining+NGEN
This PR (8228) - mean (703ms) : 672, 733
master - mean (681ms) : 651, 712
HttpMessageHandler (.NET Framework 4.8)gantt
title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (197ms) : 190, 203
master - mean (195ms) : 191, 199
section Bailout
This PR (8228) - mean (199ms) : 195, 203
master - mean (199ms) : 196, 202
section CallTarget+Inlining+NGEN
This PR (8228) - mean (1,166ms) : 1097, 1235
master - mean (1,155ms) : 1095, 1215
HttpMessageHandler (.NET Core 3.1)gantt
title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (281ms) : 274, 287
master - mean (279ms) : 272, 286
section Bailout
This PR (8228) - mean (282ms) : 277, 286
master - mean (279ms) : 275, 284
section CallTarget+Inlining+NGEN
This PR (8228) - mean (949ms) : 907, 992
master - mean (950ms) : 913, 986
HttpMessageHandler (.NET 6)gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (273ms) : 266, 279
master - mean (274ms) : 268, 279
section Bailout
This PR (8228) - mean (274ms) : 270, 278
master - mean (272ms) : 269, 276
section CallTarget+Inlining+NGEN
This PR (8228) - mean (942ms) : 915, 969
master - mean (934ms) : 903, 965
HttpMessageHandler (.NET 8)gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8228) - mean (272ms) : 265, 279
master - mean (270ms) : 265, 275
section Bailout
This PR (8228) - mean (272ms) : 266, 277
master - mean (270ms) : 266, 275
section CallTarget+Inlining+NGEN
This PR (8228) - mean (848ms) : 810, 885
master - mean (836ms) : 813, 859
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
947d1d6 to
dd07383
Compare
33c73ce to
3d24011
Compare
dd07383 to
a33aae2
Compare
## Summary of changes Adds JSON helper APIs to ensure we use the array pool where possible ## Reason for change There are various "helper" APIs, which are wrappers around Newtonsoft.JSON's `JsonSerializer` and `JsonReader`/`JsonWriter` APIs. In #8228 we updated the explicit usages to use an array pool implementation for `JsonReader`/`JsonWriter` calls, but they're used internally without a pool in the helper cases. This PR creates alternative implementations which _do_ use the pool, updates existing code to use them, and adds the existing APIs to the "banned API" list. > There's another possible approach, in which we update the vendored code to _always_ use the array pool. I was torn as to which is the better option, and went for this approach in the end, but I'm not wedded to it, so happy to take the alternative approach if people think it's preferable? ## Implementation details - Use 🤖 to find all the potential places that we need to convert. - Create "array pool" versions of the APIs - Update the call sites to use the new APIs - Add the original APIs to the list of "banned" APIs to avoid using them accidentally in the future ## Test coverage Covered by all our existing tests, added some unit tests to confirm the new tests behave as expected ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-940 Discovered this while exploring remote config optimizations, but should help lots of areas.
## Summary of changes Stop sending the fixed `RootVersion = 1` with every remote config request ## Reason for change Currently we're sending a fixed value of `RootVersion = 1` for all our remote config requests, but doing so causes the agent to repeatedly send us all the root certificates, significantly increasing the payload size, because it thinks we haven't seen them. Sending the "final" version, acknowledges it, and stops all the extra data, saving ~35kB per call. ## Implementation details - Deserialize the roots in `GetRcmResponse`, leaving them as base64 encoded `string`s (which is how they are sent in the payload) - When processing the response, de-encode the _last_ root object, and deserialize it - Using the `Base64DecodingStream` introduced in #8226 to avoid extra allocations - Using the `IArrayPool` from #8228 - Introduces a `MinimalTufRoot` (in contrast to `TufRoot`) so that we only materialize what we need (the `roots.signed.version` key) This implementation avoids ~35kB per call for subsequent remote config requests. ## Test coverage Added unit test, and did manual test with the real agent, to confirm the expected behaviour (reduction in data sent) ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-940 All part of the Remote Config perf stack
…#8228) ## Summary of changes Adds a simple `IArrayPool<char>` for use by Newtonsoft.JSON, and uses it everywhere we can ## Reason for change Newtonsoft.JSON fundamentally works with .NET's `char` type (UTF-16), (as opposed to System.Text.Json which works with UTF-8 where it can). To do so, it needs to create a bunch of `char[]` instances to use as buffers. The `JsonTextReader` and `JsonTextWriter` abstractions allow plugging in an `IArrayPool<char>` implementation, and luckily this matches (pretty much exactly) the API exposed by the `ArrayPool` in corelib (+ vendored), so it's easy to implement. This should help alleviate some GC pressure, as we currently do a fair amount of serializing and deserializing. ## Implementation details Pretty simple: - Implement `IArrayPool<char>` using `ArrayPool<char>.Shared` - Get 🤖 to find everywhere that we could use it (`JsonTextReader` and `JsonTextWriter`) and initialize - Fix some cases where these weren't being disposed. > [!WARNING] > It's important that we _do_ dispose these, so that the arrays are correctly returned to the pool, so that we don't leak memory There are actually other places we can update too, as this PR doesn't cover the common `JsonConvert.Serialize()` etc, but I'll follow up with those in a separate PR. ## Test coverage All the existing tests should pass. I worked on this as part of general perf work on remote config, so the results are a bit fuzzy (as I can't remember if it includes the savings from #8226 as well), but the results are pretty conclusive, especially for big payloads 😅 | Method | size | Mean | Error | Allocated | | ---------------------------- | ----- | ----------: | ------------: | ---------: | | DeserializeResponse_Original | Small | 22.71 ns | 2,076.771 ns | 23.83 KB | | DeserializeResponse_Updated | Small | 13.70 us | 0.186 us | 17.23 KB | | | | | | | | DeserializeResponse_Original | Big | 1,953.04 us | 58,665.219 ns |2,343.26 KB | | DeserializeResponse_Updated | Big | 614.46 us | 11.988 us | 252.37 KB | ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-940 Discovered this while exploring remote config optimizations
## Summary of changes Adds JSON helper APIs to ensure we use the array pool where possible ## Reason for change There are various "helper" APIs, which are wrappers around Newtonsoft.JSON's `JsonSerializer` and `JsonReader`/`JsonWriter` APIs. In #8228 we updated the explicit usages to use an array pool implementation for `JsonReader`/`JsonWriter` calls, but they're used internally without a pool in the helper cases. This PR creates alternative implementations which _do_ use the pool, updates existing code to use them, and adds the existing APIs to the "banned API" list. > There's another possible approach, in which we update the vendored code to _always_ use the array pool. I was torn as to which is the better option, and went for this approach in the end, but I'm not wedded to it, so happy to take the alternative approach if people think it's preferable? ## Implementation details - Use 🤖 to find all the potential places that we need to convert. - Create "array pool" versions of the APIs - Update the call sites to use the new APIs - Add the original APIs to the list of "banned" APIs to avoid using them accidentally in the future ## Test coverage Covered by all our existing tests, added some unit tests to confirm the new tests behave as expected ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-940 Discovered this while exploring remote config optimizations, but should help lots of areas.
## Summary of changes A variety of minor performance improvements to remote config ## Reason for change I did some initial benchmarking of remote config, as well as running a test app with ASM/Debugger enabled (which use RCM), and the results weren't great. Given we make RCM requests every 5s, smallish changes here should add up, though the real-world effect will be tricky to gauge. ## Implementation details Most of the individual changes are small. In summary: - Making work lazy where possible (delaying creating collections) - Don't re-create dictionaries inside loops if not required - Avoid creating empty dictionaries and collections for no-op RCM responses - Avoid creating empty collections in JSON objects when they're not in the JSON - Cache things that change rarely - The `ExtraServicesProvider` will rarely see new service names, so cache the array (as the collection is append-only) - Don't create a new request each time, just update any values that may have changed - Cache the capabilities array which will rarely change - There's actually a potential threading bug around the use of `BigInteger`. We probably could/should use `ulong` instead but that I'll look at that in a separate PR - Use abstractions introduced earlier in the stack (e.g. #8226, #8228) Additionally, I did a little bit of cleanup: - Remove unused members from the `IRcmSubscriptionManager` interface - Add `#nullable enable` to the RCM types (and fix nullability where required) ## Test coverage Mostly covered by existing unit tests, also did some manual testing. Finally, ran a few benchmarks, but it's a bit tricky to check reliably. This benchmark is benchmarking `_manager.SendRequest(_rcmTracer, _ => _steadyStateResponseTask)` and passing the same response every time. In practice, the response changes every time, so this isn't strictly representative, but with the changes to request caching, this should actually mean our improvements are _better_ than the original would be. The regression in the .NET duration is curious, but I'm not massively concerned, and the allocations are obviously down a lot at least | Method | Runtime | Mean | Error | Allocated | | ------------------------ | ------------------ | ----------: | -----------: | --------: | | PollSteadyState_Original | .NET 10.0 | 14,235.8 ns | 275.09 ns | 12.04 KB | | PollSteadyState_Updated | .NET 10.0 | 20,472.0 ns | 1,352.567 ns | 3.90 KB | | | | | | | | PollSteadyState_Original | .NET 6.0 | 33,915.7 ns | 3,339.42 ns | 12.27 KB | | PollSteadyState_Updated | .NET 6.0 | 14,979.7 ns | 447.784 ns | 3.99 KB | | | | | | | | PollSteadyState_Original | .NET Core 2.1 | 21,488.2 ns | 365.39 ns | 12.85 KB | | PollSteadyState_Updated | .NET Core 2.1 | 17,847.2 ns | 693.510 ns | 4.04 KB | | | | | | | | PollSteadyState_Original | .NET Core 3.1 | 18,925.2 ns | 304.16 ns | 12.23 KB | | PollSteadyState_Updated | .NET Core 3.1 | 15,407.7 ns | 313.26 ns | 4.01 KB | | | | | | | | PollSteadyState_Original | .NET Framework 4.8 | 20,946.8 ns | 320.06 ns | 14.21 KB | | PollSteadyState_Updated | .NET Framework 4.8 | 16,635.2 ns | 266.526 ns | 4.40 KB | ## Other details https://datadoghq.atlassian.net/browse/LANGPLAT-940 All part of the Remote Config perf stack. I think this is probably about the end of it for now
Summary of changes
Adds a simple
IArrayPool<char>for use by Newtonsoft.JSON, and uses it everywhere we canReason for change
Newtonsoft.JSON fundamentally works with .NET's
chartype (UTF-16), (as opposed to System.Text.Json which works with UTF-8 where it can). To do so, it needs to create a bunch ofchar[]instances to use as buffers.The
JsonTextReaderandJsonTextWriterabstractions allow plugging in anIArrayPool<char>implementation, and luckily this matches (pretty much exactly) the API exposed by theArrayPoolin corelib (+ vendored), so it's easy to implement.This should help alleviate some GC pressure, as we currently do a fair amount of serializing and deserializing.
Implementation details
Pretty simple:
IArrayPool<char>usingArrayPool<char>.SharedJsonTextReaderandJsonTextWriter) and initializeWarning
It's important that we do dispose these, so that the arrays are correctly returned to the pool, so that we don't leak memory
There are actually other places we can update too, as this PR doesn't cover the common
JsonConvert.Serialize()etc, but I'll follow up with those in a separate PR.Test coverage
All the existing tests should pass. I worked on this as part of general perf work on remote config, so the results are a bit fuzzy (as I can't remember if it includes the savings from #8226 as well), but the results are pretty conclusive, especially for big payloads 😅
Other details
https://datadoghq.atlassian.net/browse/LANGPLAT-940
Discovered this while exploring remote config optimizations