Skip to content

perf(engine): continuous viewport changes saturate the render thread #343

@Kohei-Wada

Description

@Kohei-Wada

Symptom

Activating the `autospin` plugin (`:Toggle autospin` in the palette) saturates a CPU core at 100 %. Visually it works — the camera rotates smoothly — but the cost is way out of proportion to what's actually happening on screen.

Repro

  1. Launch `ttymap`.
  2. `:` → "Toggle autospin".
  3. Observe `top`: ttymap process pinned at ~100 % of one core.

Same shape of cost will hit any plugin that drives continuous viewport changes: a cinematic tour scripted via `ttymap.director`, a follow-marker camera in a future tracker plugin, a "scrub through time" UI, etc. `autospin` is just the most pathological case (no human-cadence dampening).

Cost model (hypothesis)

`autospin` calls `ttymap.map:jump(lon, lat)` from `on_tick` at the engine's tick rate (~30 Hz). Each jump enqueues `UserCommand::Map(Jump)` → dispatch → render thread receives a fresh `Draw { viewport, overlays }` → full re-render:

  • `Viewport` → visible-tiles set
  • per tile, R-tree query for in-view features
  • draw fills / lines, then sorted symbols → Braille canvas
  • composed into `MapFrame` → ratatui paint

That's ~30 full re-renders per second for what's effectively a 0.3° pan between consecutive frames. Most of the visible content is identical frame-to-frame, but the engine doesn't notice — it redoes the whole pipeline.

Possible optimization avenues (not committed to any of these — profile first)

  • Viewport-delta caching: when the centre shift between two consecutive Draws is small enough that most pixels overlap, reuse the previous `MapFrame` shifted by the delta and only redraw the newly-visible margin.
  • Draw debounce on the render thread: if a newer `Draw` arrives while one is in flight, drop the older one. We'd render at whatever frequency the render thread can sustain, never the input frequency. (Probably already-true for tile fetch, but worth confirming for the full pipeline.)
  • Identify the actual hot spot: profile a release-mode `autospin` run with `cargo flamegraph` / `perf` and look at the top frames. Could be Braille canvas, earcut polygon fill, R-tree, channel/IPC overhead, or ratatui paint — different causes have different fixes.

Why this is worth fixing

ttymap's positioning is "scriptable globe" — plugins that drive the camera continuously (animation libraries, cinematic tours, follow-camera trackers) are core to the value prop. The current cost model makes "any plugin that touches the viewport per-frame" a CPU footgun. Fixing this once at the engine layer unlocks the whole class of plugins.

Workaround for now

The `autospin` plugin keeps the per-tick design — the user feedback was that throttling makes the spin choppy enough that the CPU savings aren't worth it. So this issue stands as the real fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions