Skip to content

Commit 03b324f

Browse files
committed
auto merge of #12186 : alexcrichton/rust/no-sleep-2, r=brson
Any single-threaded task benchmark will spend a good chunk of time in `kqueue()` on osx and `epoll()` on linux, and the reason for this is that each time a task is terminated it will hit the syscall. When a task terminates, it context switches back to the scheduler thread, and the scheduler thread falls out of `run_sched_once` whenever it figures out that it did some work. If we know that `epoll()` will return nothing, then we can continue to do work locally (only while there's work to be done). We must fall back to `epoll()` whenever there's active I/O in order to check whether it's ready or not, but without that (which is largely the case in benchmarks), we can prevent the costly syscall and can get a nice speedup. I've separated the commits into preparation for this change and then the change itself, the last commit message has more details.
2 parents 2fe7bfe + 2650b61 commit 03b324f

File tree

12 files changed

+157
-85
lines changed

12 files changed

+157
-85
lines changed

src/libgreen/basic.rs

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ impl EventLoop for BasicLoop {
158158
}
159159

160160
fn io<'a>(&'a mut self) -> Option<&'a mut IoFactory> { None }
161+
162+
fn has_active_io(&self) -> bool { false }
161163
}
162164

163165
struct BasicRemote {

src/libgreen/sched.rs

+97-60
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,23 @@ impl Scheduler {
252252

253253
// * Execution Functions - Core Loop Logic
254254

255-
// The model for this function is that you continue through it
256-
// until you either use the scheduler while performing a schedule
257-
// action, in which case you give it away and return early, or
258-
// you reach the end and sleep. In the case that a scheduler
259-
// action is performed the loop is evented such that this function
260-
// is called again.
255+
// This function is run from the idle callback on the uv loop, indicating
256+
// that there are no I/O events pending. When this function returns, we will
257+
// fall back to epoll() in the uv event loop, waiting for more things to
258+
// happen. We may come right back off epoll() if the idle callback is still
259+
// active, in which case we're truly just polling to see if I/O events are
260+
// complete.
261+
//
262+
// The model for this function is to execute as much work as possible while
263+
// still fairly considering I/O tasks. Falling back to epoll() frequently is
264+
// often quite expensive, so we attempt to avoid it as much as possible. If
265+
// we have any active I/O on the event loop, then we're forced to fall back
266+
// to epoll() in order to provide fairness, but as long as we're doing work
267+
// and there's no active I/O, we can continue to do work.
268+
//
269+
// If we try really hard to do some work, but no work is available to be
270+
// done, then we fall back to epoll() to block this thread waiting for more
271+
// work (instead of busy waiting).
261272
fn run_sched_once(mut ~self, stask: ~GreenTask) {
262273
// Make sure that we're not lying in that the `stask` argument is indeed
263274
// the scheduler task for this scheduler.
@@ -269,26 +280,46 @@ impl Scheduler {
269280

270281
// First we check for scheduler messages, these are higher
271282
// priority than regular tasks.
272-
let (sched, stask) =
273-
match self.interpret_message_queue(stask, DontTryTooHard) {
274-
Some(pair) => pair,
275-
None => return
276-
};
277-
278-
// This helper will use a randomized work-stealing algorithm
279-
// to find work.
280-
let (sched, stask) = match sched.do_work(stask) {
281-
Some(pair) => pair,
282-
None => return
283-
};
283+
let (mut sched, mut stask, mut did_work) =
284+
self.interpret_message_queue(stask, DontTryTooHard);
284285

285-
// Now, before sleeping we need to find out if there really
286-
// were any messages. Give it your best!
287-
let (mut sched, stask) =
288-
match sched.interpret_message_queue(stask, GiveItYourBest) {
289-
Some(pair) => pair,
290-
None => return
286+
// After processing a message, we consider doing some more work on the
287+
// event loop. The "keep going" condition changes after the first
288+
// iteration becase we don't want to spin here infinitely.
289+
//
290+
// Once we start doing work we can keep doing work so long as the
291+
// iteration does something. Note that we don't want to starve the
292+
// message queue here, so each iteration when we're done working we
293+
// check the message queue regardless of whether we did work or not.
294+
let mut keep_going = !did_work || !sched.event_loop.has_active_io();
295+
while keep_going {
296+
let (a, b, c) = match sched.do_work(stask) {
297+
(sched, task, false) => {
298+
sched.interpret_message_queue(task, GiveItYourBest)
299+
}
300+
(sched, task, true) => {
301+
let (sched, task, _) =
302+
sched.interpret_message_queue(task, GiveItYourBest);
303+
(sched, task, true)
304+
}
291305
};
306+
sched = a;
307+
stask = b;
308+
did_work = c;
309+
310+
// We only keep going if we managed to do something productive and
311+
// also don't have any active I/O. If we didn't do anything, we
312+
// should consider going to sleep, and if we have active I/O we need
313+
// to poll for completion.
314+
keep_going = did_work && !sched.event_loop.has_active_io();
315+
}
316+
317+
// If we ever did some work, then we shouldn't put our scheduler
318+
// entirely to sleep just yet. Leave the idle callback active and fall
319+
// back to epoll() to see what's going on.
320+
if did_work {
321+
return stask.put_with_sched(sched);
322+
}
292323

293324
// If we got here then there was no work to do.
294325
// Generate a SchedHandle and push it to the sleeper list so
@@ -318,7 +349,7 @@ impl Scheduler {
318349
// return None.
319350
fn interpret_message_queue(mut ~self, stask: ~GreenTask,
320351
effort: EffortLevel)
321-
-> Option<(~Scheduler, ~GreenTask)>
352+
-> (~Scheduler, ~GreenTask, bool)
322353
{
323354

324355
let msg = if effort == DontTryTooHard {
@@ -349,25 +380,25 @@ impl Scheduler {
349380
Some(PinnedTask(task)) => {
350381
let mut task = task;
351382
task.give_home(HomeSched(self.make_handle()));
352-
self.resume_task_immediately(stask, task).put();
353-
return None;
383+
let (sched, task) = self.resume_task_immediately(stask, task);
384+
(sched, task, true)
354385
}
355386
Some(TaskFromFriend(task)) => {
356387
rtdebug!("got a task from a friend. lovely!");
357-
self.process_task(stask, task,
358-
Scheduler::resume_task_immediately_cl);
359-
return None;
388+
let (sched, task) =
389+
self.process_task(stask, task,
390+
Scheduler::resume_task_immediately_cl);
391+
(sched, task, true)
360392
}
361393
Some(RunOnce(task)) => {
362394
// bypass the process_task logic to force running this task once
363395
// on this home scheduler. This is often used for I/O (homing).
364-
self.resume_task_immediately(stask, task).put();
365-
return None;
396+
let (sched, task) = self.resume_task_immediately(stask, task);
397+
(sched, task, true)
366398
}
367399
Some(Wake) => {
368400
self.sleepy = false;
369-
stask.put_with_sched(self);
370-
return None;
401+
(self, stask, true)
371402
}
372403
Some(Shutdown) => {
373404
rtdebug!("shutting down");
@@ -389,31 +420,30 @@ impl Scheduler {
389420
// event loop references we will shut down.
390421
self.no_sleep = true;
391422
self.sleepy = false;
392-
stask.put_with_sched(self);
393-
return None;
423+
(self, stask, true)
394424
}
395425
Some(NewNeighbor(neighbor)) => {
396426
self.work_queues.push(neighbor);
397-
return Some((self, stask));
398-
}
399-
None => {
400-
return Some((self, stask));
427+
(self, stask, false)
401428
}
429+
None => (self, stask, false)
402430
}
403431
}
404432

405-
fn do_work(mut ~self, stask: ~GreenTask) -> Option<(~Scheduler, ~GreenTask)> {
433+
fn do_work(mut ~self,
434+
stask: ~GreenTask) -> (~Scheduler, ~GreenTask, bool) {
406435
rtdebug!("scheduler calling do work");
407436
match self.find_work() {
408437
Some(task) => {
409438
rtdebug!("found some work! running the task");
410-
self.process_task(stask, task,
411-
Scheduler::resume_task_immediately_cl);
412-
return None;
439+
let (sched, task) =
440+
self.process_task(stask, task,
441+
Scheduler::resume_task_immediately_cl);
442+
(sched, task, true)
413443
}
414444
None => {
415445
rtdebug!("no work was found, returning the scheduler struct");
416-
return Some((self, stask));
446+
(self, stask, false)
417447
}
418448
}
419449
}
@@ -486,7 +516,8 @@ impl Scheduler {
486516
// place.
487517

488518
fn process_task(mut ~self, cur: ~GreenTask,
489-
mut next: ~GreenTask, schedule_fn: SchedulingFn) {
519+
mut next: ~GreenTask,
520+
schedule_fn: SchedulingFn) -> (~Scheduler, ~GreenTask) {
490521
rtdebug!("processing a task");
491522

492523
match next.take_unwrap_home() {
@@ -495,23 +526,23 @@ impl Scheduler {
495526
rtdebug!("sending task home");
496527
next.give_home(HomeSched(home_handle));
497528
Scheduler::send_task_home(next);
498-
cur.put_with_sched(self);
529+
(self, cur)
499530
} else {
500531
rtdebug!("running task here");
501532
next.give_home(HomeSched(home_handle));
502-
schedule_fn(self, cur, next);
533+
schedule_fn(self, cur, next)
503534
}
504535
}
505536
AnySched if self.run_anything => {
506537
rtdebug!("running anysched task here");
507538
next.give_home(AnySched);
508-
schedule_fn(self, cur, next);
539+
schedule_fn(self, cur, next)
509540
}
510541
AnySched => {
511542
rtdebug!("sending task to friend");
512543
next.give_home(AnySched);
513544
self.send_to_friend(next);
514-
cur.put_with_sched(self);
545+
(self, cur)
515546
}
516547
}
517548
}
@@ -664,18 +695,19 @@ impl Scheduler {
664695
// * Context Swapping Helpers - Here be ugliness!
665696

666697
pub fn resume_task_immediately(~self, cur: ~GreenTask,
667-
next: ~GreenTask) -> ~GreenTask {
698+
next: ~GreenTask) -> (~Scheduler, ~GreenTask) {
668699
assert!(cur.is_sched());
669-
self.change_task_context(cur, next, |sched, stask| {
700+
let mut cur = self.change_task_context(cur, next, |sched, stask| {
670701
assert!(sched.sched_task.is_none());
671702
sched.sched_task = Some(stask);
672-
})
703+
});
704+
(cur.sched.take_unwrap(), cur)
673705
}
674706

675707
fn resume_task_immediately_cl(sched: ~Scheduler,
676708
cur: ~GreenTask,
677-
next: ~GreenTask) {
678-
sched.resume_task_immediately(cur, next).put()
709+
next: ~GreenTask) -> (~Scheduler, ~GreenTask) {
710+
sched.resume_task_immediately(cur, next)
679711
}
680712

681713
/// Block a running task, context switch to the scheduler, then pass the
@@ -741,15 +773,17 @@ impl Scheduler {
741773
cur.put();
742774
}
743775

744-
fn switch_task(sched: ~Scheduler, cur: ~GreenTask, next: ~GreenTask) {
745-
sched.change_task_context(cur, next, |sched, last_task| {
776+
fn switch_task(sched: ~Scheduler, cur: ~GreenTask,
777+
next: ~GreenTask) -> (~Scheduler, ~GreenTask) {
778+
let mut cur = sched.change_task_context(cur, next, |sched, last_task| {
746779
if last_task.is_sched() {
747780
assert!(sched.sched_task.is_none());
748781
sched.sched_task = Some(last_task);
749782
} else {
750783
sched.enqueue_task(last_task);
751784
}
752-
}).put()
785+
});
786+
(cur.sched.take_unwrap(), cur)
753787
}
754788

755789
// * Task Context Helpers
@@ -769,7 +803,9 @@ impl Scheduler {
769803
}
770804

771805
pub fn run_task(~self, cur: ~GreenTask, next: ~GreenTask) {
772-
self.process_task(cur, next, Scheduler::switch_task);
806+
let (sched, task) =
807+
self.process_task(cur, next, Scheduler::switch_task);
808+
task.put_with_sched(sched);
773809
}
774810

775811
pub fn run_task_later(mut cur: ~GreenTask, next: ~GreenTask) {
@@ -836,7 +872,8 @@ impl Scheduler {
836872

837873
// Supporting types
838874

839-
type SchedulingFn = extern "Rust" fn (~Scheduler, ~GreenTask, ~GreenTask);
875+
type SchedulingFn = fn (~Scheduler, ~GreenTask, ~GreenTask)
876+
-> (~Scheduler, ~GreenTask);
840877

841878
pub enum SchedMessage {
842879
Wake,

src/librustuv/addrinfo.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl GetAddrInfoRequest {
8686
req.defuse(); // uv callback now owns this request
8787
let mut cx = Ctx { slot: None, status: 0, addrinfo: None };
8888

89-
wait_until_woken_after(&mut cx.slot, || {
89+
wait_until_woken_after(&mut cx.slot, loop_, || {
9090
req.set_data(&cx);
9191
});
9292

src/librustuv/file.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ fn execute(f: |*uvll::uv_fs_t, uvll::uv_fs_cb| -> c_int)
304304
0 => {
305305
req.fired = true;
306306
let mut slot = None;
307-
wait_until_woken_after(&mut slot, || {
307+
let loop_ = unsafe { uvll::get_loop_from_fs_req(req.req) };
308+
wait_until_woken_after(&mut slot, &Loop::wrap(loop_), || {
308309
unsafe { uvll::set_data_for_req(req.req, &slot) }
309310
});
310311
match req.get_result() {

src/librustuv/lib.rs

+25-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ via `close` and `delete` methods.
4747
use std::cast;
4848
use std::io;
4949
use std::io::IoError;
50-
use std::libc::c_int;
50+
use std::libc::{c_int, c_void};
5151
use std::ptr::null;
5252
use std::ptr;
5353
use std::rt::local::Local;
@@ -95,6 +95,10 @@ pub mod stream;
9595
pub trait UvHandle<T> {
9696
fn uv_handle(&self) -> *T;
9797

98+
fn uv_loop(&self) -> Loop {
99+
Loop::wrap(unsafe { uvll::get_loop_for_uv_handle(self.uv_handle()) })
100+
}
101+
98102
// FIXME(#8888) dummy self
99103
fn alloc(_: Option<Self>, ty: uvll::uv_handle_type) -> *T {
100104
unsafe {
@@ -136,7 +140,7 @@ pub trait UvHandle<T> {
136140
uvll::uv_close(self.uv_handle() as *uvll::uv_handle_t, close_cb);
137141
uvll::set_data_for_uv_handle(self.uv_handle(), ptr::null::<()>());
138142

139-
wait_until_woken_after(&mut slot, || {
143+
wait_until_woken_after(&mut slot, &self.uv_loop(), || {
140144
uvll::set_data_for_uv_handle(self.uv_handle(), &slot);
141145
})
142146
}
@@ -195,16 +199,20 @@ impl Drop for ForbidUnwind {
195199
}
196200
}
197201

198-
fn wait_until_woken_after(slot: *mut Option<BlockedTask>, f: ||) {
202+
fn wait_until_woken_after(slot: *mut Option<BlockedTask>,
203+
loop_: &Loop,
204+
f: ||) {
199205
let _f = ForbidUnwind::new("wait_until_woken_after");
200206
unsafe {
201207
assert!((*slot).is_none());
202208
let task: ~Task = Local::take();
209+
loop_.modify_blockers(1);
203210
task.deschedule(1, |task| {
204211
*slot = Some(task);
205212
f();
206213
Ok(())
207214
});
215+
loop_.modify_blockers(-1);
208216
}
209217
}
210218

@@ -273,6 +281,7 @@ impl Loop {
273281
pub fn new() -> Loop {
274282
let handle = unsafe { uvll::loop_new() };
275283
assert!(handle.is_not_null());
284+
unsafe { uvll::set_data_for_uv_loop(handle, 0 as *c_void) }
276285
Loop::wrap(handle)
277286
}
278287

@@ -285,6 +294,19 @@ impl Loop {
285294
pub fn close(&mut self) {
286295
unsafe { uvll::uv_loop_delete(self.handle) };
287296
}
297+
298+
// The 'data' field of the uv_loop_t is used to count the number of tasks
299+
// that are currently blocked waiting for I/O to complete.
300+
fn modify_blockers(&self, amt: uint) {
301+
unsafe {
302+
let cur = uvll::get_data_for_uv_loop(self.handle) as uint;
303+
uvll::set_data_for_uv_loop(self.handle, (cur + amt) as *c_void)
304+
}
305+
}
306+
307+
fn get_blockers(&self) -> uint {
308+
unsafe { uvll::get_data_for_uv_loop(self.handle) as uint }
309+
}
288310
}
289311

290312
// FIXME: Need to define the error constants like EOF so they can be

0 commit comments

Comments
 (0)