UI Responsiveness and QListView updating during load of large data into model
-
Hi! I have an application where the user is presented with a listview which can be populated with different data sets. After the user selects a dataset, the model is filled. This takes a fair amount of time as data is read, decrypted and converted and interpreted in various ways. During this time the UI is frozen. My current code looks something like this:
MyQStandardItemModel::setModelData(SelectedData) { QList<QStandardItem *> items; // this creates a bunch of thread that do the decryption and other processing needed on SelectedData spawnWorkerThreads(items, SelectedData); waitForThreadsToFinishLoadingData(); // actually append the items to the model for (auto i : items) appendRow(i); }
Note: The threads do not simply call appendRow() themselves as this can only be done from the main thread. Also calling QStandardItem::setData() from other threads is unsafe if the item is already inserted in the model. To speed things up, I could of course append items as the threads are finishing them, instead of waiting for the last thread to finish, but with the current behavior (freezing until the entire setModelData() function returns) it would not make a difference.
My problem is that the dataset can get so big, it could take up to 5 seconds to run this single function. During this time, the UI is frozen. What I find most strange is that no items are actually visibly appended to the QListView during the last for-loop, they only appear after the function retruns.. I would have thought
QStandardItemModel::appendRow()
would signal the view to update (and that signal-slot connection result in direct function calls). But since nothing is happening, I thought maybe the ListView is updated through an Event and the event loop is only continuing after the function completes. However, calling qApp->processEvents() during the for-loop also has no effect.What is a nice way to have a responsive UI during the loading of model data? How do I get it to show at least the first couple (let's say ten) items as they are appended? This would make the application feel a lot snappier as the first ten item would be available in a fraction of a second.
Thanks!
-
Definitely UI will freeze becz of waitForThreadsToFinishLoadingData(); You are waiting for all the threads to collect the data. If this is the case there is no point in having different threads. You have written your model. Have you looked at signals like dataChanged(...) etc for the model ?
Very similar discussion in the thread
-
@dheerendra There is definitely a point to the different threads, all these threads are preparing the data for different items. If I do this all sequentially in a single thread, the five seconds load time could turn into almost 40 (assuming 8 threads).
The threading part is not really the problem I think, I would have the same problem without the threads, only the ui freezing would be much longer. Hypothetically, let's say I did not run any threads, and the data was instantly available, but the number of items added in the for-loop was in the billions, it would still freeze until the entire loop ends, instead of showing a few items in the listview after calling appendRow() a few times.
I have looked at the signals in the model but can't find anything that helps me out. There's begin/endInsertRows() but I believe appendRow() is supposed to call those internally. I believe dataChanged() is used when existing data changes, not when new data or items are added.
I've thought about putting the entire function as it is now on a separate thread and appending the items posting a custom event, but I'm not sure it will solve my problem.
Thanks for the reply btw, I will now read through the link you mentioned.
-
Hi,
Then you should handle the update by batch rather than all at once.
Billions of items ? What are these ? Do you really need them all in memory ?
-
You should use concurrent execution in your case. No question about this. What I'm telling is that inside your processing app is waiting for threads to complete. Since app is waiting, UI will freeze for sure.
Now, it is best to avoid loading huge data in model. Model is not a data store. Better restrict the model to load only portion of the data which makes sense in UI. Entire data should be either in db/file/in-memory & it is your decision. You need to decide where to keep it. From your description, you have billions of records. I prefer to avoid load everything in-memory. It is almost like implementing sliding window - small portion of data which is exposed to model from datastore.
All this comes to writing your own logic work with huge data set.
-
Thanks for all the responses so far. I'm sorry I feel I haven't been able to make clear what my problem is exactly, and I think I have not used good examples. The threaded version is just what the function looks like now, after optimizing for speed a bit. And the billions of items mentioned previously were just hypothetical to make a point, I'm not really adding that many items.
So let's forget about the threading and the millions of items and get back to the original function I had before optimizing, and use more realistic numbers.
setModelData() { for (int i = 0; i < 1000; ++i) { loadSingleRowOfData(i); // takes just 5ms appendRow(i); } } // *
Now, after just 5ms, the user could be presented with an item in the listview (there is data in the model) . If the view can show (depending on its size) 10 or 15 items, the entire listview can be (visually) filled in a fraction of a second. However, currently the listview will stay empty for a full five seconds until the point marked *, then suddenly the entire view fills at once.
I have tried emitting various signals after appendRow() as well as calling qApp->processEvents(), but haven't gotten anything to work.
I thought about changing the above into
setModelData() { startThreadToLoadModel(); } // return almost immediately startThreadToLoadData() // runs own thread { for (int i = 0; i < 1000; ++i) { loadSingleRowOfData(i); // takes just 5ms postEvent(SingleItemReadyEvent); } }
And then, of course, catch the SingleItemReadyEvent in the eventfilter of the model and call appendRow() from there. Because the rewrite takes some time I wanted to ask if this would work at all? If it is a dumb solution because there is something simpler? And, not very important but out of curiosity, why does the original code (non-threaded, in this post) behave like this, and can it somehow be made to update the view after each appendRow()?
Thanks!
-
Well I just built two dummy examples based on the code above and I think they illustrate my problem and my solution:
// main.cc #include <QtWidgets> #include "myitemmodel.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget widget; QVBoxLayout *layout = new QVBoxLayout(&widget); QListView *listview = new QListView(&widget); QPushButton *button = new QPushButton("Populate list", &widget); MyItemModel *model = new MyItemModel; listview->setModel(model); QObject::connect(button, &QPushButton::clicked, model, &MyItemModel::loadData); layout->addWidget(listview); layout->addWidget(button); widget.show(); return app.exec(); }
// myitemmodel.h #ifndef MYITEMMODEL_H_ #define MYITEMMODEL_H_ #include <QtWidgets> class MyItemModel : public QStandardItemModel { Q_OBJECT; public slots: inline void loadData(); }; inline void MyItemModel::loadData() { clear(); for (uint i = 0; i < 1000; ++i) { qDebug() << "Creating item" << i << "... "; QStandardItem *item = new QStandardItem; item->setData("This is item " + QString::number(i), Qt::DisplayRole); QThread::msleep(5); qDebug() << "done! Appending!"; appendRow(item); } } #endif
In this version the UI freezes for a full 5 seconds after clicking the button, after which all items appear in the listview at once. During this time the listview can not be scrolled (even if it was already filled). Replacing this
myitemmodel.h
file with the following version (as I suggested in my previous post) seems to fix the problem:#ifndef MYITEMMODEL_H_ #define MYITEMMODEL_H_ #include <QtWidgets> class ItemAvailableEvent : public QEvent { QStandardItem *d_item; public: ItemAvailableEvent(QStandardItem *item) : QEvent(QEvent::Type(QEvent::User + 1)), d_item(item) {} QStandardItem *item() const { return d_item; } }; class MyItemModel : public QStandardItemModel { Q_OBJECT; public slots: inline void loadData(); private: inline void loadDataWorker(); protected: inline bool event(QEvent *event) override; }; inline void MyItemModel::loadData() { clear(); // here, I should cancel any running threads and wait for them to stop // start thread to load data QThread *thread = QThread::create([=]{loadDataWorker();}); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } inline void MyItemModel::loadDataWorker() { for (uint i = 0; i < 1000; ++i) { // if (canceled) // break; qDebug() << "Creating item" << i << "... "; QStandardItem *item = new QStandardItem; item->setData("This is item " + QString::number(i), Qt::DisplayRole); QThread::msleep(5); qDebug() << "done! Posting Event!"; QCoreApplication::postEvent(this, new ItemAvailableEvent(item)); } } inline bool MyItemModel::event(QEvent *event) { if (event->type() == QEvent::User + 1) { qDebug() << "Got event! Appending row"; appendRow(static_cast<ItemAvailableEvent *>(event)->item()); return true; } return QStandardItemModel::event(event); } #endif
Obviously it is a somewhat quick (= unsafe) example as clicking the button several times fast enough will spawn any number of threads with no way of cancelling them, I don't even keep the pointer to the thread. But items appear in the list almost immediately and the list can be scrolled normally while still loading the data.
Both versions here should compile with
qmake -project QT+=gui QT+=widgets qmake make
Are there any drawbacks to this? Or is this an overly complicated way to fix a simple problem?
Thanks!
-
You can try with simple Model APIs only. You can look at the examples at GIT. Just see whether you are looking for this kind of example.
-
Thanks! As far as I can see, our solutions are nearly identical. Where I use events, your code has a signal/slot-connection between the threads, but since this is a
Qt::QueuedConnection
it should be handled the same as the event (queued and only in the event-loop).I don't know how to pick one of these solutions over the other, if anyone has any compelling argument to go with a specific solution I'd be happy to hear it, otherwise I'll just look at my code and see which is easier to implement.
Thanks again!
-
Boiler plate code which you are trying to implement is already taken care by Qt using Signals/Slots across threads. It is better to use tried & tested inbuilt mechanism. It is good with code maintainability & readability as well. Suggest to use signals/slots.