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

Improve idle performance #42

Closed
piegamesde opened this issue Jun 27, 2021 · 12 comments
Closed

Improve idle performance #42

piegamesde opened this issue Jun 27, 2021 · 12 comments

Comments

@piegamesde
Copy link
Contributor

piegamesde commented Jun 27, 2021

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
Copy link
Owner

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
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

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

The appropriate method is #[doc(hidden)] 😈 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
Copy link
Owner

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:

  • When GTK waits for GTK events, how often does it let Actix check for new Actix events?
  • When Actix waits for Actix events, how often does it let GTK check for new GTK events?

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:

  • GTK events are triggered by humans, and 10ms is not something a human is going to feel. Actix events are triggered by other things (other processes, network) - we are causing latency on the other side.
  • Actually humans will feel 10ms delays if they repeat often enough to add up - but g_idle_add will only run the function if "there are no higher priority events pending to the default main loop" - so if GTK is busy it'll do GTK stuff until it's done and won't keep going to Actix to wait 10ms. It will go to Actix if it needs to run a signal - but in that path it doesn't wait, it resolves the signal as fast as it can, so it won't be amassing 10ms delays here.
  • If I'm wrong about that previous bullet, or if we want to be smarter about it (e.g. wait less in Actix after GTK handles events so that a single eventless frame won't trigger a 10ms downtime) we can use gtk::functions::events_pending to tell whether or not GTK needs the CPU. We don't have anything similar for Actix.

@piegamesde
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

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

@idanarye
Copy link
Owner

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
Copy link
Owner

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
Copy link
Contributor Author

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

@idanarye
Copy link
Owner

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.

idanarye added a commit that referenced this issue Jun 30, 2021
Fix #42: don't busy-wait with `glib::idle_add`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants