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
84 changes: 82 additions & 2 deletions Models/CpuSelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public sealed record CpuTopologySignature

public int LastLevelCacheGroupCount { get; init; }

public int PackageCount { get; init; }

public string Source { get; init; } = "Unknown";
}

Expand Down Expand Up @@ -73,16 +75,31 @@ public sealed class CpuTopologySnapshot
{
private readonly IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, byte> efficiencyClassesByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, int> coreIndexesByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, int> numaNodeIndexesByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, int> lastLevelCacheIndexesByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, int> packageIndexesByProcessor;
private readonly IReadOnlyDictionary<ProcessorRef, IReadOnlyList<int>> smtSiblingGlobalIndexesByProcessor;

private CpuTopologySnapshot(
IReadOnlyList<ProcessorRef> logicalProcessors,
IReadOnlyDictionary<ProcessorRef, uint> cpuSetIdsByProcessor,
IReadOnlyDictionary<ProcessorRef, byte> efficiencyClassesByProcessor,
IReadOnlyDictionary<ProcessorRef, int> coreIndexesByProcessor,
IReadOnlyDictionary<ProcessorRef, int> numaNodeIndexesByProcessor,
IReadOnlyDictionary<ProcessorRef, int> lastLevelCacheIndexesByProcessor,
IReadOnlyDictionary<ProcessorRef, int> packageIndexesByProcessor,
IReadOnlyDictionary<ProcessorRef, IReadOnlyList<int>> smtSiblingGlobalIndexesByProcessor,
CpuTopologySignature signature)
{
this.LogicalProcessors = logicalProcessors;
this.cpuSetIdsByProcessor = cpuSetIdsByProcessor;
this.efficiencyClassesByProcessor = efficiencyClassesByProcessor;
this.coreIndexesByProcessor = coreIndexesByProcessor;
this.numaNodeIndexesByProcessor = numaNodeIndexesByProcessor;
this.lastLevelCacheIndexesByProcessor = lastLevelCacheIndexesByProcessor;
this.packageIndexesByProcessor = packageIndexesByProcessor;
this.smtSiblingGlobalIndexesByProcessor = smtSiblingGlobalIndexesByProcessor;
this.Signature = signature;
}

Expand All @@ -94,7 +111,12 @@ public static CpuTopologySnapshot Create(
IEnumerable<ProcessorRef> logicalProcessors,
IReadOnlyDictionary<ProcessorRef, uint>? cpuSetIds = null,
IReadOnlyDictionary<ProcessorRef, byte>? efficiencyClasses = null,
CpuTopologySignature? signature = null)
CpuTopologySignature? signature = null,
IReadOnlyDictionary<ProcessorRef, int>? coreIndexes = null,
IReadOnlyDictionary<ProcessorRef, int>? numaNodeIndexes = null,
IReadOnlyDictionary<ProcessorRef, int>? lastLevelCacheIndexes = null,
IReadOnlyDictionary<ProcessorRef, int>? packageIndexes = null,
IReadOnlyDictionary<ProcessorRef, IReadOnlyList<int>>? smtSiblingGlobalIndexes = null)
{
ArgumentNullException.ThrowIfNull(logicalProcessors);

Expand Down Expand Up @@ -128,14 +150,45 @@ public static CpuTopologySnapshot Create(
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
?? new Dictionary<ProcessorRef, byte>();

var coreIndexMap = FilterKnownProcessorMap(coreIndexes, processorSet);
var numaNodeIndexMap = FilterKnownProcessorMap(numaNodeIndexes, processorSet);
var lastLevelCacheIndexMap = FilterKnownProcessorMap(lastLevelCacheIndexes, processorSet);
var packageIndexMap = FilterKnownProcessorMap(packageIndexes, processorSet);
var knownGlobalIndexes = processors.Select(processor => processor.GlobalIndex).ToHashSet();
var smtSiblingMap = smtSiblingGlobalIndexes?
.Where(kvp => processorSet.Contains(kvp.Key))
.ToDictionary(
kvp => kvp.Key,
kvp => (IReadOnlyList<int>)kvp.Value
.Where(knownGlobalIndexes.Contains)
.Distinct()
.OrderBy(index => index)
.ToList())
?? new Dictionary<ProcessorRef, IReadOnlyList<int>>();

var resolvedSignature = signature ?? new CpuTopologySignature
{
LogicalProcessorCount = processors.Count,
PhysicalCoreCount = coreIndexMap.Count == 0
? processors.Count
: coreIndexMap.Values.Distinct().Count(),
ProcessorGroupCount = processors.Select(processor => processor.Group).Distinct().Count(),
NumaNodeCount = numaNodeIndexMap.Values.Distinct().Count(),
LastLevelCacheGroupCount = lastLevelCacheIndexMap.Values.Distinct().Count(),
PackageCount = packageIndexMap.Values.Distinct().Count(),
Source = "Snapshot",
};

return new CpuTopologySnapshot(processors, cpuSetMap, efficiencyClassMap, resolvedSignature);
return new CpuTopologySnapshot(
processors,
cpuSetMap,
efficiencyClassMap,
coreIndexMap,
numaNodeIndexMap,
lastLevelCacheIndexMap,
packageIndexMap,
smtSiblingMap,
resolvedSignature);
}

public bool TryGetCpuSetId(ProcessorRef processor, out uint cpuSetId) =>
Expand All @@ -144,6 +197,23 @@ public bool TryGetCpuSetId(ProcessorRef processor, out uint cpuSetId) =>
public bool TryGetEfficiencyClass(ProcessorRef processor, out byte efficiencyClass) =>
this.efficiencyClassesByProcessor.TryGetValue(processor, out efficiencyClass);

public bool TryGetCoreIndex(ProcessorRef processor, out int coreIndex) =>
this.coreIndexesByProcessor.TryGetValue(processor, out coreIndex);

public bool TryGetNumaNodeIndex(ProcessorRef processor, out int numaNodeIndex) =>
this.numaNodeIndexesByProcessor.TryGetValue(processor, out numaNodeIndex);

public bool TryGetLastLevelCacheIndex(ProcessorRef processor, out int lastLevelCacheIndex) =>
this.lastLevelCacheIndexesByProcessor.TryGetValue(processor, out lastLevelCacheIndex);

public bool TryGetPackageIndex(ProcessorRef processor, out int packageIndex) =>
this.packageIndexesByProcessor.TryGetValue(processor, out packageIndex);

public IReadOnlyList<int> GetSmtSiblingGlobalIndexes(ProcessorRef processor) =>
this.smtSiblingGlobalIndexesByProcessor.TryGetValue(processor, out var siblings)
? siblings
: [];

public byte? GetPerformanceEfficiencyClass()
{
if (this.efficiencyClassesByProcessor.Count == 0)
Expand All @@ -153,6 +223,16 @@ public bool TryGetEfficiencyClass(ProcessorRef processor, out byte efficiencyCla

return this.efficiencyClassesByProcessor.Values.Max();
}

private static Dictionary<ProcessorRef, int> FilterKnownProcessorMap(
IReadOnlyDictionary<ProcessorRef, int>? source,
HashSet<ProcessorRef> processorSet)
{
return source?
.Where(kvp => processorSet.Contains(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
?? new Dictionary<ProcessorRef, int>();
}
}

/// <summary>
Expand Down
33 changes: 33 additions & 0 deletions Services/ICpuTopologyProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.Services
{
using System.Threading;
using System.Threading.Tasks;
using ThreadPilot.Models;

/// <summary>
/// Provides a topology-aware CPU snapshot without applying runtime affinity changes.
/// </summary>
public interface ICpuTopologyProvider
{
/// <summary>
/// Gets a current CPU topology snapshot.
/// </summary>
Task<CpuTopologySnapshot> GetTopologySnapshotAsync(CancellationToken cancellationToken = default);
}
}
164 changes: 164 additions & 0 deletions Services/WindowsCpuTopologyNativeLayout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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.Services
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using ThreadPilot.Models;

internal static class WindowsCpuTopologyNativeLayout
{
public static int GroupAffinitySize => Marshal.SizeOf<GroupAffinity>();

public static int ProcessorGroupCountOffset => Marshal.OffsetOf<ProcessorRelationship>(nameof(ProcessorRelationship.GroupCount)).ToInt32();

public static int ProcessorGroupMaskOffset => Marshal.OffsetOf<ProcessorRelationship>(nameof(ProcessorRelationship.GroupMask)).ToInt32();

public static int CacheReservedOffset => Marshal.OffsetOf<CacheRelationship>(nameof(CacheRelationship.Reserved)).ToInt32();

public static int CacheGroupCountOffset => Marshal.OffsetOf<CacheRelationship>(nameof(CacheRelationship.GroupCount)).ToInt32();

public static int CacheGroupMaskOffset => Marshal.OffsetOf<CacheRelationship>(nameof(CacheRelationship.GroupMask)).ToInt32();

public static int NumaReservedOffset => Marshal.OffsetOf<NumaNodeRelationship>(nameof(NumaNodeRelationship.Reserved)).ToInt32();

public static int NumaGroupCountOffset => Marshal.OffsetOf<NumaNodeRelationship>(nameof(NumaNodeRelationship.GroupCount)).ToInt32();

public static int NumaGroupMaskOffset => Marshal.OffsetOf<NumaNodeRelationship>(nameof(NumaNodeRelationship.GroupMask)).ToInt32();

internal enum ProcessorCacheType
{
CacheUnified = 0,
CacheInstruction = 1,
CacheData = 2,
CacheTrace = 3,
}

[StructLayout(LayoutKind.Sequential)]
internal struct GroupAffinity
{
public UIntPtr Mask;
public ushort Group;
public ushort Reserved0;
public ushort Reserved1;
public ushort Reserved2;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ProcessorRelationship
{
public byte Flags;
public byte EfficiencyClass;
public fixed byte Reserved[20];
public ushort GroupCount;
public GroupAffinity GroupMask;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct CacheRelationship
{
public byte Level;
public byte Associativity;
public ushort LineSize;
public uint CacheSize;
public ProcessorCacheType Type;
public fixed byte Reserved[18];
public ushort GroupCount;
public GroupAffinity GroupMask;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct NumaNodeRelationship
{
public uint NodeNumber;
public fixed byte Reserved[18];
public ushort GroupCount;
public GroupAffinity GroupMask;
}

public static IReadOnlyList<ProcessorRef> ReadProcessorRelationshipProcessors(IntPtr relationshipPtr, ushort groupCount)
{
return ReadProcessorsFromGroupMasks(relationshipPtr, ProcessorGroupMaskOffset, groupCount).ToList();
}

public static bool TryReadL3CacheProcessors(IntPtr relationshipPtr, out IReadOnlyList<ProcessorRef> processors)
{
var cache = Marshal.PtrToStructure<CacheRelationship>(relationshipPtr);
if (cache.Level != 3 || cache.GroupCount == 0)
{
processors = [];
return false;
}

processors = ReadProcessorsFromGroupMasks(relationshipPtr, CacheGroupMaskOffset, cache.GroupCount).ToList();
return processors.Count > 0;
}

public static IReadOnlyList<ProcessorRef> ReadNumaNodeProcessors(IntPtr relationshipPtr, out int nodeNumber)
{
var numaNode = Marshal.PtrToStructure<NumaNodeRelationship>(relationshipPtr);
nodeNumber = unchecked((int)numaNode.NodeNumber);
var groupCount = numaNode.GroupCount == 0
? (ushort)1
: numaNode.GroupCount;

return ReadProcessorsFromGroupMasks(relationshipPtr, NumaGroupMaskOffset, groupCount).ToList();
}

public static IEnumerable<ProcessorRef> CreateFallbackProcessors(int logicalProcessorCount)
{
return Enumerable.Range(0, logicalProcessorCount)
.Select(index => new ProcessorRef((ushort)(index / 64), (byte)(index % 64), index));
}

private static IEnumerable<ProcessorRef> ReadProcessorsFromGroupMasks(
IntPtr relationshipPtr,
int groupMaskOffset,
ushort groupCount)
{
var firstGroupMaskPtr = IntPtr.Add(relationshipPtr, groupMaskOffset);
var stride = GroupAffinitySize;
for (var index = 0; index < groupCount; index++)
{
var groupAffinity = Marshal.PtrToStructure<GroupAffinity>(IntPtr.Add(firstGroupMaskPtr, index * stride));
foreach (var logicalProcessor in ReadProcessorsFromGroupAffinity(groupAffinity))
{
yield return logicalProcessor;
}
}
}

private static IEnumerable<ProcessorRef> ReadProcessorsFromGroupAffinity(GroupAffinity groupAffinity)
{
var mask = groupAffinity.Mask.ToUInt64();
for (var bit = 0; bit < 64; bit++)
{
if ((mask & (1UL << bit)) != 0)
{
yield return CreateProcessorRef(groupAffinity.Group, (byte)bit);
}
}
}

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