From 0e8d7333c1148d8605b1c2494bdfea2574caff1b Mon Sep 17 00:00:00 2001 From: lzrdblzzrd Date: Tue, 23 Jun 2026 11:16:51 +0300 Subject: [PATCH] Rewrite: simplify to strict algorithm, remove promoted map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove promoted map entirely — processes are demoted when RSS < -mem and promoted when RSS >= -mem, no permanent state - Remove trySetPrio closure — use setPrio directly for simplicity - Remove curPrioWithHandle helper — inline procGetPriority.Call - Remove GAME ... tracked as game (blocked) repeated log — log once on detection via GAME DETECT, silently skip on subsequent scans - Game mode: log GAME DETECT once when process first exceeds game-mem - gameSaved stores original priority before IDLE demote, restores on GAME MODE OFF or shutdown - Delay between priority changes: 100ms - Delete TZ.md (was a test for other AI) --- TZ.md | 361 --------------------------------------------------- main.go | 395 +++++++++++++++++++++++--------------------------------- 2 files changed, 163 insertions(+), 593 deletions(-) delete mode 100644 TZ.md diff --git a/TZ.md b/TZ.md deleted file mode 100644 index 0f1b4a9..0000000 --- a/TZ.md +++ /dev/null @@ -1,361 +0,0 @@ -# Техническое задание: autoPriority - -## Назначение - -Программа для Windows, которая автоматически управляет приоритетами процессов на основе их потребления оперативной памяти. Работает в фоне, периодически сканируя все процессы и корректируя их приоритеты CPU. - -## Параметры командной строки - -| Параметр | По умолчанию | Описание | -|---|---|---| -| `-mem` | `512M` | Порог памяти для повышения приоритета. Процессы с RSS ≥ этого значения повышаются до HIGH | -| `-game-mem` | `4G` | Порог памяти для активации игрового режима. Процессы с RSS ≥ этого значения считаются играми. `0` — игровой режим отключён. Должен быть больше `-mem` | -| `-interval` | `1m` | Интервал сканирования. Минимум 10 секунд | -| `-dry-run` | `false` | Только логирование, без изменения приоритетов | - -Формат памяти: число с суффиксом `K`/`M`/`G` (например `512M`, `4G`, `2048M`). Без суффикра — байты. - -## Структуры данных - -Программа поддерживает четыре основные карты (map), хранящиеся в памяти между сканами: - -### `promoted: map[pid → name]` -Процессы, повышенные до HIGH в нормальном режиме. Если RSS процесса падает ниже порога `-mem`, он понижается до NORMAL и удаляется из карты. При выходе из программы все процессы из карты восстанавливаются до NORMAL. - -### `blocked: map[pid → name]` -Процессы, для которых не удалось открыть или изменить приоритет (системные процессы, античит). Полностью исключаются из попыток смены приоритета, но их RSS **всё равно измеряется** каждый скан (для определения игрового режима). Удаляются из карты, когда PID больше не существует в списке процессов. - -### `gameProcs: map[pid → name]` -Процессы, превысившие `-game-mem` порог. Определяют, активен ли игровой режим. Удаляются, когда процесс умирает (PID исчезает из списка или имя не совпадает). - -### `gameSaved: map[pid → {name, originalPriority}]` -Оригинальные приоритеты процессов, пониженных до IDLE в игровом режиме. Используются для восстановления при выключении игрового режима. Заполняется только при успешном понижении приоритета. Каждый PID сохраняется один раз (при первом понижении), чтобы запомнить приоритет, который был **до** игрового режима. - -## Запуск программы - -1. Разбор флагов командной строки -2. Проверка: если `game-mem > 0` и `game-mem <= mem` — выход с ошибкой -3. Если `interval < 10s` — принудительно установить 10s -4. **Уничтожение других экземпляров:** получить список всех процессов, найти процессы с тем же именем файла, что и у текущего процесса, но с другим PID, и завершить их через `TerminateProcess` -5. Подождать 100мс (ОС освобождает файловые дескрипторы) -6. Удалить старый лог-файл, создать новый (в `%TEMP%\autopriority.log`) -7. Установить собственный приоритет в IDLE -8. Записать в лог параметры запуска -9. Инициализировать пустые карты: `promoted`, `blocked`, `gameProcs`, `gameSaved` -10. Зарегистрировать deferred-функцию восстановления (см. раздел «Завершение») -11. Выполнить первый скан немедленно -12. Войти в цикл: ждать сигнала завершения ИЛИ тикера интервала, при тике — выполнять скан - -## Алгоритм сканирования (двухпроходный) - -Каждый скан состоит из двух проходов. **Первый проход** собирает данные о памяти и определяет игровые процессы. **Второй проход** применяет приоритеты, уже зная, активен ли игровой режим. Это необходимо, чтобы игровой режим срабатывал на первом же скане, а не на следующем. - -### Предварительный этап: очистка карт - -Перед проходами программа получает список всех процессов и строит `pidMap` (pid → name). Затем очищает четыре карты: - -- **`promoted`**: если PID исчез или имя не совпадает — удалить (процесс умер или PID перешёл другому процессу) -- **`blocked`**: если PID исчез — удалить (процесс умер, PID может быть переиспользован) -- **`gameProcs`**: если PID исчез или имя не совпадает — удалить -- **`gameSaved`**: если PID исчез или имя не совпадается — удалить - -Проверка имени защищает от PID reuse: если процесс умер и его PID получил другой процесс, старая запись удаляется. - -### Проход 1: Сбор RSS и определение игровых процессов - -Для каждого процесса (кроме себя и PID 0): - -1. Открыть процесс с правом `PROCESS_QUERY_LIMITED_INFORMATION` (0x1000). Это узкое право, которое дают почти все процессы, включая защищённые. Использование `PROCESS_QUERY_INFORMATION` (0x0400) приводит к Access Denied для многих пользовательских процессов. -2. Если `OpenProcess` упал: - - Добавить PID в `blocked` (если ещё не там) - - Записать в лог `BLOCK ... (added to exclusion list)` - - **Перейти к следующему процессу** (не добавлять в `procList`) - - Примечание: blocked процессы не попадают в `procList`, но если они были добавлены в `blocked` на предыдущем скане и всё ещё живы, они НЕ измеряются повторно. Это допустимо, потому что blocked процессы не могут менять приоритет. - - **Важное исключение:** если процесс УЖЕ в `blocked` (добавлен ранее), то не логировать повторно — просто пропустить. - -3. Вызвать `GetProcessMemoryInfo` для получения `WorkingSetSize` (RSS) -4. Если упало — пропустить процесс -5. Закрыть handle -6. Записать `{PID, Name, RSS}` в `procList` -7. Если `game-mem > 0` и `RSS >= game-mem`: - - Добавить PID в `gameProcs` (если ещё не там) - -После прохода 1 вычислить: -``` -hasGame = (game-mem > 0) AND (len(gameProcs) > 0) -``` - -### Проход 2: Применение приоритетов - -Для каждого процесса из `procList` (в порядке обхода): - -Определить `isBlocked`: процесс находится в `blocked`. - -#### Вариант A: Процесс — игровой (есть в `gameProcs`) - -Если `hasGame` (игровой режим активен): -- Если `isBlocked`: записать в лог `GAME ... — tracked as game (blocked)`, перейти к следующему -- Открыть с `PROCESS_QUERY_LIMITED_INFORMATION` -- Получить текущий приоритет -- Если уже HIGH — пропустить -- Если `dry-run`: записать `would set HIGH (game)`, пропустить -- Иначе: попытаться установить HIGH через `trySetPrio` (см. ниже) - - Успех → `GAME ... -> HIGH` - - Провал → `GAME ... — tracked as game (priority change failed)` - -Перейти к следующему процессу. - -#### Вариант B: Игровой режим активен, процесс НЕ игровой - -Если `isBlocked`: пропустить (не пытаться понизить) -Открыть с `PROCESS_QUERY_LIMITED_INFORMATION` -Получить текущий приоритет -Если уже IDLE — пропустить -Если `dry-run`: записать `would set IDLE (game)`, пропустить -Иначе: попытаться установить IDLE через `trySetPrio` - - Успех → сохранить оригинальный приоритет в `gameSaved` (если ещё не сохранён), удалить из `promoted` (если там был), записать `GAME ... -> IDLE` - - Провал → процесс добавлен в `blocked`, больше не пытаться - -Перейти к следующему процессу. - -#### Вариант C: Нормальный режим (игровой режим не активен) - -Если `isBlocked`: пропустить - -Открыть с `PROCESS_QUERY_LIMITED_INFORMATION` - -**Если RSS < `-mem`:** -- Получить текущий приоритет -- Если приоритет ABOVE_NORMAL / HIGH / REALTIME → понизить до NORMAL (DEMOTE), удалить из `promoted` (если там был) -- Иначе пропустить - -**Если RSS >= `-mem`:** -- Получить текущий приоритет -- Если уже HIGH — пропустить (процесс уже повышен, возможно из `promoted`) -- Если `dry-run`: записать `would set HIGH`, пропустить -- Иначе: попытаться установить HIGH через `trySetPrio` - - Успех → добавить в `promoted`, записать `PROMOTE ... -> HIGH` - - Провал → процесс добавлен в `blocked` - -### Финал скана: управление игровым режимом - -После прохода 2, если `game-mem > 0`: - -Вычислить `newHasGame = len(gameProcs) > 0` - -**Если `newHasGame == true` и `gameMode == false`:** -- `gameMode = true` -- Записать `GAME MODE ON` - -**Если `newHasGame == false` и `gameMode == true`:** -- `gameMode = false` -- Записать `GAME MODE OFF` -- **Восстановить приоритеты:** для каждого PID в `gameSaved`: - - Проверить, что PID всё ещё существует и имя совпадает (защита от PID reuse) - - Если `dry-run`: записать `[DRY-RUN] RESTORE ... -> originalPriority` - - Иначе: установить оригинальный приоритет через `setPrio` (отдельный `OpenProcess` с `PROCESS_SET_INFORMATION`) - - Успех → `RESTORE ... -> originalPriority` - - Провал → `RESTORE ... error` - - Удалить запись из `gameSaved` -- Очистить `gameSaved` (все записи обработаны) - -## Функция trySetPrio - -``` -trySetPrio(queryHandle, pid, name, targetClass) → bool -``` - -1. Закрыть `queryHandle` (handle от `PROCESS_QUERY_LIMITED_INFORMATION`) -2. Открыть процесс заново с правом `PROCESS_SET_INFORMATION` (0x0200) - - Разделение прав: многие процессы дают `QUERY` но не дают `SET` в комбинации -3. Если `OpenProcess` упал: - - Добавить PID в `blocked` - - Записать `BLOCK ... open SetInformation failed` - - Вернуть `false` -4. Вызвать `SetPriorityClass(handle, targetClass)` -5. Закрыть handle (через defer) -6. Если `SetPriorityClass` упал: - - Добавить PID в `blocked` - - Записать `BLOCK ... SetPriorityClass failed` - - Вернуть `false` -7. Подождать 50мс (плавная смена приоритетов, предотвращает статтеры) -8. Вернуть `true` - -## Функция setPrio (для восстановления) - -``` -setPrio(pid, targetClass) → error -``` - -1. Открыть с `PROCESS_SET_INFORMATION` -2. Вызвать `SetPriorityClass` -3. Закрыть handle -4. Вернуть ошибку или nil - -## Завершение программы (deferred) - -При получении сигнала SIGINT/SIGTERM: - -1. Получить свежий список всех процессов -2. Построить `restoreMap` (pid → name) -3. **Восстановить `gameSaved`:** для каждого PID — проверить имя, установить оригинальный приоритет, записать `RESTORE ... -> originalPriority` -4. **Восстановить `promoted`:** для каждого PID — проверить имя, установить NORMAL, записать `RESTORE ... -> NORMAL` -5. **Восстановить `gameProcs`:** для каждого PID — проверить имя, установить NORMAL, записать `RESTORE ... -> NORMAL` -6. Записать `autoPriority stopped` - -Порядок важен: `gameSaved` восстанавливается первым, потому что `promoted` мог быть удалён из `promoted` во время игрового режима, но его оригинальный приоритет сохранён в `gameSaved`. - -## Классы приоритетов Windows - -| Константа | Значение | Имя в логе | -|---|---|---| -| IDLE | 0x0040 | IDLE | -| BELOW_NORMAL | 0x4000 | BELOW_NORMAL | -| NORMAL | 0x0020 | NORMAL | -| ABOVE_NORMAL | 0x8000 | ABOVE_NORMAL | -| HIGH | 0x0080 | HIGH | -| REALTIME | 0x0100 | REALTIME | - -### Логика isAboveNormal - -Возвращает `true` для `ABOVE_NORMAL`, `HIGH`, `REALTIME`. НЕ возвращает `true` для `BELOW_NORMAL`, потому что числовое значение `BELOW_NORMAL` (0x4000) больше `NORMAL` (0x0020), хотя семантически ниже. Поэтому используется явная проверка, а не числовое сравнение. - -## Логирование - -Лог пишется в `%TEMP%\autopriority.log`. При запуске старый лог удаляется, создаётся новый. Каждая запись синхронизируется на диск (`Sync()`). Формат: `[DD.MM.YYYY HH:MM:SS] сообщение`. - -### Префиксы логов - -| Префикс | Значение | -|---|---| -| `PROMOTE` | Процесс повышен до HIGH (нормальный режим) | -| `DEMOTE` | Процесс понижен до NORMAL (нормальный режим) | -| `GAME` | Процесс изменён вgame режиме (→ HIGH или → IDLE) | -| `GAME MODE ON`| Игровой режим активирован | -| `GAME MODE OFF`| Игровой режим деактивирован, процессы восстановлены | -| `BLOCK` | OpenProcess или SetPriorityClass упал; процесс в исключениях | -| `RESTORE` | Процесс восстановлен в оригинальный приоритет | -| `[DRY-RUN]` | Было бы изменение приоритета (dry-run режим) | - -## Windows API - -Программа использует только `kernel32.dll` и `psapi.dll` через raw syscall. Без внешних зависимостей. - -### Используемые функции - -- `CreateToolhelp32Snapshot` — снимок списка процессов -- `Process32FirstW` / `Process32NextW` — обход снимка -- `OpenProcess` — открытие handle процесса -- `GetProcessMemoryInfo` — получение WorkingSetSize (RSS) -- `GetPriorityClass` — чтение текущего приоритета -- `SetPriorityClass` — установка приоритета -- `CloseHandle` — закрытие handle -- `TerminateProcess` — завершение других экземпляров - -### Права доступа - -- `PROCESS_QUERY_LIMITED_INFORMATION` (0x1000) — для чтения памяти и приоритета. Это право дают почти все процессы. -- `PROCESS_SET_INFORMATION` (0x0200) — для изменения приоритета. Запрашивается отдельно, только когда нужно менять приоритет. -- `PROCESS_TERMINATE` (0x0001) — для уничтожения других экземпляров. - -Разделение прав критично: запрос `QUERY|SET` комбинированно приводит к Access Denied для многих пользовательских процессов (KeePassXC, HD-Player), хотя каждое право по отдельности они дают. - -## Структура processEntry32 - -``` -Size uint32 -CntUsage uint32 -PID uint32 -DefaultHeapID uintptr // НЕ uint32 — intptr на x64 -ModuleID uint32 -CntThreads uint32 -ParentPID uint32 -PrioClass int32 -Flags uint32 -ExeFile [260]uint16 // UTF-16 -``` - -`DefaultHeapID` должен быть `uintptr`, не `uint32` — иначе на x64 структура неправильно выровнена и `Process32Next` падает. - -## Сценарии работы - -### Сценарий 1: Обычная работа - -1. Запуск программы -2. Chrome потребляет 600MB (≥ 512MB) → `PROMOTE Chrome.exe -> HIGH`, добавляется в `promoted` -3. Chrome сбрасывает память до 300MB (< 512MB) → `DEMOTE Chrome.exe -> NORMAL`, удаляется из `promoted` -4. Discord потребляет 700MB → `PROMOTE Discord.exe -> HIGH` -5. Discord сбрасывает до 200MB → `DEMOTE Discord.exe -> NORMAL` - -### Сценарий 2: Понижение высокого приоритета - -1. `wininit.exe` запущен с HIGH, потребляет 6MB (< 512MB порога) -2. Программа видит: RSS < mem, приоритет ABOVE_NORMAL → `DEMOTE wininit.exe -> NORMAL` -3. Это применяется ко всем процессам (включая ранее повышенные, если их RSS упал ниже порога) - -### Сценарий 3: Игровой режим - -1. Запуск RustClient, потребление растёт с 1GB до 8GB -2. На скане, где RSS ≥ 4GB: RustClient добавляется в `gameProcs` -3. `hasGame = true` -4. RustClient → `trySetPrio` HIGH → если античит блокирует → `BLOCK RustClient.exe` + `GAME RustClient.exe — tracked as game (priority change failed)` -5. RustClient остаётся в `gameProcs` (важно: блокировка SetPriorityClass не удаляет из gameProcs) -6. Все остальные процессы → IDLE (`GAME Chrome.exe -> IDLE`, etc.) -7. `GAME MODE ON` -8. При следующем скане RustClient всё ещё в `gameProcs`, остальные процессы уже IDLE — пропускаются - -### Сценарий 4: Выход из игрового режима - -1. RustClient закрыт -2. `gameProcs` очищен (PID исчез) -3. `newHasGame = false`, `gameMode = true` → переход -4. `gameMode = false`, `GAME MODE OFF` -5. Для каждого процесса в `gameSaved`: восстановить оригинальный приоритет - - `RESTORE Chrome.exe -> NORMAL` (был NORMAL до игрового режима) - - `RESTORE Discord.exe -> HIGH` (был HIGH, потому что был в `promoted`) -6. `gameSaved` очищен -7. Нормальный режим возобновляется: процессы с RSS ≥ mem повышаются, с RSS < mem и выше NORMAL — понижаются - -### Сценарий 5: Заблокированный процесс становится игрой - -1. RustClient стартует, потребляет 3.9GB -2. RSS ≥ 512MB (mem), не в `promoted` → попытка PROMOTE → античит блокирует → `BLOCK RustClient.exe` (добавлен в `blocked`) -3. RustClient дорастает до 8GB -4. На следующем скане: RustClient в `blocked`, но **проход 1 всё равно пытается его открыть** -5. `OpenProcess(QUERY_LIMITED)` для blocked процесса — если упадёт, пропустит. Если пройдёт — RSS измеряется -6. Если RustClient в `blocked` из-за `SetPriorityClass` (не `OpenProcess`), то `OpenProcess(QUERY_LIMITED)` пройдёт, RSS измерится, и он попадёт в `gameProcs` -7. `hasGame = true`, `GAME MODE ON` -8. В проходе 2: RustClient в `gameProcs`, но `isBlocked = true` → `GAME RustClient.exe — tracked as game (blocked)`, приоритет не меняется -9. Остальные процессы понижаются до IDLE - -### Сценарий 6: PID reuse - -1. Процесс A (PID 1234, "chrome.exe") повышен, в `promoted` -2. Процесс A умирает -3. Новый процесс B (PID 1234, "notepad.exe") запускается -4. На следующем скане: `pidMap[1234] = "notepad.exe"`, но `promoted[1234] = "chrome.exe"` -5. Имя не совпадает → `delete(promoted, 1234)` -6. Процесс B обрабатывается как новый процесс - -### Сценарий 7: Системные процессы - -1. `svchost.exe` (PID 932) — `OpenProcess(QUERY_LIMITED)` возвращает Access Denied -2. Добавляется в `blocked`, логируется `BLOCK` -3. На следующем скане: PID 932 всё ещё в `blocked`, `OpenProcess` снова падает, но лог НЕ пишется (процесс уже в `blocked`) -4. Когда процесс умирает: PID исчезает из `pidMap` → удаляется из `blocked` - -## Сборка - -``` -go build -ldflags="-H windowsgui -s -w" -o autopriority.exe . -``` - -- `-H windowsgui` — без консольного окна -- `-s -w` — strip debug info, уменьшение размера - -Требуется Go 1.26+. Только Windows (`//go:build windows`). - -## Автозапуск - -Поместить ярлык на `autopriority.exe` в папку `shell:startup` (Win+R → `shell:startup`). diff --git a/main.go b/main.go index 0c5b22e..4848938 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ const ( CreateToolhelp32SnapshotProcess = 0x00000002 ProcessTerminate = 0x00000001 ProcessSetInformation = 0x00000200 - ProcessQueryInformation = 0x00000400 ProcessQueryLimitedInformation = 0x00001000 PriorityClassIdle = 0x00000040 PriorityClassNormal = 0x00000020 @@ -44,22 +43,28 @@ type processEntry32 struct { } type processMemoryCounters struct { - CBM uint32 - PageFaultCount uint32 - PeakWorkingSetSize uintptr - WorkingSetSize uintptr - QuotaPeakPagedPoolUsage uintptr - QuotaPagedPoolUsage uintptr + CBM uint32 + PageFaultCount uint32 + PeakWorkingSetSize uintptr + WorkingSetSize uintptr + QuotaPeakPagedPoolUsage uintptr + QuotaPagedPoolUsage uintptr QuotaPeakNonPagedPoolUsage uintptr - QuotaNonPagedPoolUsage uintptr - PeakPagefileUsage uintptr - PagefileUsage uintptr - PrivateUsage uintptr + QuotaNonPagedPoolUsage uintptr + PeakPagefileUsage uintptr + PagefileUsage uintptr + PrivateUsage uintptr } -type processInfo struct { +type procInfo struct { PID uint32 Name string + RSS uint64 +} + +type savedPrio struct { + name string + prio uint32 } var ( @@ -102,14 +107,6 @@ func setPrio(pid uint32, cls uint32) error { return nil } -func curPrioWithHandle(h syscall.Handle) (uint32, error) { - r, _, e := procGetPriority.Call(uintptr(h)) - if r == 0 { - return 0, fmt.Errorf("GetPriorityClass failed: %w", e) - } - return uint32(r), nil -} - func utf16Trim(p []uint16) string { for i, v := range p { if v == 0 { @@ -162,37 +159,6 @@ func formatMemSize(b uint64) string { return fmt.Sprintf("%dKB", b/1024) } -func allProcs() ([]processInfo, error) { - snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0) - if snap == 0 { - return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e) - } - defer closeH(syscall.Handle(snap)) - - pe := processEntry32{Size: uint32(unsafe.Sizeof(processEntry32{}))} - r, _, e := procProcess32First.Call(snap, uintptr(unsafe.Pointer(&pe))) - if r == 0 { - return nil, fmt.Errorf("Process32First failed: %w", e) - } - - var out []processInfo - for { - name := utf16Trim(pe.ExeFile[:]) - if name != "" { - out = append(out, processInfo{PID: pe.PID, Name: name}) - } - pe.Size = uint32(unsafe.Sizeof(processEntry32{})) - r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe))) - if r == 0 { - if e == syscall.Errno(ErrorNoMoreFiles) { - break - } - return nil, fmt.Errorf("Process32Next failed: %w", e) - } - } - return out, nil -} - func isAboveNormal(c uint32) bool { return c == PriorityClassAboveNormal || c == PriorityClassHigh || @@ -218,7 +184,36 @@ func prioName(c uint32) string { } } +func allProcs() ([]procInfo, error) { + snap, _, e := procCreateSnap.Call(CreateToolhelp32SnapshotProcess, 0) + if snap == 0 { + return nil, fmt.Errorf("CreateToolhelp32Snapshot failed: %w", e) + } + defer closeH(syscall.Handle(snap)) + pe := processEntry32{Size: uint32(unsafe.Sizeof(processEntry32{}))} + r, _, e := procProcess32First.Call(snap, uintptr(unsafe.Pointer(&pe))) + if r == 0 { + return nil, fmt.Errorf("Process32First failed: %w", e) + } + + var out []procInfo + for { + name := utf16Trim(pe.ExeFile[:]) + if name != "" { + out = append(out, procInfo{PID: pe.PID, Name: name}) + } + pe.Size = uint32(unsafe.Sizeof(processEntry32{})) + r, _, e = procProcess32Next.Call(snap, uintptr(unsafe.Pointer(&pe))) + if r == 0 { + if e == syscall.Errno(ErrorNoMoreFiles) { + break + } + return nil, fmt.Errorf("Process32Next failed: %w", e) + } + } + return out, nil +} func killOtherInstances() { myPID := os.Getpid() @@ -240,7 +235,7 @@ func killOtherInstances() { func main() { memStr := flag.String("mem", "512M", "memory threshold (e.g. 512M, 1G, 2048M)") - gameMemStr := flag.String("game-mem", "4G", "game memory threshold (e.g. 3G, 4G). Must be greater than -mem. 0 = disabled.") + gameMemStr := flag.String("game-mem", "2G", "game memory threshold (e.g. 2G, 4G). Must be greater than -mem. 0 = disabled.") interval := flag.Duration("interval", time.Minute, "scan interval") dryRun := flag.Bool("dry-run", false, "log only, do not change priorities") flag.Parse() @@ -271,8 +266,7 @@ func main() { 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) + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) if err == nil { logFile = f defer logFile.Close() @@ -292,9 +286,11 @@ func main() { } if gameMem > 0 { - logf("autoPriority started (mem=%s, game-mem=%s, interval=%s, dry-run=%v)", formatMemSize(mem), formatMemSize(gameMem), *interval, *dryRun) + 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, interval=%s, dry-run=%v)", + formatMemSize(mem), *interval, *dryRun) } if err := setPrio(uint32(os.Getpid()), PriorityClassIdle); err != nil { @@ -303,53 +299,27 @@ func main() { logf("own priority set to IDLE") } - promoted := make(map[uint32]string) blocked := make(map[uint32]string) gameProcs := make(map[uint32]string) + gameSaved := make(map[uint32]savedPrio) gameMode := false - type savedPrio struct { - name string - prio uint32 - } - gameSaved := make(map[uint32]savedPrio) - defer func() { - restoreProcs, _ := allProcs() - restoreMap := make(map[uint32]string, len(restoreProcs)) - for _, p := range restoreProcs { - restoreMap[p.PID] = p.Name - } - for pid, sv := range gameSaved { - if cur, ok := restoreMap[pid]; !ok || cur != sv.name { - continue + if gameMode { + restoreProcs, _ := allProcs() + rm := make(map[uint32]string, len(restoreProcs)) + for _, p := range restoreProcs { + rm[p.PID] = p.Name } - 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)) - } - } - for pid, name := range promoted { - if cur, ok := restoreMap[pid]; !ok || cur != name { - logf("SKIP RESTORE PID %d: process name mismatch (expected %s, got %s)", pid, name, cur) - continue - } - if err := setPrio(pid, PriorityClassNormal); err != nil { - logf("RESTORE %s (PID %d) -> NORMAL error: %v", name, pid, err) - } else { - logf("RESTORE %s (PID %d) -> NORMAL", name, pid) - } - } - for pid, name := range gameProcs { - if cur, ok := restoreMap[pid]; !ok || cur != name { - logf("SKIP RESTORE PID %d: process name mismatch (expected %s, got %s)", pid, name, cur) - continue - } - if err := setPrio(pid, PriorityClassNormal); err != nil { - logf("RESTORE %s (PID %d) -> NORMAL error: %v", name, pid, err) - } else { - logf("RESTORE %s (PID %d) -> NORMAL", name, pid) + for pid, sv := range gameSaved { + if cur, ok := rm[pid]; !ok || cur != sv.name { + 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("autoPriority stopped") @@ -362,25 +332,6 @@ func main() { stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - trySetPrio := func(qh syscall.Handle, pid uint32, name string, cls uint32) bool { - closeH(qh) - sh, err := openProc(pid, ProcessSetInformation) - if err != nil { - blocked[pid] = name - logf("BLOCK %s (PID %d): open SetInformation failed: %v (added to exclusion list)", name, pid, err) - return false - } - defer closeH(sh) - sr, _, se := procSetPriority.Call(uintptr(sh), uintptr(cls)) - if sr == 0 { - blocked[pid] = name - logf("BLOCK %s (PID %d): SetPriorityClass failed: %v (added to exclusion list)", name, pid, se) - return false - } - time.Sleep(50 * time.Millisecond) - return true - } - scan := func() { procs, err := allProcs() if err != nil { @@ -393,31 +344,25 @@ func main() { pidMap[p.PID] = p.Name } - for pid, name := range promoted { - if cur, ok := pidMap[pid]; !ok || cur != name { - delete(promoted, pid) - } - } - for pid := range blocked { if _, ok := pidMap[pid]; !ok { delete(blocked, pid) } } - for pid, name := range gameProcs { if cur, ok := pidMap[pid]; !ok || cur != name { delete(gameProcs, pid) } } - - type procRSS struct { - info processInfo - rss uint64 + for pid, sv := range gameSaved { + if cur, ok := pidMap[pid]; !ok || cur != sv.name { + delete(gameSaved, pid) + } } - var procList []procRSS - for _, p := range procs { + var list []procInfo + for i := range procs { + p := &procs[i] if p.PID == myPID || p.PID == 0 { continue } @@ -434,67 +379,56 @@ func main() { 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 { - closeH(h) if _, ok := blocked[p.PID]; !ok { logf("GetProcessMemoryInfo(%d) error: %v", p.PID, e) } continue } - closeH(h) - rssBytes := uint64(m.WorkingSetSize) - procList = append(procList, procRSS{p, rssBytes}) + p.RSS = uint64(m.WorkingSetSize) + list = append(list, *p) - if gameMem > 0 && rssBytes >= gameMem { + if gameMem > 0 && p.RSS >= gameMem { if _, ok := gameProcs[p.PID]; !ok { gameProcs[p.PID] = p.Name + logf("GAME DETECT %s (PID %d) RSS=%s", p.Name, p.PID, formatMemSize(p.RSS)) } } } hasGame := gameMem > 0 && len(gameProcs) > 0 - for _, pr := range procList { - p := pr.info - rssBytes := pr.rss - isBlocked := false - if _, ok := blocked[p.PID]; ok { - isBlocked = true - } + for i := range list { + p := &list[i] + _, isBlocked := blocked[p.PID] + _, isGame := gameProcs[p.PID] - if _, ok := gameProcs[p.PID]; ok { - if hasGame { - if isBlocked { - logf("GAME %s (PID %d) RSS=%s — tracked as game (blocked)", p.Name, p.PID, formatMemSize(rssBytes)) - continue - } - h, err := openProc(p.PID, ProcessQueryLimitedInformation) - if err != nil { - continue - } - origPrio, prioErr := curPrioWithHandle(h) - if prioErr != nil { - closeH(h) - continue - } - if origPrio == PriorityClassHigh { - closeH(h) - continue - } - if *dryRun { - closeH(h) - logf("[DRY-RUN] %s (PID %d) RSS=%s — would set HIGH (game)", - p.Name, p.PID, formatMemSize(rssBytes)) - } else { - if trySetPrio(h, p.PID, p.Name, PriorityClassHigh) { - logf("GAME %s (PID %d) RSS=%s -> HIGH", - p.Name, p.PID, formatMemSize(rssBytes)) - } else { - logf("GAME %s (PID %d) RSS=%s — tracked as game (priority change failed)", - p.Name, p.PID, formatMemSize(rssBytes)) - } - } + 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 } @@ -503,33 +437,55 @@ func main() { 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 } - origPrio, prioErr := curPrioWithHandle(h) - if prioErr != nil { - closeH(h) + cur, _, _ := procGetPriority.Call(uintptr(h)) + closeH(h) + if cur == 0 || uint32(cur) == PriorityClassIdle { continue } - if origPrio == PriorityClassIdle { - closeH(h) - continue - } - origStr := prioName(origPrio) - if *dryRun { - closeH(h) - logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set IDLE (game)", - p.Name, p.PID, formatMemSize(rssBytes), origStr) + 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 trySetPrio(h, p.PID, p.Name, PriorityClassIdle) { - if _, saved := gameSaved[p.PID]; !saved { - gameSaved[p.PID] = savedPrio{name: p.Name, prio: origPrio} - } - delete(promoted, p.PID) - logf("GAME %s (PID %d) RSS=%s, %s -> IDLE", - p.Name, p.PID, formatMemSize(rssBytes), origStr) + 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 } @@ -537,54 +493,29 @@ func main() { if isBlocked { continue } - h, err := openProc(p.PID, ProcessQueryLimitedInformation) if err != nil { continue } - - if rssBytes < mem { - origPrio, prioErr := curPrioWithHandle(h) - if prioErr == nil && isAboveNormal(origPrio) { - if *dryRun { - closeH(h) - logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set NORMAL", - p.Name, p.PID, formatMemSize(rssBytes), prioName(origPrio)) - } else { - if trySetPrio(h, p.PID, p.Name, PriorityClassNormal) { - delete(promoted, p.PID) - logf("DEMOTE %s (PID %d) RSS=%s, %s -> NORMAL", - p.Name, p.PID, formatMemSize(rssBytes), prioName(origPrio)) - } - } - } else { - closeH(h) - } + cur, _, _ := procGetPriority.Call(uintptr(h)) + closeH(h) + if cur != 0 && uint32(cur) == PriorityClassHigh { continue } - - origPrio, prioErr := curPrioWithHandle(h) - if prioErr != nil { - closeH(h) - continue + origStr := "" + if cur != 0 { + origStr = prioName(uint32(cur)) } - if origPrio == PriorityClassHigh { - closeH(h) - continue - } - origStr := prioName(origPrio) - if *dryRun { - closeH(h) - logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH", - p.Name, p.PID, formatMemSize(rssBytes), origStr) + logf("[DRY-RUN] %s (PID %d) RSS=%s, priority=%s — would set HIGH", p.Name, p.PID, formatMemSize(p.RSS), origStr) continue } - - if trySetPrio(h, p.PID, p.Name, PriorityClassHigh) { - promoted[p.PID] = p.Name - logf("PROMOTE %s (PID %d) RSS=%s, %s -> HIGH", - p.Name, p.PID, formatMemSize(rssBytes), origStr) + 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) } } @@ -598,8 +529,7 @@ func main() { gameMode = false logf("GAME MODE OFF") for pid, sv := range gameSaved { - cur, ok := pidMap[pid] - if !ok || cur != sv.name { + if cur, ok := pidMap[pid]; !ok || cur != sv.name { delete(gameSaved, pid) continue } @@ -610,6 +540,7 @@ func main() { 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)