EnergyStarZ:让 Windows 后台进程给前台让路的能效管理器(v1.2 发布)

EnergyStarZ:让 Windows 后台进程给前台让路的能效管理器(v1.2 发布)

EnergyStarZ 是一个常驻系统托盘的 Windows 能效管理器。它监听前台窗口切换,把后台进程按需压到不同的调度档位------笔记本上更省电,台式机上更跟手。

项目刚刚更新到 v1.2.0 。比起最初那个只有「节流 / 不节流」两态的版本,最近两个版本补齐了三块关键能力:分层节流策略 (v1.1)、上下文豁免与真正的总开关 (v1.1)、空闲进程的内存回收与热路径零分配(v1.2)。

这篇先介绍它解决什么问题、能带来什么收益、整体怎么分层,再逐项说明新增了什么。

项目地址:https://github.com/wosledon/EnergyStarZ | 主页:https://wosledon.github.io/EnergyStarZ/

当前版本:v1.2.0 | 系统要求:Windows 11 24H2(build 26100)或更高


一、这是什么,能带来什么

1. 为什么需要它

你正在用的应用应该拿到 CPU 的优先权,其他程序在它需要资源的瞬间让开------而不是反过来,让二十个后台更新器、索引器和聊天软件把帧时间搅成一团。

Windows 其实提供了完整的底层能力(EcoQoS、优先级类、内存优先级、CPU Set),但它们都是逐进程、逐 API 的,没人愿意手动维护一个「哪些该压、压到什么程度、什么时候放开」的策略表。EnergyStarZ 做的就是这层策略。

2. 能带来什么

先用一组实测数字说结论(16 核机器,测量方法见后文对应章节):

你关心的 实测变化
笔记本续航 24 个满载后台进程的 CPU 占用:100% → 3.7%
前台流畅度 帧循环的 p99 唤醒延迟:29.2 ms → 16.2 ms(稳定 60fps)
内存 空闲后台进程的常驻内存:514 MB → 0 MB,只在真正访问时换回
工具自身开销 每次前台切换的内存分配:3824 字节 → 88 字节

几个需要说明的点:

  • 省电和流畅不冲突,但侧重不同。 电池供电时收益主要是续航;接电源或台式机上,收益转为响应速度------后台进程仍然消耗原有 95.6% 的 CPU 时间,活照样干完,只是不再挡路。
  • 只影响后台。 正在用的应用、最近用过的应用、有可见窗口的应用、白名单内的应用完全不受影响。
  • 随时可退出。 暂停、关闭、甚至进程崩溃时,所有被节流的进程都会被还原。

3. 核心功能

能力 具体行为
前台感知 通过 WinEvent 钩子监听前台窗口切换;切过去的应用立即解除限制,离开的应用随即进入后台档
分层节流 不是简单的开 / 关:EcoQoS + 空闲优先级 + 后台 IO 模式 + 低内存优先级,必要时升级到能效核绑定
上下文豁免 系统进程、用户白名单、拥有可见顶层窗口的进程、可能正在播音的进程、全屏 / 演示模式、LRU 最近使用,全部不碰
内存回收 对实测空闲的被节流进程调用 EmptyWorkingSet,把常驻内存立即还给前台应用
深度模式 熄屏、长时间无输入、电量过低时自动提升节流强度
即时还原 暂停、关闭、退出、甚至崩溃恢复时,所有被节流的进程都会回到正常状态
零打扰 托盘菜单 + 全局热键,中英双语,单实例运行,日志按大小轮转
可配置 一个 appsettings.json 管住所有策略,托盘菜单可热重载,无需重启

4. 它不做什么

这点对后台常驻工具很重要,所以写在前面:

  • 不终止任何进程------只调整调度属性;
  • 不注入 DLL、不打补丁------全部走公开的 Win32 API;
  • 不修改系统电源计划------只影响本进程之外的目标进程的调度属性;
  • 不联网------没有遥测、没有更新检查。

它的定位是「让路」,不是「优化大师」。


二、架构:三层 + 四种驱动源

整个程序可以拆成四块:触发源 → 上下文探测 → 决策层 → 执行层,外加贯穿其中的配置与观测。
flowchart TB subgraph TRIGGER"触发源" A"WinEvent 前台切换钩子\
EVENT_SYSTEM_FOREGROUND"
B"进程缓存刷新\
每 30 秒"
C"全量节流扫描\
每 N 分钟"
D"电源状态检查\
每 5 秒"
end subgraph CONTEXT"上下文探测 ProcessContext" E"可见顶层窗口\
EnumWindows"
F"全屏 / 演示模式\
SHQueryUserNotificationState"
G"空闲时长\
GetLastInputInfo"
H"屏幕开关\
GUID_MONITOR_POWER_ON"
I"混合架构 CPU Set 枚举" end subgraph CORE"决策层 EnergyManager" J"进程缓存\
ConcurrentDictionary 无锁枚举"
K"豁免判定\
系统进程 / 白名单 / 前台 / LRU / 播音 / 可见窗口"
L"档位决策\
None / Tier 1 / Tier 2"
M"反馈测量\
QueryProcessCycleTime"
end subgraph APPLY"执行层 Win32 API" N"EcoQoS\
SetProcessInformation"
O"空闲优先级 + 后台模式\
SetPriorityClass"
P"低内存优先级\
ProcessMemoryPriority"
Q"能效核绑定\
SetProcessDefaultCpuSets"
R"工作集回收\
EmptyWorkingSet"
end subgraph SIDE"配置与观测" S"托盘菜单 / 热键 / 双语" T"appsettings.json\
FrozenSet 白名单快照"
U"AppLogger\
批量落盘 + 轮转"
end A --> K C --> K B --> J D --> L E --> K F --> K G --> L H --> L I --> Q J --> K K --> L M --> L M --> R L --> N L --> O L --> P L --> Q T --> K S --> L N --> U O --> U P --> U

1. 四种驱动源,各管一件事

驱动源 频率 职责
WinEvent 前台钩子 事件驱动 用户切换应用时,立刻重新计算全局策略
进程缓存刷新 30 秒 用 CreateToolhelp32Snapshot 同步进程列表,测量 CPU 周期,做升级 / 降级 / 内存裁剪
全量节流扫描 可配置(默认 10 分钟) 兜底:即使没有任何前台切换,也保证后台进程处于正确档位
电源状态检查 5 秒 检测 AC / 电池切换,选择激进档或平衡档

2. 一次前台切换发生了什么

  1. EVENT_SYSTEM_FOREGROUND 回调触发(钩子装在 UI 线程上,带 WINEVENT_SKIPOWNPROCESS);
  2. 取窗口对应的 PID 和进程名;如果宿主是 ApplicationFrameHost.exe,还要枚举子窗口找到真正的 UWP 进程;
  3. 把新前台应用写进 LRU(带时间衰减和自适应容量);
  4. 对上一个应用做 500ms 防抖(SwitchDebounceMs),避免 Alt+Tab 时来回抖动;
  5. ApplyThrottlePolicy 遍历进程缓存,逐进程做豁免判定,得到目标档位;
  6. ApplyTier 只对「档位发生变化」的进程调用 Win32 API,并记录新的档位;
  7. 30 秒后的下一轮刷新里,用 QueryProcessCycleTime 复核:压不住的升 Tier 2,已经安静的降回 Tier 1,空闲的顺便裁掉工作集。

3. 几个关键设计

缓存是 ConcurrentDictionary,但遍历时绝不碰 .Values。 .Values 会拿全部锁并复制一个 List,而字典本身的枚举器是无锁、零分配的------这条是 v1.2.0 性能优化的核心。

白名单是不可变快照。 FrozenSet 在配置重载时整体替换,读取方既不加锁也不分配;配合 .NET 9 的 GetAlternateLookup<ReadOnlySpan<char>>(),连进程名的 ToString() 都省了。

节流参数预分配。 PROCESS_POWER_THROTTLING_STATE 的两份结构体(开 / 关)在初始化时 Marshal.AllocHGlobal 一次,之后每次施加只传指针------热路径上没有 Marshal.StructureToPtr。

状态是幂等的。 每个进程记录当前档位、是否已施加内存优先级、上次测量的周期数,目标档位没变就直接返回。这才让「每 30 秒全量重扫」的成本变得可以接受。


三、v1.1 新增:分层节流策略

最初的版本只有一个开关:后台进程要么被节流,要么不被节流。用下来暴露出三个问题:

  • 对某些进程,单靠 EcoQoS 几乎没效果(实测只降到 ~92%);
  • 一刀切的「保护 / 不保护」不够用:有可见窗口的后台工具不该动,正在播音的不该动,刚切走的应用也不该马上动;
  • 用户想彻底关掉节能时,只能切到「暂停」,而后台循环还在跑。

v1.1.0 把单一开关改成了分层策略。

1. 三档模型

档位 施加的控制
受保护------永不节流 硬编码系统进程、用户白名单、前台/待处理应用、LRU 最近使用、拥有可见顶层窗口的进程、可能正在播音的进程、全屏/演示模式
Tier 1------默认后台 EcoQoS 执行速度节流 + `IDLE_PRIORITY_CLASS
Tier 2------升级档 Tier 1 的全部控制,外加在混合架构 CPU 上绑定能效核(SetProcessDefaultCpuSets)
还原 清空全部控制:关闭 EcoQoS、恢复正常优先级、退出后台模式、释放 CPU set 绑定

PROCESS_MODE_BACKGROUND_BEGIN 这个标志值得单独说一句:它不只改 CPU 优先级,还会同时降低 IO 和内存调度优先级,所以比单纯设 IDLE_PRIORITY_CLASS 省电得多------磁盘不会被后台进程反复唤醒,工作集争抢也少了。

档位变化是幂等 的:每个进程的当前档位记在 ProcessState 里,目标档位没变就直接返回,不会每次扫描都重复调一遍 Win32 API。

2. Tier 2 什么时候触发

不是所有机器都有大小核,所以 Tier 2 的触发条件是组合的:

  • 熄屏;
  • 用户长时间无键鼠输入(默认 5 分钟,DeepModeIdleMinutes);
  • 电量低于阈值(默认 20%,仅电池供电);
  • 反馈测量 :用 QueryProcessCycleTime 测已被节流进程的实际 CPU 占用,连续多次仍然偏高(比如后台转码),就晋升到 Tier 2;反过来持续走低就退回 Tier 1。

最后一条是这套策略里最关键的部分:程序不再假设「节流一定生效」,而是测量---决策---再测量。代价是每轮要多开一次句柄、读一次周期计数,换来的是对「节流压不住」的进程真正有办法。

所有原生能力探测都做了安全降级:非混合架构、枚举不到 CPU set、SetProcessDefaultCpuSets 失败,都只记一行日志然后放弃绑定,节流本身继续工作。

3. 交流电上也继续节流

这是 v1.1.0 里最容易被误解的一点。

以前接电源就暂停管理,理由是「台式机不需要省电」。但实测发现收益并不只是省电。下面两组数据都在 16 核机器上测得。

第一组,后台进程本身被压到什么程度(24 个满载后台进程,用 QueryProcessCycleTime 测量):

配置 后台进程 CPU
基线 100%
仅 EcoQoS ~92%
+ 空闲优先级与后台模式 28%
+ 低内存优先级 3.7%
还原后 100%

再看前台响应速度(16 个满载后台进程 + 一个 60fps 的前台循环):

唤醒延迟 节流前 节流后
p50 16.7 ms 15.1 ms
p99 29.2 ms 16.2 ms
最大值 31.3 ms 16.3 ms

16ms 的 p99 意味着稳定 60fps;29ms 大约每 8 帧丢 1 帧。而后台进程仍然消耗原有 95.6% 的 CPU 时间------活照样干完,只是不再挡路。

所以现在的逻辑是:电池 = 激进档(低内存优先级、低电量升级),交流电/台式机 = 平衡档(不做低内存优先级)。想恢复旧行为,把 PauseOnAcPower 设成 true 即可。

4. 一个真正的总开关

「暂停」是临时让路,后台循环仍在跑;新的 Enabled: false 是彻底休眠:

  • 不安装 WinEvent 钩子;
  • 后台循环空转;
  • 不施加任何节流;
  • 启动时还原上次运行可能残留的节流状态。

托盘菜单里的「节能管理」可以在运行时切换这个开关,关掉时立即还原所有被节流进程并退订钩子。

5. 同期的可靠性修复

除了新功能,这个版本还修掉了几处影响可靠性的问题:

  • 暂停模式下无法还原 :ToggleEfficiencyMode 在 Paused 时直接 return,连「解除节流」也被拦掉了,导致切到暂停等于什么都没发生。改成只拦「施加」,不拦「还原」。
  • 每次前台切换泄漏一个句柄:UWP 应用的前台事件里拿到子进程句柄后没有关闭。
  • 钩子装在了没有消息循环的线程上:WinEvent 钩子必须在有消息泵的线程安装,否则永远不触发。现在统一 marshal 到 UI 线程。
  • 电源状态检测被窗口切换掩盖:两者共用一个时间戳做防抖,互相干扰。拆成独立的电源防抖时间戳。
  • 本地化文件路径 :改为从 AppContext.BaseDirectory 解析,语言偏好持久化到 %LOCALAPPDATA%。
  • CI 的 NETSDK1176 :EnableCompressionInSingleFile 只在自包含发布下合法,框架依赖的任务直接失败。

6. 性能与稳定性

  • 进程枚举从 Process.GetProcesses() 换成单次 CreateToolhelp32Snapshot,不再每 30 秒为每个进程创建一个托管 Process 对象;
  • 日志改为独立后台写线程批量落盘,加了 1MB 轮转,并且真的遵守 EnableLogging;
  • 加了单实例 Mutex,重复启动不再安装互相打架的钩子;
  • 配置文件读坏了回退默认值,而不是崩掉;
  • 补了 UI 线程 / AppDomain / TaskScheduler 的全局异常处理,尽力还原被节流的进程。

四、v1.2 新增:空闲内存回收与热路径零分配

1. 空闲后台进程的工作集回收

低内存优先级只是「让内存管理器更早回收」,属于被动策略。v1.2.0 加了主动的一步:对已被节流 且实测空闲 的进程调用 EmptyWorkingSet。

实测一个空转但常驻 514MB 的进程:

指标 数值
裁剪前工作集 514 MB
裁剪后 0 MB
4 秒后 4 MB

页面只有在进程真正再次访问时才会换入,不会自己回流。

安全性是这块设计里更重要的部分:

csharp 复制代码
private static void TrimWorkingSetIfIdle(ProcessState state, double coreFraction)
{
    // 只处理已被节流的进程:有可见窗口、LRU 最近使用、白名单内的进程天然排除
    if (!_workingSetTrimEnabled || state.Tier == ThrottleTier.None)
        return;

    // 必须连续 N 次(默认 2 次,约 1 分钟)测得低于 2% 单核占用
    if (coreFraction < WorkingSetTrimIdleThreshold)
        state.ConsecutiveIdleSamples++;
    else
    {
        state.ConsecutiveIdleSamples = 0;
        state.WorkingSetTrimmed = false;
        return;
    }

    if (state.WorkingSetTrimmed ||
        state.ConsecutiveIdleSamples < WorkingSetTrimIdleSamples)
        return;

    // 独立句柄,打不开就静默跳过,不影响节流本身
    var hQuota = Win32Api.OpenProcess(
        (uint)(Win32Api.ProcessAccessFlags.QueryInformation |
               Win32Api.ProcessAccessFlags.SetQuota), false, (uint)state.Id);
    ...
}

为什么必须「连续采样」而不是「采样一次就裁」?因为一个持续碰内存的进程被反复裁剪,会导致反复缺页换入------磁盘活动增加,反而更耗电。所以判定空闲要保守,溅射窗口要窄。

另外还有 WorkingSetTrimMinMB(默认 64MB),避免为了省几 MB 去打扰小程序。

2. 前台切换路径的零分配化

节流策略在每一次前台窗口切换 时都会执行。实测:220 个缓存进程时,单次「策略计算」要分配 3824 字节。切换一频繁,这就是持续不断的 GC 压力。

四处分配,逐个拆掉:

问题 修法
ProcessCache.Values.ToList() 触发数组倍增扩容(4→8→16→...→256) 直接 foreach 枚举字典本体
ConcurrentDictionary.Values 会拿全部节点锁并复制出一个 List 同上,枚举器本身无锁且零分配
白名单每次在锁内拷贝成新的 HashSet 换成不可变 FrozenSet,配置重载时整体替换
IsBypassProcess 每次调用 ReadOnlySpan<char>.ToString() GetAlternateLookup<ReadOnlySpan<char>>() 零分配查找

最后一条是 .NET 9+ 的新 API,配合 StringComparer.OrdinalIgnoreCase 使用时可以直接用 span 查 FrozenSet:

csharp 复制代码
// 硬编码系统进程列表
private static readonly FrozenSet<string> _hardcodedBypassList = new[]
{
    "csrss.exe", "smss.exe", "wininit.exe", "winlogon.exe",
    "lsass.exe", "services.exe", "svchost.exe", "Registry",
    "dwm.exe", "explorer.exe", "fontdrvhost.exe", /* ... */
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);

// 配置里的白名单:重载时整体替换,读者无需加锁
private static volatile FrozenSet<string> _bypassProcessList =
    Array.Empty<string>().ToFrozenSet(StringComparer.OrdinalIgnoreCase);

private static bool IsBypassProcess(ReadOnlySpan<char> processName)
{
    if (_hardcodedBypassList.GetAlternateLookup<ReadOnlySpan<char>>().Contains(processName))
        return true;

    return _bypassProcessList.GetAlternateLookup<ReadOnlySpan<char>>().Contains(processName);
}

结果:3824 字节 → 88 字节(-97.7%),在 214--223 个缓存进程下测得。

另外深度模式下不再为「可见窗口集合」分配一个空 HashSet------熄屏时根本没有窗口可枚举。

这类优化有个前提:必须有回归验证,否则「零分配」很容易变成「零功能」。当时的验证方式是跑一组功能性回归:白名单合并(配置 + 硬编码)、白名单进程确实没被节流、其他进程确实进了 Tier 1、还原生效,以及两轮完整的 HandleForegroundEvent 走通新的 span 查找路径。

3. 文档与发布方式同步更新

  • README 中英双语重新整理:徽章栏、配置项从长列表改成按用途分组(基础 / 保护范围 / LRU / 节流强度 / 深度模式 / 电源来源)、新增发行版下载对照表和 FAQ;
  • 发布说明改成三段式拼装:标签注解(本次亮点) + 固定尾部模板(下载表、系统要求) + 自动生成变更日志。

这里有个容易忽略的地方:GitHub Actions 的 checkout 默认可能只物化 commit,此时 %(contents) 会静默退化成提交标题,发布说明里的亮点段落直接消失。解决办法是显式 fetch 标签对象,并在拿不到注解时告警。


五、几个设计取舍

如果只挑几条贯穿整个项目的选择:

  1. 「幂等 + 差量」是节流类程序的通用解。记录每个对象当前处于哪一档,只在对不上时才调 API,能让「每 30 秒全量重扫」这件事的成本变得可接受。
  2. 不要假设策略生效,去测量它 。QueryProcessCycleTime 让「节流压不住的后台转码」有了出路。
  3. ConcurrentDictionary.Values 不是免费的 。它要拿全部锁再复制一份 List,遍历字典本体才是零锁零分配。
  4. FrozenSet + GetAlternateLookup<ReadOnlySpan<char>> 是热路径字符串查找的正确姿势,前提是集合本身能在配置重载时整体替换。
  5. 降级要静默 。混合架构探测、CPU set 枚举、EmptyWorkingSet 的句柄权限,任何一步失败都只记日志,绝不拖垮主流程。
  6. 暂停 ≠ 关闭。临时让路和彻底休眠是两种语义,混在一起就会出现「暂停了但进程还被节流」这种 bug。

六、安装与使用

1. 下载

从 Releases 下载,解压运行即可:

文件 适用场景
EnergyStarZ-x64-SelfContained-1.2.0.zip 推荐,内置 .NET 运行时
EnergyStarZ-x86-SelfContained-1.2.0.zip 32 位系统
EnergyStarZ-x64-FrameworkDependent-1.2.0.zip 已装 .NET 10 桌面运行时,体积小
EnergyStarZ-x86-FrameworkDependent-1.2.0.zip 32 位 + 已装运行时

2. 托盘菜单

菜单项 作用
自动 / 手动 / 暂停 切换操作模式
自动电源模式 使用电池时自动采用激进档
节能管理 完全关闭或重新启用节能管理
切换语言 中文 / English
编辑配置 / 重载配置 打开 appsettings.json,改完热重载,无需重启
退出 关闭程序并还原所有进程

3. 三种模式

模式 行为
自动 响应前台切换,并持续节流后台进程
手动 停止响应前台切换;后台进程仍按周期被节流
暂停 完全不节流------应用保持热状态,恢复是瞬间的

4. 热键

Ctrl+Alt+A 循环模式,Ctrl+Alt+P 暂停,Ctrl+Alt+R 自动。

5. 配置

appsettings.json 就在 exe 旁边。常用项:BypassProcessList(永不节流的进程)、AudibleProcessList(可能正在播音的进程)、ScanIntervalMinutes(全量扫描间隔)、EnableWorkingSetTrim / WorkingSetTrimMinMB(内存回收)、PauseOnAcPower(接电源是否暂停)。完整说明见 README。


七、后续计划

目前想到的几件事:

  • 工作集裁剪的「空闲判定」是否可以从 CPU 采样扩展到内存访问采样;
  • 混合架构上 Tier 2 的粒度------现在是整进程绑核,能不能对线程级做区分;
  • 更细的可观测性:把每轮策略的分配量、裁剪量、升级/降级次数做成托盘里的一个统计面板。

如果这些方向里有你更关心的,或者你在自己机器上发现了节流失效的场景,欢迎在 issue 里带日志反馈(energy.log 里 [THROTTLE] / [ESCALATE] / [TRIM] 三类行足够定位问题)。


项目使用 MIT 协议,基于原 EnergyStar 项目重写。

相关推荐
小李科技1 小时前
【性能优化】事件委托:目标定位与 React/Vue 实战
前端·vue.js·react.js·面试·性能优化·前端框架
Sayai2 小时前
Ubuntu 开发环境搭建避坑(一):网卡 EEE 致 60% 周期性丢包,Realtek RTL8125 网络黑洞排查与修复
linux·运维·网络·ubuntu·性能优化
曹牧2 小时前
C#:The operation has timed out
开发语言·c#
He BianGu2 小时前
【WPF-Control教程】IOC 依赖注入
c#·wpf·wpf-control
He BianGu3 小时前
【WPF-Control教程】H.Extensions.Behvaiors 开发文档
c#·wpf·wpf-control
2501_915909063 小时前
Windows 上用 VS Code 调试 iPhone 真机 App,KXBuild 断点、崩溃定位实操
ide·windows·vscode·ios·iphone·个人开发·敏捷流程
贾伟康3 小时前
【共创稿事节】@Link双向绑定的适用边界
性能优化·实战·harmonyos·arkts·arkui
He BianGu3 小时前
【WPF-Control教程】H.Style 开发文档
c#·wpf·wpf-control
情 九3 小时前
MCP 开发环境搭建:Mac 与 Windows 跑通 Python 服务和 stdio 客户端
windows·python·macos·mcp