1 Commits
Author SHA1 Message Date
lzrdblzzrd 4f8f6218ad Rewrite: improve process handling and restoration 2026-08-08 14:08:38 +03:00
3 changed files with 486 additions and 347 deletions
+57 -54
View File
@@ -1,103 +1,106 @@
# autoPriority # 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`) | | ≥ порога | любой, кроме HIGH | → HIGH (запись `PROMOTE` в журнале) |
| ≥ threshold | already HIGH | skip | | ≥ порога | уже HIGH | пропустить |
| < threshold | ABOVE_NORMAL, HIGH, or REALTIME | → NORMAL (logged as `DEMOTE`) | | < порога | ABOVE_NORMAL, HIGH или REALTIME | → NORMAL (запись `DEMOTE` в журнале) |
| < threshold | NORMAL, BELOW_NORMAL, or IDLE | skip | | < порога | 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`) 1. Этому процессу назначается **HIGH** (запись `GAME` в журнале).
2. All other processes → **IDLE** (original priorities saved) 2. Всем остальным процессам назначается **IDLE** с сохранением исходных приоритетов.
3. Log: `GAME MODE ON` 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`) 1. Всем процессам, приоритет которых был снижен в игровом режиме, **возвращается исходный приоритет** (запись `RESTORE` в журнале).
2. `GAME MODE OFF` logged 2. В журнал добавляется запись `GAME MODE OFF`.
3. Normal mode resumes — priorities recalculated by `-mem` rules 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 . 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] autopriority [flags]
``` ```
| Flag | Default | Description | | Флаг | Значение по умолчанию | Описание |
|-------------|---------------|----------------------------------| |-------------|---------------|----------------------------------|
| `-mem` | 512M | Memory threshold (e.g. 512M, 1G, 2048M) | | `-mem` | 512M | Порог памяти, например 512M, 1G или 2048M |
| `-game-mem` | 2G | Game threshold (e.g. 2G, 4G). Must be > `-mem`. 0 = disabled | | `-game-mem` | 2G | Порог игрового режима, например 2G или 4G. Должен быть больше `-mem`. 0 отключает режим |
| `-interval` | 1 minute | Scan interval (min 10s) | | `-interval` | 1 минута | Интервал проверки, минимум 10 секунд |
| `-dry-run` | false | Log only, don't change priority | | `-dry-run` | false | Только записывать решения в журнал, не менять приоритеты |
Examples: Примеры:
``` ```
# Run with 1 GB threshold, scanning every 30 seconds # Порог 1 ГБ, проверка каждые 30 секунд
autopriority -mem=1G -interval=30s autopriority -mem=1G -interval=30s
# Run with defaults (512M threshold, 2G game threshold) # Запуск со значениями по умолчанию: порог 512M, игровой порог 2G
autopriority autopriority
# Game mode: normal threshold 512M, game threshold 4G # Обычный порог 512M, игровой порог 4G
autopriority -mem=512M -game-mem=4G autopriority -mem=512M -game-mem=4G
# Dry run — log decisions without changing anything # Пробный запуск: записывать решения, ничего не изменяя
autopriority -dry-run 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) | | `PROMOTE` | Приоритет повышен до HIGH в обычном режиме |
| `DEMOTE` | Priority lowered to NORMAL (normal mode) | | `DEMOTE` | Приоритет снижен до NORMAL в обычном режиме |
| `GAME DETECT` | Process first detected as exceeding game-mem threshold | | `GAME DETECT` | Впервые обнаружен процесс, превысивший порог `-game-mem` |
| `GAME` | Process set to HIGH or IDLE (game mode) | | `GAME` | Процессу назначен HIGH или IDLE в игровом режиме |
| `GAME MODE ON` | Game mode activated | | `GAME MODE ON` | Игровой режим включен |
| `GAME MODE OFF` | Game mode deactivated — processes restored | | `GAME MODE OFF` | Игровой режим выключен, приоритеты восстановлены |
| `BLOCK` | OpenProcess or SetPriorityClass failed; process added to exclusion list | | `BLOCK` | Ошибка OpenProcess или SetPriorityClass, процесс добавлен в список исключений |
| `RESTORE` | Process restored to original/saved priority (game mode exit or shutdown) | | `SKIP` | Не удалось прочитать сведения о процессе, попытка повторится при следующей проверке |
| `[DRY-RUN]` | Would change priority (dry-run mode) | | `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.
+366 -293
View File
@@ -5,9 +5,11 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"math"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -17,7 +19,6 @@ import (
const ( const (
CreateToolhelp32SnapshotProcess = 0x00000002 CreateToolhelp32SnapshotProcess = 0x00000002
ProcessTerminate = 0x00000001
ProcessSetInformation = 0x00000200 ProcessSetInformation = 0x00000200
ProcessQueryLimitedInformation = 0x00001000 ProcessQueryLimitedInformation = 0x00001000
PriorityClassIdle = 0x00000040 PriorityClassIdle = 0x00000040
@@ -27,6 +28,7 @@ const (
PriorityClassRealtime = 0x00000100 PriorityClassRealtime = 0x00000100
PriorityClassBelowNormal = 0x00004000 PriorityClassBelowNormal = 0x00004000
ErrorNoMoreFiles = 18 ErrorNoMoreFiles = 18
ErrorAlreadyExists = 183
) )
type processEntry32 struct { type processEntry32 struct {
@@ -43,43 +45,61 @@ type processEntry32 struct {
} }
type processMemoryCounters struct { type processMemoryCounters struct {
CBM uint32 CBM uint32
PageFaultCount uint32 PageFaultCount uint32
PeakWorkingSetSize uintptr PeakWorkingSetSize uintptr
WorkingSetSize uintptr WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr QuotaPagedPoolUsage uintptr
QuotaPeakNonPagedPoolUsage uintptr QuotaPeakNonPagedPoolUsage uintptr
QuotaNonPagedPoolUsage uintptr QuotaNonPagedPoolUsage uintptr
PeakPagefileUsage uintptr PeakPagefileUsage uintptr
PagefileUsage uintptr PagefileUsage uintptr
PrivateUsage uintptr PrivateUsage uintptr
}
type processKey struct {
PID uint32
Created uint64
} }
type procInfo struct { type procInfo struct {
PID uint32 PID uint32
Name string Name string
RSS uint64 RSS uint64
Key processKey
Prio uint32
Handle syscall.Handle
CanSet bool
Keep bool
}
type trackedProc struct {
name string
handle syscall.Handle
} }
type savedPrio struct { type savedPrio struct {
name string name string
prio uint32 prio uint32
handle syscall.Handle
} }
var ( var (
k32 = syscall.NewLazyDLL("kernel32.dll") k32 = syscall.NewLazyDLL("kernel32.dll")
ps = syscall.NewLazyDLL("psapi.dll") ps = syscall.NewLazyDLL("psapi.dll")
procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot") procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = k32.NewProc("Process32FirstW") procProcess32First = k32.NewProc("Process32FirstW")
procProcess32Next = k32.NewProc("Process32NextW") procProcess32Next = k32.NewProc("Process32NextW")
procGetMemInfo = ps.NewProc("GetProcessMemoryInfo") procGetMemInfo = ps.NewProc("GetProcessMemoryInfo")
procSetPriority = k32.NewProc("SetPriorityClass") procSetPriority = k32.NewProc("SetPriorityClass")
procGetPriority = k32.NewProc("GetPriorityClass") procGetPriority = k32.NewProc("GetPriorityClass")
procOpenProcess = k32.NewProc("OpenProcess") procOpenProcess = k32.NewProc("OpenProcess")
procCloseHandle = k32.NewProc("CloseHandle") 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) { func closeH(h syscall.Handle) {
@@ -94,12 +114,7 @@ func openProc(pid uint32, acc uint32) (syscall.Handle, error) {
return syscall.Handle(r), nil return syscall.Handle(r), nil
} }
func setPrio(pid uint32, cls uint32) error { func setPrio(h syscall.Handle, pid uint32, cls uint32) error {
h, err := openProc(pid, ProcessSetInformation)
if err != nil {
return err
}
defer closeH(h)
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls)) r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
if r == 0 { if r == 0 {
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e) return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
@@ -107,13 +122,44 @@ func setPrio(pid uint32, cls uint32) error {
return nil return nil
} }
func utf16Trim(p []uint16) string { func processCreated(h syscall.Handle) (uint64, error) {
for i, v := range p { var created, exited, kernel, user syscall.Filetime
if v == 0 { r, _, e := procGetProcessTimes.Call(
return syscall.UTF16ToString(p[:i]) 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 syscall.UTF16ToString(p) 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 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) { func parseMemSize(s string) (uint64, error) {
@@ -121,32 +167,26 @@ func parseMemSize(s string) (uint64, error) {
if s == "" { if s == "" {
return 0, fmt.Errorf("empty value") return 0, fmt.Errorf("empty value")
} }
var suffix byte multiplier := uint64(1)
switch s[len(s)-1] { switch s[len(s)-1] {
case 'k', 'K': case 'k', 'K':
suffix = 'K' multiplier = 1024
s = s[:len(s)-1] s = s[:len(s)-1]
case 'm', 'M': case 'm', 'M':
suffix = 'M' multiplier = 1024 * 1024
s = s[:len(s)-1] s = s[:len(s)-1]
case 'g', 'G': case 'g', 'G':
suffix = 'G' multiplier = 1024 * 1024 * 1024
s = s[:len(s)-1] s = s[:len(s)-1]
} }
n, err := strconv.ParseUint(s, 10, 64) n, err := strconv.ParseUint(s, 10, 64)
if err != nil { if err != nil {
return 0, err return 0, err
} }
switch suffix { if n > math.MaxUint64/multiplier {
case 'K': return 0, fmt.Errorf("value overflows uint64")
return n * 1024, nil
case 'M':
return n * 1024 * 1024, nil
case 'G':
return n * 1024 * 1024 * 1024, nil
default:
return n, nil
} }
return n * multiplier, nil
} }
func formatMemSize(b uint64) string { func formatMemSize(b uint64) string {
@@ -165,6 +205,22 @@ func isAboveNormal(c uint32) bool {
c == PriorityClassRealtime 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 { func prioName(c uint32) string {
switch c { switch c {
case PriorityClassIdle: case PriorityClassIdle:
@@ -186,7 +242,7 @@ func prioName(c uint32) string {
func allProcs() ([]procInfo, error) { func allProcs() ([]procInfo, error) {
snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0) snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0)
if snap == 0 { if snap == ^uintptr(0) {
return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e) return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e)
} }
defer closeH(syscall.Handle(snap)) defer closeH(syscall.Handle(snap))
@@ -199,7 +255,7 @@ func allProcs() ([]procInfo, error) {
var out []procInfo var out []procInfo
for { for {
name := utf16Trim(pe.ExeFile[:]) name := syscall.UTF16ToString(pe.ExeFile[:])
if name != "" { if name != "" {
out = append(out, procInfo{PID: pe.PID, Name: name}) out = append(out, procInfo{PID: pe.PID, Name: name})
} }
@@ -215,22 +271,16 @@ func allProcs() ([]procInfo, error) {
return out, nil return out, nil
} }
func killOtherInstances() { func singleInstance() (syscall.Handle, bool, error) {
myPID := os.Getpid() name, err := syscall.UTF16PtrFromString(`Local\autoPriority`)
procs, err := allProcs()
if err != nil { if err != nil {
return return 0, false, err
} }
myName := filepath.Base(os.Args[0]) r, _, e := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(name)))
for _, p := range procs { if r == 0 {
if p.PID != uint32(myPID) && filepath.Base(p.Name) == myName { return 0, false, fmt.Errorf("CreateMutex failed: %w", e)
h, err := openProc(p.PID, ProcessTerminate)
if err == nil {
procTerminate.Call(uintptr(h), 0)
closeH(h)
}
}
} }
return syscall.Handle(r), e == syscall.Errno(ErrorAlreadyExists), nil
} }
func main() { func main() {
@@ -245,7 +295,10 @@ func main() {
} }
mem, err := parseMemSize(*memStr) 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) fmt.Fprintf(os.Stderr, "autoPriority: invalid -mem value: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -260,72 +313,195 @@ func main() {
os.Exit(1) os.Exit(1)
} }
killOtherInstances() mutex, exists, err := singleInstance()
time.Sleep(100 * time.Millisecond) if err != nil {
fmt.Fprintf(os.Stderr, "autoPriority: %v\n", err)
os.Exit(1)
}
if exists {
closeH(mutex)
fmt.Fprintln(os.Stderr, "autoPriority: already running")
return
}
defer closeH(mutex)
var logFile *os.File
logPath := filepath.Join(os.TempDir(), "autopriority.log") logPath := filepath.Join(os.TempDir(), "autopriority.log")
os.Remove(logPath) logFile, err := os.Create(logPath)
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) if err != nil {
if err == nil { fmt.Fprintf(os.Stderr, "autoPriority: cannot create log: %v\n", err)
logFile = f os.Exit(1)
defer logFile.Close()
}
ts := func() string {
return time.Now().Format("02.01.2006 15:04:05")
} }
defer logFile.Close()
logf := func(format string, a ...any) { logf := func(format string, a ...any) {
if logFile == nil { fmt.Fprintf(logFile, "[%s] ", time.Now().Format("02.01.2006 15:04:05"))
return
}
fmt.Fprintf(logFile, "[%s] ", ts())
fmt.Fprintf(logFile, format+"\n", a...) fmt.Fprintf(logFile, format+"\n", a...)
logFile.Sync()
} }
if gameMem > 0 { logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)",
logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)", formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun)
} else { if !*dryRun {
logf("autoPriority started (mem=%s, interval=%s, dry-run=%v)", pid := uint32(os.Getpid())
formatMemSize(mem), *interval, *dryRun) 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")
}
} }
if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil { blocked := make(map[processKey]string)
logf("warning: could not set own priority to IDLE: %v", err) unreadable := make(map[uint32]string)
} else { gameProcs := make(map[processKey]trackedProc)
logf("own priority set to IDLE") gameSaved := make(map[processKey]savedPrio)
}
blocked := make(map[uint32]string)
gameProcs := make(map[uint32]string)
gameSaved := make(map[uint32]savedPrio)
gameMode := false gameMode := false
myPID := uint32(os.Getpid())
defer func() { readProcs := func() ([]procInfo, error) {
if gameMode { procs, err := allProcs()
restoreProcs, _ := allProcs() if err != nil {
rm := make(map[uint32]string, len(restoreProcs)) return nil, err
for _, p := range restoreProcs { }
rm[p.PID] = p.Name
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
} }
for pid, sv := range gameSaved {
if cur, ok := rm[pid]; !ok || cur != sv.name { 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 continue
} }
if err := setPrio(pid, sv.prio); err != nil { logf("RESTORE %s (PID %d) -> %s", saved.name, key.PID, prioName(saved.prio))
logf("RESTORE %s (PID %d) -> %s error: %v", sv.name, pid, prioName(sv.prio), err) }
} else { if p := current[key]; p != nil {
logf("RESTORE %s (PID %d) -> %s", sv.name, pid, prioName(sv.prio)) 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") logf("autoPriority stopped")
logFile.Sync()
}() }()
myPID := uint32(os.Getpid())
ticker := time.NewTicker(*interval) ticker := time.NewTicker(*interval)
defer ticker.Stop() defer ticker.Stop()
@@ -333,219 +509,116 @@ func main() {
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
scan := func() { scan := func() {
procs, err := allProcs() list, err := readProcs()
if err != nil { if err != nil {
logf("allProcs error: %v", err) logf("process scan error: %v", err)
return return
} }
defer func() {
for _, p := range list {
if !p.Keep {
closeH(p.Handle)
}
}
}()
pidMap := make(map[uint32]string, len(procs)) current := make(map[processKey]string, len(list))
for _, p := range procs { for _, p := range list {
pidMap[p.PID] = p.Name current[p.Key] = p.Name
} }
for pid := range blocked { for key, name := range blocked {
if _, ok := pidMap[pid]; !ok { if current[key] != name {
delete(blocked, pid) delete(blocked, key)
} }
} }
for pid, name := range gameProcs { for key, game := range gameProcs {
if cur, ok := pidMap[pid]; !ok || cur != name { alive, err := processAlive(game.handle)
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)
if err != nil { if err != nil {
if _, ok := blocked[p.PID]; !ok { logf("GAME %s (PID %d) status error: %v", game.name, key.PID, err)
blocked[p.PID] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err)
}
continue continue
} }
if !alive {
var m processMemoryCounters closeH(game.handle)
m.CBM = uint32(unsafe.Sizeof(m)) delete(gameProcs, key)
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)
}
continue
} }
}
for key, saved := range gameSaved {
alive, err := processAlive(saved.handle)
if err == nil && !alive {
closeH(saved.handle)
delete(gameSaved, key)
}
}
p.RSS = uint64(m.WorkingSetSize) for i := range list {
list = append(list, *p) p := &list[i]
if gameMem > 0 && p.RSS >= gameMem { if gameMem > 0 && p.RSS >= gameMem {
if _, ok := gameProcs[p.PID]; !ok { if _, ok := gameProcs[p.Key]; !ok {
gameProcs[p.PID] = p.Name 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)) logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS))
} }
} }
} }
hasGame := gameMem > 0 && len(gameProcs) > 0 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 { for i := range list {
p := &list[i] p := &list[i]
_, isBlocked := blocked[p.PID] _, isBlocked := blocked[p.Key]
_, isGame := gameProcs[p.PID] _, isGame := gameProcs[p.Key]
target := desiredPriority(p.RSS, mem, p.Prio, hasGame, isGame)
if hasGame && isGame { action := "GAME"
if isBlocked { if !hasGame && target == PriorityClassHigh {
continue action = "PROMOTE"
} } else if !hasGame {
if *dryRun { action = "DEMOTE"
logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (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) == 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 target == 0 || target == p.Prio || isBlocked {
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 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 { if *dryRun {
logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr) 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 continue
} }
if err := setPrio(p.PID, PriorityClassHigh); err != nil { if err := setPrio(p.Handle, p.PID, target); err != nil {
blocked[p.PID] = p.Name blocked[p.Key] = p.Name
logf("BLOCK %s (PID %d): %v (added to exclusion list)", p.Name, p.PID, err) logf("BLOCK %s (PID %d): %v (priority changes disabled)", p.Name, p.PID, err)
} else { continue
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 {
if gameMem > 0 { gameSaved[p.Key] = savedPrio{name: p.Name, prio: p.Prio, handle: p.Handle}
newHasGame := len(gameProcs) > 0 p.Keep = true
if newHasGame && !gameMode {
gameMode = true
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)
} }
} }
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")
} }
} }
+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)
}
})
}
}