diff --git a/Models/CpuPreset.cs b/Models/CpuPreset.cs new file mode 100644 index 0000000..e5dc17e --- /dev/null +++ b/Models/CpuPreset.cs @@ -0,0 +1,46 @@ +/* + * 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 . + */ +namespace ThreadPilot.Models +{ + /// + /// Topology-aware CPU affinity preset generated from a CPU topology snapshot. + /// + public sealed record CpuPreset + { + public string PresetId { get; init; } = string.Empty; + + public string Name { get; init; } = string.Empty; + + public string Description { get; init; } = string.Empty; + + public CpuSelection Selection { get; init; } = new(); + + public string Reason { get; init; } = string.Empty; + + public string? SourcePresetId { get; init; } + + public string? Warning { get; init; } + + public CpuTopologySignature? GeneratedByTopologySignature { get; init; } + + public bool IsUserEditable { get; init; } = true; + + public bool IsGenerated { get; init; } = true; + + public bool ReviewRequired { get; init; } + } +} diff --git a/Services/CpuPresetGenerationOptions.cs b/Services/CpuPresetGenerationOptions.cs new file mode 100644 index 0000000..cd77711 --- /dev/null +++ b/Services/CpuPresetGenerationOptions.cs @@ -0,0 +1,28 @@ +/* + * 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 . + */ +namespace ThreadPilot.Services +{ + public sealed record CpuPresetGenerationOptions + { + public bool ExcludeCpu0ForGaming { get; init; } = true; + + public IReadOnlySet DeletedGeneratedPresetIds { get; init; } = + new HashSet(StringComparer.Ordinal); + + public bool IncludeExperimentalPresets { get; init; } + } +} diff --git a/Services/CpuPresetGenerator.cs b/Services/CpuPresetGenerator.cs new file mode 100644 index 0000000..f3f31b4 --- /dev/null +++ b/Services/CpuPresetGenerator.cs @@ -0,0 +1,340 @@ +/* + * 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 . + */ +namespace ThreadPilot.Services +{ + using ThreadPilot.Models; + + public sealed class CpuPresetGenerator : ICpuPresetGenerator + { + private const string GamingWarning = + "Suggested default. Results may vary by game and system. You can edit or delete this preset."; + + public IReadOnlyList Generate( + CpuTopologySnapshot topology, + CpuPresetGenerationOptions? options = null) + { + ArgumentNullException.ThrowIfNull(topology); + + var resolvedOptions = options ?? new CpuPresetGenerationOptions(); + var presets = new List(); + + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "all-cores", + "All cores", + "Use every logical processor reported by the current CPU topology.", + topology.LogicalProcessors, + topology, + "Uses all logical processors from the topology snapshot.")); + + var allPhysicalProcessors = HasCoreIndexForAllProcessors(topology) + ? SelectOneLogicalProcessorPerCore(topology.LogicalProcessors, topology) + : []; + if (allPhysicalProcessors.Count > 0) + { + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "all-physical-cores", + "All physical cores / no SMT", + "Use one logical processor per physical core.", + allPhysicalProcessors, + topology, + "Uses CoreIndex and SMT sibling metadata to select one logical processor per core.")); + } + + var allExceptCpu0 = topology.LogicalProcessors + .Where(processor => processor.GlobalIndex != 0) + .ToList(); + if (topology.LogicalProcessors.Count >= 2 && allExceptCpu0.Count > 0) + { + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "all-except-cpu0", + "All except CPU0", + "Use every logical processor except global CPU index 0.", + allExceptCpu0, + topology, + "Excludes GlobalIndex 0 while keeping the remaining topology-aware processor refs.")); + } + + var efficiencyClasses = topology.LogicalProcessors + .Select(processor => topology.TryGetEfficiencyClass(processor, out var efficiencyClass) + ? (byte?)efficiencyClass + : null) + .Where(efficiencyClass => efficiencyClass.HasValue) + .Select(efficiencyClass => efficiencyClass!.Value) + .Distinct() + .OrderBy(efficiencyClass => efficiencyClass) + .ToList(); + + var hasDistinctEfficiencyClasses = efficiencyClasses.Count >= 2; + List pCoreProcessors = []; + if (hasDistinctEfficiencyClasses) + { + var performanceClass = efficiencyClasses.Max(); + pCoreProcessors = topology.LogicalProcessors + .Where(processor => + topology.TryGetEfficiencyClass(processor, out var efficiencyClass) && + efficiencyClass == performanceClass) + .ToList(); + var eCoreProcessors = topology.LogicalProcessors + .Where(processor => + topology.TryGetEfficiencyClass(processor, out var efficiencyClass) && + efficiencyClass < performanceClass) + .ToList(); + + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "p-cores-only", + "P-cores only", + "Use logical processors in the highest EfficiencyClass.", + pCoreProcessors, + topology, + "Uses the highest EfficiencyClass in the topology snapshot as performance cores.")); + + if (HasCoreIndexForProcessors(pCoreProcessors, topology)) + { + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "p-cores-no-smt", + "P-cores only / no SMT", + "Use one logical processor per performance core.", + SelectOneLogicalProcessorPerCore(pCoreProcessors, topology), + topology, + "Uses EfficiencyClass plus CoreIndex and SMT sibling metadata to choose one logical processor per P-core.")); + } + + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "e-cores-only", + "E-cores only", + "Use logical processors below the highest EfficiencyClass.", + eCoreProcessors, + topology, + "Uses EfficiencyClass values below the performance class as efficiency cores.", + "Usually not recommended for games. Useful for background tasks.")); + } + + if (topology.Signature.LastLevelCacheGroupCount > 1 && HasCoreIndexForAllProcessors(topology)) + { + var l3Groups = topology.LogicalProcessors + .Select(processor => topology.TryGetLastLevelCacheIndex(processor, out var cacheIndex) + ? new { Processor = processor, CacheIndex = (int?)cacheIndex } + : null) + .Where(item => item?.CacheIndex != null) + .GroupBy(item => item!.CacheIndex!.Value) + .OrderBy(group => group.Key); + + foreach (var group in l3Groups) + { + AddPreset( + presets, + resolvedOptions, + CreatePreset( + $"l3-group-{group.Key}-physical", + $"L3 group {group.Key} / physical cores", + $"Use one logical processor per core in L3/cache group {group.Key}.", + SelectOneLogicalProcessorPerCore(group.Select(item => item!.Processor), topology), + topology, + $"Based on LastLevelCacheIndex/L3 cache group {group.Key}, not on CPU SKU naming.")); + } + } + + var bestGamingSourceId = SelectBestGamingSourcePresetId(presets, resolvedOptions); + if (bestGamingSourceId != null) + { + var sourcePreset = presets.Single(preset => preset.PresetId == bestGamingSourceId); + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "best-gaming", + "Best gaming suggestion", + "Suggested topology-aware starting point for games.", + sourcePreset.Selection.LogicalProcessors, + topology, + CreateBestGamingReason(bestGamingSourceId), + GamingWarning, + sourcePresetId: bestGamingSourceId)); + } + + AddPreset( + presets, + resolvedOptions, + CreatePreset( + "safe-compatibility", + "Safe compatibility", + "Use every logical processor for maximum compatibility.", + topology.LogicalProcessors, + topology, + "Maximum compatibility.")); + + // TODO: X3D CCD-only presets require reliable cache/topology detection. + // Do not generate X3D CCD-only until it can be detected with confidence. + return presets; + } + + private static string? SelectBestGamingSourcePresetId( + IReadOnlyList presets, + CpuPresetGenerationOptions options) + { + var orderedCandidates = options.ExcludeCpu0ForGaming + ? new[] + { + "p-cores-no-smt", + "l3-group-0-physical", + "all-physical-cores", + "all-except-cpu0", + "all-cores", + } + : new[] + { + "p-cores-no-smt", + "l3-group-0-physical", + "all-physical-cores", + "all-cores", + }; + + return orderedCandidates.FirstOrDefault(candidate => + presets.Any(preset => preset.PresetId == candidate)); + } + + private static string CreateBestGamingReason(string sourcePresetId) => + sourcePresetId switch + { + "p-cores-no-smt" => + "Selected P-cores without SMT because the topology exposes distinct performance and efficiency core classes.", + "l3-group-0-physical" => + "Selected physical cores from L3/cache group 0 because the topology exposes multiple L3 groups and no P/E core classes.", + "all-physical-cores" => + "Selected one logical processor per physical core because reliable CoreIndex metadata is available.", + "all-except-cpu0" => + "Selected all logical processors except CPU0 as a conservative gaming-oriented fallback.", + "all-cores" => + "Selected all logical processors as the safest fallback because no more specific topology preset was available.", + _ => + "Selected the best available topology-aware preset for this CPU.", + }; + + private static bool HasCoreIndexForAllProcessors(CpuTopologySnapshot topology) => + HasCoreIndexForProcessors(topology.LogicalProcessors, topology); + + private static bool HasCoreIndexForProcessors( + IEnumerable processors, + CpuTopologySnapshot topology) + { + var processorList = processors.ToList(); + return processorList.Count > 0 && + processorList.All(processor => topology.TryGetCoreIndex(processor, out _)); + } + + private static List SelectOneLogicalProcessorPerCore( + IEnumerable processors, + CpuTopologySnapshot topology) + { + return processors + .Select(processor => + { + topology.TryGetCoreIndex(processor, out var coreIndex); + return new + { + Processor = processor, + CoreIndex = coreIndex, + SmtSiblingCount = topology.GetSmtSiblingGlobalIndexes(processor).Count, + }; + }) + .GroupBy(item => item.CoreIndex) + .OrderBy(group => group.Key) + .Select(group => group + .OrderBy(item => item.Processor.GlobalIndex) + .ThenBy(item => item.SmtSiblingCount) + .First() + .Processor) + .ToList(); + } + + private static CpuPreset CreatePreset( + string presetId, + string name, + string description, + IEnumerable processors, + CpuTopologySnapshot topology, + string reason, + string? warning = null, + string? sourcePresetId = null, + bool reviewRequired = false) + { + var selectedProcessors = processors + .Distinct() + .OrderBy(processor => processor.GlobalIndex) + .ThenBy(processor => processor.Group) + .ThenBy(processor => processor.LogicalProcessorNumber) + .ToList(); + + return new CpuPreset + { + PresetId = presetId, + Name = name, + Description = description, + Selection = CpuSelection.FromProcessors(selectedProcessors, topology, reason), + Reason = reason, + SourcePresetId = sourcePresetId, + Warning = warning, + GeneratedByTopologySignature = topology.Signature, + IsUserEditable = true, + IsGenerated = true, + ReviewRequired = reviewRequired, + }; + } + + private static void AddPreset( + List presets, + CpuPresetGenerationOptions options, + CpuPreset preset) + { + if (options.DeletedGeneratedPresetIds.Contains(preset.PresetId) || + preset.Selection.LogicalProcessors.Count == 0 || + presets.Any(existing => existing.PresetId == preset.PresetId)) + { + return; + } + + var duplicateSamePurpose = presets.Any(existing => + existing.Reason == preset.Reason && + existing.Selection.GlobalLogicalProcessorIndexes.SequenceEqual( + preset.Selection.GlobalLogicalProcessorIndexes)); + if (duplicateSamePurpose) + { + return; + } + + presets.Add(preset); + } + } +} diff --git a/Services/ICpuPresetGenerator.cs b/Services/ICpuPresetGenerator.cs new file mode 100644 index 0000000..c53763a --- /dev/null +++ b/Services/ICpuPresetGenerator.cs @@ -0,0 +1,27 @@ +/* + * 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 . + */ +namespace ThreadPilot.Services +{ + using ThreadPilot.Models; + + public interface ICpuPresetGenerator + { + IReadOnlyList Generate( + CpuTopologySnapshot topology, + CpuPresetGenerationOptions? options = null); + } +} diff --git a/Services/ServiceConfiguration.cs b/Services/ServiceConfiguration.cs index eb70c5e..477fe46 100644 --- a/Services/ServiceConfiguration.cs +++ b/Services/ServiceConfiguration.cs @@ -111,6 +111,7 @@ private static IServiceCollection ConfigureCoreSystemServices(this IServiceColle services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // CoreMaskService needs IServiceProvider for checking profile references services.AddSingleton(sp => @@ -238,4 +239,3 @@ public static void ValidateServiceConfiguration(IServiceProvider serviceProvider } } } - diff --git a/Tests/ThreadPilot.Core.Tests/CpuPresetGeneratorTests.cs b/Tests/ThreadPilot.Core.Tests/CpuPresetGeneratorTests.cs new file mode 100644 index 0000000..674eaf0 --- /dev/null +++ b/Tests/ThreadPilot.Core.Tests/CpuPresetGeneratorTests.cs @@ -0,0 +1,359 @@ +namespace ThreadPilot.Core.Tests +{ + using ThreadPilot.Models; + using ThreadPilot.Services; + + public sealed class CpuPresetGeneratorTests + { + [Fact] + public void Generate_WithFourCoreEightThreadSmt_GeneratesSafeBasePresets() + { + var topology = CreateSmtTopology(physicalCoreCount: 4, threadsPerCore: 2); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + AssertPresetIdsContain( + presets, + "all-cores", + "all-physical-cores", + "all-except-cpu0", + "best-gaming", + "safe-compatibility"); + Assert.Equal(4, GetPreset(presets, "all-physical-cores").Selection.LogicalProcessors.Count); + AssertBestGamingSource(GetPreset(presets, "best-gaming"), "all-physical-cores"); + AssertValidPresets(presets, topology); + AssertStableIdsAndOrder(generator, topology, presets); + } + + [Fact] + public void Generate_WithEightCoreEightThreadSmtOff_KeepsBestGamingValid() + { + var topology = CreateSmtTopology(physicalCoreCount: 8, threadsPerCore: 1); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + var allCores = GetPreset(presets, "all-cores"); + var physical = presets.SingleOrDefault(preset => preset.PresetId == "all-physical-cores"); + if (physical != null) + { + AssertSameSelection(allCores, physical); + Assert.NotEqual(allCores.Reason, physical.Reason); + } + + var bestGaming = GetPreset(presets, "best-gaming"); + Assert.NotEmpty(bestGaming.Selection.LogicalProcessors); + AssertBestGamingSource(bestGaming, "all-physical-cores"); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WithHybridPAndECoresWithHt_GeneratesHybridPresets() + { + var topology = CreateHybridTopology(pCoreCount: 4, eCoreCount: 4, pCoreThreads: 2); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + AssertPresetIdsContain(presets, "p-cores-only", "p-cores-no-smt", "e-cores-only"); + Assert.Equal(8, GetPreset(presets, "p-cores-only").Selection.LogicalProcessors.Count); + Assert.Equal(4, GetPreset(presets, "p-cores-no-smt").Selection.LogicalProcessors.Count); + Assert.Equal(4, GetPreset(presets, "e-cores-only").Selection.LogicalProcessors.Count); + AssertBestGamingSource(GetPreset(presets, "best-gaming"), "p-cores-no-smt"); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WithHybridPAndECoresWithoutHt_HandlesNoSmtDuplicate() + { + var topology = CreateHybridTopology(pCoreCount: 4, eCoreCount: 4, pCoreThreads: 1); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + var pCoresOnly = GetPreset(presets, "p-cores-only"); + var pCoresNoSmt = presets.SingleOrDefault(preset => preset.PresetId == "p-cores-no-smt"); + if (pCoresNoSmt != null) + { + AssertSameSelection(pCoresOnly, pCoresNoSmt); + Assert.NotEqual(pCoresOnly.Reason, pCoresNoSmt.Reason); + } + + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WithRyzenDualCcdSixPlusSix_GeneratesL3PhysicalPresets() + { + var topology = CreateDualCcdTopology(physicalCoresPerCcd: 6); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + AssertPresetIdsContain(presets, "l3-group-0-physical", "l3-group-1-physical"); + Assert.Equal(6, GetPreset(presets, "l3-group-0-physical").Selection.LogicalProcessors.Count); + Assert.Equal(6, GetPreset(presets, "l3-group-1-physical").Selection.LogicalProcessors.Count); + AssertBestGamingSource(GetPreset(presets, "best-gaming"), "l3-group-0-physical"); + Assert.Contains("L3", GetPreset(presets, "l3-group-0-physical").Reason, StringComparison.OrdinalIgnoreCase); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WithRyzenDualCcdEightPlusEight_GeneratesEightPhysicalPerL3Preset() + { + var topology = CreateDualCcdTopology(physicalCoresPerCcd: 8); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + Assert.Equal(8, GetPreset(presets, "l3-group-0-physical").Selection.LogicalProcessors.Count); + Assert.Equal(8, GetPreset(presets, "l3-group-1-physical").Selection.LogicalProcessors.Count); + AssertBestGamingSource(GetPreset(presets, "best-gaming"), "l3-group-0-physical"); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WithMoreThan64LogicalProcessors_UsesCpuSelectionWithoutCpu64Alias() + { + var topology = CreateSmtTopology(physicalCoreCount: 40, threadsPerCore: 2); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + var allCores = GetPreset(presets, "all-cores"); + Assert.Contains(allCores.Selection.LogicalProcessors, processor => processor.GlobalIndex == 64); + Assert.NotEqual( + allCores.Selection.LogicalProcessors.Single(processor => processor.GlobalIndex == 64), + allCores.Selection.LogicalProcessors.Single(processor => processor.GlobalIndex == 0)); + Assert.Null(CpuSelection.ToLegacyAffinityMaskOrNull(allCores.Selection)); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_WhenGeneratedPresetWasDeleted_DoesNotRegenerateIt() + { + var topology = CreateSmtTopology(physicalCoreCount: 4, threadsPerCore: 2); + var generator = new CpuPresetGenerator(); + var options = new CpuPresetGenerationOptions + { + DeletedGeneratedPresetIds = new HashSet(StringComparer.Ordinal) + { + "best-gaming", + }, + }; + + var presets = generator.Generate(topology, options); + + Assert.DoesNotContain(presets, preset => preset.PresetId == "best-gaming"); + AssertPresetIdsContain(presets, "all-cores", "safe-compatibility"); + } + + [Fact] + public void Generate_WithoutCoreIndex_SkipsPhysicalPresets() + { + var topology = CpuTopologySnapshot.Create( + CreateProcessorRefs(8), + signature: CreateSignature(logicalProcessorCount: 8, physicalCoreCount: 0)); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + Assert.DoesNotContain(presets, preset => preset.PresetId == "all-physical-cores"); + Assert.DoesNotContain(presets, preset => preset.PresetId == "p-cores-no-smt"); + Assert.DoesNotContain(presets, preset => preset.PresetId.StartsWith("l3-group-", StringComparison.Ordinal)); + AssertPresetIdsContain(presets, "all-cores", "all-except-cpu0", "best-gaming", "safe-compatibility"); + AssertBestGamingSource(GetPreset(presets, "best-gaming"), "all-except-cpu0"); + AssertValidPresets(presets, topology); + } + + [Fact] + public void Generate_DoesNotReturnEmptySelections() + { + var topology = CreateHybridTopology(pCoreCount: 2, eCoreCount: 2, pCoreThreads: 2); + var generator = new CpuPresetGenerator(); + + var presets = generator.Generate(topology); + + Assert.All(presets, preset => Assert.NotEmpty(preset.Selection.LogicalProcessors)); + } + + private static CpuPreset GetPreset(IReadOnlyList presets, string presetId) => + presets.Single(preset => preset.PresetId == presetId); + + private static void AssertPresetIdsContain(IReadOnlyList presets, params string[] presetIds) + { + foreach (var presetId in presetIds) + { + Assert.Contains(presets, preset => preset.PresetId == presetId); + } + } + + private static void AssertSameSelection(CpuPreset expected, CpuPreset actual) => + Assert.Equal( + expected.Selection.GlobalLogicalProcessorIndexes, + actual.Selection.GlobalLogicalProcessorIndexes); + + private static void AssertBestGamingSource(CpuPreset bestGaming, string expectedSourcePresetId) + { + Assert.Equal("best-gaming", bestGaming.PresetId); + Assert.Equal(expectedSourcePresetId, bestGaming.SourcePresetId); + Assert.NotEqual(bestGaming.SourcePresetId, bestGaming.Reason); + Assert.False(string.IsNullOrWhiteSpace(bestGaming.Reason)); + } + + private static void AssertStableIdsAndOrder( + CpuPresetGenerator generator, + CpuTopologySnapshot topology, + IReadOnlyList firstRun) + { + var secondRun = generator.Generate(topology); + Assert.Equal( + firstRun.Select(preset => preset.PresetId), + secondRun.Select(preset => preset.PresetId)); + } + + private static void AssertValidPresets(IReadOnlyList presets, CpuTopologySnapshot topology) + { + Assert.NotEmpty(presets); + Assert.Equal(presets.Count, presets.Select(preset => preset.PresetId).Distinct(StringComparer.Ordinal).Count()); + + var topologyProcessors = topology.LogicalProcessors.ToHashSet(); + foreach (var preset in presets) + { + Assert.False(string.IsNullOrWhiteSpace(preset.PresetId)); + Assert.False(string.IsNullOrWhiteSpace(preset.Name)); + Assert.False(string.IsNullOrWhiteSpace(preset.Description)); + Assert.False(string.IsNullOrWhiteSpace(preset.Reason)); + Assert.True(preset.IsGenerated); + Assert.True(preset.IsUserEditable); + Assert.NotEmpty(preset.Selection.LogicalProcessors); + Assert.Equal(topology.Signature, preset.GeneratedByTopologySignature); + Assert.Equal(topology.Signature, preset.Selection.Metadata.TopologySignature); + Assert.All(preset.Selection.LogicalProcessors, processor => Assert.Contains(processor, topologyProcessors)); + } + } + + private static CpuTopologySnapshot CreateSmtTopology(int physicalCoreCount, int threadsPerCore) + { + var processors = new List(); + var coreIndexes = new Dictionary(); + var siblings = new Dictionary>(); + + for (var core = 0; core < physicalCoreCount; core++) + { + var coreProcessors = new List(); + for (var thread = 0; thread < threadsPerCore; thread++) + { + var globalIndex = (core * threadsPerCore) + thread; + var processor = CreateProcessorRef(globalIndex); + processors.Add(processor); + coreIndexes[processor] = core; + coreProcessors.Add(processor); + } + + foreach (var processor in coreProcessors) + { + siblings[processor] = coreProcessors + .Where(sibling => sibling != processor) + .Select(sibling => sibling.GlobalIndex) + .ToList(); + } + } + + return CpuTopologySnapshot.Create( + processors, + signature: CreateSignature(processors.Count, physicalCoreCount), + coreIndexes: coreIndexes, + smtSiblingGlobalIndexes: siblings); + } + + private static CpuTopologySnapshot CreateHybridTopology(int pCoreCount, int eCoreCount, int pCoreThreads) + { + var processors = new List(); + var coreIndexes = new Dictionary(); + var siblings = new Dictionary>(); + var efficiency = new Dictionary(); + + for (var core = 0; core < pCoreCount; core++) + { + var coreProcessors = new List(); + for (var thread = 0; thread < pCoreThreads; thread++) + { + var processor = CreateProcessorRef(processors.Count); + processors.Add(processor); + coreIndexes[processor] = core; + efficiency[processor] = 2; + coreProcessors.Add(processor); + } + + foreach (var processor in coreProcessors) + { + siblings[processor] = coreProcessors + .Where(sibling => sibling != processor) + .Select(sibling => sibling.GlobalIndex) + .ToList(); + } + } + + for (var core = 0; core < eCoreCount; core++) + { + var processor = CreateProcessorRef(processors.Count); + processors.Add(processor); + coreIndexes[processor] = pCoreCount + core; + efficiency[processor] = 0; + siblings[processor] = []; + } + + return CpuTopologySnapshot.Create( + processors, + efficiencyClasses: efficiency, + signature: CreateSignature(processors.Count, pCoreCount + eCoreCount), + coreIndexes: coreIndexes, + smtSiblingGlobalIndexes: siblings); + } + + private static CpuTopologySnapshot CreateDualCcdTopology(int physicalCoresPerCcd) + { + var processorCount = physicalCoresPerCcd * 2; + var processors = CreateProcessorRefs(processorCount).ToList(); + var coreIndexes = processors.ToDictionary(processor => processor, processor => processor.GlobalIndex); + var siblings = processors.ToDictionary( + processor => processor, + _ => (IReadOnlyList)[]); + var l3Indexes = processors.ToDictionary( + processor => processor, + processor => processor.GlobalIndex < physicalCoresPerCcd ? 0 : 1); + + return CpuTopologySnapshot.Create( + processors, + signature: CreateSignature( + logicalProcessorCount: processorCount, + physicalCoreCount: processorCount, + lastLevelCacheGroupCount: 2), + coreIndexes: coreIndexes, + lastLevelCacheIndexes: l3Indexes, + smtSiblingGlobalIndexes: siblings); + } + + private static IEnumerable CreateProcessorRefs(int count) => + Enumerable.Range(0, count).Select(CreateProcessorRef); + + private static ProcessorRef CreateProcessorRef(int globalIndex) => + new((ushort)(globalIndex / 64), (byte)(globalIndex % 64), globalIndex); + + private static CpuTopologySignature CreateSignature( + int logicalProcessorCount, + int physicalCoreCount, + int lastLevelCacheGroupCount = 0) => + new() + { + CpuBrand = "Synthetic CPU", + LogicalProcessorCount = logicalProcessorCount, + PhysicalCoreCount = physicalCoreCount, + ProcessorGroupCount = Math.Max(1, (logicalProcessorCount + 63) / 64), + LastLevelCacheGroupCount = lastLevelCacheGroupCount, + Source = "Test", + }; + } +}