Skip to content

Commit

Permalink
Benchmarks (#16)
Browse files Browse the repository at this point in the history
* added benchmarks (fail early and fail late) for result pattern, railway-oriented programming and exceptions
* added missing `.AndAsync` extension methods and tests
  • Loading branch information
skrasekmichael committed Apr 3, 2024
1 parent 5554b2e commit 07549ee
Show file tree
Hide file tree
Showing 25 changed files with 787 additions and 1 deletion.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ public TeamMember? GetTeamMember(Guid memberId)
}
```

*Note: using railway-oriented programming results in cleaner and shorter code, however, it also brings additional overhead and inability to return early from function, thus making it less performant.*
*Note: using railway-oriented programming results in cleaner and shorter code, however, it also brings additional overhead and inability to return early from function, thus making it less performant (see [benchmarks](benchmarks/v1.0.2.StatementBenchmark.md)).*

*Learn more about [when to not use railway-oriented programming](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/).*
9 changes: 9 additions & 0 deletions RailwayResult.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.Tests", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.FunctionalExtensions.Tests", "tests\RailwayResult.FunctionalExtensions.Tests\RailwayResult.FunctionalExtensions.Tests.csproj", "{6A979A1B-CA88-48B4-BF7D-98F0354217F2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{BA5FCA79-10DF-418F-837D-77B5EC4079EB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.FunctionalExtensions.Benchmarks", "benchmarks\RailwayResult.FunctionalExtensions.Benchmarks\RailwayResult.FunctionalExtensions.Benchmarks.csproj", "{CF09025F-E46F-4B43-9F62-71449DE2D69C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A8011620-9912-4139-B2A2-72C334F2DB82}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Expand Down Expand Up @@ -50,6 +54,10 @@ Global
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Release|Any CPU.Build.0 = Release|Any CPU
{CF09025F-E46F-4B43-9F62-71449DE2D69C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF09025F-E46F-4B43-9F62-71449DE2D69C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF09025F-E46F-4B43-9F62-71449DE2D69C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF09025F-E46F-4B43-9F62-71449DE2D69C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -60,6 +68,7 @@ Global
{D13BAF96-7FA1-4FD7-A318-77AA18285D9A} = {D27FC398-A542-46F4-A335-C6D1A7C73F36}
{BF48CBB9-7B8A-4376-9094-D71B093E0660} = {9AF71DD6-A91C-429D-9E17-91DB64571176}
{6A979A1B-CA88-48B4-BF7D-98F0354217F2} = {9AF71DD6-A91C-429D-9E17-91DB64571176}
{CF09025F-E46F-4B43-9F62-71449DE2D69C} = {BA5FCA79-10DF-418F-837D-77B5EC4079EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {54E35DCC-CE9C-4B2C-A050-D7E86E862BDD}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

using RailwayResult.FunctionalExtensions.Benchmarks.Data;
using RailwayResult.FunctionalExtensions.Benchmarks.Seeds;

namespace RailwayResult.FunctionalExtensions.Benchmarks.Benchmarks;

[SimpleJob(RuntimeMoniker.Net80)]
public class StatementBenchmark
{
public Fraction Fraction { get; set; } = null!;

[GlobalSetup]
public void Setup()
{
Fraction = FractionSeeds.JediOrder;
}

[Benchmark]
public int FailEarly_RP()
{
var result = Fraction.UpdateRank_RP(Guid.Empty, Guid.Empty, Guid.Empty);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public int FailEarly_ROP()
{
var result = Fraction.UpdateRank_ROP(Guid.Empty, Guid.Empty, Guid.Empty);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public int FailEarly_Exception()
{
try
{
Fraction.UpdateRank_Exception(Guid.Empty, Guid.Empty, Guid.Empty);
return 1;
}
catch
{
return 0;
}
}

[Benchmark]
public int FailLate_RP()
{
var result = Fraction.UpdateRank_RP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public int FailLate_ROP()
{
var result = Fraction.UpdateRank_ROP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public int FailLate_Exception()
{
try
{
Fraction.UpdateRank_Exception(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return 1;
}
catch
{
return 0;
}
}

[Benchmark]
public async Task<int> FailEarlyAsync_RP()
{
var result = await Fraction.UpdateRankAsync_RP(Guid.Empty, Guid.Empty, Guid.Empty);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public async Task<int> FailEarlyAsync_ROP()
{
var result = await Fraction.UpdateRankAsync_ROP(Guid.Empty, Guid.Empty, Guid.Empty);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public async Task<int> FailLateEarly_Exception()
{
try
{
await Fraction.UpdateRankAsync_Exception(Guid.Empty, Guid.Empty, Guid.Empty);
return 1;
}
catch
{
return 0;
}
}

[Benchmark]
public async Task<int> FailLateAsync_RP()
{
var result = await Fraction.UpdateRankAsync_RP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public async Task<int> FailLateAsync_ROP()
{
var result = await Fraction.UpdateRankAsync_ROP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return result.IsSuccess ? 1 : 0;
}

[Benchmark]
public async Task<int> FailLateAsync_Exception()
{
try
{
await Fraction.UpdateRankAsync_Exception(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
return 1;
}
catch
{
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;

public sealed record Being
{
public required Guid Id { get; init; }
public required string Name { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using RailwayResult.FunctionalExtensions.Benchmarks.Extensions;

namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;

public sealed partial class Fraction
{
public FractionMember GetMemberByBeingId_Exception(Guid beingId)
{
var member = Members.FirstOrDefault(m => m.BeingId == beingId);
if (member is null)
throw new Exception(BeingNotMemberOfFraction.Message);

return member;
}

public FractionMember GetMemberById_Exception(Guid memberId)
{
var member = Members.FirstOrDefault(m => m.Id == memberId);
if (member is null)
throw new Exception(MemberNotFound.Message);

return member;
}

public Rank GetRankById_Exception(Guid rankId)
{
var rank = Ranks.FirstOrDefault(r => r.Id == rankId);
if (rank is null)
throw new Exception(RankNotFound.Message);

return rank;
}

public void UpdateRank_Exception(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
{
var initiator = GetMemberByBeingId_Exception(initiatorBeingId);
if (!initiator.Rank.Permissions.HasFlag(Permission.UpdateRank))
throw new Exception(CannotUpdateRanks.Message);

var targetMember = GetMemberById_Exception(targetMemberId);
if (targetMember.Rank.IsHeadOfFraction)
throw new Exception(CannotUpdateRankOfHeadOfFraction.Message);

var newRank = GetRankById_Exception(newRankId);
if (newRank.IsHeadOfFraction)
throw new Exception(CannotHaveMultipleHeads.Message);

targetMember.Rank = newRank;
}

public async Task<FractionMember> GetMemberByBeingIdAsync_Exception(Guid beingId)
{
var member = await Members.FirstOrDefaultAsync(m => m.BeingId == beingId);
if (member is null)
throw new Exception(BeingNotMemberOfFraction.Message);

return member;
}

public async Task<FractionMember> GetMemberByIdAsync_Exception(Guid memberId)
{
var member = await Members.FirstOrDefaultAsync(m => m.Id == memberId);
if (member is null)
throw new Exception(MemberNotFound.Message);

return member;
}

public async Task<Rank> GetRankByIdAsync_Exception(Guid rankId)
{
var rank = await Ranks.FirstOrDefaultAsync(r => r.Id == rankId);
if (rank is null)
throw new Exception(RankNotFound.Message);

return rank;
}

public async Task UpdateRankAsync_Exception(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
{
var initiator = await GetMemberByBeingIdAsync_Exception(initiatorBeingId);
if (!initiator.Rank.Permissions.HasFlag(Permission.UpdateRank))
throw new Exception(CannotUpdateRanks.Message);

var targetMember = await GetMemberByIdAsync_Exception(targetMemberId);
if (targetMember.Rank.IsHeadOfFraction)
throw new Exception(CannotUpdateRankOfHeadOfFraction.Message);

var newRank = await GetRankByIdAsync_Exception(newRankId);
if (newRank.IsHeadOfFraction)
throw new Exception(CannotHaveMultipleHeads.Message);

targetMember.Rank = newRank;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using RailwayResult.FunctionalExtensions.Benchmarks.Extensions;

namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;

public sealed partial class Fraction
{
public Result<FractionMember> GetMemberByBeingId_ROP(Guid beingId)
{
return Members
.FirstOrDefault(m => m.BeingId == beingId)
.EnsureNotNull(BeingNotMemberOfFraction);
}

public Result<FractionMember> GetMemberById_ROP(Guid memberId)
{
return Members
.FirstOrDefault(m => m.Id == memberId)
.EnsureNotNull(MemberNotFound);
}

public Result<Rank> GetRankById_ROP(Guid rankId)
{
return Ranks
.FirstOrDefault(r => r.Id == rankId)
.EnsureNotNull(RankNotFound);
}

public Result UpdateRank_ROP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
{
return GetMemberByBeingId_ROP(initiatorBeingId)
.Ensure(initiatorMember => initiatorMember.Rank.Permissions.HasFlag(Permission.UpdateRank), CannotUpdateRanks)
.Then(_ => GetMemberById_ROP(targetMemberId))
.Ensure(target => !target.Rank.IsHeadOfFraction, CannotUpdateRankOfHeadOfFraction)
.And(() => GetRankById_ROP(newRankId))
.Ensure((_, newRank) => !newRank.IsHeadOfFraction, CannotHaveMultipleHeads)
.Tap((targetMember, newRank) => targetMember.Rank = newRank)
.ToResult();
}

public Task<Result<FractionMember>> GetMemberByBeingIdAsync_ROP(Guid beingId)
{
return Members
.FirstOrDefaultAsync(m => m.BeingId == beingId)
.EnsureNotNull(BeingNotMemberOfFraction);
}

public Task<Result<FractionMember>> GetMemberByIdAsync_ROP(Guid memberId)
{
return Members
.FirstOrDefaultAsync(m => m.Id == memberId)
.EnsureNotNull(MemberNotFound);
}

public Task<Result<Rank>> GetRankByIdAsync_ROP(Guid rankId)
{
return Ranks
.FirstOrDefaultAsync(r => r.Id == rankId)
.EnsureNotNull(RankNotFound);
}

public Task<Result> UpdateRankAsync_ROP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
{
return GetMemberByBeingIdAsync_ROP(initiatorBeingId)
.Ensure(initiatorMember => initiatorMember.Rank.Permissions.HasFlag(Permission.UpdateRank), CannotUpdateRanks)
.ThenAsync(_ => GetMemberByIdAsync_ROP(targetMemberId))
.Ensure(target => !target.Rank.IsHeadOfFraction, CannotUpdateRankOfHeadOfFraction)
.AndAsync(_ => GetRankByIdAsync_ROP(newRankId))
.Ensure((_, newRank) => !newRank.IsHeadOfFraction, CannotHaveMultipleHeads)
.Tap((targetMember, newRank) => targetMember.Rank = newRank)
.ToResultAsync();
}
}
Loading

0 comments on commit 07549ee

Please sign in to comment.