PerfUnit 0.1.0

dotnet add package PerfUnit --version 0.1.0
                    
NuGet\Install-Package PerfUnit -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PerfUnit" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PerfUnit" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="PerfUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PerfUnit --version 0.1.0
                    
#r "nuget: PerfUnit, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PerfUnit@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PerfUnit&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=PerfUnit&version=0.1.0
                    
Install as a Cake Tool

PerfUnit

PerfUnit is a C# library that allows you to easily add performance assertions to your existing xUnit tests to ensure a tested function runs within a given performance constraint (either speed, memory usage, or both).

It is almost a solution looking for a problem.

Features

  • Utilises a [PerformanceFact] attribute to replace [Fact] unit tests easily.
  • Speed and memory assertions are similarly defined using the [PerfSpeed] and [PerfMemory] attributes with semi fluent-style implementations, e.g. [PerfSpeed(MustTake.LessThan, 10, TimeUnit.Nanoseconds)].
  • A static SimpleBenchmarker class is included and designed for rapid benchmarking. Support for using Benchmark.NET as the backend instead is planned.
  • Source Generators are used to inject benchmarking code to achieve testing without resorting to runtime reflection.

Installation

dotnet add package PerfUnit

Requirements

  • xUnit
  • .Net 6.0 or higher

Getting Started

PerfUnit is designed to easily extend existing unit tests to add performance constraints.

  1. Given an existing unit test

public class CalculatorTests
{
    [Fact]
    public void Add_ShouldReturnSum()
    {
      Calculator calculator = new();
      var sum = calculator.Add(1,2);
      Assert.Equal(3, sum);
    }
}

 

  1. All we need to do is
  • Make the test class partial
  • Replace [Fact] with [PerformanceFact]
  • Add a constraint using [PerfSpeed] or [PerfMemory] or both
  • Add the .Perf() extension method into the line of code you wish to measure.
public partial class CalculatorTests
{
    [PerformanceFact]
    [PerfSpeed(MustTake.LessThan, 1, TimeUnit.Milliseconds)]
    [PerfMemory(MustUse.LessThanOrEqualTo, 8, SizeUnit.Bytes)]
    public void Add_ShouldReturnSum()
    {
      Calculator calculator = new();
      var sum = calculator.Add(1,2).Perf();
      Assert.Equal(3, sum);
    }
}

 

This will require the following assertions to pass in order for the unit test to succeed, in this order (that way if the test fails on a defined assertion, the benchmark won't run unecessarily):

  • Assert.Equal(3, sum)
  • Assert.True(benchTime < 1 millisecond)
  • Assert.True(memory < 8 bytes)

This will generate the following code behind the scenes:

<details> <summary>See generated code</summary>

public partial class CalculatorTests
{
    [Fact(DisplayName = "Add_ShouldReturnSum")]
    public void Add_ShouldReturnSum_g()
    {
      Calculator calculator = new();
      var sum = calculator.Add(1,2);
      Assert.Equal(3, sum);

      var (benchTime, memory) = SimpleBenchmarker.Run(() =>
        {
          var _dis_ = calculator.Add(1, 2);
        }, 
        new BenchmarkConfig() {ExpectedMaxMemoryBytes = 8, ExpectedMaxTimeMs = 1}
      );

      Assert.True(benchTime < 1000000, $"Expected execution to take < 1.00 ms, but took {Format.FormatTime(benchTime, true)}");
      Assert.True(memory < 8, $"Expected execution to use < 8 bytes, but took {Format.FormatMemory(memory, true)}");

    }
}

</details>

Important Notes

  • .Perf()
    • Only one .Perf() call is allowed in a test.
    • Omitting the .Perf() tag in the unit test will cause the entire unit test to be benchmarked. This is probably not what you want, except if your test contains no Arrange or Assert code itself
    • If you have a void method you're testing, you will need to place the .Perf() tag higher up in the call chain. For example, calculator.doVoidWork() should be tagged as calculator.Perf().doVoidWork().
    • You can use it in lambda methods, but be careful of scope. Only the immediate call tagged with .Perf() will be benchmarked, and it may not have access to surrounding variables. For example, in the following lambda the code will fail as n will be out of scope:
      private void Test8()
      {
          var sum = numbers.Where((n) =>
          {
              calculator.Add(n, n*n).Perf();
              return n % 3 == 0;
              }
          ).Sum(x => (long)x);
      }
      

Disable Parallelisation

Make sure to add [assembly: CollectionBehavior(DisableTestParallelization = true)] somewhere in your test project, or add classes with performance tests to the same xUnit Collection. Running tests in parallel will harm any performance results.


Reason for existence

I was playing around with refactoring huge chunks of a project of mine, and realised in several places I'd actually worsened performance in the process. I had been using Benchmark.NET to test several of these changes, but realised I could instead roll these into my unit tests; I wasn't trying to eke out every last drop of performance, but needed to ensure my functions ran within reasonable boundaries (e.g. keeping certain methods allocationless, or making sure LINQ operations weren't taking longer than a few milliseconds).

I decided to use this as an excuse to dabble with source generation and came up with PerfUnit. Of course, halfway through the project I stumbled across NBench which seems to be exactly what I needed, if a bit verbose. Ah well.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 66 6/28/2025

Initial Release