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