Skip to content

Commit b045332

Browse files
mishamyteclaude
andauthored
perf: reuse one Utf8TextWriter for custom formatter bodies (#351)
Serialization allocated a Utf8TextWriter per event inside the batch loop on the custom-ITextFormatter path -- 48 bytes each, ~47 KB of garbage per 1000-event batch. SerializationBuffers already caches the equivalent writer for the message buffer; this adds the symmetric one over the body buffer. Safe because Utf8TextWriter holds no state beyond its backing buffer (every Write goes straight through, there is no internal buffering), that buffer is already cleared per event, and SerializationBuffers is sink-owned and used serially -- the same guarantee MessageWriter already relies on. Dropping the `use` costs nothing: Utf8TextWriter does not override Dispose, so a formatter retaining the reference is exactly as protected as before. CustomFormatterSinkBenchmarks, Push(EventCount: 1000): Payload=Simple 116.03 KB -> 69.37 KB (-40.2%) Payload=Exception 7573.80 KB -> 7519.11 KB (-0.7%, exception rendering dominates) Covered by the existing batch test, which drives a custom formatter across four events and would fail if the reused writer carried state between them. Refs #349 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1d88759 commit b045332

1 file changed

Lines changed: 9 additions & 2 deletions

File tree

src/Serilog.Sinks.Grafana.Loki/Serialization.fs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type internal SerializationBuffers() =
3333

3434
let bodyWriter = JsonWriterDefaults.createWriter body
3535
let messageWriter = new Utf8TextWriter(message)
36+
let bodyTextWriter = new Utf8TextWriter(body)
3637

3738
/// Envelope buffer holding the full push payload; read by LokiPushContent after serialize.
3839
member _.Main = main
@@ -42,11 +43,16 @@ type internal SerializationBuffers() =
4243
member _.BodyWriter = bodyWriter
4344
/// Reused writer for rendering each event's message to UTF-8.
4445
member _.MessageWriter = messageWriter
46+
/// Reused TextWriter over the body buffer, handed to a custom ITextFormatter.
47+
/// Utf8TextWriter holds no state of its own, so clearing Body between events is
48+
/// the only reset needed — see the custom-formatter branch in serialize.
49+
member _.BodyTextWriter = bodyTextWriter
4550

4651
interface IDisposable with
4752
member _.Dispose() =
4853
(bodyWriter :> IDisposable).Dispose()
4954
(messageWriter :> IDisposable).Dispose()
55+
(bodyTextWriter :> IDisposable).Dispose()
5056
(main :> IDisposable).Dispose()
5157
(body :> IDisposable).Dispose()
5258
(message :> IDisposable).Dispose()
@@ -131,11 +137,12 @@ module internal Serialization =
131137
// Body: built-in fast path writes JSON straight to bodyBuffer (via the reused
132138
// writer); a custom ITextFormatter writes text through Utf8TextWriter. Either way
133139
// bodyBuffer then holds the UTF-8 body, which becomes the 2nd "values" element.
140+
// Both writers are owned by buffers and reused across the batch — Utf8TextWriter
141+
// carries no state, so clearing bodyBuffer below is the whole reset.
134142
if not (obj.ReferenceEquals(builtIn, null)) then
135143
builtIn.FormatToBuffer(event, buffers.BodyWriter, buffers.MessageWriter)
136144
else
137-
use textWriter = new Utf8TextWriter(buffers.Body)
138-
textFormatter.Format(event, textWriter)
145+
textFormatter.Format(event, buffers.BodyTextWriter)
139146

140147
// A formatter terminates the body the way a stream sink needs — Serilog's stock
141148
// output templates render a trailing newline via {NewLine} or {Exception}. That is

0 commit comments

Comments
 (0)