From 19f7ec5bf97712efd3a4d5ac6faa3c5524012f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Mon, 24 Aug 2026 19:54:37 +0200 Subject: [PATCH] [VDS] Bound pending-load drain by time and make row lookups O(1) The fixed DECK_LOADS_PER_TURN = 24 cap had no measured basis. It was guessed and existed because every applied load emitted dataChanged into each DeckPreviewWidget, whose handler resolved its own row with an O(n) linear scan per widget. The model now maintains a file path -> row hash kept in sync across scans, renames and deletions, so rowForFilePath is O(1) and the fan-out cost is gone at its source. The drain applies finished loads until a small time budget per event loop turn runs out, so throughput self-tunes instead of relying on an arbitrary count. --- .../visual_deck_storage_model.cpp | 56 ++++++++++++++----- .../visual_deck_storage_model.h | 3 + 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp index a771136e1..5d0006539 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -36,13 +37,13 @@ struct DeckLoadResult }; /** - * @brief How many finished background deck loads one event loop turn applies. + * @brief How long per event loop turn the pending-load drain applies finished loads. * - * Applying a load emits dataChanged and wakes up the proxies, which is not - * free, and the bound keeps a burst of simultaneous completions from stalling - * the UI thread. + * Applying a load updates the row and wakes up the proxies and views, which is + * not free. A time budget instead of a fixed count lets fast machines apply + * many loads in one turn while keeping the ui thread responsive everywhere. */ -constexpr int DECK_LOADS_PER_TURN = 24; +constexpr int LOAD_DRAIN_TIME_BUDGET_MS = 8; /** * @brief The path of \a path relative to the deck root, or empty if \a path @@ -194,12 +195,22 @@ const LoadedDeck &VisualDeckStorageModel::deckForRow(int row) const int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const { - for (int i = 0; i < decks.size(); ++i) { - if (decks.at(i).filePath == filePath) { - return i; + return rowByFilePath.value(filePath, -1); +} + +/** + * @brief Rebuilds the file path -> row index from scratch after bulk changes. + */ +void VisualDeckStorageModel::reindexFilePaths() +{ + rowByFilePath.clear(); + for (int row = 0; row < decks.size(); ++row) { + const QString &filePath = decks.at(row).filePath; + // First row wins, mirroring what a linear scan would return for duplicates. + if (!rowByFilePath.contains(filePath)) { + rowByFilePath.insert(filePath, row); } } - return -1; } void VisualDeckStorageModel::startScan() @@ -207,6 +218,7 @@ void VisualDeckStorageModel::startScan() ++scanGeneration; beginResetModel(); decks.clear(); + rowByFilePath.clear(); folderPaths.clear(); pendingLoads.clear(); endResetModel(); @@ -237,6 +249,7 @@ void VisualDeckStorageModel::startScan() beginInsertRows(QModelIndex(), 0, result.decks.size() - 1); decks = result.decks; + reindexFilePaths(); endInsertRows(); for (int row = 0; row < decks.size(); ++row) { @@ -311,8 +324,10 @@ void VisualDeckStorageModel::drainPendingLoads() { drainScheduled = false; - int applied = 0; - while (!pendingLoads.isEmpty() && applied < DECK_LOADS_PER_TURN) { + QElapsedTimer timer; + timer.start(); + + while (!pendingLoads.isEmpty()) { PendingDeckLoad pending = pendingLoads.takeFirst(); if (pending.generation != scanGeneration) { @@ -328,7 +343,6 @@ void VisualDeckStorageModel::drainPendingLoads() data.loadInProgress = false; if (!pending.ok) { - ++applied; continue; // Leave the row unloaded so it stays visible without deck data. } @@ -340,7 +354,12 @@ void VisualDeckStorageModel::drainPendingLoads() emit dataChanged(index(row), index(row)); emit deckLoaded(row); - ++applied; + + // Checked after applying at least one load, so a single slow application + // still makes progress instead of starving the queue. + if (timer.elapsed() >= LOAD_DRAIN_TIME_BUDGET_MS) { + break; + } } if (!pendingLoads.isEmpty()) { @@ -406,9 +425,13 @@ void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePa } DeckPreviewData &data = decks[row]; + rowByFilePath.remove(data.filePath); data.filePath = newFilePath; data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath); data.folderPath = folderPathFor(newFilePath, deckPath); + if (!rowByFilePath.contains(newFilePath)) { + rowByFilePath.insert(newFilePath, row); + } } bool VisualDeckStorageModel::renameDeck(int row, const QString &newName) @@ -472,7 +495,14 @@ bool VisualDeckStorageModel::deleteFile(int row) } beginRemoveRows(QModelIndex(), row, row); + rowByFilePath.remove(filePath); decks.removeAt(row); + // Rows after the deleted one shift down by one. + for (auto it = rowByFilePath.begin(); it != rowByFilePath.end(); ++it) { + if (it.value() > row) { + --it.value(); + } + } endRemoveRows(); return true; } diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h index e47edd052..330205356 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_model.h @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -166,6 +167,7 @@ private: void startScan(); void beginLoad(int row); static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true); + void reindexFilePaths(); void schedulePendingLoadDrain(); private slots: @@ -176,6 +178,7 @@ private: QString deckPath; QList decks; + QHash rowByFilePath; ///< Maps each deck's file path to its row for O(1) lookups. QStringList folderPaths; ///< All subdirectories of the deck folder, sorted. int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored. QVector pendingLoads; ///< Finished background loads waiting to be applied.