0

My C++ QTWidget application (both Qt 5.15.2 & also Qt 6.2.0(much worse)) are locking up during a large (~10,000 row) table (QTableView) update from a worker thread.

The rows are very simple and contain 7 string columns:

using GPSRecord = std::array<std::string, 7>;
using GPSData = std::vector<GPSRecord>;

I followed the documentation and some forum suggestions to try to make GUI responsive during the update - splitting the data from the worker into manageable chunks of 50 rows for example - emitting these in a loop until the entire table was sent to the main GUI.

The worker thread is as follows:

    // fire off a separate asynchronous thread to parse the flight plan
    // and create a simulated flight from the lambda parameters below.
    mFuture = std::async(std::launch::async,
        [this, xTrackMax, cruiseSpeed, cruiseAlt, gpsInterval, callSign, mode] {
            // note since this is created on a separate thread, we have to signal
            // batches of rows to the update the GUI thread.
            try {
                const auto flightPlan =
                    CoPilotUtils::importFlightPlan(*mFlightPlanPath);

                const auto flightTrackData =
                    CoPilotUtils::simulateFlight(*flightPlan, xTrackMax,
                        cruiseSpeed, cruiseAlt, gpsInterval, mode);

                GPSData gpsData;
                auto index = 0;
                const auto utcTime = floor<seconds>(system_clock::now());
                for (const auto& next : *flightTrackData) {
                    // "Timestamp", "UTC", "Callsign", "Position",
                    // "Altitude", "Speed", "Direction"
                    // "Timestamp"
                    const auto timeStamp = std::format("{:010}",
                        static_cast<uint32_t>(next.mUTCSecs));
                    // "UTC"
                    const auto utc = std::format("{:%FT%T}Z", utcTime);
                    // "Callsign"
                    // "Position"
                    const auto position = std::format("{:.6f},{:.6f}",
                        next.mLatitude, next.mLongitude);
                    // "Altitude"
                    const auto altitude = std::format("{}", static_cast<
                        uint32_t>(next.mAltitudeFeet));
                    // "Speed"
                    const auto speed = std::format("{}", static_cast<
                        uint32_t>(next.mSpeedKnots));
                    // "Direction"
                    const auto direction = std::format("{}", static_cast<
                        int32_t>(next.mTrueTrack));
                    gpsData.emplace_back(GPSRecord{
                        timeStamp, utc,
                        callSign.toStdString(),
                        position, altitude,
                        speed, direction });
                    if (gpsData.size() >= 50) {
                        // queued data from worker to GUI thread
                        emit flightRowsAdded(gpsData);
                        gpsData.clear();
                        // yield to allow UI to free up
                        //std::this_thread::sleep_for(milliseconds(10));
                    }
                    ++index;
                }
                if (!gpsData.empty()) {
                    emit flightRowsAdded(gpsData);
                    gpsData.clear();
                }

                // enable gui widgets upon successful import
                emit flightPlanImported(
                    std::format("{} imported with {} legs",
                        mFlightPlanPath->filename().string(),
                        flightPlan->size()).c_str());
            }
            catch (const UtlIOException& rEx) {
                // enable gui widgets upon failed import
                emit flightPlanImportFailed(
                    std::format("error: {}", rEx.what()).c_str());
            }
        });
}

During the signal/slot setup, I connect the worker thread's signal to a slot in the main GUI thread where the QStandardItemModel is updated. This uses a Qt::QueuedConnection connection, as these are separate threads and the data needs to be copied.

// Use Qt::QueuedConnection - copy the results to the GUI thread in a queue
connect(this, &MainWindow::flightRowsAdded,
    this, &MainWindow::updateFlightTable, Qt::QueuedConnection);

passing batches of rows (50 at a time) for insertion into the model - thus updating the view.

I tried inserting 50ms delays between the worker thread batch emits. I also tried inserting QCoreApplication::processEvents() in the slot where the model is updated (after processing the batch of 50 rows - this caused a crash), but neither of these helped to make the GUI responsive.

The GUI slot that performs the update is as follows

void
MainWindow::updateFlightTable(const GPSData& rGPSData)
{
    // "Timestamp", "UTC", "Callsign", "Position", "Altitude", "Speed", "Direction"
    auto index = mTableModel->rowCount();
    mTableModel->setRowCount(mTableModel->rowCount() +
        static_cast<int>(rGPSData.size()));
    for (const auto& nextRow : rGPSData) {
        // "Timestamp"
        mTableModel->setItem(index, 0, new QStandardItem(nextRow[0].c_str()));
        // "UTC"
        mTableModel->setItem(index, 1, new QStandardItem(nextRow[1].c_str()));
        // "Callsign"
        mTableModel->setItem(index, 2, new QStandardItem(nextRow[2].c_str()));
        // "Position"
        mTableModel->setItem(index, 3, new QStandardItem(nextRow[3].c_str()));
        // "Altitude"
        mTableModel->setItem(index, 4, new QStandardItem(nextRow[4].c_str()));
        // "Speed"
        mTableModel->setItem(index, 5, new QStandardItem(nextRow[5].c_str()));
        // "Direction"
        mTableModel->setItem(index, 6, new QStandardItem(nextRow[6].c_str()));
        // next row
        index++;
    }
    // try to handle background events accumulated between batch updates
    // neither of the 2 tricks below work - both crash
    //QCoreApplication::processEvents();
    //QCoreApplication::sendPostedEvents();
}
johnco3
  • 2,401
  • 4
  • 35
  • 67
  • Use a proper model derived from QAbstractTableModel instead a convenience model which is not made for such a huge amount of data. Creating a QStandardItem for every cell is not that efficient. – chehrlic Oct 15 '21 at 18:36
  • @cherhrlic I'm not sure how to optimize a QAbstractTableModel derived class to bypass creating a QStandardItem for every cell. Could you point me at an example that might be useful for this? Also I am not sure why the GUI locks up so much given that I batch things. I tried making smaller batch sizes but that did not help either - and I definitely confirmed that the worker thread is a separate thread from the main gui thread. – johnco3 Oct 15 '21 at 18:58
  • Since you do your own data storage in a derived QAbstractTableModel there are no QStandardItemModels. How to create a custom model is explained here: https://doc.qt.io/qt-5/model-view-programming.html#model-subclassing-reference – chehrlic Oct 15 '21 at 19:12
  • @chehrlic you were correct, I spent the time getting up to speed with the address book example https://doc.qt.io/qt-5/qtwidgets-itemviews-addressbook-example.html, this is based on a custom QAbstractTableModel (which seemed more appropriate) and I improved the performance significantly - 3 seconds to display 26k rows (7 columns/row). Shame there is no way to set an entire row in 1 go (instead I have to call setData 7 times per row). Also there seems to be a problem when I call model->removeRows(0, myModel->rowCount()) in my application when the model contains no rows. beginRemoveRows asserts – johnco3 Oct 17 '21 at 03:30
  • `(instead I have to call setData 7 times per row)`. Why not simply add a function to your custom class which simply sets the whole data at once? – chehrlic Oct 17 '21 at 17:14
  • I guess I could do that I think I need to emit dataChanged(startIndex, endIndex, { Qt::DisplayRole, Qt::EditRole }); with startIndex pointing to the first column and endIndex pointing to the last column in order to inform the views about which columns changed – johnco3 Oct 17 '21 at 17:36
  • No, when you add rows you should use begin/endInsertRows() – chehrlic Oct 18 '21 at 17:30
  • @chehrlic Thanks I did that and its working great now. Thanks for suggesting the rewrite of the model. – johnco3 Oct 18 '21 at 17:32

0 Answers0