From d5234909d07684f1884ba01a2cb1bcbc1faa12cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 3 Sep 2026 19:05:32 +0200 Subject: [PATCH 1/3] Optimize data-driven display names Cache method metadata and localized formats, reuse bounded thread-local builders, and avoid array enumerator allocations in data-row display-name generation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbdc5d12-fd8e-4443-96e3-55161b6ee867 --- .../Internal/TestDataSourceUtilities.cs | 95 ++++++++++++++++++- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/src/TestFramework/TestFramework/Internal/TestDataSourceUtilities.cs b/src/TestFramework/TestFramework/Internal/TestDataSourceUtilities.cs index 9d52e1d4b5..ae0e6a02d5 100644 --- a/src/TestFramework/TestFramework/Internal/TestDataSourceUtilities.cs +++ b/src/TestFramework/TestFramework/Internal/TestDataSourceUtilities.cs @@ -5,6 +5,21 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting.Internal; internal static class TestDataSourceUtilities { + private const int MaxCachedBuilderCapacity = 360; + +#pragma warning disable IDE0028 // ConditionalWeakTable is not collection-expression-constructible on .NET Framework (CS9174). + private static readonly ConditionalWeakTable MethodDataCache = new(); +#pragma warning restore IDE0028 + + [ThreadStatic] + private static StringBuilder? s_cachedBuilder; + + [ThreadStatic] + private static CultureInfo? s_cachedResourceCulture; + + [ThreadStatic] + private static string? s_cachedDisplayNameFormat; + public static string? ComputeDefaultDisplayName(MethodInfo methodInfo, object?[]? data) { if (data is null) @@ -12,15 +27,15 @@ internal static class TestDataSourceUtilities return null; } - ParameterInfo[] parameters = methodInfo.GetParameters(); + MethodData methodData = MethodDataCache.GetValue(methodInfo, static method => new(method)); string methodDisplayName = methodInfo is ReflectionTestMethodInfo reflectionTestMethodInfo ? reflectionTestMethodInfo.DisplayName : methodInfo.Name; CultureInfo currentCulture = CultureInfo.CurrentCulture; - string displayNameFormat = FrameworkMessages.DataDrivenResultDisplayName; + string displayNameFormat = GetDisplayNameFormat(); - var argumentsBuilder = new StringBuilder(); - if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object[])) + StringBuilder argumentsBuilder = AcquireBuilder(); + if (methodData.HasSingleObjectArrayParameter) { AppendHumanizedArgument(argumentsBuilder, data); } @@ -29,11 +44,64 @@ internal static class TestDataSourceUtilities AppendHumanizedArguments(argumentsBuilder, data); } + string arguments = GetStringAndReleaseBuilder(argumentsBuilder); return string.Format( currentCulture, displayNameFormat, methodDisplayName, - argumentsBuilder.ToString()); + arguments); + } + + private static string GetDisplayNameFormat() + { + CultureInfo resourceCulture = FrameworkMessages.Culture ?? CultureInfo.CurrentUICulture; + if (!resourceCulture.Equals(s_cachedResourceCulture)) + { + s_cachedResourceCulture = resourceCulture; + s_cachedDisplayNameFormat = FrameworkMessages.DataDrivenResultDisplayName; + } + + return s_cachedDisplayNameFormat!; + } + + private static StringBuilder AcquireBuilder() + { + StringBuilder? builder = s_cachedBuilder; + if (builder is null) + { + return new StringBuilder(); + } + + s_cachedBuilder = null; + builder.Clear(); + return builder; + } + + private static string GetStringAndReleaseBuilder(StringBuilder builder) + { + string result = builder.ToString(); + if (builder.Capacity <= MaxCachedBuilderCapacity) + { + s_cachedBuilder = builder; + } + + return result; + } + + /// + /// Appends a collection of objects using their display-name representation. + /// + private static void AppendHumanizedArguments(StringBuilder builder, object?[] data) + { + for (int i = 0; i < data.Length; i++) + { + if (i > 0) + { + builder.Append(','); + } + + AppendHumanizedArgument(builder, data[i]); + } } /// @@ -73,6 +141,12 @@ private static void AppendHumanizedArgument(StringBuilder builder, object? data) builder.Append('\'').Append(value).Append('\''); break; + case object?[] values: + builder.Append('['); + AppendHumanizedArguments(builder, values); + builder.Append(']'); + break; + case Array: builder.Append('['); AppendHumanizedArguments(builder, (IEnumerable)data); @@ -84,4 +158,15 @@ private static void AppendHumanizedArgument(StringBuilder builder, object? data) break; } } + + private sealed class MethodData + { + public MethodData(MethodInfo method) + { + ParameterInfo[] parameters = method.GetParameters(); + HasSingleObjectArrayParameter = parameters.Length == 1 && parameters[0].ParameterType == typeof(object[]); + } + + public bool HasSingleObjectArrayParameter { get; } + } } From 648fa25534ff18472771cb8b1df6cb425ccc1466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 3 Sep 2026 19:17:43 +0200 Subject: [PATCH 2/3] Test localized display name cache refresh Verify consecutive display-name generation on one thread refreshes the cached format when CurrentUICulture changes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbdc5d12-fd8e-4443-96e3-55161b6ee867 --- .../Attributes/DataRowAttributeTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/DataRowAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/DataRowAttributeTests.cs index fa56fe8dce..2e37081552 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/DataRowAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/DataRowAttributeTests.cs @@ -261,6 +261,26 @@ public void GetDisplayNameCapturesLocalizedFormatBeforeFormattingValues() } } + public void GetDisplayNameRefreshesLocalizedFormatWhenUICultureChanges() + { + CultureInfo previousUICulture = CultureInfo.CurrentUICulture; + try + { + MethodInfo methodInfo = typeof(DummyTestClass).GetMethod(nameof(DummyTestClass.DataRowTestMethod))!; + var attribute = new DataRowAttribute(); + + CultureInfo.CurrentUICulture = new CultureInfo("en-US"); + attribute.GetDisplayName(methodInfo, ["value"]).Should().Be("DataRowTestMethod (\"value\")"); + + CultureInfo.CurrentUICulture = new CultureInfo("ko-KR"); + attribute.GetDisplayName(methodInfo, ["value"]).Should().Be("DataRowTestMethod(\"value\")"); + } + finally + { + CultureInfo.CurrentUICulture = previousUICulture; + } + } + private class DummyDataRowAttribute : DataRowAttribute { public override string GetDisplayName(MethodInfo methodInfo, object?[]? data) => "Overridden DisplayName"; From 4bba8cf45cf7c56c5fd9ece8e400f02dad446fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 4 Sep 2026 10:39:04 +0200 Subject: [PATCH 3/3] Retry transient CI failure The replacement Azure pipeline passed all stages, but its result was not attached to the pull request's required check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dbdc5d12-fd8e-4443-96e3-55161b6ee867