Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ ASALocalRun/
# BeatPulse healthcheck temp database
healthchecksdb

# Manually supplied benchmark binaries
/benchmark/bin/

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

Expand Down
9 changes: 9 additions & 0 deletions benchmark/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions benchmark/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Project />
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Expressif.Benchmark\Expressif.Benchmark.csproj" />
</ItemGroup>
</Project>
73 changes: 73 additions & 0 deletions benchmark/Expressif.Benchmark.Tests/ExpressionAdapterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace Expressif.Benchmark.Tests;

[TestFixture]
public class ExpressionAdapterTests
{
private const string ComplexTextExpression =
"trim | lower | replace-chars(\" \", \"-\") | first-chars(10) | upper";
private const string CoercionExpression =
"trim | multiply(1.21) | round(2) | prepend(\"€\")";

[Test]
public void V1ComplexPipeline_ParsingBindingAndEvaluationSucceed()
{
var version = GetVersion(AdapterKind.V1);
var adapter = ExpressionAdapter.Load(version.Directory);

Assert.That(() => adapter.Parse(ComplexTextExpression), Throws.Nothing);
Assert.That(() => adapter.Create(ComplexTextExpression), Throws.Nothing);

var result = adapter.CreateEvaluator(ComplexTextExpression)(" Benchmark Input Value ");
Assert.That(result, Is.EqualTo("BENCHMARK-"));
}

[TestCase("V1")]
[TestCase("V2")]
public void WorkloadSources_ParseSuccessfully(string adapterName)
{
var kind = Enum.Parse<AdapterKind>(adapterName);
var adapter = ExpressionAdapter.Load(GetVersion(kind).Directory);

Assert.Multiple(() =>
{
Assert.That(() => adapter.Parse(ComplexTextExpression), Throws.Nothing);
Assert.That(() => adapter.Parse(CoercionExpression), Throws.Nothing);
Assert.That(() => adapter.Create(ComplexTextExpression), Throws.Nothing);
Assert.That(() => adapter.Create(CoercionExpression), Throws.Nothing);
});
}

[TestCase("V1")]
[TestCase("V2")]
public void CoercionPipeline_NumericStringReturnsFormattedText(string adapterName)
{
var kind = Enum.Parse<AdapterKind>(adapterName);
var version = GetVersion(kind);
var evaluate = ExpressionAdapter.Load(version.Directory).CreateEvaluator(CoercionExpression);

var result = evaluate(" 1234.56 ");

Assert.That(result, Is.TypeOf<string>());
Assert.That(result, Is.EqualTo("€1493.82"));
}

private static VersionUnderTest GetVersion(AdapterKind kind)
{
IReadOnlyList<VersionUnderTest> versions;
try
{
versions = VersionDiscovery.Discover();
}
catch (DirectoryNotFoundException exception)
{
Assert.Ignore(exception.Message);
throw;
}

var version = versions.FirstOrDefault(candidate => candidate.AdapterKind == kind);
if (version is null)
Assert.Ignore($"No {kind.ToString().ToLowerInvariant()} version folder is available.");

return version!;
}
}
24 changes: 24 additions & 0 deletions benchmark/Expressif.Benchmark.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Expressif.Benchmark", "Expressif.Benchmark\Expressif.Benchmark.csproj", "{E2C46502-DF6E-4F2B-B2EC-A02CE599202A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Expressif.Benchmark.Tests", "Expressif.Benchmark.Tests\Expressif.Benchmark.Tests.csproj", "{7027834B-46EF-47CC-9DD4-295539093E43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E2C46502-DF6E-4F2B-B2EC-A02CE599202A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2C46502-DF6E-4F2B-B2EC-A02CE599202A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2C46502-DF6E-4F2B-B2EC-A02CE599202A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2C46502-DF6E-4F2B-B2EC-A02CE599202A}.Release|Any CPU.Build.0 = Release|Any CPU
{7027834B-46EF-47CC-9DD4-295539093E43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7027834B-46EF-47CC-9DD4-295539093E43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7027834B-46EF-47CC-9DD4-295539093E43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7027834B-46EF-47CC-9DD4-295539093E43}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
3 changes: 3 additions & 0 deletions benchmark/Expressif.Benchmark/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Expressif.Benchmark.Tests")]
32 changes: 32 additions & 0 deletions benchmark/Expressif.Benchmark/BenchmarkConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;

namespace Expressif.Benchmark;

internal static class BenchmarkConfig
{
public static IConfig Create(IReadOnlyList<VersionUnderTest> versions)
{
var config = ManualConfig.Create(DefaultConfig.Instance)
.AddDiagnoser(MemoryDiagnoser.Default);

var baselineAssigned = false;
foreach (var version in versions)
{
var job = Job.Default
.WithId(version.Name)
.WithEnvironmentVariable(VersionDiscovery.VersionEnvironmentVariable, version.Directory);

if (!baselineAssigned && version.AdapterKind == AdapterKind.V1)
{
job = job.AsBaseline();
baselineAssigned = true;
}

config.AddJob(job);
}

return config;
}
}
8 changes: 8 additions & 0 deletions benchmark/Expressif.Benchmark/Expressif.Benchmark.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
</Project>
141 changes: 141 additions & 0 deletions benchmark/Expressif.Benchmark/ExpressionAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;

namespace Expressif.Benchmark;

internal sealed class ExpressionAdapter
{
private readonly Func<string, object> parseExpression;
private readonly Func<string, object> createExpression;
private readonly Func<object, object?, object?> evaluateExpression;

private ExpressionAdapter(
Func<string, object> parseExpression,
Func<string, object> createExpression,
Func<object, object?, object?> evaluateExpression)
=> (this.parseExpression, this.createExpression, this.evaluateExpression) = (
parseExpression,
createExpression,
evaluateExpression);

public static ExpressionAdapter Load(string versionDirectory)
{
if (!SetDllDirectory(versionDirectory))
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error(),
$"Could not add '{versionDirectory}' to the native DLL search path.");

var name = Path.GetFileName(versionDirectory);
var kind = name.StartsWith("v1", StringComparison.OrdinalIgnoreCase)
? AdapterKind.V1
: name.StartsWith("v2", StringComparison.OrdinalIgnoreCase)
? AdapterKind.V2
: throw new InvalidOperationException($"Unsupported version folder '{name}'.");

Check warning on line 34 in benchmark/Expressif.Benchmark/ExpressionAdapter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AaA84cz3bv0rPh6uuB1e&open=AaA84cz3bv0rPh6uuB1e&pullRequest=657
var assemblyPath = Path.Combine(versionDirectory, "Expressif.dll");
var context = new VersionLoadContext(assemblyPath);
var assembly = context.LoadFromAssemblyPath(assemblyPath);

return kind switch
{
AdapterKind.V1 => CreateV1(assembly),
AdapterKind.V2 => CreateV2(assembly),

Check warning on line 42 in benchmark/Expressif.Benchmark/ExpressionAdapter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this condition so that it does not always evaluate to 'True'. Some code paths are unreachable.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AaA84cz3bv0rPh6uuB1g&open=AaA84cz3bv0rPh6uuB1g&pullRequest=657
_ => throw new ArgumentOutOfRangeException(nameof(kind)),

Check warning on line 43 in benchmark/Expressif.Benchmark/ExpressionAdapter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The parameter name 'kind' is not declared in the argument list.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AaA84cz3bv0rPh6uuB1d&open=AaA84cz3bv0rPh6uuB1d&pullRequest=657
};
}

[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetDllDirectory(string pathName);

public object Create(string source) => createExpression(source);

public object Parse(string source) => parseExpression(source);

public Func<object?, object?> CreateEvaluator(string source)
{
var expression = createExpression(source);
return value => evaluateExpression(expression, value);
}

private static ExpressionAdapter CreateV1(Assembly assembly)
{
var expressionType = RequireType(assembly, "Expressif.Expression");
var constructor = expressionType.GetConstructor([typeof(string)])
?? throw new MissingMethodException(expressionType.FullName, ".ctor(string)");
return new ExpressionAdapter(
CompileV1Parser(assembly),
CompileConstructor(constructor),
CompileEvaluate(expressionType));
}

private static ExpressionAdapter CreateV2(Assembly assembly)
{
var implementationType = RequireType(assembly, "Expressif.Expression");
var create = implementationType.GetMethod(
"Create",
BindingFlags.Public | BindingFlags.Static,
[typeof(string)])
?? throw new MissingMethodException(implementationType.FullName, "Create(string)");
var functionType = RequireType(assembly, "Expressif.Functions.IFunction");
var parserType = RequireType(assembly, "Expressif.Syntax.ExpressionParser");
var parse = parserType.GetMethod(
"Parse",
BindingFlags.Public | BindingFlags.Static,
[typeof(string)])
?? throw new MissingMethodException(parserType.FullName, "Parse(string)");
return new ExpressionAdapter(
CompileStaticCall(parse),
CompileStaticCall(create),
CompileEvaluate(functionType));
}

private static Type RequireType(Assembly assembly, string typeName)
=> assembly.GetType(typeName, throwOnError: true)!;

Check warning on line 94 in benchmark/Expressif.Benchmark/ExpressionAdapter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this null-forgiving operator; nullable warnings are disabled here.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AaA84cz3bv0rPh6uuB1f&open=AaA84cz3bv0rPh6uuB1f&pullRequest=657

private static Func<string, object> CompileConstructor(ConstructorInfo constructor)
{
var source = Expression.Parameter(typeof(string), "source");
var body = Expression.Convert(Expression.New(constructor, source), typeof(object));
return Expression.Lambda<Func<string, object>>(body, source).Compile();
}

private static Func<string, object> CompileV1Parser(Assembly assembly)
{
var rootExpressionType = RequireType(assembly, "Expressif.Parsers.RootExpression");
var parser = rootExpressionType.GetField("Parser", BindingFlags.Public | BindingFlags.Static)?.GetValue(null)
?? throw new MissingFieldException(rootExpressionType.FullName, "Parser");
var parserType = parser.GetType();
var parserExtensions = RequireType(parserType.Assembly, "Sprache.ParserExtensions");
var parse = parserExtensions.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Single(method => method.Name == "Parse"
&& method.IsGenericMethodDefinition
&& method.GetParameters() is [_, { ParameterType: var parameterType }]
&& parameterType == typeof(string))
.MakeGenericMethod(parserType.GetGenericArguments()[0]);
var source = Expression.Parameter(typeof(string), "source");
var body = Expression.Convert(
Expression.Call(parse, Expression.Constant(parser, parserType), source),
typeof(object));
return Expression.Lambda<Func<string, object>>(body, source).Compile();
}

private static Func<string, object> CompileStaticCall(MethodInfo create)
{
var source = Expression.Parameter(typeof(string), "source");
var body = Expression.Convert(Expression.Call(create, source), typeof(object));
return Expression.Lambda<Func<string, object>>(body, source).Compile();
}

private static Func<object, object?, object?> CompileEvaluate(Type expressionType)
{
var evaluate = expressionType.GetMethod("Evaluate", [typeof(object)])
?? throw new MissingMethodException(expressionType.FullName, "Evaluate(object)");
var instance = Expression.Parameter(typeof(object), "expression");
var value = Expression.Parameter(typeof(object), "value");
var body = Expression.Convert(
Expression.Call(Expression.Convert(instance, expressionType), evaluate, value),
typeof(object));
return Expression.Lambda<Func<object, object?, object?>>(body, instance, value).Compile();
}
}
Loading