Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add benchmarks project #126

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,7 @@ paket-files/

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc

# BenchmarkDotNet artifacts
**/BenchmarkDotNet.Artifacts/**
25 changes: 25 additions & 0 deletions Gw2Sharp.Benchmarks/Gw2Sharp.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0;net5.0</TargetFrameworks>
<Nullable>enable</Nullable>
<Optimize>true</Optimize>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Gw2Sharp\Gw2Sharp.csproj" />
</ItemGroup>

<ItemGroup>
<Content Include="..\Gw2Sharp.Tests\TestFiles\**">
<Link>TestFiles\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
39 changes: 39 additions & 0 deletions Gw2Sharp.Benchmarks/ItemsPageDeserializeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.IO;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using Gw2Sharp.WebApi.V2;
using Gw2Sharp.WebApi.V2.Models;

namespace Gw2Sharp.Benchmarks
{
[MedianColumn]
[MemoryDiagnoser]
public class ItemsPageDeserializeBenchmark
{
private const int OPERATIONS_PER_INVOKE = 50;
private byte[] jsonBytes = Array.Empty<byte>();
private JsonSerializerOptions? jsonSerializerOptions = null;
private object? dump;

[GlobalSetup]
public void GlobalSetup()
{
string path = Path.Combine("TestFiles", "Items", "Items.max_page_size.json");
this.jsonBytes = System.IO.File.ReadAllBytes(path);
this.jsonSerializerOptions = SerializationHelpers.GetJsonSerializerOptions();
}

[Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)]
public object DeserializeItemsPage()
{
for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i)
{
this.dump = JsonSerializer.Deserialize<IApiV2ObjectList<Item>>(this.jsonBytes, this.jsonSerializerOptions);
}

return this.dump!;
}
}
}

45 changes: 45 additions & 0 deletions Gw2Sharp.Benchmarks/ItemsPageGetCachedBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;

namespace Gw2Sharp.Benchmarks
{
/// <summary>
/// This benchmark essentially checks how much time
/// it takes to retrieve items page from the cache.
/// The page is initially loaded in <see cref="GlobalSetupAsync"/>
/// via <see cref="Gw2Client"/> once and retrieved from the same
/// instance during the benchmark.
/// </summary>
[MedianColumn]
[MemoryDiagnoser]
public class ItemsPageGetCachedBenchmark
{
private const int OPERATIONS_PER_INVOKE = 10;
private Connection? connection;
private Gw2Client? gw2Client;
private object? dump;

[GlobalSetup]
public async Task GlobalSetupAsync()
{
this.connection = new Connection();
this.gw2Client = new Gw2Client(this.connection);
await this.gw2Client.WebApi.V2.Items.PageAsync(1).ConfigureAwait(false);
}

[GlobalCleanup]
public void GlobalCleanup() => this.gw2Client!.Dispose();

[Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)]
public async Task<object?> DeserializeItemsBulkFastestAsync()
{
for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i)
{
this.dump = await this.gw2Client!.WebApi.V2.Items.PageAsync(1).ConfigureAwait(false);
}

return this.dump;
}
}
}

12 changes: 12 additions & 0 deletions Gw2Sharp.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;

namespace Gw2Sharp.Benchmarks
{
public class Program
{
public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
}
24 changes: 24 additions & 0 deletions Gw2Sharp.Benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

## BenchmarkDotNet good practices

BenchmarkDotNet good practices for running and writing benchmarks:
https://benchmarkdotnet.org/articles/guides/good-practices.html

## Running benchmarks

Run benchmarks via dotnet cli:
```
dotnet run -f net5.0 -c Release --project .\Gw2Sharp.Benchmarks.csproj
```

* Target framework (e.g. `net5.0`) must be present in `Gw2Sharp.Benchmarks.csproj`
* After running follow instructions on the terminal to run a specific benchmark.
* Benchmark results will appear in a directory `BenchmarkDotNet.Artifacts` under
the current working directory.

## Writing new benchmarks

* When writing a microbenchmark have in mind that single benchmark
should run for `200ms` or more
* To achieve this threshold use [OperationsPerInvoke](https://benchmarkdotnet.org/api/BenchmarkDotNet.Attributes.BenchmarkAttribute.html#BenchmarkDotNet_Attributes_BenchmarkAttribute_OperationsPerInvoke)
property of the `BenchmarkDotNet.Attributes.BenchmarkAttribute`
29 changes: 29 additions & 0 deletions Gw2Sharp.Benchmarks/SerializationHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

using System.Text.Json;
using Gw2Sharp.Json;
using Gw2Sharp.Json.Converters;

namespace Gw2Sharp.Benchmarks
{
public class SerializationHelpers
{
public static JsonSerializerOptions GetJsonSerializerOptions()
{
var options = new JsonSerializerOptions
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = SnakeCaseNamingPolicy.SnakeCase
};
options.Converters.Add(new ApiEnumConverter());
options.Converters.Add(new ApiFlagsConverter());
options.Converters.Add(new ApiObjectConverter());
options.Converters.Add(new ApiObjectListConverter());
options.Converters.Add(new CastableTypeConverter());
options.Converters.Add(new DictionaryIntKeyConverter());
options.Converters.Add(new RenderUrlConverter(new Connection(), null!));
options.Converters.Add(new TimeSpanConverter());
return options;
}
}
}
38 changes: 38 additions & 0 deletions Gw2Sharp.Benchmarks/SnakeCaseJsonNamingPolicyBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using BenchmarkDotNet.Attributes;
using Gw2Sharp.Json;

namespace Gw2Sharp.Benchmarks
{
[MedianColumn]
[MemoryDiagnoser]
public class SnakeCaseJsonNamingPolicyBenchmark
{
private const int OPERATIONS_PER_INVOKE = 1_000_000;
private static readonly SnakeCaseNamingPolicy policy = SnakeCaseNamingPolicy.SnakeCase;
private const string CONVENTIONAL_PROPERTY_NAME = "HypotheticalPropertyNameWithUnrealisticlyManyWords";
private const string UNCONVENTIONAL_PROPERTY_NAME = "HyPOThETicAl_PRopeRtYnAmE_wiThUNReAlisTicLyMaNy_wOrDs";
private object? dump;

[Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)]
public object? ConvertConventionalPropertyNameFast()
{
for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i)
{
this.dump = policy.ConvertName(CONVENTIONAL_PROPERTY_NAME);
}

return this.dump;
}

[Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)]
public object? ConvertUnconventionalPropertyNameFast()
{
for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i)
{
this.dump = policy.ConvertName(UNCONVENTIONAL_PROPERTY_NAME);
}

return this.dump;
}
}
}
43 changes: 43 additions & 0 deletions Gw2Sharp.Tests/Json/SnakeCaseJsonNamingPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Gw2Sharp.Json;
using Xunit;

namespace Gw2Sharp.Tests.Json.Converters
{
public class SnakeCaseNamingPolicyTests
{
[Fact]
public void ConvertsUpperCamelCase()
{
const string propertyName = "UpperCamelCasePropertyName";
string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName);
Assert.Equal("upper_camel_case_property_name", convertedPropertyName);
}

[Fact]
public void ConvertsLowerCamelCase()
{
const string propertyName = "lowerCamelCasePropertyName";
string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName);
Assert.Equal("lower_camel_case_property_name", convertedPropertyName);
}

[Fact]
public void PreservesSnakeCase()
{
const string propertyName = "snake_case_property_name";
string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName);
Assert.Equal(propertyName, convertedPropertyName);
}

[Theory]
[InlineData("GuildWars2EOD", "guild_wars2_eod")]
[InlineData("GW2EndOfDragons", "gw2_end_of_dragons")]
[InlineData("LWSeason2", "lwseason2")]
[InlineData("LW_Season2", "lw_season2")]
public void HandlesAcronyms(string propertyName, string expectedConvertedPropertyName)
{
string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName);
Assert.Equal(expectedConvertedPropertyName, convertedPropertyName);
}
}
}
Loading