Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove scope, use_state, use_ref, bump allocator and make everything 'static #1791

Merged
merged 384 commits into from
Feb 5, 2024

Conversation

ealmloff
Copy link
Member

@ealmloff ealmloff commented Jan 6, 2024

This PR is a massively breaking change to the entirety of Dioxus, reworking state management almost entirely. It moves Dioxus away from the classic React useState towards a signal-based paradigm introduced in 0.4.3.

The inspiration for this work comes from a number of places like SolidJS with an entirely 'static lifetime system borrowed from Leptos. The manifestation of this work will look very similar to the final API of the upcoming React Forget compiler where dependencies on hooks like use_effect are managed automatically by the compiler.

This has a number of advantages

  • No more dependency arrays
  • No more requirement to call clone to bring state into closures and async blocks
  • Precise re-renders based on selectors and (eventually) incremental fine-grained reactivity
  • No loss of reactivity thanks to diffing and re-rendering architecture
  • Ability to call event handlers from async functions
  • Much simpler and more easily composable global APIs
  • Removal of the scope cx object
  • Removal of all lifetimes
  • Ability to cache vnodes for things like virtualized lists

Dioxus will still retain:

  • hooks
  • global context
  • components
  • diffing and re-rendering

The impetus for this work is finally acknowledging the difficulty of managing a lifetime managed state system while simultaneously looking forward toward's React's future. This state system is novel and does not exist in any other one particular framework.

Closes #1374
Closes #1032
Closes #619
Closes #1753
Closes #1793
Closes #1405
Closes DioxusLabs/docsite#192

Todo:

  • Fix core tests
  • Change effect order so use_effect/use_selectors are deferred and refreshed
  • Signals as props should be made in child scope
  • Introduce boxed variants of Readable<T> and Writable<T>?
  • Overhaul examples
  • Overhaul tests
  • flush_sync for consistent effect order
  • Overhaul documentation
  • Overhaul desktop APIs
  • Overhaul router APIs
  • ...more

@ealmloff ealmloff added breaking This is a breaking change experimental May or may not be merged labels Jan 6, 2024
@ealmloff ealmloff force-pushed the breaking branch 2 times, most recently from 0b116d5 to aefa8a2 Compare January 7, 2024 19:41
@ealmloff
Copy link
Member Author

ealmloff commented Jan 7, 2024

Here is the JS framework benchmark result after just removing the bump allocator:

Screenshot 2024-01-07 at 4 45 36 PM

Still the same ranking. It might be a bit slower, but there is a lot more we can do to get some of the performance benefits back without the unsafe like using a slotmap of Elements and using diffable arguments

@jkelleyrtp
Copy link
Member

Are the use_effect/use_future/use_memo all updated in this PR? Curious about test driving it with the new primitives.

As far as performance... It looks like we moved, right? We were at 1.12 before? I'd like to stay closer to that if we can. I think some obvious wins are reusing buffers (like strings) and merging buffers (again, like strings, and element vecs). Event handlers are a bit more complex and box is probably fine for those, but that's where stuff like the bump really shined.

@ealmloff
Copy link
Member Author

ealmloff commented Jan 8, 2024

Are the use_effect/use_future/use_memo all updated in this PR? Curious about test driving it with the new primitives.

Not yet, dioxus-hooks is currently just commented out. dioxus-signals works, but hasn't been changed

As far as performance... It looks like we moved, right? We were at 1.12 before? I'd like to stay closer to that if we can. I think some obvious wins are reusing buffers (like strings) and merging buffers (again, like strings, and element vecs). Event handlers are a bit more complex and box is probably fine for those, but that's where stuff like the bump really shined.

Yeah, it is a bit slower. We are at 1.12 on the official benchmark. Within 0.05 points there is also quite a bit of variance. I just wanted to test what I naive implementation would look like before I spent too much time on optimizations, but I have been working on some optimizations today. One pretty easy win is keeping the mounted information across renders instead of allocating a new buffer and copying them every time

@ealmloff
Copy link
Member Author

Here is what the js framework benchmark looks like:

#![allow(non_snake_case)]

use dioxus::prelude::*;
use dioxus_signals::*;
use js_sys::Math;

fn random(max: usize) -> usize {
    (Math::random() * 1000.0) as usize % max
}

fn main() {
    dioxus_web::launch(app);
}

#[derive(Copy, Clone, PartialEq)]
struct Label {
    key: usize,
    label: Signal<String>,
}

impl Label {
    fn new(num: usize, label: String) -> Self {
        Label {
            key: num,
            label: Signal::new(label),
        }
    }

    fn new_list(num: usize, key_from: usize) -> Vec<Self> {
        let mut labels = Vec::with_capacity(num);
        append(&mut labels, num, key_from);
        labels
    }
}

fn append(list: &mut Vec<Label>, num: usize, key_from: usize) {
    list.reserve_exact(num);
    for x in 0..num {
        let adjective = ADJECTIVES[random(ADJECTIVES.len())];
        let colour = COLOURS[random(COLOURS.len())];
        let noun = NOUNS[random(NOUNS.len())];
        let mut label = String::with_capacity(adjective.len() + colour.len() + noun.len() + 2);
        label.push_str(adjective);
        label.push(' ');
        label.push_str(colour);
        label.push(' ');
        label.push_str(noun);
        list.push(Label::new(x + key_from, label));
    }
}

#[derive(Clone, PartialEq)]
struct LabelsContainer {
    last_key: usize,
    labels: Vec<Label>,
}

impl LabelsContainer {
    fn new(num: usize, last_key: usize) -> LabelsContainer {
        let labels = Label::new_list(num, last_key + 1);
        LabelsContainer {
            labels,
            last_key: last_key + num,
        }
    }

    fn append(&mut self, num: usize) {
        self.labels.reserve(num);
        append(&mut self.labels, num, self.last_key + 1);
        self.last_key += num;
    }

    fn overwrite(&mut self, num: usize) {
        self.labels.clear();
        append(&mut self.labels, num, self.last_key + 1);
        self.last_key += num;
    }

    fn swap(&mut self, a: usize, b: usize) {
        if self.labels.len() > a + 1 && self.labels.len() > b {
            self.labels.swap(a, b);
        }
    }

    fn remove(&mut self, key: usize) {
        if let Some(to_remove) = self.labels.iter().position(|x| x.key == key) {
            self.labels.remove(to_remove);
        }
    }
}

fn app(_: ()) -> Element {
    let labels_container = use_signal(|| LabelsContainer::new(0, 0));
    let selected: Signal<Option<usize>> = use_signal(|| None);
    let prev_selected: Signal<Option<usize>> = use_signal(|| None);
    let selected_selector: Signal<rustc_hash::FxHashMap<usize, Signal<bool>>> = use_signal(Default::default);
    dioxus_signals::use_effect(move || {
        let currently_selected = selected.value();
        let selected_selector = selected_selector.read();
        let mut prev_selected = prev_selected.write();
        {
            let prev_selected = *prev_selected;
            if let Some(prev_selected) = prev_selected {
                if let Some(is_selected) = selected_selector.get(&prev_selected) {
                    is_selected.set(false);
                }
            }
        }
        if let Some(currently_selected) = currently_selected {
            if let Some(is_selected) = selected_selector.get(&currently_selected) {
                is_selected.set(true);
            }
        }
        *prev_selected = currently_selected;
    });

    render! {
        div { class: "container",
            div { class: "jumbotron",
                div { class: "row",
                    div { class: "col-md-6", h1 { "Dioxus" } }
                    div { class: "col-md-6",
                        div { class: "row",
                            ActionButton { name: "Create 1,000 rows", id: "run",
                                onclick: move |_| labels_container.write().overwrite(1_000),
                            }
                            ActionButton { name: "Create 10,000 rows", id: "runlots",
                                onclick: move |_| labels_container.write().overwrite(10_000),
                            }
                            ActionButton { name: "Append 1,000 rows", id: "add",
                                onclick: move |_| labels_container.write().append(1_000),
                            }
                            ActionButton { name: "Update every 10th row", id: "update",
                                onclick: move |_| {
                                    let labels = labels_container();
                                    for i in 0..(labels.labels.len()/10) {
                                        *labels.labels[i*10].label.write() += " !!!";
                                    }
                                },
                            }
                            ActionButton { name: "Clear", id: "clear",
                                onclick: move |_| labels_container.write().overwrite(0),
                            }
                            ActionButton { name: "Swap rows", id: "swaprows",
                                onclick: move |_| labels_container.write().swap(1, 998),
                            }
                        }
                    }
                }
            }

            table { class: "table table-hover table-striped test-data",
                tbody { id: "tbody",
                    labels_container().labels.iter().map(|item| {
                        render! {
                            Row {
                                label: item.clone(),
                                labels: labels_container.clone(),
                                selected_row: selected.clone(),
                                is_in_danger: {
                                    let read_selected_selector = selected_selector.read();
                                    match read_selected_selector.get(&item.key) {
                                        Some(is_selected) => *is_selected,
                                        None => {
                                            drop(read_selected_selector);
                                            let mut selected_selector = selected_selector.write();
                                            let is_selected = Signal::new(false);
                                            selected_selector.insert(item.key, is_selected);
                                            is_selected
                                        }
                                    }
                                },
                                key: "{item.key}"
                            }
                        }
                    })
                }
            }

            span { class: "preloadicon glyphicon glyphicon-remove", aria_hidden: "true" }
        }
    }
}

#[derive(Copy, Clone, Props)]
struct RowProps {
    label: Label,
    labels: Signal<LabelsContainer>,
    selected_row: Signal<Option<usize>>,
    is_in_danger: Signal<bool>
}

impl PartialEq for RowProps {
    fn eq(&self, other: &Self) -> bool {
        self.label == other.label
    }
}

fn Row(props: RowProps) -> Element {
    let RowProps {
        label,
        labels,
        selected_row,
        is_in_danger
    } = props;
    let is_in_danger = if is_in_danger.value() {
        "danger"
    } else {
        ""
    };

    render! {
        tr { class: "{is_in_danger}",
            td { class:"col-md-1", "{label.key}" }
            td { class:"col-md-4", onclick: move |_| {
                    selected_row.set(Some(label.key))
                },
                a { class: "lbl", "{label.label}" }
            }
            td { class: "col-md-1",
                a { class: "remove", onclick: move |_| labels.write().remove(label.key),
                    span { class: "glyphicon glyphicon-remove remove", aria_hidden: "true" }
                }
            }
            td { class: "col-md-6" }
        }
    }
}


#[derive(Clone, Props, PartialEq)]
struct ActionButtonProps {
    name: &'static str,
    id: &'static str,
    onclick: EventHandler,
}

fn ActionButton(
    ActionButtonProps {
        name,
        id,
        onclick,
    }: ActionButtonProps,
) -> Element {
    render! {
        div {
            class: "col-sm-6 smallpad",
            button {
                class:"btn btn-primary btn-block",
                r#type: "button",
                id: id.to_string(),
                onclick: move |_| onclick.call(()),
                *name
            }
        }
    }
}

static ADJECTIVES: &[&str] = &[
    "pretty",
    "large",
    "big",
    "small",
    "tall",
    "short",
    "long",
    "handsome",
    "plain",
    "quaint",
    "clean",
    "elegant",
    "easy",
    "angry",
    "crazy",
    "helpful",
    "mushy",
    "odd",
    "unsightly",
    "adorable",
    "important",
    "inexpensive",
    "cheap",
    "expensive",
    "fancy",
];

static COLOURS: &[&str] = &[
    "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black",
    "orange",
];

static NOUNS: &[&str] = &[
    "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger",
    "pizza", "mouse", "keyboard",
];

@jkelleyrtp jkelleyrtp merged commit 4f8868d into DioxusLabs:master Feb 5, 2024
7 of 9 checks passed
@marc2332
Copy link
Contributor

marc2332 commented Feb 5, 2024

Amazing work @ealmloff @jkelleyrtp ! 👏 🧬

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking This is a breaking change experimental May or may not be merged
Projects
Status: Done
4 participants