Reuse per-thread MemoryStream/Utf8JsonWriter in EventSourceLogger.ToJson#131229
Reuse per-thread MemoryStream/Utf8JsonWriter in EventSourceLogger.ToJson#131229mangod9 wants to merge 1 commit into
Conversation
ToJson allocated a new MemoryStream, a new Utf8JsonWriter, and their backing buffers on every logged event when the JsonMessage keyword is enabled. Reuse a [ThreadStatic] MemoryStream + Utf8JsonWriter, resetting them per call, so the only remaining allocation is the returned JSON string. Allocation A/B (per call): ~82-96% fewer bytes/call (824-960 B down to 32-168 B, the residual being only the result string) and 1.2-2.0x faster. Output is byte-identical; the JsonMessage-keyword unit tests parse and validate it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @dotnet/area-extensions-logging |
There was a problem hiding this comment.
Pull request overview
This PR optimizes Microsoft.Extensions.Logging.EventSource’s JSON payload generation path by reusing per-thread serialization state in EventSourceLogger.ToJson, reducing per-event allocations when JSON logging is enabled.
Changes:
- Introduces
[ThreadStatic]caching for aMemoryStreamandUtf8JsonWriterused byEventSourceLogger.ToJson. - Resets the cached instances per call via
MemoryStream.SetLength(0)andUtf8JsonWriter.Reset(stream). - Simplifies UTF-8 decoding by directly producing the JSON string from the stream buffer (with a fallback to
ToArray()).
| MemoryStream stream = t_jsonStream ??= new MemoryStream(); | ||
| Utf8JsonWriter writer = t_jsonWriter ??= new Utf8JsonWriter(stream); | ||
|
|
||
| stream.SetLength(0); | ||
| writer.Reset(stream); |
There was a problem hiding this comment.
Two notes on the reworked ToJson. The thread-static reuse itself looks correct: it's fully synchronous, per-thread, and the buffer is never live across a reentrant call, so there's no corruption risk. The per-thread buffer retention is the one worth addressing.
| : Encoding.UTF8.GetString(stream.ToArray()); | ||
|
|
||
| return Encoding.UTF8.GetString(buffer.Array!, buffer.Offset, buffer.Count); | ||
| return result; |
There was a problem hiding this comment.
The per-thread MemoryStream (and the writer's internal buffer) grows to the largest event ever serialized on this thread and is never shrunk, so a single large property value permanently inflates steady-state memory on long-lived thread-pool threads. ConsoleLogger handles the same pattern by capping its thread-static StringBuilder at 1024 (ConsoleLogger.cs). Suggest capping the stream the same way before returning:
| return result; | |
| if (stream.Capacity > 1024) | |
| { | |
| stream.Capacity = 1024; | |
| } | |
| return result; |
| { | ||
| buffer = new ArraySegment<byte>(stream.ToArray()); | ||
| } | ||
| string result = stream.TryGetBuffer(out ArraySegment<byte> buffer) |
There was a problem hiding this comment.
Minor: result just wraps the ternary and could be a direct return. Note that if you take the capacity-cap suggestion on the return line, keep the local (you need the string in hand before resetting Capacity); otherwise this can just be return stream.TryGetBuffer(...) ? ... : ...;.
Reduces per-log-event allocations in
EventSourceLogger.ToJson, which runs on every logged event when theJsonMessagekeyword is enabled (e.g.dotnet-tracecollection).Problem
ToJsonallocated anew MemoryStream(), anew Utf8JsonWriter(), and their backing buffers on every call.Change
Reuse a
[ThreadStatic]MemoryStream+Utf8JsonWriter, resetting them per call viaMemoryStream.SetLength(0)+Utf8JsonWriter.Reset(stream). The only remaining allocation is the returned JSON string. The approach uses only APIs available on all target frameworks (incl.netstandard2.0/net462).Measurements (per-call allocation A/B, isolated harness)
The residual "after" bytes are only the returned result string.
Validation
Microsoft.Extensions.Logging.EventSourceunit tests pass (26/26). TheJsonMessage-keyword tests parse and validate the emitted JSON, confirming byte-identical output.Note
This pull request was generated with the assistance of GitHub Copilot.