wip: more work on scheduler

This commit is contained in:
Jonathan Kelley 2021-09-01 23:22:34 -04:00
parent 5c3dc0a874
commit 1cd5e69712
6 changed files with 140 additions and 109 deletions

View file

@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| {
}
button {
"Start counting"
onclick: move |_| task.start()
onclick: move |_| task.resume()
}
button {
"Switch counting direcion"

View file

@ -47,7 +47,7 @@ pub static Example: FC<()> = |cx| {
}
button {
"Start counting"
onclick: move |_| task.start()
onclick: move |_| task.resume()
}
button {
"Switch counting direcion"

View file

@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": false
"rust-analyzer.inlayHints.enable": true
}

View file

@ -88,9 +88,8 @@
//! More info on how to improve this diffing algorithm:
//! - https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/
use crate::{innerlude::*, scheduler::Scheduler};
use crate::innerlude::*;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use DomEdit::*;
/// Our DiffMachine is an iterative tree differ.
@ -154,14 +153,7 @@ impl<'bump> DiffMachine<'bump> {
}
}
// pub fn new_headless(shared: &'bump SharedResources) -> Self {
// let edits = Mutations::new();
// let cur_scope = ScopeId(0);
// Self::new(edits, cur_scope, shared)
// }
//
pub async fn diff_scope(&'bump mut self, id: ScopeId) {
pub fn diff_scope(&'bump mut self, id: ScopeId) {
if let Some(component) = self.vdom.get_scope_mut(id) {
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
self.stack.push(DiffInstruction::DiffNode { new, old });
@ -173,7 +165,9 @@ impl<'bump> DiffMachine<'bump> {
/// This method implements a depth-first iterative tree traversal.
///
/// We do depth-first to maintain high cache locality (nodes were originally generated recursively).
pub async fn work(&mut self) {
///
/// Returns a `bool` indicating that the work completed properly.
pub fn work(&mut self, deadline_expired: &mut impl FnMut() -> bool) -> bool {
while let Some(instruction) = self.stack.pop() {
// defer to individual functions so the compiler produces better code
// large functions tend to be difficult for the compiler to work with
@ -193,9 +187,12 @@ impl<'bump> DiffMachine<'bump> {
DiffInstruction::PrepareMoveNode { node } => self.prepare_move_node(node),
};
// todo: call this less frequently, there is a bit of overhead involved
yield_now().await;
if deadline_expired() {
return false;
}
}
true
}
fn prepare_move_node(&mut self, node: &'bump VNode<'bump>) {

View file

@ -18,7 +18,7 @@ point if being "fast" if you can't also be "responsive."
As such, Dioxus can manually decide on what work is most important at any given moment in time. With a properly tuned
priority system, Dioxus can ensure that user interaction is prioritized and committed as soon as possible (sub 100ms).
The controller responsible for this priority management is called the "scheduler" and is responsible for juggle many
The controller responsible for this priority management is called the "scheduler" and is responsible for juggling many
different types of work simultaneously.
# How does it work?
@ -155,7 +155,7 @@ impl Scheduler {
pub fn new() -> Self {
/*
Preallocate 2000 elements and 100 scopes to avoid dynamic allocation.
Perhaps this should be configurable?
Perhaps this should be configurable from some external config?
*/
let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100)));
let raw_elements = Rc::new(UnsafeCell::new(Slab::with_capacity(2000)));
@ -283,21 +283,14 @@ impl Scheduler {
// nothing to do, no events on channels, no work
pub fn has_any_work(&self) -> bool {
self.has_work() || self.has_pending_events() || self.has_pending_garbage()
let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
pending_lanes || self.has_pending_events()
}
pub fn has_pending_events(&self) -> bool {
self.ui_events.len() > 0
}
pub fn has_work(&self) -> bool {
self.lanes.iter().find(|f| f.has_work()).is_some()
}
pub fn has_pending_garbage(&self) -> bool {
!self.garbage_scopes.is_empty()
}
fn shift_priorities(&mut self) {
self.current_priority = match (
self.lanes[0].has_work(),
@ -312,6 +305,9 @@ impl Scheduler {
};
}
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
fn balance_lanes(&mut self) {}
fn load_current_lane(&mut self) -> &mut PriorityLane {
match self.current_priority {
EventPriority::Immediate => todo!(),
@ -335,6 +331,29 @@ impl Scheduler {
}
}
/// Work the scheduler down, not polling any ongoing tasks.
///
/// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
let mut committed_mutations = Vec::<Mutations<'static>>::new();
// Internalize any pending work since the last time we ran
self.manually_poll_events();
if !self.has_any_work() {
self.pool.clean_up_garbage();
}
while self.has_any_work() {
// do work
// Create work from the pending event queue
self.consume_pending_events();
}
committed_mutations
}
/// The primary workhorse of the VirtualDOM.
///
/// Uses some fairly complex logic to schedule what work should be produced.
@ -342,7 +361,7 @@ impl Scheduler {
/// Returns a list of successful mutations.
pub async fn work_with_deadline<'a>(
&'a mut self,
mut deadline: Pin<Box<impl FusedFuture<Output = ()>>>,
mut deadline_reached: Pin<Box<impl FusedFuture<Output = ()>>>,
) -> Vec<Mutations<'a>> {
/*
Strategy:
@ -374,7 +393,7 @@ impl Scheduler {
// Wait for any new events if we have nothing to do
if !self.has_any_work() {
self.pool.clean_up_garbage();
let deadline_expired = self.wait_for_any_trigger(&mut deadline).await;
let deadline_expired = self.wait_for_any_trigger(&mut deadline_reached).await;
if deadline_expired {
return committed_mutations;
@ -384,58 +403,15 @@ impl Scheduler {
// Create work from the pending event queue
self.consume_pending_events();
// Work through the current subtree, and commit the results when it finishes
// When the deadline expires, give back the work
// shift to the correct lane
self.shift_priorities();
let saved_state = self.load_work();
let mut deadline_reached = || (&mut deadline_reached).now_or_never().is_some();
// We have to split away some parts of ourself - current lane is borrowed mutably
let mut shared = self.pool.clone();
let mut machine = unsafe { saved_state.promote(&mut shared) };
let finished_before_deadline =
self.work_on_current_lane(&mut deadline_reached, &mut committed_mutations);
if machine.stack.is_empty() {
let shared = self.pool.clone();
self.current_lane().dirty_scopes.sort_by(|a, b| {
let h1 = shared.get_scope(*a).unwrap().height;
let h2 = shared.get_scope(*b).unwrap().height;
h1.cmp(&h2)
});
if let Some(scope) = self.current_lane().dirty_scopes.pop() {
let component = self.pool.get_scope(scope).unwrap();
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
machine.stack.push(DiffInstruction::DiffNode { new, old });
}
}
let completed = {
let fut = machine.work();
pin_mut!(fut);
use futures_util::future::{select, Either};
match select(fut, &mut deadline).await {
Either::Left((_, _other)) => true,
Either::Right((_, _other)) => false,
}
};
let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
let mut saved = machine.save();
if completed {
for node in saved.seen_scopes.drain() {
self.current_lane().dirty_scopes.remove(&node);
}
let mut new_mutations = Mutations::new();
std::mem::swap(&mut new_mutations, &mut saved.mutations);
committed_mutations.push(new_mutations);
}
self.save_work(saved);
if !completed {
if !finished_before_deadline {
break;
}
}
@ -443,6 +419,57 @@ impl Scheduler {
committed_mutations
}
// returns true if the lane is finished
pub fn work_on_current_lane(
&mut self,
deadline_reached: &mut impl FnMut() -> bool,
mutations: &mut Vec<Mutations>,
) -> bool {
// Work through the current subtree, and commit the results when it finishes
// When the deadline expires, give back the work
let saved_state = self.load_work();
// We have to split away some parts of ourself - current lane is borrowed mutably
let mut shared = self.pool.clone();
let mut machine = unsafe { saved_state.promote(&mut shared) };
if machine.stack.is_empty() {
let shared = self.pool.clone();
self.current_lane().dirty_scopes.sort_by(|a, b| {
let h1 = shared.get_scope(*a).unwrap().height;
let h2 = shared.get_scope(*b).unwrap().height;
h1.cmp(&h2)
});
if let Some(scope) = self.current_lane().dirty_scopes.pop() {
let component = self.pool.get_scope(scope).unwrap();
let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
machine.stack.push(DiffInstruction::DiffNode { new, old });
}
}
let deadline_expired = machine.work(deadline_reached);
let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
let mut saved = machine.save();
if deadline_expired {
self.save_work(saved);
false
} else {
for node in saved.seen_scopes.drain() {
self.current_lane().dirty_scopes.remove(&node);
}
let mut new_mutations = Mutations::new();
std::mem::swap(&mut new_mutations, &mut saved.mutations);
mutations.push(new_mutations);
self.save_work(saved);
true
}
}
// waits for a trigger, canceling early if the deadline is reached
// returns true if the deadline was reached
// does not return the trigger, but caches it in the scheduler
@ -537,16 +564,32 @@ impl TaskHandle {
/// Toggles this coroutine off/on.
///
/// This method is not synchronous - your task will not stop immediately.
pub fn toggle(&self) {}
pub fn toggle(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn start(&self) {}
pub fn resume(&self) {
self.sender
.unbounded_send(SchedulerMsg::ResumeTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn stop(&self) {}
pub fn stop(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
/// This method is not synchronous - your task will not stop immediately.
pub fn restart(&self) {}
pub fn restart(&self) {
self.sender
.unbounded_send(SchedulerMsg::ToggleTask(self.our_id))
.unwrap()
}
}
#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Hash, Debug)]
@ -589,9 +632,9 @@ impl ElementId {
/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
pub enum EventPriority {
/// Work that must be completed during the EventHandler phase
///
/// Work that must be completed during the EventHandler phase.
///
/// Currently this is reserved for controlled inputs.
Immediate = 3,
/// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.

View file

@ -209,13 +209,14 @@ impl VirtualDom {
.get_scope_mut(self.base_scope)
.expect("The base scope should never be moved");
// // We run the component. If it succeeds, then we can diff it and add the changes to the dom.
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope(&self.scheduler.pool) {
diff_machine
.stack
.create_node(cur_component.frames.fin_head(), MountType::Append);
diff_machine.stack.scope_stack.push(self.base_scope);
diff_machine.work().await;
// let completed = diff_machine.work();
} else {
// todo: should this be a hard error?
log::warn!(
@ -227,19 +228,7 @@ impl VirtualDom {
unsafe { std::mem::transmute(diff_machine.mutations) }
}
pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> {
let mut fut = self.diff_async().boxed_local();
loop {
if let Some(edits) = (&mut fut).now_or_never() {
break edits;
}
}
}
pub async fn diff_async<'s>(&'s mut self) -> Mutations<'s> {
let mut diff_machine = DiffMachine::new(Mutations::new(), todo!());
pub fn diff<'s>(&'s mut self) -> Mutations<'s> {
let cur_component = self
.scheduler
.pool
@ -247,22 +236,24 @@ impl VirtualDom {
.expect("The base scope should never be moved");
if cur_component.run_scope(&self.scheduler.pool) {
diff_machine.diff_scope(self.base_scope).await;
let mut diff_machine = DiffMachine::new(Mutations::new(), todo!());
diff_machine.diff_scope(self.base_scope);
diff_machine.mutations
} else {
Mutations::new()
}
diff_machine.mutations
}
/// Runs the virtualdom immediately, not waiting for any suspended nodes to complete.
///
/// This method will not wait for any suspended nodes to complete.
pub fn run_immediate<'s>(&'s mut self) -> Mutations<'s> {
todo!()
// use futures_util::FutureExt;
// let mut is_ready = || false;
// self.run_with_deadline(futures_util::future::ready(()), &mut is_ready)
// .now_or_never()
// .expect("this future will always resolve immediately")
/// This method will not wait for any suspended nodes to complete. If there is no pending work, then this method will
/// return "None"
pub fn run_immediate<'s>(&'s mut self) -> Option<Vec<Mutations<'s>>> {
if self.scheduler.has_any_work() {
Some(self.scheduler.work_sync())
} else {
None
}
}
/// Run the virtualdom with a deadline.