Skip to content

Commit 2560885

Browse files
committed
Make CPU Sets mapping group-aware
1 parent ff1cd97 commit 2560885

5 files changed

Lines changed: 622 additions & 88 deletions

File tree

Platforms/Windows/CpuSetMapping.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* ThreadPilot - Advanced Windows Process and Power Plan Manager
3+
* Copyright (C) 2025 Prime Build
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, version 3 only.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
namespace ThreadPilot.Platforms.Windows
18+
{
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Linq;
22+
using ThreadPilot.Models;
23+
24+
internal sealed class CpuSetMapping
25+
{
26+
private readonly IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor;
27+
private readonly IReadOnlyDictionary<uint, ProcessorRef> processorsByCpuSetId;
28+
29+
private CpuSetMapping(
30+
IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor,
31+
IReadOnlyDictionary<uint, ProcessorRef> processorsByCpuSetId)
32+
{
33+
this.cpuSetIdsByProcessor = cpuSetIdsByProcessor;
34+
this.processorsByCpuSetId = processorsByCpuSetId;
35+
}
36+
37+
public static CpuSetMapping Empty { get; } = new(
38+
new Dictionary<ProcessorRef, uint>(),
39+
new Dictionary<uint, ProcessorRef>());
40+
41+
public bool IsEmpty => this.cpuSetIdsByProcessor.Count == 0;
42+
43+
public static CpuSetMapping Create(IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor)
44+
{
45+
ArgumentNullException.ThrowIfNull(cpuSetIdsByProcessor);
46+
47+
var forwardMap = cpuSetIdsByProcessor
48+
.OrderBy(kvp => kvp.Key.GlobalIndex)
49+
.ThenBy(kvp => kvp.Key.Group)
50+
.ThenBy(kvp => kvp.Key.LogicalProcessorNumber)
51+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
52+
53+
var inverseMap = forwardMap
54+
.GroupBy(kvp => kvp.Value)
55+
.ToDictionary(
56+
group => group.Key,
57+
group => group
58+
.Select(kvp => kvp.Key)
59+
.OrderBy(processor => processor.GlobalIndex)
60+
.ThenBy(processor => processor.Group)
61+
.ThenBy(processor => processor.LogicalProcessorNumber)
62+
.First());
63+
64+
return new CpuSetMapping(forwardMap, inverseMap);
65+
}
66+
67+
public static ProcessorRef CreateProcessorRef(ushort group, byte logicalProcessorNumber)
68+
{
69+
return new ProcessorRef(group, logicalProcessorNumber, (group * 64) + logicalProcessorNumber);
70+
}
71+
72+
public bool TryGetCpuSetId(ProcessorRef processor, out uint cpuSetId)
73+
{
74+
return this.cpuSetIdsByProcessor.TryGetValue(processor, out cpuSetId);
75+
}
76+
77+
public bool TryGetProcessorRef(uint cpuSetId, out ProcessorRef processor)
78+
{
79+
return this.processorsByCpuSetId.TryGetValue(cpuSetId, out processor);
80+
}
81+
82+
public IReadOnlyList<uint> ResolveCpuSetIds(CpuSelection selection)
83+
{
84+
ArgumentNullException.ThrowIfNull(selection);
85+
86+
if (selection.CpuSetIds.Count > 0)
87+
{
88+
return selection.CpuSetIds
89+
.Distinct()
90+
.OrderBy(cpuSetId => cpuSetId)
91+
.ToList();
92+
}
93+
94+
return selection.LogicalProcessors
95+
.Select(processor => this.TryGetCpuSetId(processor, out var cpuSetId) ? (uint?)cpuSetId : null)
96+
.Where(cpuSetId => cpuSetId.HasValue)
97+
.Select(cpuSetId => cpuSetId!.Value)
98+
.Distinct()
99+
.OrderBy(cpuSetId => cpuSetId)
100+
.ToList();
101+
}
102+
103+
public IReadOnlyList<uint> ResolveLegacyAffinityMask(long affinityMask, int logicalProcessorCount)
104+
{
105+
var unsignedMask = unchecked((ulong)affinityMask);
106+
var maxLegacyBits = Math.Min(Math.Max(logicalProcessorCount, 0), 64);
107+
var cpuSetIds = new List<uint>();
108+
109+
for (var bit = 0; bit < maxLegacyBits; bit++)
110+
{
111+
if ((unsignedMask & (1UL << bit)) == 0)
112+
{
113+
continue;
114+
}
115+
116+
var processor = CreateProcessorRef(0, (byte)bit);
117+
if (this.TryGetCpuSetId(processor, out var cpuSetId))
118+
{
119+
cpuSetIds.Add(cpuSetId);
120+
}
121+
}
122+
123+
return cpuSetIds
124+
.Distinct()
125+
.OrderBy(cpuSetId => cpuSetId)
126+
.ToList();
127+
}
128+
}
129+
}

Platforms/Windows/IProcessCpuSetHandler.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
namespace ThreadPilot.Platforms.Windows
1818
{
1919
using System;
20+
using ThreadPilot.Models;
2021

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

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

48+
/// <summary>
49+
/// Applies a topology-aware CPU selection to the process using CPU Sets.
50+
/// </summary>
51+
/// <param name="selection">The CPU selection to apply.</param>
52+
/// <param name="clearSelection">If true, clears the CPU Set selection and ignores <paramref name="selection"/>.</param>
53+
/// <returns>True if the operation succeeded, false otherwise.</returns>
54+
bool ApplyCpuSelection(CpuSelection selection, bool clearSelection = false);
55+
4456
/// <summary>
4557
/// Gets the average CPU usage for this process.
4658
/// </summary>
@@ -53,4 +65,3 @@ public interface IProcessCpuSetHandler : IDisposable
5365
bool IsValid { get; }
5466
}
5567
}
56-
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* ThreadPilot - Advanced Windows Process and Power Plan Manager
3+
* Copyright (C) 2025 Prime Build
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, version 3 only.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
namespace ThreadPilot.Platforms.Windows
18+
{
19+
using System;
20+
using System.Runtime.InteropServices;
21+
using Microsoft.Win32.SafeHandles;
22+
23+
internal interface IProcessCpuSetNativeApi
24+
{
25+
SafeProcessHandle OpenProcess(ProcessAccessFlags access, bool inheritHandle, uint processId);
26+
27+
bool SetProcessDefaultCpuSets(SafeProcessHandle process, uint[]? cpuSetIds, uint cpuSetIdCount);
28+
29+
bool GetProcessTimes(
30+
SafeProcessHandle process,
31+
out FILETIME creationTime,
32+
out FILETIME exitTime,
33+
out FILETIME kernelTime,
34+
out FILETIME userTime);
35+
36+
bool GetSystemCpuSetInformation(
37+
IntPtr information,
38+
uint bufferLength,
39+
ref uint returnedLength,
40+
SafeProcessHandle process,
41+
uint flags);
42+
43+
int GetLastWin32Error();
44+
}
45+
46+
internal sealed class ProcessCpuSetNativeApi : IProcessCpuSetNativeApi
47+
{
48+
public static ProcessCpuSetNativeApi Instance { get; } = new();
49+
50+
private ProcessCpuSetNativeApi()
51+
{
52+
}
53+
54+
public SafeProcessHandle OpenProcess(ProcessAccessFlags access, bool inheritHandle, uint processId)
55+
{
56+
return CpuSetNativeMethods.OpenProcess(access, inheritHandle, processId);
57+
}
58+
59+
public bool SetProcessDefaultCpuSets(SafeProcessHandle process, uint[]? cpuSetIds, uint cpuSetIdCount)
60+
{
61+
return CpuSetNativeMethods.SetProcessDefaultCpuSets(process, cpuSetIds, cpuSetIdCount);
62+
}
63+
64+
public bool GetProcessTimes(
65+
SafeProcessHandle process,
66+
out FILETIME creationTime,
67+
out FILETIME exitTime,
68+
out FILETIME kernelTime,
69+
out FILETIME userTime)
70+
{
71+
return CpuSetNativeMethods.GetProcessTimes(process, out creationTime, out exitTime, out kernelTime, out userTime);
72+
}
73+
74+
public bool GetSystemCpuSetInformation(
75+
IntPtr information,
76+
uint bufferLength,
77+
ref uint returnedLength,
78+
SafeProcessHandle process,
79+
uint flags)
80+
{
81+
return CpuSetNativeMethods.GetSystemCpuSetInformation(information, bufferLength, ref returnedLength, process, flags);
82+
}
83+
84+
public int GetLastWin32Error()
85+
{
86+
return Marshal.GetLastWin32Error();
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)