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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **MemoryTestService** — `ManagementObject` instances in `GetModulesAsync` WMI
query are now properly disposed via `using (mo)` block, preventing native handle
leaks when enumerating physical memory modules.
- **NetworkSharedState** — `Dispose()` now fully releases all SkiaSharp paint
resources: series paints (stroke, geometry, fill), axis paints (name, labels,
separators), and class-level legend/tooltip paints. Previously only typefaces
were disposed, leaking unmanaged `SKPaint` handles.

### Added
- **ServicesViewModelTests** — 20 unit tests covering ApplyFilter logic: category
filters (All, Running, Stopped, Safe to disable, Advanced), text search by name/
Expand Down
21 changes: 12 additions & 9 deletions SysManager/SysManager/Services/MemoryTestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,19 @@ public async Task<IReadOnlyList<MemoryModuleHealth>> GetModulesAsync()
"SELECT BankLabel, DeviceLocator, Manufacturer, Capacity, Speed, ConfiguredClockSpeed, PartNumber FROM Win32_PhysicalMemory");
foreach (ManagementObject mo in s.Get())
{
double cap = Convert.ToDouble(mo["Capacity"] ?? 0) / 1024d / 1024d / 1024d;
list.Add(new MemoryModuleHealth
using (mo)
{
Slot = mo["DeviceLocator"]?.ToString() ?? mo["BankLabel"]?.ToString() ?? "",
Manufacturer = (mo["Manufacturer"]?.ToString() ?? "").Trim(),
CapacityGB = Math.Round(cap, 0),
SpeedMHz = Convert.ToUInt32(mo["Speed"] ?? 0u),
ConfiguredSpeedMHz = Convert.ToUInt32(mo["ConfiguredClockSpeed"] ?? 0u),
PartNumber = (mo["PartNumber"]?.ToString() ?? "").Trim()
});
double cap = Convert.ToDouble(mo["Capacity"] ?? 0) / 1024d / 1024d / 1024d;
list.Add(new MemoryModuleHealth
{
Slot = mo["DeviceLocator"]?.ToString() ?? mo["BankLabel"]?.ToString() ?? "",
Manufacturer = (mo["Manufacturer"]?.ToString() ?? "").Trim(),
CapacityGB = Math.Round(cap, 0),
SpeedMHz = Convert.ToUInt32(mo["Speed"] ?? 0u),
ConfiguredSpeedMHz = Convert.ToUInt32(mo["ConfiguredClockSpeed"] ?? 0u),
PartNumber = (mo["PartNumber"]?.ToString() ?? "").Trim()
});
}
}
}
catch (ManagementException) { /* WMI class not available */ }
Expand Down
36 changes: 32 additions & 4 deletions SysManager/SysManager/ViewModels/NetworkSharedState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ private static void DisposeSeries(ISeries series)
(line.GeometryFill as IDisposable)?.Dispose();
(line.Fill as IDisposable)?.Dispose();
}
else if (series is LineSeries<ObservablePoint> traceLine)
{
(traceLine.Stroke as IDisposable)?.Dispose();
(traceLine.GeometryStroke as IDisposable)?.Dispose();
(traceLine.GeometryFill as IDisposable)?.Dispose();
(traceLine.Fill as IDisposable)?.Dispose();
}
}

public void ClearHistory()
Expand Down Expand Up @@ -551,10 +558,31 @@ public void Dispose()
TraceMonitor.Dispose();
FlushTimer?.Stop();

// Dispose SKTypeface (unmanaged SkiaSharp memory) — LEAK-003
// Dispose series paint objects (unmanaged SkiaSharp handles)
foreach (var series in LatencySeries) DisposeSeries(series);
foreach (var series in TraceSeries) DisposeSeries(series);

// Dispose axis paint objects
DisposeAxisPaints(LatencyXAxes);
DisposeAxisPaints(LatencyYAxes);
DisposeAxisPaints(TraceXAxes);
DisposeAxisPaints(TraceYAxes);

// Dispose class-level paint objects and their typefaces
LegendTextPaint.SKTypeface?.Dispose();
LegendBackgroundPaint.SKTypeface?.Dispose();
TooltipTextPaint.SKTypeface?.Dispose();
TooltipBackgroundPaint.SKTypeface?.Dispose();
(LegendTextPaint as IDisposable)?.Dispose();
(LegendBackgroundPaint as IDisposable)?.Dispose();
(TooltipTextPaint as IDisposable)?.Dispose();
(TooltipBackgroundPaint as IDisposable)?.Dispose();
}

private static void DisposeAxisPaints(Axis[] axes)
{
foreach (var axis in axes)
{
(axis.NamePaint as IDisposable)?.Dispose();
(axis.LabelsPaint as IDisposable)?.Dispose();
(axis.SeparatorsPaint as IDisposable)?.Dispose();
}
}
}
Loading