# ThinkGeo Support Ticket ## Summary `MapPrinterLayer.DrawCore()` deadlocks the UI thread when a page layout map's `PrinterInteractiveOverlay` is refreshed (`MapView.RefreshAsync`) while the parent map contains at least one async-capable layer (a layer derived from `WmtsAsyncLayer`). The application hangs permanently — not slow, not eventually recovers, permanently blocked — requiring the process to be killed. We have reproduced this consistently and captured full-process memory dumps showing an identical deadlock signature across multiple independent repro runs. ## Environment - ThinkGeo.Core.dll: 14.5.2 - ThinkGeo.Printers.dll: 14.5.2 - ThinkGeo.UI.WinForms.dll: 14.5.2 - ThinkGeo.Gdal.dll / ThinkGeo.SqlServer.dll: 14.5.2 - (Also present in the same process, unrelated legacy layers: ThinkGeo.MapSuite.dll 10.6.24, ThinkGeo.MapSuite.Layers.Printers.dll 10.6.18 — not implicated in this issue, listed for completeness since both `MapSuite.Layers.MapPrinterLayer` and `Core.MapPrinterLayer` types exist side by side in our dependency tree; this report concerns `ThinkGeo.Core.MapPrinterLayer` only) - .NET Framework 4.8 - Visual Basic .NET, WinForms application - Visual Studio 2022 - Windows 11 ## Reproduction steps 1. Create a map (`ThinkGeo.UI.WinForms.MapView`/`CustomMapView`) containing: - At least one `WmtsAsyncLayer`-derived layer (in our case a custom subclass adding WMTS/Planet basemap support), **and** - A page layout / print layer setup: a `PrinterInteractiveOverlay` containing a `MapPrinterLayer`-derived layer whose `ParentMap` is the map above, with `DrawingMode = MapPrinterDrawingMode.Vector`. 2. Switch the application's active view from the map view to the page layout (print) view. This triggers, on the UI thread: ``` Await mapPrinterLayerCollection... (page layout maps).Open() ' or similar Await pageLayoutMapView.RefreshAsync(printerOverlay) ``` (Our internal method is `RefreshMapExtents`, invoked from a toolbar-click event handler chain — full call chain below.) 3. The call to `RefreshAsync` never returns. The UI thread is permanently blocked. Windows may still report the process as "responding" intermittently, but the UI is completely frozen and never recovers. **Critical variable:** if the map contains **only** synchronous feature layers (e.g. our `SqlFeatureLayer`-derived layers) and no `WmtsAsyncLayer`-derived layer, the exact same action completes normally with no hang. Adding even a single `WmtsAsyncLayer`-derived layer to the map is sufficient to reproduce the deadlock every time. We also confirmed this is not caused by any fire-and-forget/async-void pattern on our side: we changed our calling code so the entire chain from the button-click handler down to `RefreshAsync` is properly `Await`-chained end-to-end (no unawaited `Async Sub` in the path). The hang is identical either way — because, as the stack trace below shows, the call never reaches a real `await` suspension point before it deadlocks; it blocks synchronously inside `RefreshAsync` itself. ## Stack trace (UI thread, captured via `dotnet-dump`/SOS from a full process dump) ``` OS Thread Id: System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object) System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken) System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) ThinkGeo.Core.MapPrinterLayer.BkY=(System.Func`1) <-- blocking Task.Wait() here ThinkGeo.Core.MapPrinterLayer.BEY=(z0U=, GeographyUnit, RectangleShape, RectangleShape, Collection`1 labelsInAllLayers, Double, Double) ThinkGeo.Core.MapPrinterLayer.DrawCore(GeoCanvas, Collection`1) MapUtilities.CustomMapPrinterLayer.DrawCore(GeoCanvas, Collection`1) <-- our override, calls MyBase.DrawCore ThinkGeo.Core.Layer.wyA=(GeoCanvas, Collection`1) ThinkGeo.Core.Layer.Draw(GeoCanvas, Collection`1) ThinkGeo.UI.WinForms.LayerTileView+d__19.MoveNext() System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start[[LayerTileView+d__19]](...) ThinkGeo.UI.WinForms.LayerTileView.9iA=(GeoCanvas, LayerBase) ThinkGeo.UI.WinForms.LayerTileView+d__18.MoveNext() ThinkGeo.UI.WinForms.LayerTileView.DrawAsyncCore(GeoCanvas) ThinkGeo.UI.WinForms.TileView+d__143.MoveNext() ThinkGeo.UI.WinForms.TileView.DrawAsync(GeoCanvas) ThinkGeo.UI.WinForms.TileView+d__134.MoveNext() ThinkGeo.UI.WinForms.TileView.qkg=(Single, GeographyUnit, CancellationToken) ThinkGeo.UI.WinForms.PrinterInteractiveOverlay+d__53.MoveNext() ThinkGeo.UI.WinForms.PrinterInteractiveOverlay.lR4=(LayerTileView, OverlayRefreshType, Boolean) ThinkGeo.UI.WinForms.PrinterInteractiveOverlay+d__51.MoveNext() ThinkGeo.UI.WinForms.PrinterInteractiveOverlay.DrawAsyncCore(RectangleShape, OverlayRefreshType, CancellationToken) ThinkGeo.UI.WinForms.Overlay+d__56.MoveNext() ThinkGeo.UI.WinForms.Overlay.Ikg=(RectangleShape, OverlayRefreshType, CancellationToken) ThinkGeo.UI.WinForms.MapViewBase+d__360.MoveNext() ThinkGeo.UI.WinForms.MapViewBase.j0c=(Overlay, RectangleShape, OverlayRefreshType, Boolean, CancellationToken) ThinkGeo.UI.WinForms.MapViewBase+d__354.MoveNext() ThinkGeo.UI.WinForms.MapViewBase.iUc=(IEnumerable`1, RectangleShape, OverlayRefreshType, Boolean, CancellationToken) ThinkGeo.UI.WinForms.MapViewBase+d__260.MoveNext() ThinkGeo.UI.WinForms.MapViewBase.d0c=(RectangleShape, IEnumerable`1, OverlayRefreshType, CancellationToken) ThinkGeo.UI.WinForms.MapView+d__143.MoveNext() ThinkGeo.UI.WinForms.MapView.RefreshAsync(Overlay, OverlayRefreshType, CancellationToken) <-- our call site, properly Awaited [our application code: SilvicsPageLayout.RefreshMapExtents() -> mSwitchActiveView()] ``` Note: member names like `BkY=`, `BEY=`, `wyA=`, `9iA=`, `lR4=`, `Ikg=`, `j0c=`, `iUc=`, `d0c=`, `qkg=` appear to be obfuscated/minified internal member names as resolved from the release DLL's metadata — we don't have your source, so we can't map these to real member names, but the parameter shapes should let your team identify the actual methods (e.g. `BEY=` takes `(canvas, GeographyUnit, drawingBoundingBox As RectangleShape, adjustedWorldExtent As RectangleShape, labelsInAllLayers As Collection(Of SimpleCandidate), width As Double, height As Double)`, strongly suggesting cross-layer label/collision placement logic). ## Root cause analysis 1. `MapView.RefreshAsync` kicks off `MapViewBase.RefreshInternalAsync` → `DrawOverlaysAsync` → ... → `LayerTileView.DrawAsyncCore`, all genuinely `async`/`Task`-returning methods. 2. However, none of these intermediate async methods actually suspend (`AsyncTaskMethodBuilder.Start` appears repeatedly in the stack, with no corresponding "resumed continuation" frames) — the entire chain runs synchronously in one unbroken call on the calling (UI) thread, all the way down into `LayerTileView.DrawLayerAsync`, which calls the layer's **synchronous** `Layer.Draw()` → `DrawCore()`. 3. `MapPrinterLayer.DrawCore()` internally calls a label-placement routine (`BEY=`) that, in turn, sets a property (`BkY=`) accepting a `Func(Of Task)` — and blocks synchronously on that task (`Task.InternalWait` → `ManualResetEventSlim.Wait` → `Monitor.ObjWait`, the classic `Task.Wait()`/`.Result` sync-over-async pattern). 4. This blocking wait executes **on the UI thread**, which owns the `SynchronizationContext` that WinForms uses to marshal continuations. If the `Func(Of Task)` being waited on needs that same `SynchronizationContext` to post its continuation (which is exactly what happens by default for any `await` inside code that doesn't use `ConfigureAwait(false)`), the UI thread can never process that continuation, because it is currently blocked synchronously waiting for it. Self-deadlock. 5. This is only reachable when there is something for `BkY=`'s task to genuinely wait on — which lines up exactly with our empirical finding that a `WmtsAsyncLayer`-derived layer (performing real async HTTP tile fetches) in the same map is necessary and sufficient to trigger it. With only synchronous feature layers present, nothing forces the wrapped task to suspend, so the wait resolves synchronously/immediately and no deadlock occurs. ## What we ruled out on our end - **Not an async-void/fire-and-forget bug in our code.** We restructured our call chain so `RefreshAsync` is reached via a fully `Await`-chained path with no unawaited `Async Sub` anywhere upstream. The deadlock is identical, because (per the stack trace) execution never reaches a real suspension point before blocking — `Await` provides no protection here. - **Not fixable by moving the call off the UI thread.** We tried wrapping the `RefreshAsync` call in `Task.Run(...)` to get the blocking wait off the UI thread. This "fixes" the hang but immediately throws: ``` System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it. at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.DependencyObject.SetValueInternal(DependencyProperty dp, Object value) at ThinkGeo.UI.WinForms.MapViewBase.i0c=(Double angle, ScreenPointF pivot) at ThinkGeo.UI.WinForms.MapViewBase.d__354.MoveNext() ``` This shows that the **same** `DrawOverlaysAsync` call graph also touches a WPF `DependencyObject` (apparently a rotation/compass-related visual hosted via WPF/WindowsFormsIntegration) that requires actual UI-thread `Dispatcher` affinity. So one part of this call graph must run on the UI thread (the WPF interop code), while another part (`MapPrinterLayer.DrawCore`'s blocking wait) must not run on the UI thread to avoid deadlocking — there is no single thread that satisfies both constraints from the calling application's side. This looks like an internal inconsistency in the library itself, not something we can route around by choosing which thread to call from. - **`DrawingMode` is already `Vector`** on our page layout maps (not `Raster` or `Default`), so this isn't a raster-caching code path issue. - **`PreviewDrawingMode` is left at `Default`** (we never set it) — we have not found documentation on what this controls and didn't want to guess blindly, but if there's a mode that skips the cross-layer label-collision computation we'd welcome guidance on whether it's relevant here. ## Request 1. Please confirm whether `MapPrinterLayer.DrawCore()`'s internal label-placement logic is expected to synchronously block on other layers' async draw/load tasks, and whether that's intended to be safe to call from a UI thread when an async layer is present in the same view. 2. Is there a supported way to either (a) make this wait non-blocking/async all the way up, or (b) opt a `MapPrinterLayer` out of the cross-layer label collision computation (`labelsInAllLayers`) so it doesn't need to wait on sibling async layers at all? 3. We are building a minimal standalone repro project (WinForms + one `MapPrinterLayer` + one WMTS-backed async layer) and will attach it separately. We're happy to provide full process memory dumps (.dmp, ~700MB-1.6GB) via a secure channel if that would help your engineers diagnose this faster than the excerpted stack trace above.