Day 22: Séance smells a rat

Rats, we're rats, we're the rats!

—The rats, Rat Movie: Mystery of the Mayan Treasure

Today saw the long-awaited addition of a TUI to Séance. I’ll be using Ratatui for this, which is to my knowledge the most popular TUI library for Rust.

What will it look like?

My goal is radical system status transparency, inspired by both the visibility of system status usability heuristic from the Neilson Norman Group and this passage from the permacomputing manifesto:

Computer systems should also make their own inner workings as observable as possible. If the computer produces visual output, it would use a fraction of its resources to visualize its own intro- and extrospection.

—Ville-Matias "Viznut" Heikkilä, Permacomputing

To this end, I will display the full state of episode download hierarchically. At each level, I aim to display as much relevant state as possible, though with an eye for typographic hierarchy to prevent information overload. In a perfect world, this state would update on every network packet, telling you if you are waiting on DNS, HTTPS, TCP ACKs, etc. In the short term, it will simply display whether a request is outstanding, and later the progress of the download. Here is a mockup:

Never Post - A podcast about and for the internet, hosted by Mike Rugnetta.
    │
    ├── Posting from Inside
    │        ╰── Downloading ████████░░░░░░░░░░░░░░░░░ 33%
    │
    ╰── Mailbag #11: Someone To Tell Us How To Be
             ╰── Resolving play.prx.org

Minimizing short-lived allocations

Though Ratatui is far and away the most popular TUI framework for Rust, I hesitated at first to use it because it’s API encourages the creation of lots of short-lived allocations. After familiarizing myself more with its API, however, I think I have found a way around this issue.

First, an explanation of the problem. Ratatui styles itself as an immediate mode library. This means it is based around a series of drawing commands, rather than creating a persistent structure representing your UI (like the DOM, for example). Ratatui’s built-in text utilities are Text, Line, and Span, each of which aggregates the next. Here’s a simplified version of their definitions:

struct Text<'a> {
    // ...
    pub lines: Vec<Line<'a>>
}

struct Line {
    // ...
    pub spans: Vec<Span<'a>>
}

struct Span<'a> {
    // ...
    pub content: Cow<'a, str>
}

Note how each struct contains a heap-allocated container (or potentially heap-allocated, in the case of Cow). This means that the following code creates three short-lived allocations:

Text::raw(format!("Hello, {name}"))

And were we to use the standard immediate mode–style interface, we would be doing that on each frame.

Now, as I’ve mentioned before, this almost certainly doesn’t matter. Memory allocators are quite efficient and the overhead from dereferencing the pointers would likely be on the order of nanoseconds, or microseconds at worst. But reducing temporary memory allocations is an artistic goal, rather than a technical one. So how can we do it?

One solution would be to store the structures within the application state, reusing them between draw calls. This is a strategy that Ratatui’s docs mention as well. I don’t like this approach though, because it requires duplicating the application state and maintaining it separately.

Instead, we don’t use Text etc. in the first place! Ratatui works internally on a grid-based system. Each Widget draws itself within a given rectangle. If we want to render a horizontal list of lines, we can instead do so by manually rendering each item to the next line.

To avoid having to allocate a new string or similar for the text, we will reuse the strings already stored in the agent’s state. Each struct containing the state will implement Widget itself. Here’s what that looks like:

fn render(&self, frame: &mut Frame) {
    for (row, channel) in self.channels.iter().enumerate() {
        frame.render_widget(channel, Rect {
            x: frame.area().x,
            y: frame.area().y + row as u16,
            width: frame.area().width,
            height: 1,
        });
    }
}

Echoing another recurring topic in this project, you could consider this a form of Church encoding. Similar to World::act, we are representing the list within control flow rather than as a structure in memory.