From 39c1d817007a113f47eede078caa0b7323c1faf5 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 19:28:23 +0100 Subject: [PATCH 1/4] perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments Build the joined argument display directly into a StringBuilder instead of allocating an intermediate string[] and calling string.Join. Also short-circuit the single-argument case to skip the builder entirely. Closes #6033 --- TUnit.Core/Helpers/ArgumentFormatter.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index cc836bc2ba..25c9c66427 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Text; namespace TUnit.Core.Helpers; @@ -44,12 +45,19 @@ public static string FormatArguments(IEnumerable arguments) if (list.Count == 0) return string.Empty; - var formatted = new string[list.Count]; + if (list.Count == 1) + return FormatDefault(list[0]); + + var builder = new StringBuilder(); for (int i = 0; i < list.Count; i++) { - formatted[i] = FormatDefault(list[i]); + if (i > 0) + { + builder.Append(", "); + } + builder.Append(FormatDefault(list[i])); } - return string.Join(", ", formatted); + return builder.ToString(); } var elements = new List(); From 2d86c182cb97807bd5ace3f09c4e4a23e2d5dcb2 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 20:29:51 +0100 Subject: [PATCH 2/4] perf: apply StringBuilder path to non-IList fallback and FormatTuple Address review on PR #6080: extend the same allocation-avoiding treatment to the two remaining string.Join sites for consistency. - Non-IList fallback now builds directly into a StringBuilder instead of a List + string.Join (output identical, including empty -> ""). - FormatTuple short-circuits empty/single-element tuples and otherwise uses a StringBuilder, dropping the intermediate string[] allocation. --- TUnit.Core/Helpers/ArgumentFormatter.cs | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index 25c9c66427..c5499da102 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -60,13 +60,18 @@ public static string FormatArguments(IEnumerable arguments) return builder.ToString(); } - var elements = new List(); + // Non-IList fallback (rare: callers normally pass object?[]). Build directly into a + // StringBuilder rather than allocating an intermediate List + string.Join. + var fallbackBuilder = new StringBuilder(); foreach (var arg in arguments) { - elements.Add(FormatDefault(arg)); + if (fallbackBuilder.Length > 0) + { + fallbackBuilder.Append(", "); + } + fallbackBuilder.Append(FormatDefault(arg)); } - - return elements.Count == 0 ? string.Empty : string.Join(", ", elements); + return fallbackBuilder.ToString(); } private static string FormatDefault(object? o) @@ -148,12 +153,24 @@ private static string FormatDefault(object? o, Type? parameterType) private static string FormatTuple(object tuple) { var elements = TupleHelper.UnwrapTuple(tuple); - var formatted = new string[elements.Length]; + + if (elements.Length == 0) + return "()"; + + if (elements.Length == 1) + return $"({FormatDefault(elements[0])})"; + + var builder = new StringBuilder("("); for (int i = 0; i < elements.Length; i++) { - formatted[i] = FormatDefault(elements[i]); + if (i > 0) + { + builder.Append(", "); + } + builder.Append(FormatDefault(elements[i])); } - return $"({string.Join(", ", formatted)})"; + builder.Append(')'); + return builder.ToString(); } private static string FormatEnumerable(IEnumerable enumerable) From 7d6d4044362689acfb15ab51dc775aa964daf5d9 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 20:36:24 +0100 Subject: [PATCH 3/4] perf(core): pre-size StringBuilder in ArgumentFormatter multi-arg path --- TUnit.Core/Helpers/ArgumentFormatter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index c5499da102..f8e8f4f797 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -48,7 +48,8 @@ public static string FormatArguments(IEnumerable arguments) if (list.Count == 1) return FormatDefault(list[0]); - var builder = new StringBuilder(); + // ~16 chars per formatted arg avoids the internal buffer resizing for typical counts. + var builder = new StringBuilder(list.Count * 16); for (int i = 0; i < list.Count; i++) { if (i > 0) From 43ffa7f2ba264658c17532065e8b8a48bd6e380f Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 28 May 2026 21:00:24 +0100 Subject: [PATCH 4/4] perf(core): pre-size FormatTuple StringBuilder; trim fallback comment --- TUnit.Core/Helpers/ArgumentFormatter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index f8e8f4f797..4826a5b76b 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -61,8 +61,7 @@ public static string FormatArguments(IEnumerable arguments) return builder.ToString(); } - // Non-IList fallback (rare: callers normally pass object?[]). Build directly into a - // StringBuilder rather than allocating an intermediate List + string.Join. + // Non-IList fallback — rare in practice; callers normally pass object?[]. var fallbackBuilder = new StringBuilder(); foreach (var arg in arguments) { @@ -161,7 +160,9 @@ private static string FormatTuple(object tuple) if (elements.Length == 1) return $"({FormatDefault(elements[0])})"; - var builder = new StringBuilder("("); + // ~16 chars per element + parentheses avoids resizing for typical tuple sizes. + var builder = new StringBuilder(elements.Length * 16 + 2); + builder.Append('('); for (int i = 0; i < elements.Length; i++) { if (i > 0)