diff --git a/README.md b/README.md index 37dbe30..3024d0e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main.go b/main.go index 4848938..b8a9443 100644 --- a/main.go +++ b/main.go @@ -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 { @@ -43,43 +45,61 @@ type processEntry32 struct { } type processMemoryCounters struct { - CBM uint32 - PageFaultCount uint32 - PeakWorkingSetSize uintptr - WorkingSetSize uintptr - QuotaPeakPagedPoolUsage uintptr - QuotaPagedPoolUsage uintptr - QuotaPeakNonPagedPoolUsage uintptr - QuotaNonPagedPoolUsage uintptr - PeakPagefileUsage uintptr - PagefileUsage uintptr - PrivateUsage uintptr + CBM uint32 + PageFaultCount uint32 + PeakWorkingSetSize uintptr + WorkingSetSize uintptr + QuotaPeakPagedPoolUsage uintptr + QuotaPagedPoolUsage uintptr + QuotaPeakNonPagedPoolUsage uintptr + QuotaNonPagedPoolUsage uintptr + PeakPagefileUsage uintptr + PagefileUsage uintptr + PrivateUsage uintptr +} + +type processKey struct { + PID uint32 + Created uint64 } type procInfo struct { - PID uint32 - Name string - RSS uint64 + 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 + name string + prio uint32 + handle syscall.Handle } var ( k32 = syscall.NewLazyDLL("kernel32.dll") ps = syscall.NewLazyDLL("psapi.dll") - procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot") - procProcess32First = k32.NewProc("Process32FirstW") - procProcess32Next = k32.NewProc("Process32NextW") - procGetMemInfo = ps.NewProc("GetProcessMemoryInfo") - procSetPriority = k32.NewProc("SetPriorityClass") - procGetPriority = k32.NewProc("GetPriorityClass") - procOpenProcess = k32.NewProc("OpenProcess") - procCloseHandle = k32.NewProc("CloseHandle") - procTerminate = k32.NewProc("TerminateProcess") + procCreateSnap = k32.NewProc("CreateToolhelp32Snapshot") + procProcess32First = k32.NewProc("Process32FirstW") + procProcess32Next = k32.NewProc("Process32NextW") + procGetMemInfo = ps.NewProc("GetProcessMemoryInfo") + procSetPriority = k32.NewProc("SetPriorityClass") + procGetPriority = k32.NewProc("GetPriorityClass") + procOpenProcess = k32.NewProc("OpenProcess") + procCloseHandle = k32.NewProc("CloseHandle") + 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 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) { @@ -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 + return 0, false, err } - 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) - } - } + 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) + mutex, exists, err := singleInstance() + 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") - 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() - } - - ts := func() string { - return time.Now().Format("02.01.2006 15:04:05") + 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) { - if logFile == nil { - return - } - fmt.Fprintf(logFile, "[%s] ", ts()) + 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) + logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)", + formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun) + + 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") + } } - if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); 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) + 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()) - defer func() { - if gameMode { - restoreProcs, _ := allProcs() - rm := make(map[uint32]string, len(restoreProcs)) - for _, p := range restoreProcs { - rm[p.PID] = p.Name + 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 } - 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 } - 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)) - } + 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,219 +509,116 @@ 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) - } - continue + 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) + } + } - 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] - - if hasGame && isGame { - if isBlocked { - continue - } - if *dryRun { - 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 + _, 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 { - 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) - } + if target == 0 || target == p.Prio || isBlocked { 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) + 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 } - 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 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 } - } - - if gameMem > 0 { - newHasGame := len(gameProcs) > 0 - 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) + 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 } } + 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") } } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..730ef6e --- /dev/null +++ b/main_test.go @@ -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) + } + }) + } +}