I3status-rust: Discussion: Async I/O

Created on 21 Aug 2019  路  9Comments  路  Source: greshake/i3status-rust

When I first designed i3status-rust, async I/O was still a long way from being usable. Thus my design involves a custom task scheduler to combine all the block computations in a single thread, but I/O still either blocks the main thread or has to be deferred to a separate thread with Mutexes and callbacks.. This is of course not helpful to reach the goal of maximum efficiency and minimal overhead.

In the years since, Rust has made great progress towards async I/O. Futures are stabilized, Tokio has reached 0.2 and the general ecosystem has massively improved. We can now borrow across async tasks too using the Pin trait, which has also been a blocker for me.
We could conceivably reimplement the core system to use futures, but I had built a prototype and it was ergonomic hell, and I don't want to put anyone who wants to contribute through that process. So, I believe we should wait for async/await syntax, whcih has also made great progress (https://areweasyncyet.rs/). We could build a proof-of-concept in the current nightly, but there are still some open issues that prevent me from going for it right now.

Would you guys agree that we should wait for the following features/ or do you think we can or should work around these issues right now?

  • Can't have async fn in traits like the block trait... There is a macro workaround though (https://github.com/dtolnay/async-trait)
  • Can't output streams from async fns without generators (which are still at least 1-2 years away from stable I'd say...). We would benefit from async fn streams ergonomically as every block essentially just outputs a stream of UI updates and receives an async stream of UI input. Alternatively, we could pass a reference to a stream to every block and have them put data in them manually.
discussion enhancement future-rewrite help wanted

Most helpful comment

Maybe it's time to reconsider using async I/O now that async/await is stabilized?

Doing a complete architecture overhaul sounds quite intimidating though.

Maybe we could work out a way to progressively "async" the core by supporting both _async_ and _non-async_ behaviours at the same time?

  • :thread: = spawns a thread to watch or do something and schedule an update via tx: Sender<Task> if necessary
  • :anchor: = may take some time

| Block | Behaviour |
|-----------------|--------------------|
| backlight | :thread: |
| battery | :thread: |
| bluetooth | :thread: |
| cpu | |
| custom | :thread: |
| disk_space | |
| docker | :anchor:? |
| focused_window | :thread: |
| ibus | :thread: |
| keyboard_layout | :thread: |
| load | |
| maildir | :anchor: |
| memory | |
| music | :thread: |
| net | |
| network_manager | :thread: |
| notmuch | :anchor: |
| nvidia_gpu | |
| pacman | :anchor: |
| pomodoro | |
| sound | :thread: |
| speedtest | :thread: :anchor:? |
| taskwarrior | :anchor:? |
| temperature | |
| time | |
| toggle | :anchor:? |
| uptime | |
| watson | :anchor:? |
| weather | :anchor:? |
| xrandr | |

I would say most blocks either wait for:

  • 1: their next scheduled update
  • 2: wait for their update logic to run
  • 3: wait in a another thread for something to happen and reschedule an update

  • _1_ is fine (performance-wise) for blocks that don't spawn a background thread and have light update logic.
    For blocks relying on a background thread.

  • _2_ is fine for light update logic: they won't run (block other updates) for long.
    For blocks relying on a web service (weather) or querying a DB (pacman) it could be an issue.
  • _3_ can be a waste of resources if an async interface is available instead.

All 9 comments

I'd definitely wait for stable async/await for a release, but working on a PoC in nightly seems like a good idea.

The macro workaround looks fine to me as it's easy to remove it when the feature has landed.
If your estimation for streams are right then I wouldn't wait that long and start with the reference workaround.


If we do a full rewrite of the core system may I suggest some general improvements:

  • I'd love to have generic format option for all blocks, which could be implemented lazy for only used placeholders
  • Switchable short and long format options, like the load block but customizable. This would be useful e.g. for the net block to hide the ip / ssid and expand on demand.

(I know it's unrelated, but I think it would be a good idea to think about that during the rewrite, if there are no reasons against it. Feel free to move this to another issue to ease the discussion here)

A piece that's missing from this discussion is, what are some problems with the existing approach? Can we solve them without async/await? I don't totally understand what the issues are outside of the abstract we-block-on-I/O, and it could be beneficial to list them in this discussion thread.

From an API design side, I tend to think that the omission of generators from the initial async/await implementation limits what we could do for the main update loop, but maybe that's just a lack of imagination on my part.

An alternative might be to try async/await somewhere else: for instance, one place that we tend to proliferate threads is when dealing with D-Bus code. Maybe it's possible to come up with an ergonomic way for blocks to opt-in to a shared D-Bus threadpool using Futures. (We might have to check out the upstream options, of course.)

Good point.
The goal of any rewrite would be to set i3status-rust up for multiple backends (other than i3status), to increase simplicity, reduce boilerplate and minimize memory operations and increase performance.

Right now, we have a ton of boilerplate, many unnecessary copy's and no flexibility. I'm hoping async IO would also help us streamline a composable, low-overhead rendering pipeline that is also much more stable. Right now, there are many edge-case bugs that can arise from blocking I/O that can't be fixed easily.. Like, block updates that are synced on a shared interval output only a single visual update to i3status for performance, but the timers can be desynced if you, for example, use a custom block with network interaction.
But I totally agree that there is a lot more low-hanging fruit to be had in the current implementation. We could provide a shared futures thread on-demand for blocks that require async IO. And we could use something like this https://docs.rs/calloop/0.4.4/calloop/ to make a rendering pipeline with callbacks and shared references to visual states :)

We can also take inspiration from some of these projects that have elegant solutions:

Azul has the architecture that is most similar to ours; https://github.com/maps4print/azul
To solve async issues in azul, they implemented multiple solutions: https://github.com/maps4print/azul/wiki/Timer,-Threads-and-Tasks

Maybe it's time to reconsider using async I/O now that async/await is stabilized?

Doing a complete architecture overhaul sounds quite intimidating though.

Maybe we could work out a way to progressively "async" the core by supporting both _async_ and _non-async_ behaviours at the same time?

  • :thread: = spawns a thread to watch or do something and schedule an update via tx: Sender<Task> if necessary
  • :anchor: = may take some time

| Block | Behaviour |
|-----------------|--------------------|
| backlight | :thread: |
| battery | :thread: |
| bluetooth | :thread: |
| cpu | |
| custom | :thread: |
| disk_space | |
| docker | :anchor:? |
| focused_window | :thread: |
| ibus | :thread: |
| keyboard_layout | :thread: |
| load | |
| maildir | :anchor: |
| memory | |
| music | :thread: |
| net | |
| network_manager | :thread: |
| notmuch | :anchor: |
| nvidia_gpu | |
| pacman | :anchor: |
| pomodoro | |
| sound | :thread: |
| speedtest | :thread: :anchor:? |
| taskwarrior | :anchor:? |
| temperature | |
| time | |
| toggle | :anchor:? |
| uptime | |
| watson | :anchor:? |
| weather | :anchor:? |
| xrandr | |

I would say most blocks either wait for:

  • 1: their next scheduled update
  • 2: wait for their update logic to run
  • 3: wait in a another thread for something to happen and reschedule an update

  • _1_ is fine (performance-wise) for blocks that don't spawn a background thread and have light update logic.
    For blocks relying on a background thread.

  • _2_ is fine for light update logic: they won't run (block other updates) for long.
    For blocks relying on a web service (weather) or querying a DB (pacman) it could be an issue.
  • _3_ can be a waste of resources if an async interface is available instead.

Is anyone working on this? I am bored and would be interested in having a go at progressively making stuff async with tokio.

Hello, FYI I'm not working on this.

I have mixed feelings about using async inside i3status-rust (only my opinion):

  • most widgets won't benefit from being async
  • mixing threads and async looks daunting to me
  • threads works fine, even spawning a thread per block remains quite cheap

Nevertheless some widgets may benefit from being async (or simply being run inside a thread): pacman, weather, speedtest and maildir...?

@Noah-Kennedy I'll be looking forward reading your code if you decide to give it a go ;)

Any updates on this?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ammgws picture ammgws  路  6Comments

concatime picture concatime  路  4Comments

Rikorose picture Rikorose  路  5Comments

michaelmrose picture michaelmrose  路  4Comments

l4l picture l4l  路  6Comments