perf(ui): offload TUI initial task load to a worker thread#934
Closed
Kohei-Wada wants to merge 1 commit into
Closed
perf(ui): offload TUI initial task load to a worker thread#934Kohei-Wada wants to merge 1 commit into
Kohei-Wada wants to merge 1 commit into
Conversation
The first task fetch ran synchronously on the Textual event loop (on_mount -> call_after_refresh -> sync httpx list_tasks + presenter transforms), freezing first paint until the round-trip and all view-model builds completed -- and up to the 30s client timeout on a slow or unreachable server. Move load_tasks onto a background worker, mirroring ConnectionMonitor: sample widget geometry + sort state on the main thread, run the blocking fetch + transform via asyncio.to_thread, then apply state/UI updates back on the loop. An exclusive "load_tasks" worker group makes a reload arriving mid-flight supersede the previous one (last result wins). This also unblocks the shared reload path (sort toggle, websocket task changes). TaskUIManager now takes the app to dispatch the worker. tests: tui/services is excluded from the default suite (pre-existing, see tests/conftest.py), so add tests/tui/test_startup_smoke.py -- which IS in the suite -- proving a slow client no longer blocks mount/first paint; rework the TaskUIManager unit tests around the worker decomposition.
There was a problem hiding this comment.
The worker offload implementation successfully addresses the TUI startup blocking issue. The changes are well-architected with proper thread safety, error handling, and comprehensive test coverage. All 905 tests pass, confirming no regressions. The code follows async Python best practices and integrates cleanly with the existing architecture.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
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.
Summary
TUI startup blocked first paint.
on_mountscheduled the initial load viacall_after_refresh(_load_tasks_if_ready)— a synchronous callback that ran the blockinghttpx.Client.list_tasksplus the table/gantt presenter transforms directly on the Textual event loop. Nothing could paint until the round-trip and all view-model builds finished, and on a slow/unreachable server the UI froze for up to the client's 30s timeout.The rest of the app already offloads sync HTTP (
ConnectionMonitor,show/stats/auditdialogs) viarun_worker+asyncio.to_thread; only the initial/reload path was left on the loop. This change brings it in line.Change
TaskUIManager.load_tasksnow:gantt_widget.calculate_date_range()) and sort state on the main thread (these must not be read off-thread), thenasyncio.to_thread, thenawait), so nocall_from_threadmarshalling is needed."load_tasks"worker group → a reload arriving mid-flight (sort toggle, websocket task change) supersedes the in-flight one (last result wins).on_error/notifyruns on the main thread.TaskUIManagernow takes theappto dispatch the worker (mirrorsConnectionMonitor).load_tasks, so the shared reload path is unblocked too.Effect
First interactive paint no longer waits for the
list_tasksround-trip + deserialization + 2×N view-model builds; a slow/unreachable server keeps the UI responsive instead of freezing.Verification
tests/tui/test_startup_smoke.py, runs in the default suite): a deliberately blocking API client no longer blocks mount — the app reaches an interactive state with an empty list while the fetch is in flight, then fills in once released. Confirmed it fails against the old synchronous code (mount blocks ~5s and the cache is already populated at first paint), so it genuinely guards the regression.make test-ui(full suite): 905 passed, no regressions.TaskUIManagerunit tests reworked around the worker decomposition: 26 passed when run directly. Note:tests/tui/services/is excluded from the default suite (pre-existing, seetests/conftest.py— "fix in a separate PR"), which is why the startup proof lives undertests/tui/.ruff✓,mypy --strict(172 files) ✓,vulture(no dead code) ✓,codespell✓.🤖 Generated with Claude Code