Skip to content

[triage:ui-04-tui-runtime-and-client-per-poll] TUI builds a fresh tokio runtime + reqwest client on every poll/action #154

Description

@ebursztein

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<AppState> {
    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<ActionOutcome> {
    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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions