Component lifecycle scheduler optimizations (#2065)

* yew: partial render deduplication implementation

* yew/scheduler: batching and comment fixes

* yew/scheduler: clippy fixes

* yew/vcomp: add lifecycle debug logging

* ci: add release clippy and cargo check passes

* ci: fix release check

* ci: remove duplicate check calls
This commit is contained in:
Janis Petersons 2021-09-22 18:57:14 +03:00 committed by GitHub
parent 4ca68e07ca
commit a442ce5df5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 572 additions and 267 deletions

View File

@ -41,6 +41,35 @@ jobs:
command: clippy
args: --all-targets -- -D warnings
lint-release:
name: Clippy on release profile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
profile: minimal
components: clippy
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/Cargo.toml') }}
restore-keys: |
cargo-${{ runner.os }}-${{ github.job }}-
- name: Run clippy
if: always()
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --release -- -D warnings
check_examples:
name: Check Examples
runs-on: ubuntu-latest
@ -105,7 +134,7 @@ jobs:
run: |
cd packages/yew
cargo test --doc --features "doc_test wasm_test"
- name: Run website code snippet tests
run: |
cd packages/website-test

View File

@ -3,6 +3,7 @@
# public tasks:
# * pr-flow
# * lint
# * lint-release
# * tests
# * benchmarks
#
@ -25,7 +26,7 @@ namespace = "core"
toolchain = "stable"
category = "Checks"
description = "Lint and test"
run_task = { name = ["lint", "tests"], fork = true }
run_task = { name = ["lint", "lint-release", "tests"], fork = true }
[tasks.lint]
category = "Checks"
@ -50,6 +51,13 @@ private = true
workspace = true
dependencies = ["core::check-format-flow", "core::clippy-flow"]
# Needed, because we have some code differences between debug and release builds
[tasks.lint-release]
category = "Checks"
workspace = true
command = "cargo"
args = ["clippy", "--all-targets", "--release", "--", "--deny=warnings"]
[tasks.tests-setup]
private = true
script_runner = "@duckscript"

View File

@ -16,8 +16,10 @@ pub(crate) struct ComponentState<COMP: Component> {
next_sibling: NodeRef,
node_ref: NodeRef,
has_rendered: bool,
pending_root: Option<VNode>,
pending_updates: Vec<UpdateEvent<COMP>>,
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> ComponentState<COMP> {
@ -29,6 +31,12 @@ impl<COMP: Component> ComponentState<COMP> {
scope: Scope<COMP>,
props: Rc<COMP::Properties>,
) -> Self {
#[cfg(debug_assertions)]
let vcomp_id = {
use super::Scoped;
scope.to_any().vcomp_id
};
let context = Context { scope, props };
let component = Box::new(COMP::create(&context));
@ -40,39 +48,14 @@ impl<COMP: Component> ComponentState<COMP> {
next_sibling,
node_ref,
has_rendered: false,
pending_root: None,
pending_updates: Vec::new(),
}
}
fn drain_pending_updates(&mut self, state: &Shared<Option<ComponentState<COMP>>>) {
if !self.pending_updates.is_empty() {
scheduler::push_component_updates(self.pending_updates.drain(..).map(|update| {
Box::new(ComponentRunnable {
state: state.clone(),
event: update.into(),
}) as Box<dyn Runnable>
}));
#[cfg(debug_assertions)]
vcomp_id,
}
}
}
/// Internal Component lifecycle event
pub(crate) enum ComponentLifecycleEvent<COMP: Component> {
Create(CreateEvent<COMP>),
Update(UpdateEvent<COMP>),
Render,
Rendered,
Destroy,
}
impl<COMP: Component> From<CreateEvent<COMP>> for ComponentLifecycleEvent<COMP> {
fn from(create: CreateEvent<COMP>) -> Self {
Self::Create(create)
}
}
pub(crate) struct CreateEvent<COMP: Component> {
pub(crate) struct CreateRunner<COMP: Component> {
pub(crate) parent: Element,
pub(crate) next_sibling: NodeRef,
pub(crate) placeholder: VNode,
@ -81,15 +64,26 @@ pub(crate) struct CreateEvent<COMP: Component> {
pub(crate) scope: Scope<COMP>,
}
impl<COMP: Component> From<UpdateEvent<COMP>> for ComponentLifecycleEvent<COMP> {
fn from(update: UpdateEvent<COMP>) -> Self {
Self::Update(update)
impl<COMP: Component> Runnable for CreateRunner<COMP> {
fn run(self: Box<Self>) {
let mut current_state = self.scope.state.borrow_mut();
if current_state.is_none() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create");
*current_state = Some(ComponentState::new(
self.parent,
self.next_sibling,
self.placeholder,
self.node_ref,
self.scope.clone(),
self.props,
));
}
}
}
pub(crate) enum UpdateEvent<COMP: Component> {
/// First update
First,
/// Wraps messages for a component.
Message(COMP::Message),
/// Wraps batch of messages for a component.
@ -98,97 +92,110 @@ pub(crate) enum UpdateEvent<COMP: Component> {
Properties(Rc<COMP::Properties>, NodeRef, NodeRef),
}
pub(crate) struct ComponentRunnable<COMP: Component> {
pub(crate) struct UpdateRunner<COMP: Component> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
pub(crate) event: ComponentLifecycleEvent<COMP>,
pub(crate) event: UpdateEvent<COMP>,
}
impl<COMP: Component> Runnable for ComponentRunnable<COMP> {
impl<COMP: Component> Runnable for UpdateRunner<COMP> {
fn run(self: Box<Self>) {
let mut current_state = self.state.borrow_mut();
match self.event {
ComponentLifecycleEvent::Create(event) => {
if current_state.is_none() {
*current_state = Some(ComponentState::new(
event.parent,
event.next_sibling,
event.placeholder,
event.node_ref,
event.scope.clone(),
event.props,
));
if let Some(mut state) = self.state.borrow_mut().as_mut() {
let schedule_render = match self.event {
UpdateEvent::Message(message) => state.component.update(&state.context, message),
UpdateEvent::MessageBatch(messages) => {
messages.into_iter().fold(false, |acc, msg| {
state.component.update(&state.context, msg) || acc
})
}
}
ComponentLifecycleEvent::Update(event) => {
if let Some(mut state) = current_state.as_mut() {
if state.pending_root.is_some() {
state.pending_updates.push(event);
return;
}
let should_render = match event {
UpdateEvent::First => true,
UpdateEvent::Message(message) => {
state.component.update(&state.context, message)
}
UpdateEvent::MessageBatch(messages) => {
messages.into_iter().fold(false, |acc, msg| {
state.component.update(&state.context, msg) || acc
})
}
UpdateEvent::Properties(props, node_ref, next_sibling) => {
// When components are updated, a new node ref could have been passed in
state.node_ref = node_ref;
// When components are updated, their siblings were likely also updated
state.next_sibling = next_sibling;
// Only trigger changed if props were changed
if state.context.props != props {
state.context.props = Rc::clone(&props);
state.component.changed(&state.context)
} else {
false
}
}
};
if should_render {
state.pending_root = Some(state.component.view(&state.context));
state.context.scope.process(ComponentLifecycleEvent::Render);
};
}
}
ComponentLifecycleEvent::Render => {
if let Some(state) = current_state.as_mut() {
if let Some(mut new_root) = state.pending_root.take() {
std::mem::swap(&mut new_root, &mut state.root_node);
let ancestor = Some(new_root);
let new_root = &mut state.root_node;
let scope = state.context.scope.clone().into();
let next_sibling = state.next_sibling.clone();
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
state.node_ref.link(node);
state
.context
.scope
.process(ComponentLifecycleEvent::Rendered);
UpdateEvent::Properties(props, node_ref, next_sibling) => {
// When components are updated, a new node ref could have been passed in
state.node_ref = node_ref;
// When components are updated, their siblings were likely also updated
state.next_sibling = next_sibling;
// Only trigger changed if props were changed
if state.context.props != props {
state.context.props = Rc::clone(&props);
state.component.changed(&state.context)
} else {
false
}
}
};
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(
state.vcomp_id,
format!("update(schedule_render={})", schedule_render),
);
if schedule_render {
scheduler::push_component_render(
self.state.as_ptr() as usize,
RenderRunner {
state: self.state.clone(),
},
RenderedRunner {
state: self.state.clone(),
},
);
// Only run from the scheduler, so no need to call `scheduler::start()`
}
ComponentLifecycleEvent::Rendered => {
if let Some(mut state) = current_state.as_mut() {
let first_render = !state.has_rendered;
state.component.rendered(&state.context, first_render);
state.has_rendered = true;
state.drain_pending_updates(&self.state);
}
}
ComponentLifecycleEvent::Destroy => {
if let Some(mut state) = current_state.take() {
state.component.destroy(&state.context);
state.root_node.detach(&state.parent);
state.node_ref.set(None);
}
}
}
}
}
pub(crate) struct DestroyRunner<COMP: Component> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for DestroyRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(mut state) = self.state.borrow_mut().take() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy");
state.component.destroy(&state.context);
state.root_node.detach(&state.parent);
state.node_ref.set(None);
}
}
}
pub(crate) struct RenderRunner<COMP: Component> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for RenderRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
let mut new_root = state.component.view(&state.context);
std::mem::swap(&mut new_root, &mut state.root_node);
let ancestor = Some(new_root);
let new_root = &mut state.root_node;
let scope = state.context.scope.clone().into();
let next_sibling = state.next_sibling.clone();
let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor);
state.node_ref.link(node);
}
}
}
pub(crate) struct RenderedRunner<COMP: Component> {
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
}
impl<COMP: Component> Runnable for RenderedRunner<COMP> {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered");
let first_render = !state.has_rendered;
state.component.rendered(&state.context, first_render);
state.has_rendered = true;
}
}
}
@ -314,7 +321,7 @@ mod tests {
}
}
fn test_lifecycle(props: Props, expected: &[String]) {
fn test_lifecycle(props: Props, expected: &[&str]) {
let document = crate::utils::document();
let scope = Scope::<Comp>::new(None);
let el = document.create_element("div").unwrap();
@ -335,12 +342,7 @@ mod tests {
lifecycle: lifecycle.clone(),
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
],
&["create", "view", "child rendered", "rendered(true)"],
);
test_lifecycle(
@ -351,11 +353,11 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(false)",
],
);
@ -366,13 +368,13 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(true)".to_string(),
"view".to_string(),
"rendered(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(true)",
"view",
"rendered(false)",
],
);
@ -383,11 +385,11 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(false)",
],
);
@ -398,11 +400,11 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(false)",
],
);
@ -413,16 +415,17 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(true)".to_string(),
"view".to_string(),
"rendered(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(true)",
"view",
"rendered(false)",
],
);
// This also tests render deduplication after the first render
test_lifecycle(
Props {
lifecycle,
@ -432,16 +435,14 @@ mod tests {
..Props::default()
},
&[
"create".to_string(),
"view".to_string(),
"child rendered".to_string(),
"rendered(true)".to_string(),
"update(true)".to_string(),
"view".to_string(),
"rendered(false)".to_string(),
"update(true)".to_string(),
"view".to_string(),
"rendered(false)".to_string(),
"create",
"view",
"child rendered",
"rendered(true)",
"update(true)",
"update(true)",
"view",
"rendered(false)",
],
);
}

View File

@ -76,10 +76,18 @@ pub trait Component: Sized + 'static {
/// Components define their visual layout using a JSX-style syntax through the use of the
/// `html!` procedural macro. The full guide to using the macro can be found in [Yew's
/// documentation](https://yew.rs/concepts/html).
///
/// Note that `view()` calls do not always follow a render request from `update()` or
/// `changed()`. Yew may optimize some calls out to reduce virtual DOM tree generation overhead.
/// The `create()` call is always followed by a call to `view()`.
fn view(&self, ctx: &Context<Self>) -> Html;
/// The `rendered` method is called after each time a Component is rendered but
/// before the browser updates the page.
///
/// Note that `rendered()` calls do not always follow a render request from `update()` or
/// `changed()`. Yew may optimize some calls out to reduce virtual DOM tree generation overhead.
/// The `create()` call is always followed by a call to `view()` and later `rendered()`.
#[allow(unused_variables)]
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {}

View File

@ -2,7 +2,8 @@
use super::{
lifecycle::{
ComponentLifecycleEvent, ComponentRunnable, ComponentState, CreateEvent, UpdateEvent,
ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent,
UpdateRunner,
},
Component,
};
@ -27,6 +28,10 @@ pub struct AnyScope {
type_id: TypeId,
parent: Option<Rc<AnyScope>>,
state: Rc<dyn Any>,
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> From<Scope<COMP>> for AnyScope {
@ -35,6 +40,9 @@ impl<COMP: Component> From<Scope<COMP>> for AnyScope {
type_id: TypeId::of::<COMP>(),
parent: scope.parent,
state: scope.state,
#[cfg(debug_assertions)]
vcomp_id: scope.vcomp_id,
}
}
}
@ -46,6 +54,9 @@ impl AnyScope {
type_id: TypeId::of::<()>(),
parent: None,
state: Rc::new(()),
#[cfg(debug_assertions)]
vcomp_id: 0,
}
}
@ -61,12 +72,24 @@ impl AnyScope {
/// Attempts to downcast into a typed scope
pub fn downcast<COMP: Component>(self) -> Scope<COMP> {
let state = self
.state
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
.expect("unexpected component type");
#[cfg(debug_assertions)]
let vcomp_id = state
.borrow()
.as_ref()
.map(|s| s.vcomp_id)
.unwrap_or_default();
Scope {
parent: self.parent,
state: self
.state
.downcast::<RefCell<Option<ComponentState<COMP>>>>()
.expect("unexpected component type"),
state,
#[cfg(debug_assertions)]
vcomp_id,
}
}
@ -116,14 +139,22 @@ impl<COMP: Component> Scoped for Scope<COMP> {
/// Process an event to destroy a component
fn destroy(&mut self) {
self.process(ComponentLifecycleEvent::Destroy);
scheduler::push_component_destroy(DestroyRunner {
state: self.state.clone(),
});
// Not guaranteed to already have the scheduler started
scheduler::start();
}
}
/// A context which allows sending messages to a component.
pub struct Scope<COMP: Component> {
parent: Option<Rc<AnyScope>>,
state: Shared<Option<ComponentState<COMP>>>,
pub(crate) state: Shared<Option<ComponentState<COMP>>>,
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: u64,
}
impl<COMP: Component> fmt::Debug for Scope<COMP> {
@ -137,6 +168,9 @@ impl<COMP: Component> Clone for Scope<COMP> {
Scope {
parent: self.parent.clone(),
state: self.state.clone(),
#[cfg(debug_assertions)]
vcomp_id: self.vcomp_id,
}
}
}
@ -160,7 +194,17 @@ impl<COMP: Component> Scope<COMP> {
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
let parent = parent.map(Rc::new);
let state = Rc::new(RefCell::new(None));
Scope { parent, state }
#[cfg(debug_assertions)]
let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default();
Scope {
state,
parent,
#[cfg(debug_assertions)]
vcomp_id,
}
}
/// Mounts a component with `props` to the specified `element` in the DOM.
@ -171,6 +215,8 @@ impl<COMP: Component> Scope<COMP> {
node_ref: NodeRef,
props: Rc<COMP::Properties>,
) {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "create placeholder");
let placeholder = {
let placeholder: Node = document().create_text_node("").into();
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
@ -178,15 +224,24 @@ impl<COMP: Component> Scope<COMP> {
VNode::VRef(placeholder)
};
self.schedule(UpdateEvent::First.into());
self.process(ComponentLifecycleEvent::Create(CreateEvent {
parent,
next_sibling,
placeholder,
node_ref,
props,
scope: self.clone(),
}));
scheduler::push_component_create(
CreateRunner {
parent,
next_sibling,
placeholder,
node_ref,
props,
scope: self.clone(),
},
RenderRunner {
state: self.state.clone(),
},
RenderedRunner {
state: self.state.clone(),
},
);
// Not guaranteed to already have the scheduler started
scheduler::start();
}
pub(crate) fn reuse(
@ -195,28 +250,19 @@ impl<COMP: Component> Scope<COMP> {
node_ref: NodeRef,
next_sibling: NodeRef,
) {
self.process(UpdateEvent::Properties(props, node_ref, next_sibling).into());
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse");
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
}
pub(crate) fn process(&self, event: ComponentLifecycleEvent<COMP>) {
self.schedule(event);
scheduler::start();
}
fn schedule(&self, event: ComponentLifecycleEvent<COMP>) {
use ComponentLifecycleEvent::*;
let push = match &event {
Create(_) => scheduler::push_component_create,
Update(_) => scheduler::push_component_update,
Render => scheduler::push_component_render,
Rendered => scheduler::push_component_rendered,
Destroy => scheduler::push_component_destroy,
};
push(Box::new(ComponentRunnable {
fn push_update(&self, event: UpdateEvent<COMP>) {
scheduler::push_component_update(UpdateRunner {
state: self.state.clone(),
event,
}));
});
// Not guaranteed to already have the scheduler started
scheduler::start();
}
/// Send a message to the component.
@ -227,7 +273,7 @@ impl<COMP: Component> Scope<COMP> {
where
T: Into<COMP::Message>,
{
self.process(UpdateEvent::Message(msg.into()).into());
self.push_update(UpdateEvent::Message(msg.into()));
}
/// Send a batch of messages to the component.
@ -245,7 +291,7 @@ impl<COMP: Component> Scope<COMP> {
return;
}
self.process(UpdateEvent::MessageBatch(messages).into());
self.push_update(UpdateEvent::MessageBatch(messages));
}
/// Creates a `Callback` which will send a message to the linked

View File

@ -1,20 +1,12 @@
//! This module contains a scheduler.
use std::cell::RefCell;
use std::collections::VecDeque;
use std::collections::{hash_map::Entry, HashMap, VecDeque};
use std::rc::Rc;
/// Alias for Rc<RefCell<T>>
pub type Shared<T> = Rc<RefCell<T>>;
thread_local! {
/// This is a global scheduler suitable to schedule and run any tasks.
///
/// Exclusivity of mutable access is controlled by only accessing it through a set of public
/// functions.
static SCHEDULER: RefCell<Scheduler> = Default::default();
}
/// A routine which could be run.
pub trait Runnable {
/// Runs a routine with a context instance.
@ -26,70 +18,78 @@ pub trait Runnable {
#[allow(missing_debug_implementations)] // todo
struct Scheduler {
// Main queue
main: VecDeque<Box<dyn Runnable>>,
main: Vec<Box<dyn Runnable>>,
// Component queues
destroy: VecDeque<Box<dyn Runnable>>,
create: VecDeque<Box<dyn Runnable>>,
update: VecDeque<Box<dyn Runnable>>,
render: VecDeque<Box<dyn Runnable>>,
destroy: Vec<Box<dyn Runnable>>,
create: Vec<Box<dyn Runnable>>,
update: Vec<Box<dyn Runnable>>,
render_first: VecDeque<Box<dyn Runnable>>,
render: RenderScheduler,
// Stack
rendered: Vec<Box<dyn Runnable>>,
/// Stacks to ensure child calls are always before parent calls
rendered_first: Vec<Box<dyn Runnable>>,
rendered: RenderedScheduler,
}
/// Execute closure with a mutable reference to the scheduler
#[inline]
fn with(f: impl FnOnce(&mut Scheduler)) {
SCHEDULER.with(|s| f(&mut *s.borrow_mut()));
fn with<R>(f: impl FnOnce(&mut Scheduler) -> R) -> R {
thread_local! {
/// This is a global scheduler suitable to schedule and run any tasks.
///
/// Exclusivity of mutable access is controlled by only accessing it through a set of public
/// functions.
static SCHEDULER: RefCell<Scheduler> = Default::default();
}
SCHEDULER.with(|s| f(&mut *s.borrow_mut()))
}
/// Push a generic Runnable to be executed
#[inline]
/// Push a generic [Runnable] to be executed
pub fn push(runnable: Box<dyn Runnable>) {
with(|s| s.main.push_back(runnable));
with(|s| s.main.push(runnable));
// Execute pending immediately. Necessary for runnables added outside the component lifecycle,
// which would otherwise be delayed.
start();
}
/// Push a component creation Runnable to be executed
#[inline]
pub(crate) fn push_component_create(runnable: Box<dyn Runnable>) {
with(|s| s.create.push_back(runnable));
/// Push a component creation, first render and first rendered [Runnable]s to be executed
pub(crate) fn push_component_create(
create: impl Runnable + 'static,
first_render: impl Runnable + 'static,
first_rendered: impl Runnable + 'static,
) {
with(|s| {
s.create.push(Box::new(create));
s.render_first.push_back(Box::new(first_render));
s.rendered_first.push(Box::new(first_rendered));
});
}
/// Push a component destruction Runnable to be executed
#[inline]
pub(crate) fn push_component_destroy(runnable: Box<dyn Runnable>) {
with(|s| s.destroy.push_back(runnable));
/// Push a component destruction [Runnable] to be executed
pub(crate) fn push_component_destroy(runnable: impl Runnable + 'static) {
with(|s| s.destroy.push(Box::new(runnable)));
}
/// Push a component render Runnable to be executed
#[inline]
pub(crate) fn push_component_render(runnable: Box<dyn Runnable>) {
with(|s| s.render.push_back(runnable));
/// Push a component render and rendered [Runnable]s to be executed
pub(crate) fn push_component_render(
component_id: usize,
render: impl Runnable + 'static,
rendered: impl Runnable + 'static,
) {
with(|s| {
s.render.schedule(component_id, Box::new(render));
s.rendered.schedule(component_id, Box::new(rendered));
});
}
/// Push a component Runnable to be executed after a component is rendered
#[inline]
pub(crate) fn push_component_rendered(runnable: Box<dyn Runnable>) {
with(|s| s.rendered.push(runnable));
/// Push a component update [Runnable] to be executed
pub(crate) fn push_component_update(runnable: impl Runnable + 'static) {
with(|s| s.update.push(Box::new(runnable)));
}
/// Push a component update Runnable to be executed
#[inline]
pub(crate) fn push_component_update(runnable: Box<dyn Runnable>) {
with(|s| s.update.push_back(runnable));
}
/// Push a batch of component updates to be executed
#[inline]
pub(crate) fn push_component_updates(it: impl IntoIterator<Item = Box<dyn Runnable>>) {
with(|s| s.update.extend(it));
}
/// Execute any pending Runnables
/// Execute any pending [Runnable]s
pub(crate) fn start() {
thread_local! {
// The lock is used to prevent recursion. If the lock cannot be acquired, it is because the
@ -99,23 +99,161 @@ pub(crate) fn start() {
LOCK.with(|l| {
if let Ok(_lock) = l.try_borrow_mut() {
while let Some(runnable) = SCHEDULER.with(|s| s.borrow_mut().next_runnable()) {
runnable.run();
let mut queue = vec![];
loop {
with(|s| s.fill_queue(&mut queue));
if queue.is_empty() {
break;
}
for r in queue.drain(..) {
r.run();
}
}
}
});
}
impl Scheduler {
/// Pop next Runnable to be executed according to Runnable type execution priority
fn next_runnable(&mut self) -> Option<Box<dyn Runnable>> {
self.destroy
.pop_front()
.or_else(|| self.create.pop_front())
.or_else(|| self.update.pop_front())
.or_else(|| self.render.pop_front())
.or_else(|| self.rendered.pop())
.or_else(|| self.main.pop_front())
/// Fill vector with tasks to be executed according to Runnable type execution priority
///
/// This method is optimized for typical usage, where possible, but does not break on
/// non-typical usage (like scheduling renders in [crate::Component::create()] or
/// [crate::Component::rendered()] calls).
fn fill_queue(&mut self, to_run: &mut Vec<Box<dyn Runnable>>) {
// Placed first to avoid as much needless work as possible, handling all the other events.
// Drained completely, because they are the highest priority events anyway.
to_run.append(&mut self.destroy);
// Create events can be batched, as they are typically just for object creation
to_run.append(&mut self.create);
// First render must never be skipped and takes priority over main, because it may need
// to init `NodeRef`s
//
// Should be processed one at time, because they can spawn more create and rendered events
// for their children.
if let Some(r) = self.render_first.pop_front() {
to_run.push(r);
}
// These typically do nothing and don't spawn any other events - can be batched.
// Should be run only after all first renders have finished.
if !to_run.is_empty() {
return;
}
to_run.extend(self.rendered_first.drain(..).rev());
// Updates are after the first render to ensure we always have the entire child tree
// rendered, once an update is processed.
//
// Can be batched, as they can cause only non-first renders.
to_run.append(&mut self.update);
// Likely to cause duplicate renders via component updates, so placed before them
to_run.append(&mut self.main);
// Run after all possible updates to avoid duplicate renders.
//
// Should be processed one at time, because they can spawn more create and first render
// events for their children.
if !to_run.is_empty() {
return;
}
if let Some(r) = self.render.pop() {
to_run.push(r);
}
// These typically do nothing and don't spawn any other events - can be batched.
// Should be run only after all renders have finished.
if !to_run.is_empty() {
return;
}
self.rendered.drain_into(to_run);
}
}
/// Task to be executed for specific component
struct QueueTask {
/// Tasks in the queue to skip for this component
skip: usize,
/// Runnable to execute
runnable: Box<dyn Runnable>,
}
/// Scheduler for non-first component renders with deduplication
#[derive(Default)]
struct RenderScheduler {
/// Task registry by component ID
tasks: HashMap<usize, QueueTask>,
/// Task queue by component ID
queue: VecDeque<usize>,
}
impl RenderScheduler {
/// Schedule render task execution
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
self.queue.push_back(component_id);
match self.tasks.entry(component_id) {
Entry::Vacant(e) => {
e.insert(QueueTask { skip: 0, runnable });
}
Entry::Occupied(mut e) => {
let v = e.get_mut();
v.skip += 1;
// Technically the 2 runners should be functionally identical, but might as well
// overwrite it for good measure, accounting for future changes. We have it here
// anyway.
v.runnable = runnable;
}
}
}
/// Try to pop a task from the queue, if any
fn pop(&mut self) -> Option<Box<dyn Runnable>> {
while let Some(id) = self.queue.pop_front() {
match self.tasks.entry(id) {
Entry::Occupied(mut e) => {
let v = e.get_mut();
if v.skip == 0 {
return Some(e.remove().runnable);
}
v.skip -= 1;
}
Entry::Vacant(_) => (),
}
}
None
}
}
/// Deduplicating scheduler for component rendered calls with deduplication
#[derive(Default)]
struct RenderedScheduler {
/// Task registry by component ID
tasks: HashMap<usize, Box<dyn Runnable>>,
/// Task stack by component ID
stack: Vec<usize>,
}
impl RenderedScheduler {
/// Schedule rendered task execution
fn schedule(&mut self, component_id: usize, runnable: Box<dyn Runnable>) {
if self.tasks.insert(component_id, runnable).is_none() {
self.stack.push(component_id);
}
}
/// Drain all tasks into `dst`, if any
fn drain_into(&mut self, dst: &mut Vec<Box<dyn Runnable>>) {
for id in self.stack.drain(..).rev() {
if let Some(t) = self.tasks.remove(&id) {
dst.push(t);
}
}
}
}

View File

@ -9,6 +9,34 @@ use std::ops::Deref;
use std::rc::Rc;
use web_sys::Element;
thread_local! {
#[cfg(debug_assertions)]
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<u64, Vec<String>>>
= Default::default();
}
/// Push [VComp] event to lifecycle debugging registry
#[cfg(debug_assertions)]
pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) {
EVENT_HISTORY.with(|h| {
h.borrow_mut()
.entry(vcomp_id)
.or_default()
.push(event.to_string())
});
}
/// Get [VComp] event log from lifecycle debugging registry
#[cfg(debug_assertions)]
pub(crate) fn get_event_log(vcomp_id: u64) -> Vec<String> {
EVENT_HISTORY.with(|h| {
h.borrow()
.get(&vcomp_id)
.map(|l| (*l).clone())
.unwrap_or_default()
})
}
/// A virtual component.
pub struct VComp {
type_id: TypeId,
@ -16,6 +44,10 @@ pub struct VComp {
props: Option<Box<dyn Mountable>>,
pub(crate) node_ref: NodeRef,
pub(crate) key: Option<Key>,
/// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) id: u64,
}
impl Clone for VComp {
@ -24,12 +56,18 @@ impl Clone for VComp {
panic!("Mounted components are not allowed to be cloned!");
}
#[cfg(debug_assertions)]
log_event(self.id, "clone");
Self {
type_id: self.type_id,
scope: None,
props: self.props.as_ref().map(|m| m.copy()),
node_ref: self.node_ref.clone(),
key: self.key.clone(),
#[cfg(debug_assertions)]
id: self.id,
}
}
}
@ -97,12 +135,40 @@ impl VComp {
props: Some(Box::new(PropsWrapper::<COMP>::new(props))),
scope: None,
key,
#[cfg(debug_assertions)]
id: {
thread_local! {
static ID_COUNTER: std::cell::RefCell<u64> = Default::default();
}
ID_COUNTER.with(|c| {
let c = &mut *c.borrow_mut();
*c += 1;
*c
})
},
}
}
pub(crate) fn root_vnode(&self) -> Option<impl Deref<Target = VNode> + '_> {
self.scope.as_ref().and_then(|scope| scope.root_vnode())
}
/// Take ownership of [Box<dyn Scoped>] or panic with error message, if component is not mounted
#[inline]
fn take_scope(&mut self) -> Box<dyn Scoped> {
self.scope.take().unwrap_or_else(|| {
#[cfg(not(debug_assertions))]
panic!("no scope; VComp should be mounted");
#[cfg(debug_assertions)]
panic!(
"no scope; VComp should be mounted after: {:?}",
get_event_log(self.id)
);
})
}
}
trait Mountable {
@ -156,7 +222,7 @@ impl<COMP: Component> Mountable for PropsWrapper<COMP> {
impl VDiff for VComp {
fn detach(&mut self, _parent: &Element) {
self.scope.take().expect("VComp is not mounted").destroy();
self.take_scope().destroy();
}
fn apply(
@ -173,7 +239,7 @@ impl VDiff for VComp {
// If the ancestor is the same type, reuse it and update its properties
if self.type_id == vcomp.type_id && self.key == vcomp.key {
self.node_ref.reuse(vcomp.node_ref.clone());
let scope = vcomp.scope.take().expect("VComp is not mounted");
let scope = vcomp.take_scope();
mountable.reuse(self.node_ref.clone(), scope.borrow(), next_sibling);
self.scope = Some(scope);
return vcomp.node_ref.clone();

View File

@ -57,7 +57,16 @@ impl VNode {
let text_node = vtext.reference.as_ref().expect("VText is not mounted");
text_node.clone().into()
}
VNode::VComp(vcomp) => vcomp.node_ref.get().expect("VComp is not mounted"),
VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| {
#[cfg(not(debug_assertions))]
panic!("no node_ref; VComp should be mounted");
#[cfg(debug_assertions)]
panic!(
"no node_ref; VComp should be mounted after: {:?}",
crate::virtual_dom::vcomp::get_event_log(vcomp.id),
);
}),
VNode::VList(vlist) => vlist.get(0).expect("VList is not mounted").first_node(),
VNode::VRef(node) => node.clone(),
}