Skip to content

Conversation

@TimothyMakkison
Copy link
Contributor

Use ValueStringBuilder and Span.Split to avoid allocating string[] and string.Substring

Note that while the savings are small for modern versions of .NET - 2.5MB, this represents a substantial speedup for .NET framework where string.Split would allocate 60MB of int[]

Before

image

After

image

@thomhurst
Copy link
Owner

Summary

Optimizes stack trace filtering to reduce allocations using ValueStringBuilder and Span.Split instead of string.Split and LINQ.

Critical Issues

None found ✅

Suggestions

Minor optimization opportunity:
The current implementation appends Environment.NewLine conditionally on each iteration. You could simplify this slightly by appending the newline after the slice (except for the last element), avoiding the added boolean:

var vsb = new ValueStringBuilder(stackalloc char[256]);
var enumerator = stackTrace.AsSpan().Split(Environment.NewLine);
var isFirst = true;

foreach (var range in enumerator)
{
    var slice = stackTrace.AsSpan()[range];
    if (slice.Trim().StartsWith("at TUnit"))
    {
        break;
    }
    
    if (!isFirst)
    {
        vsb.Append(Environment.NewLine);
    }
    
    vsb.Append(slice);
    isFirst = false;
}

However, the current implementation is clear and correct, so this is entirely optional.

Alignment with TUnit Rules

Performance First (Rule 4): This change directly aligns with TUnit's performance-first principle. Exception handling is in the execution hot path (every test failure creates a TUnitFailedException), making allocation reduction valuable.

AOT Compatible (Rule 5): Uses span-based APIs compatible with Native AOT.

Modern C# (Code Principle): Leverages latest .NET features (Span APIs, stackalloc, ValueStringBuilder).

The Polyfill package ensures MemoryExtensions.Split is available on netstandard2.0, so this will work across all target frameworks.

Verdict

APPROVE - Great performance improvement with correct implementation!

@thomhurst
Copy link
Owner

How does the stackalloc work if there's more than 256 chars?

@TimothyMakkison
Copy link
Contributor Author

TimothyMakkison commented Jan 10, 2026

How does the stackalloc work if there's more than 256 chars?

We initialise ValueStringBuilder with a starting buffer, in this case we used stackalloc to create an allocation free initial buffer, however any compatible collection would be a valid starting buffer. If we need more than 256 characters; void Grow(int additionalCapacityBeyondPos) is called which copies the current buffer into a new array rented from ArrayPool, returning any previously rented arrays. Finally, Dispose or ToString will return any rented arrays when we are done with the ValueStringBuilder.

Just like any other value on the stack, the stackalloc buffer will be dissappear when the stack frame pops at the end of the functions scope.

This is true for ValueListBuilder as well.

@thomhurst thomhurst merged commit b81c533 into thomhurst:main Jan 10, 2026
8 of 10 checks passed
This was referenced Jan 11, 2026
This was referenced Jan 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants