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

My toy framework is 8.54x faster than vanillajs 🥇 #1442

Closed
Niedzwiedzw opened this issue Oct 20, 2023 · 8 comments
Closed

My toy framework is 8.54x faster than vanillajs 🥇 #1442

Niedzwiedzw opened this issue Oct 20, 2023 · 8 comments

Comments

@Niedzwiedzw
Copy link
Contributor

Niedzwiedzw commented Oct 20, 2023

All jokes aside, this is the the result of me running benchmarks locally

image

I have also manually tested the implementation and ran the isKeyed test (my implemnetation is non-keyed).

Obviously this is some problem with measurements and I would really like to get this straight. Any ideas what might be corrupting the results?

cheers

@Niedzwiedzw Niedzwiedzw changed the title My toy framework is 20x faster than vanillajs 🥇 My toy framework is 8.54x faster than vanillajs 🥇 Oct 20, 2023
@syduki
Copy link
Contributor

syduki commented Oct 20, 2023

Faster than vanillajs can be only vanillajs.
Jokes aside, there is an issue in this very case as well as in general, it is related not with measurements but with the simplicity of this very benchmark. So, here is either an over-optimization or a deviation from the expected implementation (aka cheating) in your toy framework which tricks the benchmark.
With that being said, those figures are easily achieved with a simple css rule, like this one:

table tr:nth-child(n+11) { display: none; }

@krausest I guess this "issue" is a evidence that some kind of checking for elements visibility should be implemented to improve the reliability of the benchmark.

@Niedzwiedzw
Copy link
Contributor Author

Niedzwiedzw commented Oct 20, 2023

Faster than vanillajs can be only vanillajs. Jokes aside, there is an issue in this very case as well as in general, it is related not with measurements but with the simplicity of this very benchmark. So, here is either an over-optimization or a deviation from the expected implementation (aka cheating) in your toy framework which tricks the benchmark. With that being said, those figures are easily achieved with a simple css rule, like this one:

table tr:nth-child(n+11) { display: none; }

@krausest I guess this "issue" is a evidence that some kind of checking for elements visibility should be implemented to improve the reliability of the benchmark.

Thank you for replying. I am happy to collaborate in order to track down the exact cause. My implementation is pretty much a mix of an immediate mode gui with elm-like message passing. I'm hooking the dom events with messages that then get handled asynchronously (updating the app state), and then running old_builder.rebuild(new_builder) - comparing old changes with the new changes and applying the diff. Framework was not written with cheating of any benchmark in mind. To be honest, I haven't done any optimizations at this point as I focused on the api and only ran the benchmark out of curiosity.

TLDR: I'm not a troll :P

@Niedzwiedzw
Copy link
Contributor Author

Niedzwiedzw commented Oct 20, 2023

use std::sync::atomic::{AtomicUsize, Ordering};

use futures_util::StreamExt;
use korvin::core::{
    element_builder::{AsElementBuilder, ElementBuilder},
    flavors::elm_like::Communicator,
    Runtime,
};
use rand::{seq::SliceRandom, thread_rng};
use web_sys::MouseEvent;

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",
];

fn button<F: FnOnce(ElementBuilder) -> ElementBuilder>(
    id: &str,
    text: &str,
    button: F,
) -> ElementBuilder {
    "div".attribute("class", "col-sm-6 smallpad").child(button(
        "button"
            .attribute("id", id)
            .attribute("class", "btn btn-primary btn-block")
            .attribute("type", "button")
            .text(text),
    ))
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct RowData {
    id: usize,
    label: String,
}

static ID_COUNTER: AtomicUsize = AtomicUsize::new(1);

fn build_data(count: usize) -> Vec<RowData> {
    let mut thread_rng = thread_rng();

    let mut data = Vec::new();
    data.reserve_exact(count);

    for _i in 0..count {
        let adjective = ADJECTIVES.choose(&mut thread_rng).unwrap();
        let colour = COLOURS.choose(&mut thread_rng).unwrap();
        let noun = NOUNS.choose(&mut thread_rng).unwrap();
        let capacity = adjective.len() + colour.len() + noun.len() + 2;
        let mut label = String::with_capacity(capacity);
        label.push_str(adjective);
        label.push(' ');
        label.push_str(colour);
        label.push(' ');
        label.push_str(noun);

        data.push(RowData {
            id: ID_COUNTER.fetch_add(1, Ordering::Relaxed),
            label,
        });
    }

    data
}

#[derive(Default)]
struct App {
    data: Vec<RowData>,
    selected: Option<usize>,
}

impl App {
    fn handle(&mut self, message: AppMessage) {
        match message {
            AppMessage::Create1000Rows => self.data = build_data(1000),
            AppMessage::Create10000Rows => self.data = build_data(10000),
            AppMessage::Append1000Rows => self.data.append(&mut build_data(1000)),
            AppMessage::UpdateEvery10thRow => self
                .data
                .iter_mut()
                .step_by(10)
                .for_each(|row| row.label.push_str(" !!!")),
            AppMessage::Clear => {
                self.selected = None;
                self.data.clear();
            }
            AppMessage::SwapRows => {
                if self.data.len() > 998 {
                    self.data.swap(1, 998)
                }
            }
            AppMessage::SetSelected(id) => self.selected = Some(id),
            AppMessage::Remove(id) => self.data.retain(|row| row.id != id),
        }
    }
}

#[derive(Clone, Copy, Hash)]
enum AppMessage {
    Create1000Rows,
    Create10000Rows,
    Append1000Rows,
    UpdateEvery10thRow,
    Clear,
    SwapRows,
    SetSelected(usize),
    Remove(usize),
}

fn app(communicator: Communicator<AppMessage>, App { data, selected }: &App) -> ElementBuilder {
    let div = |class: &str| "div".attribute("class", class);
    let mousedown_send = move |message| move |_: MouseEvent| communicator.send(message);
    let element_mousedown_send = |element: ElementBuilder, message| {
        element.event(message, "mousedown", mousedown_send(message))
    };

    let message_button =
        |id, text, message| button(id, text, |button| element_mousedown_send(button, message));
    let jumbotron = div("jumbotron")
        .child(div("col-md-6").child("h1".text("Korvin")))
        .child(div("col-md-6"))
        .child(
            div("row")
                .child(message_button(
                    "run",
                    "Create 1,000 rows",
                    AppMessage::Create1000Rows,
                ))
                .child(message_button(
                    "runlots",
                    "Create 10,000 rows",
                    AppMessage::Create10000Rows,
                ))
                .child(message_button(
                    "add",
                    "Append 1,000 rows",
                    AppMessage::Append1000Rows,
                ))
                .child(message_button(
                    "update",
                    "Update every 10th row",
                    AppMessage::UpdateEvery10thRow,
                ))
                .child(message_button("clear", "Clear", AppMessage::Clear))
                .child(message_button(
                    "swaprows",
                    "Swap Rows",
                    AppMessage::SwapRows,
                )),
        );
    let table = "table"
        .attribute("class", "table table-hover table-striped test-data")
        .child("tbody".children(data.iter().map(|RowData { id, label }| {
            let mut tr = "tr".into_builder();
            if selected
                .as_ref()
                .map(|selected| selected.eq(id))
                .unwrap_or_default()
            {
                tr = tr.attribute("class", "danger");
            }
            let td = |class: &str| "td".attribute("class", class);
            let row = {
                td("col-md-4").child(element_mousedown_send(
                    "a".text(label.as_str()),
                    AppMessage::SetSelected(*id),
                ))
            };
            tr.child(td("col-md-1").text(id.to_string().as_str()))
                .child(row)
                .child(
                    td("col-md-1").child(element_mousedown_send(
                        "a".child(
                            "span"
                                .attribute("class", "glyphicon glyphicon-remove")
                                .attribute("aria-hidden", "true"),
                        ),
                        AppMessage::Remove(*id),
                    )),
                )
                .child(td("col-md-6"))
        })));
    div("container").child(jumbotron).child(table).child(
        "span"
            .attribute("class", "preloadicon glyphicon glyphicon-remove")
            .attribute("aria-hidden", "true"),
    )
}

fn main() {
    console_error_panic_hook::set_once();
    let main = web_sys::window()
        .and_then(|w| w.document())
        .map(|d| d.query_selector("#main").unwrap().unwrap())
        .unwrap();

    wasm_bindgen_futures::spawn_local(async move {
        let mut runtime = Runtime::new(main);
        let (mut rx, communicator) = Communicator::create();
        let mut state = App::default();

        runtime
            .dom_executor
            .rebuild(app(communicator, &state).build())
            .unwrap();
        while let Some(message) = rx.next().await {
            state.handle(message);
            runtime
                .dom_executor
                .rebuild(app(communicator, &state).build())
                .unwrap()
        }
    });
}

here is the code for reference

@syduki
Copy link
Contributor

syduki commented Oct 20, 2023

TLDR: I'm not a troll :P

Don't get me wrong, people love challenges and some frameworks in this benchmark can be tracked down to have been built just to fulfill the purpose of "fastest framework".
Anyway, I see this is a Rust implementation and the few Rust frameworks here doesn't show such a breakthrough in performance. Again, as I said, I personally don't think this is a measurement issue, as the measurements here are pretty straightforward and have been battle-tested on very different kind of implementations. Thus my only assumption is that there is something wrong within your framework/implementation, but I cannot confirm this only by looking at the code you provided. Maybe you can just submit a PR with this non-keyed framework, for a better visibility for all concerned?

@krausest
Copy link
Owner

Please create a PR and I‘ll take a look at it.

@Niedzwiedzw
Copy link
Contributor Author

Niedzwiedzw commented Oct 21, 2023

@krausest #1451

sorry for the delay, but as I said it's a toy project and I needed to clean it up, also korvin is a much more serious name than bimber (kind of polish alcoholic drink). In case you need help with building everything from source ping me on discord: niedzwiedzw

@Niedzwiedzw
Copy link
Contributor Author

repo with library source code:
https://github.com/Niedzwiedzw/korvin

@Niedzwiedzw
Copy link
Contributor Author

Niedzwiedzw commented Oct 22, 2023

we have figured out the reason of measurement corruption
turns out the implementation needs to use "click" event, as using "mousedown" will offset the measurement by the browser lag. closing :)
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants