[triage:ui-04-tui-runtime-and-client-per-poll] TUI builds a fresh tokio runtime + reqwest client on every poll/action
- Dominant language
- Rust
- Stars
- 72
- Forks
- 13
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 5
Description
Imported from Capsem triage report `ui-04-tui-runtime-and-client-per-poll.md`.
- Severity: `low`
- Category: `performance`
- Area: `capsem-tui`
- Location: `crates/capsem-tui/src/gateway_provider.rs:102-133` and `:66-72`
- Confidence: `verified`
## Summary
`GatewayProvider::load()` (the polling path) and `invoke()` (the action path)
each construct a brand-new single-threaded tokio runtime per call. Polling runs
on the `RefreshBridge` worker at the configured `refresh_ms` (default 1000ms,
floored at 100ms — `main.rs:50,128-132`). On top of that, every
`GatewayProvider` holds a `reqwest::Client` created with
`reqwest::Client::new()` (line 69); building a `reqwest::Client` allocates a new
connection pool and TLS configuration. So each refresh spins up and tears down a
full async runtime, and the same provider instance is cloned into both the
refresh thread and the control thread.
## Evidence
```
// gateway_provider.rs:126 (StateProvider::load, called on every refresh)
fn load(&self) -> Result {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build() // new runtime every poll
.context("build capsem-tui gateway provider runtime")?;
runtime.block_on(self.load_async())
}
// gateway_provider.rs:102 (invoke, same pattern per action)
pub fn invoke(&self, action: &ControlAction) -> Result {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all().build()...;
runtime.block_on(self.invoke_async(action))
}
```
`RefreshBridge::spawn_with_loader` calls `loader()` (== `provider.load()`) once
per `request()` (`main.rs:398-415`), and `run_loop` issues a `request()` every
`refresh_interval` (`main.rs:238-243`).
## Impact
Wasted CPU and allocations every second for the lifetime of the TUI: a full
tokio runtime build/teardown plus the per-provider connection pool not being
reused across polls. Not a correctness bug and not a tight loop (the
`in_flight` guard prevents overlap), but unnecessary steady-state churn in a
long-lived UI process.
## Suggested fix
Build one runtime (or one `Handle`) when the `RefreshBridge`/`ControlBridge`
worker thread starts and reuse it across `load`/`invoke` calls. The
`reqwest::Client` is already retained on the provider, so the main remaining win
is hoisting the runtime out of the per-call path.
## Triage
Confirmed from the local reviewed report in `/Users/elie/git/capsem/tmp/bugs/ui-04-tui-runtime-and-client-per-poll.md`. Track implementation in the triage sprint; add regression coverage before fixing.
Contributor guide
Research direction
Start with crates/capsem-tui/src/gateway_provider.rs:66-72 and :102-133, then trace RefreshBridge and ControlBridge worker startup in main.rs:238-243 and :398-415. Verify how load and invoke are called, and inspect existing TUI tests before changing the runtime lifecycle. Done means polling and actions reuse worker-owned async resources without per-call runtime construction, with regression coverage added.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100