diff --git a/docs/articles/guides/console-args.md b/docs/articles/guides/console-args.md index 31b54a9137..765ad2e2bb 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. +* `--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 new file mode 100644 index 0000000000..333b3e4345 --- /dev/null +++ b/docs/articles/samples/IntroWakeLock.md @@ -0,0 +1,32 @@ +--- +uid: BenchmarkDotNet.Samples.IntroWakeLock +--- + +## Sample: IntroWakeLock + +Running benchmarks may sometimes take enough time such that the system enters sleep or turns off the display. + +Using a WakeLock prevents the Windows system doing so. + +### Source code + +[!code-csharp[IntroWakeLock.cs](../../../samples/BenchmarkDotNet.Samples/IntroWakeLock.cs)] + +### Command line + +``` +--wakeLock None +``` +``` +--wakeLock System +``` +``` +--wakeLock Display +``` + +### 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..099b347c7b --- /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.System)] + +namespace BenchmarkDotNet.Samples; + +// *** Attribute Style *** +[WakeLock(WakeLockType.Display)] +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.System; + } + + [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..1243ac8f8f --- /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 + /// Windows 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..551c38fa09 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 => 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/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index b70333cc1d..8585a77797 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.System; + 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..cdfb64a234 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,12 @@ 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) + { + 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 new file mode 100644 index 0000000000..f547e44764 --- /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. + /// + None, + + /// + /// Forces the system to be in the working state while benchmarks are running. + /// + System, + + /// + /// Forces the display to be on while benchmarks are running. + /// + Display + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs index 1dcc93281a..6c7b48b0e2 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("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.")] 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..4f520da38d 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -55,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.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..8e6203574d --- /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 sealed 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..a748f24626 --- /dev/null +++ b/src/BenchmarkDotNet/Running/WakeLock.cs @@ -0,0 +1,69 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Loggers; +using System; +using System.ComponentModel; +using System.Linq; + +namespace BenchmarkDotNet.Running; + +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, 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 ILogger logger; + + public WakeLockSentinel(WakeLockType wakeLockType, string reason, ILogger logger) + { + this.wakeLockType = wakeLockType; + this.logger = logger; + try + { + 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 (safePowerHandle != null) + { + 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(); + } + 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..bb634b5228 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/PowerRequestsParser.cs @@ -0,0 +1,187 @@ +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) + { + // 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) + { + 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); + } + } + } + + /// + /// 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/WakeLockTests.cs b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs new file mode 100644 index 0000000000..cf8d0d39b9 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/WakeLockTests.cs @@ -0,0 +1,179 @@ +using BenchmarkDotNet.Attributes; +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.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests; + +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.None, new DebugBuildConfig().WakeLock); + Assert.Equal(WakeLockType.None, new DebugInProcessConfig().WakeLock); + } + + [TheoryEnvSpecific(EnvRequirement.NonWindows)] + [InlineData(WakeLockType.None)] + [InlineData(WakeLockType.System)] + [InlineData(WakeLockType.Display)] + public void WakeLockIsWindowsOnly(WakeLockType wakeLockType) + { + 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", logger); + Assert.Null(wakeLock); + } + + [FactEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] + public void WakeLockRequireSystem() + { + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.System, "WakeLockTests", logger)) + { + Assert.NotNull(wakeLock); + Assert.Equal("SYSTEM", GetPowerRequests("WakeLockTests")); + } + Assert.Equal("", GetPowerRequests()); + } + + [FactEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] + public void WakeLockRequireDisplay() + { + using (IDisposable wakeLock = WakeLock.Request(WakeLockType.Display, "WakeLockTests", logger)) + { + Assert.NotNull(wakeLock); + Assert.Equal("DISPLAY, SYSTEM", GetPowerRequests("WakeLockTests")); + } + Assert.Equal("", GetPowerRequests()); + } + + [FactEnvSpecific(EnvRequirement.NonWindows)] + public void BenchmarkRunnerIgnoresWakeLock() => + _ = CanExecute(fullValidation: false); + + [WakeLock(WakeLockType.Display)] + public class IgnoreWakeLock + { + [Benchmark] public void Sleep() { } + } + +#if !NET462 + [SupportedOSPlatform("windows")] +#endif + [TheoryEnvSpecific(EnvRequirement.WindowsOnly, EnvRequirement.NeedsPrivilegedProcess)] + [InlineData(typeof(Default), "SYSTEM")] + [InlineData(typeof(None), "")] + [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 Default : Base { } + + [WakeLock(WakeLockType.None)] public class None : Base { } + + [WakeLock(WakeLockType.System)] public class RequireSystem : Base { } + + [WakeLock(WakeLockType.Display)] 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) + { + 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 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..48da315cd6 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(["--wakeLock", "Display"], new OutputLogger(Output)).config; + + Assert.Equal(WakeLockType.Display, config.WakeLock); + } + + [Fact] + public void WhenUserDoesNotSpecifyWakeLockTheDefaultValueIsUsed() + { + var config = ConfigParser.Parse([], new OutputLogger(Output)).config; + + Assert.Equal(DefaultConfig.Instance.WakeLock, 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..d77c5202ed 100644 --- a/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs +++ b/tests/BenchmarkDotNet.Tests/Configs/ImmutableConfigTests.cs @@ -376,6 +376,45 @@ 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 customWakeLock = WakeLockType.Display; + var mutable = ManualConfig.CreateEmpty().WithWakeLock(customWakeLock); + var final = ImmutableConfigBuilder.Create(mutable); + Assert.Equal(customWakeLock, final.WakeLock); + } + + [Theory] + [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) { var config = ManualConfig.CreateEmpty(); 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