Skip to content

Commit

Permalink
Reduce memory allocation in EventSourceEventFormatting (#43947)
Browse files Browse the repository at this point in the history
  • Loading branch information
pharring authored Jul 22, 2024
1 parent ad944df commit 8a56c24
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 42 deletions.
135 changes: 93 additions & 42 deletions sdk/core/Azure.Core/src/Shared/EventSourceEventFormatting.cs
Original file line number Diff line number Diff line change
@@ -1,79 +1,130 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Core.Diagnostics;
using System;
using System.Buffers;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using System.Text;

#nullable enable

namespace Azure.Core.Shared
namespace Azure.Core.Shared;

internal static class EventSourceEventFormatting
{
internal static class EventSourceEventFormatting
[ThreadStatic]
private static StringBuilder? s_cachedStringBuilder;
private const int CachedStringBuilderCapacity = 512;

public static string Format(EventWrittenEventArgs eventData)
{
public static string Format(EventWrittenEventArgs eventData)
{
var payloadArray = eventData.Payload?.ToArray() ?? Array.Empty<object?>();
var payloadArray = eventData.Payload?.ToArray() ?? Array.Empty<object?>();

ProcessPayloadArray(payloadArray);
ProcessPayloadArray(payloadArray);

if (eventData.Message != null)
if (eventData.Message != null)
{
try
{
try
{
return string.Format(CultureInfo.InvariantCulture, eventData.Message, payloadArray);
}
catch (FormatException)
{
}
return string.Format(CultureInfo.InvariantCulture, eventData.Message, payloadArray);
}
catch (FormatException)
{
}
}

var stringBuilder = new StringBuilder();
stringBuilder.Append(eventData.EventName);
StringBuilder stringBuilder = RentStringBuilder();
stringBuilder.Append(eventData.EventName);

if (!string.IsNullOrWhiteSpace(eventData.Message))
{
stringBuilder.AppendLine();
stringBuilder.Append(nameof(eventData.Message)).Append(" = ").Append(eventData.Message);
}

if (!string.IsNullOrWhiteSpace(eventData.Message))
if (eventData.PayloadNames != null)
{
for (int i = 0; i < eventData.PayloadNames.Count; i++)
{
stringBuilder.AppendLine();
stringBuilder.Append(nameof(eventData.Message)).Append(" = ").Append(eventData.Message);
stringBuilder.Append(eventData.PayloadNames[i]).Append(" = ").Append(payloadArray[i]);
}
}

if (eventData.PayloadNames != null)
{
for (int i = 0; i < eventData.PayloadNames.Count; i++)
{
stringBuilder.AppendLine();
stringBuilder.Append(eventData.PayloadNames[i]).Append(" = ").Append(payloadArray[i]);
}
}
return ToStringAndReturnStringBuilder(stringBuilder);
}

return stringBuilder.ToString();
private static void ProcessPayloadArray(object?[] payloadArray)
{
for (int i = 0; i < payloadArray.Length; i++)
{
payloadArray[i] = FormatValue(payloadArray[i]);
}
}

private static void ProcessPayloadArray(object?[] payloadArray)
private static object? FormatValue(object? o)
{
if (o is byte[] bytes)
{
for (int i = 0; i < payloadArray.Length; i++)
#if NET6_0_OR_GREATER
return Convert.ToHexString(bytes);
#else
// Down-level implementation of Convert.ToHexString that uses a
// Span<char> instead of a StringBuilder to avoid allocations.
// The implementation is copied from .NET's HexConverter.ToString
// See https://github.com/dotnet/runtime/blob/acd31754892ab0431ac2c40038f541ffa7168be7/src/libraries/Common/src/System/HexConverter.cs#L180
// The only modification is that we allow larger stack allocations.
Span<char> result = bytes.Length > 32 ?
new char[bytes.Length * 2] :
stackalloc char[bytes.Length * 2];

int pos = 0;
foreach (byte b in bytes)
{
payloadArray[i] = FormatValue(payloadArray[i]);
ToCharsBuffer(b, result, pos);
pos += 2;
}
}

private static object? FormatValue(object? o)
{
if (o is byte[] bytes)
return result.ToString();

static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex)
{
var stringBuilder = new StringBuilder();
foreach (byte b in bytes)
{
stringBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0:X2}", b);
}
// An explanation of this algorithm can be found at
// https://github.com/dotnet/runtime/blob/acd31754892ab0431ac2c40038f541ffa7168be7/src/libraries/Common/src/System/HexConverter.cs#L33
uint difference = ((value & 0xF0U) << 4) + (value & 0x0FU) - 0x8989U;
uint packedResult = (((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U;

return stringBuilder.ToString();
buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
buffer[startingIndex] = (char)(packedResult >> 8);
}
#endif
}

return o;
}

return o;
private static StringBuilder RentStringBuilder()
{
StringBuilder? builder = s_cachedStringBuilder;
if (builder is null)
{
return new StringBuilder(CachedStringBuilderCapacity);
}

s_cachedStringBuilder = null;
return builder;
}

private static string ToStringAndReturnStringBuilder(StringBuilder builder)
{
string result = builder.ToString();
if (builder.Capacity <= CachedStringBuilderCapacity)
{
s_cachedStringBuilder = builder.Clear();
}

return result;
}
}
12 changes: 12 additions & 0 deletions sdk/core/Azure.Core/tests/AzureEventSourceListenerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Security.Cryptography;
using Azure.Core.Diagnostics;
using Moq;
using NUnit.Framework;
Expand Down Expand Up @@ -83,6 +84,17 @@ public void FormatsByteArrays()
Assert.AreEqual("Logging 0001E9", message);
}

[Test]
public void FormatsLargeByteArrays()
{
byte[] largeArray = new byte[64];
using RandomNumberGenerator rng = RandomNumberGenerator.Create();
rng.GetBytes(largeArray);

(EventWrittenEventArgs e, string message) = ExpectSingleEvent(() => TestSource.Log.LogWithByteArray(largeArray));
Assert.AreEqual($"Logging {string.Join("", largeArray.Select(b => b.ToString("X2")))}", message);
}

[Test]
public void FormatsAsKeyValuesIfNoMessage()
{
Expand Down

0 comments on commit 8a56c24

Please sign in to comment.