Skip to content

perf(ui): offload TUI initial task load to a worker thread#934

Closed
Kohei-Wada wants to merge 1 commit into
mainfrom
worktree-tui-startup-worker-offload
Closed

perf(ui): offload TUI initial task load to a worker thread#934
Kohei-Wada wants to merge 1 commit into
mainfrom
worktree-tui-startup-worker-offload

Conversation

@Kohei-Wada
Copy link
Copy Markdown
Owner

Summary

TUI startup blocked first paint. on_mount scheduled the initial load via call_after_refresh(_load_tasks_if_ready) — a synchronous callback that ran the blocking httpx.Client.list_tasks plus 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/audit dialogs) via run_worker + asyncio.to_thread; only the initial/reload path was left on the loop. This change brings it in line.

Change

TaskUIManager.load_tasks now:

  1. Samples widget geometry (gantt_widget.calculate_date_range()) and sort state on the main thread (these must not be read off-thread), then
  2. dispatches a worker that runs the blocking fetch + transform via asyncio.to_thread, then
  3. applies state/UI mutations back on the loop (after the await), so no call_from_thread marshalling is needed.
  • Exclusive "load_tasks" worker group → a reload arriving mid-flight (sort toggle, websocket task change) supersedes the in-flight one (last result wins).
  • Error handling moved after the await so on_error/notify runs on the main thread.
  • TaskUIManager now takes the app to dispatch the worker (mirrors ConnectionMonitor).
  • Call sites are unchanged — every reload funnels through load_tasks, so the shared reload path is unblocked too.

Effect

First interactive paint no longer waits for the list_tasks round-trip + deserialization + 2×N view-model builds; a slow/unreachable server keeps the UI responsive instead of freezing.

Verification

  • Startup smoke test (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.
  • TaskUIManager unit tests reworked around the worker decomposition: 26 passed when run directly. Note: tests/tui/services/ is excluded from the default suite (pre-existing, see tests/conftest.py — "fix in a separate PR"), which is why the startup proof lives under tests/tui/.
  • ruff ✓, mypy --strict (172 files) ✓, vulture (no dead code) ✓, codespell ✓.

🤖 Generated with Claude Code

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.
Copy link
Copy Markdown

@amazon-q-developer amazon-q-developer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Kohei-Wada Kohei-Wada closed this May 31, 2026
@Kohei-Wada Kohei-Wada deleted the worktree-tui-startup-worker-offload branch May 31, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant