A high-performance Windows process management service written in Rust that automatically applies CPU affinity, priority, I/O priority, memory priority, and kernel-enforced job object affinity rules to running processes based on configuration files.
⚠️ Breaking Change in v2.0: The config rule syntax now includes ajob_affinityfield (3rd field) betweenpriorityandaffinity. Rules from v1.x (formatname:priority:affinity:...) are not compatible — add0after priority to skip job object affinity:name:priority:0:affinity:.... See Configuration.
ProcGovernor continuously monitors running processes and applies customized scheduling policies based on rules defined in configuration files. It supports:
- Process Priority Management: Set process priority class (Idle to Real-time) — see Priority Levels
- Job Object Affinity: Kernel-enforced CPU affinity via Windows Job Objects (prevents processes and children from running on excluded CPUs) — see Job Object Affinity Limits
- CPU Affinity: Hard-pin processes to specific logical processors (legacy ≤64 core systems) — see
apply_affinity() - CPU Sets: Soft CPU preferences across all processor groups (modern >64 core systems) — see
apply_process_default_cpuset() - Prime Thread Scheduling: Dynamically identify and assign CPU-intensive threads to designated "prime" cores — see Prime Thread Scheduling section below
- Ideal Processor Assignment: Static thread-to-CPU assignment for top N busiest threads — see Ideal Processor Assignment section below
- I/O Priority Control: Control disk I/O scheduling priority — see
apply_io_priority() - Memory Priority Control: Adjust memory page priority for process working set — see
apply_memory_priority() - Hot Reload: Automatically detect and apply config file changes
- Rule Grades: Control application frequency per process rule — see Rule Grades
| Topic | Documentation |
|---|---|
| Architecture | docs/main.md - Main loop and entry point |
| Configuration | docs/config.md - Config parsing and CPU specifications |
| Apply Logic | docs/apply.md - How settings are applied to processes |
| Scheduler | docs/scheduler.md - Prime thread scheduler implementation |
| CLI Options | docs/cli.md - Command-line arguments |
| Priority Levels | docs/priority.md - Priority enum definitions |
| Windows API | docs/winapi.md - Windows API wrappers |
| Logging | docs/logging.md - Error tracking and logging |
| ETW Monitoring | docs/event_trace.md - ETW-based reactive process monitoring |
- Build or download the release binary
- Download
config.iniandblacklist.inito your working directory - Edit
config.inito match your CPU topology (see Configuration section) - Run with appropriate privileges:
# Basic usage with console output
ProcGovernor.exe -config my_config.ini -console
# Run with admin elevation (recommended for full functionality)
powershell -Command "Start-Process -FilePath './ProcGovernor.exe' -Verb RunAs -Wait"
# Show all available options
ProcGovernor.exe -helpall| Feature | Description |
|---|---|
| Process Priority | Set priority class: Idle, BelowNormal, Normal, AboveNormal, High, Realtime |
| Job Object Affinity | Kernel-enforced CPU affinity via Windows Job Objects (prevents child inheritance bypass). See apply_job_object_affinity() |
| CPU Affinity | Legacy mask-based affinity (≤64 cores, SetProcessAffinityMask) |
| CPU Sets | Modern soft CPU preferences (unlimited cores, SetProcessDefaultCpuSets) |
| Prime Thread Scheduling | Dynamic thread-to-core assignment using hysteresis-based algorithm |
| Ideal Processor Assignment | Hysteresis-based ideal-processor assignment using the same algorithm and constants (MIN_ACTIVE_STREAK, ENTRY_THRESHOLD, KEEP_THRESHOLD) as Prime Thread Scheduling |
| I/O Priority | VeryLow, Low, Normal, High (requires admin for High) |
| Memory Priority | VeryLow, Low, Medium, BelowNormal, Normal |
| Timer Resolution | Configure system timer resolution for tighter loops |
| Hot Reload | Auto-reload config when files change |
| ETW Process Monitoring | Real-time process start/stop detection via Event Tracing for Windows |
| Rule Grades | Control how often each rule is applied |
see also: Timer Resolution Bench
The prime thread scheduler dynamically identifies the most CPU-intensive threads and assigns them to designated "prime" cores using Windows CPU Sets:
Algorithm:
- Monitors thread CPU cycle consumption at configurable intervals via
prefetch_all_thread_cycles() - Applies hysteresis to prevent thrashing:
- Entry threshold: Thread must exceed configured % of max cycles to become a candidate
- Keep threshold: Once promoted, thread stays prime if above configured % of max cycles
- Active streak: Requires consecutive active intervals before promotion
- Filters low-activity threads automatically
- Supports multi-segment CPU assignment: different modules can use different core sets
- Per-module thread priority control (explicit or auto-boost)
- Thread tracking mode: logs detailed statistics when process exits
See apply_prime_threads() and the scheduler module for implementation details.
Thread Tracking Output: When a tracked process exits, detailed statistics are logged for top N threads:
- Thread ID and total CPU cycles consumed
- Start address resolved to
module.dll+offsetformat - Kernel time and user time
- Thread priority, base priority, context switches
- Thread state and wait reason
An optional ideal specification assigns preferred processors to the most CPU-active threads, using the same hysteresis-based filter as prime thread scheduling.
Algorithm:
- Per-iteration cycle data is provided by the shared
prefetch_all_thread_cycles()pass (oneQueryThreadCycleTimeper thread, capped at the logical-CPU count) - Applies
MIN_ACTIVE_STREAK,ENTRY_THRESHOLD, andKEEP_THRESHOLDconstants — identical to prime thread scheduling:- Pass 1 (keep): threads already assigned and still above
KEEP_THRESHOLDretain their slot with zero write syscalls - Pass 2 (promote): new candidates above
ENTRY_THRESHOLDwhose streak counter has reachedMIN_ACTIVE_STREAKreceive a slot
- Pass 1 (keep): threads already assigned and still above
- Lazy set: if a newly-selected thread's current ideal processor is already in the free CPU pool,
SetThreadIdealProcessorExis skipped — the slot is claimed in-place - Demotion: threads that fall out of selection have their original ideal processor restored
- Each assignment log line includes
start=module+offset(e.g.start=cs2.exe+0xEA60) - Multi-rule syntax allows different CPU sets for different module prefixes
See apply_ideal_processors() for implementation details.
When a process's CPU affinity is changed, ProcGovernor automatically resets per-thread ideal processor assignments to prevent Windows from clamping threads to narrow CPU ranges.
This can also be enabled for CPU set changes by prefixing the cpuset field with @:
# After setting CPU set to 0-3, redistribute thread ideal processors across CPUs 0-3
game.exe:normal:*a:@0-3:*p:normal:normal:1How it works:
- Collects threads' total CPU time and sorts in descending order
- Assigns ideal processors round-robin across the configured CPUs
- Applies a small random shift to avoid clumping
- Runs automatically after affinity changes, or after CPU set changes when
@prefix is used
See reset_thread_ideal_processors() for implementation details.
The configuration file uses INI-like format with sections for constants, aliases, and rules.
Process rules follow this format:
process_name:priority:job_affinity:affinity:cpuset:prime_cpus[@prefixes]:io_priority:memory_priority:ideal[@prefixes]:grade
Migrating from v1.x: Add
0as the 3rd field to skip job object affinity. Example:game.exe:high:*a:0:0:normal:none:0:1→game.exe:high:0:*a:0:0:normal:none:0:1.
See ProcessLevelConfig for the parsed representation.
The job_affinity field (3rd field, v2.0+) applies a kernel-enforced CPU affinity via Windows Job Objects. Unlike regular affinity (which is inherited by child processes but can be overridden — a child can call SetProcessAffinityMask on itself to escape the parent's restriction), job object affinity limits are enforced by the kernel and cannot be bypassed by the process or any of its children (escaping would require job object API access with proper token and privileges).
Key differences from regular affinity:
| Property | job_affinity (Job Object) |
affinity (SetProcessAffinityMask) |
|---|---|---|
| Enforcement | Kernel-level | Process-level |
| Child inheritance | Children inherit the limit AND cannot change it | Children inherit but can override via SetProcessAffinityMask |
| Can be bypassed | No (kernel rejects SetProcessAffinityMask; escape requires job object API + privileges) |
Yes (child can call SetProcessAffinityMask on itself) |
| Persists after exit | Yes (job object is named) | No |
Usage: Set job_affinity to restrict processes to specific CPUs, then use affinity for a potentially tighter subset. Use 0 to skip (no job object).
# Job object enforces E-cores only; process affinity also set to E-cores:
chrome.exe:normal:*ecore:*ecore:0:0:low:none:0:1
# Job object enforces P-cores; process sees all CPUs (soft affinity unrestricted):
game.exe:high:*pcore:*all:0:0:normal:none:0:1
# No job object (legacy behavior):
notepad.exe:normal:0:*pcore:0:0:none:none:0:1See apply_job_object_affinity() and JobObjectManager for implementation details.
| Format | Example | Description |
|---|---|---|
| Range | 0-7 |
Cores 0 through 7 |
| Multiple ranges | 0-7;64-71 |
For systems with >64 logical processors |
| Individual | 0;2;4;6 |
Specific cores |
| Single | 5 |
Single core (NOT a bitmask) |
| Hex mask | 0xFF |
Legacy format (≤64 cores only) |
| Alias | *pcore |
Reference to predefined CPU alias |
Important: Plain numbers mean core indices, not bitmasks. Use 0-7 for cores 0-7, NOT 7.
The grade field (default: 1) controls how often a rule is applied:
| Grade | Frequency | Use Case |
|---|---|---|
1 |
Every loop | Critical processes (games, real-time apps) |
2 |
Every 2nd loop | Semi-critical processes |
5 |
Every 5th loop | Background utilities |
10 |
Every 10th loop | Rarely changing processes |
Process Priority: none, idle, below normal, normal, above normal, high, real time
Thread Priority: none, idle, lowest, below normal, normal, above normal, highest, time critical
I/O Priority: none, very low, low, normal, high (admin required for high)
Memory Priority: none, very low, low, medium, below normal, normal
For detailed configuration syntax, including aliases, groups, prime scheduling, ideal assignment, constants, and examples, see parse_and_insert_rules.
| Option | Description | Default |
|---|---|---|
-help |
Show basic help | - |
-helpall |
Show detailed help with examples | - |
-console |
Output to console instead of log files | Log to file |
-config <file> |
Custom config file | config.ini |
-blacklist <file> |
Blacklist file for -find mode |
- |
-noUAC |
Run without requesting admin privileges | Request elevation |
-interval <ms> |
Check interval in milliseconds (min: 16) | 5000 |
-resolution <ticks> |
Timer resolution (1 tick = 0.0001ms), 0 = don't set |
- |
| Mode | Description |
|---|---|
-convert |
Convert Process Lasso config (-in <file> -out <file>) |
-autogroup |
Auto-group rules with identical settings into named groups (-in <file> -out <file>) |
-find |
Log unmanaged processes with default affinity |
-validate |
Validate config file syntax without running |
-processlogs |
Process logs to find new processes and search paths |
-dryrun |
Show what would be changed without applying |
| Option | Description |
|---|---|
-loop <count> |
Number of loops to run (default: infinite) |
-logloop |
Log message at start of each loop |
-noDebugPriv |
Don't request SeDebugPrivilege |
-noIncBasePriority |
Don't request SeIncreaseBasePriorityPrivilege |
-noETW |
Don't start ETW process monitoring |
continuous_process_level_apply |
Re-apply process-level settings every loop instead of once per PID |
See cli.md for complete CLI documentation.
Validate your configuration before running:
ProcGovernor.exe -validate -config my_config.iniPreview changes without applying them:
ProcGovernor.exe -dryrun -noUAC -config test.iniFind processes not covered by your config:
ProcGovernor.exe -find -blacklist blacklist.iniConvert Process Lasso config format:
ProcGovernor.exe -convert -in prolasso.ini -out my_config.iniAutomatically merge rules with identical settings into named group blocks:
ProcGovernor.exe -autogroup -in config.ini -out config_grouped.iniRules that share the exact same rule string are collected into a grp_N { } block, with members sorted alphabetically. Groups that fit within 128 characters are written on a single line; larger groups wrap across multiple lines with : -separated members, each line kept under 128 characters.
Input:
explorer.exe:none:*a:*e:0:none:none:0:4
cmd.exe:none:*a:*e:0:none:none:0:4
notepad.exe:none:*a:*e:0:none:none:0:4Output:
grp_0 { cmd.exe: explorer.exe: notepad.exe }:none:*a:*e:0:none:none:0:4The preamble (@constants, *aliases, and leading comments) is preserved verbatim. Per-process inline comments between rules are dropped during regrouping.
See sort_and_group_config() for implementation.
| Target Process | No Admin | Admin | Notes |
|---|---|---|---|
| Same-user processes | ✅ Full | ✅ Full | Works without elevation |
| Elevated processes | ❌ | ✅ Full | Needs admin |
| SYSTEM processes | ❌ | ✅ Full | Needs admin |
| Protected processes | ❌ | ❌ | Even admin cannot modify |
| Rule | No Admin | Admin | Notes |
|---|---|---|---|
| Process Priority | ✅ | ✅ | All levels work |
| CPU Affinity | ✅ | ✅ | ≤64 cores only |
| CPU Sets | ✅ | ✅ | Works on >64 cores |
| Prime Scheduling | ✅ | ✅ | Thread-level CPU sets |
| I/O Priority - High | ❌ | ✅ | Requires admin (SeIncreaseBasePriorityPrivilege) |
| Memory Priority | ✅ | ✅ | All levels work |
Recommendation: Run with admin privileges for full functionality, especially for I/O priority high and managing SYSTEM processes.
- Rust toolchain (edition 2021 or 2024)
- Windows SDK
- Visual Studio Build Tools (for MSVC) or MinGW (for GNU toolchain)
# Release build
cargo build --release
# Run tests
cargo test
# Validate config
cargo build --release && ./target/release/ProcGovernor.exe -validateThe release binary will be at:
target/release/ProcGovernor.exe
ProcGovernor continuously monitors running processes and applies configured rules for process priority, CPU affinity/sets, I/O/memory priority, prime thread scheduling, and ideal processor assignment. It uses ETW for reactive process detection and supports hot reloading of config files.
For detailed architecture and implementation, see docs/main.md.
-
[SET_AFFINITY][ACCESS_DENIED]on own child processes: When this service spawns a child process (e.g.conhost.exeattached by a scheduled task runner or a UAC re-launch) that happens to match one of the configured rules, it will attempt to apply affinity and log a line such as:apply_config: [SET_AFFINITY][ACCESS_DENIED] 6976-conhost.exeto
.find.log. This is intentional — the service applies rules to every matching process name it sees in the snapshot, including its own short-lived children. The child is terminated before the main loop starts (see startup cleanup), so this entry will appear at most once per run and can be safely ignored. -
[OPEN][ACCESS_DENIED]per-thread deduplication: Whenapply_process_level()/apply_thread_level()fails to open a process or thread due toACCESS_DENIED(or any other error), the error is written to.find.logexactly once per unique(pid, tid, process_name, operation)combination. After each snapshot, the deduplication map is reconciled: entries whose PID has exited or been reused for a different executable are evicted, so if the same process name later re-appears under a new PID the error fires once more. Multiple concurrent instances of the same executable (e.g. severalsvchost.exeprocesses with different PIDs) are tracked independently — one denied instance never silences errors for any other PID sharing the same name.
See is_new_error() for error deduplication implementation.
-
CPU Affinity ≤64 cores: The legacy
SetProcessAffinityMaskAPI only works within a single processor group. For >64 core systems, use CPU Sets instead. -
Job Object Affinity ≤64 cores: The
JOB_OBJECT_LIMIT_AFFINITYmask is a 64-bit value, limiting kernel-enforced job object affinity to a single processor group (≤64 logical processors). Jobs will not be created for CPU specs where all indices are ≥64. -
Multi-group/NUMA systems: This project has not been tested on multi-processor-group or NUMA systems. The
idealprocessor assignment currently assigns processors within processor group 0 only. Systems with >64 logical processors or multiple CPU groups may experience unexpected behavior. -
Protected processes: Processes like
csrss.exeandsmss.execannot be modified, even with admin privileges. -
Console output with elevation: When using
-consolewith UAC elevation, the elevated process spawns in a new window that closes immediately. Use log file output instead. -
Thread start address resolution: Requires admin elevation with SeDebugPrivilege. Without elevation, start addresses show as
0x0. -
Timer resolution: The system timer resolution affects loop precision. Very low values (<1ms) may impact system stability.
Issues and pull requests are welcome.
Please update the commit SHA here when you try to update this README: 29c0140. This give the developer a way to compare source code from the newest to understand changes.
License LICENSE.