da13a6d549
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.
632 lines
15 KiB
Go
632 lines
15 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
const (
|
|
CreateToolhelp32SnapshotProcess = 0x00000002
|
|
ProcessTerminate = 0x00000001
|
|
ProcessSetInformation = 0x00000200
|
|
ProcessQueryInformation = 0x00000400
|
|
ProcessQueryLimitedInformation = 0x00001000
|
|
PriorityClassIdle = 0x00000040
|
|
PriorityClassNormal = 0x00000020
|
|
PriorityClassHigh = 0x00000080
|
|
PriorityClassAboveNormal = 0x00008000
|
|
PriorityClassRealtime = 0x00000100
|
|
PriorityClassBelowNormal = 0x00004000
|
|
ErrorNoMoreFiles = 18
|
|
)
|
|
|
|
type processEntry32 struct {
|
|
Size uint32
|
|
CntUsage uint32
|
|
PID uint32
|
|
DefaultHeapID uintptr
|
|
ModuleID uint32
|
|
CntThreads uint32
|
|
ParentPID uint32
|
|
PrioClass int32
|
|
Flags uint32
|
|
ExeFile [260]uint16
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type processInfo struct {
|
|
PID uint32
|
|
Name string
|
|
}
|
|
|
|
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")
|
|
)
|
|
|
|
func closeH(h syscall.Handle) {
|
|
_, _, _ = procCloseHandle.Call(uintptr(h))
|
|
}
|
|
|
|
func openProc(pid uint32, acc uint32) (syscall.Handle, error) {
|
|
r, _, e := procOpenProcess.Call(uintptr(acc), 0, uintptr(pid))
|
|
if r == 0 {
|
|
return 0, fmt.Errorf("OpenProcess(%d) failed: %w", pid, e)
|
|
}
|
|
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)
|
|
r, _, e := procSetPriority.Call(uintptr(h), uintptr(cls))
|
|
if r == 0 {
|
|
return fmt.Errorf("SetPriorityClass(%d) failed: %w", pid, e)
|
|
}
|
|
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 {
|
|
return syscall.UTF16ToString(p[:i])
|
|
}
|
|
}
|
|
return syscall.UTF16ToString(p)
|
|
}
|
|
|
|
func parseMemSize(s string) (uint64, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0, fmt.Errorf("empty value")
|
|
}
|
|
var suffix byte
|
|
switch s[len(s)-1] {
|
|
case 'k', 'K':
|
|
suffix = 'K'
|
|
s = s[:len(s)-1]
|
|
case 'm', 'M':
|
|
suffix = 'M'
|
|
s = s[:len(s)-1]
|
|
case 'g', 'G':
|
|
suffix = 'G'
|
|
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
|
|
}
|
|
}
|
|
|
|
func formatMemSize(b uint64) string {
|
|
if b >= 1024*1024*1024 {
|
|
return fmt.Sprintf("%.1fGB", float64(b)/(1024*1024*1024))
|
|
}
|
|
if b >= 1024*1024 {
|
|
return fmt.Sprintf("%dMB", b/1024/1024)
|
|
}
|
|
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 ||
|
|
c == PriorityClassRealtime
|
|
}
|
|
|
|
func prioName(c uint32) string {
|
|
switch c {
|
|
case PriorityClassIdle:
|
|
return "IDLE"
|
|
case PriorityClassBelowNormal:
|
|
return "BELOW_NORMAL"
|
|
case PriorityClassNormal:
|
|
return "NORMAL"
|
|
case PriorityClassAboveNormal:
|
|
return "ABOVE_NORMAL"
|
|
case PriorityClassHigh:
|
|
return "HIGH"
|
|
case PriorityClassRealtime:
|
|
return "REALTIME"
|
|
default:
|
|
return fmt.Sprintf("0x%X", c)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func killOtherInstances() {
|
|
myPID := os.Getpid()
|
|
procs, err := allProcs()
|
|
if err != nil {
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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.")
|
|
interval := flag.Duration("interval", time.Minute, "scan interval")
|
|
dryRun := flag.Bool("dry-run", false, "log only, do not change priorities")
|
|
flag.Parse()
|
|
|
|
if *interval < 10*time.Second {
|
|
*interval = 10 * time.Second
|
|
}
|
|
|
|
mem, err := parseMemSize(*memStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: invalid -mem value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
gameMem, err := parseMemSize(*gameMemStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: invalid -game-mem value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if gameMem > 0 && gameMem <= mem {
|
|
fmt.Fprintf(os.Stderr, "autoPriority: -game-mem must be greater than -mem\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
killOtherInstances()
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
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")
|
|
}
|
|
|
|
logf := func(format string, a ...any) {
|
|
if logFile == nil {
|
|
return
|
|
}
|
|
fmt.Fprintf(logFile, "[%s] ", ts())
|
|
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)
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
promoted := make(map[uint32]string)
|
|
blocked := make(map[uint32]string)
|
|
gameProcs := make(map[uint32]string)
|
|
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 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)
|
|
}
|
|
}
|
|
logf("autoPriority stopped")
|
|
}()
|
|
|
|
myPID := uint32(os.Getpid())
|
|
ticker := time.NewTicker(*interval)
|
|
defer ticker.Stop()
|
|
|
|
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 {
|
|
logf("allProcs error: %v", err)
|
|
return
|
|
}
|
|
|
|
pidMap := make(map[uint32]string, len(procs))
|
|
for _, p := range procs {
|
|
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
|
|
}
|
|
var procList []procRSS
|
|
|
|
for _, p := range procs {
|
|
if p.PID == myPID || p.PID == 0 {
|
|
continue
|
|
}
|
|
|
|
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
|
|
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)
|
|
}
|
|
continue
|
|
}
|
|
|
|
var m processMemoryCounters
|
|
m.CBM = uint32(unsafe.Sizeof(m))
|
|
r, _, e := procGetMemInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(unsafe.Sizeof(m)))
|
|
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})
|
|
|
|
if gameMem > 0 && rssBytes >= gameMem {
|
|
if _, ok := gameProcs[p.PID]; !ok {
|
|
gameProcs[p.PID] = p.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if hasGame {
|
|
if isBlocked {
|
|
continue
|
|
}
|
|
h, err := openProc(p.PID, ProcessQueryLimitedInformation)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
origPrio, prioErr := curPrioWithHandle(h)
|
|
if prioErr != nil {
|
|
closeH(h)
|
|
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)
|
|
} 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)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
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)
|
|
}
|
|
continue
|
|
}
|
|
|
|
origPrio, prioErr := curPrioWithHandle(h)
|
|
if prioErr != nil {
|
|
closeH(h)
|
|
continue
|
|
}
|
|
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)
|
|
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 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 {
|
|
cur, ok := pidMap[pid]
|
|
if !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))
|
|
}
|
|
}
|
|
delete(gameSaved, pid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scan()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
logf("received shutdown signal")
|
|
return
|
|
case <-ticker.C:
|
|
scan()
|
|
}
|
|
}
|
|
}
|