Big problem with thread performance on 64 bit

I have an interesting issue. I’m building for macOS, 64 bit. I’m using a thread to load a listbox with items from an array. This array is huge — it may contain 100,000 records or more.

In my old 32 bit version, this was no problem. Performance was fast. I took about 2-3 seconds to load the listbox.

Now I am using 64 bit, with a threaded task using the UpdateUI method. Here’s the interesting problem:

If I load the data from the array using a non-threaded method, it is fast. The UI locks up for a few seconds, and then the rows appear, and all is well. However, I don’t want the UI to lockup, even for a second. So I put this in a thread. When I do, it now takes over a minute (!!) to populate the listbox! What is going on here?

Test for yourself. Make a new project. Add a listbox to the window, and use this code in the Open even:

for i as integer=1 to 200000 ListBox1.addRow "ABCDEFGHIJKLMNOPQRSTUVWXYZ "+cstr(i) next

Run the project. Boom, the listbox populates in a couple seconds. Now use a thread:

Add Threaded Tasks to your project. Add a thread to the main window. Change the Open even to read

Thread1.run

Set Thread1’s subclass to Task

Add this code to the thread’s Run event:

for i as integer=1 to 200000 Me.UpdateUI("boxAddRow":"ABCDEFGHIJKLMNOPQRSTUVWXYZ "+cstr(i)) next

Add this code to the thread’s UpdateUI event:

if args.HasKey("boxAddRow") then dim rowData as string=args.value("boxAddRow").StringValue ListBox1.addRow rowData end if

Not only does the process now take much, much longer, but the UI locks up, you get a spinning beachball, and the whole thing freezes. After awhile, the rows will eventually load and the program will respond again.

Just curious what the reason for this is, and if there is a solution?

You wont want to hear these but -
you shouldnt do something that affects the UI , in a thread.
Putting 100,000 records in a listbox is bad UI anyway. Who wants to scroll down through 100,000 items?

When loading to a listbox I find it is faster if I turn off the vertical scrollbar.
And after adding one row, set the listindex to be 1 so that it isnt trying to display item 99,999

If you must have that many rows, consider having them empty, and loading the data into a dictionary , array, or collection.
You can display the text (read only) in the celltextpaint event, on demand
And if it must be editable, you could set the value of the displayed row, only when it is displayed for the first time.

It is important for all rows to be in the listbox, and there is a search field to filter the results. It isn’t bad UI, it is what the customer expects. And it worked perfectly fine in 32 bit builds. I don’t understand why it’s so slow and unusable now in 64 bit.

Haven’t heard of the method of showing the text in the textpaint event. How does that handle with scrolling and filtering? I’m not really sure I understand the concept and process behind that. Even adding blank rows with nothing in them has the same issue — adding 100,000 empty rows takes just as long in my example above.

I could have the listbox be empty, and prompt the user to enter a search string first, which is acceptable I suppose. But this also causes issues, because some search strings will return many many results. My main question is just, why the heck is this breaking so bad now? I thought with 64 bit, it would be faster, if anything.

@Jeff Tullin - he’s using the Task class. It is designed to handle this scenario.

Try adding a Me.Sleep(1, True) to the Thread’s Run loop after the call to UpdateUI.

However, I would urge you to considering loading a dictionary (which you can still search) and only updating the rows above and below the current listindex of the listbox for display.

@Tim Jones Thank you, that resolved the freezing. Now the records are updating in realtime, and the program does not appear unresponsive. However, it still takes much longer for the records to populate the listbox — it’s adding rows and refreshing for over a minute.

The entries are already in a dictionary. How would I go about implementing your suggestion of only loading rows above and below the listindex? I apologize for being obtuse, this just isn’t something I’ve explored before. Are there any examples of this I could study?

Thanks again for your helpful answer.

This is what we refer to a data on demand in other threads.

Check this (scroll to the bottom for his latest code in that thread):

Yet another listbox on demand thread

@Tim Jones Thank you. I downloaded the test app, but it runs kind of weird and slow on my computer. I didn’t see any source code links or other examples of how it’s done. Did I miss something?

I’d prefer to use native listboxes because I’m doing retina UI with support for DarkMode, and native listboxes get all of that functionality “for free”.

When using the task method the UI update can only occur as fast as the timer fires. It takes over a minute because you’re only adding one row per UI update event and you have 100,000 rows. You will need to rework the update UI event to be able to add more than one row to the Listbox at a time.

I’ve seen this done by stacking the messages sent to the update UI method and dispatching them all at once. With this design the thread code can still process one row at a time, and the event handler for the timer action works through the stack of updates.

From the first post I understood that the same code works fast in 32bit and slow 64bit but then reading the comments from Jeff, Tim and Tim, it looks like some changes must be done to the code to make it work.

Can Perry clarify if we are talking about the same code with same Xojo version? Thanks.

Threading won’t make it faster. All that’s happening is while it’s populating the listbox, it takes regular breaks so the interface and other things can do things.

Are you making the listbox hidden when you add the data? I’d also recommend adding the data in chunks.

listbox.visible = false ~ add 10 rows listbox.visible = true

This will prevent it from updating for every single row added.

Modern macOS versions have 3 levels of on-screen caching now; screen caching, window caching and control caching. Each time a control is updated, it has to update all 3 of these caches.

I would really recommend you reconsider how you display the data. With this much data; I would suggest not using a listbox, instead use a canvas to fashion a listbox, that only displays the data needed. After all the data is in a dictionary. I would suggest you consider an array for more performance.

If you think about it; you have all the data in one place, when you add a row to a listbox, you’re copying the data into the listbox, then asking the listbox to update (which in turn updates 3 levels of screen caching).

If you resort to a display on demand ideology; you cut out the copying, cut out 300,000 refreshes also. In the paint event it simply displays the data from one position to another (only the rows that are actually visible). Yes; it’s a lot more work, but ultimately you’re going to end with a program that’s even faster than the 32-Bit version, and uses less memory.

100k rows aren’t really a problem. The listbox is too slow. NSTableView from MBS can do the 100k rows just fine. It can also the the “on-demand” loading that Sam described.

x-platform its a problem as NSTable doesnt work on Windows or Linux :stuck_out_tongue:

THIS is exactly the kind of thing Xojo should spend time on

[quote=446388:@Beatrix Willius]

100k rows aren’t really a problem. The listbox is too slow. NSTableView from MBS can do the 100k rows just fine. It can also the the “on-demand” loading that Sam described.[/quote]
Natively that’s how NSTableView works (AFAIK); it doesn’t keep a record of the data, just the positioning and what rows are visible. It’s then all down to the developer to supply the data when the Table needs it.

The ‘display it on demand’ idea using normal listbox goes like this:

Add the 100000 blank rows at design time, in the ‘initialvalue’ property.
Create the array of values at run time.

If your array is bigger than 100000, add some more rows
if it is smaller, delete a few rows

When the listbox displays, each row that is VISIBLE will trigger a celltextpaint event.
You can draw the text yourself in that paint event, (taking the value from the array which matches the row, if the row’s text is still empty. then Change the text of the row to the right value, (so if it appears again, you allow the normal display of text.)

In this way, no time is spent filling the list.

Mind you:

How does filtering work with a listbox of 100000 rows?
Do you ditch the contents and refill the list in response to a filter?
If so, the suggestion above is a non starter.

Instead, the NSTAbleview style approach (display on demand ideology) is a better fit, but the main problem with that is getting a usable vertical scroll bar.
Essentially, you have a list that only has (say) a fixed 20 rows.
You ‘know’ which row number is at the top
An associated scrollbar is used to get a new ‘top row’ value.
When the scroll bar is used, you refresh the 20 rows with values starting from your new ‘top row’ value.
At the simplest, no visible smooth scrolling occurs… values change in situ.

[quote=446393:@Jeff Tullin]When the scroll bar is used, you refresh the 20 rows with values starting from your new ‘top row’ value.
At the simplest, no visible smooth scrolling occurs… values change in situ.[/quote]
You can simulate smoother scrolling, by using a pixel offset.

The list on the popover, sort popping out of the sidebar, is a canvas (because of listbox location issues on a popover). I use the scrollbar to adjust the offset pixel value. I do some caching before hand so that each row knows where in the Y axis it is (if offset is zero).

With a bit of math, you can quickly find the “top” row, then loop down until the “last” row (where it has even 1 pixel in view). simply asking each row to draw at it’s own offset - the list offset.

Albeit; I’ll confess this list I’m showing only has 20’ish items at the moment, not 100,000. But in principle it shouldn’t be a problem, if all the rows are the same height; you can even skip the step of determining the Y location, because that can be easily calculated.

Hope this helps.

Yes it is bad UI design and it is why you are were you are. You can sometimes get away with this stuff for a while but eventually it will come back to haunt you. People can deal with between 5 to 7 items at a time and you can comfortably display maybe 100 items, so loading a 100k into a listbox isn’t ever going to be a great solution. The user defines how they expect the application to behave, not how you should implement it.

As someone else pointed out, the XOJO threading model will never make the overall process faster, it can make the UI more responsive while the process is being carried out, but that is all.

You need to decide between a better design and accepting that no matter how much tweaking you do on the current implementation, it always has the potential to blow up again with the next release of XOJO, the OS or a change request.

[quote=446391:@Jeff Tullin]The ‘display it on demand’ idea using normal listbox goes like this:

Add the 100000 blank rows at design time, in the ‘initialvalue’ property.
Create the array of values at run time.

If your array is bigger than 100000, add some more rows
if it is smaller, delete a few rows[/quote]

In my case, I want the user to be able to sort the listbox by clicking in the header. To me, this implies that the listbox already be completely populated. I use the CompareRows event for this.

Also I expect to be able to scroll the listbox as if it had the proper number of rows. In my case, that could be between zero and 10,000 or more, and is likely to change as the app runs - the user will click a button and entirely reload the listbox from a different data source.

[quote]When the listbox displays, each row that is VISIBLE will trigger a celltextpaint event.
You can draw the text yourself in that paint event, (taking the value from the array which matches the row, if the row’s text is still empty. then Change the text of the row to the right value, (so if it appears again, you allow the normal display of text.)

In this way, no time is spent filling the list.[/quote]

But is filling the listbox any slower than, when reading the data, filling an array, particularly if one uses the invisible/visible trick? The listbox is nice as it has some useful characteristics to take advantage of. Not much point in doing a lot of work to replace its abilities to avoid filling it up entirely, if that doesn’t speed up the app overall.

[quote=446455:@Tim Streater]In my case, I want the user to be able to sort the listbox by clicking in the header. To me, this implies that the listbox already be completely populated. I use the CompareRows event for this.
[/quote]

You can use the SortColumn even to implement your own sort.

Use paging and refresh buttons. This is a common technique and is well known to users.

And what do you do when it slows your app down, does not work in cases where the user does not have sufficient memory or tries to use your application when memory is low… and the end of the day only you can make the call. But if I’m expecting 10k of records, then I’ll code for 100k and so on, just so I won’t be back reworking any time soon.

Use SQL with LIMIT and ORDER BY to control the order and page size, and never load into the listbox any more than the user can see at one time (or at worst never more that 1k)