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();
}
8
  • 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. Commented Oct 15, 2021 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. Commented Oct 15, 2021 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: doc.qt.io/qt-5/… Commented Oct 15, 2021 at 19:12
  • @chehrlic you were correct, I spent the time getting up to speed with the address book example 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 Commented Oct 17, 2021 at 3: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? Commented Oct 17, 2021 at 17:14

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.