From a7b1edc23ebb7546c719435bd133d7b629ed64c0 Mon Sep 17 00:00:00 2001 From: Leon van der Meer <64834803+leonvandermeer@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:35:39 +0200 Subject: [PATCH 1/4] Implement WakeLock so that the system does not enter sleep while benchmarks are running --- docs/articles/guides/console-args.md | 1 + docs/articles/samples/IntroWakeLock.md | 32 +++ docs/articles/samples/toc.yml | 2 + .../BenchmarkDotNet.Samples/IntroWakeLock.cs | 30 +++ .../Attributes/WakeLockAttribute.cs | 18 ++ src/BenchmarkDotNet/Configs/DebugConfig.cs | 1 + src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 + src/BenchmarkDotNet/Configs/IConfig.cs | 2 + .../Configs/ImmutableConfig.cs | 3 + .../Configs/ImmutableConfigBuilder.cs | 1 + src/BenchmarkDotNet/Configs/ManualConfig.cs | 11 ++ src/BenchmarkDotNet/Configs/WakeLockType.cs | 20 ++ .../ConsoleArguments/CommandLineOptions.cs | 4 + .../ConsoleArguments/ConfigParser.cs | 3 + .../Running/BenchmarkRunnerClean.cs | 1 + .../Running/WakeLock.PInvoke.cs | 77 ++++++++ .../Running/WakeLock.SafePowerHandle.cs | 13 ++ src/BenchmarkDotNet/Running/WakeLock.cs | 44 +++++ .../PowerRequest.cs | 9 + .../PowerRequestsParser.cs | 183 ++++++++++++++++++ .../StringExtensions.cs | 29 +++ .../WakeLockTests.cs | 182 +++++++++++++++++ .../ConfigParserTests.cs | 16 ++ .../Configs/ImmutableConfigTests.cs | 34 ++++ 24 files changed, 718 insertions(+) create mode 100644 docs/articles/samples/IntroWakeLock.md create mode 100644 samples/BenchmarkDotNet.Samples/IntroWakeLock.cs create mode 100644 src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs create mode 100644 src/BenchmarkDotNet/Configs/WakeLockType.cs create mode 100644 src/BenchmarkDotNet/Running/WakeLock.PInvoke.cs create mode 100644 src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs create mode 100644 src/BenchmarkDotNet/Running/WakeLock.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/PowerRequest.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs diff --git a/docs/articles/guides/console-args.md b/docs/articles/guides/console-args.md index 31b54a9137..8b79d6b506 100644 --- a/docs/articles/guides/console-args.md +++ b/docs/articles/guides/console-args.md @@ -243,6 +243,7 @@ dotnet run -c Release -- --filter * --runtimes net6.0 net8.0 --statisticalTest 5 * `--platform` the Platform that should be used. If not specified, the host process platform is used (default). AnyCpu/X86/X64/Arm/Arm64/LoongArch64. * `--runOncePerIteration` run the benchmark exactly once per iteration. * `--buildTimeout` build timeout in seconds. +* `--preventSleep` prevents the system from entering sleep or turning off the display. No/RequireSystem/RequireDisplay. * `--wasmEngine` full path to a java script engine used to run the benchmarks, used by Wasm toolchain. * `--wasmMainJS` path to the test-main.js file used by Wasm toolchain. Mandatory when using \"--runtimes wasm\" * `--expose_wasm` arguments for the JavaScript engine used by Wasm toolchain. diff --git a/docs/articles/samples/IntroWakeLock.md b/docs/articles/samples/IntroWakeLock.md new file mode 100644 index 0000000000..1cb3c5c96b --- /dev/null +++ b/docs/articles/samples/IntroWakeLock.md @@ -0,0 +1,32 @@ +--- +uid: BenchmarkDotNet.Samples.IntroWakeLock +--- + +## Sample: IntroWakeLock + +Running Benchmarks usually takes enough time such that the system enters sleep or turns of the display. + +Using a WakeLock prevents the system doing so. + +### Source code + +[!code-csharp[IntroWakeLock.cs](../../../samples/BenchmarkDotNet.Samples/IntroWakeLock.cs)] + +### Command line + +``` +--preventSleep No +``` +``` +--preventSleep RequireSystem +``` +``` +--preventSleep RequireDisplay +``` + +### Links + +* @BenchmarkDotNet.Attributes.WakeLockAttribute +* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroWakeLock + +--- diff --git a/docs/articles/samples/toc.yml b/docs/articles/samples/toc.yml index b9a1c45bf1..8dab3b5c6b 100644 --- a/docs/articles/samples/toc.yml +++ b/docs/articles/samples/toc.yml @@ -126,6 +126,8 @@ href: IntroTailcall.md - name: IntroVisualStudioProfiler href: IntroVisualStudioProfiler.md +- name: IntroWakeLock + href: IntroWakeLock.md - name: IntroWasm href: IntroWasm.md - name: IntroUnicode diff --git a/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs new file mode 100644 index 0000000000..136d60aba5 --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs @@ -0,0 +1,30 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System; +using System.Threading; + +// *** Attribute Style applied to Assembly *** +[assembly: WakeLock(WakeLockType.RequireSystem)] + +namespace BenchmarkDotNet.Samples; + +// *** Attribute Style *** +[WakeLock(WakeLockType.RequireDisplay)] +public class IntroWakeLock +{ + [Benchmark] + public void LongRunning() => Thread.Sleep(TimeSpan.FromSeconds(10)); +} + +// *** Object Style *** +[Config(typeof(Config))] +public class IntroWakeLockObjectStyle +{ + private class Config : ManualConfig + { + public Config() => WakeLock = WakeLockType.RequireSystem; + } + + [Benchmark] + public void LongRunning() => Thread.Sleep(TimeSpan.FromSeconds(10)); +} diff --git a/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs new file mode 100644 index 0000000000..b712f376c3 --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs @@ -0,0 +1,18 @@ +using BenchmarkDotNet.Configs; +using System; + +namespace BenchmarkDotNet.Attributes +{ + /// + /// Placing a on your assembly or class controls whether the + /// system enters sleep or turns off the display while benchmarks run. + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)] + public sealed class WakeLockAttribute : Attribute, IConfigSource + { + public WakeLockAttribute(WakeLockType wakeLockType) => + Config = ManualConfig.CreateEmpty().WithWakeLock(wakeLockType); + + public IConfig Config { get; } + } +} diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 0fdda7b08d..f36001d33d 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -71,6 +71,7 @@ public abstract class DebugConfig : IConfig public SummaryStyle SummaryStyle => SummaryStyle.Default; public ConfigUnionRule UnionRule => ConfigUnionRule.Union; public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout; + public WakeLockType WakeLock => DefaultConfig.Instance.WakeLock; public string ArtifactsPath => null; // DefaultConfig.ArtifactsPath will be used if the user does not specify it in explicit way diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index b70333cc1d..fc874b1359 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -87,6 +87,8 @@ public IEnumerable GetValidators() public TimeSpan BuildTimeout => TimeSpan.FromSeconds(120); + public WakeLockType WakeLock => WakeLockType.No; + public string ArtifactsPath { get diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index b311c235f5..9e3bf50ce6 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -55,6 +55,8 @@ public interface IConfig /// TimeSpan BuildTimeout { get; } + public WakeLockType WakeLock { get; } + /// /// Collect any errors or warnings when composing the configuration /// diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index b6e03126fd..0043ecbe4b 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -56,6 +56,7 @@ internal ImmutableConfig( SummaryStyle summaryStyle, ConfigOptions options, TimeSpan buildTimeout, + WakeLockType wakeLock, IReadOnlyList configAnalysisConclusion) { columnProviders = uniqueColumnProviders; @@ -78,6 +79,7 @@ internal ImmutableConfig( SummaryStyle = summaryStyle; Options = options; BuildTimeout = buildTimeout; + WakeLock = wakeLock; ConfigAnalysisConclusion = configAnalysisConclusion; } @@ -89,6 +91,7 @@ internal ImmutableConfig( public ICategoryDiscoverer CategoryDiscoverer { get; } public SummaryStyle SummaryStyle { get; } public TimeSpan BuildTimeout { get; } + public WakeLockType WakeLock { get; } public IEnumerable GetColumnProviders() => columnProviders; public IEnumerable GetExporters() => exporters; diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index d7c0b5eb0f..f93e5590d0 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -76,6 +76,7 @@ public static ImmutableConfig Create(IConfig source) source.SummaryStyle ?? SummaryStyle.Default, source.Options, source.BuildTimeout, + source.WakeLock, configAnalyse.AsReadOnly() ); } diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 5ea1be24e9..b14f41993b 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -58,6 +58,7 @@ public class ManualConfig : IConfig [PublicAPI] public ICategoryDiscoverer CategoryDiscoverer { get; set; } [PublicAPI] public SummaryStyle SummaryStyle { get; set; } [PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout; + [PublicAPI] public WakeLockType WakeLock { get; set; } = DefaultConfig.Instance.WakeLock; public IReadOnlyList ConfigAnalysisConclusion => emptyConclusion; @@ -109,6 +110,12 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) return this; } + public ManualConfig WithWakeLock(WakeLockType wakeLockType) + { + WakeLock = wakeLockType; + return this; + } + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This method will soon be removed, please start using .AddColumn() instead.")] public void Add(params IColumn[] newColumns) => AddColumn(newColumns); @@ -273,6 +280,7 @@ public void Add(IConfig config) columnHidingRules.AddRange(config.GetColumnHidingRules()); Options |= config.Options; BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout); + WakeLock = GetWakeLock(WakeLock, config.WakeLock); } /// @@ -327,5 +335,8 @@ private static TimeSpan GetBuildTimeout(TimeSpan current, TimeSpan other) => current == DefaultConfig.Instance.BuildTimeout ? other : TimeSpan.FromMilliseconds(Math.Max(current.TotalMilliseconds, other.TotalMilliseconds)); + + private static WakeLockType GetWakeLock(WakeLockType current, WakeLockType other) => + current.CompareTo(other) > 0 ? current : other; } } diff --git a/src/BenchmarkDotNet/Configs/WakeLockType.cs b/src/BenchmarkDotNet/Configs/WakeLockType.cs new file mode 100644 index 0000000000..e676c57a51 --- /dev/null +++ b/src/BenchmarkDotNet/Configs/WakeLockType.cs @@ -0,0 +1,20 @@ +namespace BenchmarkDotNet.Configs +{ + public enum WakeLockType + { + /// + /// Allows the system to enter sleep and/or turn off the display while benchmarks are running. + /// + No, + + /// + /// Forces the system to be in the working state while benchmarks are running. + /// + RequireSystem, + + /// + /// Forces the display to be on while benchmarks are running. + /// + RequireDisplay + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 1dcc93281a..63090f68f2 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.ConsoleArguments.ListBenchmarks; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Engines; @@ -179,6 +180,9 @@ public bool UseDisassemblyDiagnoser [Option("buildTimeout", Required = false, HelpText = "Build timeout in seconds.")] public int? TimeOutInSeconds { get; set; } + [Option("preventSleep", Required = false, HelpText = "Prevents the system from entering sleep or turning off the display. No/RequireSystem/RequireDisplay.")] + public WakeLockType? WakeLock { get; set; } + [Option("stopOnFirstError", Required = false, Default = false, HelpText = "Stop on first error.")] public bool StopOnFirstError { get; set; } diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs index 9ec37c39eb..a58c22da8b 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs @@ -392,6 +392,9 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo if (options.TimeOutInSeconds.HasValue) config.WithBuildTimeout(TimeSpan.FromSeconds(options.TimeOutInSeconds.Value)); + if (options.WakeLock.HasValue) + config.WithWakeLock(options.WakeLock.Value); + return config; } diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 6d9d48c3cc..5fb2a90e2c 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -38,6 +38,7 @@ internal static class BenchmarkRunnerClean internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) { + using var wakeLock = WakeLock.Request(WakeLock.GetWakeLockType(benchmarkRunInfos), "BenchmarkDotNet Running Benchmarks"); using var taskbarProgress = new TaskbarProgress(TaskbarProgressState.Indeterminate); var resolver = DefaultResolver; diff --git a/src/BenchmarkDotNet/Running/WakeLock.PInvoke.cs b/src/BenchmarkDotNet/Running/WakeLock.PInvoke.cs new file mode 100644 index 0000000000..9da3fd1eff --- /dev/null +++ b/src/BenchmarkDotNet/Running/WakeLock.PInvoke.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace BenchmarkDotNet.Running; + +internal partial class WakeLock +{ + private static class PInvoke + { + public static SafePowerHandle PowerCreateRequest(string reason) + { + REASON_CONTEXT context = new REASON_CONTEXT() + { + Version = POWER_REQUEST_CONTEXT_VERSION, + Flags = POWER_REQUEST_CONTEXT_FLAGS.POWER_REQUEST_CONTEXT_SIMPLE_STRING, + SimpleReasonString = reason + }; + SafePowerHandle safePowerHandle = PowerCreateRequest(context); + if (safePowerHandle.IsInvalid) { throw new Win32Exception(); } + return safePowerHandle; + } + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + private static extern SafePowerHandle PowerCreateRequest(REASON_CONTEXT Context); + + public static void PowerSetRequest(SafePowerHandle safePowerHandle, POWER_REQUEST_TYPE requestType) + { + if (!InvokePowerSetRequest(safePowerHandle, requestType)) + { + throw new Win32Exception(); + } + } + + [DllImport("kernel32.dll", EntryPoint = "PowerSetRequest", ExactSpelling = true, SetLastError = true)] + private static extern bool InvokePowerSetRequest(SafePowerHandle PowerRequest, POWER_REQUEST_TYPE RequestType); + + public static void PowerClearRequest(SafePowerHandle safePowerHandle, POWER_REQUEST_TYPE requestType) + { + if (!InvokePowerClearRequest(safePowerHandle, requestType)) + { + throw new Win32Exception(); + } + } + + [DllImport("kernel32.dll", EntryPoint = "PowerClearRequest", ExactSpelling = true, SetLastError = true)] + private static extern bool InvokePowerClearRequest(SafePowerHandle PowerRequest, POWER_REQUEST_TYPE RequestType); + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + public static extern bool CloseHandle(nint hObject); + + private struct REASON_CONTEXT + { + public uint Version; + + public POWER_REQUEST_CONTEXT_FLAGS Flags; + + [MarshalAs(UnmanagedType.LPWStr)] + public string SimpleReasonString; + } + + private const uint POWER_REQUEST_CONTEXT_VERSION = 0U; + + private enum POWER_REQUEST_CONTEXT_FLAGS : uint + { + POWER_REQUEST_CONTEXT_DETAILED_STRING = 2U, + POWER_REQUEST_CONTEXT_SIMPLE_STRING = 1U, + } + + public enum POWER_REQUEST_TYPE + { + PowerRequestDisplayRequired = 0, + PowerRequestSystemRequired = 1, + PowerRequestAwayModeRequired = 2, + PowerRequestExecutionRequired = 3, + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs b/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs new file mode 100644 index 0000000000..7ddc6d2eaa --- /dev/null +++ b/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs @@ -0,0 +1,13 @@ +using Microsoft.Win32.SafeHandles; + +namespace BenchmarkDotNet.Running; + +internal partial class WakeLock +{ + private class SafePowerHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafePowerHandle() : base(true) { } + + protected override bool ReleaseHandle() => PInvoke.CloseHandle(handle); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/WakeLock.cs b/src/BenchmarkDotNet/Running/WakeLock.cs new file mode 100644 index 0000000000..12e115e2e3 --- /dev/null +++ b/src/BenchmarkDotNet/Running/WakeLock.cs @@ -0,0 +1,44 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Helpers; +using System; +using System.Linq; + +namespace BenchmarkDotNet.Running; + +internal partial class WakeLock +{ + public static WakeLockType GetWakeLockType(BenchmarkRunInfo[] benchmarkRunInfos) => + benchmarkRunInfos.Select(static i => i.Config.WakeLock).Max(); + + public static IDisposable Request(WakeLockType wakeLockType, string reason) => + wakeLockType == WakeLockType.No || !OsDetector.IsWindows() ? null : new WakeLockSentinel(wakeLockType, reason); + + private class WakeLockSentinel : DisposeAtProcessTermination + { + private readonly WakeLockType wakeLockType; + private readonly SafePowerHandle safePowerHandle; + + public WakeLockSentinel(WakeLockType wakeLockType, string reason) + { + this.wakeLockType = wakeLockType; + safePowerHandle = PInvoke.PowerCreateRequest(reason); + PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); + if (wakeLockType == WakeLockType.RequireDisplay) + { + PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + } + } + + public override void Dispose() + { + if (wakeLockType == WakeLockType.RequireDisplay) + { + PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + } + PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); + safePowerHandle.Dispose(); + base.Dispose(); + } + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/PowerRequest.cs b/tests/BenchmarkDotNet.IntegrationTests/PowerRequest.cs new file mode 100644 index 0000000000..676ad23d40 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/PowerRequest.cs @@ -0,0 +1,9 @@ +namespace BenchmarkDotNet.IntegrationTests; + +internal class PowerRequest(string requestType, string requesterType, string requesterName, string? reason) +{ + public string RequestType { get; } = requestType; + public string RequesterType { get; } = requesterType; + public string RequesterName { get; } = requesterName; + public string? Reason { get; } = reason; +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs b/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs new file mode 100644 index 0000000000..9be168b106 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace BenchmarkDotNet.IntegrationTests; + +/// +/// Parses the output of 'powercfg /requests' command into a list of s. +/// +/// +/// +/// Not using Sprache. It is superseded by Superpower. +/// Not using Superpower. I gained more knowledge +/// implementing this class from scratch. +/// +/// Example input: +/// +/// DISPLAY: +/// [PROCESS] \Device\HarddiskVolume3\Program Files (x86)\Google\Chrome\Application\chrome.exe +/// Video Wake Lock +/// +/// SYSTEM: +/// [DRIVER] Realtek High Definition Audio(SST) ... +/// Er wordt momenteel een audiostream gebruikt. +/// [PROCESS] \Device\HarddiskVolume3\...\NoSleep.exe +/// [PROCESS] \Device\HarddiskVolume3\Program Files (x86)\Google\Chrome\Application\chrome.exe +/// Video Wake Lock +/// +/// AWAYMODE: +/// None. +/// +/// EXECUTION: +/// [PROCESS] \Device\HarddiskVolume3\Program Files (x86)\Google\Chrome\Application\chrome.exe +/// Playing audio +/// +/// PERFBOOST: +/// None. +/// +/// ACTIVELOCKSCREEN: +/// None. +/// +/// +/// +internal class PowerRequestsParser +{ + /// + /// Parses output of 'powercfg /requests' into a list of s. + /// + /// + /// + /// This method takes a list of s. Examines next token and decides how to + /// parse. + /// + /// + /// Output of 'powercfg /requests'. + public static IEnumerable Parse(string input) + { + using TokenStream tokens = new TokenStream(Tokens(input)); + while (tokens.TryPeek().HasValue) + { + foreach (PowerRequest item in ParseRequestType(tokens)) + { + yield return item; + } + } + } + + private static IEnumerable ParseRequestType(TokenStream tokens) + { + Token requestType = tokens.Take(TokenType.RequestType); + if (tokens.Peek().TokenType == TokenType.RequesterType) + { + while (tokens.Peek().TokenType == TokenType.RequesterType) + { + yield return ParseRequesterType(requestType, tokens); + } + } + else + { + _ = tokens.Take(TokenType.None); + } + _ = tokens.Take(TokenType.EmptyLine); + } + + private static PowerRequest ParseRequesterType(Token requestType, TokenStream tokens) + { + Token requesterType = tokens.Take(TokenType.RequesterType); + Token requesterName = tokens.Take(TokenType.RequesterName); + Token? reason = null; + if (tokens.Peek().TokenType == TokenType.Reason) + { + reason = tokens.Take(TokenType.Reason); + } + return new PowerRequest(requestType.Value, requesterType.Value, requesterName.Value, reason?.Value); + } + + /// + /// Converts the input into a list of s. + /// + /// + /// + /// Looking at above sample, tokenizing is made simple when done line by line. Each line + /// contains one or two s. + /// + /// + /// Output of 'powercfg /requests'. + private static IEnumerable Tokens(string input) + { + foreach (string line in input.EnumerateLines()) + { + if (line.Length == 0) + { + yield return new Token(TokenType.EmptyLine, ""); + } + else if (line[line.Length - 1] == ':') + { + yield return new Token(TokenType.RequestType, line.Substring(0, line.Length - 1).ToString()); + } + else if (string.Equals(line, "None.", StringComparison.InvariantCulture)) + { + yield return new Token(TokenType.None, line); + } + else if (line[0] == '[') + { + int pos = line.IndexOf(']'); + yield return new Token(TokenType.RequesterType, line.Substring(1, pos - 1)); + yield return new Token(TokenType.RequesterName, line.Substring(pos + 2)); + } + else + { + yield return new Token(TokenType.Reason, line.ToString()); + } + } + } + + /// + /// Adds and to an of + /// s. + /// + /// + private class TokenStream(IEnumerable tokens) : IDisposable + { + private readonly IEnumerator tokens = tokens.GetEnumerator(); + private Token? cached; + + public Token? TryPeek() => cached ??= tokens.MoveNext() ? tokens.Current : null; + + public Token Peek() => TryPeek() ?? throw new EndOfStreamException(); + + public Token Take(TokenType requestType) + { + Token peek = Peek(); + if (peek.TokenType == requestType) + { + cached = null; + return peek; + } + else + { + throw new InvalidCastException($"Unexpected Token of type '{peek.TokenType}'. Expected type '{requestType}'."); + } + } + + public void Dispose() => tokens.Dispose(); + } + + private enum TokenType + { + EmptyLine, + None, + Reason, + RequesterName, + RequesterType, + RequestType + } + + private readonly struct Token(TokenType tokenType, string value) + { + public TokenType TokenType { get; } = tokenType; + + public string Value { get; } = value; + } +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs b/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs new file mode 100644 index 0000000000..41ec5f7934 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace BenchmarkDotNet.IntegrationTests; + +internal static class StringExtensions +{ + private static readonly char[] newLine = Environment.NewLine.ToCharArray(); + + /// + /// Creates an array of strings by splitting this string at each occurrence of a newLine separator. + /// Contrary to calling value.Split('\r', '\n'), this method does not return an empty + /// string when CR is followed by LF. + /// + public static IEnumerable EnumerateLines(this string value) + { + int pos; + while ((pos = value.IndexOfAny(newLine)) >= 0) + { + yield return value.Substring(0, pos); + int stride = value[pos] == '\r' && value[pos + 1] == '\n' ? 2 : 1; + value = value.Substring(pos + stride); + } + if (value.Length > 0) + { + yield return value; + } + } +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs new file mode 100644 index 0000000000..bfe27c8370 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs @@ -0,0 +1,182 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Tests.XUnit; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests; + +public class WakeLockTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + private const string PingEventName = @"Global\WakeLockTests-ping"; + private const string PongEventName = @"Global\WakeLockTests-pong"; + private static readonly TimeSpan testTimeout = TimeSpan.FromMinutes(1); + + [Fact] + public void ConfigurationDefaultValue() + { + Assert.Equal(WakeLockType.No, DefaultConfig.Instance.WakeLock); + Assert.Equal(WakeLockType.No, new DebugBuildConfig().WakeLock); + Assert.Equal(WakeLockType.No, new DebugInProcessConfig().WakeLock); + } + + [TheoryEnvSpecific(EnvRequirement.NonWindows)] + [InlineData(WakeLockType.No)] + [InlineData(WakeLockType.RequireSystem)] + [InlineData(WakeLockType.RequireDisplay)] + public void WakeLockIsWindowsOnly(WakeLockType wakeLockType) + { + using IDisposable wakeLock = WakeLock.Request(wakeLockType, "dummy"); + Assert.Null(wakeLock); + } + + [FactEnvSpecific(EnvRequirement.WindowsOnly)] + public void WakeLockSleepOrDisplayIsAllowed() + { + using IDisposable wakeLock = WakeLock.Request(WakeLockType.No, "dummy"); + Assert.Null(wakeLock); + } + + [FactEnvSpecific(EnvRequirement.WindowsOnly)] + public void WakeLockRequireSystem() + { + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.RequireSystem, "WakeLockTests")) + { + Assert.NotNull(wakeLock); + Assert.Equal("SYSTEM", GetPowerRequests("WakeLockTests")); + } + Assert.Equal("", GetPowerRequests()); + } + + [FactEnvSpecific(EnvRequirement.WindowsOnly)] + public void WakeLockRequireDisplay() + { + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.RequireDisplay, "WakeLockTests")) + { + Assert.NotNull(wakeLock); + Assert.Equal("DISPLAY, SYSTEM", GetPowerRequests("WakeLockTests")); + } + Assert.Equal("", GetPowerRequests()); + } + + [FactEnvSpecific(EnvRequirement.NonWindows)] + public void BenchmarkRunnerIgnoresWakeLock() => + _ = CanExecute(fullValidation: false); + + [WakeLock(WakeLockType.RequireDisplay)] + public class IgnoreWakeLock + { + [Benchmark] public void Sleep() { } + } + +#if !NET462 + [SupportedOSPlatform("windows")] +#endif + [TheoryEnvSpecific(EnvRequirement.WindowsOnly)] + [InlineData(typeof(Sleepy), "")] + [InlineData(typeof(RequireSystem), "SYSTEM")] + [InlineData(typeof(RequireDisplay), "DISPLAY, SYSTEM")] + public async Task BenchmarkRunnerAcquiresWakeLock(Type type, string expected) + { + using EventWaitHandle + ping = new EventWaitHandle(false, EventResetMode.AutoReset, PingEventName), + pong = new EventWaitHandle(false, EventResetMode.AutoReset, PongEventName); + string pwrRequests = null; + Task task = WaitForBenchmarkRunningAndGetPowerRequests(); + _ = CanExecute(type, fullValidation: false); + await task; + + Assert.Equal(expected, pwrRequests); + + async Task WaitForBenchmarkRunningAndGetPowerRequests() + { + await AsTask(ping, testTimeout); + pwrRequests = GetPowerRequests("BenchmarkDotNet Running Benchmarks"); + pong.Set(); + } + } + + public class Sleepy : Base { } + + [WakeLock(WakeLockType.RequireSystem)] public class RequireSystem : Base { } + + [WakeLock(WakeLockType.RequireDisplay)] public class RequireDisplay : Base { } + + public class Base + { + [Benchmark] +#if !NET462 + [SupportedOSPlatform("windows")] +#endif + public void SignalBenchmarkRunningAndWaitForGetPowerRequests() + { + using EventWaitHandle + ping = EventWaitHandle.OpenExisting(PingEventName), + pong = EventWaitHandle.OpenExisting(PongEventName); + ping.Set(); + pong.WaitOne(testTimeout); + } + } + + private string GetPowerRequests(string? expectedReason = null) + { + Assert.True(IsAdministrator(), "'powercfg /requests' requires administrator privileges and must be executed from an elevated command prompt."); + string pwrRequests = ProcessHelper.RunAndReadOutput("powercfg", "/requests"); + Output.WriteLine(pwrRequests); // Useful to analyse failing tests. + string fileName = Process.GetCurrentProcess().MainModule.FileName; + string mustEndWith = fileName.Substring(Path.GetPathRoot(fileName).Length); + + return string.Join(", ", + from pr in PowerRequestsParser.Parse(pwrRequests) + where + pr.RequesterName.EndsWith(mustEndWith, StringComparison.InvariantCulture) && + string.Equals(pr.RequesterType, "PROCESS", StringComparison.InvariantCulture) && + (expectedReason == null || string.Equals(pr.Reason, expectedReason, StringComparison.InvariantCulture)) + select pr.RequestType); + } + + private static bool IsAdministrator() + { +#if !NET462 + // Following line prevents error CA1416: This call site is reachable on all platforms. + // 'WindowsIdentity.GetCurrent()' is only supported on: 'windows'. + Debug.Assert(OperatingSystem.IsWindows()); +#endif + using WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); + return new WindowsPrincipal(currentUser).IsInRole(WindowsBuiltInRole.Administrator); + } + + private Task AsTask(WaitHandle waitHandle, TimeSpan timeout) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + RegisteredWaitHandle rwh = null; + rwh = ThreadPool.RegisterWaitForSingleObject( + waitHandle, + (object state, bool timedOut) => + { + rwh.Unregister(null); + if (timedOut) + { + tcs.SetException(new TimeoutException()); + } + else + { + tcs.SetResult(true); + } + }, + null, + timeout, + true); + return tcs.Task; + } +} diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 0e7c439ddf..45caf9e603 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -364,6 +364,22 @@ public void WhenUserDoesNotSpecifyTimeoutTheDefaultValueIsUsed() Assert.Equal(DefaultConfig.Instance.BuildTimeout, config.BuildTimeout); } + [Fact] + public void UserCanSpecifyWakeLock() + { + var config = ConfigParser.Parse(["--preventSleep", "RequireSystem"], new OutputLogger(Output)).config; + + Assert.Equal(WakeLockType.RequireSystem, config.WakeLock); + } + + [Fact] + public void WhenUserDoesNotSpecifyWakeLockTheDefaultValueIsUsed() + { + var config = ConfigParser.Parse([], new OutputLogger(Output)).config; + + Assert.Equal(WakeLockType.No, config.WakeLock); + } + [Theory] [InlineData("net461")] [InlineData("net462")] diff --git a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs index 2393cb737c..cdd7c06a0b 100644 --- a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs +++ b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs @@ -376,6 +376,40 @@ public void WhenTwoCustomTimeoutsAreProvidedTheLongerOneIsUsed(bool direction) Assert.Equal(TimeSpan.FromSeconds(2), final.BuildTimeout); } + [Fact] + public void WhenWakeLockIsNotSpecifiedTheDefaultValueIsUsed() + { + var mutable = ManualConfig.CreateEmpty(); + var final = ImmutableConfigBuilder.Create(mutable); + Assert.Equal(DefaultConfig.Instance.WakeLock, final.WakeLock); + } + + [Fact] + public void CustomWakeLockHasPrecedenceOverDefaultWakeLock() + { + WakeLockType customTimeout = WakeLockType.RequireDisplay; + var mutable = ManualConfig.CreateEmpty().WithWakeLock(customTimeout); + var final = ImmutableConfigBuilder.Create(mutable); + Assert.Equal(customTimeout, final.WakeLock); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void WhenTwoCustomWakeLocksAreProvidedTheLongerOneIsUsed(bool direction) + { + var system = ManualConfig.CreateEmpty().WithWakeLock(WakeLockType.RequireSystem); + var display = ManualConfig.CreateEmpty().WithWakeLock(WakeLockType.RequireDisplay); + + if (direction) + system.Add(display); + else + display.Add(system); + + var final = ImmutableConfigBuilder.Create(direction ? system : display); + Assert.Equal(WakeLockType.RequireDisplay, final.WakeLock); + } + private static ManualConfig CreateConfigFromJobs(params Job[] jobs) { var config = ManualConfig.CreateEmpty(); From 75b75c98285157e05f491b187b97fe5257252d2a Mon Sep 17 00:00:00 2001 From: leonvandermeer <64834803+leonvandermeer@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:46:24 +0100 Subject: [PATCH 2/4] Apply rework from code review Co-authored-by: Tim Cassell <35501420+timcassell@users.noreply.github.com> --- docs/articles/guides/console-args.md | 2 +- docs/articles/samples/IntroWakeLock.md | 8 ++-- .../BenchmarkDotNet.Samples/IntroWakeLock.cs | 6 +-- src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 8 +++- src/BenchmarkDotNet/Configs/WakeLockType.cs | 6 +-- .../ConsoleArguments/CommandLineOptions.cs | 2 +- src/BenchmarkDotNet/Running/WakeLock.cs | 10 +++-- .../WakeLockTests.cs | 31 ++++++++------- .../ConfigParserTests.cs | 6 +-- .../Configs/ImmutableConfigTests.cs | 39 +++++++++++-------- 11 files changed, 68 insertions(+), 52 deletions(-) diff --git a/docs/articles/guides/console-args.md b/docs/articles/guides/console-args.md index 8b79d6b506..765ad2e2bb 100644 --- a/docs/articles/guides/console-args.md +++ b/docs/articles/guides/console-args.md @@ -243,7 +243,7 @@ dotnet run -c Release -- --filter * --runtimes net6.0 net8.0 --statisticalTest 5 * `--platform` the Platform that should be used. If not specified, the host process platform is used (default). AnyCpu/X86/X64/Arm/Arm64/LoongArch64. * `--runOncePerIteration` run the benchmark exactly once per iteration. * `--buildTimeout` build timeout in seconds. -* `--preventSleep` prevents the system from entering sleep or turning off the display. No/RequireSystem/RequireDisplay. +* `--wakeLock` prevents the system from entering sleep or turning off the display. None/System/Display. * `--wasmEngine` full path to a java script engine used to run the benchmarks, used by Wasm toolchain. * `--wasmMainJS` path to the test-main.js file used by Wasm toolchain. Mandatory when using \"--runtimes wasm\" * `--expose_wasm` arguments for the JavaScript engine used by Wasm toolchain. diff --git a/docs/articles/samples/IntroWakeLock.md b/docs/articles/samples/IntroWakeLock.md index 1cb3c5c96b..6e6effacc0 100644 --- a/docs/articles/samples/IntroWakeLock.md +++ b/docs/articles/samples/IntroWakeLock.md @@ -4,7 +4,7 @@ uid: BenchmarkDotNet.Samples.IntroWakeLock ## Sample: IntroWakeLock -Running Benchmarks usually takes enough time such that the system enters sleep or turns of the display. +Running benchmarks may sometimes take enough time such that the system enters sleep or turns off the display. Using a WakeLock prevents the system doing so. @@ -15,13 +15,13 @@ Using a WakeLock prevents the system doing so. ### Command line ``` ---preventSleep No +--wakeLock None ``` ``` ---preventSleep RequireSystem +--wakeLock System ``` ``` ---preventSleep RequireDisplay +--wakeLock Display ``` ### Links diff --git a/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs index 136d60aba5..099b347c7b 100644 --- a/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs +++ b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs @@ -4,12 +4,12 @@ using System.Threading; // *** Attribute Style applied to Assembly *** -[assembly: WakeLock(WakeLockType.RequireSystem)] +[assembly: WakeLock(WakeLockType.System)] namespace BenchmarkDotNet.Samples; // *** Attribute Style *** -[WakeLock(WakeLockType.RequireDisplay)] +[WakeLock(WakeLockType.Display)] public class IntroWakeLock { [Benchmark] @@ -22,7 +22,7 @@ public class IntroWakeLockObjectStyle { private class Config : ManualConfig { - public Config() => WakeLock = WakeLockType.RequireSystem; + public Config() => WakeLock = WakeLockType.System; } [Benchmark] diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index fc874b1359..8585a77797 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -87,7 +87,7 @@ public IEnumerable GetValidators() public TimeSpan BuildTimeout => TimeSpan.FromSeconds(120); - public WakeLockType WakeLock => WakeLockType.No; + public WakeLockType WakeLock => WakeLockType.System; public string ArtifactsPath { diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index b14f41993b..cdfb64a234 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -336,7 +336,11 @@ private static TimeSpan GetBuildTimeout(TimeSpan current, TimeSpan other) ? other : TimeSpan.FromMilliseconds(Math.Max(current.TotalMilliseconds, other.TotalMilliseconds)); - private static WakeLockType GetWakeLock(WakeLockType current, WakeLockType other) => - current.CompareTo(other) > 0 ? current : other; + private static WakeLockType GetWakeLock(WakeLockType current, WakeLockType other) + { + if (current == DefaultConfig.Instance.WakeLock) { return other; } + if (other == DefaultConfig.Instance.WakeLock) { return current; } + return current.CompareTo(other) > 0 ? current : other; + } } } diff --git a/src/BenchmarkDotNet/Configs/WakeLockType.cs b/src/BenchmarkDotNet/Configs/WakeLockType.cs index e676c57a51..f547e44764 100644 --- a/src/BenchmarkDotNet/Configs/WakeLockType.cs +++ b/src/BenchmarkDotNet/Configs/WakeLockType.cs @@ -5,16 +5,16 @@ public enum WakeLockType /// /// Allows the system to enter sleep and/or turn off the display while benchmarks are running. /// - No, + None, /// /// Forces the system to be in the working state while benchmarks are running. /// - RequireSystem, + System, /// /// Forces the display to be on while benchmarks are running. /// - RequireDisplay + Display } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 63090f68f2..6c7b48b0e2 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs @@ -180,7 +180,7 @@ public bool UseDisassemblyDiagnoser [Option("buildTimeout", Required = false, HelpText = "Build timeout in seconds.")] public int? TimeOutInSeconds { get; set; } - [Option("preventSleep", Required = false, HelpText = "Prevents the system from entering sleep or turning off the display. No/RequireSystem/RequireDisplay.")] + [Option("wakeLock", Required = false, HelpText = "Prevents the system from entering sleep or turning off the display. None/System/Display.")] public WakeLockType? WakeLock { get; set; } [Option("stopOnFirstError", Required = false, Default = false, HelpText = "Stop on first error.")] diff --git a/src/BenchmarkDotNet/Running/WakeLock.cs b/src/BenchmarkDotNet/Running/WakeLock.cs index 12e115e2e3..79318b282c 100644 --- a/src/BenchmarkDotNet/Running/WakeLock.cs +++ b/src/BenchmarkDotNet/Running/WakeLock.cs @@ -11,8 +11,12 @@ internal partial class WakeLock public static WakeLockType GetWakeLockType(BenchmarkRunInfo[] benchmarkRunInfos) => benchmarkRunInfos.Select(static i => i.Config.WakeLock).Max(); + private static readonly bool OsVersionIsSupported = + // Must be windows 7 or greater + OsDetector.IsWindows() && Environment.OSVersion.Version >= new Version(6, 1); + public static IDisposable Request(WakeLockType wakeLockType, string reason) => - wakeLockType == WakeLockType.No || !OsDetector.IsWindows() ? null : new WakeLockSentinel(wakeLockType, reason); + wakeLockType == WakeLockType.None || !OsVersionIsSupported ? null : new WakeLockSentinel(wakeLockType, reason); private class WakeLockSentinel : DisposeAtProcessTermination { @@ -24,7 +28,7 @@ public WakeLockSentinel(WakeLockType wakeLockType, string reason) this.wakeLockType = wakeLockType; safePowerHandle = PInvoke.PowerCreateRequest(reason); PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); - if (wakeLockType == WakeLockType.RequireDisplay) + if (wakeLockType == WakeLockType.Display) { PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); } @@ -32,7 +36,7 @@ public WakeLockSentinel(WakeLockType wakeLockType, string reason) public override void Dispose() { - if (wakeLockType == WakeLockType.RequireDisplay) + if (wakeLockType == WakeLockType.Display) { PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); } diff --git a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs index bfe27c8370..4d79a9b08f 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs @@ -25,15 +25,15 @@ public class WakeLockTests(ITestOutputHelper output) : BenchmarkTestExecutor(out [Fact] public void ConfigurationDefaultValue() { - Assert.Equal(WakeLockType.No, DefaultConfig.Instance.WakeLock); - Assert.Equal(WakeLockType.No, new DebugBuildConfig().WakeLock); - Assert.Equal(WakeLockType.No, new DebugInProcessConfig().WakeLock); + Assert.Equal(WakeLockType.System, DefaultConfig.Instance.WakeLock); + Assert.Equal(WakeLockType.System, new DebugBuildConfig().WakeLock); + Assert.Equal(WakeLockType.System, new DebugInProcessConfig().WakeLock); } [TheoryEnvSpecific(EnvRequirement.NonWindows)] - [InlineData(WakeLockType.No)] - [InlineData(WakeLockType.RequireSystem)] - [InlineData(WakeLockType.RequireDisplay)] + [InlineData(WakeLockType.None)] + [InlineData(WakeLockType.System)] + [InlineData(WakeLockType.Display)] public void WakeLockIsWindowsOnly(WakeLockType wakeLockType) { using IDisposable wakeLock = WakeLock.Request(wakeLockType, "dummy"); @@ -43,14 +43,14 @@ public void WakeLockIsWindowsOnly(WakeLockType wakeLockType) [FactEnvSpecific(EnvRequirement.WindowsOnly)] public void WakeLockSleepOrDisplayIsAllowed() { - using IDisposable wakeLock = WakeLock.Request(WakeLockType.No, "dummy"); + using IDisposable wakeLock = WakeLock.Request(WakeLockType.None, "dummy"); Assert.Null(wakeLock); } [FactEnvSpecific(EnvRequirement.WindowsOnly)] public void WakeLockRequireSystem() { - using (IDisposable wakeLock = WakeLock.Request(WakeLockType.RequireSystem, "WakeLockTests")) + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.System, "WakeLockTests")) { Assert.NotNull(wakeLock); Assert.Equal("SYSTEM", GetPowerRequests("WakeLockTests")); @@ -61,7 +61,7 @@ public void WakeLockRequireSystem() [FactEnvSpecific(EnvRequirement.WindowsOnly)] public void WakeLockRequireDisplay() { - using (IDisposable wakeLock = WakeLock.Request(WakeLockType.RequireDisplay, "WakeLockTests")) + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.Display, "WakeLockTests")) { Assert.NotNull(wakeLock); Assert.Equal("DISPLAY, SYSTEM", GetPowerRequests("WakeLockTests")); @@ -73,7 +73,7 @@ public void WakeLockRequireDisplay() public void BenchmarkRunnerIgnoresWakeLock() => _ = CanExecute(fullValidation: false); - [WakeLock(WakeLockType.RequireDisplay)] + [WakeLock(WakeLockType.Display)] public class IgnoreWakeLock { [Benchmark] public void Sleep() { } @@ -83,7 +83,8 @@ [Benchmark] public void Sleep() { } [SupportedOSPlatform("windows")] #endif [TheoryEnvSpecific(EnvRequirement.WindowsOnly)] - [InlineData(typeof(Sleepy), "")] + [InlineData(typeof(Default), "SYSTEM")] + [InlineData(typeof(None), "")] [InlineData(typeof(RequireSystem), "SYSTEM")] [InlineData(typeof(RequireDisplay), "DISPLAY, SYSTEM")] public async Task BenchmarkRunnerAcquiresWakeLock(Type type, string expected) @@ -106,11 +107,13 @@ async Task WaitForBenchmarkRunningAndGetPowerRequests() } } - public class Sleepy : Base { } + public class Default : Base { } - [WakeLock(WakeLockType.RequireSystem)] public class RequireSystem : Base { } + [WakeLock(WakeLockType.None)] public class None : Base { } - [WakeLock(WakeLockType.RequireDisplay)] public class RequireDisplay : Base { } + [WakeLock(WakeLockType.System)] public class RequireSystem : Base { } + + [WakeLock(WakeLockType.Display)] public class RequireDisplay : Base { } public class Base { diff --git a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs index 45caf9e603..48da315cd6 100644 --- a/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs +++ b/tests/BenchmarkDotNet.Tests/ConfigParserTests.cs @@ -367,9 +367,9 @@ public void WhenUserDoesNotSpecifyTimeoutTheDefaultValueIsUsed() [Fact] public void UserCanSpecifyWakeLock() { - var config = ConfigParser.Parse(["--preventSleep", "RequireSystem"], new OutputLogger(Output)).config; + var config = ConfigParser.Parse(["--wakeLock", "Display"], new OutputLogger(Output)).config; - Assert.Equal(WakeLockType.RequireSystem, config.WakeLock); + Assert.Equal(WakeLockType.Display, config.WakeLock); } [Fact] @@ -377,7 +377,7 @@ public void WhenUserDoesNotSpecifyWakeLockTheDefaultValueIsUsed() { var config = ConfigParser.Parse([], new OutputLogger(Output)).config; - Assert.Equal(WakeLockType.No, config.WakeLock); + Assert.Equal(DefaultConfig.Instance.WakeLock, config.WakeLock); } [Theory] diff --git a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs index cdd7c06a0b..d77c5202ed 100644 --- a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs +++ b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs @@ -387,27 +387,32 @@ public void WhenWakeLockIsNotSpecifiedTheDefaultValueIsUsed() [Fact] public void CustomWakeLockHasPrecedenceOverDefaultWakeLock() { - WakeLockType customTimeout = WakeLockType.RequireDisplay; - var mutable = ManualConfig.CreateEmpty().WithWakeLock(customTimeout); + WakeLockType customWakeLock = WakeLockType.Display; + var mutable = ManualConfig.CreateEmpty().WithWakeLock(customWakeLock); var final = ImmutableConfigBuilder.Create(mutable); - Assert.Equal(customTimeout, final.WakeLock); + Assert.Equal(customWakeLock, final.WakeLock); } [Theory] - [InlineData(false)] - [InlineData(true)] - public void WhenTwoCustomWakeLocksAreProvidedTheLongerOneIsUsed(bool direction) - { - var system = ManualConfig.CreateEmpty().WithWakeLock(WakeLockType.RequireSystem); - var display = ManualConfig.CreateEmpty().WithWakeLock(WakeLockType.RequireDisplay); - - if (direction) - system.Add(display); - else - display.Add(system); - - var final = ImmutableConfigBuilder.Create(direction ? system : display); - Assert.Equal(WakeLockType.RequireDisplay, final.WakeLock); + [InlineData(WakeLockType.None, WakeLockType.None, WakeLockType.None)] + [InlineData(WakeLockType.System, WakeLockType.None, WakeLockType.None)] + [InlineData(WakeLockType.Display, WakeLockType.None, WakeLockType.Display)] + [InlineData(WakeLockType.None, WakeLockType.System, WakeLockType.None)] + [InlineData(WakeLockType.System, WakeLockType.System, WakeLockType.System)] + [InlineData(WakeLockType.Display, WakeLockType.System, WakeLockType.Display)] + [InlineData(WakeLockType.None, WakeLockType.Display, WakeLockType.Display)] + [InlineData(WakeLockType.System, WakeLockType.Display, WakeLockType.Display)] + [InlineData(WakeLockType.Display, WakeLockType.Display, WakeLockType.Display)] + public void WhenTwoCustomWakeLocksAreProvidedDisplayBeatsNoneBeatsDefault( + WakeLockType left, WakeLockType right, WakeLockType expected) + { + var l = ManualConfig.CreateEmpty().WithWakeLock(left); + var r = ManualConfig.CreateEmpty().WithWakeLock(right); + + l.Add(r); + + var final = ImmutableConfigBuilder.Create(l); + Assert.Equal(expected, final.WakeLock); } private static ManualConfig CreateConfigFromJobs(params Job[] jobs) From 868a4d281fd4e97d36bb35e686dd434746cd76f5 Mon Sep 17 00:00:00 2001 From: leonvandermeer <64834803+leonvandermeer@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:11:21 +0100 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Adam Sitnik --- docs/articles/samples/IntroWakeLock.md | 2 +- src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs | 2 +- src/BenchmarkDotNet/Configs/DebugConfig.cs | 2 +- src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs | 2 +- src/BenchmarkDotNet/Running/WakeLock.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/articles/samples/IntroWakeLock.md b/docs/articles/samples/IntroWakeLock.md index 6e6effacc0..333b3e4345 100644 --- a/docs/articles/samples/IntroWakeLock.md +++ b/docs/articles/samples/IntroWakeLock.md @@ -6,7 +6,7 @@ uid: BenchmarkDotNet.Samples.IntroWakeLock Running benchmarks may sometimes take enough time such that the system enters sleep or turns off the display. -Using a WakeLock prevents the system doing so. +Using a WakeLock prevents the Windows system doing so. ### Source code diff --git a/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs index b712f376c3..1243ac8f8f 100644 --- a/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs +++ b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs @@ -5,7 +5,7 @@ namespace BenchmarkDotNet.Attributes { /// /// Placing a on your assembly or class controls whether the - /// system enters sleep or turns off the display while benchmarks run. + /// Windows system enters sleep or turns off the display while benchmarks run. /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)] public sealed class WakeLockAttribute : Attribute, IConfigSource diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index f36001d33d..551c38fa09 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -71,7 +71,7 @@ public abstract class DebugConfig : IConfig public SummaryStyle SummaryStyle => SummaryStyle.Default; public ConfigUnionRule UnionRule => ConfigUnionRule.Union; public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout; - public WakeLockType WakeLock => DefaultConfig.Instance.WakeLock; + public WakeLockType WakeLock => WakeLockType.None; public string ArtifactsPath => null; // DefaultConfig.ArtifactsPath will be used if the user does not specify it in explicit way diff --git a/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs b/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs index 7ddc6d2eaa..8e6203574d 100644 --- a/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs +++ b/src/BenchmarkDotNet/Running/WakeLock.SafePowerHandle.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Running; internal partial class WakeLock { - private class SafePowerHandle : SafeHandleZeroOrMinusOneIsInvalid + private sealed class SafePowerHandle : SafeHandleZeroOrMinusOneIsInvalid { private SafePowerHandle() : base(true) { } diff --git a/src/BenchmarkDotNet/Running/WakeLock.cs b/src/BenchmarkDotNet/Running/WakeLock.cs index 79318b282c..ec9fd311a7 100644 --- a/src/BenchmarkDotNet/Running/WakeLock.cs +++ b/src/BenchmarkDotNet/Running/WakeLock.cs @@ -15,7 +15,7 @@ public static WakeLockType GetWakeLockType(BenchmarkRunInfo[] benchmarkRunInfos) // Must be windows 7 or greater OsDetector.IsWindows() && Environment.OSVersion.Version >= new Version(6, 1); - public static IDisposable Request(WakeLockType wakeLockType, string reason) => + public static IDisposable? Request(WakeLockType wakeLockType, string reason) => wakeLockType == WakeLockType.None || !OsVersionIsSupported ? null : new WakeLockSentinel(wakeLockType, reason); private class WakeLockSentinel : DisposeAtProcessTermination From a256b78b315cbf3017e95d24033ba58e79f87ce5 Mon Sep 17 00:00:00 2001 From: Leon van der Meer <64834803+leonvandermeer@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:30:29 +0100 Subject: [PATCH 4/4] Apply rework from code review --- .../Running/BenchmarkRunnerClean.cs | 2 +- src/BenchmarkDotNet/Running/WakeLock.cs | 45 ++++++++++++++----- .../PowerRequestsParser.cs | 8 +++- .../StringExtensions.cs | 29 ------------ .../WakeLockTests.cs | 40 +++++++---------- .../XUnit/EnvRequirement.cs | 3 +- .../XUnit/EnvRequirementChecker.cs | 12 +++++ 7 files changed, 71 insertions(+), 68 deletions(-) delete mode 100644 tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 5fb2a90e2c..4f520da38d 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -38,7 +38,6 @@ internal static class BenchmarkRunnerClean internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) { - using var wakeLock = WakeLock.Request(WakeLock.GetWakeLockType(benchmarkRunInfos), "BenchmarkDotNet Running Benchmarks"); using var taskbarProgress = new TaskbarProgress(TaskbarProgressState.Indeterminate); var resolver = DefaultResolver; @@ -56,6 +55,7 @@ internal static Summary[] Run(BenchmarkRunInfo[] benchmarkRunInfos) using (var streamLogger = new StreamLogger(GetLogFileStreamWriter(benchmarkRunInfos, logFilePath))) { var compositeLogger = CreateCompositeLogger(benchmarkRunInfos, streamLogger); + using var wakeLock = WakeLock.Request(WakeLock.GetWakeLockType(benchmarkRunInfos), "BenchmarkDotNet Running Benchmarks", streamLogger); var eventProcessor = new CompositeEventProcessor(benchmarkRunInfos); eventProcessor.OnStartValidationStage(); diff --git a/src/BenchmarkDotNet/Running/WakeLock.cs b/src/BenchmarkDotNet/Running/WakeLock.cs index ec9fd311a7..a748f24626 100644 --- a/src/BenchmarkDotNet/Running/WakeLock.cs +++ b/src/BenchmarkDotNet/Running/WakeLock.cs @@ -1,7 +1,9 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Loggers; using System; +using System.ComponentModel; using System.Linq; namespace BenchmarkDotNet.Running; @@ -15,33 +17,52 @@ public static WakeLockType GetWakeLockType(BenchmarkRunInfo[] benchmarkRunInfos) // Must be windows 7 or greater OsDetector.IsWindows() && Environment.OSVersion.Version >= new Version(6, 1); - public static IDisposable? Request(WakeLockType wakeLockType, string reason) => - wakeLockType == WakeLockType.None || !OsVersionIsSupported ? null : new WakeLockSentinel(wakeLockType, reason); + public static IDisposable? Request(WakeLockType wakeLockType, string reason, ILogger logger) => + wakeLockType == WakeLockType.None || !OsVersionIsSupported ? null : new WakeLockSentinel(wakeLockType, reason, logger); private class WakeLockSentinel : DisposeAtProcessTermination { private readonly WakeLockType wakeLockType; - private readonly SafePowerHandle safePowerHandle; + private readonly SafePowerHandle? safePowerHandle; + private readonly ILogger logger; - public WakeLockSentinel(WakeLockType wakeLockType, string reason) + public WakeLockSentinel(WakeLockType wakeLockType, string reason, ILogger logger) { this.wakeLockType = wakeLockType; - safePowerHandle = PInvoke.PowerCreateRequest(reason); - PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); - if (wakeLockType == WakeLockType.Display) + this.logger = logger; + try { - PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + safePowerHandle = PInvoke.PowerCreateRequest(reason); + PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); + if (wakeLockType == WakeLockType.Display) + { + PInvoke.PowerSetRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + } + } + catch (Win32Exception ex) + { + logger.WriteLineError($"Unable to prevent the system from entering sleep or turning off the display (error message: {ex.Message})."); } } public override void Dispose() { - if (wakeLockType == WakeLockType.Display) + if (safePowerHandle != null) { - PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + try + { + if (wakeLockType == WakeLockType.Display) + { + PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestDisplayRequired); + } + PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); + } + catch (Win32Exception ex) + { + logger.WriteLineError($"Unable to allow the system from entering sleep or turning off the display (error message: {ex.Message})."); + } + safePowerHandle.Dispose(); } - PInvoke.PowerClearRequest(safePowerHandle, PInvoke.POWER_REQUEST_TYPE.PowerRequestSystemRequired); - safePowerHandle.Dispose(); base.Dispose(); } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs b/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs index 9be168b106..bb634b5228 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs @@ -106,7 +106,11 @@ private static PowerRequest ParseRequesterType(Token requestType, TokenStream to /// Output of 'powercfg /requests'. private static IEnumerable Tokens(string input) { - foreach (string line in input.EnumerateLines()) + // Contrary to calling input.Split('\r', '\n'), StringReader's ReadLine method does not + // return an empty string when CR is followed by LF. + StringReader reader = new StringReader(input); + string? line; + while ((line = reader.ReadLine()) != null) { if (line.Length == 0) { @@ -128,7 +132,7 @@ private static IEnumerable Tokens(string input) } else { - yield return new Token(TokenType.Reason, line.ToString()); + yield return new Token(TokenType.Reason, line); } } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs b/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs deleted file mode 100644 index 41ec5f7934..0000000000 --- a/tests/BenchmarkDotNet.IntegrationTests/StringExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace BenchmarkDotNet.IntegrationTests; - -internal static class StringExtensions -{ - private static readonly char[] newLine = Environment.NewLine.ToCharArray(); - - /// - /// Creates an array of strings by splitting this string at each occurrence of a newLine separator. - /// Contrary to calling value.Split('\r', '\n'), this method does not return an empty - /// string when CR is followed by LF. - /// - public static IEnumerable EnumerateLines(this string value) - { - int pos; - while ((pos = value.IndexOfAny(newLine)) >= 0) - { - yield return value.Substring(0, pos); - int stride = value[pos] == '\r' && value[pos + 1] == '\n' ? 2 : 1; - value = value.Substring(pos + stride); - } - if (value.Length > 0) - { - yield return value; - } - } -} diff --git a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs index 4d79a9b08f..cf8d0d39b9 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs @@ -2,13 +2,13 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Tests.Loggers; using BenchmarkDotNet.Tests.XUnit; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Versioning; -using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -16,18 +16,24 @@ namespace BenchmarkDotNet.IntegrationTests; -public class WakeLockTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +public class WakeLockTests : BenchmarkTestExecutor { private const string PingEventName = @"Global\WakeLockTests-ping"; private const string PongEventName = @"Global\WakeLockTests-pong"; private static readonly TimeSpan testTimeout = TimeSpan.FromMinutes(1); + private readonly OutputLogger logger; + + public WakeLockTests(ITestOutputHelper output) : base(output) + { + logger = new OutputLogger(Output); + } [Fact] public void ConfigurationDefaultValue() { Assert.Equal(WakeLockType.System, DefaultConfig.Instance.WakeLock); - Assert.Equal(WakeLockType.System, new DebugBuildConfig().WakeLock); - Assert.Equal(WakeLockType.System, new DebugInProcessConfig().WakeLock); + Assert.Equal(WakeLockType.None, new DebugBuildConfig().WakeLock); + Assert.Equal(WakeLockType.None, new DebugInProcessConfig().WakeLock); } [TheoryEnvSpecific(EnvRequirement.NonWindows)] @@ -36,21 +42,21 @@ public void ConfigurationDefaultValue() [InlineData(WakeLockType.Display)] public void WakeLockIsWindowsOnly(WakeLockType wakeLockType) { - using IDisposable wakeLock = WakeLock.Request(wakeLockType, "dummy"); + using IDisposable wakeLock = WakeLock.Request(wakeLockType, "dummy", logger); Assert.Null(wakeLock); } [FactEnvSpecific(EnvRequirement.WindowsOnly)] public void WakeLockSleepOrDisplayIsAllowed() { - using IDisposable wakeLock = WakeLock.Request(WakeLockType.None, "dummy"); + using IDisposable wakeLock = WakeLock.Request(WakeLockType.None, "dummy", logger); Assert.Null(wakeLock); } - [FactEnvSpecific(EnvRequirement.WindowsOnly)] + [FactEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] public void WakeLockRequireSystem() { - using (IDisposable wakeLock = WakeLock.Request(WakeLockType.System, "WakeLockTests")) + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.System, "WakeLockTests", logger)) { Assert.NotNull(wakeLock); Assert.Equal("SYSTEM", GetPowerRequests("WakeLockTests")); @@ -58,10 +64,10 @@ public void WakeLockRequireSystem() Assert.Equal("", GetPowerRequests()); } - [FactEnvSpecific(EnvRequirement.WindowsOnly)] + [FactEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] public void WakeLockRequireDisplay() { - using (IDisposable wakeLock = WakeLock.Request(WakeLockType.Display, "WakeLockTests")) + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.Display, "WakeLockTests", logger)) { Assert.NotNull(wakeLock); Assert.Equal("DISPLAY, SYSTEM", GetPowerRequests("WakeLockTests")); @@ -82,7 +88,7 @@ [Benchmark] public void Sleep() { } #if !NET462 [SupportedOSPlatform("windows")] #endif - [TheoryEnvSpecific(EnvRequirement.WindowsOnly)] + [TheoryEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] [InlineData(typeof(Default), "SYSTEM")] [InlineData(typeof(None), "")] [InlineData(typeof(RequireSystem), "SYSTEM")] @@ -133,7 +139,6 @@ public void SignalBenchmarkRunningAndWaitForGetPowerRequests() private string GetPowerRequests(string? expectedReason = null) { - Assert.True(IsAdministrator(), "'powercfg /requests' requires administrator privileges and must be executed from an elevated command prompt."); string pwrRequests = ProcessHelper.RunAndReadOutput("powercfg", "/requests"); Output.WriteLine(pwrRequests); // Useful to analyse failing tests. string fileName = Process.GetCurrentProcess().MainModule.FileName; @@ -148,17 +153,6 @@ from pr in PowerRequestsParser.Parse(pwrRequests) select pr.RequestType); } - private static bool IsAdministrator() - { -#if !NET462 - // Following line prevents error CA1416: This call site is reachable on all platforms. - // 'WindowsIdentity.GetCurrent()' is only supported on: 'windows'. - Debug.Assert(OperatingSystem.IsWindows()); -#endif - using WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); - return new WindowsPrincipal(currentUser).IsInRole(WindowsBuiltInRole.Administrator); - } - private Task AsTask(WaitHandle waitHandle, TimeSpan timeout) { TaskCompletionSource tcs = new TaskCompletionSource(); diff --git a/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirement.cs b/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirement.cs index 563513c6f6..79bfb5a1ae 100644 --- a/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirement.cs +++ b/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirement.cs @@ -7,5 +7,6 @@ public enum EnvRequirement NonLinux, FullFrameworkOnly, NonFullFramework, - DotNetCoreOnly + DotNetCoreOnly, + NeedsPrivilegedProcess } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirementChecker.cs b/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirementChecker.cs index 80ea642d07..22f131455d 100644 --- a/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirementChecker.cs +++ b/tests/BenchmarkDotNet.Tests/XUnit/EnvRequirementChecker.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Runtime.InteropServices; +using System.Security.Principal; using BenchmarkDotNet.Jobs; using JetBrains.Annotations; using BdnRuntimeInformation = BenchmarkDotNet.Portability.RuntimeInformation; @@ -19,8 +20,19 @@ 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.NeedsPrivilegedProcess => IsPrivilegedProcess() ? null : "Needs authorization to perform security-relevant functions", _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, "Unknown value") }; + private static bool IsPrivilegedProcess() + { +#if NET462 + using WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); + return new WindowsPrincipal(currentUser).IsInRole(WindowsBuiltInRole.Administrator); +#else + return Environment.IsPrivilegedProcess; +#endif + } + private static bool IsRuntime(RuntimeMoniker moniker) => BdnRuntimeInformation.GetCurrentRuntime().RuntimeMoniker == moniker; } \ No newline at end of file