From baf42abba482dcffd4ccdedca93ce97b6dc0344a Mon Sep 17 00:00:00 2001 From: Shawn Yuan Date: Wed, 17 Sep 2025 17:09:01 +0800 Subject: [PATCH] add power management Signed-off-by: Shawn Yuan --- src/modules/awake/Awake/Core/Manager.cs | 107 +++++++++++++- .../awake/Awake/Core/PowerSchemeManager.cs | 136 ++++++++++++++++++ 2 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 src/modules/awake/Awake/Core/PowerSchemeManager.cs diff --git a/src/modules/awake/Awake/Core/Manager.cs b/src/modules/awake/Awake/Core/Manager.cs index 1e22372999..9fb9d28fec 100644 --- a/src/modules/awake/Awake/Core/Manager.cs +++ b/src/modules/awake/Awake/Core/Manager.cs @@ -63,9 +63,16 @@ namespace Awake.Core // Foreground usage tracker instance (lifecycle managed by Program) internal static ForegroundUsageTracker? UsageTracker { get; set; } + private static PowerSchemeManager powerSchemeManager; + + // Power scheme auto-switch (activity mode) + private static string? _originalPowerSchemeGuid; + private static bool _powerSchemeSwitched; + static Manager() { _tokenSource = new CancellationTokenSource(); + powerSchemeManager = new PowerSchemeManager(); _stateQueue = []; ModuleSettings = new SettingsUtils(); } @@ -207,6 +214,94 @@ namespace Awake.Core forceAdd ? TrayIconAction.Add : TrayIconAction.Update); } + private static void CaptureOriginalPowerScheme() + { + try + { + powerSchemeManager.RefreshSchemes(); + _originalPowerSchemeGuid = powerSchemeManager + .GetAllSchemes() + .FirstOrDefault(s => s.IsActive)?.PSGuid; + Logger.LogInfo($"Captured original power scheme: {_originalPowerSchemeGuid ?? "UNKNOWN"}"); + } + catch (Exception ex) + { + Logger.LogWarning($"Failed to capture original power scheme: {ex.Message}"); + } + } + + private static void SwitchToHighestPerformanceIfNeeded() + { + if (_powerSchemeSwitched) + { + return; + } + + try + { + powerSchemeManager.RefreshSchemes(); + var highest = powerSchemeManager.GetHighestPerformanceScheme(); + if (highest == null) + { + Logger.LogInfo("No power schemes found when attempting high performance switch."); + return; + } + + if (highest.IsActive) + { + Logger.LogInfo("Already on highest performance scheme – no switch needed."); + return; + } + + if (_originalPowerSchemeGuid == null) + { + CaptureOriginalPowerScheme(); + } + + if (powerSchemeManager.SwitchScheme(highest.PSGuid)) + { + _powerSchemeSwitched = true; + Logger.LogInfo($"Switched to highest performance scheme: {highest.Name} ({highest.PSGuid})"); + } + else + { + Logger.LogWarning($"Failed to switch to highest performance scheme: {highest.Name} ({highest.PSGuid})"); + } + } + catch (Exception ex) + { + Logger.LogWarning($"Exception while attempting to switch power scheme: {ex.Message}"); + } + } + + private static void RestoreOriginalPowerSchemeIfNeeded() + { + if (!_powerSchemeSwitched || string.IsNullOrWhiteSpace(_originalPowerSchemeGuid)) + { + return; + } + + try + { + if (powerSchemeManager.SwitchScheme(_originalPowerSchemeGuid)) + { + Logger.LogInfo($"Restored original power scheme: {_originalPowerSchemeGuid}"); + } + else + { + Logger.LogWarning($"Failed to restore original power scheme: {_originalPowerSchemeGuid}"); + } + } + catch (Exception ex) + { + Logger.LogWarning($"Exception restoring original power scheme: {ex.Message}"); + } + finally + { + _powerSchemeSwitched = false; + } + } + internal static void SetIndefiniteKeepAwake(bool keepDisplayOn = false, int processId = 0, [CallerMemberName] string callerName = "") { PowerToysTelemetry.Log.WriteEvent(new Telemetry.AwakeIndefinitelyKeepAwakeEvent()); @@ -424,15 +519,11 @@ namespace Awake.Core internal static void CompleteExit(int exitCode) { SetPassiveKeepAwake(updateSettings: false); - + RestoreOriginalPowerSchemeIfNeeded(); if (TrayHelper.WindowHandle != IntPtr.Zero) { - // Delete the icon. TrayHelper.SetShellIcon(TrayHelper.WindowHandle, string.Empty, null, TrayIconAction.Delete); - - // Close the message window that we used for the tray. Bridge.SendMessage(TrayHelper.WindowHandle, Native.Constants.WM_CLOSE, 0, 0); - Bridge.DestroyWindow(TrayHelper.WindowHandle); } @@ -611,6 +702,9 @@ namespace Awake.Core IsDisplayOn = keepDisplayOn; SetModeShellIcon(); + // Capture original scheme before any switch + CaptureOriginalPowerScheme(); + TimeSpan sampleInterval = TimeSpan.FromSeconds(_activitySample); Observable.Interval(sampleInterval).Subscribe( @@ -648,6 +742,7 @@ namespace Awake.Core { _activityLastHigh = DateTimeOffset.Now; _stateQueue.Add(ComputeAwakeState(_activityKeepDisplay)); + SwitchToHighestPerformanceIfNeeded(); } TrayHelper.SetShellIcon( @@ -661,6 +756,7 @@ namespace Awake.Core Logger.LogInfo("Activity thresholds not met within timeout window. Ending activity mode."); _activityActive = false; CancelExistingThread(); + RestoreOriginalPowerSchemeIfNeeded(); if (IsUsingPowerToysConfig) { @@ -668,7 +764,6 @@ namespace Awake.Core } else { - Logger.LogInfo("Exiting after activity mode idle."); CompleteExit(Environment.ExitCode); } } diff --git a/src/modules/awake/Awake/Core/PowerSchemeManager.cs b/src/modules/awake/Awake/Core/PowerSchemeManager.cs new file mode 100644 index 0000000000..394e8e8d6c --- /dev/null +++ b/src/modules/awake/Awake/Core/PowerSchemeManager.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace Awake.Core +{ + public class PowerSchemeManager + { + private readonly List _schemes = new(); + + public PowerSchemeManager() + { + RefreshSchemes(); + } + + public void RefreshSchemes() + { + _schemes.Clear(); + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powercfg", + Arguments = "/L", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + process.Start(); + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + var matches = Regex.Matches(output, @"Power Scheme GUID: ([a-fA-F0-9\-]+)\s+\((.+?)\)(\s+\*)?"); + foreach (Match match in matches) + { + _schemes.Add(new PowerScheme + { + PSGuid = match.Groups[1].Value, + Name = match.Groups[2].Value, + IsActive = match.Groups[3].Value.Contains('*'), + }); + } + + // Rank schemes by performance (descending) + _schemes.Sort((a, b) => GetScore(b).CompareTo(GetScore(a))); + } + + /// + /// Returns all power schemes sorted by performance (highest first). + /// + public IReadOnlyList GetAllSchemes() => _schemes; + + /// + /// Returns the highest performance scheme currently available (may already be active). + /// + public PowerScheme? GetHighestPerformanceScheme() + { + if (_schemes.Count == 0) + { + RefreshSchemes(); + } + + return _schemes.Count == 0 ? null : _schemes[0]; + } + + public bool SwitchScheme(string psGuid) + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "powercfg", + Arguments = $"/setactive {psGuid}", + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + process.Start(); + process.WaitForExit(); + RefreshSchemes(); + return true; + } + catch + { + return false; + } + } + + private static int GetScore(PowerScheme scheme) + { + // Heuristic based on name (multi-language basic keywords). + string name = scheme.Name.ToLowerInvariant(); + + // High performance indicators + if (name.Contains("ultimate") || name.Contains("ultra")) + { + return 380; + } + + if (name.Contains("high")) + { + return 310; + } + + if (name.Contains("balanced")) + { + return 200; + } + + if (name.Contains("saver")) + { + return 120; + } + + // Default for unknown custom plans. + return 180; + } + + public class PowerScheme + { + public string PSGuid { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public bool IsActive { get; set; } + } + } +}