Skip to content
Draft
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
9 changes: 9 additions & 0 deletions Models/CoreMask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public partial class CoreMask : ObservableObject
/// </summary>
public ObservableCollection<bool> BoolMask { get; set; } = new();

public int ProfileSchemaVersion { get; set; } = CpuAffinityProfileSchemaVersions.Legacy;

public CpuSelection? CpuSelection { get; set; }

public CpuSelectionMigrationMetadata? CpuSelectionMigration { get; set; }

[ObservableProperty]
private bool isDefault = false;

Expand Down Expand Up @@ -135,6 +141,9 @@ public CoreMask Clone()
Description = this.Description,
IsEnabled = this.IsEnabled,
IsDefault = false,
ProfileSchemaVersion = this.ProfileSchemaVersion,
CpuSelection = this.CpuSelection,
CpuSelectionMigration = this.CpuSelectionMigration,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
Expand Down
25 changes: 25 additions & 0 deletions Models/CpuAffinityProfileSchemaVersions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.Models
{
public static class CpuAffinityProfileSchemaVersions
{
public const int Legacy = 1;

public const int CpuSelection = 2;
}
}
35 changes: 35 additions & 0 deletions Models/CpuSelectionMigrationMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.Models
{
public sealed record CpuSelectionMigrationMetadata
{
public bool CreatedFromLegacyAffinityMask { get; init; }

public bool CreatedFromLegacyCoreMask { get; init; }

public bool ReviewRequired { get; init; }

public string MigrationConfidence { get; init; } = string.Empty;

public string Reason { get; init; } = string.Empty;

public CpuTopologySignature? TopologySignature { get; init; }

public long? SourceLegacyAffinityMask { get; init; }
}
}
35 changes: 35 additions & 0 deletions Models/ProcessProfileSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.Models
{
using System.Diagnostics;

public sealed class ProcessProfileSnapshot
{
public int ProfileSchemaVersion { get; set; } = CpuAffinityProfileSchemaVersions.Legacy;

public string ProcessName { get; set; } = string.Empty;

public ProcessPriorityClass Priority { get; set; }

public long ProcessorAffinity { get; set; }

public CpuSelection? CpuSelection { get; set; }

public CpuSelectionMigrationMetadata? CpuSelectionMigration { get; set; }
}
}
12 changes: 12 additions & 0 deletions Models/ProfileModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public partial class ProfileModel : ObservableObject, IModel
[ObservableProperty]
private long processorAffinity = -1; // All cores

[ObservableProperty]
private int profileSchemaVersion = CpuAffinityProfileSchemaVersions.Legacy;

[ObservableProperty]
private CpuSelection? cpuSelection = null;

[ObservableProperty]
private CpuSelectionMigrationMetadata? cpuSelectionMigration = null;

[ObservableProperty]
private string description = string.Empty;

Expand Down Expand Up @@ -80,6 +89,9 @@ public IModel Clone()
ProcessName = this.ProcessName,
Priority = this.Priority,
ProcessorAffinity = this.ProcessorAffinity,
ProfileSchemaVersion = this.ProfileSchemaVersion,
CpuSelection = this.CpuSelection,
CpuSelectionMigration = this.CpuSelectionMigration,
Description = this.Description,
IsEnabled = this.IsEnabled,
createdAt = DateTime.UtcNow,
Expand Down
78 changes: 76 additions & 2 deletions Services/CoreMaskService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class CoreMaskService : ICoreMaskService
private readonly ILogger<CoreMaskService> logger;
private readonly ICpuTopologyService cpuTopologyService;
private readonly IServiceProvider serviceProvider;
private readonly ICpuTopologyProvider? cpuTopologyProvider;
private readonly CpuSelectionMigrationService cpuSelectionMigrationService;
private readonly string masksFilePath;
private bool initialized = false;

Expand All @@ -63,11 +65,15 @@ public class CoreMaskService : ICoreMaskService
public CoreMaskService(
ILogger<CoreMaskService> logger,
ICpuTopologyService cpuTopologyService,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
ICpuTopologyProvider? cpuTopologyProvider = null,
CpuSelectionMigrationService? cpuSelectionMigrationService = null)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.cpuTopologyService = cpuTopologyService ?? throw new ArgumentNullException(nameof(cpuTopologyService));
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
this.cpuTopologyProvider = cpuTopologyProvider;
this.cpuSelectionMigrationService = cpuSelectionMigrationService ?? new CpuSelectionMigrationService();

StoragePaths.EnsureAppDataDirectories();
this.masksFilePath = StoragePaths.CoreMasksFilePath;
Expand Down Expand Up @@ -184,12 +190,17 @@ public async Task SaveMasksAsync()
{
try
{
await this.ApplyCpuSelectionMigrationAsync().ConfigureAwait(false);

var data = this.AvailableMasks.Select(m => new
{
id = m.Id,
name = m.Name,
description = m.Description,
boolMask = m.BoolMask.ToList(),
profileSchemaVersion = m.ProfileSchemaVersion,
cpuSelection = m.CpuSelection,
cpuSelectionMigration = m.CpuSelectionMigration,
isDefault = m.IsDefault,
isEnabled = m.IsEnabled,
createdAt = m.CreatedAt,
Expand Down Expand Up @@ -238,6 +249,9 @@ public async Task LoadMasksAsync()
Id = item.GetProperty("id").GetString() ?? Guid.NewGuid().ToString(),
Name = item.GetProperty("name").GetString() ?? "Unnamed",
Description = item.GetProperty("description").GetString() ?? string.Empty,
ProfileSchemaVersion = item.TryGetProperty("profileSchemaVersion", out var schemaVersion)
? schemaVersion.GetInt32()
: CpuAffinityProfileSchemaVersions.Legacy,
IsDefault = item.GetProperty("isDefault").GetBoolean(),
IsEnabled = item.GetProperty("isEnabled").GetBoolean(),
CreatedAt = item.GetProperty("createdAt").GetDateTime(),
Expand All @@ -250,6 +264,18 @@ public async Task LoadMasksAsync()
mask.BoolMask.Add(bit.GetBoolean());
}

if (item.TryGetProperty("cpuSelection", out var cpuSelectionElement) &&
cpuSelectionElement.ValueKind != JsonValueKind.Null)
{
mask.CpuSelection = cpuSelectionElement.Deserialize<CpuSelection>(JsonOptions);
}

if (item.TryGetProperty("cpuSelectionMigration", out var migrationElement) &&
migrationElement.ValueKind != JsonValueKind.Null)
{
mask.CpuSelectionMigration = migrationElement.Deserialize<CpuSelectionMigrationMetadata>(JsonOptions);
}

this.AvailableMasks.Add(mask);
}
catch (Exception ex)
Expand All @@ -258,6 +284,7 @@ public async Task LoadMasksAsync()
}
}

await this.ApplyCpuSelectionMigrationAsync().ConfigureAwait(false);
this.logger.LogInformation("Loaded {Count} masks from {Path}", this.AvailableMasks.Count, this.masksFilePath);
}
catch (Exception ex)
Expand All @@ -280,6 +307,54 @@ public async Task<bool> IsMaskReferencedByProfilesAsync(string maskId)
}
}

private async Task ApplyCpuSelectionMigrationAsync()
{
var topology = await this.TryGetTopologySnapshotAsync().ConfigureAwait(false);
if (topology == null)
{
return;
}

foreach (var mask in this.AvailableMasks)
{
if (mask.CpuSelection != null)
{
mask.ProfileSchemaVersion = CpuAffinityProfileSchemaVersions.CpuSelection;
continue;
}

if (mask.BoolMask.Count == 0)
{
continue;
}

var migrated = this.cpuSelectionMigrationService.MigrateFromLegacyCoreMask(
mask.BoolMask.ToList(),
topology);
mask.ProfileSchemaVersion = CpuAffinityProfileSchemaVersions.CpuSelection;
mask.CpuSelection = migrated.Selection;
mask.CpuSelectionMigration = migrated.Metadata;
}
}

private async Task<CpuTopologySnapshot?> TryGetTopologySnapshotAsync()
{
if (this.cpuTopologyProvider == null)
{
return null;
}

try
{
return await this.cpuTopologyProvider.GetTopologySnapshotAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
this.logger.LogWarning(ex, "Failed to get CPU topology snapshot for core mask CpuSelection migration");
return null;
}
}

public async Task<bool> IsMaskActivelyAppliedAsync(string maskId)
{
try
Expand Down Expand Up @@ -732,4 +807,3 @@ private CoreMask CreateCoreMaskFromBoolList(string name, List<bool> boolMask, st
}
}
}

24 changes: 24 additions & 0 deletions Services/CpuSelectionMigrationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 ThreadPilot.Models;

public sealed record CpuSelectionMigrationResult(
CpuSelection Selection,
CpuSelectionMigrationMetadata Metadata);
}
Loading
Loading