how-u-doin: A progress reporting abstraction

A common behaviour when building a CLI app is progress reporting. I have come across this requirement many times, usually reaching for indicatif or tui. These libraries work well with displaying progress, yet lately I have needed to decouple the reporting of the progress from the display. The requirement arose from two scenarios;

  1. A long-running CLI app was being invoked from a server, and
  2. A long-running algorithm was being run on another thread.

With the second scenario, a progress reporting structure was being passed through to the function. I ended up generalising the reporting methods into a trait so the display of the progress could be done by various implementations. The abstraction did not transfer well to solving scenario #1. It resulted in defining a bunch of serialisable structures which would be serialised and printed to stdout. At this point I refactored the system, looking to build an abstraction not over the consumption, but the producer of progress.

So how-u-doin was born.

how-u-doin is a progress reporting abstraction in a similar fashion as the log crate. It provides an unobtrusive interface for producing progress reports, which is decoupled from the consumption and display of the progress. It works by using a global static transmitter and a consumer loop. If there is no consumer loop initialised, progress reports are cheap void operations. Here's an example of producing progress:

// start a new report
let rpt = howudoin::new().label("Progress").set_len(1000);

for _ in 0..1000 {
    rpt.inc(); // increment the position

    // check for cancellation flag
    if rpt.cancelled() {
        break;
    }
}

// finish the report
rpt.finish();

Notice that there is no reference to the consumer of the reports. For use of the progress reports, an initialisation must occur somewhere in the program. This initialisation links the transmitter and receiver and starts a consumption loop. Importantly, when a consumer loop is running, all progress reports are stored in a structure and can be queried statically. The design of the consumer loop uses the channel pattern I have previously discussed. There are a few predefined consumers, for example a consumer loop can be initialised with the TermLine consumer

howudoin::init(howudoin::consumers::TermLine::default());

Running cargo run --all-features --example term-line gives an example of the TermLine consumer:

term-line


So how does how-u-doin solve the aforementioned use cases? The abstraction of the progress reporting makes it agnostic to the consumer, and since it uses a static transmitter, it does not need to propagate around a structure. Reports can be made and used locally. For scenario #2, this helped greatly to avoid muddying the function signatures and fighting the borrow checker, spawning a function on another thread.

Consumers get to choose the display mechanism. My CLI apps commonly output some sort of progress by default, but for scenario #1 I created a JsonPrinter which serialises the progress structure. how-u-doin defines this structure publicly, so there is no need to create structures just to serialise them.

how-u-doin also supports fetching the progress structure statically. This feature was added to support requesting patterns, such as a REST API.

how-u-doin is published on crates.io.

Previous
Previous

Profr and Crowd Tic-Tac-Toe

Next
Next

Category Theory with Rust (pt2)