mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
[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.
This commit is contained in:
parent
717a2deff8
commit
19f7ec5bf9
2 changed files with 46 additions and 13 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
|
|
@ -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
|
* Applying a load updates the row and wakes up the proxies and views, which is
|
||||||
* free, and the bound keeps a burst of simultaneous completions from stalling
|
* not free. A time budget instead of a fixed count lets fast machines apply
|
||||||
* the UI thread.
|
* 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
|
* @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
|
int VisualDeckStorageModel::rowForFilePath(const QString &filePath) const
|
||||||
{
|
{
|
||||||
for (int i = 0; i < decks.size(); ++i) {
|
return rowByFilePath.value(filePath, -1);
|
||||||
if (decks.at(i).filePath == filePath) {
|
}
|
||||||
return i;
|
|
||||||
|
/**
|
||||||
|
* @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()
|
void VisualDeckStorageModel::startScan()
|
||||||
|
|
@ -207,6 +218,7 @@ void VisualDeckStorageModel::startScan()
|
||||||
++scanGeneration;
|
++scanGeneration;
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
decks.clear();
|
decks.clear();
|
||||||
|
rowByFilePath.clear();
|
||||||
folderPaths.clear();
|
folderPaths.clear();
|
||||||
pendingLoads.clear();
|
pendingLoads.clear();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
|
|
@ -237,6 +249,7 @@ void VisualDeckStorageModel::startScan()
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), 0, result.decks.size() - 1);
|
beginInsertRows(QModelIndex(), 0, result.decks.size() - 1);
|
||||||
decks = result.decks;
|
decks = result.decks;
|
||||||
|
reindexFilePaths();
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
for (int row = 0; row < decks.size(); ++row) {
|
for (int row = 0; row < decks.size(); ++row) {
|
||||||
|
|
@ -311,8 +324,10 @@ void VisualDeckStorageModel::drainPendingLoads()
|
||||||
{
|
{
|
||||||
drainScheduled = false;
|
drainScheduled = false;
|
||||||
|
|
||||||
int applied = 0;
|
QElapsedTimer timer;
|
||||||
while (!pendingLoads.isEmpty() && applied < DECK_LOADS_PER_TURN) {
|
timer.start();
|
||||||
|
|
||||||
|
while (!pendingLoads.isEmpty()) {
|
||||||
PendingDeckLoad pending = pendingLoads.takeFirst();
|
PendingDeckLoad pending = pendingLoads.takeFirst();
|
||||||
|
|
||||||
if (pending.generation != scanGeneration) {
|
if (pending.generation != scanGeneration) {
|
||||||
|
|
@ -328,7 +343,6 @@ void VisualDeckStorageModel::drainPendingLoads()
|
||||||
data.loadInProgress = false;
|
data.loadInProgress = false;
|
||||||
|
|
||||||
if (!pending.ok) {
|
if (!pending.ok) {
|
||||||
++applied;
|
|
||||||
continue; // Leave the row unloaded so it stays visible without deck data.
|
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 dataChanged(index(row), index(row));
|
||||||
emit deckLoaded(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()) {
|
if (!pendingLoads.isEmpty()) {
|
||||||
|
|
@ -406,9 +425,13 @@ void VisualDeckStorageModel::setFilePathForRow(int row, const QString &newFilePa
|
||||||
}
|
}
|
||||||
|
|
||||||
DeckPreviewData &data = decks[row];
|
DeckPreviewData &data = decks[row];
|
||||||
|
rowByFilePath.remove(data.filePath);
|
||||||
data.filePath = newFilePath;
|
data.filePath = newFilePath;
|
||||||
data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath);
|
data.relativeFilePath = relativeFilePathFor(newFilePath, deckPath);
|
||||||
data.folderPath = folderPathFor(newFilePath, deckPath);
|
data.folderPath = folderPathFor(newFilePath, deckPath);
|
||||||
|
if (!rowByFilePath.contains(newFilePath)) {
|
||||||
|
rowByFilePath.insert(newFilePath, row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VisualDeckStorageModel::renameDeck(int row, const QString &newName)
|
bool VisualDeckStorageModel::renameDeck(int row, const QString &newName)
|
||||||
|
|
@ -472,7 +495,14 @@ bool VisualDeckStorageModel::deleteFile(int row)
|
||||||
}
|
}
|
||||||
|
|
||||||
beginRemoveRows(QModelIndex(), row, row);
|
beginRemoveRows(QModelIndex(), row, row);
|
||||||
|
rowByFilePath.remove(filePath);
|
||||||
decks.removeAt(row);
|
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();
|
endRemoveRows();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
|
@ -166,6 +167,7 @@ private:
|
||||||
void startScan();
|
void startScan();
|
||||||
void beginLoad(int row);
|
void beginLoad(int row);
|
||||||
static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true);
|
static void recomputeDeckMetadata(DeckPreviewData &data, bool recomputeColorIdentity = true);
|
||||||
|
void reindexFilePaths();
|
||||||
void schedulePendingLoadDrain();
|
void schedulePendingLoadDrain();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
@ -176,6 +178,7 @@ private:
|
||||||
|
|
||||||
QString deckPath;
|
QString deckPath;
|
||||||
QList<DeckPreviewData> decks;
|
QList<DeckPreviewData> decks;
|
||||||
|
QHash<QString, int> rowByFilePath; ///< Maps each deck's file path to its row for O(1) lookups.
|
||||||
QStringList folderPaths; ///< All subdirectories of the deck folder, sorted.
|
QStringList folderPaths; ///< All subdirectories of the deck folder, sorted.
|
||||||
int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored.
|
int scanGeneration = 0; ///< Bumped on every scan so stale results are ignored.
|
||||||
QVector<PendingDeckLoad> pendingLoads; ///< Finished background loads waiting to be applied.
|
QVector<PendingDeckLoad> pendingLoads; ///< Finished background loads waiting to be applied.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue