idanarye / woab

Widgets on Actors Bridge - a GUI microframework for combining GTK with Actix
https://crates.io/crates/woab
MIT License
14 stars 1 forks source link

Improve idle performance #42

Closed piegamesde closed 3 years ago

piegamesde commented 3 years ago

When there are no events to be processed, because of idle_add, the GTK loop continues to spin. This results in a CPU usage that is not negligible.

A naive idea to resolve this would be to recognize when Actix is idle for some time and replace the idle_add with a timer on a slower frequency (maybe 10Hz).

Alternatively, slomo suggested: "it could be improved if you can somehow get a future that runs the actix runtime"

idanarye commented 3 years ago

Neither Actix nor Tokio expose an API for checking if any futures were executed in a crank of the runtime. They don't even officially expose the concept of "runtime cranking" (Tokio used to) - this is something I emulate by blocking on a future that waits 0 seconds.

Take a look at #43 - if I change that future to wait 10ms instead, it drastically decreases the CPU usage. I'll have to check with a GUI-heavy application to make sure this doesn't harm the GUI performance though.

piegamesde commented 3 years ago

Let's try to find a more "proper" solution first instead. I've had a brief look at axtix/tokio bootstrapping and it looks like we could hook into the internals by providing a custom Runtime. The best solution would be a runtime that simply delegates everything to GTK by providing a Future that can be polled. (Effectively, our main issue is that we use block_on as an intermediate so we lose the information about wakeups.)

Regarding my proposal, I'm thinking about a wrapper that counts how many times it had to poll the underlying future. The assumption is, that an idle system will resolve rather quickly. An alternative would be to count the GTK signals we get – of course this is less accurate but would still be an improvement over low-polling all the time.

idanarye commented 3 years ago

Let's try to find a more "proper" solution first instead. I've had a brief look at axtix/tokio bootstrapping and it looks like we could hook into the internals by providing a custom Runtime.

Where? How? I've looked at Tokio's docs and didn't see any way to provide your own runtime - at most you can use a runtime builder that can be configured in very specific ways. And I looked at Actix' docs too - you can't pass a Tokio runtime to it, you can only create a System which creates a Tokio runtime on its own.

What am I missing?

Regarding my proposal, I'm thinking about a wrapper that counts how many times it had to poll the underlying future. The assumption is, that an idle system will resolve rather quickly.

Which "underlying future"? We only have access to the crank future we use to crank the runtime. Currently it's an actix::clock::sleep so it should only wake once regardless of load. If we change it to something that wakes after each cycle - we'll have a busy wait there...

An alternative would be to count the GTK signals we get – of course this is less accurate but would still be an improvement over low-polling all the time.

Actix actors in WoAB apps are not limited to just handling GTK signals. Specifically in Kosem (the project I originally created WoAB for) I'm using actix-web-actors to make a WebSocket client, and need entering WebSocket messages to trigger the Actix runtime which will eventually update the GUI. These are not GTK signals, and I can't wait for some arbitrary GTK signal to randomly show up and crank the Runtime so that I can handle the WebSocket message.

piegamesde commented 3 years ago

And I looked at Actix' docs too - you can't pass a Tokio runtime to it

The appropriate method is #[doc(hidden)] :smiling_imp: https://docs.rs/actix-rt/2.2.0/src/actix_rt/system.rs.html#49

Which "underlying future"?

We'd need something that builds wraps around actix::clock::sleep(core::time::Duration::new(0, 0)). But I'll admit that I haven't thought this 100% through, so maybe this simply is not feasible.

These are not GTK signals, and I can't wait for some arbitrary GTK signal to randomly show up and crank the Runtime so that I can handle the WebSocket message.

You missed my point here. I was talking about reducing the frequency to the 100Hz you suggested in that case, so events would still get processed.

idanarye commented 3 years ago

The appropriate method is #[doc(hidden)] smiling_imp https://docs.rs/actix-rt/2.2.0/src/actix_rt/system.rs.html#49

OK, so that's one problem solved - but the Tokio runtime itself still seems closed - we can't supply our own implementation.

You missed my point here. I was talking about reducing the frequency to the 100Hz you suggested in that case, so events would still get processed.

There are actually two "frequencies" here:

Currently both frequencies tend to infinity. GTK will let Actix check for events whenever its idle, and Actix will yield back to GTK immediately after a quick check. This causes the busy waiting.

From what I understand, your solution is to reduce the frequency where GTK lets Actix do its thing, and when Actix actually has stuff to do just increase that frequency. My solution is the opposite - reduce the frequency where Actix lets GTK do its thing.

I think the advantages of my approach are:

piegamesde commented 3 years ago

Ooh, I haven't ever thought about the other way around. I can see the advantages. I've dug a bit on the Tokio code and couldn't find any opening. There is an issue linked by a discussion thread that probably covers our use case.

Thus, I suggest merging that change in the lack of better solution. We may want to re-discuss this once executor independent async frameworks are a thing in Rust (i.e. once Actix allows use to move off Tokio).

idanarye commented 3 years ago

Maybe I should add a heavy load example - something that creates a ListBox and fills it with many rows - so test my hypothesis about the GTK loop keeping busy and not going idle.

piegamesde commented 3 years ago

Maybe continuously drawing non-trivial things on a canvas would be a more realistic example use case.

idanarye commented 3 years ago

What do you mean by that? Drawing on a canvas is done inside the draw signal handler, so it's going to stress Cairo and it's going to stress the function that does the drawing - but that still counts as one event so it won't stress the GTK event loop, which is what I want to test.

idanarye commented 3 years ago

Maybe many canvases with simple draw handlers? That will trigger many signals when GTK needs to redraw them and all these signals will have to go through WoAB to Actix.

piegamesde commented 3 years ago

I was thinking about many redraws, like for an animation.

idanarye commented 3 years ago

I ended up doing both - a simple animation on lots of canvases. The 10ms waits in Actix do not seem to slow down the responsiveness.