Skip to content

GUI is killed by Windows during long computations: main thread runs the calculation and never pumps messages #1157

Description

@jipclaassens

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::OnTimerUpdateView()
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_sharedproducer->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 CertainUpdateFencedBlocker 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):
ProcessAppOpersoperation_queue::Process (SilentBlocker) → PrepareDataUsage(Certain)
WaitForReadyOrSuspendTriggerIsBlocked()WaitReadyItemReadLockJoin()
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").

Metadata

Metadata

Labels

QtEventlog, layout detailpage, main menu, knoppen, treeviewcrashTermination of program

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions