Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Unittests around memory allocation #29

Merged
merged 2 commits into from
Apr 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion Mediator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Im
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Roslyn40.Tests", "test\Mediator.SourceGenerator.Roslyn40.Tests\Mediator.SourceGenerator.Roslyn40.Tests.csproj", "{E4EE80C5-179C-4483-9E91-3107B3E1CD5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SmokeTestConsole", "test\Mediator.SmokeTestConsole\Mediator.SmokeTestConsole.csproj", "{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SmokeTestConsole", "test\Mediator.SmokeTestConsole\Mediator.SmokeTestConsole.csproj", "{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.MemAllocationTests", "test\Mediator.MemAllocationTests\Mediator.MemAllocationTests.csproj", "{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.Tests.Common", "test\Mediator.Tests.Common\Mediator.Tests.Common.csproj", "{32DC3B96-7E6E-40B0-AA12-40656B2D0381}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -336,6 +340,30 @@ Global
{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x64.Build.0 = Release|Any CPU
{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x86.ActiveCfg = Release|Any CPU
{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x86.Build.0 = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|x64.ActiveCfg = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|x64.Build.0 = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|x86.ActiveCfg = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Debug|x86.Build.0 = Debug|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|Any CPU.Build.0 = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|x64.ActiveCfg = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|x64.Build.0 = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|x86.ActiveCfg = Release|Any CPU
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD}.Release|x86.Build.0 = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|x64.ActiveCfg = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|x64.Build.0 = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|x86.ActiveCfg = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Debug|x86.Build.0 = Debug|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|Any CPU.Build.0 = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|x64.ActiveCfg = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|x64.Build.0 = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|x86.ActiveCfg = Release|Any CPU
{32DC3B96-7E6E-40B0-AA12-40656B2D0381}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -363,6 +391,8 @@ Global
{22984D49-8DDF-4263-8375-D018643E44B2} = {438C928E-FBB5-45B2-9CD2-7ED3341AC75E}
{E4EE80C5-179C-4483-9E91-3107B3E1CD5A} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84}
{65D8C0F8-E4CF-452D-86FB-482FE63C7C89} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84}
{EECFACC8-E841-4EF5-A1D7-A64C84B7DBCD} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84}
{32DC3B96-7E6E-40B0-AA12-40656B2D0381} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D45B5457-4190-49B6-BF89-7FA5F4C8ABE2}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!--<ReportAnalyzer>true</ReportAnalyzer>-->
</PropertyGroup>

<ItemGroup>
<Using Include="Xunit" />
<Using Include="System.Runtime.CompilerServices" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(DotNetVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="JetBrains.DotMemoryUnit" Version="3.1.20200127.214830" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Mediator.SourceGenerator.Implementation\Mediator.SourceGenerator.Implementation.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\src\Mediator.SourceGenerator.Roslyn40\Mediator.SourceGenerator.Roslyn40.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\src\Mediator\Mediator.csproj" />
<ProjectReference Include="..\Mediator.Tests.Common\Mediator.Tests.Common.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Mediator.Tests;

[CollectionDefinition("Non-Parallel", DisableParallelization = true)]
public class NonParallelCollectionDefinitionClass
{
}
32 changes: 32 additions & 0 deletions test/Mediator.MemAllocationTests/NotificationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Mediator.Tests.Common;
using Microsoft.Extensions.DependencyInjection;

namespace Mediator.MemAllocationTests;

[Collection("Non-Parallel")]
public class NotificationTests
{
[Fact]
public void Test_Notification_Handler()
{
var services = new ServiceCollection();

services.AddMediator();

using var sp = services.BuildServiceProvider(validateScopes: true);
var mediator = sp.GetRequiredService<IMediator>();

var id = Guid.NewGuid();
var notification = new SomeNotificationMemAllocTracking(id);

// Ensure everything is cached
mediator.Publish(notification); // Everything returns sync

var beforeBytes = Allocations.GetCurrentThreadAllocatedBytes();
mediator.Publish(notification); // Everything returns sync
var afterBytes = Allocations.GetCurrentThreadAllocatedBytes();

// TODO: why 40? benchmarkdotnet reports 0 alloc for this case
Assert.Equal(40, afterBytes - beforeBytes);
}
}
94 changes: 94 additions & 0 deletions test/Mediator.MemAllocationTests/RequestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using JetBrains.dotMemoryUnit;
using JetBrains.dotMemoryUnit.Kernel;
using Mediator.Tests.Common;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;

namespace Mediator.MemAllocationTests;

[Collection("Non-Parallel")]
public class RequestTests
{
private readonly ITestOutputHelper _output;

public RequestTests(ITestOutputHelper output)
{
_output = output;
DotMemoryUnitTestOutput.SetOutputMethod(output.WriteLine);
}

/// <summary>
/// To run these tests
/// dotMemoryUnit.exe "$((Get-Command dotnet.exe).Path)" -- test "./test/Mediator.MemAllocationTests/"
/// TODO: find a way to filter out dotMemoryUnit's own mem allocations from report...
/// </summary>
/// <returns></returns>
[DotMemoryUnit(CollectAllocations = true)]
[Fact]
public async Task Test_Request_Handler_dotMemory()
{
if (!dotMemoryApi.IsEnabled)
return;

var services = new ServiceCollection();

services.AddMediator();

await using var sp = services.BuildServiceProvider(validateScopes: true);
var mediator = sp.GetRequiredService<Mediator>();

var id = Guid.NewGuid();
var request = new SomeRequestMemAllocTracking(id);

// Ensure everything is cached
await mediator.Send(request);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete();
GC.Collect();

var checkpoint = dotMemory.Check();
var beforeBytes = GC.GetAllocatedBytesForCurrentThread();
await mediator.Send(request);
var afterBytes = GC.GetAllocatedBytesForCurrentThread();

Assert.Equal(0, afterBytes - beforeBytes);
dotMemory.Check(memory =>
{
var traffic = memory.GetDifference(checkpoint);
var newObjects = traffic.GetNewObjects();
foreach (var obj in newObjects.GroupByType())
_output.WriteLine($"Allocations for {obj.TypeFullyQualifiedName}: {obj.SizeInBytes} bytes");

//_output.WriteLine($"Allocated: {traffic.AllocatedMemory.SizeInBytes}");
//foreach (var obj in traffic.GroupByType())
// _output.WriteLine($"Allocations for {obj.TypeFullyQualifiedName}: {obj.AllocatedMemoryInfo.SizeInBytes}");

Assert.Equal(0, traffic.GetNewObjects().ObjectsCount);
});
}


[Fact]
public void Test_Request_Handler()
{
var services = new ServiceCollection();

services.AddMediator();

using var sp = services.BuildServiceProvider(validateScopes: true);
var mediator = sp.GetRequiredService<IMediator>();

var id = Guid.NewGuid();
var request = new SomeRequestMemAllocTracking(id);

// Ensure everything is cached
mediator.Send(request); // Everything returns sync

var beforeBytes = Allocations.GetCurrentThreadAllocatedBytes();
mediator.Send(request); // Everything returns sync
var afterBytes = Allocations.GetCurrentThreadAllocatedBytes();

Assert.Equal(0, afterBytes - beforeBytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Mediator.MemAllocationTests;

public sealed record SomeNotificationMemAllocTracking(Guid Id) : INotification;

public sealed class SomeNotificationMemAllocTrackingHandler : INotificationHandler<SomeNotificationMemAllocTracking>
{
public ValueTask Handle(SomeNotificationMemAllocTracking notification, CancellationToken cancellationToken) => default;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Mediator.MemAllocationTests;

public sealed record SomeRequestMemAllocTracking(Guid Id) : IRequest;

public sealed class SomeRequestMemAllocTrackingHandler : IRequestHandler<SomeRequestMemAllocTracking>
{
public ValueTask<Unit> Handle(SomeRequestMemAllocTracking request, CancellationToken cancellationToken) => default;
}
22 changes: 22 additions & 0 deletions test/Mediator.Tests.Common/Fixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Mediator.Tests.Common;

public static class Allocations
{
public static long GetCurrentThreadAllocatedBytes()
{
// Run before and after message sending to test allocations.
// Stolen from https://github.com/dotnet/BenchmarkDotNet/blob/4bd433d85fff4fb6ba8c4f8df3e685ad669e2519/src/BenchmarkDotNet/Engines/GcStats.cs#L132
// There is a reason GC.Collect is run first
GC.Collect();
return GC.GetAllocatedBytesForCurrentThread();
}

public static long GetCurrentAllocatedBytes()
{
// Run before and after message sending to test allocations.
// Stolen from https://github.com/dotnet/BenchmarkDotNet/blob/4bd433d85fff4fb6ba8c4f8df3e685ad669e2519/src/BenchmarkDotNet/Engines/GcStats.cs#L132
// There is a reason GC.Collect is run first
GC.Collect();
return GC.GetTotalAllocatedBytes(true);
}
}
15 changes: 15 additions & 0 deletions test/Mediator.Tests.Common/Mediator.Tests.Common.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(DotNetVersion)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Mediator\Mediator.csproj" />
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion test/Mediator.Tests/Fixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public static (IServiceProvider sp, IMediator mediator) GetMediatorCustomContain
return (sp, mediator!);
}


private sealed class CustomServiceProvider : IServiceProvider
{
private readonly IServiceCollection _services;
Expand Down
1 change: 1 addition & 0 deletions test/Mediator.Tests/Mediator.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<ProjectReference Include="..\..\src\Mediator.SourceGenerator.Implementation\Mediator.SourceGenerator.Implementation.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\src\Mediator.SourceGenerator.Roslyn38\Mediator.SourceGenerator.Roslyn38.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\..\src\Mediator\Mediator.csproj" />
<ProjectReference Include="..\Mediator.Tests.Common\Mediator.Tests.Common.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions test/Mediator.Tests/NonParallelCollectionDefinitionClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Mediator.Tests;

[CollectionDefinition("Non-Parallel", DisableParallelization = true)]
public class NonParallelCollectionDefinitionClass
{
}