[Symbol Database] DEBUG-5045 DEBUG-5046 Fix trailing bytes in symbol uploader and performance improvements#8097
Conversation
There was a problem hiding this comment.
Pull request overview
This PR optimizes the SymbolDB upload path by reducing allocations and fixing a bug where trailing null bytes could be included in uploads when reusing the internal payload buffer.
Changes:
- Fixed trailing byte bug by sending only valid payload bytes via
ArraySegment<byte>(_payload, 0, count)instead of the full buffer - Reduced allocations by precomputing JSON prefix/suffix once per batch and reusing a pooled
TextWriterfor class serialization - Introduced
Utf8CountingPooledTextWriterto write JSON into pooled char arrays while tracking UTF-8 byte counts
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
SymbolsUploader.cs |
Refactored batch assembly to precompute prefix/suffix, use pooled writer for serialization, and send only valid bytes via ArraySegment |
Utf8CountingPooledTextWriter.cs |
Added new pooled TextWriter implementation that tracks UTF-8 byte count without materializing strings |
SymbolUploaderTest.cs |
Added regression test to verify trailing null bytes are not sent when reusing payload buffer |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tracer/src/Datadog.Trace/Debugger/Symbols/Utf8CountingPooledTextWriter.cs
Outdated
Show resolved
Hide resolved
BenchmarksBenchmark execution time: 2026-01-23 17:24:18 Comparing candidate commit 2a9b8c6 in PR branch Found 6 performance improvements and 7 performance regressions! Performance is the same for 157 metrics, 22 unstable metrics. scenario:Benchmarks.Trace.AgentWriterBenchmark.WriteAndFlushEnrichedTraces net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody netcoreapp3.1
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody net6.0
scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody netcoreapp3.1
scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net472
scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces netcoreapp3.1
scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net6.0
scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog netcoreapp3.1
scenario:Benchmarks.Trace.SpanBenchmark.StartFinishSpan net6.0
|
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8097) 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 (8097) - mean (69ms) : 67, 70
master - mean (68ms) : 67, 70
section Bailout
This PR (8097) - mean (73ms) : 71, 74
master - mean (72ms) : 71, 73
section CallTarget+Inlining+NGEN
This PR (8097) - mean (1,022ms) : 939, 1105
master - mean (1,010ms) : 950, 1069
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 (8097) - mean (106ms) : 104, 109
master - mean (106ms) : 104, 109
section Bailout
This PR (8097) - mean (107ms) : 106, 108
master - mean (107ms) : 106, 108
section CallTarget+Inlining+NGEN
This PR (8097) - mean (743ms) : 688, 799
master - mean (738ms) : 679, 797
FakeDbCommand (.NET 6)gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8097) - mean (94ms) : 92, 97
master - mean (94ms) : 91, 96
section Bailout
This PR (8097) - mean (95ms) : 94, 96
master - mean (95ms) : 94, 96
section CallTarget+Inlining+NGEN
This PR (8097) - mean (721ms) : 684, 758
master - mean (718ms) : 684, 753
FakeDbCommand (.NET 8)gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8097) - mean (93ms) : 91, 95
master - mean (92ms) : 90, 94
section Bailout
This PR (8097) - mean (94ms) : 92, 95
master - mean (93ms) : 92, 95
section CallTarget+Inlining+NGEN
This PR (8097) - mean (642ms) : 622, 662
master - mean (637ms) : 621, 652
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 (8097) - mean (196ms) : 192, 201
master - mean (196ms) : 189, 203
section Bailout
This PR (8097) - mean (200ms) : 197, 203
master - mean (198ms) : 195, 201
section CallTarget+Inlining+NGEN
This PR (8097) - mean (1,145ms) : 1074, 1216
master - mean (1,130ms) : 1065, 1195
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 (8097) - mean (282ms) : 273, 291
master - mean (281ms) : 274, 288
section Bailout
This PR (8097) - mean (282ms) : 274, 291
master - mean (280ms) : 275, 286
section CallTarget+Inlining+NGEN
This PR (8097) - mean (942ms) : 891, 993
master - mean (928ms) : 881, 975
HttpMessageHandler (.NET 6)gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8097) - mean (277ms) : 266, 288
master - mean (272ms) : 266, 277
section Bailout
This PR (8097) - mean (273ms) : 268, 278
master - mean (272ms) : 268, 276
section CallTarget+Inlining+NGEN
This PR (8097) - mean (933ms) : 886, 981
master - mean (928ms) : 877, 978
HttpMessageHandler (.NET 8)gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8097) - mean (272ms) : 264, 280
master - mean (272ms) : 266, 279
section Bailout
This PR (8097) - mean (271ms) : 267, 275
master - mean (272ms) : 266, 278
section CallTarget+Inlining+NGEN
This PR (8097) - mean (841ms) : 823, 858
master - mean (838ms) : 816, 860
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
…ith byte counting
Use parameter-specific ArgumentOutOfRangeException (index/count) for easier diagnosis; no behavioral change in the valid-path. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2a9b8c6 to
afe4855
Compare
Summary of changes
Reason for change
Symbol upload happens on assembly load and can be noisy in both allocation rate and CPU. The existing implementation was already solid, but it still created avoidable transient allocations (especially per-class JSON strings) and relied on an expensive buffer clear to prevent trailing byte leakage.
Implementation details
Send only the valid payload bytes via ArraySegment(_payload, 0, count) instead of the full buffer and remove the Array.Clear buffer reset since we no longer risk sending stale bytes.
Precompute a root JSON prefix/suffix once and build each batch by appending classes between them, avoiding repeated Substring allocations and StringBuilder.Insert(0, ...).
Replace JsonConvert.SerializeObject(classScope, ...) with a reusable Newtonsoft JsonSerializer writing into a pooled TextWriter.
Introduce Utf8CountingPooledTextWriter to write JSON into a pooled char[] while tracking UTF-8 byte count.
Test coverage
Added a regression test to ensure we do not send trailing \0 bytes when reusing the internal payload buffer across uploads.
Existing SymbolDB uploader tests continue to validate batching behavior and payload deserialization.
Follow-ups
Phase 2: introduce a netcore fast-path using Utf8JsonWriter while keeping the Newtonsoft path as the compat baseline.
Future consideration: evolve toward stream-based upload to further reduce large transient buffers and avoid creating the final payload string altogether.