diff --git a/App.axaml b/App.axaml index 544f4c4..f5363d4 100644 --- a/App.axaml +++ b/App.axaml @@ -16,7 +16,7 @@ - + diff --git a/Endpoints/EngineEndpoints.cs b/Endpoints/EngineEndpoints.cs index 9956dc3..c771173 100644 --- a/Endpoints/EngineEndpoints.cs +++ b/Endpoints/EngineEndpoints.cs @@ -1,4 +1,5 @@ using LocalLLMServerManager.Services; +using LocalLLMServerManager.Shared.Interfaces; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -9,15 +10,24 @@ public static class EngineEndpoints { public static void MapEngineEndpoints(this WebApplication app) { - app.MapGet("/api/gpu/vram", (IGpuTelemetryProvider telemetryProvider) => + app.MapGet("/api/gpu/vram", (IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService) => { - var (gpuName, vramBytes, usedVramBytes) = telemetryProvider.GetGpuInfo(); + return Results.Ok(BuildMemoryTelemetryResponse(telemetryProvider, settingsService)); + }); + + app.MapGet("/api/hardware/memory", (IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService) => + { + return Results.Ok(BuildMemoryTelemetryResponse(telemetryProvider, settingsService)); + }); + + app.MapPost("/api/memory/purge", async (VramOrchestrator orchestrator, IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService, CancellationToken ct) => + { + var eviction = await orchestrator.PurgeAllCachesAsync(stopIdleEngines: true, ct); return Results.Ok(new { - GpuName = gpuName, - VramBytes = vramBytes, - UsedVramBytes = usedVramBytes, - VramGB = Math.Round((double)vramBytes / (1024 * 1024 * 1024), 2) + status = "purged", + eviction, + memory = BuildMemoryTelemetryResponse(telemetryProvider, settingsService) }); }); @@ -43,6 +53,7 @@ public static void MapEngineEndpoints(this WebApplication app) return Results.BadRequest(new { message = $"Invalid or missing ComfyUI executable path: {execPath}" }); } + orchestrator.RecordActivity("comfy"); await orchestrator.EnsureVramForComfyUiAsync(); var success = await engineManager.StartComfyUiAsync(execPath, logger); @@ -60,7 +71,7 @@ public static void MapEngineEndpoints(this WebApplication app) return Results.Ok(new { message = "ComfyUI Stopped" }); }); - app.MapPost("/api/forge/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => + app.MapPost("/api/forge/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => { var logger = loggerFactory.CreateLogger("EngineEndpoints"); var settings = settingsService.LoadSettings(); @@ -71,6 +82,7 @@ public static void MapEngineEndpoints(this WebApplication app) return Results.BadRequest(new { message = $"Invalid or missing SD Forge executable path: {execPath}" }); } + orchestrator.RecordActivity("forge"); var success = await engineManager.StartForgeAsync(execPath, logger); if (success) { @@ -88,11 +100,12 @@ public static void MapEngineEndpoints(this WebApplication app) app.MapPost("/api/comfy/free", async (VramOrchestrator orchestrator) => { + orchestrator.RecordActivity("comfy"); await orchestrator.FreeComfyUiVramAsync(); return Results.Ok(new { message = "ComfyUI VRAM freed" }); }); - app.MapPost("/api/audio/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => + app.MapPost("/api/audio/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => { var logger = loggerFactory.CreateLogger("EngineEndpoints"); var settings = settingsService.LoadSettings(); @@ -106,6 +119,7 @@ public static void MapEngineEndpoints(this WebApplication app) } } + orchestrator.RecordActivity("audio"); var success = await engineManager.StartAudioEngineAsync(execPath, logger); if (success) { @@ -123,12 +137,13 @@ public static void MapEngineEndpoints(this WebApplication app) app.MapGet("/api/audio/voices", HandleGetVoicesAsync); - app.MapPost("/api/ollama/start", async (IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => + app.MapPost("/api/ollama/start", async (VramOrchestrator orchestrator, IAiEngineManager engineManager, ISettingsService settingsService, ILoggerFactory loggerFactory) => { var logger = loggerFactory.CreateLogger("EngineEndpoints"); var settings = settingsService.LoadSettings(); var execPath = AiEngineManager.ResolveOllamaExecutable(settings.OllamaExecutablePath); + orchestrator.RecordActivity("ollama"); var success = await engineManager.StartOllamaAsync(execPath, logger); if (success) { @@ -144,8 +159,9 @@ public static void MapEngineEndpoints(this WebApplication app) return Results.Ok(new { message = "Ollama Stopped" }); }); - app.MapPost("/api/engine/start", async (EngineToggleRequest req, IAiEngineManager engineManager) => + app.MapPost("/api/engine/start", async (EngineToggleRequest req, VramOrchestrator orchestrator, IAiEngineManager engineManager) => { + orchestrator.RecordActivity(req.Engine ?? ""); var result = await engineManager.StartEngineAsync(req.Engine ?? ""); if (result.Success) { @@ -165,6 +181,44 @@ public static void MapEngineEndpoints(this WebApplication app) }); } + public static object BuildMemoryTelemetryResponse(IGpuTelemetryProvider telemetryProvider, ISettingsService settingsService) + { + var (gpuName, vramBytes, usedVramBytes) = telemetryProvider.GetGpuInfo(); + var sysMem = telemetryProvider.GetSystemMemoryInfo(); + var settings = settingsService.LoadSettings(); + double totalVramGb = Math.Round((double)vramBytes / (1024.0 * 1024.0 * 1024.0), 2); + double usedVramGb = Math.Round((double)usedVramBytes / (1024.0 * 1024.0 * 1024.0), 2); + double freeVramGb = Math.Max(0.0, Math.Round(totalVramGb - usedVramGb, 2)); + int usedVramPct = vramBytes > 0 ? (int)Math.Clamp(Math.Round((double)usedVramBytes / vramBytes * 100.0), 0, 100) : 0; + double totalRamGb = Math.Round((double)sysMem.TotalRamBytes / (1024.0 * 1024.0 * 1024.0), 2); + double usedRamGb = Math.Round((double)sysMem.UsedRamBytes / (1024.0 * 1024.0 * 1024.0), 2); + double processWsMb = Math.Round((double)sysMem.ProcessWorkingSetBytes / (1024.0 * 1024.0), 1); + double nonPagedMb = Math.Round((double)sysMem.NonPagedPoolBytes / (1024.0 * 1024.0), 1); + + return new + { + GpuName = gpuName, + VramBytes = vramBytes, + UsedVramBytes = usedVramBytes, + VramGB = totalVramGb, + TotalVramGb = totalVramGb, + UsedVramGb = usedVramGb, + FreeVramGb = freeVramGb, + UsedPercent = usedVramPct, + TotalRamBytes = sysMem.TotalRamBytes, + UsedRamBytes = sysMem.UsedRamBytes, + AvailableRamBytes = sysMem.AvailableRamBytes, + TotalRamGb = totalRamGb, + UsedRamGb = usedRamGb, + RamUsedPercent = sysMem.RamUsedPercent, + ProcessWorkingSetMb = processWsMb, + NonPagedPoolMb = nonPagedMb, + ModelCacheIdleTimeoutMinutes = settings.ModelCacheIdleTimeoutMinutes, + EngineHibernationTimeoutMinutes = settings.EngineHibernationTimeoutMinutes, + RamPressureThresholdPercent = settings.RamPressureThresholdPercent + }; + } + public static async Task HandleGetVoicesAsync(ISettingsService settingsService, System.Net.Http.IHttpClientFactory clientFactory) { var settings = settingsService.LoadSettings(); @@ -197,5 +251,3 @@ public static async Task HandleGetVoicesAsync(ISettingsService settings return Results.Ok(new { voices = defaultVoices, preferred = settings.PreferredAudioVoice }); } } - - diff --git a/Endpoints/HealthEndpoints.cs b/Endpoints/HealthEndpoints.cs index 31a8825..9c7faad 100644 --- a/Endpoints/HealthEndpoints.cs +++ b/Endpoints/HealthEndpoints.cs @@ -29,7 +29,7 @@ public static void MapHealthEndpoints(this WebApplication app) StableDiffusion = forgeHealthy ? "Online" : "Offline", ComfyUI = comfyHealthy ? "Online" : "Offline", PreferredImageEngine = settings.PreferredImageEngine, - Version = "4.0.0", + Version = "4.1.0", }); }); } diff --git a/LocalLLMServerManager.Shared/Interfaces/ITelemetryService.cs b/LocalLLMServerManager.Shared/Interfaces/ITelemetryService.cs index 870cee2..25641e1 100644 --- a/LocalLLMServerManager.Shared/Interfaces/ITelemetryService.cs +++ b/LocalLLMServerManager.Shared/Interfaces/ITelemetryService.cs @@ -7,7 +7,15 @@ public record GpuTelemetryInfo( double UsedVramGb, double TotalVramGb, double FreeVramGb, - int Percent + int Percent, + double UsedRamGb = 0.0, + double TotalRamGb = 32.0, + int RamPercent = 0, + double ProcessWorkingSetMb = 0.0, + double NonPagedPoolMb = 0.0, + int ModelCacheIdleTimeoutMinutes = 15, + int EngineHibernationTimeoutMinutes = 30, + int RamPressureThresholdPercent = 75 ); public interface ITelemetryService diff --git a/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj b/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj index 5fe6bb2..bf0ab73 100644 --- a/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj +++ b/LocalLLMServerManager.Shared/LocalLLMServerManager.Shared.csproj @@ -4,9 +4,9 @@ net10.0 enable enable - 4.0.0 - 4.0.0.0 - 4.0.0.0 + 4.1.0 + 4.1.0.0 + 4.1.0.0 true diff --git a/LocalLLMServerManager.Shared/Models/AppSettings.cs b/LocalLLMServerManager.Shared/Models/AppSettings.cs index 191a4fc..cba174f 100644 --- a/LocalLLMServerManager.Shared/Models/AppSettings.cs +++ b/LocalLLMServerManager.Shared/Models/AppSettings.cs @@ -32,7 +32,8 @@ public record AppSettings( string AiAssistantApiKey = "", string AiAssistantModel = "google/gemini-2.5-flash", string AiAssistantCustomSystemPrompt = "", - string AiAssistantPromptsDirectory = "" + string AiAssistantPromptsDirectory = "", + int ModelCacheIdleTimeoutMinutes = 15, + int EngineHibernationTimeoutMinutes = 30, + int RamPressureThresholdPercent = 75 ); - - diff --git a/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs b/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs index 12bbdbe..24e6378 100644 --- a/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs +++ b/LocalLLMServerManager.Shared/Models/EngineStatusCardModel.cs @@ -84,6 +84,12 @@ public class TelemetryData public double TotalVramGb { get; set; } = 16.0; public int VramPercent { get; set; } + public double UsedRamGb { get; set; } = 0.0; + public double TotalRamGb { get; set; } = 32.0; + public int RamPercent { get; set; } = 0; + public double ProcessWorkingSetMb { get; set; } = 0.0; + public double NonPagedPoolMb { get; set; } = 0.0; + public TelemetryData() { } diff --git a/LocalLLMServerManager.Shared/Services/TelemetryService.cs b/LocalLLMServerManager.Shared/Services/TelemetryService.cs index 9d8de10..20efd92 100644 --- a/LocalLLMServerManager.Shared/Services/TelemetryService.cs +++ b/LocalLLMServerManager.Shared/Services/TelemetryService.cs @@ -16,18 +16,68 @@ public async Task QueryGpuVramAsync(string apiBase, HttpClient if (resp.IsSuccessStatusCode) { var doc = JsonNode.Parse(await resp.Content.ReadAsStringAsync()); - string gpuName = doc?["gpuName"]?.ToString() ?? "GPU Telemetry Active"; - double used = doc?["usedVramGb"]?.GetValue() ?? 0.0; - double total = doc?["totalVramGb"]?.GetValue() ?? 16.0; - double free = doc?["freeVramGb"]?.GetValue() ?? (total - used); - int pct = doc?["usedPercent"]?.GetValue() ?? (int)Math.Round((used / (total > 0 ? total : 1.0)) * 100.0); + string gpuName = doc?["gpuName"]?.ToString() + ?? doc?["GpuName"]?.ToString() + ?? "GPU Telemetry Active"; - return new GpuTelemetryInfo(gpuName, used, total, free, pct); + double totalVram = TryGetDouble(doc, "totalVramGb", "TotalVramGb") + ?? TryGetDouble(doc, "vramGB", "VramGB") + ?? (TryGetDouble(doc, "vramBytes", "VramBytes") is double vb ? Math.Round(vb / (1024.0 * 1024.0 * 1024.0), 2) : 16.0); + + double usedVram = TryGetDouble(doc, "usedVramGb", "UsedVramGb") + ?? (TryGetDouble(doc, "usedVramBytes", "UsedVramBytes") is double uvb ? Math.Round(uvb / (1024.0 * 1024.0 * 1024.0), 2) : 0.0); + + double freeVram = TryGetDouble(doc, "freeVramGb", "FreeVramGb") + ?? Math.Max(0.0, Math.Round(totalVram - usedVram, 2)); + + int pct = TryGetInt(doc, "usedPercent", "UsedPercent") + ?? (int)Math.Round((usedVram / (totalVram > 0 ? totalVram : 1.0)) * 100.0); + + double totalRam = TryGetDouble(doc, "totalRamGb", "TotalRamGb") ?? 32.0; + double usedRam = TryGetDouble(doc, "usedRamGb", "UsedRamGb") ?? 0.0; + int ramPct = TryGetInt(doc, "ramUsedPercent", "RamUsedPercent") + ?? (int)Math.Round((usedRam / (totalRam > 0 ? totalRam : 1.0)) * 100.0); + + double processWs = TryGetDouble(doc, "processWorkingSetMb", "ProcessWorkingSetMb") ?? 0.0; + double nonPaged = TryGetDouble(doc, "nonPagedPoolMb", "NonPagedPoolMb") ?? 0.0; + int modelCacheTimeout = TryGetInt(doc, "modelCacheIdleTimeoutMinutes", "ModelCacheIdleTimeoutMinutes") ?? 15; + int hibernationTimeout = TryGetInt(doc, "engineHibernationTimeoutMinutes", "EngineHibernationTimeoutMinutes") ?? 30; + int ramPressureThreshold = TryGetInt(doc, "ramPressureThresholdPercent", "RamPressureThresholdPercent") ?? 75; + + return new GpuTelemetryInfo( + gpuName, + usedVram, + totalVram, + freeVram, + pct, + usedRam, + totalRam, + ramPct, + processWs, + nonPaged, + modelCacheTimeout, + hibernationTimeout, + ramPressureThreshold + ); } } catch { } - return new GpuTelemetryInfo("GPU Telemetry Active", 0.0, 16.0, 16.0, 0); + return new GpuTelemetryInfo("GPU Telemetry Active", 0.0, 16.0, 16.0, 0, 0.0, 32.0, 0, 0.0, 0.0, 15, 30, 75); + } + + private static double? TryGetDouble(JsonNode? node, string camel, string pascal) + { + var target = node?[camel] ?? node?[pascal]; + if (target == null) return null; + try { return target.GetValue(); } catch { return null; } + } + + private static int? TryGetInt(JsonNode? node, string camel, string pascal) + { + var target = node?[camel] ?? node?[pascal]; + if (target == null) return null; + try { return target.GetValue(); } catch { return null; } } public async Task<(bool Ollama, bool Forge, bool ComfyUi)> CheckServiceHealthAsync(string apiBase, string comfyUrl, HttpClient http) diff --git a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml index 4eaccb9..be3f013 100644 --- a/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml +++ b/LocalLLMServerManager.Shared/Styles/DesignTokens.axaml @@ -52,6 +52,7 @@ M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm3-3c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z + M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z diff --git a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs index 98270b6..5ba2fd3 100644 --- a/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/SettingsViewModel.cs @@ -73,6 +73,17 @@ public void SelectCategory(string category) [ObservableProperty] private string _audioEngineUrl = "http://127.0.0.1:8880"; [ObservableProperty] private string _preferredAudioVoice = "af_heart"; + [ObservableProperty] private int _modelCacheIdleTimeoutMinutes = 15; + [ObservableProperty] private int _engineHibernationTimeoutMinutes = 30; + [ObservableProperty] private int _ramPressureThresholdPercent = 75; + + [ObservableProperty] private string _audioPath = ""; + [ObservableProperty] private string _videoModelsPath = ""; + [ObservableProperty] private string _videoOutputPath = ""; + [ObservableProperty] private bool _aiAssistantEnabled; + [ObservableProperty] private string _aiAssistantCustomSystemPrompt = ""; + [ObservableProperty] private string _aiAssistantPromptsDirectory = ""; + [ObservableProperty] private string _aiAssistantEndpoint = "http://127.0.0.1:4000/v1"; [ObservableProperty] private string _aiAssistantApiKey = ""; [ObservableProperty] private string _aiAssistantModel = "google/gemini-2.5-flash"; @@ -885,6 +896,16 @@ public async Task LoadSettingsAsync(string apiBase, HttpClient http) AiAssistantApiKey = settings.AiAssistantApiKey ?? ""; AiAssistantModel = settings.AiAssistantModel ?? "google/gemini-2.5-flash"; + AudioPath = settings.AudioPath ?? ""; + VideoModelsPath = settings.VideoModelsPath ?? ""; + VideoOutputPath = settings.VideoOutputPath ?? ""; + AiAssistantEnabled = settings.AiAssistantEnabled; + AiAssistantCustomSystemPrompt = settings.AiAssistantCustomSystemPrompt ?? ""; + AiAssistantPromptsDirectory = settings.AiAssistantPromptsDirectory ?? ""; + ModelCacheIdleTimeoutMinutes = settings.ModelCacheIdleTimeoutMinutes; + EngineHibernationTimeoutMinutes = settings.EngineHibernationTimeoutMinutes; + RamPressureThresholdPercent = settings.RamPressureThresholdPercent; + if (settings.CustomPresets != null && settings.CustomPresets.Count > 0) { foreach (var preset in settings.CustomPresets) @@ -936,15 +957,24 @@ public async Task SaveSettingsAsync(string apiBase, HttpClient http) ServiceName: this.ServiceName, PublishOutputPath: this.PublishOutputPath, ComfyModelsPath: this.ComfyModelsPath, + AudioPath: this.AudioPath, LanAccessUrl: this.LanAccessUrl, SelectedThemeStyle: this.SelectedThemeStyle, AudioEngineExecutablePath: this.AudioEngineExecutablePath, AudioEngineUrl: this.AudioEngineUrl, PreferredAudioVoice: this.PreferredAudioVoice, + VideoModelsPath: this.VideoModelsPath, + VideoOutputPath: this.VideoOutputPath, CustomPresets: customPresets, + AiAssistantEnabled: this.AiAssistantEnabled, AiAssistantEndpoint: this.AiAssistantEndpoint, AiAssistantApiKey: this.AiAssistantApiKey, - AiAssistantModel: this.AiAssistantModel + AiAssistantModel: this.AiAssistantModel, + AiAssistantCustomSystemPrompt: this.AiAssistantCustomSystemPrompt, + AiAssistantPromptsDirectory: this.AiAssistantPromptsDirectory, + ModelCacheIdleTimeoutMinutes: this.ModelCacheIdleTimeoutMinutes, + EngineHibernationTimeoutMinutes: this.EngineHibernationTimeoutMinutes, + RamPressureThresholdPercent: this.RamPressureThresholdPercent ); var content = new StringContent( diff --git a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs index a4abec0..0eadaa8 100644 --- a/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs +++ b/LocalLLMServerManager.Shared/ViewModels/TelemetryViewModel.cs @@ -14,11 +14,94 @@ public partial class TelemetryViewModel : ObservableObject private readonly ITelemetryService _telemetryService; [ObservableProperty] private bool _isCollapsed = false; - [ObservableProperty] private string _gpuName = "GPU Telemetry Active"; - [ObservableProperty] private double _vramUsedGb = 0.0; - [ObservableProperty] private double _vramTotalGb = 16.0; - [ObservableProperty] private double _vramPercentage = 0.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private string _gpuName = "GPU Telemetry Active"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _vramUsedGb = 0.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _vramTotalGb = 16.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _vramPercentage = 0.0; + [ObservableProperty] private string _vramStatusText = "0.0 GB / 16.0 GB (0%)"; + [ObservableProperty] private string _vramCompactText = "0.0/16.0G"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _ramUsedGb = 0.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _ramTotalGb = 32.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(RamBarColor))] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _ramPercentage = 0.0; + + [ObservableProperty] private string _ramStatusText = "0.0 GB / 32.0 GB (0%)"; + [ObservableProperty] private string _ramCompactText = "0.0/32.0G"; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _processWorkingSetMb = 0.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private double _nonPagedPoolMb = 0.0; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private int _modelCacheIdleTimeoutMinutes = 15; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private int _engineHibernationTimeoutMinutes = 30; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(RamBarColor))] + [NotifyPropertyChangedFor(nameof(HardwareTooltipText))] + private int _ramPressureThresholdPercent = 75; + + public string RamBarColor => RamPercentage >= 90 + ? "#EF4444" + : (RamPercentage >= RamPressureThresholdPercent ? "#F59E0B" : "#6366F1"); + + public string HardwareTooltipText => + $"{GpuName}\n" + + $"VRAM: {VramUsedGb:0.0} / {VramTotalGb:0.0} GB ({VramPercentage:0}%)\n" + + $"Host RAM: {RamUsedGb:0.0} / {RamTotalGb:0.0} GB ({RamPercentage:0}%)\n" + + $"App + Engine Working Set: {ProcessWorkingSetMb:0.0} MB\n" + + $"Kernel Non-Paged Pool: {NonPagedPoolMb:0.0} MB\n" + + $"Cache Lifetime: {ModelCacheIdleTimeoutMinutes}m Unload / {EngineHibernationTimeoutMinutes}m Hibernate / {RamPressureThresholdPercent}% Pressure"; + + partial void OnVramUsedGbChanged(double value) => UpdateVramStrings(); + partial void OnVramTotalGbChanged(double value) => UpdateVramStrings(); + partial void OnVramPercentageChanged(double value) => UpdateVramStrings(); + + private void UpdateVramStrings() + { + VramStatusText = $"{VramUsedGb:0.0} GB / {VramTotalGb:0.0} GB ({VramPercentage:0}%)"; + VramCompactText = $"{VramUsedGb:0.0}/{VramTotalGb:0.0}G"; + } + + partial void OnRamUsedGbChanged(double value) => UpdateRamStrings(); + partial void OnRamTotalGbChanged(double value) => UpdateRamStrings(); + partial void OnRamPercentageChanged(double value) => UpdateRamStrings(); + + private void UpdateRamStrings() + { + RamStatusText = $"{RamUsedGb:0.0} GB / {RamTotalGb:0.0} GB ({RamPercentage:0}%)"; + RamCompactText = $"{RamUsedGb:0.0}/{RamTotalGb:0.0}G"; + } [ObservableProperty] private EngineStatusCardModel _ollamaCard = new("ollama", "Ollama", 11434); [ObservableProperty] private EngineStatusCardModel _comfyUiCard = new("comfyui", "ComfyUI", 8188); @@ -385,6 +468,17 @@ public void UpdateFromTelemetry(TelemetryData data) VramUsedGb = data.UsedVramGb; VramPercentage = data.VramPercent > 0 ? data.VramPercent : Math.Round((VramUsedGb / VramTotalGb) * 100.0, 1); VramStatusText = $"{VramUsedGb} GB / {VramTotalGb} GB ({VramPercentage}%)"; + VramCompactText = $"{VramUsedGb:0.0}/{VramTotalGb:0.0}G"; + } + if (data.TotalRamGb > 0) + { + RamTotalGb = data.TotalRamGb; + RamUsedGb = data.UsedRamGb; + RamPercentage = data.RamPercent > 0 ? data.RamPercent : Math.Round((RamUsedGb / RamTotalGb) * 100.0, 1); + RamStatusText = $"{RamUsedGb:0.0} GB / {RamTotalGb:0.0} GB ({RamPercentage:0}%)"; + RamCompactText = $"{RamUsedGb:0.0}/{RamTotalGb:0.0}G"; + ProcessWorkingSetMb = data.ProcessWorkingSetMb; + NonPagedPoolMb = data.NonPagedPoolMb; } IsServiceRunning = (OllamaCard?.IsOnline ?? false) || (ComfyUiCard?.IsOnline ?? false) || (ForgeCard?.IsOnline ?? false) || (KokoroCard?.IsOnline ?? false); @@ -394,7 +488,8 @@ public void UpdateFromTelemetry(TelemetryData data) [RelayCommand] public async Task RefreshStatusAsync() { - await RefreshStatusAsync(ApiBase, "http://127.0.0.1:8188", HttpHelper.CreateClient(ApiBase)); + using var http = HttpHelper.CreateClient(ApiBase); + await RefreshStatusAsync(ApiBase, "http://127.0.0.1:8188", http); } public async Task RefreshStatusAsync(string apiBase, string comfyUrl, HttpClient http) @@ -445,5 +540,49 @@ public async Task CheckGpuVramAsync(string apiBase, HttpClient http) VramUsedGb = info.UsedVramGb; VramPercentage = info.Percent; VramStatusText = $"{VramUsedGb} GB / {VramTotalGb} GB ({VramPercentage}%)"; + VramCompactText = $"{VramUsedGb:0.0}/{VramTotalGb:0.0}G"; + + RamTotalGb = info.TotalRamGb; + RamUsedGb = info.UsedRamGb; + RamPercentage = info.RamPercent; + RamStatusText = $"{RamUsedGb:0.0} GB / {RamTotalGb:0.0} GB ({RamPercentage:0}%)"; + RamCompactText = $"{RamUsedGb:0.0}/{RamTotalGb:0.0}G"; + ProcessWorkingSetMb = info.ProcessWorkingSetMb; + NonPagedPoolMb = info.NonPagedPoolMb; + ModelCacheIdleTimeoutMinutes = info.ModelCacheIdleTimeoutMinutes; + EngineHibernationTimeoutMinutes = info.EngineHibernationTimeoutMinutes; + RamPressureThresholdPercent = info.RamPressureThresholdPercent; + } + + [RelayCommand] + public async Task PurgeMemoryAsync() + { + using var http = HttpHelper.CreateClient(ApiBase); + await PurgeMemoryAsync(ApiBase, http); + } + + public async Task PurgeMemoryAsync(string apiBase, HttpClient http) + { + try + { + var url = HttpHelper.FormatEndpoint(apiBase, "/api/memory/purge"); + var resp = await http.PostAsync(url, null); + if (resp.IsSuccessStatusCode) + { + ToastService.Instance.Show("Memory caches purged.", ToastType.Success, 2500); + } + else + { + ToastService.Instance.Show("Failed to purge memory caches.", ToastType.Warning, 2500); + } + } + catch (Exception ex) + { + ToastService.Instance.Show($"Error purging memory: {ex.Message}", ToastType.Error, 3000); + } + finally + { + await CheckGpuVramAsync(apiBase, http); + } } } diff --git a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml index 38e788c..405da58 100644 --- a/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml +++ b/LocalLLMServerManager.Shared/Views/Controls/SettingsTabControl.axaml @@ -147,7 +147,7 @@ - + @@ -493,6 +493,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +