wip: more work on jank free

This commit is contained in:
Jonathan Kelley 2021-08-09 13:17:19 -04:00
parent f66630c935
commit a44e9fcffa
2 changed files with 203 additions and 152 deletions

View file

@ -75,7 +75,7 @@ use DomEdit::*;
pub struct DiffMachine<'r, 'bump> {
pub vdom: &'bump SharedResources,
pub edits: Mutations<'bump>,
pub mutations: Mutations<'bump>,
pub scope_stack: SmallVec<[ScopeId; 5]>,
@ -95,7 +95,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
shared: &'bump SharedResources,
) -> Self {
Self {
edits,
mutations: edits,
scope_stack: smallvec![cur_scope],
vdom: shared,
diffed: FxHashSet::default(),
@ -110,7 +110,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
/// This will PANIC if any component elements are passed in.
pub fn new_headless(shared: &'bump SharedResources) -> Self {
Self {
edits: Mutations { edits: Vec::new() },
mutations: Mutations { edits: Vec::new() },
scope_stack: smallvec![ScopeId(0)],
vdom: shared,
diffed: FxHashSet::default(),
@ -182,13 +182,13 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
if old.attributes.len() == new.attributes.len() {
for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) {
if old_attr.value != new_attr.value {
please_commit(&mut self.edits.edits);
please_commit(&mut self.mutations.edits);
self.edit_set_attribute(new_attr);
}
}
} else {
// TODO: provide some sort of report on how "good" the diffing was
please_commit(&mut self.edits.edits);
please_commit(&mut self.mutations.edits);
for attribute in old.attributes {
self.edit_remove_attribute(attribute);
}
@ -209,7 +209,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
if old.listeners.len() == new.listeners.len() {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
if old_l.event != new_l.event {
please_commit(&mut self.edits.edits);
please_commit(&mut self.mutations.edits);
self.edit_remove_event_listener(old_l.event);
self.edit_new_event_listener(new_l, cur_scope);
}
@ -217,7 +217,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
self.fix_listener(new_l);
}
} else {
please_commit(&mut self.edits.edits);
please_commit(&mut self.mutations.edits);
for listener in old.listeners {
self.edit_remove_event_listener(listener.event);
}
@ -1343,42 +1343,42 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
// Navigation
pub(crate) fn edit_push_root(&mut self, root: ElementId) {
let id = root.as_u64();
self.edits.edits.push(PushRoot { id });
self.mutations.edits.push(PushRoot { id });
}
pub(crate) fn edit_pop(&mut self) {
self.edits.edits.push(PopRoot {});
self.mutations.edits.push(PopRoot {});
}
// Add Nodes to the dom
// add m nodes from the stack
pub(crate) fn edit_append_children(&mut self, many: u32) {
self.edits.edits.push(AppendChildren { many });
self.mutations.edits.push(AppendChildren { many });
}
// replace the n-m node on the stack with the m nodes
// ends with the last element of the chain on the top of the stack
pub(crate) fn edit_replace_with(&mut self, n: u32, m: u32) {
self.edits.edits.push(ReplaceWith { n, m });
self.mutations.edits.push(ReplaceWith { n, m });
}
pub(crate) fn edit_insert_after(&mut self, n: u32) {
self.edits.edits.push(InsertAfter { n });
self.mutations.edits.push(InsertAfter { n });
}
pub(crate) fn edit_insert_before(&mut self, n: u32) {
self.edits.edits.push(InsertBefore { n });
self.mutations.edits.push(InsertBefore { n });
}
// Remove Nodesfrom the dom
pub(crate) fn edit_remove(&mut self) {
self.edits.edits.push(Remove);
self.mutations.edits.push(Remove);
}
// Create
pub(crate) fn edit_create_text_node(&mut self, text: &'bump str, id: ElementId) {
let id = id.as_u64();
self.edits.edits.push(CreateTextNode { text, id });
self.mutations.edits.push(CreateTextNode { text, id });
}
pub(crate) fn edit_create_element(
@ -1389,15 +1389,15 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
) {
let id = id.as_u64();
match ns {
Some(ns) => self.edits.edits.push(CreateElementNs { id, ns, tag }),
None => self.edits.edits.push(CreateElement { id, tag }),
Some(ns) => self.mutations.edits.push(CreateElementNs { id, ns, tag }),
None => self.mutations.edits.push(CreateElement { id, tag }),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn edit_create_placeholder(&mut self, id: ElementId) {
let id = id.as_u64();
self.edits.edits.push(CreatePlaceholder { id });
self.mutations.edits.push(CreatePlaceholder { id });
}
// events
@ -1410,7 +1410,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
let element_id = mounted_node.get().unwrap().as_u64();
self.edits.edits.push(NewEventListener {
self.mutations.edits.push(NewEventListener {
scope,
event_name: event,
mounted_node_id: element_id,
@ -1418,12 +1418,12 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
}
pub(crate) fn edit_remove_event_listener(&mut self, event: &'static str) {
self.edits.edits.push(RemoveEventListener { event });
self.mutations.edits.push(RemoveEventListener { event });
}
// modify
pub(crate) fn edit_set_text(&mut self, text: &'bump str) {
self.edits.edits.push(SetText { text });
self.mutations.edits.push(SetText { text });
}
pub(crate) fn edit_set_attribute(&mut self, attribute: &'bump Attribute) {
@ -1437,7 +1437,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
// field: &'static str,
// value: &'bump str,
// ns: Option<&'static str>,
self.edits.edits.push(SetAttribute {
self.mutations.edits.push(SetAttribute {
field: name,
value,
ns: *namespace,
@ -1460,7 +1460,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
// field: &'static str,
// value: &'bump str,
// ns: Option<&'static str>,
self.edits.edits.push(SetAttribute {
self.mutations.edits.push(SetAttribute {
field: name,
value,
ns: Some(namespace),
@ -1469,7 +1469,7 @@ impl<'r, 'bump> DiffMachine<'r, 'bump> {
pub(crate) fn edit_remove_attribute(&mut self, attribute: &Attribute) {
let name = attribute.name;
self.edits.edits.push(RemoveAttribute { name });
self.mutations.edits.push(RemoveAttribute { name });
}
}

View file

@ -19,7 +19,7 @@
//! This module includes just the barebones for a complete VirtualDOM API.
//! Additional functionality is defined in the respective files.
#![allow(unreachable_code)]
use futures_util::StreamExt;
use futures_util::{Future, StreamExt};
use fxhash::FxHashMap;
use crate::hooks::{SuspendedContext, SuspenseHook};
@ -271,7 +271,7 @@ impl VirtualDom {
/// that has suspended nodes or suspended tasks. Be warned - any async tasks running forever will prevent this method
/// from completing. Consider using `run` and specifing a deadline.
pub async fn run_unbounded<'s>(&'s mut self) -> Result<Mutations<'s>> {
self.run_with_deadline(|| false).await
self.run_with_deadline(async {}).await
}
/// Run the virtualdom with a time limit.
@ -283,181 +283,232 @@ impl VirtualDom {
/// This method is useful when needing to schedule the virtualdom around other tasks on the main thread to prevent
/// "jank". It will try to finish whatever work it has by the deadline to free up time for other work.
///
/// Due to platform differences in how time is handled, this method accepts a closure that must return true when the
/// deadline is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room
/// into the deadline closure manually.
/// Due to platform differences in how time is handled, this method accepts a future that resolves when the deadline
/// is exceeded. However, the deadline won't be met precisely, so you might want to build some wiggle room into the
/// deadline closure manually.
///
/// The deadline is checked before starting to diff components. This strikes a balance between the overhead of checking
/// the deadline and just completing the work. However, if an individual component takes more than 16ms to render, then
/// the screen will "jank" up. In debug, this will trigger an alert.
///
/// If there are no in-flight fibers when this method is called, it will await any possible tasks, aborting early if
/// the provided deadline future resolves.
///
/// For use in the web, it is expected that this method will be called to be executed during "idle times" and the
/// mutations to be applied during the "paint times" IE "animation frames". With this strategy, it is possible to craft
/// entirely jank-free applications that perform a ton of work.
///
/// # Example
///
/// ```no_run
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!( div {"hello"} )));
/// static App: FC<()> = |cx| rsx!(in cx, div {"hello"} );
/// let mut dom = VirtualDom::new(App);
/// loop {
/// let started = std::time::Instant::now();
/// let deadline = move || std::time::Instant::now() - started > std::time::Duration::from_millis(16);
///
/// let deadline = TimeoutFuture::from_ms(16);
/// let mutations = dom.run_with_deadline(deadline).await;
/// apply_mutations(mutations);
/// }
/// ```
pub async fn run_with_deadline<'s>(
&'s mut self,
mut deadline_exceeded: impl FnMut() -> bool,
mut deadline: impl Future<Output = ()>,
) -> Result<Mutations<'s>> {
let cur_component = self.base_scope;
// Configure our deadline
use futures_util::FutureExt;
let mut deadline_future = deadline.boxed_local();
let mut diff_machine =
DiffMachine::new(Mutations { edits: Vec::new() }, cur_component, &self.shared);
let is_ready = || -> bool { (&mut deadline_future).now_or_never().is_some() };
let mut diff_machine = DiffMachine::new(
Mutations { edits: Vec::new() },
self.base_scope,
&self.shared,
);
/*
Strategy:
1. Check if there are any events in the receiver.
2. If there are, process them and create a new fiber.
3. If there are no events, then choose a fiber to work on.
4. If there are no fibers, then wait for the next event from the receiver.
4. If there are no fibers, then wait for the next event from the receiver. Abort if the deadline is reached.
5. While processing a fiber, periodically check if we're out of time
6. If we are almost out of time, then commit our edits to the realdom
6. If our deadling is reached, then commit our edits to the realdom
7. Whenever a fiber is finished, immediately commit it. (IE so deadlines can be infinite if unsupported)
We slice fibers based on time. Each batch of events between frames is its own fiber. This is the simplest way
to conceptualize what *is* or *isn't* a fiber. IE if a bunch of events occur during a time slice, they all
get batched together as a single operation of "dirty" scopes.
This approach is designed around the "diff during rIC and commit during rAF"
We need to make sure to not call multiple events while the diff machine is borrowing the same scope. Because props
and listeners hold references to hook data, it is wrong to run a scope that is already being diffed.
*/
// 1. Consume any pending events and create new fibers
let mut receiver = self.shared.task_receiver.borrow_mut();
while let Ok(Some(trigger)) = receiver.try_next() {
// todo: cache the fibers
let mut fiber = Fiber::new();
match &trigger.event {
// If any input event is received, then we need to create a new fiber
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(_) => {
if let Some(scope) = self.shared.get_scope_mut(trigger.originator) {
scope.call_listener(trigger)?;
// On the primary event queue, there is no batching.
let mut trigger = {
match receiver.try_next() {
Ok(Some(trigger)) => trigger,
_ => {
// Continuously poll the future pool and the event receiver for work
let mut tasks = self.shared.async_tasks.borrow_mut();
let tasks_tasks = tasks.next();
let mut receiver = self.shared.task_receiver.borrow_mut();
let reciv_task = receiver.next();
futures_util::pin_mut!(tasks_tasks);
futures_util::pin_mut!(reciv_task);
// Poll the event receiver and the future pool for work
// Abort early if our deadline has ran out
use futures_util::select;
let mut deadline = (&mut deadline_future).fuse();
let trig = select! {
trigger = tasks_tasks => trigger,
trigger = reciv_task => trigger,
_ = deadline => { return Ok(diff_machine.mutations); }
};
trig.unwrap()
}
}
};
// since the last time we were ran with a deadline, we've accumulated many updates
// IE a button was clicked twice, or a scroll trigger was fired twice.
// We consider the button a event to be a function of the current state, which means we can batch many updates
// together.
match &trigger.event {
// If any input event is received, then we need to create a new fiber
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(_) => {
if let Some(scope) = self.shared.get_scope_mut(trigger.originator) {
scope.call_listener(trigger)?;
}
}
VirtualEvent::AsyncEvent { .. } => while let Ok(Some(event)) = receiver.try_next() {},
// These shouldn't normally be received, but if they are, it's done because some task set state manually
// Instead of processing it serially,
// We will batch all the scheduled updates together in one go.
VirtualEvent::ScheduledUpdate { height: u32 } => {}
// Suspense Events! A component's suspended node is updated
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
// Safety: this handler is the only thing that can mutate shared items at this moment in tim
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// safety: we are sure that there are no other references to the inner content of suspense hooks
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
let cx = Context { scope, props: &() };
let scx = SuspendedContext { inner: cx };
// generate the new node!
let nodes: Option<VNode> = (&hook.callback)(scx);
match nodes {
None => {
log::warn!(
"Suspense event came through, but there were no generated nodes >:(."
);
}
Some(nodes) => {
// allocate inside the finished frame - not the WIP frame
let nodes = scope.frames.finished_frame().bump.alloc(nodes);
// push the old node's root onto the stack
let real_id = domnode.get().ok_or(Error::NotMounted)?;
diff_machine.edit_push_root(real_id);
// push these new nodes onto the diff machines stack
let meta = diff_machine.create_vnode(&*nodes);
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.edit_replace_with(1, meta.added_to_stack);
}
}
}
VirtualEvent::AsyncEvent { .. } => {
while let Ok(Some(event)) = receiver.try_next() {
fiber.pending_scopes.push(event.originator);
}
}
// Collecting garabge is not currently interruptible.
//
// In the future, it could be though
VirtualEvent::GarbageCollection => {
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// These shouldn't normally be received, but if they are, it's done because some task set state manually
// Instead of batching the results,
VirtualEvent::ScheduledUpdate { height: u32 } => {}
let mut garbage_list = scope.consume_garbage();
// Suspense Events! A component's suspended node is updated
VirtualEvent::SuspenseEvent { hook_idx, domnode } => {
// Safety: this handler is the only thing that can mutate shared items at this moment in tim
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
// safety: we are sure that there are no other references to the inner content of suspense hooks
let hook = unsafe { scope.hooks.get_mut::<SuspenseHook>(*hook_idx) }.unwrap();
let cx = Context { scope, props: &() };
let scx = SuspendedContext { inner: cx };
// generate the new node!
let nodes: Option<VNode> = (&hook.callback)(scx);
match nodes {
None => {
log::warn!(
"Suspense event came through, but there were no generated nodes >:(."
);
let mut scopes_to_kill = Vec::new();
while let Some(node) = garbage_list.pop() {
match &node.kind {
VNodeKind::Text(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Anchor(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Suspended(_) => {
self.shared.collect_garbage(node.direct_id());
}
Some(nodes) => {
// allocate inside the finished frame - not the WIP frame
let nodes = scope.frames.finished_frame().bump.alloc(nodes);
// push the old node's root onto the stack
let real_id = domnode.get().ok_or(Error::NotMounted)?;
diff_machine.edit_push_root(real_id);
VNodeKind::Element(el) => {
self.shared.collect_garbage(node.direct_id());
for child in el.children {
garbage_list.push(child);
}
}
// push these new nodes onto the diff machines stack
let meta = diff_machine.create_vnode(&*nodes);
VNodeKind::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.edit_replace_with(1, meta.added_to_stack);
VNodeKind::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.ass_scope.get().unwrap();
let scope = self.get_scope(scope_id).unwrap();
let root = scope.root();
garbage_list.push(root);
scopes_to_kill.push(scope_id);
}
}
}
// Collecting garabge is not currently interruptible.
//
// In the future, it could be though
VirtualEvent::GarbageCollection => {
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
let mut garbage_list = scope.consume_garbage();
let mut scopes_to_kill = Vec::new();
while let Some(node) = garbage_list.pop() {
match &node.kind {
VNodeKind::Text(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Anchor(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Suspended(_) => {
self.shared.collect_garbage(node.direct_id());
}
VNodeKind::Element(el) => {
self.shared.collect_garbage(node.direct_id());
for child in el.children {
garbage_list.push(child);
}
}
VNodeKind::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNodeKind::Component(comp) => {
// TODO: run the hook destructors and then even delete the scope
let scope_id = comp.ass_scope.get().unwrap();
let scope = self.get_scope(scope_id).unwrap();
let root = scope.root();
garbage_list.push(root);
scopes_to_kill.push(scope_id);
}
}
}
for scope in scopes_to_kill {
// oy kill em
log::debug!("should be removing scope {:#?}", scope);
}
for scope in scopes_to_kill {
// oy kill em
log::debug!("should be removing scope {:#?}", scope);
}
}
}
while !deadline_exceeded() {
let mut receiver = self.shared.task_receiver.borrow_mut();
// while !deadline() {
// let mut receiver = self.shared.task_receiver.borrow_mut();
// no messages to receive, just work on the fiber
}
// // no messages to receive, just work on the fiber
// }
Ok(diff_machine.edits)
Ok(diff_machine.mutations)
}
pub fn get_event_sender(&self) -> futures_channel::mpsc::UnboundedSender<EventTrigger> {