Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions Platforms/Windows/CpuSetMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* ThreadPilot - Advanced Windows Process and Power Plan Manager
* Copyright (C) 2025 Prime Build
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace ThreadPilot.Platforms.Windows
{
using System;
using System.Collections.Generic;
using System.Linq;
using ThreadPilot.Models;

internal sealed class CpuSetMapping
{
private readonly IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor;
private readonly IReadOnlyDictionary<uint, ProcessorRef> processorsByCpuSetId;

private CpuSetMapping(
IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor,
IReadOnlyDictionary<uint, ProcessorRef> processorsByCpuSetId)
{
this.cpuSetIdsByProcessor = cpuSetIdsByProcessor;
this.processorsByCpuSetId = processorsByCpuSetId;
}

public static CpuSetMapping Empty { get; } = new(
new Dictionary<ProcessorRef, uint>(),
new Dictionary<uint, ProcessorRef>());

public bool IsEmpty => this.cpuSetIdsByProcessor.Count == 0;

public static CpuSetMapping Create(IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor)
{
ArgumentNullException.ThrowIfNull(cpuSetIdsByProcessor);

var forwardMap = cpuSetIdsByProcessor
.OrderBy(kvp => kvp.Key.GlobalIndex)
.ThenBy(kvp => kvp.Key.Group)
.ThenBy(kvp => kvp.Key.LogicalProcessorNumber)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

var inverseMap = forwardMap
.GroupBy(kvp => kvp.Value)
.ToDictionary(
group => group.Key,
group => group
.Select(kvp => kvp.Key)
.OrderBy(processor => processor.GlobalIndex)
.ThenBy(processor => processor.Group)
.ThenBy(processor => processor.LogicalProcessorNumber)
.First());

return new CpuSetMapping(forwardMap, inverseMap);
}

public static ProcessorRef CreateProcessorRef(ushort group, byte logicalProcessorNumber)
{
return new ProcessorRef(group, logicalProcessorNumber, (group * 64) + logicalProcessorNumber);
}

public bool TryGetCpuSetId(ProcessorRef processor, out uint cpuSetId)
{
return this.cpuSetIdsByProcessor.TryGetValue(processor, out cpuSetId);
}

public bool TryGetProcessorRef(uint cpuSetId, out ProcessorRef processor)
{
return this.processorsByCpuSetId.TryGetValue(cpuSetId, out processor);
}

public IReadOnlyList<uint> ResolveCpuSetIds(CpuSelection selection)
{
ArgumentNullException.ThrowIfNull(selection);

if (selection.CpuSetIds.Count > 0)
{
return selection.CpuSetIds
.Distinct()
.OrderBy(cpuSetId => cpuSetId)
.ToList();
}

return selection.LogicalProcessors
.Select(processor => this.TryGetCpuSetId(processor, out var cpuSetId) ? (uint?)cpuSetId : null)
.Where(cpuSetId => cpuSetId.HasValue)
.Select(cpuSetId => cpuSetId!.Value)
.Distinct()
.OrderBy(cpuSetId => cpuSetId)
.ToList();
}

public IReadOnlyList<uint> ResolveLegacyAffinityMask(long affinityMask, int logicalProcessorCount)
{
var unsignedMask = unchecked((ulong)affinityMask);
var maxLegacyBits = Math.Min(Math.Max(logicalProcessorCount, 0), 64);
var cpuSetIds = new List<uint>();

for (var bit = 0; bit < maxLegacyBits; bit++)
{
if ((unsignedMask & (1UL << bit)) == 0)
{
continue;
}

var processor = CreateProcessorRef(0, (byte)bit);
if (this.TryGetCpuSetId(processor, out var cpuSetId))
{
cpuSetIds.Add(cpuSetId);
}
}

return cpuSetIds
.Distinct()
.OrderBy(cpuSetId => cpuSetId)
.ToList();
}
}
}
13 changes: 12 additions & 1 deletion Platforms/Windows/IProcessCpuSetHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace ThreadPilot.Platforms.Windows
{
using System;
using ThreadPilot.Models;

/// <summary>
/// Interface for handling CPU Set operations on a specific process.
Expand All @@ -35,12 +36,23 @@ public interface IProcessCpuSetHandler : IDisposable

/// <summary>
/// Applies a CPU affinity mask to the process using CPU Sets.
/// This legacy path is valid only for single-processor-group systems with up to
/// 64 logical processors. It will be superseded by <see cref="ApplyCpuSelection"/>
/// for topology-aware CPU Set selection.
/// </summary>
/// <param name="affinityMask">The affinity mask where each bit represents a logical processor.</param>
/// <param name="clearMask">If true, clears the CPU Set (allows all cores); if false, applies the mask.</param>
/// <returns>True if the operation succeeded, false otherwise.</returns>
bool ApplyCpuSetMask(long affinityMask, bool clearMask = false);

/// <summary>
/// Applies a topology-aware CPU selection to the process using CPU Sets.
/// </summary>
/// <param name="selection">The CPU selection to apply. Ignored and allowed to be null when <paramref name="clearSelection"/> is true.</param>
/// <param name="clearSelection">If true, clears the CPU Set selection and ignores <paramref name="selection"/>.</param>
/// <returns>True if the operation succeeded, false otherwise.</returns>
bool ApplyCpuSelection(CpuSelection? selection, bool clearSelection = false);

/// <summary>
/// Gets the average CPU usage for this process.
/// </summary>
Expand All @@ -53,4 +65,3 @@ public interface IProcessCpuSetHandler : IDisposable
bool IsValid { get; }
}
}

89 changes: 89 additions & 0 deletions Platforms/Windows/IProcessCpuSetNativeApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* ThreadPilot - Advanced Windows Process and Power Plan Manager
* Copyright (C) 2025 Prime Build
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace ThreadPilot.Platforms.Windows
{
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

internal interface IProcessCpuSetNativeApi
{
SafeProcessHandle OpenProcess(ProcessAccessFlags access, bool inheritHandle, uint processId);

bool SetProcessDefaultCpuSets(SafeProcessHandle process, uint[]? cpuSetIds, uint cpuSetIdCount);

bool GetProcessTimes(
SafeProcessHandle process,
out FILETIME creationTime,
out FILETIME exitTime,
out FILETIME kernelTime,
out FILETIME userTime);

bool GetSystemCpuSetInformation(
IntPtr information,
uint bufferLength,
ref uint returnedLength,
SafeProcessHandle process,
uint flags);

int GetLastWin32Error();
}

internal sealed class ProcessCpuSetNativeApi : IProcessCpuSetNativeApi
{
public static ProcessCpuSetNativeApi Instance { get; } = new();

private ProcessCpuSetNativeApi()
{
}

public SafeProcessHandle OpenProcess(ProcessAccessFlags access, bool inheritHandle, uint processId)
{
return CpuSetNativeMethods.OpenProcess(access, inheritHandle, processId);
}

public bool SetProcessDefaultCpuSets(SafeProcessHandle process, uint[]? cpuSetIds, uint cpuSetIdCount)
{
return CpuSetNativeMethods.SetProcessDefaultCpuSets(process, cpuSetIds, cpuSetIdCount);
}

public bool GetProcessTimes(
SafeProcessHandle process,
out FILETIME creationTime,
out FILETIME exitTime,
out FILETIME kernelTime,
out FILETIME userTime)
{
return CpuSetNativeMethods.GetProcessTimes(process, out creationTime, out exitTime, out kernelTime, out userTime);
}

public bool GetSystemCpuSetInformation(
IntPtr information,
uint bufferLength,
ref uint returnedLength,
SafeProcessHandle process,
uint flags)
{
return CpuSetNativeMethods.GetSystemCpuSetInformation(information, bufferLength, ref returnedLength, process, flags);
}

public int GetLastWin32Error()
{
return Marshal.GetLastWin32Error();
}
}
}
Loading
Loading