Previously, once a process was promoted to HIGH, it stayed HIGH forever even if its RSS dropped well below the -mem threshold. Now promoted processes are demoted back to NORMAL when RSS < -mem, and removed from the promoted map. Also: added TZ.md with full technical specification of the project.
24 KiB
Техническое задание: 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 сохраняется один раз (при первом понижении), чтобы запомнить приоритет, который был до игрового режима.
Запуск программы
- Разбор флагов командной строки
- Проверка: если
game-mem > 0иgame-mem <= mem— выход с ошибкой - Если
interval < 10s— принудительно установить 10s - Уничтожение других экземпляров: получить список всех процессов, найти процессы с тем же именем файла, что и у текущего процесса, но с другим PID, и завершить их через
TerminateProcess - Подождать 100мс (ОС освобождает файловые дескрипторы)
- Удалить старый лог-файл, создать новый (в
%TEMP%\autopriority.log) - Установить собственный приоритет в IDLE
- Записать в лог параметры запуска
- Инициализировать пустые карты:
promoted,blocked,gameProcs,gameSaved - Зарегистрировать deferred-функцию восстановления (см. раздел «Завершение»)
- Выполнить первый скан немедленно
- Войти в цикл: ждать сигнала завершения ИЛИ тикера интервала, при тике — выполнять скан
Алгоритм сканирования (двухпроходный)
Каждый скан состоит из двух проходов. Первый проход собирает данные о памяти и определяет игровые процессы. Второй проход применяет приоритеты, уже зная, активен ли игровой режим. Это необходимо, чтобы игровой режим срабатывал на первом же скане, а не на следующем.
Предварительный этап: очистка карт
Перед проходами программа получает список всех процессов и строит pidMap (pid → name). Затем очищает четыре карты:
promoted: если PID исчез или имя не совпадает — удалить (процесс умер или PID перешёл другому процессу)blocked: если PID исчез — удалить (процесс умер, PID может быть переиспользован)gameProcs: если PID исчез или имя не совпадает — удалитьgameSaved: если PID исчез или имя не совпадается — удалить
Проверка имени защищает от PID reuse: если процесс умер и его PID получил другой процесс, старая запись удаляется.
Проход 1: Сбор RSS и определение игровых процессов
Для каждого процесса (кроме себя и PID 0):
-
Открыть процесс с правом
PROCESS_QUERY_LIMITED_INFORMATION(0x1000). Это узкое право, которое дают почти все процессы, включая защищённые. ИспользованиеPROCESS_QUERY_INFORMATION(0x0400) приводит к Access Denied для многих пользовательских процессов. -
Если
OpenProcessупал:- Добавить PID в
blocked(если ещё не там) - Записать в лог
BLOCK ... (added to exclusion list) - Перейти к следующему процессу (не добавлять в
procList) - Примечание: blocked процессы не попадают в
procList, но если они были добавлены вblockedна предыдущем скане и всё ещё живы, они НЕ измеряются повторно. Это допустимо, потому что blocked процессы не могут менять приоритет.
Важное исключение: если процесс УЖЕ в
blocked(добавлен ранее), то не логировать повторно — просто пропустить. - Добавить PID в
-
Вызвать
GetProcessMemoryInfoдля полученияWorkingSetSize(RSS) -
Если упало — пропустить процесс
-
Закрыть handle
-
Записать
{PID, Name, RSS}вprocList -
Если
game-mem > 0иRSS >= game-mem:- Добавить PID в
gameProcs(если ещё не там)
- Добавить PID в
После прохода 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
- Закрыть
queryHandle(handle отPROCESS_QUERY_LIMITED_INFORMATION) - Открыть процесс заново с правом
PROCESS_SET_INFORMATION(0x0200)- Разделение прав: многие процессы дают
QUERYно не даютSETв комбинации
- Разделение прав: многие процессы дают
- Если
OpenProcessупал:- Добавить PID в
blocked - Записать
BLOCK ... open SetInformation failed - Вернуть
false
- Добавить PID в
- Вызвать
SetPriorityClass(handle, targetClass) - Закрыть handle (через defer)
- Если
SetPriorityClassупал:- Добавить PID в
blocked - Записать
BLOCK ... SetPriorityClass failed - Вернуть
false
- Добавить PID в
- Подождать 50мс (плавная смена приоритетов, предотвращает статтеры)
- Вернуть
true
Функция setPrio (для восстановления)
setPrio(pid, targetClass) → error
- Открыть с
PROCESS_SET_INFORMATION - Вызвать
SetPriorityClass - Закрыть handle
- Вернуть ошибку или nil
Завершение программы (deferred)
При получении сигнала SIGINT/SIGTERM:
- Получить свежий список всех процессов
- Построить
restoreMap(pid → name) - Восстановить
gameSaved: для каждого PID — проверить имя, установить оригинальный приоритет, записатьRESTORE ... -> originalPriority - Восстановить
promoted: для каждого PID — проверить имя, установить NORMAL, записатьRESTORE ... -> NORMAL - Восстановить
gameProcs: для каждого PID — проверить имя, установить NORMAL, записатьRESTORE ... -> NORMAL - Записать
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— закрытие handleTerminateProcess— завершение других экземпляров
Права доступа
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: Обычная работа
- Запуск программы
- Chrome потребляет 600MB (≥ 512MB) →
PROMOTE Chrome.exe -> HIGH, добавляется вpromoted - Chrome сбрасывает память до 300MB (< 512MB) →
DEMOTE Chrome.exe -> NORMAL, удаляется изpromoted - Discord потребляет 700MB →
PROMOTE Discord.exe -> HIGH - Discord сбрасывает до 200MB →
DEMOTE Discord.exe -> NORMAL
Сценарий 2: Понижение высокого приоритета
wininit.exeзапущен с HIGH, потребляет 6MB (< 512MB порога)- Программа видит: RSS < mem, приоритет ABOVE_NORMAL →
DEMOTE wininit.exe -> NORMAL - Это применяется ко всем процессам (включая ранее повышенные, если их RSS упал ниже порога)
Сценарий 3: Игровой режим
- Запуск RustClient, потребление растёт с 1GB до 8GB
- На скане, где RSS ≥ 4GB: RustClient добавляется в
gameProcs hasGame = true- RustClient →
trySetPrioHIGH → если античит блокирует →BLOCK RustClient.exe+GAME RustClient.exe — tracked as game (priority change failed) - RustClient остаётся в
gameProcs(важно: блокировка SetPriorityClass не удаляет из gameProcs) - Все остальные процессы → IDLE (
GAME Chrome.exe -> IDLE, etc.) GAME MODE ON- При следующем скане RustClient всё ещё в
gameProcs, остальные процессы уже IDLE — пропускаются
Сценарий 4: Выход из игрового режима
- RustClient закрыт
gameProcsочищен (PID исчез)newHasGame = false,gameMode = true→ переходgameMode = false,GAME MODE OFF- Для каждого процесса в
gameSaved: восстановить оригинальный приоритетRESTORE Chrome.exe -> NORMAL(был NORMAL до игрового режима)RESTORE Discord.exe -> HIGH(был HIGH, потому что был вpromoted)
gameSavedочищен- Нормальный режим возобновляется: процессы с RSS ≥ mem повышаются, с RSS < mem и выше NORMAL — понижаются
Сценарий 5: Заблокированный процесс становится игрой
- RustClient стартует, потребляет 3.9GB
- RSS ≥ 512MB (mem), не в
promoted→ попытка PROMOTE → античит блокирует →BLOCK RustClient.exe(добавлен вblocked) - RustClient дорастает до 8GB
- На следующем скане: RustClient в
blocked, но проход 1 всё равно пытается его открыть OpenProcess(QUERY_LIMITED)для blocked процесса — если упадёт, пропустит. Если пройдёт — RSS измеряется- Если RustClient в
blockedиз-заSetPriorityClass(неOpenProcess), тоOpenProcess(QUERY_LIMITED)пройдёт, RSS измерится, и он попадёт вgameProcs hasGame = true,GAME MODE ON- В проходе 2: RustClient в
gameProcs, ноisBlocked = true→GAME RustClient.exe — tracked as game (blocked), приоритет не меняется - Остальные процессы понижаются до IDLE
Сценарий 6: PID reuse
- Процесс A (PID 1234, "chrome.exe") повышен, в
promoted - Процесс A умирает
- Новый процесс B (PID 1234, "notepad.exe") запускается
- На следующем скане:
pidMap[1234] = "notepad.exe", ноpromoted[1234] = "chrome.exe" - Имя не совпадает →
delete(promoted, 1234) - Процесс B обрабатывается как новый процесс
Сценарий 7: Системные процессы
svchost.exe(PID 932) —OpenProcess(QUERY_LIMITED)возвращает Access Denied- Добавляется в
blocked, логируетсяBLOCK - На следующем скане: PID 932 всё ещё в
blocked,OpenProcessснова падает, но лог НЕ пишется (процесс уже вblocked) - Когда процесс умирает: 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).