Rewrite: improve process handling and restoration

This commit is contained in:
2026-08-08 14:08:38 +03:00
parent bf37dd6974
commit 4f8f6218ad
3 changed files with 486 additions and 347 deletions
+57 -54
View File
@@ -1,103 +1,106 @@
# autoPriority
Monitors process memory usage on Windows and automatically adjusts CPU priority: promotes memory-heavy processes to HIGH and demotes everything else to NORMAL. Optionally enters **Game Mode** when a process exceeds a higher threshold, boosting it to HIGH and setting all other processes to IDLE.
Отслеживает использование памяти процессами в Windows и автоматически управляет их приоритетом: ресурсоемким процессам назначает HIGH, а менее ресурсоемким снижает приоритет до NORMAL, только если он был выше. При превышении повышенного порога может включать **игровой режим**, назначая обнаруженному процессу HIGH, а всем остальным — IDLE.
**Windows only.**
**Только для Windows.**
## How it works
## Принцип работы
### Normal mode
### Обычный режим
Every scan interval the program iterates over all running processes:
Через заданный интервал программа проверяет все запущенные процессы:
| RSS vs `-mem` | Current priority | Action |
| RSS относительно `-mem` | Текущий приоритет | Действие |
|---|---|---|
| ≥ threshold | anything except HIGH | → HIGH (logged as `PROMOTE`) |
| ≥ threshold | already HIGH | skip |
| < threshold | ABOVE_NORMAL, HIGH, or REALTIME | → NORMAL (logged as `DEMOTE`) |
| < threshold | NORMAL, BELOW_NORMAL, or IDLE | skip |
| ≥ порога | любой, кроме HIGH | → HIGH (запись `PROMOTE` в журнале) |
| ≥ порога | уже HIGH | пропустить |
| < порога | ABOVE_NORMAL, HIGH или REALTIME | → NORMAL (запись `DEMOTE` в журнале) |
| < порога | NORMAL, BELOW_NORMAL или IDLE | пропустить |
### Game mode (`-game-mem`)
### Игровой режим (`-game-mem`)
When any process reaches or exceeds the `-game-mem` threshold:
Когда любой процесс достигает или превышает порог `-game-mem`:
1. That process → **HIGH** (logged as `GAME`)
2. All other processes → **IDLE** (original priorities saved)
3. Log: `GAME MODE ON`
1. Этому процессу назначается **HIGH** (запись `GAME` в журнале).
2. Всем остальным процессам назначается **IDLE** с сохранением исходных приоритетов.
3. В журнал добавляется запись `GAME MODE ON`.
When all such processes close:
Когда все такие процессы завершаются:
1. All processes that were demoted during game mode are **restored to their original priority** (logged as `RESTORE`)
2. `GAME MODE OFF` logged
3. Normal mode resumes — priorities recalculated by `-mem` rules
1. Всем процессам, приоритет которых был снижен в игровом режиме, **возвращается исходный приоритет** (запись `RESTORE` в журнале).
2. В журнал добавляется запись `GAME MODE OFF`.
3. Возобновляется обычный режим, и приоритеты пересчитываются по правилам `-mem`.
If `SetPriorityClass` or `OpenProcess` fails for a process (e.g., anti-cheat protection, system processes), the process is added to an in-memory exclusion list and never touched again (logged as `BLOCK`). Blocked processes are still measured for RSS each scan — if a blocked process exceeds `-game-mem`, it triggers game mode without attempting to change its priority. When the process exits, it is automatically removed from the list.
Если изменить приоритет процесса невозможно, например из-за античита или системных ограничений, процесс добавляется во внутренний список исключений и больше не изменяется (запись `BLOCK` в журнале). Его RSS продолжает измеряться, поэтому он может включить игровой режим. Процессы, память которых прочитать не удалось, пропускаются до следующей проверки.
On shutdown, all processes demoted during game mode are restored to their saved original priority.
При штатном завершении программа несколько раз пытается восстановить приоритеты всех процессов, измененных в игровом режиме. Если Windows отклоняет восстановление, в журнал записывается предупреждение.
## Build
В одном сеансе Windows может работать только один экземпляр программы. Второй экземпляр завершается, не прерывая работу первого.
Requires Go 1.26+.
## Сборка
Требуется Go 1.26 или новее.
```
# Standard build (with console window)
# Обычная сборка с окном консоли
go build -o autopriority.exe .
# Background build (no console, minimal size)
go build -ldflags="-H windowsgui -s -w" -o autopriority.exe .
# Фоновая сборка без консоли, минимальный размер
go build -trimpath -buildvcs=false -gcflags="all=-l" -ldflags="-H=windowsgui -s -w -buildid=" -o autopriority.exe .
```
## Usage
## Использование
```
autopriority [flags]
```
| Flag | Default | Description |
| Флаг | Значение по умолчанию | Описание |
|-------------|---------------|----------------------------------|
| `-mem` | 512M | Memory threshold (e.g. 512M, 1G, 2048M) |
| `-game-mem` | 2G | Game threshold (e.g. 2G, 4G). Must be > `-mem`. 0 = disabled |
| `-interval` | 1 minute | Scan interval (min 10s) |
| `-dry-run` | false | Log only, don't change priority |
| `-mem` | 512M | Порог памяти, например 512M, 1G или 2048M |
| `-game-mem` | 2G | Порог игрового режима, например 2G или 4G. Должен быть больше `-mem`. 0 отключает режим |
| `-interval` | 1 минута | Интервал проверки, минимум 10 секунд |
| `-dry-run` | false | Только записывать решения в журнал, не менять приоритеты |
Examples:
Примеры:
```
# Run with 1 GB threshold, scanning every 30 seconds
# Порог 1 ГБ, проверка каждые 30 секунд
autopriority -mem=1G -interval=30s
# Run with defaults (512M threshold, 2G game threshold)
# Запуск со значениями по умолчанию: порог 512M, игровой порог 2G
autopriority
# Game mode: normal threshold 512M, game threshold 4G
# Обычный порог 512M, игровой порог 4G
autopriority -mem=512M -game-mem=4G
# Dry run — log decisions without changing anything
# Пробный запуск: записывать решения, ничего не изменяя
autopriority -dry-run
```
## Log
## Журнал
Log is always written to `%TEMP%\autopriority.log`. A new log file is created on each run (previous log is deleted).
Журнал всегда записывается в `%TEMP%\autopriority.log`. При каждом запуске создается новый файл, а предыдущий удаляется.
Log entries:
Типы записей:
| Prefix | Meaning |
| Префикс | Значение |
|---|---|
| `PROMOTE` | Priority raised to HIGH (normal mode) |
| `DEMOTE` | Priority lowered to NORMAL (normal mode) |
| `GAME DETECT` | Process first detected as exceeding game-mem threshold |
| `GAME` | Process set to HIGH or IDLE (game mode) |
| `GAME MODE ON` | Game mode activated |
| `GAME MODE OFF` | Game mode deactivated — processes restored |
| `BLOCK` | OpenProcess or SetPriorityClass failed; process added to exclusion list |
| `RESTORE` | Process restored to original/saved priority (game mode exit or shutdown) |
| `[DRY-RUN]` | Would change priority (dry-run mode) |
| `PROMOTE` | Приоритет повышен до HIGH в обычном режиме |
| `DEMOTE` | Приоритет снижен до NORMAL в обычном режиме |
| `GAME DETECT` | Впервые обнаружен процесс, превысивший порог `-game-mem` |
| `GAME` | Процессу назначен HIGH или IDLE в игровом режиме |
| `GAME MODE ON` | Игровой режим включен |
| `GAME MODE OFF` | Игровой режим выключен, приоритеты восстановлены |
| `BLOCK` | Ошибка OpenProcess или SetPriorityClass, процесс добавлен в список исключений |
| `SKIP` | Не удалось прочитать сведения о процессе, попытка повторится при следующей проверке |
| `RESTORE` | Восстановлен исходный приоритет при выходе из игрового режима или завершении программы |
| `[DRY-RUN]` | Приоритет был бы изменен в пробном режиме |
## Auto-start
## Автозапуск
Open **Win+R**, type `shell:startup`, and press Enter. Then place a shortcut to `autopriority.exe` in the folder that opens.
Нажмите **Win+R**, введите `shell:startup` и нажмите Enter. Поместите ярлык `autopriority.exe` в открывшуюся папку.
## Dependencies
## Зависимости
None. Uses only Windows API (kernel32, psapi) via raw syscall.
Отсутствуют. Программа напрямую вызывает Windows API (`kernel32`, `psapi`) через syscall.
+337 -264
View File
@@ -5,9 +5,11 @@ package main
import (
"flag"
"fmt"
"math"
"os"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
@@ -17,7 +19,6 @@ import (
const (
CreateToolhelp32SnapshotProcess = 0x00000002
ProcessTerminate = 0x00000001
ProcessSetInformation = 0x00000200
ProcessQueryLimitedInformation = 0x00001000
PriorityClassIdle = 0x00000040
@@ -27,6 +28,7 @@ const (
PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000
ErrorNoMoreFiles = 18
ErrorAlreadyExists = 183
)
type processEntry32 struct {
@@ -56,15 +58,31 @@ type processMemoryCounters struct {
PrivateUsage uintptr
}
type processKey struct {
PID uint32
Created uint64
}
type procInfo struct {
PID uint32
Name string
RSS uint64
Key processKey
Prio uint32
Handle syscall.Handle
CanSet bool
Keep bool
}
type trackedProc struct {
name string
handle syscall.Handle
}
type savedPrio struct {
name string
prio uint32
handle syscall.Handle
}
var (
@@ -79,7 +97,9 @@ var (
procGetPriority = k32.NewProc("GetPriorityClass")
procOpenProcess = k32.NewProc("OpenProcess")
procCloseHandle = k32.NewProc("CloseHandle")
procTerminate = k32.NewProc("TerminateProcess")
procGetProcessTimes = k32.NewProc("GetProcessTimes")
procQueryImageName = k32.NewProc("QueryFullProcessImageNameW")
procCreateMutex = k32.NewProc("CreateMutexW")
)
func closeH(h syscall.Handle) {
@@ -94,12 +114,7 @@ func openProc(pid uint32, acc uint32) (syscall.Handle, error) {
return syscall.Handle(r), nil
}
func setPrio(pid uint32, cls uint32) error {
h, err := openProc(pid, ProcessSetInformation)
if err != nil {
return err
}
defer closeH(h)
func setPrio(h syscall.Handle, pid uint32, cls uint32) error {
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
if r == 0 {
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
@@ -107,13 +122,44 @@ func setPrio(pid uint32, cls uint32) error {
return nil
}
func utf16Trim(p []uint16) string {
for i, v := range p {
if v == 0 {
return syscall.UTF16ToString(p[:i])
func processCreated(h syscall.Handle) (uint64, error) {
var created, exited, kernel, user syscall.Filetime
r, _, e := procGetProcessTimes.Call(
uintptr(h),
uintptr(unsafe.Pointer(&created)),
uintptr(unsafe.Pointer(&exited)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r == 0 {
return 0, fmt.Errorf("GetProcessTimes failed: %w", e)
}
return uint64(created.HighDateTime)<<32 | uint64(created.LowDateTime), nil
}
func processName(h syscall.Handle) (string, error) {
buf := make([]uint16, 32768)
size := uint32(len(buf))
r, _, e := procQueryImageName.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size)))
if r == 0 {
return "", fmt.Errorf("QueryFullProcessImageName failed: %w", e)
}
return syscall.UTF16ToString(p)
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
}
func processAlive(h syscall.Handle) (bool, error) {
var created, exited, kernel, user syscall.Filetime
r, _, e := procGetProcessTimes.Call(
uintptr(h),
uintptr(unsafe.Pointer(&created)),
uintptr(unsafe.Pointer(&exited)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if r == 0 {
return false, fmt.Errorf("GetProcessTimes failed: %w", e)
}
return exited.HighDateTime == 0 && exited.LowDateTime == 0, nil
}
func parseMemSize(s string) (uint64, error) {
@@ -121,32 +167,26 @@ func parseMemSize(s string) (uint64, error) {
if s == "" {
return 0, fmt.Errorf("empty value")
}
var suffix byte
multiplier := uint64(1)
switch s[len(s)-1] {
case 'k', 'K':
suffix = 'K'
multiplier = 1024
s = s[:len(s)-1]
case 'm', 'M':
suffix = 'M'
multiplier = 1024 * 1024
s = s[:len(s)-1]
case 'g', 'G':
suffix = 'G'
multiplier = 1024 * 1024 * 1024
s = s[:len(s)-1]
}
n, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0, err
}
switch suffix {
case 'K':
return n * 1024, nil
case 'M':
return n * 1024 * 1024, nil
case 'G':
return n * 1024 * 1024 * 1024, nil
default:
return n, nil
if n > math.MaxUint64/multiplier {
return 0, fmt.Errorf("value overflows uint64")
}
return n * multiplier, nil
}
func formatMemSize(b uint64) string {
@@ -165,6 +205,22 @@ func isAboveNormal(c uint32) bool {
c == PriorityClassRealtime
}
func desiredPriority(rss, mem uint64, current uint32, gameMode, isGame bool) uint32 {
if gameMode {
if isGame {
return PriorityClassHigh
}
return PriorityClassIdle
}
if rss >= mem {
return PriorityClassHigh
}
if isAboveNormal(current) {
return PriorityClassNormal
}
return 0
}
func prioName(c uint32) string {
switch c {
case PriorityClassIdle:
@@ -186,7 +242,7 @@ func prioName(c uint32) string {
func allProcs() ([]procInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 0 {
if snap == ^uintptr(0) {
return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e)
}
defer closeH(syscall.Handle(snap))
@@ -199,7 +255,7 @@ func allProcs() ([]procInfo, error) {
var out []procInfo
for {
name := utf16Trim(pe.ExeFile[:])
name := syscall.UTF16ToString(pe.ExeFile[:])
if name != "" {
out = append(out, procInfo{PID: pe.PID, Name: name})
}
@@ -215,22 +271,16 @@ func allProcs() ([]procInfo, error) {
return out, nil
}
func killOtherInstances() {
myPID := os.Getpid()
procs, err := allProcs()
func singleInstance() (syscall.Handle, bool, error) {
name, err := syscall.UTF16PtrFromString(`Local\autoPriority`)
if err != nil {
return
}
myName := filepath.Base(os.Args[0])
for _, p := range procs {
if p.PID != uint32(myPID) && filepath.Base(p.Name) == myName {
h, err := openProc(p.PID, ProcessTerminate)
if err == nil {
procTerminate.Call(uintptr(h), 0)
closeH(h)
}
return 0, false, err
}
r, _, e := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
if r == 0 {
return 0, false, fmt.Errorf("CreateMutex failed: %w", e)
}
return syscall.Handle(r), e == syscall.Errno(ErrorAlreadyExists), nil
}
func main() {
@@ -245,7 +295,10 @@ func main() {
}
mem, err := parseMemSize(*memStr)
if err != nil {
if err != nil || mem == 0 {
if err == nil {
err = fmt.Errorf("must be greater than zero")
}
fmt.Fprintf(os.Stderr, "autoPriority: invalid -mem value: %v\n", err)
os.Exit(1)
}
@@ -260,72 +313,195 @@ func main() {
os.Exit(1)
}
killOtherInstances()
time.Sleep(100 * time.Millisecond)
var logFile *os.File
logPath := filepath.Join(os.TempDir(), "autopriority.log")
os.Remove(logPath)
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
if err == nil {
logFile = f
defer logFile.Close()
mutex, exists, err := singleInstance()
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: %v\n", err)
os.Exit(1)
}
ts := func() string {
return time.Now().Format("02.01.2006 15:04:05")
}
logf := func(format string, a ...any) {
if logFile == nil {
if exists {
closeH(mutex)
fmt.Fprintln(os.Stderr, "autoPriority: already running")
return
}
fmt.Fprintf(logFile, "[%s] ", ts())
defer closeH(mutex)
logPath := filepath.Join(os.TempDir(), "autopriority.log")
logFile, err := os.Create(logPath)
if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: cannot create log: %v\n", err)
os.Exit(1)
}
defer logFile.Close()
logf := func(format string, a ...any) {
fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
fmt.Fprintf(logFile, format+"\n", a...)
logFile.Sync()
}
if gameMem > 0 {
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
} else {
logf("autoPriority started (mem=%s, interval=%s, dry-run=%v)",
formatMemSize(mem), *interval, *dryRun)
}
if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil {
if !*dryRun {
pid := uint32(os.Getpid())
h, err := openProc(pid, ProcessSetInformation)
if err == nil {
err = setPrio(h, pid, PriorityClassIdle)
closeH(h)
}
if err != nil {
logf("warning: could not set own priority to IDLE: %v", err)
} else {
logf("own priority set to IDLE")
}
blocked := make(map[uint32]string)
gameProcs := make(map[uint32]string)
gameSaved := make(map[uint32]savedPrio)
gameMode := false
defer func() {
if gameMode {
restoreProcs, _ := allProcs()
rm := make(map[uint32]string, len(restoreProcs))
for _, p := range restoreProcs {
rm[p.PID] = p.Name
}
for pid, sv := range gameSaved {
if cur, ok := rm[pid]; !ok || cur != sv.name {
blocked := make(map[processKey]string)
unreadable := make(map[uint32]string)
gameProcs := make(map[processKey]trackedProc)
gameSaved := make(map[processKey]savedPrio)
gameMode := false
myPID := uint32(os.Getpid())
readProcs := func() ([]procInfo, error) {
procs, err := allProcs()
if err != nil {
return nil, err
}
live := make(map[uint32]string, len(procs))
list := make([]procInfo, 0, len(procs))
for i := range procs {
p := &procs[i]
live[p.PID] = p.Name
if p.PID == myPID || p.PID == 0 {
continue
}
if err := setPrio(pid, sv.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", sv.name, pid, prioName(sv.prio), err)
} else {
logf("RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
h, setErr := openProc(p.PID, ProcessQueryLimitedInformation|ProcessSetInformation)
p.CanSet = setErr == nil
openErr := error(nil)
if !p.CanSet {
h, openErr = openProc(p.PID, ProcessQueryLimitedInformation)
}
if openErr != nil {
if unreadable[p.PID] != p.Name {
unreadable[p.PID] = p.Name
logf("SKIP %s (PID %d): %v", p.Name, p.PID, openErr)
}
continue
}
name, readErr := processName(h)
if readErr == nil {
p.Name = name
}
created := uint64(0)
if readErr == nil {
created, readErr = processCreated(h)
}
var m processMemoryCounters
if readErr == nil {
m.CBM = uint32(unsafe.Sizeof(m))
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
if r == 0 {
readErr = fmt.Errorf("GetProcessMemoryInfo failed: %w", e)
}
}
var cur uintptr
if readErr == nil {
r, _, e := procGetPriority.Call(uintptr(h))
cur = r
if cur == 0 {
readErr = fmt.Errorf("GetPriorityClass failed: %w", e)
}
}
if readErr != nil {
closeH(h)
if unreadable[p.PID] != p.Name {
unreadable[p.PID] = p.Name
logf("SKIP %s (PID %d): %v", p.Name, p.PID, readErr)
}
continue
}
delete(unreadable, p.PID)
p.Key = processKey{PID: p.PID, Created: created}
p.RSS = uint64(m.WorkingSetSize)
p.Prio = uint32(cur)
p.Handle = h
if !p.CanSet {
if _, ok := blocked[p.Key]; !ok {
blocked[p.Key] = p.Name
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, setErr)
}
}
list = append(list, *p)
}
for pid, name := range unreadable {
if live[pid] != name {
delete(unreadable, pid)
}
}
return list, nil
}
restore := func(list []procInfo) {
current := make(map[processKey]*procInfo, len(list))
for i := range list {
current[list[i].Key] = &list[i]
}
for key, saved := range gameSaved {
alive, err := processAlive(saved.handle)
if err != nil {
logf("RESTORE %s (PID %d) status error: %v", saved.name, key.PID, err)
continue
}
if !alive {
closeH(saved.handle)
delete(gameSaved, key)
continue
}
cur, _, e := procGetPriority.Call(uintptr(saved.handle))
if cur == 0 {
logf("RESTORE %s (PID %d) priority error: %v", saved.name, key.PID, e)
continue
}
if uint32(cur) != saved.prio {
if err := setPrio(saved.handle, key.PID, saved.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", saved.name, key.PID, prioName(saved.prio), err)
continue
}
logf("RESTORE %s (PID %d) -> %s", saved.name, key.PID, prioName(saved.prio))
}
if p := current[key]; p != nil {
p.Prio = saved.prio
}
closeH(saved.handle)
delete(gameSaved, key)
}
}
defer func() {
for attempt := 0; attempt < 3 && len(gameSaved) > 0; attempt++ {
restore(nil)
if len(gameSaved) > 0 {
time.Sleep(50 * time.Millisecond)
}
}
if len(gameSaved) > 0 {
logf("warning: %d process priorities could not be restored", len(gameSaved))
for _, saved := range gameSaved {
closeH(saved.handle)
}
}
for _, game := range gameProcs {
closeH(game.handle)
}
logf("autoPriority stopped")
logFile.Sync()
}()
myPID := uint32(os.Getpid())
ticker := time.NewTicker(*interval)
defer ticker.Stop()
@@ -333,220 +509,117 @@ func main() {
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
scan := func() {
procs, err := allProcs()
list, err := readProcs()
if err != nil {
logf("allProcs error: %v", err)
logf("process scan error: %v", err)
return
}
defer func() {
for _, p := range list {
if !p.Keep {
closeH(p.Handle)
}
}
}()
pidMap := make(map[uint32]string, len(procs))
for _, p := range procs {
pidMap[p.PID] = p.Name
current := make(map[processKey]string, len(list))
for _, p := range list {
current[p.Key] = p.Name
}
for pid := range blocked {
if _, ok := pidMap[pid]; !ok {
delete(blocked, pid)
for key, name := range blocked {
if current[key] != name {
delete(blocked, key)
}
}
for pid, name := range gameProcs {
if cur, ok := pidMap[pid]; !ok || cur != name {
delete(gameProcs, pid)
}
}
for pid, sv := range gameSaved {
if cur, ok := pidMap[pid]; !ok || cur != sv.name {
delete(gameSaved, pid)
}
}
var list []procInfo
for i := range procs {
p := &procs[i]
if p.PID == myPID || p.PID == 0 {
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
for key, game := range gameProcs {
alive, err := processAlive(game.handle)
if err != nil {
if _, ok := blocked[p.PID]; !ok {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
}
logf("GAME %s (PID %d) status error: %v", game.name, key.PID, err)
continue
}
var m processMemoryCounters
m.CBM = uint32(unsafe.Sizeof(m))
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
closeH(h)
if r == 0 {
if _, ok := blocked[p.PID]; !ok {
logf("GetProcessMemoryInfo(%d) error: %v", p.PID, e)
if !alive {
closeH(game.handle)
delete(gameProcs, key)
}
}
for key, saved := range gameSaved {
alive, err := processAlive(saved.handle)
if err == nil && !alive {
closeH(saved.handle)
delete(gameSaved, key)
}
continue
}
p.RSS = uint64(m.WorkingSetSize)
list = append(list, *p)
for i := range list {
p := &list[i]
if gameMem > 0 && p.RSS >= gameMem {
if _, ok := gameProcs[p.PID]; !ok {
gameProcs[p.PID] = p.Name
if _, ok := gameProcs[p.Key]; !ok {
gameProcs[p.Key] = trackedProc{name: p.Name, handle: p.Handle}
p.Keep = true
logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS))
}
}
}
hasGame := gameMem > 0 && len(gameProcs) > 0
enteringGame := hasGame && !gameMode
if enteringGame {
gameMode = true
}
if !hasGame && gameMode {
restore(list)
if len(gameSaved) == 0 {
gameMode = false
logf("GAME MODE OFF")
} else {
return
}
}
if hasGame {
sort.SliceStable(list, func(i, j int) bool {
_, iGame := gameProcs[list[i].Key]
_, jGame := gameProcs[list[j].Key]
return iGame && !jGame
})
}
for i := range list {
p := &list[i]
_, isBlocked := blocked[p.PID]
_, isGame := gameProcs[p.PID]
_, isBlocked := blocked[p.Key]
_, isGame := gameProcs[p.Key]
target := desiredPriority(p.RSS, mem, p.Prio, hasGame, isGame)
action := "GAME"
if !hasGame && target == PriorityClassHigh {
action = "PROMOTE"
} else if !hasGame {
action = "DEMOTE"
}
if hasGame && isGame {
if isBlocked {
if target == 0 || target == p.Prio || isBlocked {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (game)", p.Name, p.PID, formatMemSize(p.RSS))
logf("[DRY-RUN] %s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
if err := setPrio(p.Handle, p.PID, target); err != nil {
blocked[p.Key] = p.Name
logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, err)
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur != 0 && uint32(cur) == PriorityClassHigh {
continue
}
if err := setPrio(p.PID, PriorityClassHigh); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
logf("GAME %s (PID %d) RSS=%s — tracked as game (priority change failed)", p.Name, p.PID, formatMemSize(p.RSS))
} else {
logf("GAME %s (PID %d) RSS=%s -> HIGH", p.Name, p.PID, formatMemSize(p.RSS))
time.Sleep(100 * time.Millisecond)
}
continue
}
if hasGame {
if isBlocked {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set IDLE (game)", p.Name, p.PID, formatMemSize(p.RSS))
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur == 0 || uint32(cur) == PriorityClassIdle {
continue
}
if err := setPrio(p.PID, PriorityClassIdle); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} else {
if _, saved := gameSaved[p.PID]; !saved {
gameSaved[p.PID] = savedPrio{name: p.Name, prio: uint32(cur)}
}
logf("GAME %s (PID %d) RSS=%s, %s -> IDLE", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
time.Sleep(100 * time.Millisecond)
}
continue
}
if p.RSS < mem {
if isBlocked {
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur == 0 || !isAboveNormal(uint32(cur)) {
continue
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set NORMAL", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
continue
}
if err := setPrio(p.PID, PriorityClassNormal); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} else {
logf("DEMOTE %s (PID %d) RSS=%s, %s -> NORMAL", p.Name, p.PID, formatMemSize(p.RSS), prioName(uint32(cur)))
time.Sleep(100 * time.Millisecond)
}
continue
}
if isBlocked {
continue
}
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
if err != nil {
continue
}
cur, _, _ := procGetPriority.Call(uintptr(h))
closeH(h)
if cur != 0 && uint32(cur) == PriorityClassHigh {
continue
}
origStr := ""
if cur != 0 {
origStr = prioName(uint32(cur))
}
if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr)
continue
}
if err := setPrio(p.PID, PriorityClassHigh); err != nil {
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
} else {
logf("PROMOTE %s (PID %d) RSS=%s, %s -> HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr)
time.Sleep(100 * time.Millisecond)
if hasGame && !isGame && target == PriorityClassIdle {
if _, saved := gameSaved[p.Key]; !saved {
gameSaved[p.Key] = savedPrio{name: p.Name, prio: p.Prio, handle: p.Handle}
p.Keep = true
}
}
if gameMem > 0 {
newHasGame := len(gameProcs) > 0
if newHasGame && !gameMode {
gameMode = true
logf("%s %s (PID %d) RSS=%s, %s -> %s", action, p.Name, p.PID, formatMemSize(p.RSS), prioName(p.Prio), prioName(target))
p.Prio = target
}
if enteringGame {
logf("GAME MODE ON")
}
if !newHasGame && gameMode {
gameMode = false
logf("GAME MODE OFF")
for pid, sv := range gameSaved {
if cur, ok := pidMap[pid]; !ok || cur != sv.name {
delete(gameSaved, pid)
continue
}
if *dryRun {
logf("[DRY-RUN] RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
} else {
if err := setPrio(pid, sv.prio); err != nil {
logf("RESTORE %s (PID %d) -> %s error: %v", sv.name, pid, prioName(sv.prio), err)
} else {
logf("RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio))
time.Sleep(100 * time.Millisecond)
}
}
delete(gameSaved, pid)
}
}
}
}
scan()
+63
View File
@@ -0,0 +1,63 @@
//go:build windows
package main
import (
"math"
"testing"
)
func TestParseMemSize(t *testing.T) {
tests := []struct {
input string
want uint64
bad bool
}{
{"512M", 512 * 1024 * 1024, false},
{" 1g ", 1024 * 1024 * 1024, false},
{"0", 0, false},
{"18446744073709551615", math.MaxUint64, false},
{"17179869183G", 17179869183 * 1024 * 1024 * 1024, false},
{"17179869184G", 0, true},
{"", 0, true},
{"G", 0, true},
{"1GB", 0, true},
{"1.5G", 0, true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := parseMemSize(tt.input)
if (err != nil) != tt.bad || got != tt.want {
t.Fatalf("parseMemSize(%q) = %d, %v; want %d, bad=%v", tt.input, got, err, tt.want, tt.bad)
}
})
}
}
func TestDesiredPriority(t *testing.T) {
const mem = 512 * 1024 * 1024
tests := []struct {
name string
rss uint64
current uint32
gameMode, isGame bool
want uint32
}{
{"promote heavy", mem, PriorityClassNormal, false, false, PriorityClassHigh},
{"keep heavy high", mem, PriorityClassHigh, false, false, PriorityClassHigh},
{"demote light high", mem - 1, PriorityClassHigh, false, false, PriorityClassNormal},
{"demote light realtime", mem - 1, PriorityClassRealtime, false, false, PriorityClassNormal},
{"keep light normal", mem - 1, PriorityClassNormal, false, false, 0},
{"boost game", 1, PriorityClassIdle, true, true, PriorityClassHigh},
{"idle non-game", mem * 10, PriorityClassHigh, true, false, PriorityClassIdle},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := desiredPriority(tt.rss, mem, tt.current, tt.gameMode, tt.isGame); got != tt.want {
t.Fatalf("desiredPriority() = %#x, want %#x", got, tt.want)
}
})
}
}