-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Conversation
0b116d5
to
aefa8a2
Compare
Here is the JS framework benchmark result after just removing the bump allocator: ![]() 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 |
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. |
Not yet, dioxus-hooks is currently just commented out.
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 |
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(¤tly_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",
]; |
Amazing work @ealmloff @jkelleyrtp ! 👏 🧬 |
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
clone
to bring state into closures and async blocksDioxus will still retain:
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:
Readable<T>
andWritable<T>
?