feat(runtime): opt-in Qt event loop integration via qasync - #10584
Draft
larsoner wants to merge 1 commit into
Draft
feat(runtime): opt-in Qt event loop integration via qasync#10584larsoner wants to merge 1 commit into
larsoner wants to merge 1 commit into
Conversation
…am#1986) Adds runtime.gui_event_loop = "qt": the kernel's asyncio loop becomes a qasync QEventLoop, so Qt windows opened from cells (napari, MNE-Python, ...) stay responsive while the kernel is idle, with no library-side processEvents() polling. Edit mode only; missing deps fall back to the default loop with an error logged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
All contributors have signed the CLA ✍️ ✅ |
Author
|
I have read the CLA Document and I hereby sign the CLA |
Author
|
Also verified on macOS 26.5.2 (Python 3.14.4, PySide6 6.11.1, qasync 0.28.0), same numbers: kernel loop is EDIT: Also, I do have access to a Windows machine if interactive testing is required there, too, at some point. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request was authored by a coding agent. (Claude Code, driven by @larsoner)
📝 Summary
WIP exploration of GUI event loop integration (#1986), sized so you can see what real support would take. Adds an opt-in config:
With it set, the kernel's asyncio loop is created by a factory returning a qasync
QEventLoopinstead of a plain selector loop, so the same loop drives both asyncio and Qt. Qt windows opened from cells (napari, MNE-Python'sraw.plot(), plainQWidgets) then stay responsive whenever the kernel is idle — no library-sideprocessEvents()polling task, no added latency, no CPU burn.This came out of mne-tools/mne-qt-browser#448: every Qt-based library currently hand-rolls the same asyncio pump to survive inside marimo (napari users in this issue's thread, MNE, others). A pump caps GUI responsiveness at its polling interval and burns CPU while idle; sharing the loop is the architecturally right integration, and marimo already had the seam for it (
launch_kernel'sloop_factory, previously used only for Windows' proactor loop).What's in the diff
runtime.gui_event_loopconfig key ("qt"| absent). Backend-only; the frontend config schema is alooseObjectso no frontend change is needed formarimo.toml/pyproject.tomlusers. Not exposed in the settings UI yet — happy to add if you want this at all.marimo/_runtime/gui_loop.py: resolves a Qt binding (already-imported →QT_APIenv var → probe), then builds a factory that creates theQApplicationand wraps it in aqasync.QEventLoop. Two hazards found during real testing are handled here:import PyQt5bare, which false-positives on partial installs (a strayPyQt5-Qt5wheel leaves an importable namespace package withoutQtCore). We import the binding (verified viaQtCore) first, which pins qasync'ssys.modules-based detection — and we un-import partial packages our probe pulled in, or qasync would trip over them.app.setQuitOnLastWindowClosed(False): the Qt loop is the kernel's loop, so a user closing their last window must not stop the kernel.launch_kernelwiring: misconfiguration or missing deps logs an error and falls back to the default loop — the kernel always starts. Ignored (with a warning) in run mode, where the kernel doesn't own the process's main thread that GUI toolkits require._asyncio_runhelper:asyncio.run(loop_factory=...)is 3.12+; 3.11 usesasyncio.Runner, 3.10 gets a small manual equivalent.qasyncis optional (DependencyManager), not a dependency of marimo.Verified end-to-end (Linux, PySide6 6.11.1, real X11/Wayland display)
Measured with a 100 ms
QTimertick counter inside a realmarimo editkernel (50 ticks/5 s = fully responsive):gui_event_loop = "qt"QLabel, no pump anywhereraw.plot()(its own pump disabled)_UnixSelectorEventLoopqasync.QSelectorEventLoopKnown caveats (documented, not solved here)
%gui qt. The docs section points users at gating downstream cells onmo.ui.run_button.app.exec()inside a cell is incompatible with a shared loop (Qt warns "the event loop is already running" and returns immediately), and library code that connectslastWindowClosed → app.quit()(e.g. MNE's blocking helper) would stop the kernel's loop. This needs library-side cooperation; mne-qt-browser now skips its pump when it detects a qasync loop (mne-tools/mne-qt-browser@c73f635).processEvents()) re-enters asyncio and can log "Cannot enter into task" errors if another task is ready to step. Benign in testing once the redundant library pump was removed.Open questions for maintainers
mo.enable_gui("qt")can't swap an already-running loop, so it would have to be config/CLI anyway)?gui_event_loopvs IPython'sgui?Fixes #1986 (partially — Qt only).