mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 22:20:19 +00:00
wip: more work on scheduler
This commit is contained in:
parent
5c3dc0a874
commit
1cd5e69712
6 changed files with 140 additions and 109 deletions
|
@ -28,7 +28,7 @@ pub static App: FC<()> = |cx| {
|
|||
}
|
||||
button {
|
||||
"Start counting"
|
||||
onclick: move |_| task.start()
|
||||
onclick: move |_| task.resume()
|
||||
}
|
||||
button {
|
||||
"Switch counting direcion"
|
||||
|
|
|
@ -47,7 +47,7 @@ pub static Example: FC<()> = |cx| {
|
|||
}
|
||||
button {
|
||||
"Start counting"
|
||||
onclick: move |_| task.start()
|
||||
onclick: move |_| task.resume()
|
||||
}
|
||||
button {
|
||||
"Switch counting direcion"
|
||||
|
|
2
packages/core/.vscode/settings.json
vendored
2
packages/core/.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
}
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue