GUI is killed by Windows during long computations: the main thread runs the calculation and never pumps messages
Symptom
Computing a multi-minute item from the GUI makes GeoDmsGuiQt go "Not Responding". Windows
ghosts the window and the process is closed. There is no crash: WER reports
EventType=AppHangB1, IsFatal=1, Hang Type=134217728, with UI[3]=GeoDmsGuiQt.exe is not responding / UI[5]=Close the program, no exception code and no faulting module. Users report
this as "the GUI crashed", because they walk away and come back to a closed application.
Reproduced with /MaakOntkoppeldeData/Write_FinalMutationTable (Stikstof cfg, 2.325.426 rows).
Headless the same item completes fine: GeoDmsRun exit 0 in 146 s (125 s of computation).
This is distinct from #1156 (EmptyWorkingSet storm). #1156 is fixed and made the run 27 %
faster, but it does not fix this: even with zero EmptyWorkingSet calls, a computation of
several minutes freezes the GUI, because Windows ghosts a top-level window after ~5 s without a
GetMessage/PeekMessage on its owning thread.
Mechanism
SetMainThreadID (rtc/dll/src/act/MainThread.cpp:82-114, called from init_geodms,
main_qt.cpp:120) sets both sMainThreadID and sMetaThreadID to the Qt GUI thread, so
IsMetaThread() == IsMainThread(). Operator payloads run inline on it whenever doASync is
false (OperationContext.cpp:2047; Schedule(..., runDirect=true) at :1367), and Join()
also steals and executes tasks via TryRunningTaskInline() (:1537).
The design is cooperative: UPDATE_TIMER_ID (100 ms) → DataView::OnTimer → UpdateView() →
GVS_Break → re-arm (shv/dll/src/dataview.cpp:983-1029). That works for the pure
SuspendibleUpdate walk. It breaks whenever a SuspendTrigger Blocker is on the stack:
// rtc/dll/src/act/TriggerOperator.cpp:387
bool MustSuspend() noexcept
{
if (BlockerBase::IsBlocked())
return false; // no yield, ever
With the yield disabled the main thread parks in polling loops that never peek the message
queue:
OperationContext::Join() — cv_TaskCompleted.wait_for(500 ms) at OperationContext.cpp:2550;
the only escape (return task_status::suspended at :2543) is guarded by MustSuspend().
tile_task_group::AwaitRunningSlots() — m_TileTasksDone.wait_for(500 ms) at :498, entered
from tile_task_group::Join() :506, which installs its own SilentBlocker. Reached from
parallel_for / parallel_tileloop (ParallelTiles.h:185-215), i.e. from most tiled operators.
treeitem_production_task::lock_unique — untimed cv_lockrelease.wait(...) at
ItemLocks.cpp:60; assert(IsMetaThread() || oc.expired()) at :51 documents it as a
main-thread call.
The most important bypass is WaitForReadyOrSuspendTrigger (ItemLocks.cpp:813-865):
if (SuspendTrigger::BlockerBase::IsBlocked())
return WaitReady(item); // :820 — skips the whole suspend/retry loop
WaitReady is just ItemReadLock lock(item); → lock_shared → producer->Join().
Blockers reachable from ordinary GUI interaction
| Site |
Trigger |
shv/dll/src/GraphicObject.cpp:293 FencedBlocker("@GraphicObject::PrepareDataOrUpdateViewLater") |
every MapView/TableView layer data prep |
rtc/dll/src/act/MainThread.cpp:237 SilentBlocker("operation_queue::Process") |
every PostMainThreadOper callback |
rtc/dll/src/tic/TicInterface.cpp:666 FencedBlocker("TreeItem_GetProgressState") |
treeview icon/colour, every paint (DmsTreeView.cpp:508,563) |
shv/dll/src/ShvDllInterface.cpp:459 FencedBlocker("SHV_GetViewStyleFlags") |
every treeview paint (DmsTreeView.cpp:228) |
rtc/dll/src/tic/DataLocks.h:145 PreparedDataReadLock : SuspendTrigger::FencedBlocker, DataReadLock |
~40 call sites; runs PrepareDataUsage(Certain) in the member-init list |
rtc/dll/src/act/Actor.cpp:561-573 CertainUpdate → FencedBlocker |
Update(mustUpdate=true) |
shv/dll/src/MenuData.cpp:50 SilentBlocker("MenuItem::Execute()") |
any context-menu item |
shv/dll/src/dataview.cpp:860 FencedBlocker("DataView::OnCommand()") |
any toolbar button |
Worked example — shv/dll/src/Theme.cpp:464-498 (Jenks-Fisher classification for a legend):
ProcessAppOpers → operation_queue::Process (SilentBlocker) → PrepareDataUsage(Certain) →
WaitForReadyOrSuspendTrigger → IsBlocked() → WaitReady → ItemReadLock → Join() →
cv_TaskCompleted.wait_for(500 ms) loop. The main thread is parked for the whole
classification, pumping nothing.
Two further switches that disable the yield
HasWaitingMessages() (Environment.cpp:167-170) is IsMultiThreaded0() && GetQueueStatus(...),
and IsMultiThreaded0() is RSF_SuspendForGUI — a user checkbox (DmsOptions.cpp:504). With
it off, the message-driven yield in MustSuspend() is dead even without any Blocker.
IsMultiThreaded2() off makes GraphicObject.cpp:318 return early, so the async dms_task
offload of the ItemReadLock/DataReadLock wait never happens.
Also note kill-switch if (!s_ProgressMade) return false; (TriggerOperator.cpp:397):
MarkProgress() is only reached from Join():2472, DoWorkWhileWaiting:2579/2633 and
ItemLocks.cpp:856, so a long single-operator computation that never passes through those keeps
MustSuspend() false throughout.
Suggestion
Whatever the eventual design, the blocking waits above are the place to decide it: they are all
wait_for(..., 500 ms) loops already, so they have a natural point at which the meta thread
could pump the message queue (or hand the wait to a worker and return suspended) instead of
sleeping. Filing this for a decision rather than proposing a patch, since it touches the
threading contract.
Found while debugging #1156; both surfaced from the same report ("the GUI crashes when I compute
this item").
GUI is killed by Windows during long computations: the main thread runs the calculation and never pumps messages
Symptom
Computing a multi-minute item from the GUI makes GeoDmsGuiQt go "Not Responding". Windows
ghosts the window and the process is closed. There is no crash: WER reports
EventType=AppHangB1,IsFatal=1,Hang Type=134217728, withUI[3]=GeoDmsGuiQt.exe is not responding/UI[5]=Close the program, no exception code and no faulting module. Users reportthis as "the GUI crashed", because they walk away and come back to a closed application.
Reproduced with
/MaakOntkoppeldeData/Write_FinalMutationTable(Stikstof cfg, 2.325.426 rows).Headless the same item completes fine:
GeoDmsRunexit 0 in 146 s (125 s of computation).This is distinct from #1156 (EmptyWorkingSet storm). #1156 is fixed and made the run 27 %
faster, but it does not fix this: even with zero EmptyWorkingSet calls, a computation of
several minutes freezes the GUI, because Windows ghosts a top-level window after ~5 s without a
GetMessage/PeekMessageon its owning thread.Mechanism
SetMainThreadID(rtc/dll/src/act/MainThread.cpp:82-114, called frominit_geodms,main_qt.cpp:120) sets bothsMainThreadIDandsMetaThreadIDto the Qt GUI thread, soIsMetaThread() == IsMainThread(). Operator payloads run inline on it wheneverdoASyncisfalse (
OperationContext.cpp:2047;Schedule(..., runDirect=true)at:1367), andJoin()also steals and executes tasks via
TryRunningTaskInline()(:1537).The design is cooperative:
UPDATE_TIMER_ID(100 ms) →DataView::OnTimer→UpdateView()→GVS_Break→ re-arm (shv/dll/src/dataview.cpp:983-1029). That works for the pureSuspendibleUpdatewalk. It breaks whenever aSuspendTriggerBlocker is on the stack:With the yield disabled the main thread parks in polling loops that never peek the message
queue:
OperationContext::Join()—cv_TaskCompleted.wait_for(500 ms)atOperationContext.cpp:2550;the only escape (
return task_status::suspendedat:2543) is guarded byMustSuspend().tile_task_group::AwaitRunningSlots()—m_TileTasksDone.wait_for(500 ms)at:498, enteredfrom
tile_task_group::Join():506, which installs its ownSilentBlocker. Reached fromparallel_for/parallel_tileloop(ParallelTiles.h:185-215), i.e. from most tiled operators.treeitem_production_task::lock_unique— untimedcv_lockrelease.wait(...)atItemLocks.cpp:60;assert(IsMetaThread() || oc.expired())at:51documents it as amain-thread call.
The most important bypass is
WaitForReadyOrSuspendTrigger(ItemLocks.cpp:813-865):WaitReadyis justItemReadLock lock(item);→lock_shared→producer->Join().Blockers reachable from ordinary GUI interaction
shv/dll/src/GraphicObject.cpp:293FencedBlocker("@GraphicObject::PrepareDataOrUpdateViewLater")rtc/dll/src/act/MainThread.cpp:237SilentBlocker("operation_queue::Process")PostMainThreadOpercallbackrtc/dll/src/tic/TicInterface.cpp:666FencedBlocker("TreeItem_GetProgressState")DmsTreeView.cpp:508,563)shv/dll/src/ShvDllInterface.cpp:459FencedBlocker("SHV_GetViewStyleFlags")DmsTreeView.cpp:228)rtc/dll/src/tic/DataLocks.h:145PreparedDataReadLock : SuspendTrigger::FencedBlocker, DataReadLockPrepareDataUsage(Certain)in the member-init listrtc/dll/src/act/Actor.cpp:561-573CertainUpdate→FencedBlockerUpdate(mustUpdate=true)shv/dll/src/MenuData.cpp:50SilentBlocker("MenuItem::Execute()")shv/dll/src/dataview.cpp:860FencedBlocker("DataView::OnCommand()")Worked example —
shv/dll/src/Theme.cpp:464-498(Jenks-Fisher classification for a legend):ProcessAppOpers→operation_queue::Process(SilentBlocker) →PrepareDataUsage(Certain)→WaitForReadyOrSuspendTrigger→IsBlocked()→WaitReady→ItemReadLock→Join()→cv_TaskCompleted.wait_for(500 ms)loop. The main thread is parked for the wholeclassification, pumping nothing.
Two further switches that disable the yield
HasWaitingMessages()(Environment.cpp:167-170) isIsMultiThreaded0() && GetQueueStatus(...),and
IsMultiThreaded0()isRSF_SuspendForGUI— a user checkbox (DmsOptions.cpp:504). Withit off, the message-driven yield in
MustSuspend()is dead even without any Blocker.IsMultiThreaded2()off makesGraphicObject.cpp:318return early, so the asyncdms_taskoffload of the
ItemReadLock/DataReadLockwait never happens.Also note kill-switch
if (!s_ProgressMade) return false;(TriggerOperator.cpp:397):MarkProgress()is only reached fromJoin():2472,DoWorkWhileWaiting:2579/2633 andItemLocks.cpp:856, so a long single-operator computation that never passes through those keepsMustSuspend()false throughout.Suggestion
Whatever the eventual design, the blocking waits above are the place to decide it: they are all
wait_for(..., 500 ms)loops already, so they have a natural point at which the meta threadcould pump the message queue (or hand the wait to a worker and return
suspended) instead ofsleeping. Filing this for a decision rather than proposing a patch, since it touches the
threading contract.
Found while debugging #1156; both surfaced from the same report ("the GUI crashes when I compute
this item").