A high-performance Windows process management service written in Rust that automatically applies CPU affinity, priority, I/O priority, and memory priority rules to running processes based on configuration files.
AffinityServiceRust 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
- 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
AffinityServiceRust.exe -config my_config.ini -console
# Run with admin elevation (recommended for full functionality)
powershell -Command "Start-Process -FilePath './AffinityServiceRust.exe' -Verb RunAs -Wait"
# Show all available options
AffinityServiceRust.exe -helpall| Feature | Description |
|---|---|
| Process Priority | Set priority class: Idle, BelowNormal, Normal, AboveNormal, High, Realtime |
| 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, AffinityServiceRust 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:affinity:cpuset:prime_cpus[@prefixes]:io_priority:memory_priority:ideal[@prefixes]:grade
See ProcessConfig for the parsed representation.
| 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:
AffinityServiceRust.exe -validate -config my_config.iniPreview changes without applying them:
AffinityServiceRust.exe -dryrun -noUAC -config test.iniFind processes not covered by your config:
AffinityServiceRust.exe -find -blacklist blacklist.iniConvert Process Lasso config format:
AffinityServiceRust.exe -convert -in prolasso.ini -out my_config.iniAutomatically merge rules with identical settings into named group blocks:
AffinityServiceRust.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/AffinityServiceRust.exe -validateThe release binary will be at:
target/release/AffinityServiceRust.exe
AffinityServiceRust 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_config_process_level()/apply_config_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 SetProcessAffinityMask API only works within a single processor group. For >64 core systems, use CPU Sets instead.
-
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: 920d8fafb3d9e22e6078f62bbb7d8d97e7d21c4b. This give the developer a way to compare source code from the newest to understand changes.
License LICENSE.