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

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


EnergyStarZ is a Windows energy efficiency manager that resides in the system tray. It monitors the switching of foreground windows and pushes background processes to different scheduling gears as needed – more power-saving on laptops and more convenient on desktops.

The project has just been updated to v1.2.0. Compared with the original version that only had two states: “throttling/non-throttling”, the last two versions have completed three key capabilities:Hierarchical throttling strategy(v1.1),Contextual immunity versus the true master switch(v1.1),Memory reclamation and hot path zero allocation for idle processes(v1.2).

This article first introduces what problems it solves, what benefits it can bring, how it is layered as a whole, and then explains what is added item by item.

Project address: | Home page: https://wosledon.github.io/EnergyStarZ/
Current version: v1.2.0 | System requirements: Windows 11 24H2 (build 26100) or higher


1. What is this and what can it bring?

1. Why you need it

The application you’re using should get priority on the CPU, and other programs get out of the way the moment it needs resources – not the other way around, with twenty background updaters, indexers, and chat software messing up frame times.

Windows actually provides complete underlying capabilities (EcoQoS, priority classes, memory priority, CPU Set), but they are all process-by-process and API-by-API. No one wants to manually maintain a policy table of “which should be suppressed, to what extent, and when to release”. This is what EnergyStarZ does.

2. What can it bring?

Let’s first use a set of measured numbers to conclude (for a 16-core machine, see the corresponding chapter below for measurement methods):

you care Measured changes
Notebook battery life CPU usage of 24 fully loaded background processes: 100% → 3.7%
Front desk fluency p99 wake-up delay for frame loop: 29.2 ms → 16.2 ms(Stable 60fps)
Memory Resident memory for idle background processes: 514 MB → 0 MBonly exchange it back when actually accessing
Tool own overhead Memory allocation per foreground switch: 3824 bytes → 88 bytes

A few points that need to be explained:

  • There is no conflict between power saving and smoothness, but the emphasis is different. When powered by battery, the main benefit is battery life; when plugged in or on a desktop, the benefit turns to responsiveness – the background process still consumes 95.6% of the original CPU time, and the work is still done, but it is no longer in the way.
  • Only affects the background. Applications currently in use, recently used applications, applications with visible windows, and applications in the whitelist are not affected at all.
  • You can exit at any time. All throttled processes will be restored when paused, shut down, or even when the process crashes.

3. Core functions

ability specific behavior
foreground awareness Monitor the foreground window switching through the WinEvent hook; the application you switched to is immediately released from the restriction, and the application you left immediately enters the background file.
Hierarchical throttling It is not a simple on/off: EcoQoS + idle priority + background IO mode + low memory priority, upgrade to energy efficiency core binding if necessary
Contextual exemption System processes, user whitelists, processes with visible top-level windows, processes that may be playing, full screen/demo mode, LRU recently used, all left untouched
memory reclamation Called to the throttled process that is actually idle EmptyWorkingSetreturn the resident memory to the foreground application immediately
depth mode Automatically increase throttling intensity when the screen is turned off, there is no input for a long time, or the battery is too low
Instant restore When paused, closed, exited, or even crashed and recovered, all throttled processes will return to their normal state.
Zero interruption Tray menu + global hotkey, Chinese and English bilingual, single instance running, log rotation according to size
Configurable one appsettings.json Keep all policies in mind and the tray menu can be hot-reloaded without rebooting

4. What it doesn’t do

This is very important for background resident tools, so I write it in front:

  • Do not terminate any processes——Only adjust the scheduling attributes;
  • No DLL injection, no patching——All use the public Win32 API;
  • Do not modify the system power plan——Only affects the scheduling attributes of target processes other than this process;
  • Not connected to the Internet– No telemetry, no update checks.

Its positioning is to “give way”, not to be an “optimization master”.


2. Architecture: three layers + four drive sources

The entire program can be broken into four pieces:Trigger source → context detection → decision-making layer → execution layerplus configurations and observations throughout.

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. Four driving sources, each responsible for one thing

Drive source frequency Responsibilities
WinEvent frontend hook event driven When the user switches applications, the global policy is immediately recalculated
Process cache refresh 30 seconds use CreateToolhelp32Snapshot Synchronize process list, measure CPU cycles, perform upgrade/downgrade/memory trimming
Full throttling scan Configurable (default 10 minutes) Bottom line: Even without any foreground switching, the background process is guaranteed to be in the correct gear.
Power status check 5 seconds Detect AC/battery switching, select aggressive or balanced gear

2. What happens when a foreground switch occurs?

  1. EVENT_SYSTEM_FOREGROUND Callback trigger (the hook is installed on the UI thread, with WINEVENT_SKIPOWNPROCESS);
  2. Get the PID and process name corresponding to the window; if the host is ApplicationFrameHost.exeand also enumerate the child windows to find the real UWP process;
  3. Write new front-end applications into LRU (with time decay and adaptive capacity);
  4. Perform 500ms anti-shake on the previous application (SwitchDebounceMs) to avoid jittering back and forth when Alt+Tab;
  5. ApplyThrottlePolicy Traverse the process cache, make exemption determinations process by process, and obtain the target gear;
  6. ApplyTier Only call the Win32 API for processes whose “gear changes” and record the new gear;
  7. In the next round of refresh after 30 seconds, use QueryProcessCycleTime Review: I couldn’t help but move up to Tier 2, and have quietly dropped back to Tier 1. When I have free time, I cut the working set.

3. Several key designs

cache is ConcurrentDictionarybut never touch when traversing .Values。 .Values will take all locks and make a copy Listand the enumerator of the dictionary itself is lock-free and zero-allocated – this is the core of v1.2.0 performance optimization.

Whitelists are immutable snapshots. FrozenSet When the configuration is reloaded, the entire replacement is performed, and the reader neither locks nor allocates; in conjunction with .NET 9 GetAlternateLookup>()even with the process name ToString() All saved.

Throttle parameter pre-allocation. PROCESS_POWER_THROTTLING_STATE The two structures (on/off) are initialized during initialization Marshal.AllocHGlobal Once, then each time it is applied only the pointer is passed – nothing on the hot path Marshal.StructureToPtr。

States are idempotent. Each process records the current gear, whether memory priority has been applied, and the number of cycles last measured. It returns directly if the target gear does not change. This makes the cost of “full rescan every 30 seconds” acceptable.


3. New in v1.1: Hierarchical throttling strategy

The initial version only had a switch: background processes were either throttled or not. Using it exposed three problems:

  • For some processes, EcoQoS alone has almost no effect (the actual measurement only drops to ~92%);
  • One-size-fits-all “protect/unprotect” is not enough: background tools with visible windows should not be moved, broadcasting should not be moved, and applications that have just been switched away should not be moved immediately;
  • When users want to completely turn off energy saving, they can only switch to “pause” while the background loop is still running.

v1.1.0 changed the single switch to a layered strategy.

1. Three-speed model

gear control exerted
protected——Never throttle Hardcoded system processes, user whitelist, foreground/pending apps, LRU recently used, processes with visible top-level windows, processes that may be playing, full screen/presentation mode
Tier 1——Default background EcoQoS performs speed throttling + IDLE_PRIORITY_CLASS | PROCESS_MODE_BACKGROUND_BEGIN(Depress IO and memory scheduling at the same time) + low memory priority
Tier 2——upgrade file All the controls of Tier 1, plus binding energy efficiency cores on hybrid architecture CPUs (SetProcessDefaultCpuSets)
reduction Clear all controls: turn off EcoQoS, restore normal priority, exit background mode, release CPU set binding

PROCESS_MODE_BACKGROUND_BEGIN This flag deserves a separate mention: it not only changes the CPU priority, but also reduces the IO and memory scheduling priorities at the same time, so it is better than simply setting IDLE_PRIORITY_CLASS Much more power savings – the disk is not woken up repeatedly by background processes, and there is less contention for the working set.

The gear change isIdempotentof: the current gear of each process is recorded in ProcessState Here, the target gear will be returned directly without changing, and the Win32 API will not be adjusted repeatedly for each scan.

2. When is Tier 2 triggered?

Not all machines have large and small cores, so the triggering conditions for Tier 2 are combined:

  • Turn off the screen;
  • The user has no keyboard or mouse input for a long time (default 5 minutes,DeepModeIdleMinutes);
  • The power is lower than the threshold (default 20%, battery powered only);
  • feedback measurement:use QueryProcessCycleTime If the actual CPU usage of the throttling process is measured and is still high for many consecutive times (such as background transcoding), it will be promoted to Tier 2; if it continues to fall, it will be returned to Tier 1.

The last one is the most critical part of this strategy: the program no longer assumes that “throttling will definitely take effect”, butMeasure – decide – measure again. The price is that one more handle must be opened and the cycle count must be read in each round. In exchange, the process can be effectively dealt with the process that cannot be controlled by throttling.

All native capability detections have been safely downgraded: non-hybrid architecture, CPU set cannot be enumerated,SetProcessDefaultCpuSets If it fails, it only records a line of logs and then gives up the binding, and the throttling itself continues to work.

3. Throttling continues on AC power

This is the most misunderstood point in v1.1.0.

In the past, management was suspended when the power was turned on. The reason was that “desktop computers do not need to save power.” But actual measurements have shown that the benefits are not just about saving electricity. The following two sets of data were measured on a 16-core machine.

The first group, to what extent the background processes themselves are stressed (24 fully loaded background processes, with QueryProcessCycleTime Measurement):

Configuration Background process CPU
baseline 100%
EcoQoS only ~92%
+ Idle priority and background mode 28%
+ low memory priority 3.7%
After restore 100%

Let’s look at the foreground response speed (16 fully loaded background processes + a 60fps foreground loop):

Wake-up delay Before throttling After throttling
p50 16.7 ms 15.1 ms
p99 29.2 ms 16.2 ms
maximum value 31.3 ms 16.3 ms

16ms of p99 means a stable 60fps; 29ms drops about 1 frame every 8 frames. The background process still consumes the original 95.6% of CPU time – the work is still done, it’s just no longer in the way.

So the current logic is: Battery = Aggressive (low memory priority, low battery upgrade), AC/Desktop = Balanced (no low memory priority). If you want to return to your old behavior, put PauseOnAcPower Set to true That’s it.

4. A true master switch

“Pause” is to give way temporarily, the background loop is still running; the new Enabled: false Is complete hibernation:

  • Do not install WinEvent hook;
  • Background loop idling;
  • No throttling is imposed;
  • Restore any remaining throttling status from the last run at startup。

“Energy Saving Management” in the tray menu can toggle this switch during runtime. When turned off, all throttled processes will be restored immediately and the hook will be unsubscribed.

5. Reliability fixes during the same period

In addition to new features, this version also fixed several issues affecting reliability:

  • Unable to restore in pause mode:ToggleEfficiencyMode It returns directly when Paused, and even “unthrottling” is blocked, causing the switch to pause to mean nothing happens. Changed to only block “apply” and not “restore”.
  • Leaks a handle every time the foreground switches: The UWP application is not closed after getting the child process handle in the foreground event.
  • The hook is installed on a thread without a message loop: The WinEvent hook must be installed on a thread with a message pump, otherwise it will never be triggered. Now unified marshal to UI thread.
  • Power state detection is masked by window switching: The two share a timestamp for anti-shake and interfere with each other. Split into independent power supply anti-shake timestamps.
  • localization file path:Changed from AppContext.BaseDirectory parsed, language preferences persisted to %LOCALAPPDATA%。
  • NETSDK1176 for CI:EnableCompressionInSingleFile Only valid under self-contained distribution, tasks that the framework depends on will fail directly.

6. Performance and stability

  • Process enumeration starts from Process.GetProcesses() Change to single CreateToolhelp32Snapshotno longer creating a managed host every 30 seconds for each process Process object;
  • The log was changed to an independent background writing thread to be written in batches, and 1MB rotation was added, and it was really complied with. EnableLogging;
  • A single instance of Mutex has been added, and hooks that fight with each other will no longer be installed on repeated startups;
  • If the configuration file is read incorrectly, it will fall back to the default value instead of crashing;
  • Added global exception handling for UI thread/AppDomain/TaskScheduler, and tried our best to restore the throttled process.

4. New in v1.2: Free memory recycling and hot path zero allocation

1. Working set recycling of idle background processes

Low memory priority just “allows the memory manager to recycle earlier” and is a passive strategy. v1.2.0 adds a proactive step: yeshas been throttledandMeasured idleprocess call EmptyWorkingSet。

Actual measurement of an idling but resident 514MB process:

index numerical value
Working set before cropping 514 MB
After cropping 0 MB
4 seconds later 4 MB

The page will only be swapped in when the process actually accesses it again, and will not flow back by itself.

Security is a more important part of this design:

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);
    ...
}

Why is it necessary to “sample continuously” instead of “sample once and then cut it”? Because a process that continues to touch memory is repeatedly trimmed, it will cause repeated page faults – increased disk activity will consume more power. Therefore, the idle judgment should be conservative and the sputtering window should be narrow.

In addition there are WorkingSetTrimMinMB(Default 64MB), to avoid disturbing the mini program just to save a few MB.

2. Zero allocation of foreground switching paths

The throttling strategy isEvery time the front window switcheswill be executed every time. Actual measurement: When there are 220 cache processes, a single “strategy calculation” needs to be allocated 3824 bytes. If you switch frequently, this is a constant GC pressure.

Distribute it around and tear it down one by one:

question Amend the law
ProcessCache.Values.ToList() Trigger array doubling and expansion (4→8→16→…→256) direct foreach Enum dictionary ontology
ConcurrentDictionary.Values Will take all node locks and copy one List Same as above, the enumerator itself is lock-free and zero-allocated
The whitelist is copied into a new one each time in the lock. HashSet Change to immutable FrozenSetreplaced entirely when the configuration is reloaded
IsBypassProcess every call ReadOnlySpan.ToString() GetAlternateLookup>() zero allocation lookup

The last one is the new API of .NET 9+, which works with StringComparer.OrdinalIgnoreCase Can be used directly when using span check FrozenSet:

// 硬编码系统进程列表
private static readonly FrozenSet _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 _bypassProcessList =
    Array.Empty().ToFrozenSet(StringComparer.OrdinalIgnoreCase);

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

    return _bypassProcessList.GetAlternateLookup>().Contains(processName);
}

Result: 3824 bytes → 88 bytes (-97.7%), measured with 214–223 cache processes.

In addition, in depth mode, an empty window is no longer allocated to the “visible window collection”. HashSet——There are no windows to enumerate at all when the screen is turned off.

This type of optimization has a prerequisite: regression verification is required, otherwise “zero allocation” can easily become “zero function”. The verification method at that time was to run a set of functional regressions: whitelist merge (configuration + hardcoding), the whitelist process was indeed not throttled, other processes were indeed entered into Tier 1, the restore took effect, and two complete rounds HandleForegroundEvent Follow the search path through the new span.

3. Synchronous updates of documents and publishing methods

  • README has been reorganized in Chinese and English: the badge column, configuration items have been changed from a long list to grouped by purpose (Basic / Protection Range / LRU / Throttle Strength / Deep Mode / Power Source), new release download comparison table and FAQ;
  • The release notes are changed to three-stage assembly:Tag annotation (highlight this time) + Fixed tail template (download form, system requirements) + Automatically generate change log。

Here’s something that’s easy to overlook: GitHub Actions checkout By default, only commit may be materialized. At this time, %(contents) It will silently degenerate into the submission title, and the highlight paragraph in the release notes will disappear directly. The solution is to explicitly fetch the label object and alert when the annotation cannot be obtained.


5. Several design trade-offs

If you were to pick just a few choices throughout the entire project:

  1. “Impotent + Difference” is the universal solution for throttling programs. Recording which gear each object is currently in, and only adjusting the API when it cannot match, can make the cost of “full rescan every 30 seconds” acceptable.
  2. Don’t assume a strategy works, measure it。QueryProcessCycleTime There is a way out for “backend transcoding that cannot be suppressed by throttling”.
  3. ConcurrentDictionary.Values not free. It needs to take all the locks and make another copy Listtraversing the dictionary ontology is zero lock and zero allocation.
  4. FrozenSet + GetAlternateLookup> Is the correct posture for hot path string lookupprovided that the collection itself can be replaced as a whole during configuration reload.
  5. Downgrade silently. Hybrid architecture detection, CPU set enumeration,EmptyWorkingSet The handle permissions, any step failure will only be logged, and it will never bring down the main process.
  6. Pause ≠ Close. Temporary giving way and complete hibernation are two semantics. When mixed together, a bug such as “the process is paused but the process is still throttled” will appear.

6. Installation and use

1. Download

Download from Releases, unzip and run:

document Applicable scenarios
EnergyStarZ-x64-SelfContained-1.2.0.zip recommendbuilt-in .NET runtime
EnergyStarZ-x86-SelfContained-1.2.0.zip 32-bit systems
EnergyStarZ-x64-FrameworkDependent-1.2.0.zip .NET 10 desktop runtime installed, small size
EnergyStarZ-x86-FrameworkDependent-1.2.0.zip 32-bit+ installed runtime

2. Tray menu

menu item effect
Automatic/Manual/Pause Switch operating mode
Auto power mode Automatically adopt aggressive mode when using battery
Energy saving management Turn off or re-enable energy saving management completely
switch language Chinese/English
Edit configuration/reload configuration Open appsettings.jsonno need to restart after changing hot reload
quit Close the program and restore all processes

3. Three modes

model Behavior
automatic Respond to foreground switching and continue to throttle background processes
Manual Stops responding to foreground switching; background processes are still throttled periodically
pause No throttling at all – applications stay hot and recovery is instant

4. Hotkeys

Ctrl+Alt+A cycle mode,Ctrl+Alt+P pause,Ctrl+Alt+R automatic.

5. Configuration

appsettings.json Right next to the exe. Commonly used items:BypassProcessList(a process that never throttles),AudibleProcessList(may be in the process of broadcasting),ScanIntervalMinutes(full scan interval),EnableWorkingSetTrim / WorkingSetTrimMinMB(memory recycling),PauseOnAcPower(Whether to pause when connected to power). See README for complete instructions.


7. Follow-up plan

A few things that come to mind right now:

  • Whether the “idle determination” of working set clipping can be extended from CPU sampling to memory access sampling;
  • The granularity of Tier 2 on the hybrid architecture – now the entire process is tied to the core, can it be distinguished at the thread level?
  • More detailed observability: Turn the allocation amount, cropping amount, and upgrade/downgrade times of each round of strategy into a statistics panel in the tray.

If you are more concerned about any of these directions, or if you find a throttling failure scenario on your machine, you are welcome to provide log feedback in the issue (energy.log inside [THROTTLE] / [ESCALATE] / [TRIM] Three types of rows are sufficient to locate the problem).


The project uses the MIT license and is rewritten based on the original EnergyStar project.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *