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

TaskPool.spawn_pollable #2691

Closed
wants to merge 5 commits into from
Closed

Conversation

hanabi1224
Copy link

Add fn spawn_pollable->PollableTask api to TaskPool to avoid using poll_once and block_on in user code

Objective

  • Avoid poll_once and block_on in user system code as they actually do nothing but add non-trival performance overhead
  • Cleaner API

Solution

@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Aug 19, 2021
@alice-i-cecile alice-i-cecile added A-Tasks Tools for parallel and async work C-Feature A new feature, making something new possible and removed S-Needs-Triage This issue needs to be labelled labels Aug 24, 2021
@Ratysz
Copy link
Contributor

Ratysz commented Sep 15, 2021

I've used something similar recently: create a one-shot async channel, return the wrapped receiver as the thing that can be "polled", wrap the compute future in a future that sends the result into the sender, spawn it and detach the task.

Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm broadly in favour - this should be significantly cheaper than the heavyweight block_on. Just a few thoughts on the implementation.

/// on every frame update without blocking on a future
#[derive(Debug)]
pub struct PollableTask<T> {
result: Arc<RwLock<Option<T>>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's unclear why this is an Arc<RwLock>, instead of e.g. a std::sync::mpsc::Reciever<T> constructed using std::sync::mpsc::sync_channel(1).

@@ -76,22 +75,22 @@ fn spawn_tasks(mut commands: Commands, thread_pool: Res<AsyncComputeTaskPool>) {
/// removes the task component from the entity.
fn handle_tasks(
mut commands: Commands,
mut transform_tasks: Query<(Entity, &mut Task<Transform>)>,
transform_tasks: Query<(Entity, &PollableTask<Transform>)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the PollableTask as a component is probably an anti-pattern (due to the potential for multiple libraries to create such tasks with the same result type); we should make a custom wrapper struct for this task.

(PollableTask should not be Component once [derive(Component)] lands)

}
}

pub fn poll(&self) -> LockResult<RwLockReadGuard<Option<T>>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need interior mutability here - having interior mutable things in the ecs is an anti-pattern, because it makes ambiguity detection less useful, for example. Additionally, we should simplify the return type to an Option, which just takes the result if it exists.

That is, imo the only method should be:

pub fn poll(&self)->Option<T> { ... }

pub struct PollableTask<T> {
result: Arc<RwLock<Option<T>>>,
// this is to keep the task alive
_task: Task<()>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should provide a way to destructure this if the user has an owned valud, to allow e.g. Task::detach to be called.

.into_iter()
.map(|i| {
pool.spawn_pollable(async move {
futures_timer::Delay::new(Duration::from_secs_f32(0.5)).await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't have time based tests here. Just removing both sets of delays would make this a good enough test.

let transform_fn = |i: usize| i * 3;

let pool = TaskPool::new();
let nums: Vec<usize> = (1..10).into_iter().collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaik, into_iter is unneeded here. I.e.

Suggested change
let nums: Vec<usize> = (1..10).into_iter().collect();
let nums: Vec<usize> = (1..10).collect();

would be just as valid.

@@ -309,6 +336,42 @@ mod tests {
assert_eq!(count.load(Ordering::Relaxed), 100);
}

#[test]
fn test_spawn_pollable() {
let transform_fn = |i: usize| i * 3;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear why you use usize throughout this function - in this case, u32 or even u8 might be clearer, since these values are only numbers; they're not used for anything other than simple arithmetic and comparison (i.e. no pointer maths/indexing)

Comment on lines +359 to +367
for i in 0..pollables.len() {
let locked = pollables[i].poll().unwrap();
if let Some(r) = *locked {
assert_eq!(r, transform_fn(nums[i]));
} else {
done = false;
break;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for i in 0..pollables.len() {
let locked = pollables[i].poll().unwrap();
if let Some(r) = *locked {
assert_eq!(r, transform_fn(nums[i]));
} else {
done = false;
break;
}
}
for (pollable, source) in pollables.iter().zip(nums.clone()) {
let locked = pollable.poll().unwrap();
if let Some(r) = *locked {
assert_eq!(r, transform_fn(source));
} else {
done = false;
break;
}
}

Doing it this way also lets you avoid allocating nums as a vec entirely - it can stay as a Range

if done {
return;
}
std::thread::sleep(Duration::from_secs_f32(1.0 / 60.0));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you remove the timing based parts of this task, it will make sense to change this to std::thread::yield_now

fn test_spawn_pollable() {
let transform_fn = |i: usize| i * 3;

let pool = TaskPool::new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd have some way to limit the number of threads this spawns, since each task is extremely lightweight. That's not a problem for this PR to solve

@pubrrr
Copy link
Contributor

pubrrr commented Feb 28, 2022

Is there any progress on merging this PR? I guess this would mostly solve my questions in #4045

@IceSentry
Copy link
Contributor

IceSentry commented Feb 28, 2022

It just needs more reviews. If we get 3 community reviews we can then add a S-Ready-For-Final-Review label and show it to cart. It would also help if the author responded to the current reviews

@alice-i-cecile alice-i-cecile added C-Performance A change motivated by improving speed, memory usage or compile times C-Usability A targeted quality-of-life change that makes Bevy easier to use and removed C-Feature A new feature, making something new possible labels May 18, 2022
@alice-i-cecile
Copy link
Member

Closing in favor of #4062.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Tasks Tools for parallel and async work C-Performance A change motivated by improving speed, memory usage or compile times C-Usability A targeted quality-of-life change that makes Bevy easier to use
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants