Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebuild widget tree only after an application update #597

Merged
merged 10 commits into from
Nov 8, 2020

Conversation

hecrj
Copy link
Member

@hecrj hecrj commented Nov 6, 2020

This PR improves the implementation of the event loop of an Application in iced_winit and iced_glutin to call Application::view only when strictly necessary.

Rust and The Elm Architecture

As you may know, iced is heavily inspired by Elm and its architecture.

In short, The Elm Architecture consists in initializing an application, obtaining its widget tree, and using it to process user interactions in a loop.

The widget tree may produce messages after a user interaction. In that case, the application reacts to these messages and changes state accordingly. When the application changes state, the widget tree needs to be reconstructed in order to stay up-to-date.

We can express this in a few lines of Rust:

// Initialization
let mut application = MyApplication::new();
let mut widgets = application.view();

// Event loop
loop {
    // Process user interactions and obtain resulting application messages
    let messages = widgets.update(/* ... */);

    // If the user interaction has produced application messages...
    if !messages.is_empty() {
        // ... update the application
        for message in messages {
            application.update(message);
        }

        // ... and rebuild the widget tree
        widgets = application.view();
    }

    // ...
}

This seems simple enough! And still, iced does not currently implement its event loop like this. Instead, iced rebuilds the widget tree on every iteration, right before processing user interactions. Let me explain why.

Widgets and mutable references

The Application trait in iced is defined as follows:

pub trait Application {
    // ...

    fn view(&mut self) -> Element<'_, Self::Message>;
}

In other words, iced applications produce widget trees that may contain mutable references to different parts of the application state.

As a consequence, after calling Application::view, Rust's borrow checker will forbid any further references to the application until the resulting widget tree is dropped.

But this should be fine! In our event loop, we only reference the application again when an update is necessary. And in that case, we are rebuilding the widget tree afterwards anyways!

Additionally, since non-lexical lifetimes landed, the borrow checker should be smart enough to deal with the borrows in the previous event loop.

So, what's the problem? Basically, Element represents a Box<dyn Widget> and, therefore, needs to free memory when dropped. Thus, if we tried to implement the previous event loop as-is, the compiler would complain:

error[E0499]: cannot borrow `application` as mutable more than once at a time
  --> src/main.rs:31:23
   |
25 |     let mut widgets = application.view();
   |                       ----------- first mutable borrow occurs here
...
31 |             widgets = application.view();
   |             -------   ^^^^^^^^^^^ second mutable borrow occurs here
   |             |
   |             first borrow might be used here, when `widgets` is dropped and runs the `Drop` code for type `Element`

Dropping widgets manually

Once I first hit the previous error, I assumed it would take some unsafe code to please the borrow checker. So, for the time being, I worked around it by rebuilding the widget tree at every iteration.

Earlier this week, I was playing with the idea of a lazy widget to tackle the problem and I brought up this issue in the Zulip server. Eventually, we realized that we could leverage ManuallyDrop to aid the compiler and fix the issue (thanks @twitchyliquid64!).

Specifically, we can use ManuallyDrop to inhibit the compiler from automatically calling the destructor of the widget tree. This requires no unsafe code since potentially leaking memory is safe in Rust:

use std::mem::ManuallyDrop;

let mut application = MyApplication::new();
let mut widgets = ManuallyDrop::new(application.view());

loop {
    let messages = widgets.update(/* ... */);

    if !messages.is_empty() {
        // Manually drop the widget tree to avoid leaking memory
        drop(ManuallyDrop::into_inner(widgets));

        for message in messages {
            application.update(message);
        }

        // As we control drop behavior now, this will not trigger the error anymore!
        widgets = ManuallyDrop::new(application.view());
    }

    // ...
}

Great! This event loop compiles. That should be it, right? No, not so fast! iced_winit cannot use a loop to implement the event loop!

Closures and lifetimes

For very acceptable reasons, EventLoop::run in winit takes control of the calling thread and asks for a closure which is run on every iteration.

This is equivalent to replacing our previous loop with a closure:

use std::mem::ManuallyDrop;

let mut application = MyApplication::new();
let mut widgets = ManuallyDrop::new(application.view());

run(move |event| {
    let messages = widgets.update(/* ... */);

    if !messages.is_empty() {
        drop(ManuallyDrop::into_inner(widgets));

        for message in messages {
            application.update(message);
        }

        widgets = ManuallyDrop::new(application.view());
    }

    // ...
})

Unfortunately, this no longer compiles. Rust is not able to properly desugar the closure and keep track of the appropriate (non-lexical) lifetimes.

We were so close! All would be good if we could somehow turn the closure calls into a loop of events...

Futures as closures

Futures are data structures that hold both state and execution progress. Similar to how closures can be called, futures can be polled to resume their execution. Could we maybe leverage them to turn a closure into a loop of calls?

Yes! We can rewrite our event loop using an mpsc::Receiver and await syntax:

use futures::channel::mpsc;

async fn run_application(events: mpsc::UnboundedReceiver<Event>) {
  use std::mem::ManuallyDrop;

  let mut application = MyApplication::new();
  let mut widgets = ManuallyDrop::new(application.view());

  while let Some(event) = events.next().await {
      let messages = widgets.update(/* ... */);

      if !messages.is_empty() {
          drop(ManuallyDrop::into_inner(widgets));

          for message in messages {
              application.update(message);
          }

          widgets = ManuallyDrop::new(application.view());
      }

      // ...
  }

  drop(ManuallyDrop::into_inner(widgets));
}

Then, we wire it together with the closure:

// Initialize channels
let (mut sender, receiver) = mpsc::unbounded();

// Prepare the application
let instance = Box::pin(run_application(receiver));

run(move |event| {
    // Send the event to the application
    sender.start_send(event).expect("Send event");

    // Resume the application
    let poll_result = instance.as_mut().poll(/* ... */);

    // Handle poll result...
})

And that's it! This is the strategy that this PR uses to leverage the borrow checker to avoid calling Application::view unnecessarily without any unsafe code.

The main issue with this approach is that we are breaking the Future contract, as run_application blocks very often (when drawing, for instance). However, this Future is an implementation detail, it's not exposed publicly, and we are manually polling it. We may be able to get rid of it once closures can handle non-lexical lifetimes.

In any case, if anyone is aware of a better way to do this, please let me know!

Fixes #579.

@hecrj hecrj added the improvement An internal improvement label Nov 6, 2020
@hecrj hecrj added this to the 0.2.0 milestone Nov 6, 2020
@hecrj hecrj merged commit da1a3ee into master Nov 8, 2020
@hecrj hecrj deleted the improvement/reuse-view-in-event-loop branch November 8, 2020 12:31
@hecrj hecrj self-assigned this Nov 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
improvement An internal improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Prevent re-rendering while not receiving 'registered' Message
1 participant