From 4aa11dda9c7560b5f7c50272e2d354dfa95bbc40 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Fri, 15 May 2026 17:47:15 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20code=20quality=20=E2=80=94=20paint=20lea?= =?UTF-8?q?k,=20stable=20chart=20offset,=20PerformanceVM=20dispose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CQ-M1: Dispose SkiaSharp paint objects when removing ping targets - CQ-M2: Use stable host-hash offset instead of IndexOf for chart lines - CQ-M4: PerformanceViewModel overrides Dispose --- CHANGELOG.md | 11 +++++ .../ViewModels/NetworkSharedState.cs | 41 ++++++++++++++++--- .../ViewModels/PerformanceViewModel.cs | 12 ++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d64297..2a7c6269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.48.27] - 2026-05-15 + +### Fixed +- **NetworkSharedState** — SkiaSharp `SolidColorPaint` objects are now disposed + when a ping target is removed, preventing unmanaged memory leaks (CQ-M1). +- **NetworkSharedState** — latency chart offset now uses a stable hash of the + target host instead of `Targets.IndexOf`, preventing visual jumps when + targets are removed mid-session (CQ-M2). +- **PerformanceViewModel** — added `Dispose` override to clean up snapshot + reference and satisfy the base class disposal contract (CQ-M4). + ## [0.48.26] - 2026-05-15 ### Changed diff --git a/SysManager/SysManager/ViewModels/NetworkSharedState.cs b/SysManager/SysManager/ViewModels/NetworkSharedState.cs index 15bf4cf6..23d1452b 100644 --- a/SysManager/SysManager/ViewModels/NetworkSharedState.cs +++ b/SysManager/SysManager/ViewModels/NetworkSharedState.cs @@ -225,10 +225,23 @@ internal void RemoveTargetInternal(PingTarget target) _targetHandlers.Remove(target.Host); } Targets.Remove(target); + + // CQ-M1: Dispose SkiaSharp paint objects attached to the series being removed. + // Without this, SolidColorPaint instances (and their unmanaged SKPaint handles) + // leak every time a target is removed. var idx = LatencySeries.ToList().FindIndex(s => s.Name?.Contains($"({target.Host})") == true); - if (idx >= 0) LatencySeries.RemoveAt(idx); + if (idx >= 0) + { + DisposeSeries(LatencySeries[idx]); + LatencySeries.RemoveAt(idx); + } var tIdx = TraceSeries.ToList().FindIndex(s => s.Name?.Contains($"({target.Host})") == true); - if (tIdx >= 0) TraceSeries.RemoveAt(tIdx); + if (tIdx >= 0) + { + DisposeSeries(TraceSeries[tIdx]); + TraceSeries.RemoveAt(tIdx); + } + Buffers.TryRemove(target.Host, out _); TraceBuffers.TryRemove(target.Host, out _); LatestRoutes.Remove(target.Host); @@ -237,6 +250,18 @@ internal void RemoveTargetInternal(PingTarget target) RefreshHopTable(); } + /// Disposes paint resources attached to a chart series. + private static void DisposeSeries(ISeries series) + { + if (series is LineSeries line) + { + (line.Stroke as IDisposable)?.Dispose(); + (line.GeometryStroke as IDisposable)?.Dispose(); + (line.GeometryFill as IDisposable)?.Dispose(); + (line.Fill as IDisposable)?.Dispose(); + } + } + public void ClearHistory() { foreach (var buf in Buffers.Values) buf.Clear(); @@ -313,8 +338,9 @@ internal void FlushPending() double? shown = sample.LatencyMs; if (shown.HasValue) { - var idx = Targets.IndexOf(target); - var offset = ((idx % 8) - 3.5) * 0.25; + // CQ-M2: Stable offset (same fix as RecomputeStats). + var stableIdx = Math.Abs(target.Host.GetHashCode()) % 8; + var offset = ((stableIdx % 8) - 3.5) * 0.25; shown = shown.Value + offset; } @@ -355,8 +381,11 @@ private void RecomputeStats(PingTarget target, ObservableCollection