Skip to content

Commit cbe2402

Browse files
Migrate Process tab manual affinity apply to CpuSelection (#16)
Migrate Process tab manual affinity apply to CpuSelection Route manual Process tab affinity operations through a topology-aware coordinator so CpuSelection and CPU Sets are preferred over legacy long masks. - Add ProcessAffinityApplyCoordinator for manual Process tab affinity apply - Prefer CoreMask.CpuSelection when available - Migrate legacy BoolMask selections to CpuSelection when topology is available - Block unsafe legacy fallback for selections above CPU64 - Keep legacy long mask fallback only for safe single-group <=64 selections - Wire SetAffinity, ApplyCoreMaskToProcessAsync and QuickApplyAffinityAndPowerPlan through the coordinator - Document CoreMask.ToProcessorAffinity as legacy-only - Ensure ProcessViewModel fallback coordinator receives topology provider - Add regression tests for CPU64, multi-group, safe legacy fallback, migration and protected-process messages No UI redesign, preset generator, memory priority, persistent rules engine, migration logic, version or tag changes.
1 parent 5f9f7b5 commit cbe2402

7 files changed

Lines changed: 545 additions & 25 deletions

File tree

‎Models/CoreMask.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ public partial class CoreMask : ObservableObject
7171
public int SelectedCoreCount => this.BoolMask.Count(b => b);
7272

7373
/// <summary>
74-
/// Converts the boolean mask to a processor affinity value.
74+
/// Converts the boolean mask to a legacy 64-bit processor affinity value.
75+
/// This is only safe for single processor-group selections below CPU 64;
76+
/// topology-aware apply paths must prefer <see cref="CpuSelection"/>.
7577
/// </summary>
7678
public long ToProcessorAffinity()
7779
{
@@ -162,4 +164,3 @@ public override string ToString()
162164
}
163165
}
164166
}
165-
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* ThreadPilot - process tab affinity apply coordination.
3+
*/
4+
namespace ThreadPilot.Services
5+
{
6+
using Microsoft.Extensions.Logging;
7+
using ThreadPilot.Models;
8+
9+
public interface IProcessAffinityApplyCoordinator
10+
{
11+
Task<AffinityApplyResult> ApplyCoreMaskAsync(
12+
ProcessModel process,
13+
CoreMask coreMask,
14+
CancellationToken cancellationToken = default);
15+
16+
Task<AffinityApplyResult> ApplyCoreSelectionAsync(
17+
ProcessModel process,
18+
IReadOnlyList<bool> boolMask,
19+
string selectionReason,
20+
CancellationToken cancellationToken = default);
21+
}
22+
23+
public sealed class ProcessAffinityApplyCoordinator : IProcessAffinityApplyCoordinator
24+
{
25+
private readonly IAffinityApplyService affinityApplyService;
26+
private readonly ICpuTopologyProvider? cpuTopologyProvider;
27+
private readonly CpuSelectionMigrationService cpuSelectionMigrationService;
28+
private readonly ILogger<ProcessAffinityApplyCoordinator> logger;
29+
30+
public ProcessAffinityApplyCoordinator(
31+
IAffinityApplyService affinityApplyService,
32+
ICpuTopologyProvider? cpuTopologyProvider,
33+
CpuSelectionMigrationService cpuSelectionMigrationService,
34+
ILogger<ProcessAffinityApplyCoordinator> logger)
35+
{
36+
this.affinityApplyService = affinityApplyService ?? throw new ArgumentNullException(nameof(affinityApplyService));
37+
this.cpuTopologyProvider = cpuTopologyProvider;
38+
this.cpuSelectionMigrationService = cpuSelectionMigrationService ?? throw new ArgumentNullException(nameof(cpuSelectionMigrationService));
39+
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
40+
}
41+
42+
public Task<AffinityApplyResult> ApplyCoreMaskAsync(
43+
ProcessModel process,
44+
CoreMask coreMask,
45+
CancellationToken cancellationToken = default)
46+
{
47+
ArgumentNullException.ThrowIfNull(coreMask);
48+
49+
if (HasSelectionPayload(coreMask.CpuSelection))
50+
{
51+
return this.affinityApplyService.ApplyAsync(process, coreMask.CpuSelection!);
52+
}
53+
54+
return this.ApplyCoreSelectionAsync(
55+
process,
56+
coreMask.BoolMask.ToList(),
57+
$"Manual Process tab mask '{coreMask.Name}'",
58+
cancellationToken);
59+
}
60+
61+
public async Task<AffinityApplyResult> ApplyCoreSelectionAsync(
62+
ProcessModel process,
63+
IReadOnlyList<bool> boolMask,
64+
string selectionReason,
65+
CancellationToken cancellationToken = default)
66+
{
67+
ArgumentNullException.ThrowIfNull(process);
68+
ArgumentNullException.ThrowIfNull(boolMask);
69+
70+
if (boolMask.Count == 0 || !boolMask.Any(selected => selected))
71+
{
72+
return AffinityApplyResult.Failed(
73+
AffinityApplyErrorCodes.InvalidSelection,
74+
ProcessOperationUserMessages.InvalidTopology,
75+
"Manual CPU selection is empty.",
76+
isInvalidTopology: true,
77+
failureReason: AffinityApplyFailureReason.InvalidMask);
78+
}
79+
80+
var migratedSelection = await this.TryMigrateToCpuSelectionAsync(
81+
boolMask,
82+
selectionReason,
83+
cancellationToken).ConfigureAwait(false);
84+
if (migratedSelection != null)
85+
{
86+
return await this.affinityApplyService.ApplyAsync(process, migratedSelection).ConfigureAwait(false);
87+
}
88+
89+
if (!TryBuildSafeLegacyMask(boolMask, out var legacyMask, out var legacyFailure))
90+
{
91+
return legacyFailure;
92+
}
93+
94+
return await this.affinityApplyService.ApplyAsync(process, legacyMask).ConfigureAwait(false);
95+
}
96+
97+
private async Task<CpuSelection?> TryMigrateToCpuSelectionAsync(
98+
IReadOnlyList<bool> boolMask,
99+
string selectionReason,
100+
CancellationToken cancellationToken)
101+
{
102+
if (this.cpuTopologyProvider == null)
103+
{
104+
return null;
105+
}
106+
107+
try
108+
{
109+
var topology = await this.cpuTopologyProvider.GetTopologySnapshotAsync(cancellationToken).ConfigureAwait(false);
110+
var migrated = this.cpuSelectionMigrationService.MigrateFromLegacyCoreMask(boolMask, topology);
111+
if (!HasSelectionPayload(migrated.Selection))
112+
{
113+
return null;
114+
}
115+
116+
return migrated.Selection with
117+
{
118+
Metadata = migrated.Selection.Metadata with
119+
{
120+
SelectionReason = string.IsNullOrWhiteSpace(selectionReason)
121+
? migrated.Selection.Metadata.SelectionReason
122+
: selectionReason,
123+
},
124+
};
125+
}
126+
catch (Exception ex) when (ex is not OperationCanceledException)
127+
{
128+
this.logger.LogDebug(ex, "Could not migrate manual Process tab CPU selection to CpuSelection");
129+
return null;
130+
}
131+
}
132+
133+
private static bool HasSelectionPayload(CpuSelection? selection) =>
134+
selection != null &&
135+
(selection.CpuSetIds.Count > 0 || selection.LogicalProcessors.Count > 0);
136+
137+
private static bool TryBuildSafeLegacyMask(
138+
IReadOnlyList<bool> boolMask,
139+
out long legacyMask,
140+
out AffinityApplyResult failure)
141+
{
142+
legacyMask = 0;
143+
failure = default!;
144+
145+
if (boolMask.Count > 64)
146+
{
147+
failure = AffinityApplyResult.Failed(
148+
AffinityApplyErrorCodes.LegacyFallbackUnsafe,
149+
ProcessOperationUserMessages.LegacyFallbackBlocked,
150+
"Manual CPU selection exceeds the legacy single-group 64-bit affinity mask.",
151+
isLegacyFallbackBlocked: true,
152+
failureReason: AffinityApplyFailureReason.InvalidMask);
153+
return false;
154+
}
155+
156+
for (var bit = 0; bit < boolMask.Count; bit++)
157+
{
158+
if (boolMask[bit])
159+
{
160+
legacyMask |= 1L << bit;
161+
}
162+
}
163+
164+
if (legacyMask == 0)
165+
{
166+
failure = AffinityApplyResult.Failed(
167+
AffinityApplyErrorCodes.InvalidSelection,
168+
ProcessOperationUserMessages.InvalidTopology,
169+
"Manual CPU selection does not contain any enabled CPUs.",
170+
isInvalidTopology: true,
171+
failureReason: AffinityApplyFailureReason.InvalidMask);
172+
return false;
173+
}
174+
175+
return true;
176+
}
177+
}
178+
}

‎Services/ServiceConfiguration.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ private static IServiceCollection ConfigureCoreSystemServices(this IServiceColle
106106
services.AddSingleton<IProcessClassifier, ProcessClassifier>();
107107
services.AddSingleton<IProcessService, ProcessService>();
108108
services.AddSingleton<IAffinityApplyService, AffinityApplyService>();
109+
services.AddSingleton<IProcessAffinityApplyCoordinator, ProcessAffinityApplyCoordinator>();
109110
services.AddSingleton<IProcessMemoryPriorityNativeApi>(ProcessMemoryPriorityNativeApi.Instance);
110111
services.AddSingleton<IProcessMemoryPriorityService, ProcessMemoryPriorityService>();
111112
services.AddSingleton<IPersistentProcessRuleStore, PersistentProcessRuleJsonStore>();

0 commit comments

Comments
 (0)