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
<PackageReference Include="LeakDetectorSuite.Threading" Version="1.0.0.5" />
<PackageVersion Include="LeakDetectorSuite.Threading" Version="1.0.0.5" />
<PackageReference Include="LeakDetectorSuite.Threading" />
paket add LeakDetectorSuite.Threading --version 1.0.0.5
#r "nuget: LeakDetectorSuite.Threading, 1.0.0.5"
#:package LeakDetectorSuite.Threading@1.0.0.5
#addin nuget:?package=LeakDetectorSuite.Threading&version=1.0.0.5
#tool nuget:?package=LeakDetectorSuite.Threading&version=1.0.0.5
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.
๐ฆ 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:
Page lifecycle hook:
Application.PageAppearingis subscribed. Every page that appears is automatically passed toLeakTracker.Track(page, page.GetType().Name).ViewModel tracking: If the page has a non-null
BindingContext, it is also tracked automatically under its type name.Periodic logging: Every 5 seconds
LeakTracker.LogAlive()fires and prints a live-object count to the debug output.Platform lifecycle: Platform-specific lifecycle hooks (Android
OnCreate, iOSFinishedLaunching, WindowsOnLaunched) 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:
Static event subscription โ the page subscribed to a
static eventand 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; }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; }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 DEBUGbuilds 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.WarnThresholdMsappropriate for your platform (mobile is slower than desktop). - Don't leave
ThreadGuard.ThrowOnViolation = truein production; use it only in automated tests. - Configure
ThreadGuard.MainThreadDetectoron non-MAUI UI stacks before relying onEnsureMainThread()orWarnIfBlockingCall().
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:
- Tap Scenario 1 card on the Home page.
- Tap ๐ธ Take Snapshot Here โ records a
beforebaseline. - Tap โ Go Back (with cleanup) โ
OnDisappearingfires, event is unsubscribed, and the diff runs automatically. - 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:
- Tap Scenario 2 card.
- Tap ๐ธ Take Before Snapshot.
- Tap โ Go Back (NO cleanup).
- On the Home page tap ๐ Force GC.
- 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:
- On the Home page tap ๐ธ Snapshot to take a global before.
- Tap Scenario 3 โ see Instance #1 on screen โ tap โ Go Back.
- Tap Scenario 3 again โ see Instance #2 โ tap โ Go Back.
- Tap Scenario 3 again โ see Instance #3 โ tap โ Go Back.
- 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 | Versions 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. |
-
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.