spacr.qt.gc_policy¶
Collect cyclic garbage on the GUI thread only.
Background: pressing Run in the live preview with cpsam produced, on
2026-09-01:
QObject::killTimer: Timers cannot be stopped from another thread
QObject::~QObject: Timers cannot be stopped from another thread
Segmentation fault (core dumped)
Those two lines, in that order, are reproduced exactly by
tests/qt/test_gc_runs_on_the_gui_thread.py with no spaCR code involved at
all: build a QObject that owns a running QTimer, drop it into a reference
CYCLE so only the collector can free it, then call gc.collect() from a
worker thread. The collector runs the destructor on whichever thread it
happens to be running on, and Qt cannot stop a timer from there.
The defect is not specific to the preview. CPython runs an automatic collection whenever an allocation pushes a generation past its threshold, so the collection runs on the thread that allocated most recently. A Cellpose pass allocates heavily in a worker thread, so that worker is the likely thread to run the collection and therefore to destroy unrelated widgets the GUI thread released earlier. The resulting crash is timing-dependent, occurs away from the code that created the cycle, and names no file.
Automatic collection is switched off and driven from a QTimer instead. That timer lives on the GUI thread, so every destructor the collector runs is run there – which is the thread that owns the widgets.
Explicit pipeline cleanup uses spacr._gc.collect(), which requests a
full sweep on the next GUI timer tick. gc.disable() stops only automatic
collection, so third-party code calling the standard-library gc.collect
directly can still collect on a worker. spacr.qt.thread_guard() reports
wrong-thread widget destruction if it recurs.
Memory is still collected: each tick applies CPython’s own generational
policy against the same thresholds, so collections happen about as often as
they otherwise would, checked every INTERVAL_MS milliseconds on the
GUI thread.
Functions¶
|
Run at most one generation's collection, CPython's own policy. |
|
Take cyclic collection off the worker threads. |
|
Whether the GUI-thread collection policy is currently in force. |
|
Restore the interpreter's own collection policy. |
Module Contents¶
- spacr.qt.gc_policy.collect_once() int[source]¶
Run at most one generation’s collection, CPython’s own policy.
The interpreter collects generation n when that generation’s count passes its threshold, youngest first, and collecting a generation also clears the younger ones. Reproducing that here – rather than calling a full
gc.collect()on every tick – is what keeps the cost the same as it was before this module existed. A full sweep every second would walk every live numpy array in the process.Explicit worker cleanup requests take precedence and collect all generations, even below their normal thresholds.
- Returns:
the generation collected, or
-1when nothing was due.
- spacr.qt.gc_policy.install(parent=None) bool[source]¶
Take cyclic collection off the worker threads.
- Parameters:
parent – a QObject to own the timer, normally the QApplication.
- Returns:
whether the policy was installed.
Falsewhen it already was, or when Qt is unavailable. Never raises; when installation fails the interpreter’s own collection policy stays in place.