async_ui icon indicating copy to clipboard operation
async_ui copied to clipboard

Re-using allocated components inside a VirtualizedList?

Open RandomInsano opened this issue 11 months ago • 3 comments

Heya! This crate is pretty neat and I'm evaluating whether or not I want to use this for a commercial product and so far, it's quite nice! If I do move forward, I'd help remove the current warnings being thrown and such.

I was playing around with the VirtualizedList component to see if it can efficiently re-use components and from the example, it looks like it can but there are also notices that it's not done yet. From my testing it seems like it's not quite working yet? The following eventually runs out of the pre-allocated TicketCompent instances.

use std::cell::RefCell;

use async_ui_web::{
    html::{
        Div,
        Text
    },
    join,
    lists::VirtualizedList,
    shortcut_traits::ShortcutClassList
};
use uuid::Uuid;

pub async fn app() {
    let root = Div::new();
    root.add_class(style::wrapper);

    let mut divs = Vec::new();
    for _ in 0 .. 10000 {
        let div = TicketComponent::new();
        divs.push(div);
    }
    let divs = &RefCell::new(divs);

    let list = VirtualizedList::new(
        &root.element,
        Div::new().element.into(),
        Div::new().element.into(),
        |index| async move {

            let mut div = divs.borrow_mut().pop()
                .expect("Ran out of divs!");
            div.set_number(index);

            let pool_data = Text::new();
            let number_of_divs_left = divs.borrow().len().to_string();
            pool_data.set_data(&format!("{} remaining" , number_of_divs_left));

            let holder = Div::new().render(
                join((div.render(), pool_data.render()))
            );

            let fut = holder;

            fut.await;

            divs.borrow_mut().push(div);
        },
    );
    list.set_num_items(10000);
    root.render(list.render()).await;
}

struct TicketComponent {
    container: Div,
    uuid_container: Div,
    label: Text,
    data: Text,
    id: Text,
}

impl TicketComponent {
    fn new() -> Self {
        let label = Text::new();
        label.set_data("Index: ");

        let data = Text::new();
        data.set_data("NaN");

        let id = Text::new();
        id.set_data(&Uuid::new_v4().to_string());

        Self {
            container: Div::new(),
            uuid_container: Div::new(),
            label,
            data,
            id,
        }
    }

    fn set_number(&mut self, value: usize) {
        self.data.set_data(&value.to_string());
    }

    async fn render(&self) {
        self.container.render(
            join((
                self.label.render(),
                self.data.render(),
                self.uuid_container.render(
                    self.id.render()
                )
            ))).await;
    }
}

mod style {
    use async_ui_web::css;

    css!(
        "
.wrapper {
	height: 75vh;
	width: 24em;
	overflow: scroll;
}
	"
    );
}

RandomInsano avatar Dec 27 '24 00:12 RandomInsano

Ah, so it looks like the whole function gets dropped which makes some sense as to why there was a scopeguard::defer() call... For example, this never panics:

            fut.await;

            panic!("This should cause some trouble...");

            divs.borrow_mut().push(div);

I wasn't able to use it because of the borrow happening as part of TicketComponent::render() but I don't think the overhead of creating that component is a huge deal.

RandomInsano avatar Dec 27 '24 03:12 RandomInsano

You should use scopeguard::guard! It is slightly more powerful than scopeguard::defer in that it allows you to borrow the "guarded" object. Here's the code:

        |index| async move {

            let mut div = divs.borrow_mut().pop()
                .expect("Ran out of divs!");
            div.set_number(index);

            let pool_data = Text::new();
            let number_of_divs_left = divs.borrow().len().to_string();
            pool_data.set_data(&format!("{} remaining" , number_of_divs_left));

            // 👇 wrap your component in a guard
            let guarded_div = scopeguard::guard(div, |unloaded_div| {
                // 👇 when the future is dropped, add the wrapped object back to `divs`
                divs.borrow_mut().push(unloaded_div);
            });
            let holder = Div::new().render(
                // 👇 borrow the guarded object to call the render method
                join((guarded_div.render(), pool_data.render()))
            );

            let fut = holder;

            fut.await;
        }

As for why your initial code didn't work: In Async UI, removal of a UI element is done not by completing the corresponding future but rather by dropping it. So your fut never completes. Rather, the whole async closure gets dropped when the VirtualizedList decides its out of the viewport. The reasoning here is in async Rust, the executor has no control over what happens inside a future object. It could only poll it or drop it. There is no general way to tell a future object "we are done; complete yourself immediately so that the cleanup code below you can run".

Let me know if this helps!

PS: You're probably doing this for testing but I still want to note: the goal of VirtualizedList is to not waste memory with elements that are not visible on the screen, so you shouldn't create 10000 elements in your elements cache (divs). A few hundred should be enough. If not, you can also lazily create new ones the same way I did in the example (divs.borrow_mut().pop().unwrap_or_else(Div::new)).

wishawa avatar Dec 27 '24 21:12 wishawa

That’s quite helpful! I’ll play with it some more and see how it goes.

I’m going to build a more complex UI around the VirtualizedList this week to get more comfortable with things. Something I want to try and figure out is how I can extend it to batch query in the backend. I have it working right now with a client-side cache but it’d be nicer to just have the control query the server with a fixed API. I’ll spend some time refactoring the code this week if you’re open to giving me some criticism to make it good. Feel free to close this up!

Thanks!

Edwin

RandomInsano avatar Dec 30 '24 00:12 RandomInsano