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

vBox unusably slow if you give it hundreds of items #264

Closed
hyperrealgopher opened this issue Feb 28, 2020 · 7 comments
Closed

vBox unusably slow if you give it hundreds of items #264

hyperrealgopher opened this issue Feb 28, 2020 · 7 comments

Comments

@hyperrealgopher
Copy link

I am writing this: https://github.com/hyperreal-gopher/hopher and I profiled it and found out vBox is choking on gopher pages with hundreds of lines (gopher is very line-oriented, so I have made each line a widget, especially so I can move the selector around while also being able to scroll text in the viewport independent of the selection...).

It's basically unusuable and at this point I'm considering using the List widget but I'm not so sure that will be ideal for exactly what I want, either.

@jtdaugherty
Copy link
Owner

Greetings, and thanks for your report!

The behavior you are experiencing is definitely unpleasant, but is expected. The problem isn't vBox per se, but the fact that you are putting hundreds of items in it and then scrolling the result. I've made mention of this pitfall in a few places, but the use case that you may be trying to work with is a common one, and you are solving it in a common way. The basic reason why this is slow is because all of the items in the vBox are rendered regardless of the viewport scroll position. That means that the whole list is rendered, then cropped, and then displayed. If that list is long, that will be expensive. The List widget has some intelligence to avoid that. It does so by 1) assuming the per-item height and 2) using the viewport height to render only enough items to fill it. That gives it much better performance. But when List isn't the thing you want, you sorta have to deal with this issue yourself. I've dealt with it in a few ways in other applications.

To get better performance, there are two main options:

  • Put your viewport contents into the rendering cache. Then deal with cache invalidation when you update the underlying data structure in your event handler.
  • Render only enough of your viewport contents to fill the screen. This is easy to do if all you want to do is render the last N messages (for a window height of N, say); a custom widget can be written to render them until the screen space is exhausted, and then stop. That limits the amount of rendering and will get you back to better responsiveness. That's not so easy if you want to scroll around a cursor in the list. For that kind of approach, I would suggest using List since it already does what you want. A final caveat is that List only works on items whose rendering heights are uniform; you can't mix, say, one- and two-row-high items in a List since that will break the optimization I described above.

Let me know if either of those options help. I'm happy to provide more details or point you at resources.

@hyperrealgopher
Copy link
Author

Thank you for your reply. Sorry I hadn't read that pitfall.

I am not sure the caching would help the arrow/link selection changes the widgets; when I cache it does not visibly move anymore. I imagine there are no gains to be had if the big lag comes from moving the link selection/manipulating the widgets to move the arrow/active link style and I just invalidate the cache every time I move the link as aforementioned.

Here is an early example of desired behavior as I had it: https://cf.mastohost.com/v1/AUTH_91eb37814936490c95da7b85993cc2ff/fosstodon/media_attachments/files/003/984/420/original/013774236259d7f4.mp4

Here's another example from more recent: https://cf.mastohost.com/v1/AUTH_91eb37814936490c95da7b85993cc2ff/fosstodon/media_attachments/files/003/996/873/original/d1c39724ffad67c9.mp4

It sounds like the only option for me is to try some trickery with only rendering as much contents as between the current and next link, roughly. I also was never able to figure out getting the available height.

@jtdaugherty
Copy link
Owner

Sorry I hadn't read that pitfall.

No problem - I mentioned that not to say that it should have been obvious, but just to say that I had tried. But I could probably do better putting a big red warning sign on this kind of situation. It has come up before, at least a few times.

I am not sure the caching would help the arrow/link selection changes the widgets; when I cache it does not visibly move anymore.

That's right; the caching approach only works for content that stays the same while you scroll it. If you want a cursor, you need a List or a manual highlighting approach.

I imagine there are no gains to be had if the big lag comes from moving the link selection/manipulating the widgets to move the arrow/active link style and I just invalidate the cache every time I move the link as aforementioned.

That's right. The caching will only help if it allows you to avoid re-rendering all of the contents every time, especially the off-screen contents.

It sounds like the only option for me is to try some trickery with only rendering as much contents as between the current and next link, roughly. I also was never able to figure out getting the available height.

Oh! For that, see this section of the User Guide. The key is to get the rendering context and look at the availHeightL field. The example code in that section shows that in action.

@hyperrealgopher
Copy link
Author

Okay, I think I saw something on how you can avoid using lens by basically dropping the L... I'm new to Haskell, so forgive me if I take a while to test this out and respond. I will attempt to only render the lines of the gopher resource that are being shown in the viewport or something of the sort.

@jtdaugherty
Copy link
Owner

Okay, I think I saw something on how you can avoid using lens by basically dropping the L... I'm new to Haskell, so forgive me if I take a while to test this out and respond.

No problem. And yes, the convention I used in brick is that fooL is a lens version of a plain function foo, so you can do ctx^.availHeightL or availHeight ctx, depending on your preference.

@hyperrealgopher
Copy link
Author

hyperrealgopher commented Feb 29, 2020

Thanks, I'll try using the list for this purpose. I will keep posting in this thread to keep others who experienced this caveat (namely I'll link to a commit later), but will close it for now since vbox is expected to have this behavior.

Here is what I've come up with (this is worth reading, for those wondering; basically the ListVi application example was exactly what I wanted): https://fosstodon.org/@coffeerobot/103742873209495658

It still needs some work but this effectively solves my problems. Thank you so much for your work developing a remarkable library @jtdaugherty and for having the patience and thoroughness when addressing my concerns so quickly.

Also thanks to @Garmelon (who has been helping me to understand Haskell and Brick, specifically).

@jtdaugherty
Copy link
Owner

You're welcome, and I'm glad I was able to help!

mpickering pushed a commit to bgamari/ghc-debug that referenced this issue Apr 29, 2024
`viewport` is unreasonably inefficient for our usecase when the IOTree grows, as it
decides what to display once it rendered to full `IOTree`.
Especially while scrolling, this inefficiency became apparent.

Unfortunately, brick doesn't have any builtin way to improve this, see jtdaugherty/brick#264

We take `drawListElement` as an inspiration, and only render up to
`heightOfBox * 2` elements of the `IOTree`, including expanded tree
nodes.
This speeds up the view noticeably, giving the user a responsive experience.

In particular, we decouple rendering metadata computation from the actual rendering to decide which rows to render.
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

2 participants