1
Files
autoPriority/TZ.md
T
lzrdblzzrd da13a6d549 Demote promoted processes when RSS drops below threshold
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.
2026-06-23 08:58:11 +03:00

24 KiB
Raw Blame History

Техническое задание: 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 = trueGAME 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).