Skip to content

Prohect/AffinityServiceRust

Repository files navigation

AffinityServiceRust

GitHub Downloads (all assets, all releases)

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.

Overview

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

Documentation

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

Quick Start

  1. Build or download the release binary
  2. Download config.ini and blacklist.ini to your working directory
  3. Edit config.ini to match your CPU topology (see Configuration section)
  4. 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

Features

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

Prime Thread Scheduling

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+offset format
  • Kernel time and user time
  • Thread priority, base priority, context switches
  • Thread state and wait reason

Ideal Processor Assignment

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 (one QueryThreadCycleTime per thread, capped at the logical-CPU count)
  • Applies MIN_ACTIVE_STREAK, ENTRY_THRESHOLD, and KEEP_THRESHOLD constants — identical to prime thread scheduling:
    • Pass 1 (keep): threads already assigned and still above KEEP_THRESHOLD retain their slot with zero write syscalls
    • Pass 2 (promote): new candidates above ENTRY_THRESHOLD whose streak counter has reached MIN_ACTIVE_STREAK receive a slot
  • Lazy set: if a newly-selected thread's current ideal processor is already in the free CPU pool, SetThreadIdealProcessorEx is 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.

Ideal Processor Reset

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:1

How 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.

Configuration

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.

CPU Specification Formats

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.

Rule Grades

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

Priority Levels

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.

Command Line Options

Basic Options

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 -

Operating Modes

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

Debug Options

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.

Tools

Config Validation

Validate your configuration before running:

AffinityServiceRust.exe -validate -config my_config.ini

Dry Run Mode

Preview changes without applying them:

AffinityServiceRust.exe -dryrun -noUAC -config test.ini

Process Discovery

Find processes not covered by your config:

AffinityServiceRust.exe -find -blacklist blacklist.ini

Config Conversion

Convert Process Lasso config format:

AffinityServiceRust.exe -convert -in prolasso.ini -out my_config.ini

Config Auto-Grouping

Automatically merge rules with identical settings into named group blocks:

AffinityServiceRust.exe -autogroup -in config.ini -out config_grouped.ini

Rules 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:4

Output:

grp_0 { cmd.exe: explorer.exe: notepad.exe }:none:*a:*e:0:none:none:0:4

The 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.

Privileges and Capabilities

What You Need to Know

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.

Building

Requirements

  • Rust toolchain (edition 2021 or 2024)
  • Windows SDK
  • Visual Studio Build Tools (for MSVC) or MinGW (for GNU toolchain)

Build Commands

# Release build
cargo build --release

# Run tests
cargo test

# Validate config
cargo build --release && ./target/release/AffinityServiceRust.exe -validate

Output

The release binary will be at:

target/release/AffinityServiceRust.exe

How It Works

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.

Known Behaviours

  1. [SET_AFFINITY][ACCESS_DENIED] on own child processes: When this service spawns a child process (e.g. conhost.exe attached 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.exe
    

    to .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.

  2. [OPEN][ACCESS_DENIED] per-thread deduplication: When apply_config_process_level()/apply_config_thread_level() fails to open a process or thread due to ACCESS_DENIED (or any other error), the error is written to .find.log exactly 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. several svchost.exe processes 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.

Known Limitations

  1. CPU Affinity ≤64 cores: The legacy SetProcessAffinityMask API only works within a single processor group. For >64 core systems, use CPU Sets instead.

  2. Multi-group/NUMA systems: This project has not been tested on multi-processor-group or NUMA systems. The ideal processor assignment currently assigns processors within processor group 0 only. Systems with >64 logical processors or multiple CPU groups may experience unexpected behavior.

  3. Protected processes: Processes like csrss.exe and smss.exe cannot be modified, even with admin privileges.

  4. Console output with elevation: When using -console with UAC elevation, the elevated process spawns in a new window that closes immediately. Use log file output instead.

  5. Thread start address resolution: Requires admin elevation with SeDebugPrivilege. Without elevation, start addresses show as 0x0.

  6. Timer resolution: The system timer resolution affects loop precision. Very low values (<1ms) may impact system stability.

Contributions

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 LICENSE.