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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# changelog

## unreleased - panel efficiency

- preserved the installed popup's layout, fonts, themes, controls and navigation.
- kept controls intact on timestamp-only refreshes and reused frozen brushes.
- replaced frequent file checks with debounced notifications and a recovery timer.
- paced live-glass handoff to its 60 Hz capture rate while retaining display-rate
entrance and exit motion; stopped capture workers before reopening.
- bounded panel error logs and closed window-owned timers and capture resources.
- added reproducible, isolated memory, refresh and navigation checks; see
[performance checks](docs/performance.md). collectors and account behavior are
unchanged.

## 0.4.1 - 2026-10-26

- changed **Dashboard** to switch pages inside the existing tray popup. removed
Expand Down
66 changes: 66 additions & 0 deletions docs/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# panel performance checks

Fleet keeps its existing native WPF panel, controls, fonts, themes and collectors.
The panel reuses frozen brushes, keeps its controls on timestamp-only model
updates, and watches cache-file notifications with a one-second recovery timer.
The normal tray-window message path is unchanged. File notifications are debounced
so creating and writing a request does not toggle the popup twice.

Live glass still captures up to 60 frames per second. Its UI handoff now runs only
at that rate, instead of subscribing continuously to display-rate rendering.
Entrance and exit animations retain their display-rate animation. Closing stops
and joins the capture worker; closing the window disposes its watcher, timers and
capture resources. Error logging keeps two bounded files and omits exception
messages that could contain input.

## reproduce

Use a synthetic `panel.json` generated with `panel_model.build`, including
`summary`, `coverage`, `api_accounts` and dashboard history. Do not copy credentials
or a user's raw logs into a fixture. Install `pythonnet` and `psutil` in a test
environment. Both checks require Windows; neither collects provider data.

```powershell
python scripts/measure-panel.py PANEL_EXE SAMPLE_JSON OUTPUT_DIR 3
python scripts/check-panel-performance.py PANEL_EXE SAMPLE_JSON OUTPUT_DIR
python scripts/check-dashboard.py PANEL_EXE SAMPLE_JSON OUTPUT_DIR
```

Use different output directories for each binary and run. Keep all generated
artifacts, virtual environments, package caches, TEMP and TMP on the requested
scratch/output drive. Do not run performance checks concurrently.

`measure-panel.py` starts real standalone panel processes with isolated caches,
samples fresh-hidden, open and closed-after-open states, then requests graceful
exit. Its windows stay offscreen and use the real open/close and capture paths.
It reports private resident memory (USS), total working set, private commit,
process CPU time, handles and threads separately. CPU time is per process, not a
percentage of every logical processor.

`check-panel-performance.py` hosts the same assembly in Python/CLR to exercise 100
timestamp-only refreshes and 100 open/close/navigation cycles. It checks that
changed content still replaces controls, and that coverage, settings, summary and
account changes are not lost. Its memory samples include Python/CLR test overhead
and must not be presented as the standalone app's footprint. Managed allocation
totals include the host's other managed work. No check forces garbage collection
or trims the working set.

## acceptance and limits

Compare identical fixtures, display settings, timing and process age. Separate
startup memory from the memory after interaction. Check late-cycle ranges for
private resident memory, working set, private commit, kernel handles, GDI objects
and threads. A short plateau is evidence for that workload, not a guarantee about
multi-day behavior.

For appearance, render each binary with `--render` against the same synthetic
model and background. Compare usage, dashboard, settings and expanded-provider
pages in both themes. Static pixel parity does not by itself prove animation,
multi-monitor, or live-background parity; use the dashboard interaction check and
a real desktop smoke check as well.

Unit tests should not depend on a developer's real Credential Manager entries.
An unisolated provider-enablement test can discover an already-connected admin
account. For reproducible local tests, mock `api_credentials.read` to return
`None` around test discovery/execution; credential-specific tests use their own
mocks. Never clear real credentials to make a test pass.
30 changes: 21 additions & 9 deletions native/FleetPanel/LiveBackdrop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal sealed class LiveBackdrop
private readonly object gate = new object();
private Thread thread;
private volatile bool running;
private readonly ManualResetEventSlim stopping = new ManualResetEventSlim(false);
private int left, top, width, height;
private byte[] frame;
private int frameWidth, frameHeight;
Expand All @@ -41,6 +42,7 @@ public void Start(int l, int t, int w, int h)
totalMs = 0;
}
running = true;
stopping.Reset();
thread = new Thread(Run) { IsBackground = true, Name = "fleet-glass" };
thread.Start();
}
Expand All @@ -56,7 +58,15 @@ public void Move(int l, int t, int w, int h)
}
}

public void Stop() => running = false;
public void Stop()
{
running = false;
stopping.Set();
// A new open must not revive the previous worker before it exits.
// Wake its paced wait immediately; only an in-flight copy can remain.
thread?.Join();
thread = null;
}

/// <summary>The newest frame, if one arrived since the last call.</summary>
public bool TryTake(ref byte[] target, out int w, out int h, out double lum)
Expand Down Expand Up @@ -100,7 +110,7 @@ private void Run()
}
if (w <= 0 || h <= 0)
{
Thread.Sleep(16);
stopping.Wait(16);
continue;
}
var cw = Math.Max(1, w / ScreenCapture.Downscale);
Expand Down Expand Up @@ -128,7 +138,7 @@ private void Run()
clock.Stop();
if (!ok)
{
Thread.Sleep(16);
stopping.Wait(16);
continue;
}
// Sixty copies a second while something behind moves: under a blur that
Expand All @@ -144,7 +154,7 @@ private void Run()
}
if (rest >= 1)
{
Thread.Sleep((int)Math.Ceiling(rest));
stopping.Wait((int)Math.Ceiling(rest));
}
lock (gate)
{
Expand Down Expand Up @@ -174,16 +184,18 @@ private void Run()

private static void Free(ref IntPtr dc, ref IntPtr dib)
{
if (dib != IntPtr.Zero)
{
Native.DeleteObject(dib);
dib = IntPtr.Zero;
}
// Release the DC's selection before deleting its bitmap. GDI refuses
// DeleteObject while the bitmap is still selected into a live DC.
if (dc != IntPtr.Zero)
{
Native.DeleteDC(dc);
dc = IntPtr.Zero;
}
if (dib != IntPtr.Zero)
{
Native.DeleteObject(dib);
dib = IntPtr.Zero;
}
}
}
}
8 changes: 7 additions & 1 deletion native/FleetPanel/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ namespace FleetPanel
internal sealed class PanelData
{
public double Ts;
// Compare every field except the collection timestamp. Freshness, account
// status and coverage timestamps remain part of the displayed content.
public string ContentKey;
public int Live;
public int Cached;
public List<Provider> Providers = new List<Provider>();
Expand All @@ -27,7 +30,8 @@ internal sealed class PanelData
public static PanelData Load(string path)
{
var text = File.ReadAllText(path);
var root = new JavaScriptSerializer { MaxJsonLength = 8 * 1024 * 1024 }.DeserializeObject(text)
var serializer = new JavaScriptSerializer { MaxJsonLength = 8 * 1024 * 1024 };
var root = serializer.DeserializeObject(text)
as Dictionary<string, object>;
var data = new PanelData();
if (root == null)
Expand Down Expand Up @@ -150,6 +154,8 @@ public static PanelData Load(string path)
data.SkippedArchives = Num(coverage, "skipped_zst"); data.LogUpdated = Num(coverage, "updated");
data.LogSince = Num(coverage, "since");
}
root.Remove("ts");
data.ContentKey = serializer.Serialize(root);
return data;
}

Expand Down
Loading
Loading