diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index cc06b11216..e27cf54364 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -64,7 +64,7 @@ public bool UseDisassemblyDiagnoser public OutlierMode Outliers { get; set; } [Option("affinity", Required = false, HelpText = "Affinity mask to set for the benchmark process")] - public int? Affinity { get; set; } + public ulong? Affinity { get; set; } [Option("allStats", Required = false, Default = false, HelpText = "Displays all statistics (min, max & more)")] public bool DisplayAllStatistics { get; set; } diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 3c15744439..2e2521d445 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -380,9 +380,28 @@ private static bool Validate(CommandLineOptions options, ILogger logger) return false; } + if (options.Affinity.HasValue && !TryConvertAffinity(options.Affinity.Value, IntPtr.Size, out _)) + { + logger.WriteLineError($"The provided affinity mask 0x{options.Affinity.Value:X} does not fit into the {IntPtr.Size * 8} bit process that hosts the benchmarks. Use a mask of at most 32 bits or run in a 64 bit process."); + return false; + } + return true; } + // a 32 bit process only reaches the first 32 processors and new IntPtr(long) throws there instead of truncating + internal static bool TryConvertAffinity(ulong mask, int pointerSize, out IntPtr affinity) + { + if (pointerSize >= 8) + { + affinity = new IntPtr(unchecked((long)mask)); + return true; + } + + affinity = new IntPtr(unchecked((int)mask)); + return mask <= uint.MaxValue; + } + private static IConfig CreateConfig(CommandLineOptions options, IConfig? globalConfig, string[] args) { var config = new ManualConfig(); @@ -469,8 +488,8 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig? globalConfig) if (baseJob != Job.Dry && options.Outliers != OutlierMode.RemoveUpper) baseJob = baseJob.WithOutlierMode(options.Outliers); - if (options.Affinity.HasValue) - baseJob = baseJob.WithAffinity((IntPtr)options.Affinity.Value); + if (options.Affinity.HasValue && TryConvertAffinity(options.Affinity.Value, IntPtr.Size, out var affinity)) + baseJob = baseJob.WithAffinity(affinity); if (options.LaunchCount.HasValue) baseJob = baseJob.WithLaunchCount(options.LaunchCount.Value); diff --git a/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs b/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs index 636dcfc35e..b8515879d2 100644 --- a/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs +++ b/src/BenchmarkDotNet/Jobs/EnvironmentMode.cs @@ -148,7 +148,7 @@ internal Runtime GetRuntime() { Jit = HasValue(JitCharacteristic) ? Jit : null, Runtime = HasValue(RuntimeCharacteristic) ? Runtime?.RuntimeMoniker : null, - Affinity = HasValue(AffinityCharacteristic) ? (int)Affinity : null + Affinity = HasValue(AffinityCharacteristic) ? (long)Affinity : null }; } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Models/BdnEnvironment.cs b/src/BenchmarkDotNet/Models/BdnEnvironment.cs index 1256d83c6f..1c56fa7e13 100644 --- a/src/BenchmarkDotNet/Models/BdnEnvironment.cs +++ b/src/BenchmarkDotNet/Models/BdnEnvironment.cs @@ -8,5 +8,5 @@ internal class BdnEnvironment : EnvironmentInfo { public RuntimeMoniker? Runtime { get; set; } public Jit? Jit { get; set; } - public int? Affinity { get; set; } + public long? Affinity { get; set; } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index a735ff4d99..7869fb30a9 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -495,6 +495,67 @@ public void PackagesPathParsedCorrectly() Assert.Equal(fakeRestoreDirectory, ((DotNetCliGenerator)toolchain.Generator).PackagesPath); } + [Fact] + public void UserCanSpecifyAffinity() + { + const ulong affinity = 0b1010; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(new IntPtr((long)affinity), config.GetJobs().Single().Environment.Affinity); + } + + [FactEnvSpecific("A mask with a bit above 32 does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] + public void UserCanSpecifyAffinityBeyondThirtyTwoProcessors() + { + const ulong affinity = 1UL << 40; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(new IntPtr((long)affinity), config.GetJobs().Single().Environment.Affinity); + } + + [FactEnvSpecific("A mask with the top bit set does not fit in IntPtr on a 32 bit runtime", EnvRequirement.Platform64BitOnly)] + public void UserCanSpecifyAffinityForTheSixtyFourthProcessor() + { + // the top bit is the 64th cpu, the last one FixAffinity supports without cpu groups. + // as an unsigned option it is written the way the mask reads + const ulong affinity = 1UL << 63; + var config = ConfigParser.Parse(["--affinity", affinity.ToString()], new OutputLogger(Output)).config; + + Assert.NotNull(config); + Assert.Equal(new IntPtr(unchecked((long)affinity)), config.GetJobs().Single().Environment.Affinity); + } + + [Theory] + [InlineData(0b1010UL)] + [InlineData(1UL << 31)] + [InlineData(uint.MaxValue)] + public void AffinityThatFitsThirtyTwoBitsIsAcceptedByAThirtyTwoBitProcess(ulong affinity) + { + Assert.True(ConfigParser.TryConvertAffinity(affinity, pointerSize: 4, out var converted)); + Assert.Equal(new IntPtr(unchecked((int)affinity)), converted); + } + + [Theory] + [InlineData(1UL << 32)] + [InlineData(1UL << 40)] + [InlineData(1UL << 63)] + public void AffinityWiderThanThirtyTwoBitsIsRejectedByAThirtyTwoBitProcess(ulong affinity) + { + Assert.False(ConfigParser.TryConvertAffinity(affinity, pointerSize: 4, out _)); + } + + [Theory] + [InlineData(0b1010UL)] + [InlineData(1UL << 40)] + [InlineData(1UL << 63)] + public void AffinityOfAnyWidthIsAcceptedByASixtyFourBitProcess(ulong affinity) + { + Assert.True(ConfigParser.TryConvertAffinity(affinity, pointerSize: 8, out var converted)); + Assert.Equal(new IntPtr(unchecked((long)affinity)), converted); + } + [Fact] public void UserCanSpecifyBuildTimeout() { diff --git a/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs b/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs index 81d35e488d..f8931012d8 100644 --- a/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs +++ b/tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs @@ -145,7 +145,7 @@ public Task PerfonarTableTest(string key) ] }; - private static EntryInfo Job(RuntimeMoniker? runtime = null, Jit? jit = null, int? affinity = null) => new EntryInfo + private static EntryInfo Job(RuntimeMoniker? runtime = null, Jit? jit = null, long? affinity = null) => new EntryInfo { Job = new JobInfo { diff --git a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs index 1e44f59fb0..d193ba41db 100644 --- a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs +++ b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs @@ -10,6 +10,7 @@ public enum EnvRequirement FullFrameworkOnly, NonFullFramework, DotNetCoreOnly, + Platform64BitOnly, NeedsPrivilegedProcess, NonGitHubDraftPR, } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs index 3c7ecd931f..82cf6de05d 100644 --- a/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs +++ b/tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirementChecker.cs @@ -20,6 +20,7 @@ public static class EnvRequirementChecker EnvRequirement.FullFrameworkOnly => BdnRuntimeInformation.IsFullFramework ? null : "Full .NET Framework-only test", EnvRequirement.NonFullFramework => !BdnRuntimeInformation.IsFullFramework ? null : "Non-Full .NET Framework test", EnvRequirement.DotNetCoreOnly => BdnRuntimeInformation.IsNetCore ? null : ".NET/.NET Core-only test", + EnvRequirement.Platform64BitOnly => BdnRuntimeInformation.Is64BitPlatform() ? null : "64 bit platform-only test", EnvRequirement.NeedsPrivilegedProcess => IsPrivilegedProcess() ? null : "Needs authorization to perform security-relevant functions", EnvRequirement.NonGitHubDraftPR => !IsGitHubDraftPR() ? null : "GitHub draft PR", _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, "Unknown value")