Skip to content

Commit

Permalink
Benchmarks: update to latest libraries and add tests (Azure#7994)
Browse files Browse the repository at this point in the history
This builds on the work @mathewc started with benchmarks. There are a lot of things that would be nice to have in benchmark form as we evaluate improving various parts of pipeline performance like async/await elimination, `Task` vs. `ValueTask`, allocations, general algorithmic improvements, etc.

Overall:
- Upgrades to latest BenchmarkDotNet
- Moves the benchmarks project out of tests/ to benchmarks/ (this allows us to do fun things with `Directory.Build.props` and such down the road for _only_ tests and simplifies some things - the move itself is minor just doing it up front.
- Adds some benchmarks as examples - a few areas I'm poking at but haven't gotten into PRs yet (these can be dropped from this PR if wanted).

To run benchmarks:
```ps1
dotnet run -c Release -f net6.0 --project .\benchmarks\WebJobs.Script.Benchmarks\
```

This will present a prompt with all benchmarks:
```text
Available Benchmarks:
  #0 AuthUtilityBenchmarks
  Azure#1 CSharpCompilationBenchmarks
  Azure#2 ScriptLoggingBuilderExtensionsBenchmarks

You should select the target benchmark(s). Please, print a number of a benchmark (e.g. `0`) or a contained benchmark caption (e.g. `AuthUtilityBenchmarks`).
If you want to select few, please separate them with space ` ` (e.g. `1 2 3`).
You can also provide the class name in console arguments by using --filter. (e.g. `--filter *AuthUtilityBenchmarks*`).
```
Note the instructions to do similar from the command line, e.g.:
```ps1
dotnet run -c Release -f net6.0 --project .\benchmarks\WebJobs.Script.Benchmarks\ --filter *ScriptLogging*
```

Co-authored-by: Lilian Kasem <[email protected]>
  • Loading branch information
NickCraver and liliankasem authored Jan 12, 2022
1 parent 368d5af commit 6286c54
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 46 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ TestResults/
/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/runtimeAssemblies.txt

local.settings.json
msbuild.binlog
msbuild.binlog
BenchmarkDotNet.Artifacts/
19 changes: 12 additions & 7 deletions WebJobs.Script.sln
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Script.Tests.Benchmarks", "test\Benchmarks\Microsoft.Azure.WebJobs.Script.Tests.Benchmarks.csproj", "{09D16953-A048-4E6B-B366-1E0D7E5EF86E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "sample\Samples.csproj", "{F381CDD6-50BD-48BC-B292-230C52589A30}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D5F77934-E468-4EE7-82F8-3B8149591174}"
ProjectSection(SolutionItems) = preProject
benchmarks\README.md = benchmarks\README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebJobs.Script.Benchmarks", "benchmarks\WebJobs.Script.Benchmarks\Microsoft.Azure.WebJobs.Script.Benchmarks.csproj", "{766DE3D5-4FB1-4602-9BB5-3779EECC232D}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
Expand Down Expand Up @@ -90,12 +95,12 @@ Global
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}.Release|Any CPU.Build.0 = Release|Any CPU
{09D16953-A048-4E6B-B366-1E0D7E5EF86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09D16953-A048-4E6B-B366-1E0D7E5EF86E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09D16953-A048-4E6B-B366-1E0D7E5EF86E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09D16953-A048-4E6B-B366-1E0D7E5EF86E}.Release|Any CPU.Build.0 = Release|Any CPU
{F381CDD6-50BD-48BC-B292-230C52589A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F381CDD6-50BD-48BC-B292-230C52589A30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{766DE3D5-4FB1-4602-9BB5-3779EECC232D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{766DE3D5-4FB1-4602-9BB5-3779EECC232D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{766DE3D5-4FB1-4602-9BB5-3779EECC232D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{766DE3D5-4FB1-4602-9BB5-3779EECC232D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -110,7 +115,7 @@ Global
{38920568-003E-448F-963B-41B739D1E01C} = {16351B76-87CA-4A8C-80A1-3DD83A0C4AA6}
{5C308A72-5CF3-45E8-B64F-2C98F567054A} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C} = {16351B76-87CA-4A8C-80A1-3DD83A0C4AA6}
{09D16953-A048-4E6B-B366-1E0D7E5EF86E} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
{766DE3D5-4FB1-4602-9BB5-3779EECC232D} = {D5F77934-E468-4EE7-82F8-3B8149591174}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}
Expand Down
32 changes: 32 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Benchmarks

Welcome to the benchmarks! This folder is for code benchmarking (e.g. components rather than end-to-do testing).
The intent is to benchmark areas we think are interesting and measure improvements as well as ensuring we don't unintentionally regress over time.

There's a lot of things that would be nice to have in benchmark form as we evaluate improving various parts of pipeline performance like async/await elimination,
`Task` vs. `ValueTask`, allocations, general algorithmic improvements, etc. This is where those assessments live.

To run benchmarks (from solution root - otherwise shorten the project path!):

```ps1
dotnet run -c Release -f net6.0 --project .\benchmarks\WebJobs.Script.Benchmarks\
```

This will present a prompt with all benchmarks discovered - something like this:

```text
Available Benchmarks:
#0 AuthUtilityBenchmarks
#1 CSharpCompilationBenchmarks
#2 ScriptLoggingBuilderExtensionsBenchmarks
You should select the target benchmark(s). Please, print a number of a benchmark (e.g. `0`) or a contained benchmark caption (e.g. `AuthUtilityBenchmarks`).
If you want to select few, please separate them with space ` ` (e.g. `1 2 3`).
You can also provide the class name in console arguments by using --filter. (e.g. `--filter *AuthUtilityBenchmarks*`).
```

Or, you can directly run a set of benchmarks from the command line as noted above:

```ps1
dotnet run -c Release -f net6.0 --project .\benchmarks\WebJobs.Script.Benchmarks\ --filter *Grpc*
```
114 changes: 114 additions & 0 deletions benchmarks/WebJobs.Script.Benchmarks/AuthUtilityBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using BenchmarkDotNet.Attributes;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication;
using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace Microsoft.Azure.WebJobs.Script.Benchmarks
{
public class AuthUtilityBenchmarks
{
private ClaimsPrincipal Principal;
private static List<Claim> TotallyRandomClaims { get; } = new List<Claim>()
{
new Claim(SecurityConstants.AuthLevelKeyNameClaimType, "test1"),
new Claim(SecurityConstants.AuthLevelKeyNameClaimType, "test2"),
new Claim(SecurityConstants.AuthLevelKeyNameClaimType, "test3"),
new Claim(SecurityConstants.AuthLevelClaimType, nameof(AuthorizationLevel.Function)),
new Claim(SecurityConstants.AuthLevelClaimType, nameof(AuthorizationLevel.Anonymous)),
new Claim(SecurityConstants.AuthLevelClaimType, nameof(AuthorizationLevel.User)),
new Claim(SecurityConstants.AuthLevelClaimType, nameof(AuthorizationLevel.Admin)),
new Claim(SecurityConstants.AuthLevelClaimType, nameof(AuthorizationLevel.System)),
};

[Params(null, "code")]
public string KeyName;

[Params(0, 4, 8)]
public int ClaimsCount;

[GlobalSetup]
public void Setup()
{
var identity = new ClaimsIdentity(TotallyRandomClaims.Take(ClaimsCount));
Principal = new ClaimsPrincipal(identity);
}

[Benchmark(Baseline = true)]
public bool PrincipalHasAuthLevelClaim() =>
AuthUtility.PrincipalHasAuthLevelClaim(Principal, AuthorizationLevel.Function);

[Benchmark]
public bool PrincipalHasAuthLevelClaimNoArray() =>
PrincipalHasAuthLevelClaimNoArray(Principal, AuthorizationLevel.Function);

[Benchmark]
public bool PrincipalHasAuthLevelClaimHasClaim() =>
PrincipalHasAuthLevelClaimHasClaim(Principal, AuthorizationLevel.Function);

public static bool PrincipalHasAuthLevelClaimNoArray(ClaimsPrincipal principal, AuthorizationLevel requiredLevel, string keyName = null)
{
// If the required auth level is anonymous, the requirement is met
if (requiredLevel == AuthorizationLevel.Anonymous)
{
return true;
}

// Still allocating a enumerate from Identities -> Claims
foreach (var claim in principal.Claims)
{
if (claim.Type == SecurityConstants.AuthLevelClaimType)
{
var level = claim.Value switch
{
nameof(AuthorizationLevel.Admin) => AuthorizationLevel.Admin,
nameof(AuthorizationLevel.Function) => AuthorizationLevel.Function,
nameof(AuthorizationLevel.System) => AuthorizationLevel.System,
nameof(AuthorizationLevel.User) => AuthorizationLevel.User,
_ => AuthorizationLevel.Anonymous
};
if (level == AuthorizationLevel.Admin)
{
// If we have a claim with Admin level, regardless of whether a name is required, return true.
return true;
}
if (level == requiredLevel && (keyName == null || string.Equals(principal.FindFirstValue(SecurityConstants.AuthLevelKeyNameClaimType), keyName, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
}

return false;
}

public static bool PrincipalHasAuthLevelClaimHasClaim(ClaimsPrincipal principal, AuthorizationLevel requiredLevel, string keyName = null)
{
// If the required auth level is anonymous, the requirement is met
if (requiredLevel == AuthorizationLevel.Anonymous)
{
return true;
}

var levelString = requiredLevel switch
{
AuthorizationLevel.Admin => nameof(AuthorizationLevel.Admin),
AuthorizationLevel.Function => nameof(AuthorizationLevel.Function),
AuthorizationLevel.System => nameof(AuthorizationLevel.System),
AuthorizationLevel.User => nameof(AuthorizationLevel.User),
_ => throw new ArgumentOutOfRangeException(nameof(requiredLevel))
};

return principal.HasClaim(c => c.Type == SecurityConstants.AuthLevelClaimType
&& (c.Value == nameof(AuthorizationLevel.Admin)
|| (c.Value == levelString && (keyName == null || string.Equals(principal.FindFirstValue(SecurityConstants.AuthLevelKeyNameClaimType), keyName, StringComparison.OrdinalIgnoreCase)))
));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using BenchmarkDotNet.Attributes;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Extensibility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Microsoft.Azure.WebJobs.Script.Benchmarks
{
public class CSharpCompilationBenchmarks
{
// Set of samples to benchmark
// TODOO: BlobTrigger, needs assembly refs working
[Params("DocumentDB", "HttpTrigger", "HttpTrigger-Cancellation", "HttpTrigger-CustomRoute", "NotificationHub")]
public string BenchmarkTrigger;

// Script source
private string ScriptPath;
private static string GetCSharpSamplePath([CallerFilePath] string thisFilePath = null) =>
Path.Combine(thisFilePath, "..", "..", "..", "sample", "CSharp");
private string ScriptSource;
private FunctionMetadata FunctionMetadata;

// Dyanmic Compilation
private readonly InteractiveAssemblyLoader AssemblyLoader = new InteractiveAssemblyLoader();
private IFunctionMetadataResolver Resolver;
private CSharpCompilationService CompilationService;

private IDotNetCompilation ScriptCompilation;
private DotNetCompilationResult ScriptAssembly;

[GlobalSetup]
public async Task SetupAsync()
{
ScriptPath = Path.Combine(GetCSharpSamplePath(), BenchmarkTrigger, "run.csx");
ScriptSource = File.ReadAllText(ScriptPath);
FunctionMetadata = new FunctionMetadata()
{
FunctionDirectory = Path.GetDirectoryName(ScriptPath),
ScriptFile = ScriptPath,
Name = BenchmarkTrigger,
Language = DotNetScriptTypes.CSharp
};

Resolver = new ScriptFunctionMetadataResolver(ScriptPath, Array.Empty<IScriptBindingProvider>(), NullLogger.Instance);
CompilationService = new CSharpCompilationService(Resolver, OptimizationLevel.Release);

ScriptCompilation = await CompilationService.GetFunctionCompilationAsync(FunctionMetadata);
ScriptAssembly = await ScriptCompilation.EmitAsync(default);
}

[Benchmark(Description = nameof(CSharpScript) + "." + nameof(CSharpScript.Create))]
public Script<object> ScriptCreation() =>
CSharpScript.Create(ScriptSource, options: Resolver.CreateScriptOptions(), assemblyLoader: AssemblyLoader);

[Benchmark(Description = nameof(CSharpCompilationService) + "." + nameof(CSharpCompilationService.GetFunctionCompilationAsync))]
public Task<IDotNetCompilation> GetFunctionCompilationAsync() => CompilationService.GetFunctionCompilationAsync(FunctionMetadata);

[Benchmark(Description = nameof(CSharpCompilationBenchmarks) + "." + nameof(CSharpCompilationBenchmarks.EmitAsync))]
public Task<DotNetCompilationResult> EmitAsync() => ScriptCompilation.EmitAsync(default);

[Benchmark(Description = nameof(DotNetCompilationResult) + "." + nameof(DotNetCompilationResult.Load))]
public void Load() => ScriptAssembly.Load(FunctionMetadata,Resolver, NullLogger.Instance);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Google.Protobuf.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Azure.WebJobs.Script.Grpc;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;

namespace Microsoft.Azure.WebJobs.Script.Benchmarks
{
public class GrpcMessageConversionBenchmarks
{
private static byte[] _byteArray = new byte[2000];
private static string _str = new string('-', 2000);
private static double _dbl = 2000;
private static byte[][] _byteJaggedArray = new byte[1000][];
private static string[] _strArray = new string[]{ new string('-', 1000), new string('-', 1000) };
private static double[] _dblArray = new double[1000];
private static long[] _longArray = new long[1000];
private static JObject _jObj = JObject.Parse(@"{'name': 'lilian'}");
internal GrpcCapabilities grpcCapabilities = new GrpcCapabilities(NullLogger.Instance);

// Not easy to benchmark
// public static HttpRequest _httpRequest;

[Benchmark]
public Task ToRpc_Null() => InvokeToRpc(((object)null));

[Benchmark]
public Task ToRpc_ByteArray() => InvokeToRpc(_byteArray);

[Benchmark]
public Task ToRpc_String() => InvokeToRpc(_str);

[Benchmark]
public Task ToRpc_Double() => InvokeToRpc(_dbl);

[Benchmark]
public Task ToRpc_ByteJaggedArray() => InvokeToRpc(_byteJaggedArray);

[Benchmark]
public Task ToRpc_StringArray() => InvokeToRpc(_strArray);

[Benchmark]
public Task ToRpc_DoubleArray() => InvokeToRpc(_dblArray);

[Benchmark]
public Task ToRpc_LongArray() => InvokeToRpc(_longArray);

[Benchmark]
public Task ToRpc_JObject() => InvokeToRpc(_jObj);

public async Task InvokeToRpc(object obj) => await obj.ToRpc(NullLogger.Instance, grpcCapabilities);

[GlobalSetup]
public void Setup()
{
MapField<string, string> addedCapabilities = new MapField<string, string>
{
{ RpcWorkerConstants.TypedDataCollection, "1" }
};
grpcCapabilities.UpdateCapabilities(addedCapabilities);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<!-- BenchmarkDotNet looks for a matching Project, see https://github.com/dotnet/BenchmarkDotNet/issues/1019 -->
<!--<AssemblyName>Microsoft.Azure.WebJobs.Script.Benchmarks</AssemblyName>-->
<RootNamespace>Microsoft.Azure.WebJobs.Script.Benchmarks</RootNamespace>
<LangVersion>Latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<ProjectReference Include="../../src/WebJobs.Script/WebJobs.Script.csproj" />
<ProjectReference Include="../../src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj" />
</ItemGroup>
</Project>
18 changes: 18 additions & 0 deletions benchmarks/WebJobs.Script.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using BenchmarkDotNet.Running;
using System.IO;

namespace Microsoft.Azure.WebJobs.Script.Benchmarks
{
public static class Program
{
public static void Main(string[] args) =>
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(args, RecommendedConfig.Create(
artifactsPath: new DirectoryInfo(Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "BenchmarkDotNet.Artifacts"))
));
}
}
Loading

0 comments on commit 6286c54

Please sign in to comment.