JsonLayout - Faster Json encoding with INoAllocationStringValueRenderer#6082
JsonLayout - Faster Json encoding with INoAllocationStringValueRenderer#6082
Conversation
WalkthroughAdds an allocation-free string-rendering path: introduces internal Changes
Sequence Diagram(s)sequenceDiagram
participant Logger as "Logger"
participant JsonAttr as "JsonAttribute"
participant VTInfo as "ValueTypeLayoutInfo\n(SimpleStringValue)"
participant LayoutR as "LayoutRenderer\n(e.g., Message/Level/Literal)"
participant Serializer as "DefaultJsonSerializer"
Logger->>JsonAttr: request render attribute
JsonAttr->>VTInfo: check SimpleStringValue
alt SimpleStringValue available
VTInfo-->>JsonAttr: INoAllocationStringValueRenderer
JsonAttr->>LayoutR: GetFormattedStringNoAllocation(logEvent)
LayoutR-->>JsonAttr: string? (no-allocation)
JsonAttr->>Serializer: RenderAppendJsonStringValue(string)
Serializer-->>JsonAttr: append escaped quoted string
else No simple renderer
JsonAttr->>LayoutR: render via layout (object/string)
LayoutR-->>JsonAttr: object or string
JsonAttr->>Serializer: generic JSON value rendering (fallback)
end
JsonAttr-->>Logger: attribute appended
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
2257bc8 to
51c04c0
Compare
51c04c0 to
e2d9ad1
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/NLog/LayoutRenderers/LevelLayoutRenderer.cs`:
- Around line 103-108: GetLogLevelStringValue currently falls back to
logEvent.ToString() for custom levels, which emits the whole event rather than
just the level text; replace that fallback to call
GetFormattedLevelName(logLevel, _format, _uppercase) so custom LogLevel ordinals
outside _levelNames are rendered using the configured format and casing. Locate
GetLogLevelStringValue and change the else branch (where it uses
logEvent.ToString()) to return GetFormattedLevelName(logLevel, _format,
_uppercase), keeping the existing retrieval of logLevel via GetValue(logEvent)
?? LogLevel.Trace and the ordinal check against _levelNames.
🧹 Nitpick comments (2)
src/NLog/Internal/INoAllocationStringValueRenderer.cs (1)
36-50: LGTM! Well-designed interface for allocation-free rendering.The interface correctly extends
IStringValueRendererand provides a clear contract where returningnullsignals the fast path is unavailable. This aligns with the PR's goal of reducing CPU overhead for simple string-valued attributes.Minor nitpick: The
<param name="logEvent">documentation on line 47 is empty. Consider adding a brief description like "The log event to render."📝 Suggested documentation improvement
/// <summary> /// Renders the value of layout renderer in the context of the specified log event /// </summary> - /// <param name="logEvent"></param> + /// <param name="logEvent">The log event to render.</param> /// <returns>null if not possible or unknown</returns> string? GetFormattedStringNoAllocation(LogEventInfo logEvent);src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (1)
100-103: Consider restoring SimpleStringValue when ValueType is reset to null.Right now it stays cleared, so a later
ValueType = nullloses the fast path.♻️ Proposed tweak
- SimpleStringValue = null; + SimpleStringValue = value is null ? ResolveStringValueMethod(_layout) : null;
5429cbd to
74bf42e
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/NLog/LayoutRenderers/LevelLayoutRenderer.cs`:
- Around line 150-163: GetTriLetterString incorrectly maps LogLevel.Off to the
default trace value; update the method to explicitly handle LogLevel.Off and
return upperCase ? "OFF" : "Off" for that case (leave Trace mapping as the
default/fallback to "TRC"/"Trc"), i.e., add a check for LogLevel.Off in
GetTriLetterString and return the proper three-letter "OFF"/"Off" based on the
upperCase flag so TriLetter format renders Off correctly.
🧹 Nitpick comments (1)
src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (1)
80-89: Consider avoiding repeated LINQ enumeration.
simpleLayout.LayoutRenderers.Count()and.First()enumerate the collection twice. IfLayoutRenderersisIEnumerable<T>without an efficientCountimplementation, this incurs overhead on each layout assignment.♻️ Suggested optimization
private static INoAllocationStringValueRenderer? ResolveStringValueMethod(Layout layout) { - var stringValueRenderer = (layout is SimpleLayout simpleLayout && simpleLayout.LayoutRenderers.Count() == 1) ? simpleLayout.LayoutRenderers.First() as INoAllocationStringValueRenderer : null; + if (layout is SimpleLayout simpleLayout) + { + using var enumerator = simpleLayout.LayoutRenderers.GetEnumerator(); + if (enumerator.MoveNext()) + { + var first = enumerator.Current; + if (!enumerator.MoveNext() && first is INoAllocationStringValueRenderer renderer) + { + var stringValue = renderer.GetFormattedStringNoAllocation(LogEventInfo.CreateNullEvent()); + return stringValue is null ? null : renderer; + } + } + } + return null; - if (stringValueRenderer != null) - { - var stringValue = stringValueRenderer.GetFormattedStringNoAllocation(LogEventInfo.CreateNullEvent()); - return stringValue is null ? null : stringValueRenderer; - } - return stringValueRenderer; }Also, line 88 (
return stringValueRenderer;) is only reached whenstringValueRendererisnull, making it effectivelyreturn null;. This isn't a bug but could be simplified for clarity.
7c65775 to
008bbef
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/NLog/LayoutRenderers/Wrappers/CachedLayoutRendererWrapper.cs`:
- Line 180: The no-allocation path
INoAllocationStringValueRenderer.GetFormattedStringNoAllocation must not call
RenderInner (which can allocate and populate the cache); change its
implementation to be side-effect free by returning only an already-populated
cached value when Cached is true (e.g., read the internal cached-value
field/prop without invoking RenderInner) and otherwise return null so probing
with a null event cannot seed the cache; update the method that currently reads
Cached and calls RenderInner to instead only return the cached storage if
present.
🧹 Nitpick comments (1)
src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (1)
67-88: Avoid double enumeration of LayoutRenderers.
Count()+First()can enumerate twice and adds LINQ overhead. Consider direct list access when there’s exactly one renderer.♻️ Proposed refactor
- var stringValueRenderer = (layout is SimpleLayout simpleLayout && simpleLayout.LayoutRenderers.Count() == 1) ? simpleLayout.LayoutRenderers.First() as INoAllocationStringValueRenderer : null; + INoAllocationStringValueRenderer? stringValueRenderer = null; + if (layout is SimpleLayout simpleLayout && simpleLayout.LayoutRenderers.Count == 1) + { + stringValueRenderer = simpleLayout.LayoutRenderers[0] as INoAllocationStringValueRenderer; + } if (stringValueRenderer != null) { var stringValue = stringValueRenderer.GetFormattedStringNoAllocation(LogEventInfo.CreateNullEvent()); return stringValue is null ? null : stringValueRenderer; } return stringValueRenderer;
008bbef to
d6d1016
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/NLog/Internal/IStringValueRenderer.cs`:
- Around line 40-42: The IStringValueRenderer interface was changed from public
to internal which is a public API breaking change; to fix, restore the original
public accessibility on the IStringValueRenderer interface (remove the internal
modifier) so external layout renderers still implement it, update its XML docs
if needed, and run/adjust any visibility-related tests; if the change was
intentional, instead add an explicit entry for IStringValueRenderer in the
project's breaking-changes/compatibility notes and update release notes and
compatibility tests to reflect the intentional removal from the public surface.
In `@src/NLog/LayoutRenderers/EventPropertiesLayoutRenderer.cs`:
- Around line 150-156:
INoAllocationStringValueRenderer.GetFormattedStringNoAllocation currently
ignores when Format is "json" and returns a string, causing JsonAttribute to
escape raw JSON; update the method to return null when Format is "json"
(case-insensitive) in addition to the existing Format == null check, otherwise
keep the existing behavior (use TryGetValue(logEvent, out var value) ? value as
string : string.Empty), referencing the
INoAllocationStringValueRenderer.GetFormattedStringNoAllocation implementation,
the Format property, and the TryGetValue method.
In `@src/NLog/LayoutRenderers/GdcLayoutRenderer.cs`:
- Around line 112-115: The no-allocation renderer path currently ignores json
formatting: update
INoAllocationStringValueRenderer.GetFormattedStringNoAllocation to respect
Format/FormatAsJson by returning null (i.e. opt out of the no-allocation
fast-path) when Format indicates JSON so JsonAttribute can handle proper JSON
serialization; specifically change the method that currently returns Format is
null ? null : GetValue() as string to check FormatAsJson (or Format=="json") and
return null when JSON is requested, otherwise continue returning GetValue() as
string.
In `@src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs`:
- Around line 80-87: ResolveStringValueMethod currently calls
GetFormattedStringNoAllocation with LogEventInfo.CreateNullEvent(), which can
produce false negatives for renderers like MessageLayoutRenderer; remove that
pre-check so the fast-path isn't disabled. In ResolveStringValueMethod: keep the
type check for INoAllocationStringValueRenderer on
simpleLayout.LayoutRenderers.First(), but do not call
GetFormattedStringNoAllocation(LogEventInfo.CreateNullEvent()); instead return
the renderer directly (or if you prefer, test against a representative non-null
event) so runtime calls decide actual behaviour. Ensure references remain to
ResolveStringValueMethod, INoAllocationStringValueRenderer,
GetFormattedStringNoAllocation and SimpleLayout.LayoutRenderers so the change is
easy to locate.
🧹 Nitpick comments (1)
src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (1)
72-73: ComputeSimpleStringValuecache whenValueTypeisstringtype, not just whennull.The cache is inconsistently managed: it's only set when
ValueTypeisnull, but the codebase treatstypeof(string)equivalently (see lines 134, 224, 237). WhenValueTypeis explicitly set totypeof(string)or reset tonull, the optimization cache is skipped, reducing performance for the common string type scenario.Apply the same cache logic to both the
Layoutsetter (line 72) andValueTypesetter (line 102):♻️ Suggested update
- SimpleStringValue = ValueType is null ? ResolveStringValueMethod(_layout) : null; + SimpleStringValue = (ValueType is null || typeof(string).Equals(ValueType)) + ? ResolveStringValueMethod(_layout) + : null;- SimpleStringValue = null; + SimpleStringValue = (value is null || typeof(string).Equals(value)) + ? ResolveStringValueMethod(_layout) + : null;Also applies to: 101-102
5a3533c to
98577a7
Compare
98577a7 to
24bd7f0
Compare
24bd7f0 to
0e2d166
Compare
|



It is much faster to enumerate a string to check if Json-encoding needed, than appending string to StringBuilder and enumerate the StringBuilder to check if Json-encoding needed.
When NLog is used as Microsoft Logging Provider, then it will by default generate simple LogEvents (message formatting done upfront).
My own use-case is using NLog for writing string-blobs into a json-file, and want to reduce the overhead (writing 10000 blobs/sec)
With the following
JsonLayout, and the log-message"The cat sat at the bar.".Even for a very small payloads, then there is a 50pct speed-improvement. And when increasing log-message to 500 characters, then 250pct speed-improvement: