From 8a56c2461bf56851e6a54d677de4007bc8deb458 Mon Sep 17 00:00:00 2001 From: Paul Harrington Date: Mon, 22 Jul 2024 12:47:28 -0700 Subject: [PATCH] Reduce memory allocation in EventSourceEventFormatting (#43947) --- .../src/Shared/EventSourceEventFormatting.cs | 135 ++++++++++++------ .../tests/AzureEventSourceListenerTests.cs | 12 ++ 2 files changed, 105 insertions(+), 42 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/EventSourceEventFormatting.cs b/sdk/core/Azure.Core/src/Shared/EventSourceEventFormatting.cs index 01f675ca7b7c..8e2ceac11c5e 100644 --- a/sdk/core/Azure.Core/src/Shared/EventSourceEventFormatting.cs +++ b/sdk/core/Azure.Core/src/Shared/EventSourceEventFormatting.cs @@ -1,8 +1,8 @@ // 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; @@ -10,70 +10,121 @@ #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(); + var payloadArray = eventData.Payload?.ToArray() ?? Array.Empty(); - 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 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 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 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; } } diff --git a/sdk/core/Azure.Core/tests/AzureEventSourceListenerTests.cs b/sdk/core/Azure.Core/tests/AzureEventSourceListenerTests.cs index bb7b3facafed..523cb23b5260 100644 --- a/sdk/core/Azure.Core/tests/AzureEventSourceListenerTests.cs +++ b/sdk/core/Azure.Core/tests/AzureEventSourceListenerTests.cs @@ -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; @@ -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() {