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
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
23 changes: 21 additions & 2 deletions src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Jobs/EnvironmentMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Models/BdnEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
61 changes: 61 additions & 0 deletions tests/BenchmarkDotNet.Tests/ConfigParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
2 changes: 1 addition & 1 deletion tests/BenchmarkDotNet.Tests/Perfonar/PerfonarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
1 change: 1 addition & 0 deletions tests/BenchmarkDotNet.Tests/Shared/XUnit/EnvRequirement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum EnvRequirement
FullFrameworkOnly,
NonFullFramework,
DotNetCoreOnly,
Platform64BitOnly,
NeedsPrivilegedProcess,
NonGitHubDraftPR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down