LeakDetectorSuite.Threading 1.0.0.5

dotnet add package LeakDetectorSuite.Threading --version 1.0.0.5
                    
NuGet\Install-Package LeakDetectorSuite.Threading -Version 1.0.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LeakDetectorSuite.Threading" Version="1.0.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LeakDetectorSuite.Threading" Version="1.0.0.5" />
                    
Directory.Packages.props
<PackageReference Include="LeakDetectorSuite.Threading" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LeakDetectorSuite.Threading --version 1.0.0.5
                    
#r "nuget: LeakDetectorSuite.Threading, 1.0.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LeakDetectorSuite.Threading@1.0.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LeakDetectorSuite.Threading&version=1.0.0.5
                    
Install as a Cake Addin
#tool nuget:?package=LeakDetectorSuite.Threading&version=1.0.0.5
                    
Install as a Cake Tool

LeakDetectorSuite

Runtime diagnostics toolkit for .NET and MAUI โ€” detect memory leaks, profile performance, and enforce thread-safety checks with lightweight core libraries and a separate MAUI integration package.

NuGet License: MIT .NET net10.0


๐Ÿ“ฆ Packages

Package Description Target
LeakDetectorSuite.Memory Core memory leak engine net8.0 net9.0 net10.0
LeakDetectorSuite.Maui MAUI integration & auto page tracking net10.0-android net10.0-ios net10.0-maccatalyst net10.0-windows10.0.19041.0
LeakDetectorSuite.Performance Execution time profiler with warnings net8.0 net9.0 net10.0
LeakDetectorSuite.Threading UI thread guard & blocking-call detector net8.0 net9.0 net10.0

1. Overview

LeakDetectorSuite is a lightweight, production-ready diagnostics library suite designed to surface common runtime problems in .NET applications โ€” especially MAUI mobile apps โ€” during development and testing:

  • ๐Ÿง  Memory Leaks: Track objects with weak references. Compare snapshots before/after navigation to find objects that should have been collected.
  • โฑ Performance: Measure execution time of any sync or async operation. Get automatic warnings when operations exceed your threshold.
  • ๐Ÿงต Threading: Guard UI operations, detect background-thread UI access, and warn about blocking async calls.

LeakDetectorSuite.Memory, LeakDetectorSuite.Performance, and LeakDetectorSuite.Threading ship without NuGet dependencies and target net8.0, net9.0, and net10.0. LeakDetectorSuite.Maui targets MAUI on .NET 10 and depends on Microsoft.Maui.Controls plus LeakDetectorSuite.Memory.


2. Installation

# Full MAUI integration (includes Memory engine)
dotnet add package LeakDetectorSuite.Maui

# Core memory engine only (non-MAUI .NET apps)
dotnet add package LeakDetectorSuite.Memory

# Performance profiler
dotnet add package LeakDetectorSuite.Performance

# Thread safety guards
dotnet add package LeakDetectorSuite.Threading

The published package IDs use the LeakDetectorSuite.* prefix. The code namespaces stay as LeakDetector.*, so your using statements do not change.


3. Quick Start โ€” MAUI

In MauiProgram.cs:

using LeakDetector.Maui;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder
            .UseMauiApp<App>()
            .UseLeakDetector();   // โ† add this line

        return builder.Build();
    }
}

Once registered, LeakDetector automatically tracks every page and its BindingContext when it appears, and logs a live-object summary every 5 seconds to the debug output.


4. Usage Examples

Memory Leak Detection

using LeakDetector.Memory;

// Take a snapshot before an action
var before = LeakTracker.Snapshot();

// โ€ฆ navigate, create objects, etc. โ€ฆ

// Take a snapshot after
var after = LeakTracker.Snapshot();

// Compare and print diff
var diff = LeakTracker.Compare(before, after);
Debug.WriteLine(diff);
// Output:
// [LeakDetector] Snapshot diff (ฮ”=+2):
//   [HomePage] ฮ”=+1  (after=1)
//   [HomePageViewModel] ฮ”=+1  (after=1)

Manual object tracking:

// Track a specific object with an optional tag
LeakTracker.Track(myService, "MyService");

// Force a full GC, then inspect what's still alive
LeakTracker.ForceGC();
LeakTracker.LogAlive();

// Get a programmatic list of alive objects
var alive = LeakTracker.GetAliveObjects();
foreach (var (tag, obj) in alive)
    Debug.WriteLine($"{tag}: {obj}");

Custom logger:

// Route messages to your preferred logger
LeakTracker.Logger = msg => MyLogger.Log(msg);

Performance Profiling

using LeakDetector.Performance;

// Sync measurement
Perf.Measure(() => SomeMethod(), "SomeMethod");
// Output: [Perf] SomeMethod: 42.17 ms

// Auto-warn when over threshold (default 100 ms)
Perf.Measure(() => Thread.Sleep(200), "SlowOp");
// Output: โš ๏ธ  [Perf WARNING] SlowOp: 201.44 ms (threshold=100 ms)

// Return a value
var result = Perf.Measure<string>(() => ComputeName(), "ComputeName");

// Async variants
await Perf.MeasureAsync(async () => await FetchDataAsync(), "FetchData");
var data = await Perf.MeasureAsync<List<Item>>(async () => await LoadAsync(), "Load");

// Adjust warning threshold
Perf.WarnThresholdMs = 50;

Thread Safety

using LeakDetector.Threading;

// Assert we are on the UI thread (logs warning if we're not)
ThreadGuard.EnsureMainThread();

// Check for possible blocking calls in async-aware contexts
ThreadGuard.WarnIfBlockingCall();

// Throw instead of just log
ThreadGuard.ThrowOnViolation = true;
ThreadGuard.EnsureMainThread(); // throws InvalidOperationException on background threads

// Query directly
bool onMain = ThreadGuard.IsMainThread();

For non-MAUI targets, ThreadGuard does not guess which thread is the UI thread. Configure ThreadGuard.MainThreadDetector first if you want reliable UI-thread assertions in WPF, WinForms, or another desktop framework.

ThreadGuard.MainThreadDetector = () => MyUiDispatcher.CheckAccess();

5. MAUI Automatic Tracking Behavior

When UseLeakDetector() is called:

  1. Page lifecycle hook: Application.PageAppearing is subscribed. Every page that appears is automatically passed to LeakTracker.Track(page, page.GetType().Name).

  2. ViewModel tracking: If the page has a non-null BindingContext, it is also tracked automatically under its type name.

  3. Periodic logging: Every 5 seconds LeakTracker.LogAlive() fires and prints a live-object count to the debug output.

  4. Platform lifecycle: Platform-specific lifecycle hooks (Android OnCreate, iOS FinishedLaunching, Windows OnLaunched) log a confirmation message.


6. Reading the Output

LeakDetector writes all messages to the debug output (Android logcat, Xcode console, Visual Studio Output window). This section explains every message type, what state it indicates, and what action to take.


6.1 Startup Messages

[LeakDetector] Registered. Tracking will begin once app launches.

What it means: UseLeakDetector() was called successfully inside MauiProgram.CreateMauiApp().
Action needed: None. This is a confirmation that the library is wired up correctly.


[LeakDetector] Android lifecycle hooked.
[LeakDetector] iOS/Mac lifecycle hooked.
[LeakDetector] Windows lifecycle hooked.

What it means: The platform-specific app lifecycle event fired (OnCreate / FinishedLaunching / OnLaunched) and LeakDetector has activated its host. Page tracking is now live.
Action needed: None. If you do NOT see this line, UseLeakDetector() was not called or the MAUI workload is not installed.


6.2 Tracking Messages

[LeakDetector] Tracking: HomePage
[LeakDetector] Tracking: HomePageViewModel

What it means: The page HomePage appeared on screen. LeakDetector registered both the page and its BindingContext (the ViewModel) as weakly-tracked objects. They are now included in all future snapshots.
Action needed: None. Every tracked entry is expected. If a type you expect to see is missing, the page's BindingContext was null when it appeared โ€” wire up the ViewModel in the constructor before InitializeComponent().


6.3 Periodic Live-Object Report (every 5 seconds)

[LeakDetector] โ”€โ”€ Live Objects โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
[LeakDetector]   HomePage: 1
[LeakDetector]   HomePageViewModel: 1
[LeakDetector] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

What it means: 5 seconds have passed. The GC has not collected HomePage or HomePageViewModel โ€” they are still in memory. The number is the count of live instances for that type.

Count What it signals
1 while page is visible โœ… Normal โ€” the page is currently on screen
1 after navigating away + GC โŒ Leak โ€” something is still holding a reference
2 for a page you only opened once โŒ Leak โ€” previous instance was never collected
0 for any type โœ… Collected โ€” the GC reclaimed the object as expected

Tip: Call LeakTracker.ForceGC() before reading this report in development. Otherwise, the GC may not have run yet and all objects will still show 1 even if they are unreachable.


6.4 Snapshot Diff โ€” the most important output

Produced by LeakTracker.Compare(before, after):

โœ… Clean navigation (no leaks)
[LeakDetector] No changes detected.

What it means: Exactly the same types and counts are alive in both snapshots. Every object created during navigation was properly collected.
Action needed: None.


โš ๏ธ Objects grew โ€” possible leak
[LeakDetector] Snapshot diff (ฮ”=+2):
  [HomePage] ฮ”=+1  (after=1)
  [HomePageViewModel] ฮ”=+1  (after=1)

What it means, field by field:

Field Meaning
ฮ”=+2 (header) Total net increase across all tracked types in this snapshot window
[HomePage] The type name of the leaked object
ฮ”=+1 One more instance of HomePage is alive now than before
(after=1) There is currently 1 live HomePage instance

What caused this: Something is holding a strong reference to HomePage and its ViewModel. The most common causes:

  1. Static event subscription โ€” the page subscribed to a static event and never unsubscribed. The static event's delegate list holds a reference to the page forever.

    // โŒ Common leak pattern
    SomeService.StaticEvent += OnSomething; // keeps 'this' alive
    
    // โœ… Fix: unsubscribe when the page is done
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        SomeService.StaticEvent -= OnSomething;
    }
    
  2. Long-lived service holds a page reference โ€” a singleton injected into the page stores a callback or reference.

    // โŒ Leak: singleton captures 'this'
    _myService.OnUpdate = () => UpdateUI(); // 'this' is captured in closure
    
    // โœ… Fix: clear the callback on disappearing
    protected override void OnDisappearing()
    {
        _myService.OnUpdate = null;
    }
    
  3. BindingContext not released โ€” the ViewModel is subscribed to a service event and was never told to clean up.

    // โœ… Fix: implement IDisposable on ViewModel and unsubscribe there
    public void Dispose() => SomeService.Changed -= OnChanged;
    

โœ… Objects released โ€” healthy
[LeakDetector] Snapshot diff (ฮ”=-2):
  [SecondPage] ฮ”=-1  (after=0)
  [SecondPageViewModel] ฮ”=-1  (after=0)

What it means: Objects that existed in the before snapshot were collected. Count went down. This is the expected outcome after navigating away from a page.
Action needed: None. Negative deltas are healthy.


โš ๏ธ Count growing across multiple navigations
[LeakDetector] Snapshot diff (ฮ”=+4):
  [ProductPage] ฮ”=+4  (after=4)
  [ProductViewModel] ฮ”=+4  (after=4)

What it means: You navigated to ProductPage four times and all four instances are still in memory. The count growing proportionally with navigation count is a strong indicator of a hard reference held by a shared/singleton resource (e.g. a message bus, an event aggregator, or a static collection).
Fix pattern:

// Check for subscriptions to any shared event bus or messenger
// โŒ Leak: each navigation creates a new subscriber, old ones never removed
MessagingCenter.Subscribe<App, string>(this, "Refresh", OnRefresh);

// โœ… Fix: unsubscribe in OnDisappearing or in page Dispose
protected override void OnDisappearing()
{
    MessagingCenter.Unsubscribe<App, string>(this, "Refresh");
}

6.5 GC and Performance Messages

[LeakDetector] GC forced.

What it means: LeakTracker.ForceGC() was called. The runtime ran a full blocking garbage collection across all generations and waited for all pending finalizers. Any object with no remaining strong references is now collected.
When to call it: Always call this before taking an after snapshot or reading the live-object report. Without this, the GC may not have run yet and previously unreachable objects can still appear as alive, producing false positives.


[LeakDetector] Tracker reset.

What it means: LeakTracker.Reset() was called. All tracked entries and the ID counter are cleared.
When to use: Between isolated test runs to ensure a clean baseline. Do not use in production flows.


[LeakDetector] No live tracked objects.

What it means: The periodic 5-second timer fired but GetAliveObjects() returned zero results. All tracked objects have been collected.
Action needed: None โ€” this is the ideal state after navigating away from all pages.


6.6 Performance Output

[Perf] LoadData: 34.82 ms

What it means: The measured operation completed in 34.82 ms, which is under the warning threshold (default 100 ms). No action needed.


โš ๏ธ  [Perf WARNING] FetchProducts: 312.50 ms (threshold=100 ms)

What it means: FetchProducts took 312 ms โ€” more than 3ร— the threshold. This is slow enough to cause noticeable UI lag on mobile devices (anything over ~16 ms per frame is perceptible).

Common causes and fixes:

Root cause Fix
Synchronous network call on UI thread Move to async/await with HttpClient
Blocking database read Use async EF Core queries or SQLite-net async API
Heavy JSON deserialization Cache the result, or deserialize on a background thread
Image loading inline Use CommunityToolkit.Maui async image loading

Adjust the threshold for your platform:

Perf.WarnThresholdMs = 50;  // stricter โ€” for performance-sensitive paths
Perf.WarnThresholdMs = 500; // looser โ€” for cold-start one-time operations

6.7 Thread Safety Output

[ThreadGuard] โš ๏ธ  NOT on main thread! Called from 'LoadData' in ProductViewModel.cs:47 (Thread #6)

What it means: Code that must run on the UI thread (e.g. updating an ObservableCollection, changing a label) was called from Thread #6, which is a background/thread pool thread. This can cause UI crashes or unpredictable rendering bugs.

Fix:

// โŒ Wrong โ€” updating UI from a background task
await Task.Run(() => Items.Clear()); // crashes on MAUI

// โœ… Fix โ€” dispatch UI updates back to the main thread
await Task.Run(() =>
{
    var results = FetchFromDatabase();
    MainThread.BeginInvokeOnMainThread(() => Items.ReplaceRange(results));
});

[ThreadGuard] โš ๏ธ  Possible blocking call detected! A SynchronizationContext is active but code is running on Thread #5. Avoid .Wait() / .Result in 'GetUser' (UserService.cs:83).

What it means: You called .Wait() or .Result on a Task while a SynchronizationContext is active. This is a classic async deadlock setup โ€” the continuation needs the main thread to resume, but the main thread is blocked waiting for the result.

Fix:

// โŒ Deadlock risk
var user = GetUserAsync().Result;

// โœ… Fix โ€” always await async methods
var user = await GetUserAsync();

7. Best Practices

  • Use in #if DEBUG builds only. The tracker adds minor overhead (GC pressure from WeakReferences).
  • Snapshot before and after navigation to isolate which types are leaking.
  • Call LeakTracker.ForceGC() before diffing to ensure the GC has had a chance to collect unreachable objects.
  • Unsubscribe static events when pages are navigated away โ€” these are the most common MAUI leak source.
  • Keep Perf.WarnThresholdMs appropriate for your platform (mobile is slower than desktop).
  • Don't leave ThreadGuard.ThrowOnViolation = true in production; use it only in automated tests.
  • Configure ThreadGuard.MainThreadDetector on non-MAUI UI stacks before relying on EnsureMainThread() or WarnIfBlockingCall().

8. Architecture

LeakDetectorSuite/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ LeakDetectorSuite.Memory/          # Core engine (net8/9/10)
โ”‚   โ”‚   โ”œโ”€โ”€ LeakTracker.cs                 # WeakReference tracking, snapshots
โ”‚   โ”‚   โ”œโ”€โ”€ LeakSnapshot.cs                # Immutable snapshot value object
โ”‚   โ”‚   โ””โ”€โ”€ SnapshotDiff.cs                # Diff between two snapshots
โ”‚   โ”œโ”€โ”€ LeakDetectorSuite.Maui/            # MAUI integration (net10.0-*)
โ”‚   โ”‚   โ”œโ”€โ”€ LeakDetectorHost.cs            # Lifecycle hooks + 5-second timer
โ”‚   โ”‚   โ””โ”€โ”€ LeakDetectorMauiExtensions.cs  # UseLeakDetector() extension
โ”‚   โ”œโ”€โ”€ LeakDetectorSuite.Performance/     # Profiler (net8/9/10)
โ”‚   โ”‚   โ””โ”€โ”€ Perf.cs                        # Measure(), MeasureAsync()
โ”‚   โ””โ”€โ”€ LeakDetectorSuite.Threading/       # Thread guard (net8/9/10)
โ”‚       โ””โ”€โ”€ ThreadGuard.cs                 # EnsureMainThread(), WarnIfBlockingCall()
โ””โ”€โ”€ samples/
    โ””โ”€โ”€ LeakDetectorSuite.Demo/            # Full MAUI interactive demo app
        โ”œโ”€โ”€ Services/
        โ”‚   โ””โ”€โ”€ DiagnosticsService.cs      # In-app log capture & ObservableCollection
        โ”œโ”€โ”€ Pages/
        โ”‚   โ”œโ”€โ”€ HomePage.xaml(.cs)         # Scenario card hub + global tools
        โ”‚   โ”œโ”€โ”€ DiagnosticsPage.xaml(.cs)  # Live color-coded log viewer
        โ”‚   โ”œโ”€โ”€ Scenario1CleanNavPage      # Proper OnDisappearing cleanup โ†’ no leak
        โ”‚   โ”œโ”€โ”€ Scenario2LeakyPage         # Static event leak (no cleanup)
        โ”‚   โ”œโ”€โ”€ Scenario3MultiNavPage      # Growing leak via repeated navigation
        โ”‚   โ”œโ”€โ”€ Scenario4PerfPage          # Fast/slow ops + threshold tuning
        โ”‚   โ”œโ”€โ”€ Scenario5ThreadingPage     # Wrong thread + blocking call detection
        โ”‚   โ””โ”€โ”€ Scenario6SnapshotPage      # Step-by-step manual snapshot cycle
        โ””โ”€โ”€ ViewModels/                    # Tracked automatically by LeakDetectorHost

9. Roadmap

  • Debug overlay โ€” floating in-app panel showing live object counts
  • Roslyn analyzer โ€” warn at compile time about common leak patterns (static events, closures)
  • CI integration โ€” MSTest/XUnit assertions via LeakTracker.AssertNoLeaks(before, after)
  • Blazor Hybrid support โ€” track components and DI-scoped services
  • Export to JSON โ€” snapshot diffs exportable for CI artifact storage

10. Demo App โ€” Full Walkthrough

The LeakDetectorSuite.Demo MAUI app is a self-contained interactive playground that demonstrates every README output scenario on-screen. You do not need Android Studio, Xcode, or a log viewer โ€” all output is captured and displayed live inside the app itself.


10.1 How Output Gets Into the App

In MauiProgram.cs, all three library loggers are wired to a single DiagnosticsService before the app is built:

var diagnostics = DiagnosticsService.Instance;

builder.UseLeakDetector(diagnostics.Log);  // LeakTracker messages
Perf.Logger        = diagnostics.Log;       // [Perf] messages
ThreadGuard.Logger = diagnostics.Log;       // [ThreadGuard] messages

DiagnosticsService stores entries in an ObservableCollection<DiagnosticEntry> (newest first) that the Diagnostics Log page binds to directly. Every message that would normally appear in logcat or the VS Output window now appears on-screen, color-coded:

Color Meaning
๐Ÿ”ต Soft blue-gray Normal info โ€” tracking started, GC ran, snapshot taken
๐ŸŸ  Orange Warning โ€” slow operation, possible blocking call
๐Ÿ”ด Red Problem โ€” NOT on main thread, unexpected error

10.2 Home Page โ€” The Scenario Hub

The home page is a card-based navigator. Each card has a colored left-accent stripe matching the scenario's severity:

Stripe color Scenario Meaning
๐ŸŸข Green S1 โ€” Clean Navigation Expected correct pattern
๐Ÿ”ด Red S2 โ€” Static Event Leak Intentional bad pattern
๐ŸŸ  Orange S3 โ€” Growing Leak Bad pattern that accumulates
๐Ÿ”ต Blue S4 โ€” Performance Measurement and thresholds
๐ŸŸฃ Purple S5 โ€” Thread Safety Threading violation detection
๐Ÿฉต Teal S6 โ€” Snapshot Comparison Full manual snapshot API

Global Tools strip at the top of the Home page:

Button What it does
๐Ÿ—‘ Force GC Calls LeakTracker.ForceGC() โ€” use after navigating back to verify collection
๐Ÿ“ธ Snapshot Takes a before baseline snapshot โ€” tap Force GC then this again to see the diff
๐Ÿ”„ Reset Clears all tracking state and the Scenario 3 instance counter

10.3 Scenario 1 โ€” Clean Navigation โœ…

What it proves: Proper OnDisappearing cleanup prevents memory leaks entirely.

The code pattern:

// constructor:
CleanStaticEvent += OnCleanEvent;   // subscribe

// OnDisappearing:
CleanStaticEvent -= OnCleanEvent;   // โœ… unsubscribe before GC

How to run:

  1. Tap Scenario 1 card on the Home page.
  2. Tap ๐Ÿ“ธ Take Snapshot Here โ€” records a before baseline.
  3. Tap โ† Go Back (with cleanup) โ€” OnDisappearing fires, event is unsubscribed, and the diff runs automatically.
  4. Open the Diagnostics Log โ€” you will see:
[S1] โœ… OnDisappearing: unsubscribed from static event.
[LeakDetector] GC forced.
[LeakDetector] No changes detected. โœ…
[S1] โœ… No leak โ€” page was properly collected.

10.4 Scenario 2 โ€” Static Event Leak โŒ

What it proves: A single missing -= on a static event is enough to keep an entire page + ViewModel in memory indefinitely.

The code pattern:

// constructor:
LeakyStaticEvent += OnLeakyEvent;   // subscribe
// โŒ No OnDisappearing โ€” event never unsubscribed

How to run:

  1. Tap Scenario 2 card.
  2. Tap ๐Ÿ“ธ Take Before Snapshot.
  3. Tap โ† Go Back (NO cleanup).
  4. On the Home page tap ๐Ÿ—‘ Force GC.
  5. Open the Diagnostics Log โ€” you will see:
[S2] Page constructed โ€” subscribed to static event (no cleanup planned).
[S2] Navigating back WITHOUT cleanup (leak incoming)...
[LeakDetector] GC forced.
[LeakDetector] Snapshot diff (ฮ”=+1):
  [Scenario2LeakyPage] ฮ”=+1  (after=1)
[S2] โŒ LEAK DETECTED โ€” page still alive after GC!

What to fix: Add LeakyStaticEvent -= OnLeakyEvent; inside OnDisappearing().


10.5 Scenario 3 โ€” Growing Leak (Repeated Navigation) โš ๏ธ

What it proves: A leak that appears once will accumulate with every navigation. After 3 visits, 3 instances are alive simultaneously.

How to run:

  1. On the Home page tap ๐Ÿ“ธ Snapshot to take a global before.
  2. Tap Scenario 3 โ†’ see Instance #1 on screen โ†’ tap โ† Go Back.
  3. Tap Scenario 3 again โ†’ see Instance #2 โ†’ tap โ† Go Back.
  4. Tap Scenario 3 again โ†’ see Instance #3 โ†’ tap โ† Go Back.
  5. Tap ๐Ÿ—‘ Force GC โ†’ the diff is computed automatically:
[Home] Auto-snapshot before S3 navigation.
[S3] Instance #1 created and subscribed to static event (no cleanup).
[S3] Instance #1 navigating back โ€” reference still held by event.
[S3] Instance #2 created ...
[S3] Instance #3 created ...
[LeakDetector] GC forced.
[LeakDetector] Snapshot diff (ฮ”=+3):
  [Scenario3MultiNavPage] ฮ”=+3  (after=3)

The instance counter (#1, #2, #3) visible on each page visit makes it obvious that new objects are being created while the old ones are never released.


10.6 Scenario 4 โ€” Performance Profiling โฑ

What it proves: Perf.MeasureAsync gives precise timing and auto-warns when you exceed the threshold โ€” no manual stopwatch needed.

How to run โ€” demo sequence:

Button Operation duration Expected output
โšก Fast Operation 30 ms [Perf] FastOperation: 30.14 ms
๐Ÿข Slow Operation 350 ms โš ๏ธ [Perf WARNING] SlowOperation: 351.22 ms (threshold=100 ms)
Set threshold โ†’ 20 ms โ€” [S4] Threshold set to 20 ms (strict mode).
Run 50 ms operation 50 ms โš ๏ธ [Perf WARNING] MediumOperation: 51.08 ms (threshold=20 ms)
Reset threshold โ†’ 100 ms โ€” [S4] Threshold reset to 100 ms (default).

All output appears live in the Diagnostics Log page. Notice the threshold display at the top of the page updates in real-time as you change it.


10.7 Scenario 5 โ€” Thread Safety Guards ๐Ÿงต

What it proves: ThreadGuard catches both wrong-thread UI access and potential async deadlock patterns โ€” pinpointing the exact file and line number.

Three sub-scenarios:

โœ… Correct โ€” Main thread check (silent pass)
[S5] โœ… EnsureMainThread passed โ€” we are on Thread #1 (main thread).

Button click handlers always run on the UI thread. This confirms the guard is working and is silent when correct.

โŒ Wrong thread โ€” EnsureMainThread from Task.Run
[S5] Dispatching EnsureMainThread() to a background thread (Task.Run)...
[S5] Now running on Thread #6 (background). Calling EnsureMainThread()...
[ThreadGuard] โš ๏ธ  NOT on main thread! Called from 'OnWrongThread' in
              Scenario5ThreadingPage.xaml.cs:41 (Thread #6)

This is the warning you would see if you updated an ObservableCollection or changed a label directly from inside Task.Run.

โŒ Blocking call โ€” .Wait()/.Result pattern
[S5] On Thread #8 with SynchronizationContext installed. Calling WarnIfBlockingCall()...
[ThreadGuard] โš ๏ธ  Possible blocking call detected! A SynchronizationContext is
              active but code is running on Thread #8. Avoid .Wait()/.Result
              in 'OnBlockingCall' (Scenario5ThreadingPage.xaml.cs:70).

The demo creates a LongRunning OS thread, installs a SynchronizationContext on it (simulating a captured context), then calls WarnIfBlockingCall() โ€” matching exactly the conditions under which a deadlock would occur in a real app.


10.8 Scenario 6 โ€” Manual Snapshot Comparison ๐Ÿ“ธ

What it proves: You can precisely control when snapshots are taken and compare them to see exactly which types grew or shrank.

Step-by-step flow (buttons enable sequentially):

Step Button What happens Output
1 Take Before Snapshot ForceGC() then Snapshot() [S6] Before snapshot โ€” 0 DemoObjects alive
2 Create 3 Tracked Objects Creates 3 DemoObject instances, calls LeakTracker.Track() on each [S6] Tracked 3 DemoObjects (strong refs held)
3 Release + Force GC Clears the list, runs ForceGC() [S6] Released all strong refs + GC forced
4 Take After + Compare Snapshot() then Compare() [LeakDetector] No changes detected. โœ…

To intentionally trigger a leak in this scenario: Run Steps 1 โ†’ 2 โ†’ 4 (skip Step 3). The output will show:

[LeakDetector] Snapshot diff (ฮ”=+3):
  [DemoObject] ฮ”=+3  (after=3)
[S6] โŒ Leaks detected โ€” some DemoObjects are still in memory.

Because you kept strong references in the list, the GC cannot collect them, and the diff correctly reports them as growth.


10.9 Key Design Decisions

Decision Why
DiagnosticsService.Instance (singleton) All 3 library loggers (LeakTracker, Perf, ThreadGuard) route into one place. No DI setup required โ€” any page accesses DiagnosticsService.Instance.Log(...) directly.
Live in-app log You never need logcat or the VS Output window. The color-coded CollectionView is visible on any device or emulator without connecting to a debugger.
Color-coded entries ๐Ÿ”ต Info / ๐ŸŸ  Warning / ๐Ÿ”ด Problem allows instant visual triage of dozens of log lines without reading every message.
Scenario 1 vs 2 contrast Both pages use the exact same static event pattern. S1 adds one OnDisappearing override with a single -=. Same code structure, completely opposite GC outcome โ€” making the difference impossible to miss.
Scenario 3 instance counter Each visit shows #1, #2, #3 on-screen. The number is a static int incremented in the constructor. It makes it visually obvious that new objects are created while old ones survive โ€” the growing leak becomes intuitive.
Scenario 5 LongRunning thread Task.Run strips the SynchronizationContext, so WarnIfBlockingCall would not fire from it. Using TaskCreationOptions.LongRunning creates a real OS thread we fully control โ€” we install the context manually to reproduce the exact conditions of a .Wait() deadlock.
Scenario 6 step machine Buttons are enabled/disabled sequentially (Steps 1โ†’2โ†’3โ†’4). This prevents out-of-order execution and guides the user through the correct snapshot workflow, matching the README documentation exactly.

License

MIT โ€” see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0.5 106 4/9/2026
1.0.0.4 113 4/9/2026
1.0.0 105 4/9/2026