-
-
Notifications
You must be signed in to change notification settings - Fork 970
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 support for BenchmarkRunner.RunSource on .NET 5 #1677
Comments
@jonsequitur
Is P.S BDN print a lot of logs at the moment. I can send you a workaround on how to display only validation errors and a table. |
Is there an API for adding assembly references that the benchmarked code can depend on? If so, then we can use .NET Interactive to acquire the dependencies (including from NuGet) and provide the paths to the needed assemblies. |
This is good because it would require a dependency (maintainers don't like to add dependencies) The generated project from
We can add optional parameter:
MetadataReference.CreateFromFile(assembly.Location); //assembly.Location is null Possible workaround: https://stackoverflow.com/a/54371960
|
I'm not interested in benchmarking debug assemblies. I'm thinking of the notebook or REPL as a way to provide a UX for writing code to be benchmarked, but not as the compilation environment for the benchmark.
It's not something we've looked into much and not currently a goal.
The idea I had in mind was to provide a magic command that benchmarks the code within a cell rather than directly running it. But also allowing people to benchmark entirely external code (e.g. in a project) might also be nice because of the notebook's usefulness in visualizing and sharing results. |
I don't think anything needs to be done on the BDN side as it's hard to get this working in .NET Core.Step 1:Add package that scans
Step 2:Copy the reference assemblies to
Now, you can run it as following: var assemblyPaths = DependencyContext.Default.CompileLibraries
.Where(l => l.Assemblies.Count != 0)
.SelectMany(l => l.ResolveReferencePaths()).ToArray();
var references = assemblyPaths
.Select(path => MetadataReference.CreateFromFile(path))
.Cast<MetadataReference>()
.ToArray();
string source = ...;
var executingAssembly = Assembly.GetExecutingAssembly();
BenchmarkRunner.Run(source, executingAssembly, references); Not very convenient API. It would be easier if you implemented it completely on your side.Full code: using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Running;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.DependencyModel;
using TypeInfo = System.Reflection.TypeInfo;
public class Program
{
public static void Main(string[] args)
{
// uses <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
string source = @"
using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
[ShortRunJob(RuntimeMoniker.Net60)]
[MemoryDiagnoser]
public class MyBenchmarks
{
[Benchmark] public double B1() => MathNet.Numerics.SpecialFunctions.Beta(1, 2);
}
";
var assemblyPaths = DependencyContext.Default.CompileLibraries
.Where(l => l.Assemblies.Count != 0)
.SelectMany(l => l.ResolveReferencePaths()).ToArray();
var references = assemblyPaths
.Select(path => MetadataReference.CreateFromFile(path))
.Cast<MetadataReference>()
.ToArray();
// it should be Assembly.GetEntryAssembly(), but it returns null when:
// 1) calling from unmanaged code
// 2) calling from unit test framework? (https://stackoverflow.com/a/53228317)
// 3) debugging?
var executingAssembly = Assembly.GetExecutingAssembly();
var assemblyName = AssemblyName.GetAssemblyName(executingAssembly.Location).Name + Postfix; //ThisProject__GeneratedProject
var runInfos = GetBenchmarksFromSource(source, assemblyName, references);
//todo: copy ThisProject.csproj to ThisProject__GeneratedProject.csproj
BenchmarkRunner.Run(runInfos);
}
private const string Postfix = "__GeneratedProject";
public static BenchmarkRunInfo[] GetBenchmarksFromSource(string source, string assemblyName, IReadOnlyCollection<MetadataReference> assemblyReferences, IConfig config = null)
{
if (source == null) throw new ArgumentNullException(nameof(source));
var logger = HostEnvironmentInfo.FallbackLogger;
var compiledAssembly = CompileAssembly(source, assemblyName, assemblyReferences, logger);
if (compiledAssembly == null)
return Array.Empty<BenchmarkRunInfo>();
return compiledAssembly.GetTypes()
.Where(type => BenchmarkHelper.ContainsRunnableBenchmarks(type))
.Select(type => BenchmarkConverter.TypeToBenchmarks(type, config))
.Select(runInfo => new BenchmarkRunInfo(
runInfo.BenchmarksCases.Select(b => BenchmarkWithSource(b, source)).ToArray(),
runInfo.Type,
runInfo.Config))
.ToArray();
//// Version 2: simpler solution
//
// return compiledAssembly.GetTypes()
// .Select(type =>
// {
// try
// {
// return BenchmarkConverter.TypeToBenchmarks(type, config);
// }
// catch
// {
// return null;
// }
// })
// .Where(runInfo => runInfo != null && runInfo.BenchmarksCases.Length != 0)
// .ToArray();
static BenchmarkCase BenchmarkWithSource(BenchmarkCase b, string additionalLogic)
{
return BenchmarkCase.Create(
new Descriptor(
b.Descriptor.Type,
b.Descriptor.WorkloadMethod,
b.Descriptor.GlobalSetupMethod,
b.Descriptor.GlobalCleanupMethod,
b.Descriptor.IterationSetupMethod,
b.Descriptor.IterationCleanupMethod,
b.Descriptor.WorkloadMethodDisplayInfo,
additionalLogic,
b.Descriptor.Baseline,
b.Descriptor.Categories,
b.Descriptor.OperationsPerInvoke),
b.Job,
b.Parameters,
b.Config);
}
}
private static Assembly CompileAssembly(string benchmarkContent, string assemblyName, IReadOnlyCollection<MetadataReference> references, ILogger logger)
{
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOptimizationLevel(OptimizationLevel.Release)
.WithAllowUnsafe(true);
var parseOptions = new CSharpParseOptions(LanguageVersion.Preview, DocumentationMode.Parse, SourceCodeKind.Regular);
var syntaxTree = CSharpSyntaxTree.ParseText(benchmarkContent, parseOptions);
var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree }, references, compilationOptions);
string directoryName = Path.GetDirectoryName(typeof(BenchmarkCase).Assembly.Location)
?? throw new DirectoryNotFoundException(typeof(BenchmarkCase).Assembly.Location);
var outputPath = Path.Combine(directoryName, $"{assemblyName}.dll");
var compilationResult = compilation.Emit(outputPath);
if (!compilationResult.Success)
{
logger.WriteLine($"Compilation done with errors:");
foreach (var diagnostic in compilationResult.Diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error))
{
logger.WriteError($"{diagnostic.Location}{diagnostic.Id}: {diagnostic.GetMessage()}");
}
return null;
}
return Assembly.LoadFrom(outputPath);
}
}
public static class BenchmarkHelper
{
// from ReflectionExtensions.cs
internal static bool ContainsRunnableBenchmarks(Type type)
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsAbstract
|| typeInfo.IsSealed
|| typeInfo.IsNotPublic
|| typeInfo.IsGenericType && !IsRunnableGenericType(typeInfo))
return false;
return GetBenchmarks(typeInfo).Any();
}
private static bool IsRunnableGenericType(TypeInfo typeInfo)
=> // if it is an open generic - there must be GenericBenchmark attributes
(!typeInfo.IsGenericTypeDefinition || typeInfo.GenericTypeArguments.Any() || typeInfo.GetCustomAttributes(true).OfType<GenericTypeArgumentsAttribute>().Any())
&& typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); // we need public parameterless ctor to create it
private static MethodInfo[] GetBenchmarks(TypeInfo typeInfo)
=> typeInfo
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static) // we allow for Static now to produce a nice Validator warning later
.Where(method => method.GetCustomAttributes(true).OfType<BenchmarkAttribute>().Any())
.ToArray();
} You need to copy
|
// important assumption! project's file name === output dll name | |
string projectName = benchmarkTarget.GetTypeInfo().Assembly.GetName().Name; | |
var possibleNames = new HashSet<string> { $"{projectName}.csproj", $"{projectName}.fsproj", $"{projectName}.vbproj" }; | |
var projectFiles = rootDirectory | |
.EnumerateFiles("*proj", SearchOption.AllDirectories) | |
.Where(file => possibleNames.Contains(file.Name)) | |
.ToArray(); |
I suggest deprecating or removing the
|
@YegorStepanov I agree. These were experimental methods that were added in January 2016, but it seems that nowadays they are not useful or popular. Also, they have a limited compatibility scope and it's hard to maintain them. I think we should deprecate them in the next release and remove them in one of the future releases. |
👍 💯 (I really don't like these methods ;) ) |
I shared the code for This issue can be closed. |
Currently,
RunSource
is only supported on .NET Framework. It would be very useful to add support for this for .NET 5.The text was updated successfully, but these errors were encountered: