From 28fe7da06f400a1602e633660f0bb2b1da921453 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 31 Dec 2025 17:26:09 +0100 Subject: [PATCH] Revert "Use `DynamicDependency` (#2941)" This reverts commit 3cebcb9057a847de27c94d80909382fbce6fbfa2. --- src/BenchmarkDotNet/Code/CodeGenerator.cs | 49 ++++++++---- src/BenchmarkDotNet/Running/BuildPartition.cs | 4 + .../Templates/BenchmarkProgram.txt | 78 +++---------------- .../Templates/WasmLinkerDescription.xml | 1 + .../Toolchains/MonoWasm/WasmGenerator.cs | 17 +++- .../NaiveRunnableEmitDiff.cs | 8 +- .../WasmTests.cs | 21 ++--- 7 files changed, 73 insertions(+), 105 deletions(-) diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index 11ad184306..b75f7641c9 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -27,6 +27,8 @@ internal static string Generate(BuildPartition buildPartition) var benchmarksCode = new List(buildPartition.Benchmarks.Length); + var extraDefines = new List(); + foreach (var buildInfo in buildPartition.Benchmarks) { var benchmark = buildInfo.BenchmarkCase; @@ -62,11 +64,19 @@ internal static string Generate(BuildPartition buildPartition) benchmarksCode.Add(benchmarkTypeCode); } + if (buildPartition.IsNativeAot) + extraDefines.Add("#define NATIVEAOT"); + else if (buildPartition.IsNetFramework) + extraDefines.Add("#define NETFRAMEWORK"); + else if (buildPartition.IsWasm) + extraDefines.Add("#define WASM"); + string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt")) .Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath) - .Replace("$ExtraDefines$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty) + .Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines)) .Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode)) - .Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition)) + .Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor)) + .Replace("$NativeAotSwitch$", GetNativeAotSwitch(buildPartition)) .ToString(); return benchmarkProgramContent; @@ -184,19 +194,8 @@ private static string GetPassArguments(BenchmarkCase benchmarkCase) benchmarkCase.Descriptor.WorkloadMethod.GetParameters() .Select((parameter, index) => $"{GetParameterModifier(parameter)} arg{index}")); - private static string GetExtraAttributes(BuildPartition buildPartition) - { - var attrs = new List(buildPartition.Benchmarks.Length + 1); - if (buildPartition.RepresentativeBenchmarkCase.Descriptor.WorkloadMethod.GetCustomAttributes(false).OfType().Any()) - { - attrs.Add("[global::System.STAThread]"); - } - foreach (var buildInfo in buildPartition.Benchmarks) - { - attrs.Add($"[global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods, typeof(global::BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}))]"); - } - return string.Join(Environment.NewLine, attrs); - } + private static string GetExtraAttributes(Descriptor descriptor) + => descriptor.WorkloadMethod.GetCustomAttributes(false).OfType().Any() ? "[System.STAThreadAttribute]" : string.Empty; private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase) { @@ -252,6 +251,26 @@ private static string GetParameterModifier(ParameterInfo parameterInfo) return "ref"; } + /// + /// for NativeAOT we can't use reflection to load type and run a method, so we simply generate a switch for all types.. + /// + private static string GetNativeAotSwitch(BuildPartition buildPartition) + { + if (!buildPartition.IsNativeAot) + return default; + + var @switch = new StringBuilder(buildPartition.Benchmarks.Length * 30); + @switch.AppendLine("switch (id) {"); + + foreach (var buildInfo in buildPartition.Benchmarks) + @switch.AppendLine($"case {buildInfo.Id.Value}: BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;"); + + @switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");"); + @switch.AppendLine("}"); + + return @switch.ToString(); + } + private static Type GetFieldType(Type argumentType, ParameterInstance argument) { // #774 we can't store Span in a field, so we store an array (which is later casted to Span when we load the arguments) diff --git a/src/BenchmarkDotNet/Running/BuildPartition.cs b/src/BenchmarkDotNet/Running/BuildPartition.cs index 1a4c424e06..2ebe5ee44c 100644 --- a/src/BenchmarkDotNet/Running/BuildPartition.cs +++ b/src/BenchmarkDotNet/Running/BuildPartition.cs @@ -12,6 +12,7 @@ using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Toolchains.CsProj; using BenchmarkDotNet.Toolchains.DotNetCli; +using BenchmarkDotNet.Toolchains.MonoWasm; using BenchmarkDotNet.Toolchains.Roslyn; using JetBrains.Annotations; @@ -56,6 +57,9 @@ public BuildPartition(BenchmarkBuildInfo[] benchmarks, IResolver resolver) public bool IsNativeAot => RepresentativeBenchmarkCase.Job.IsNativeAOT(); + public bool IsWasm => Runtime is WasmRuntime // given job can have Wasm toolchain set, but Runtime == default ;) + || (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && toolchain is WasmToolchain); + public bool IsNetFramework => Runtime is ClrRuntime || (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && (toolchain is RoslynToolchain || toolchain is CsProjClassicNetToolchain)); diff --git a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt index 8a19f61832..f8db135569 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt @@ -4,10 +4,13 @@ $ExtraDefines$ // this file must not be importing any namespaces // we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778 -#pragma warning disable CS0436 // Type conflicts with imported type #if !NET7_0_OR_GREATER namespace System.Diagnostics.CodeAnalysis { + /// + /// Specifies that this constructor sets all required members for the current type, + /// and callers do not need to set any required members themselves. + /// [global::System.AttributeUsage(global::System.AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal sealed class SetsRequiredMembersAttribute : global::System.Attribute @@ -15,70 +18,6 @@ namespace System.Diagnostics.CodeAnalysis } } #endif -#if !NET5_0_OR_GREATER -namespace System.Diagnostics.CodeAnalysis -{ - [global::System.Flags] - internal enum DynamicallyAccessedMemberTypes - { - None = 0, - PublicParameterlessConstructor = 0x0001, - PublicConstructors = 0x0002 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, - NonPublicConstructors = 0x0004, - PublicMethods = 0x0008, - NonPublicMethods = 0x0010, - PublicFields = 0x0020, - NonPublicFields = 0x0040, - PublicNestedTypes = 0x0080, - NonPublicNestedTypes = 0x0100, - PublicProperties = 0x0200, - NonPublicProperties = 0x0400, - PublicEvents = 0x0800, - NonPublicEvents = 0x1000, - Interfaces = 0x2000, - All = ~global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None - } - [global::System.AttributeUsage(global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Field | global::System.AttributeTargets.Method, AllowMultiple = true, Inherited = false)] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [global::System.Diagnostics.Conditional("MULTI_TARGETING_SUPPORT_ATTRIBUTES")] - internal sealed class DynamicDependencyAttribute : global::System.Attribute - { - public DynamicDependencyAttribute(string memberSignature) - { - MemberSignature = memberSignature; - } - public DynamicDependencyAttribute(string memberSignature, global::System.Type type) - { - MemberSignature = memberSignature; - Type = type; - } - public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName) - { - MemberSignature = memberSignature; - TypeName = typeName; - AssemblyName = assemblyName; - } - public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, global::System.Type type) - { - MemberTypes = memberTypes; - Type = type; - } - public DynamicDependencyAttribute(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName) - { - MemberTypes = memberTypes; - TypeName = typeName; - AssemblyName = assemblyName; - } - public string MemberSignature { get; } - public global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes MemberTypes { get; } - public global::System.Type Type { get; } - public string TypeName { get; } - public string AssemblyName { get; } - public string Condition { get; set; } - } -} -#endif -#pragma warning restore CS0436 // Type conflicts with imported type // the namespace name must be in sync with WindowsDisassembler.BuildArguments namespace BenchmarkDotNet.Autogenerated @@ -99,7 +38,7 @@ namespace BenchmarkDotNet.Autogenerated private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args) { - global::BenchmarkDotNet.Engines.IHost host; + global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it if (global::BenchmarkDotNet.Engines.AnonymousPipesHost.TryGetFileHandles(args, out global::System.String writeHandle, out global::System.String readHandle)) host = new global::BenchmarkDotNet.Engines.AnonymousPipesHost(writeHandle, readHandle); else @@ -115,18 +54,23 @@ namespace BenchmarkDotNet.Autogenerated // which could cause the jitting/assembly loading to happen before we do anything // we have some jitting diagnosers and we want them to catch all the informations!! + // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it global::System.String benchmarkName = global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--benchmarkName"), 1)) ?? "not provided"; global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode = (global::BenchmarkDotNet.Diagnosers.RunMode) global::System.Int32.Parse(global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--diagnoserRunMode"), 1)) ?? "0"); global::System.Int32 id = args.Length > 0 - ? global::System.Int32.Parse(args[args.Length - 1]) + ? global::System.Int32.Parse(args[args.Length - 1]) // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it : 0; // used when re-using generated exe without BDN (typically to repro a bug) if (args.Length == 0) { host.WriteLine("You have not specified benchmark id (an integer) so the first benchmark will be executed."); } +#if NATIVEAOT + $NativeAotSwitch$ +#else global::System.Type type = typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}"); type.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static).Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode }); +#endif return 0; } catch (global::System.Exception oom) when (oom is global::System.OutOfMemoryException || oom is global::System.Reflection.TargetInvocationException reflection && reflection.InnerException is global::System.OutOfMemoryException) diff --git a/src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml b/src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml index 56899a3b8a..6c3f5a5c16 100644 --- a/src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml +++ b/src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml @@ -2,4 +2,5 @@ + diff --git a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs index fc9d0af94c..d1c6b41bb4 100644 --- a/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs @@ -29,10 +29,9 @@ protected override void GenerateProject(BuildPartition buildPartition, Artifacts if (((WasmRuntime)buildPartition.Runtime).Aot) { GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger); - - var linkDescriptionFileName = "WasmLinkerDescription.xml"; - File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath), linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName)); - } else + GenerateLinkerDescriptionFile(artifactsPaths); + } + else { GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger); } @@ -67,6 +66,16 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths GatherReferences(buildPartition, artifactsPaths, logger); } + protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths) + { + const string linkDescriptionFileName = "WasmLinkerDescription.xml"; + + var content = ResourceHelper.LoadTemplate(linkDescriptionFileName) + .Replace("$PROGRAMNAME$", artifactsPaths.ProgramName); + + File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, linkDescriptionFileName), content); + } + protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "AppBundle", MainJS); protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration) diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs index a269fd1792..ea0ea4a6e2 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs @@ -13,12 +13,8 @@ public class NaiveRunnableEmitDiff private static readonly HashSet IgnoredTypeNames = new HashSet() { "BenchmarkDotNet.Autogenerated.UniqueProgramName", - // not required to be used in the InProcess toolchains (it's already used in the host process) - "BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", - // Polyfill types added for older runtimes. - "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", - "System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes", - "System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute", + "BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", // not required to be used in the InProcess toolchains (it's already used in the host process) + "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", // Conditionally added in runtimes older than .Net 7. }; private static readonly HashSet IgnoredAttributeTypeNames = new HashSet() diff --git a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs index 2f4493c99d..72ff886eb2 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs @@ -11,7 +11,6 @@ using BenchmarkDotNet.Tests.Loggers; using BenchmarkDotNet.Tests.XUnit; using BenchmarkDotNet.Toolchains.DotNetCli; -using BenchmarkDotNet.Toolchains.MonoAotLLVM; using BenchmarkDotNet.Toolchains.MonoWasm; using Xunit; using Xunit.Abstractions; @@ -28,11 +27,11 @@ namespace BenchmarkDotNet.IntegrationTests /// public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { - private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode) + private ManualConfig GetConfig() { var dotnetVersion = "net8.0"; var logger = new OutputLogger(Output); - var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm", aotCompilerMode: aotCompilerMode); + var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm"); var mainJsPath = Path.Combine(AppContext.BaseDirectory, "AppBundle", "test-main.js"); return ManualConfig.CreateEmpty() @@ -45,10 +44,8 @@ private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode) .WithOption(ConfigOptions.GenerateMSBuildBinLog, true); } - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] - [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] - public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) + [FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + public void WasmIsSupported() { // Test fails on Linux non-x64. if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) @@ -56,13 +53,11 @@ public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode) return; } - CanExecute(GetConfig(aotCompilerMode)); + CanExecute(GetConfig()); } - [TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] - [InlineData(MonoAotCompilerMode.mini)] - [InlineData(MonoAotCompilerMode.wasm)] - public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) + [FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)] + public void WasmSupportsInProcessDiagnosers() { // Test fails on Linux non-x64. if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64) @@ -73,7 +68,7 @@ public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode) try { var diagnoser = new MockInProcessDiagnoser1(BenchmarkDotNet.Diagnosers.RunMode.NoOverhead); - var config = GetConfig(aotCompilerMode).AddDiagnoser(diagnoser); + var config = GetConfig().AddDiagnoser(diagnoser); CanExecute(config);