diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 1c68f97cc..79505b018 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 5f82c31e5..2c53384ab 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -96,13 +96,11 @@ impl<'src, P> Context<'src, P> { /// ## Notice: you should prefer using prepare_update and get_scope_id /// pub fn schedule_update(&self) -> Rc { - let cb = self.scope.vdom.schedule_update(); - let id = self.get_scope_id(); - Rc::new(move || cb(id)) + self.scope.memoized_updater.clone() } pub fn prepare_update(&self) -> Rc { - self.scope.vdom.schedule_update() + self.scope.shared.schedule_any_immediate.clone() } pub fn schedule_effect(&self) -> Rc { @@ -160,7 +158,7 @@ impl<'src, P> Context<'src, P> { /// /// pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.scope.vdom.submit_task(task) + (self.scope.shared.submit_task)(task) } /// Add a state globally accessible to child components via tree walking @@ -174,46 +172,11 @@ impl<'src, P> Context<'src, P> { }); } - /// Walk the tree to find a shared state with the TypeId of the generic type - /// pub fn consume_shared_state(self) -> Option> { - let mut scope = Some(self.scope); - let mut parent = None; - + let getter = &self.scope.shared.get_shared_context; let ty = TypeId::of::(); - while let Some(inner) = scope { - log::debug!( - "Searching {:#?} for valid shared_context", - inner.our_arena_idx - ); - let shared_ctx = { - let shared_contexts = inner.shared_contexts.borrow(); - - log::debug!( - "This component has {} shared contexts", - shared_contexts.len() - ); - shared_contexts.get(&ty).map(|f| f.clone()) - }; - - if let Some(shared_cx) = shared_ctx { - log::debug!("found matching cx"); - let rc = shared_cx - .clone() - .downcast::() - .expect("Should not fail, already validated the type from the hashmap"); - parent = Some(rc); - break; - } else { - match inner.parent_idx { - Some(parent_id) => { - scope = unsafe { inner.vdom.get_scope(parent_id) }; - } - None => break, - } - } - } - parent + let idx = self.scope.our_arena_idx; + getter(idx, ty).map(|f| f.downcast().unwrap()) } /// Store a value between renders diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a301b7642..47d8d378c 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -88,7 +88,7 @@ //! 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::{arena::Scheduler, innerlude::*}; +use crate::{innerlude::*, scheduler::Scheduler}; use fxhash::{FxHashMap, FxHashSet}; use DomEdit::*; @@ -110,9 +110,7 @@ pub struct DiffMachine<'bump> { pub mutations: &'bump mut Mutations<'bump>, pub stack: DiffStack<'bump>, - pub diffed: FxHashSet, - pub seen_scopes: FxHashSet, } @@ -310,6 +308,7 @@ impl<'bump> DiffMachine<'bump> { let parent_idx = self.stack.current_scope().unwrap(); + let shared = self.vdom.channel.clone(); // Insert a new scope into our component list let new_idx = self.vdom.insert_scope_with_key(|new_idx| { let parent_scope = self.vdom.get_scope(parent_idx).unwrap(); @@ -320,8 +319,7 @@ impl<'bump> DiffMachine<'bump> { Some(parent_idx), height, ScopeChildren(vcomponent.children), - // self.vdom.clone(), - vcomponent.name, + shared, ) }); diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 87ff359d9..f74aace14 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -17,169 +17,34 @@ use crate::{ #[derive(Debug)] pub struct EventTrigger { /// The originator of the event trigger - pub originator: ScopeId, + pub scope: ScopeId, /// The optional real node associated with the trigger - pub real_node_id: Option, + pub mounted_dom_id: Option, /// The type of event pub event: VirtualEvent, - /// The priority of the event - pub priority: EventPriority, -} - -#[derive(PartialEq, Eq, Clone, Copy)] -pub struct EventKey { - /// The originator of the event trigger - pub originator: ScopeId, - /// The priority of the event - pub priority: EventPriority, - /// The height of the scope (used for ordering) - pub height: u32, - // TODO: add the time that the event was queued -} - -impl PartialOrd for EventKey { - fn partial_cmp(&self, other: &Self) -> Option { - todo!() - } -} -impl Ord for EventKey { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - todo!() - } -} - -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - NoPriority = 0 -/// - LowPriority = 1 -/// - NormalPriority = 2 -/// - UserBlocking = 3 -/// - HighPriority = 4 -/// - ImmediatePriority = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking". - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take presedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -impl EventTrigger { - pub fn new( - event: VirtualEvent, - scope: ScopeId, - mounted_dom_id: Option, - priority: EventPriority, - ) -> Self { - Self { - priority, - originator: scope, - real_node_id: mounted_dom_id, - event, - } - } + pub discrete: bool, } pub enum VirtualEvent { - // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" - // - // Async events don't necessarily propagate into a scope being ran. It's up to the event itself - // to force an update for itself. - // - // Most async events should have a low priority. - // - // This type exists for the task/concurrency system to signal that a task is ready. - // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its - // own re-run. - AsyncEvent { - should_rerender: bool, - }, - - // Suspense events are a type of async event generated when suspended nodes are ready to be processed. - // - // they have the lowest priority - SuspenseEvent { - hook_idx: usize, - domnode: Rc>>, - }, - - // image event has conflicting method types - // ImageEvent(event_data::ImageEvent), - - // Real events + KeyboardEvent(on::KeyboardEvent), + TouchEvent(on::TouchEvent), + MouseEvent(on::MouseEvent), ClipboardEvent(on::ClipboardEvent), CompositionEvent(on::CompositionEvent), - KeyboardEvent(on::KeyboardEvent), FocusEvent(on::FocusEvent), FormEvent(on::FormEvent), SelectionEvent(on::SelectionEvent), - TouchEvent(on::TouchEvent), UIEvent(on::UIEvent), WheelEvent(on::WheelEvent), MediaEvent(on::MediaEvent), AnimationEvent(on::AnimationEvent), TransitionEvent(on::TransitionEvent), ToggleEvent(on::ToggleEvent), - MouseEvent(on::MouseEvent), PointerEvent(on::PointerEvent), -} - -impl VirtualEvent { - pub fn is_input_event(&self) -> bool { - match self { - VirtualEvent::ClipboardEvent(_) - | VirtualEvent::CompositionEvent(_) - | VirtualEvent::KeyboardEvent(_) - | VirtualEvent::FocusEvent(_) - | VirtualEvent::FormEvent(_) - | VirtualEvent::SelectionEvent(_) - | VirtualEvent::TouchEvent(_) - | VirtualEvent::UIEvent(_) - | VirtualEvent::WheelEvent(_) - | VirtualEvent::MediaEvent(_) - | VirtualEvent::AnimationEvent(_) - | VirtualEvent::TransitionEvent(_) - | VirtualEvent::ToggleEvent(_) - | VirtualEvent::MouseEvent(_) - | VirtualEvent::PointerEvent(_) => true, - - VirtualEvent::AsyncEvent { .. } | VirtualEvent::SuspenseEvent { .. } => false, - } - } + // ImageEvent(event_data::ImageEvent), } impl std::fmt::Debug for VirtualEvent { @@ -200,8 +65,6 @@ impl std::fmt::Debug for VirtualEvent { VirtualEvent::ToggleEvent(_) => "ToggleEvent", VirtualEvent::MouseEvent(_) => "MouseEvent", VirtualEvent::PointerEvent(_) => "PointerEvent", - VirtualEvent::AsyncEvent { .. } => "AsyncEvent", - VirtualEvent::SuspenseEvent { .. } => "SuspenseEvent", }; f.debug_struct("VirtualEvent").field("type", &name).finish() @@ -215,8 +78,7 @@ pub mod on { //! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling //! Arc allocation through "get_mut" //! - //! - //! + //! React recently dropped support for re-using event allocation and just passes the real event along. #![allow(unused)] use bumpalo::boxed::Box as BumpBox; @@ -285,14 +147,6 @@ pub mod on { } // The Dioxus Synthetic event system - // - // - // - // - // - // - // - // event_directory! { ClipboardEventInner(ClipboardEvent): [ /// Called when "copy" diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 3a0510ace..912427efc 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -130,9 +130,9 @@ where event: VirtualEvent::AsyncEvent { should_rerender: false, }, - originator, + scope: originator, priority: EventPriority::Low, - real_node_id: None, + mounted_dom_id: None, } }))); @@ -209,9 +209,9 @@ where *slot.borrow_mut() = Some(Box::new(output) as Box); EventTrigger { event: VirtualEvent::SuspenseEvent { hook_idx, domnode }, - originator, + scope: originator, priority: EventPriority::Low, - real_node_id: None, + mounted_dom_id: None, } }))); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d105f9770..5db7e88d4 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -28,7 +28,6 @@ pub mod prelude { // types used internally that are important pub(crate) mod innerlude { - pub use crate::arena::*; pub use crate::bumpframe::*; pub use crate::childiter::*; pub use crate::component::*; @@ -43,6 +42,7 @@ pub(crate) mod innerlude { pub use crate::hooks::*; pub use crate::mutations::*; pub use crate::nodes::*; + pub use crate::scheduler::*; // pub use crate::scheduler::*; pub use crate::scope::*; pub use crate::util::*; @@ -56,11 +56,12 @@ pub(crate) mod innerlude { } pub mod exports { - // export important things here + //! Important dependencies that are used by the rest of the library + + // the foundation of this library pub use bumpalo; } -pub mod arena; pub mod bumpframe; pub mod childiter; pub mod component; @@ -75,7 +76,7 @@ pub mod hooklist; pub mod hooks; pub mod mutations; pub mod nodes; -// pub mod scheduler; +pub mod scheduler; pub mod scope; pub mod signals; pub mod util; diff --git a/packages/core/src/arena.rs b/packages/core/src/scheduler.rs similarity index 61% rename from packages/core/src/arena.rs rename to packages/core/src/scheduler.rs index 678bf10cc..51e3eca85 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/scheduler.rs @@ -1,4 +1,4 @@ -use std::cell::{RefCell, RefMut}; +use std::cell::{Cell, RefCell, RefMut}; use std::fmt::Display; use std::{cell::UnsafeCell, rc::Rc}; @@ -23,25 +23,68 @@ use futures_util::Future; use futures_util::FutureExt; use futures_util::StreamExt; -type UiReceiver = UnboundedReceiver; -type UiSender = UnboundedSender; +#[derive(Clone)] +pub struct EventChannel { + pub task_counter: Rc>, + pub sender: UnboundedSender, + pub schedule_any_immediate: Rc, + pub submit_task: Rc TaskHandle>, + pub get_shared_context: Rc Option>>, +} -type TaskReceiver = UnboundedReceiver; -type TaskSender = UnboundedSender; +pub enum SchedulerMsg { + Immediate(ScopeId), + UiEvent(), + SubmitTask(u64), + ToggleTask(u64), + PauseTask(u64), + ResumeTask(u64), + DropTask(u64), +} -/// These are resources shared among all the components and the virtualdom itself +/// The scheduler holds basically everything around "working" +/// +/// Each scope has the ability to lightly interact with the scheduler (IE, schedule an update) but ultimately the scheduler calls the components. +/// +/// In Dioxus, the scheduler provides 3 priority levels - each with their own "DiffMachine". The DiffMachine state can be saved if the deadline runs +/// out. +/// +/// Saved DiffMachine state can be self-referential, so we need to be careful about how we save it. All self-referential data is a link between +/// pending DiffInstructions, Mutations, and their underlying Scope. It's okay for us to be self-referential with this data, provided we don't priority +/// task shift to a higher priority task that needs mutable access to the same scopes. +/// +/// We can prevent this safety issue from occurring if we track which scopes are invalidated when starting a new task. +/// +/// pub struct Scheduler { - pub components: UnsafeCell>, + /* + This *has* to be an UnsafeCell. - pub(crate) heuristics: HeuristicsEngine, + Each BumpFrame and Scope is located in this Slab - and we'll need mutable access to a scope while holding on to + its bumpframe conents immutably. - // Used by "set_state" and co - is its own queue - pub immediate_sender: TaskSender, - pub immediate_receiver: TaskReceiver, + However, all of the interaction with this Slab is done in this module and the Diff module, so it should be fairly + simple to audit. - /// Triggered by event listeners - pub ui_event_sender: UiSender, - pub ui_event_receiver: UiReceiver, + Wrapped in Rc so the "get_shared_context" closure can walk the tree (immutably!) + */ + pub components: Rc>>, + + /* + Yes, a slab of "nil". We use this for properly ordering ElementIDs - all we care about is the allocation strategy + that slab uses. The slab essentially just provides keys for ElementIDs that we can re-use in a Vec on the client. + + This just happened to be the simplest and most efficient way to implement a deterministic keyed map with slot reuse. + + In the future, we could actually store a pointer to the VNode instead of nil to provide O(1) lookup for VNodes... + */ + pub raw_elements: Slab<()>, + + pub heuristics: HeuristicsEngine, + + pub channel: EventChannel, + + pub receiver: UnboundedReceiver, // Garbage stored pub pending_garbage: FxHashSet, @@ -49,12 +92,6 @@ pub struct Scheduler { // In-flight futures pub async_tasks: FuturesUnordered, - /// We use a SlotSet to keep track of the keys that are currently being used. - /// However, we don't store any specific data since the "mirror" - pub raw_elements: Slab<()>, - - pub task_setter: Rc, - // scheduler stuff pub current_priority: EventPriority, @@ -66,75 +103,78 @@ pub struct Scheduler { pub garbage_scopes: HashSet, - pub high_priorty: PriortySystem, - pub medium_priority: PriortySystem, - pub low_priority: PriortySystem, + pub fibers: [PriortySystem; 3], } impl Scheduler { pub fn new() -> Self { // preallocate 2000 elements and 20 scopes to avoid dynamic allocation - let components: UnsafeCell> = UnsafeCell::new(Slab::with_capacity(100)); + let components = Rc::new(UnsafeCell::new(Slab::with_capacity(100))); // elements are super cheap - the value takes no space let raw_elements = Slab::with_capacity(2000); - let (ui_sender, ui_receiver) = futures_channel::mpsc::unbounded(); - let (immediate_sender, immediate_receiver) = futures_channel::mpsc::unbounded(); - let heuristics = HeuristicsEngine::new(); - // we allocate this task setter once to save us from having to allocate later - let task_setter = { - let queue = immediate_sender.clone(); - let components = components.clone(); - Rc::new(move |idx: ScopeId| { - let comps = unsafe { &*components.get() }; - - if let Some(scope) = comps.get(idx.0) { - // todo!("implement immediates again") - // - - // queue - // .unbounded_send(EventTrigger::new( - // V - // idx, - // None, - // EventPriority::High, - // )) - // .expect("The event queu receiver should *never* be dropped"); - } - }) as Rc + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let task_counter = Rc::new(Cell::new(0)); + let channel = EventChannel { + task_counter, + sender: sender.clone(), + schedule_any_immediate: { + Rc::new(move |id| sender.unbounded_send(SchedulerMsg::Immediate(id)).unwrap()) + }, + submit_task: Rc::new(|_| { + // + todo!() + // TaskHandle {} + }), + get_shared_context: { + let components = components.clone(); + Rc::new(|id, ty| { + let components = unsafe { &*components.get() }; + let mut search: Option<&Scope> = components.get(id.0); + while let Some(inner) = search.take() { + if let Some(shared) = inner.shared_contexts.borrow().get(&ty) { + return Some(shared.clone()); + } else { + search = inner.parent_idx.map(|id| components.get(id.0)).flatten(); + } + } + None + }) + }, }; Self { + channel, + receiver, + components, async_tasks: FuturesUnordered::new(), - ui_event_receiver: ui_receiver, - ui_event_sender: ui_sender, - - immediate_receiver: immediate_receiver, - immediate_sender: immediate_sender, - pending_garbage: FxHashSet::default(), - heuristics: heuristics, - raw_elements: raw_elements, - task_setter, + heuristics, + raw_elements, // a storage for our receiver to dump into pending_events: VecDeque::new(), + pending_immediates: VecDeque::new(), + pending_tasks: VecDeque::new(), garbage_scopes: HashSet::new(), current_priority: EventPriority::Low, - high_priorty: PriortySystem::new(), - medium_priority: PriortySystem::new(), - low_priority: PriortySystem::new(), + // a dedicated fiber for each priority + fibers: [ + PriortySystem::new(), + PriortySystem::new(), + PriortySystem::new(), + ], } } @@ -176,12 +216,12 @@ impl Scheduler { } pub fn reserve_node(&self) -> ElementId { - ElementId(self.raw_elements.borrow_mut().insert(())) + ElementId(self.raw_elements.insert(())) } /// return the id, freeing the space of the original node pub fn collect_garbage(&self, id: ElementId) { - self.raw_elements.borrow_mut().remove(id.0); + self.raw_elements.remove(id.0); } pub fn insert_scope_with_key(&self, f: impl FnOnce(ScopeId) -> Scope) -> ScopeId { @@ -192,24 +232,12 @@ impl Scheduler { id } - pub fn schedule_update(&self) -> Rc { - self.task_setter.clone() - } - - pub fn submit_task(&self, task: FiberTask) -> TaskHandle { - self.async_tasks.borrow_mut().push(task); - TaskHandle {} - } - pub fn make_trigger_key(&self, trigger: &EventTrigger) -> EventKey { - let height = self - .get_scope(trigger.originator) - .map(|f| f.height) - .unwrap(); + let height = self.get_scope(trigger.scope).map(|f| f.height).unwrap(); EventKey { height, - originator: trigger.originator, + originator: trigger.scope, priority: trigger.priority, } } @@ -356,8 +384,8 @@ impl Scheduler { | VirtualEvent::ToggleEvent(_) | VirtualEvent::MouseEvent(_) | VirtualEvent::PointerEvent(_) => { - if let Some(scope) = self.get_scope_mut(trigger.originator) { - if let Some(element) = trigger.real_node_id { + if let Some(scope) = self.get_scope_mut(trigger.scope) { + if let Some(element) = trigger.mounted_dom_id { scope.call_listener(trigger.event, element)?; let receiver = self.immediate_receiver.clone(); @@ -406,10 +434,10 @@ impl Scheduler { /// If a the fiber finishes its works (IE needs to be committed) the scheduler will drop the dirty scope pub async fn work_with_deadline<'a>( - &mut self, - mutations: &mut Mutations<'_>, + &'a mut self, + mutations: &mut Mutations<'a>, deadline: &mut Pin>>, - ) -> FiberResult { + ) -> bool { // check if we need to elevate priority self.current_priority = match ( self.high_priorty.has_work(), @@ -421,7 +449,7 @@ impl Scheduler { (false, false, _) => EventPriority::Low, }; - let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); + // let mut machine = DiffMachine::new(mutations, ScopeId(0), &self); let dirty_root = { let dirty_roots = match self.current_priority { @@ -433,7 +461,7 @@ impl Scheduler { let mut dirty_root = { let root = dirty_roots.iter().next(); if root.is_none() { - return FiberResult::Done; + return true; } root.unwrap() }; @@ -449,17 +477,12 @@ impl Scheduler { dirty_root }; - match { - let fut = machine.diff_scope(*dirty_root).fuse(); - pin_mut!(fut); + let fut = machine.diff_scope(*dirty_root).fuse(); + pin_mut!(fut); - match futures_util::future::select(deadline, fut).await { - futures_util::future::Either::Left((deadline, work_fut)) => true, - futures_util::future::Either::Right((_, deadline_fut)) => false, - } - } { - true => FiberResult::Done, - false => FiberResult::Interrupted, + match futures_util::future::select(deadline, fut).await { + futures_util::future::Either::Left((deadline, work_fut)) => true, + futures_util::future::Either::Right((_, deadline_fut)) => false, } } @@ -468,45 +491,31 @@ impl Scheduler { // does not return the trigger, but caches it in the scheduler pub async fn wait_for_any_trigger( &mut self, - mut deadline: &mut Pin>>, + deadline: &mut Pin>>, ) -> bool { - use futures_util::select; + use futures_util::future::{select, Either}; - let _immediates = self.immediate_receiver.clone(); - let mut immediates = _immediates.borrow_mut(); - - let mut _ui_events = self.ui_event_receiver.clone(); - let mut ui_events = _ui_events.borrow_mut(); - - let mut _tasks = self.async_tasks.clone(); - let mut tasks = _tasks.borrow_mut(); - - // set_state called - select! { - dirty_scope = immediates.next() => { - if let Some(scope) = dirty_scope { - self.add_dirty_scope(scope, EventPriority::Low); + let event_fut = async { + match select(self.receiver.next(), self.async_tasks.next()).await { + Either::Left((msg, _other)) => { + self.handle_channel_msg(msg.unwrap()); + } + Either::Right((task, _other)) => { + // do nothing, async task will likely generate a set of scheduler messages } } + }; - ui_event = ui_events.next() => { - if let Some(event) = ui_event { - self.pending_events.push_back(event); - } - } - - async_task = tasks.next() => { - if let Some(event) = async_task { - self.pending_events.push_back(event); - } - } - - _ = deadline => { - return true; - } + pin_mut!(event_fut); + match select(event_fut, deadline).await { + Either::Left((msg, _other)) => false, + Either::Right((deadline, _)) => true, } - false + } + + pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) { + // } pub fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) { @@ -518,12 +527,24 @@ impl Scheduler { } } -pub struct TaskHandle {} +pub struct TaskHandle { + channel: EventChannel, + our_id: u64, +} impl TaskHandle { + /// Toggles this coroutine off/on. + /// + /// This method is not synchronous - your task will not stop immediately. pub fn toggle(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn start(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn stop(&self) {} + + /// This method is not synchronous - your task will not stop immediately. pub fn restart(&self) {} } @@ -567,3 +588,72 @@ impl ElementId { self.0 as u64 } } + +// // Whenever a task is ready (complete) Dioxus produces this "AsyncEvent" +// // +// // Async events don't necessarily propagate into a scope being ran. It's up to the event itself +// // to force an update for itself. +// // +// // Most async events should have a low priority. +// // +// // This type exists for the task/concurrency system to signal that a task is ready. +// // However, this does not necessarily signal that a scope must be re-ran, so the hook implementation must cause its +// // own re-run. +// AsyncEvent { +// should_rerender: bool, +// }, + +// // Suspense events are a type of async event generated when suspended nodes are ready to be processed. +// // +// // they have the lowest priority +// SuspenseEvent { +// hook_idx: usize, +// domnode: Rc>>, +// }, + +/// Priority of Event Triggers. +/// +/// Internally, Dioxus will abort work that's taking too long if new, more important, work arrives. Unlike React, Dioxus +/// won't be afraid to pause work or flush changes to the RealDOM. This is called "cooperative scheduling". Some Renderers +/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. +/// +/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. +/// +/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, +/// we keep it simple, and just use a 3-tier priority system. +/// +/// - NoPriority = 0 +/// - LowPriority = 1 +/// - NormalPriority = 2 +/// - UserBlocking = 3 +/// - HighPriority = 4 +/// - ImmediatePriority = 5 +/// +/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. +/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be +/// flushed before proceeding. Multiple discrete events is highly unlikely, though. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] +pub enum EventPriority { + /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. + /// + /// This is typically reserved for things like user interaction. + /// + /// React calls these "discrete" events, but with an extra category of "user-blocking". + High = 2, + + /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important + /// than "High Priority" events and will take presedence over low priority events. + /// + /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. + /// + /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). + Medium = 1, + + /// "Low Priority" work will always be pre-empted unless the work is significantly delayed, in which case it will be + /// advanced to the front of the work queue until completed. + /// + /// The primary user of Low Priority work is the asynchronous work system (suspense). + /// + /// This is considered "idle" work or "background" work. + Low = 0, +} diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index 4088a6de7..fda9960f5 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -1,5 +1,6 @@ use crate::innerlude::*; use bumpalo::boxed::Box as BumpBox; +use futures_channel::mpsc::UnboundedSender; use fxhash::FxHashSet; use std::{ any::{Any, TypeId}, @@ -43,10 +44,9 @@ pub struct Scope { pub(crate) hooks: HookList, pub(crate) shared_contexts: RefCell>>, - // meta - pub(crate) function_name: &'static str, - // // A reference to the resources shared by all the comonents - // pub(crate) vdom: SharedResources, + pub(crate) memoized_updater: Rc, + + pub(crate) shared: EventChannel, } // The type of closure that wraps calling components @@ -74,28 +74,22 @@ impl Scope { child_nodes: ScopeChildren, - // vdom: SharedResources, - function_name: &'static str, + shared: EventChannel, ) -> Self { let child_nodes = unsafe { child_nodes.extend_lifetime() }; - // // insert ourself as a descendent of the parent - // // when the parent is removed, this map will be traversed, and we will also be cleaned up. - // if let Some(parent) = &parent { - // let parent = unsafe { vdom.get_scope(*parent) }.unwrap(); - // parent.descendents.borrow_mut().insert(arena_idx); - // } + let up = shared.schedule_any_immediate.clone(); + let memoized_updater = Rc::new(move || up(arena_idx)); Self { - function_name, + memoized_updater, + shared, child_nodes, caller, parent_idx: parent, our_arena_idx: arena_idx, height, - // vdom, frames: ActiveFrame::new(), - hooks: Default::default(), shared_contexts: Default::default(), listeners: Default::default(), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8f015a784..558bb4129 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -105,18 +105,17 @@ impl VirtualDom { let root_props: Pin> = Box::pin(root_props); let props_ptr = root_props.as_ref().downcast_ref::

().unwrap() as *const P; - let link = components.clone(); + let update_sender = components.immediate_sender.clone(); let base_scope = components.insert_scope_with_key(move |myidx| { let caller = NodeFactory::create_component_caller(root, props_ptr as *const _); let name = type_name_of(root); - Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), link, name) + Scope::new(caller, myidx, None, 0, ScopeChildren(&[]), update_sender) }); Self { base_scope, _root_props: root_props, - scheduler: Scheduler::new(components.clone()), scheduler: components, _root_prop_type: TypeId::of::

(), } @@ -152,28 +151,29 @@ impl VirtualDom { /// the diff and creating nodes can be expensive, so we provide this method to avoid blocking the main thread. This /// method can be useful when needing to perform some crucial periodic tasks. pub async fn rebuild_async<'s>(&'s mut self) -> Mutations<'s> { - let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); + todo!() + // let mut diff_machine = DiffMachine::new(Mutations::new(), self.base_scope, &self.scheduler); - let cur_component = self - .scheduler - .get_scope_mut(self.base_scope) - .expect("The base scope should never be moved"); + // let cur_component = self + // .scheduler + // .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. - if cur_component.run_scope().is_ok() { - diff_machine - .stack - .create_node(cur_component.frames.fin_head(), MountType::Append); - diff_machine.work().await; - } else { - // todo: should this be a hard error? - log::warn!( - "Component failed to run succesfully during rebuild. - This does not result in a failed rebuild, but indicates a logic failure within your app." - ); - } + // // // We run the component. If it succeeds, then we can diff it and add the changes to the dom. + // if cur_component.run_scope().is_ok() { + // diff_machine + // .stack + // .create_node(cur_component.frames.fin_head(), MountType::Append); + // diff_machine.work().await; + // } else { + // // todo: should this be a hard error? + // log::warn!( + // "Component failed to run succesfully during rebuild. + // This does not result in a failed rebuild, but indicates a logic failure within your app." + // ); + // } - diff_machine.mutations + // diff_machine.mutations } pub fn diff_sync<'s>(&'s mut self) -> Mutations<'s> { diff --git a/packages/core/tests/create_iterative.rs b/packages/core/tests/create_iterative.rs index 0d1b6343c..445e990ed 100644 --- a/packages/core/tests/create_iterative.rs +++ b/packages/core/tests/create_iterative.rs @@ -1,7 +1,7 @@ //! tests to prove that the iterative implementation works use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit, Mutations}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit, Mutations}; mod test_logging; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 8f59165bc..e8a018e42 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -3,7 +3,7 @@ use bumpalo::Bump; use dioxus::{ - arena::Scheduler, diff::DiffMachine, prelude::*, DiffInstruction, DomEdit, MountType, + diff::DiffMachine, prelude::*, scheduler::Scheduler, DiffInstruction, DomEdit, MountType, }; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/eventsystem.rs b/packages/core/tests/eventsystem.rs index 869aa8fa3..2b90ca256 100644 --- a/packages/core/tests/eventsystem.rs +++ b/packages/core/tests/eventsystem.rs @@ -1,7 +1,7 @@ use bumpalo::Bump; use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/hooks.rs b/packages/core/tests/hooks.rs index 3946b209d..3fcc0bccd 100644 --- a/packages/core/tests/hooks.rs +++ b/packages/core/tests/hooks.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, DomEdit}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, DomEdit}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; diff --git a/packages/core/tests/work_sync.rs b/packages/core/tests/work_sync.rs index 93f3589b1..075060c20 100644 --- a/packages/core/tests/work_sync.rs +++ b/packages/core/tests/work_sync.rs @@ -3,7 +3,7 @@ //! This means you can actually call it synchronously if you want. use anyhow::{Context, Result}; -use dioxus::{arena::Scheduler, diff::DiffMachine, prelude::*, scope::Scope}; +use dioxus::{diff::DiffMachine, prelude::*, scheduler::Scheduler, scope::Scope}; use dioxus_core as dioxus; use dioxus_html as dioxus_elements; use futures_util::FutureExt;