[Card Database] Improve loading times through binary cache (#7051)

* [Card Database] Improve loading times through binary cache

Took 10 minutes


Took 9 minutes

Took 16 seconds

* [Card Database] Remove lib qt include

Took 18 minutes


Took 14 seconds

* Downgrade to 6.3 datastream

Took 5 minutes

* go up to 6.4 datastream

Took 1 minute

* Address comments

* Small bug fixes

Took 20 minutes


Took 10 seconds

* More fixes.

Took 4 minutes


Took 4 seconds

* Even more fixes.

Took 11 minutes


Took 4 seconds

* More fixes.

Took 6 minutes

Took 26 seconds

Took 8 minutes

* Namespace instead of class

Took 6 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-07-27 23:12:53 +02:00 committed by GitHub
parent 4bb8831531
commit 749223c2dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1604 additions and 106 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ mysql.cnf
.DS_Store
.idea/
*.aps
*.cache
cmake-build*
preferences
compile_commands.json

View file

@ -541,6 +541,12 @@ MainWindow::MainWindow(QWidget *parent)
void MainWindow::startupConfigCheck()
{
// checkUnknownSets() is intentionally deferred from the card database load
// (which runs in main() before MainWindow exists) so that
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
// receivers when emitted.
CardDatabaseManager::getInstance()->checkUnknownSets();
if (SettingsCache::instance().debug().getLocalGameOnStartup()) {
LocalGameOptions options;
options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount();
@ -571,8 +577,6 @@ void MainWindow::startupConfigCheck()
<< "differs, assuming first start after update";
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
alertForcedOracleRun(VERSION_STRING, true);
} else {
const auto reloadOk0 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
}
qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected.";
@ -629,8 +633,6 @@ void MainWindow::startupConfigCheck()
}
}
const auto reloadOk1 = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
// Run the tips dialog only on subsequent startups.
// On the first run after an install/update the startup is already crowded enough
if (tip->successfulInit && SettingsCache::instance().personal().getShowTipsOnStartup() &&

View file

@ -258,6 +258,19 @@ int main(int argc, char *argv[])
qCInfo(MainLog) << "Starting main program";
// Front-load the card database before constructing the main window. The
// binary-cache read is cheap when uncontended (~1s); doing it here -- while no
// GUI work yet competes for CPU -- avoids the multi-second, GUI-freezing
// contention that happens when the load runs alongside window construction.
// The CardDatabaseModel populates from the already-loaded data in its
// constructor, so the window appears fully populated with no startup lag.
// Note: checkUnknownSets() is deferred from the initial load to
// MainWindow::startupConfigCheck() so that
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled have live
// receivers when emitted. Subsequent reloads (e.g. path changes) call
// checkUnknownSets() directly from the loader after the first load.
CardDatabaseManager::getInstance()->loadCardDatabases();
MainWindow ui;
if (parser.isSet("connect")) {
ui.setConnectTo(parser.value("connect"));

View file

@ -31,6 +31,7 @@ add_library(
libcockatrice/card/card_info.cpp
libcockatrice/card/card_info_comparator.cpp
libcockatrice/card/database/card_database.cpp
libcockatrice/card/database/card_database_cache.cpp
libcockatrice/card/database/card_database_loader.cpp
libcockatrice/card/database/card_database_manager.cpp
libcockatrice/card/database/card_database_querier.cpp

View file

@ -5,7 +5,10 @@
#include "relation/card_relation.h"
#include "set/card_set.h"
#include <QDataStream>
#include <QDir>
#include <QMutex>
#include <QMutexLocker>
#include <QRegularExpression>
#include <QSharedPointer>
#include <QString>
@ -19,6 +22,57 @@ class CardInfo;
using CardInfoPtr = QSharedPointer<CardInfo>;
namespace
{
QByteArray serializeProperties(const QVariantHash &props)
{
QByteArray blob;
QDataStream out(&blob, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_6_4);
out << props;
return blob;
}
} // namespace
void CardInfo::ensurePropertiesLoaded() const
{
QMutexLocker lock(&propertiesMutex);
if (propertiesLoaded) {
return;
}
if (!propertiesBlob.isEmpty()) {
QDataStream in(propertiesBlob);
in.setVersion(QDataStream::Qt_6_4);
in >> propertiesCache;
}
propertiesLoaded = true;
}
const QVariantHash &CardInfo::getPropertiesHash() const
{
ensurePropertiesLoaded();
return propertiesCache;
}
void CardInfo::setProperty(const QString &_name, const QString &_value)
{
ensurePropertiesLoaded();
if (propertiesCache.value(_name).toString() == _value) {
return;
}
propertiesCache.insert(_name, _value);
propertiesBlob = serializeProperties(propertiesCache);
emit cardInfoChanged(smartThis);
}
void CardInfo::setProperties(const QVariantHash &_props)
{
ensurePropertiesLoaded();
propertiesCache = _props;
propertiesBlob = serializeProperties(propertiesCache);
emit cardInfoChanged(smartThis);
}
CardInfo::CardInfo(const QString &_name,
const QString &_text,
bool _isToken,
@ -27,14 +81,39 @@ CardInfo::CardInfo(const QString &_name,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes)
: name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards),
: name(_name), text(_text), isToken(_isToken), relatedCards(_relatedCards),
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes)
{
propertiesCache = std::move(_properties);
propertiesBlob = serializeProperties(propertiesCache);
propertiesLoaded = true;
simpleName = CardInfo::simplifyName(name);
refreshCachedSets();
}
CardInfo::CardInfo(const QString &_name,
const QString &_text,
bool _isToken,
QByteArray _propertiesBlob,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes,
QString _simpleName,
QSet<QString> _altNames)
: name(_name), simpleName(std::move(_simpleName)), text(_text), isToken(_isToken),
propertiesBlob(std::move(_propertiesBlob)), relatedCards(_relatedCards),
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes),
altNames(std::move(_altNames))
{
// propertiesBlob is materialized lazily on first query; simpleName and
// altNames are supplied by the caller (binary cache). Only the set-name
// display list still depends on the current enabled-set state.
refreshCachedSetNames();
}
CardInfoPtr CardInfo::newInstance(const QString &_name)
{
return newInstance(_name, "", false, {}, {}, {}, {}, {});
@ -63,6 +142,35 @@ CardInfoPtr CardInfo::newInstance(const QString &_name,
return ptr;
}
CardInfoPtr CardInfo::newInstance(const QString &_name,
const QString &_text,
bool _isToken,
QByteArray _propertiesBlob,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
const UiAttributes _uiAttributes,
QString _simpleName,
QSet<QString> _altNames,
bool _appendToSets)
{
CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_propertiesBlob), _relatedCards,
_reverseRelatedCards, _sets, _uiAttributes, std::move(_simpleName),
std::move(_altNames)));
ptr->setSmartPointer(ptr);
if (_appendToSets) {
for (const auto &printings : _sets) {
for (const PrintingInfo &printing : printings) {
printing.getSet()->append(ptr);
break;
}
}
}
return ptr;
}
QString CardInfo::getCorrectedName() const
{
// remove all the characters reserved in windows file paths,
@ -104,13 +212,16 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info)
void CardInfo::combineLegalities(const QVariantHash &props)
{
ensurePropertiesLoaded();
QHashIterator<QString, QVariant> it(props);
while (it.hasNext()) {
it.next();
if (it.key().startsWith("format-")) {
smartThis->setProperty(it.key(), it.value().toString());
propertiesCache.insert(it.key(), it.value());
}
}
propertiesBlob = serializeProperties(propertiesCache);
emit cardInfoChanged(smartThis);
}
void CardInfo::refreshCachedSets()

View file

@ -10,6 +10,7 @@
#include <QLoggingCategory>
#include <QMap>
#include <QMetaType>
#include <QMutex>
#include <QSharedPointer>
#include <QVariant>
#include <utility>
@ -74,7 +75,20 @@ private:
QString simpleName; ///< Simplified name for fuzzy matching.
QString text; ///< Text description or rules text of the card.
bool isToken; ///< Whether this card is a token or not.
QVariantHash properties; ///< Key-value store of dynamic card properties.
// Properties are stored as a pre-serialized blob (cheap to load) and the
// QVariantHash is materialized on first query, so database load avoids
// constructing thousands of QVariants per card.
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
mutable QVariantHash propertiesCache; ///< Materialized properties (query form).
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
mutable QMutex propertiesMutex; ///< Guards lazy materialization.
/**
* @brief Materializes propertiesCache from propertiesBlob if not already done.
* Safe to call from const getters (members are mutable).
*/
void ensurePropertiesLoaded() const;
QList<CardRelation *> relatedCards; ///< Forward references to related cards.
QList<CardRelation *> reverseRelatedCards; ///< Cards that refer back to this card.
QList<CardRelation *> reverseRelatedCardsToMe; ///< Cards that consider this card as related.
@ -106,6 +120,38 @@ public:
SetToPrintingsMap _sets,
UiAttributes _uiAttributes);
/**
* @brief Constructs a CardInfo from a cache snapshot with precomputed derived
* state.
*
* Used by the binary cache reader to skip recomputing @p _simpleName and
* @p _altNames (which otherwise require a Unicode normalization and a full
* printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob
* so the QVariantHash is not built at load time (it is materialized on first
* query).
*
* @param _name The card name.
* @param _text Rules text or description of the card.
* @param _isToken Token flag.
* @param _propertiesBlob Pre-serialized properties blob (as written by the cache).
* @param _relatedCards Forward relationships.
* @param _reverseRelatedCards Reverse relationships.
* @param _sets Printing information per set.
* @param _uiAttributes Attributes that affect display and game logic.
* @param _simpleName Precomputed simplified name.
* @param _altNames Precomputed alternate names.
*/
explicit CardInfo(const QString &_name,
const QString &_text,
bool _isToken,
QByteArray _propertiesBlob,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
UiAttributes _uiAttributes,
QString _simpleName,
QSet<QString> _altNames);
/**
* @brief Copy constructor for CardInfo.
*
@ -115,7 +161,7 @@ public:
*/
CardInfo(const CardInfo &other)
: QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text),
isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards),
isToken(other.isToken), propertiesBlob(other.propertiesBlob), relatedCards(other.relatedCards),
reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe),
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
altNames(other.altNames)
@ -154,6 +200,38 @@ public:
SetToPrintingsMap _sets,
UiAttributes _uiAttributes);
/**
* @brief Creates a new instance from a cache snapshot with precomputed
* derived state.
*
* @param _name Name of the card.
* @param _text Rules text or description.
* @param _isToken Token flag.
* @param _propertiesBlob Pre-serialized properties blob (as written by the cache).
* @param _relatedCards Forward relationships.
* @param _reverseRelatedCards Reverse relationships.
* @param _sets Printing information per set.
* @param _uiAttributes Attributes that affect display and game logic.
* @param _simpleName Precomputed simplified name.
* @param _altNames Precomputed alternate names.
* @param _appendToSets When true (default), the card is appended to each of
* its CardSets. Pass false when building cards in parallel so the
* (non-thread-safe) set membership is populated in a later
* single-threaded pass.
* @return Shared pointer to the new CardInfo instance.
*/
static CardInfoPtr newInstance(const QString &_name,
const QString &_text,
bool _isToken,
QByteArray _propertiesBlob,
const QList<CardRelation *> &_relatedCards,
const QList<CardRelation *> &_reverseRelatedCards,
SetToPrintingsMap _sets,
UiAttributes _uiAttributes,
QString _simpleName,
QSet<QString> _altNames,
bool _appendToSets = true);
/**
* @brief Clones the current CardInfo instance.
*
@ -208,20 +286,32 @@ public:
}
[[nodiscard]] QStringList getProperties() const
{
return properties.keys();
return getPropertiesHash().keys();
}
[[nodiscard]] const QVariantHash &getPropertiesHash() const;
/**
* @brief Stores the pre-serialized properties blob and invalidates the
* materialized cache. Used by the binary cache reader so the
* QVariantHash is not built at load time.
* @param _blob The serialized properties (as written by the cache writer).
*/
void setPropertiesBlob(QByteArray _blob) const
{
QMutexLocker lock(&propertiesMutex);
propertiesBlob = std::move(_blob);
propertiesLoaded = false;
propertiesCache.clear();
}
[[nodiscard]] QString getProperty(const QString &propertyName) const
{
return properties.value(propertyName).toString();
}
void setProperty(const QString &_name, const QString &_value)
{
properties.insert(_name, _value);
emit cardInfoChanged(smartThis);
return getPropertiesHash().value(propertyName).toString();
}
void setProperty(const QString &_name, const QString &_value);
void setProperties(const QVariantHash &_props);
[[nodiscard]] bool hasProperty(const QString &propertyName) const
{
return properties.contains(propertyName);
return getPropertiesHash().contains(propertyName);
}
[[nodiscard]] const SetToPrintingsMap &getSets() const
{

View file

@ -20,6 +20,7 @@ CardDatabase::CardDatabase(QObject *parent,
{
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
qRegisterMetaType<CardDatabaseData>("CardDatabaseData");
// create loader and wire it up
loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController);
@ -28,6 +29,10 @@ CardDatabase::CardDatabase(QObject *parent,
connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed);
connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound);
connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled);
// swap the finished snapshot into the live database. Uses AutoConnection so
// that cross-thread loads are delivered via the event loop while same-thread/
// synchronous callers get the swap immediately.
connect(loader, &CardDatabaseLoader::databaseDataReady, this, &CardDatabase::swapInDatabaseData);
querier = new CardDatabaseQuerier(this, this, prefs);
}
@ -66,7 +71,12 @@ void CardDatabase::reloadCardDatabasesAndNotify()
loadCardDatabases();
if (loadStatus == Ok) {
notifyEnabledSetsChanged();
checkUnknownSets();
// A reload reconstructs the exact same enabled-set state, so the cached
// set-name / alt-name data computed during construction is still valid.
// Skipping the per-card refresh avoids a full 36k-card recompute that
// would otherwise duplicate the work already done while building cards.
notifyEnabledSetsChanged(false);
}
}
@ -77,13 +87,18 @@ bool CardDatabase::saveCustomTokensToFile()
void CardDatabase::refreshCachedReverseRelatedCards()
{
for (const auto &card : cards) {
refreshCachedReverseRelatedCards(cards);
}
void CardDatabase::refreshCachedReverseRelatedCards(CardNameMap &cardMap)
{
for (const auto &card : cardMap) {
card->resetReverseRelatedCards2Me();
}
for (const auto &card : cards) {
for (const auto &card : cardMap) {
for (auto *rel : card->getReverseRelatedCards()) {
if (auto target = cards.value(rel->getName())) {
if (auto target = cardMap.value(rel->getName())) {
auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(),
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
rel->getIsFaceDown());
@ -205,12 +220,16 @@ void CardDatabase::markAllSetsAsKnown()
_sets.markAllAsKnown();
}
void CardDatabase::notifyEnabledSetsChanged()
void CardDatabase::notifyEnabledSetsChanged(bool recomputeCachedSets)
{
// refresh the list of cached set names
if (recomputeCachedSets) {
// refresh the list of cached set names / alt names for every card; this is
// only needed when the enabled-set state actually changed (e.g. the user
// toggled a set in the settings UI).
for (const CardInfoPtr &card : cards) {
card->refreshCachedSets();
}
}
// inform the carddatabasemodels that they need to re-check their list of cards
emit cardDatabaseEnabledSetsChanged();
@ -220,3 +239,30 @@ void CardDatabase::addFormat(const FormatRulesPtr &format)
{
formats.insert(format->formatName.toLower(), format);
}
void CardDatabase::swapInDatabaseData(CardDatabaseData data)
{
for (const auto &card : cards) {
for (auto *rel : card->getRelatedCards()) {
rel->deleteLater();
}
for (auto *rel : card->getReverseRelatedCards()) {
rel->deleteLater();
}
for (auto *rel : card->getReverseRelatedCards2Me()) {
rel->deleteLater();
}
}
cards = std::move(data.cards);
simpleNameCards = std::move(data.simpleNameCards);
sets = std::move(data.sets);
formats = std::move(data.formats);
loadStatus = cards.isEmpty() ? NotLoaded : Ok;
// inform listeners that the whole database was replaced; they should
// rebuild from the live containers in a single batch instead of reacting
// to individual card additions.
emit cardDatabaseReset();
}

View file

@ -2,6 +2,7 @@
#define CARDDATABASE_H
#include "../set/card_set_list.h"
#include "card_database_data.h"
#include "card_database_loader.h"
#include "card_database_querier.h"
@ -54,16 +55,17 @@ protected:
CardDatabaseQuerier *querier;
private:
/**
* @brief Check for sets that are unknown and emit signals if needed.
*/
void checkUnknownSets();
/**
* @brief Refreshes the cached reverse-related cards for all cards.
*/
void refreshCachedReverseRelatedCards();
/**
* @brief Refreshes the cached reverse-related cards for the given card map.
* @param cardMap Map of cards to process.
*/
void refreshCachedReverseRelatedCards(CardNameMap &cardMap);
/** @brief Mutexes for thread safety. */
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
*removeCardMutex = new QBasicMutex();
@ -127,8 +129,25 @@ public:
/** @brief Marks all sets as known. */
void markAllSetsAsKnown();
/** @brief Notifies listeners that enabled sets changed. */
void notifyEnabledSetsChanged();
/**
* @brief Notifies listeners that enabled sets changed.
* @param recomputeCachedSets When true, every card's cached set-name / alt-name
* data is recomputed (needed only when enabled-set state actually
* changed). Pass false after a plain reload, where enabled-set state is
* unchanged and the cached data is still valid.
*/
void notifyEnabledSetsChanged(bool recomputeCachedSets = true);
/**
* @brief Check for sets that are unknown and emit signals if needed.
*
* Called from MainWindow::startupConfigCheck() after signal connections are
* live (the front-loaded parse in main() runs before MainWindow exists, so
* signals emitted there would have no receivers). May also be called by
* the loader via the friend declaration. Has side effects: enables all
* sets when none are enabled (first-run), marks all sets as known.
*/
void checkUnknownSets();
ICardSetPriorityController *getPriorityController()
{
@ -159,10 +178,26 @@ public slots:
*/
bool saveCustomTokensToFile();
/**
* @brief Replaces the entire contents of the database with the provided
* snapshot in a single operation. Intended to be called once a
* background load has finished building a CardDatabaseData, so that
* observers never see a partially-populated database.
* @param data The fully-built snapshot to adopt.
*/
void swapInDatabaseData(CardDatabaseData data);
signals:
/** @brief Emitted when the card database has finished loading successfully. */
void cardDatabaseLoadingFinished();
/**
* @brief Emitted once after the live database contents have been replaced
* by a freshly loaded snapshot. Models should rebuild from the
* database in a single batch rather than reacting to per-card adds.
*/
void cardDatabaseReset();
/** @brief Emitted when the card database fails to load. */
void cardDatabaseLoadingFailed();

View file

@ -0,0 +1,485 @@
#include "card_database_cache.h"
#include "../card_info.h"
#include "../format/format_legality_rules.h"
#include "../printing/printing_info.h"
#include "../relation/card_relation.h"
#include "../relation/card_relation_type.h"
#include "../set/card_set.h"
#include "card_database_loader.h"
#include <QBuffer>
#include <QDataStream>
#include <QElapsedTimer>
#include <QFile>
#include <QSaveFile>
#include <QVariantHash>
namespace
{
constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC"
constexpr quint32 CACHE_VERSION = 1;
// ---- Primitives -----------------------------------------------------------
void writeString(QDataStream &out, const QString &s)
{
out << s;
}
QString readString(QDataStream &in)
{
QString s;
in >> s;
return s;
}
// Stores a QVariantHash as a single pre-serialized blob. The reader keeps the
// blob as-is and materializes the QVariantHash lazily on first query, which is
// what removes the allocation storm from database load (see card_info.cpp /
// printing_info.cpp).
void writeHashBlob(QDataStream &out, const QVariantHash &h)
{
QByteArray blob;
QDataStream blobOut(&blob, QIODevice::WriteOnly);
blobOut.setVersion(QDataStream::Qt_6_4);
blobOut << h;
out << static_cast<quint32>(blob.size());
out.writeRawData(blob.constData(), blob.size());
}
QByteArray readHashBlob(QDataStream &in)
{
quint32 len = 0;
in >> len;
if (in.status() != QDataStream::Ok || static_cast<qint64>(len) > in.device()->bytesAvailable()) {
return {};
}
QByteArray blob(len, Qt::Uninitialized);
in.readRawData(blob.data(), static_cast<int>(len));
return blob;
}
void writeDate(QDataStream &out, const QDate &d)
{
out << d;
}
QDate readDate(QDataStream &in)
{
QDate d;
in >> d;
return d;
}
// ---- CardRelation ----------------------------------------------------------
void writeRelation(QDataStream &out, const CardRelation *rel)
{
writeString(out, rel->getName());
out << static_cast<quint32>(rel->getAttachType());
out << rel->getIsCreateAllExclusion();
out << rel->getIsVariable();
out << rel->getDefaultCount();
out << rel->getIsPersistent();
out << rel->getIsFaceDown();
}
CardRelation *readRelation(QDataStream &in)
{
QString name = readString(in);
quint32 attachType = 0;
bool isExclusion = false;
bool isVariable = false;
int defaultCount = 1;
bool isPersistent = false;
bool isFaceDown = false;
in >> attachType;
in >> isExclusion;
in >> isVariable;
in >> defaultCount;
in >> isPersistent;
in >> isFaceDown;
return new CardRelation(name, static_cast<CardRelationType>(attachType), isExclusion, isVariable, defaultCount,
isPersistent, isFaceDown);
}
// ---- PrintingInfo ----------------------------------------------------------
// A printing references its set by short name; the CardSetPtr is resolved after
// all sets have been reconstructed on read.
void writePrinting(QDataStream &out, const PrintingInfo &p)
{
writeString(out, p.getSet() ? p.getSet()->getShortName() : QString());
writeHashBlob(out, p.getPropertiesHash());
}
PrintingInfo readPrinting(QDataStream &in, const SetNameMap &sets)
{
QString setName = readString(in);
QByteArray propsBlob = readHashBlob(in);
PrintingInfo p;
if (auto set = sets.value(setName)) {
p = PrintingInfo(set);
}
p.setPropertiesBlob(propsBlob);
return p;
}
// ---- CardSet ---------------------------------------------------------------
void writeSet(QDataStream &out, const CardSetPtr &set)
{
writeString(out, set->getShortName());
writeString(out, set->getLongName());
writeString(out, set->getSetType());
writeDate(out, set->getReleaseDate());
out << static_cast<quint32>(set->getPriority());
}
CardSetPtr readSet(QDataStream &in, ICardSetPriorityController *priorityController)
{
QString shortName = readString(in);
QString longName = readString(in);
QString setType = readString(in);
QDate releaseDate = readDate(in);
quint32 priority = 0;
in >> priority;
return CardSet::newInstance(priorityController, shortName, longName, setType, releaseDate,
static_cast<CardSet::Priority>(priority));
}
// ---- CardInfo --------------------------------------------------------------
void writeCard(QDataStream &out, const CardInfoPtr &card)
{
writeString(out, card->getName());
writeString(out, card->getText());
out << card->getIsToken();
writeHashBlob(out, card->getPropertiesHash());
CardInfo::UiAttributes ui = card->getUiAttributes();
out << ui.cipt;
out << ui.landscapeOrientation;
out << ui.tableRow;
out << ui.upsideDownArt;
// Precomputed derived state, so the reader can skip simplifyName() and the
// per-printing alt-name scan entirely.
writeString(out, card->getSimpleName());
{
const QSet<QString> altNames = card->getAltNames();
out << static_cast<quint32>(altNames.size());
for (const QString &alt : altNames) {
writeString(out, alt);
}
}
// setsToPrintings
const SetToPrintingsMap sets = card->getSets();
out << static_cast<quint32>(sets.size());
for (auto it = sets.constBegin(); it != sets.constEnd(); ++it) {
writeString(out, it.key());
out << static_cast<quint32>(it.value().size());
for (const PrintingInfo &p : it.value()) {
writePrinting(out, p);
}
}
// related cards
const QList<CardRelation *> related = card->getRelatedCards();
out << static_cast<quint32>(related.size());
for (const CardRelation *rel : related) {
writeRelation(out, rel);
}
// reverse-related cards (the reverseRelatedCards list, not the computed 2Me)
const QList<CardRelation *> reverse = card->getReverseRelatedCards();
out << static_cast<quint32>(reverse.size());
for (const CardRelation *rel : reverse) {
writeRelation(out, rel);
}
}
CardInfoPtr readCard(QDataStream &in, const SetNameMap &sets)
{
QString name = readString(in);
QString text = readString(in);
bool isToken = false;
in >> isToken;
QByteArray propertiesBlob = readHashBlob(in);
CardInfo::UiAttributes ui;
in >> ui.cipt;
in >> ui.landscapeOrientation;
in >> ui.tableRow;
in >> ui.upsideDownArt;
// Precomputed derived state, restored directly to skip simplifyName() and the
// per-printing alt-name scan.
QString simpleName = readString(in);
QSet<QString> altNames;
quint32 altNameCount = 0;
in >> altNameCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
altNames.reserve(altNameCount);
for (quint32 i = 0; i < altNameCount; ++i) {
altNames.insert(readString(in));
}
SetToPrintingsMap cardSets;
quint32 setCount = 0;
in >> setCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
for (quint32 i = 0; i < setCount; ++i) {
QString setName = readString(in);
quint32 printingCount = 0;
in >> printingCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
QList<PrintingInfo> printings;
printings.reserve(printingCount);
for (quint32 j = 0; j < printingCount; ++j) {
printings.append(readPrinting(in, sets));
}
cardSets.insert(setName, printings);
}
QList<CardRelation *> related;
quint32 relatedCount = 0;
in >> relatedCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
related.reserve(relatedCount);
for (quint32 i = 0; i < relatedCount; ++i) {
related.append(readRelation(in));
}
QList<CardRelation *> reverse;
quint32 reverseCount = 0;
in >> reverseCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
reverse.reserve(reverseCount);
for (quint32 i = 0; i < reverseCount; ++i) {
reverse.append(readRelation(in));
}
return CardInfo::newInstance(name, text, isToken, propertiesBlob, related, reverse, cardSets, ui, simpleName,
altNames, false);
}
// ---- FormatRules -----------------------------------------------------------
void writeFormat(QDataStream &out, const FormatRulesPtr &format)
{
writeString(out, format->formatName);
out << format->minDeckSize;
out << format->maxDeckSize;
out << format->maxSideboardSize;
out << static_cast<quint32>(format->allowedCounts.size());
for (const AllowedCount &ac : format->allowedCounts) {
out << ac.max;
writeString(out, ac.label);
}
out << static_cast<quint32>(format->exceptions.size());
for (const ExceptionRule &ex : format->exceptions) {
out << static_cast<quint32>(ex.conditions.size());
for (const CardCondition &cond : ex.conditions) {
writeString(out, cond.field);
writeString(out, cond.matchType);
writeString(out, cond.value);
}
out << ex.maxCopies;
}
}
FormatRulesPtr readFormat(QDataStream &in)
{
FormatRulesPtr format(new FormatRules());
format->formatName = readString(in);
in >> format->minDeckSize;
in >> format->maxDeckSize;
in >> format->maxSideboardSize;
quint32 allowedCount = 0;
in >> allowedCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
for (quint32 i = 0; i < allowedCount; ++i) {
AllowedCount ac;
in >> ac.max;
ac.label = readString(in);
format->allowedCounts.append(ac);
}
quint32 exceptionCount = 0;
in >> exceptionCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
for (quint32 i = 0; i < exceptionCount; ++i) {
ExceptionRule ex;
quint32 condCount = 0;
in >> condCount;
if (in.status() != QDataStream::Ok) {
return nullptr;
}
for (quint32 j = 0; j < condCount; ++j) {
CardCondition cond;
cond.field = readString(in);
cond.matchType = readString(in);
cond.value = readString(in);
ex.conditions.append(cond);
}
in >> ex.maxCopies;
format->exceptions.append(ex);
}
return format;
}
} // namespace
bool CardDatabaseCache::write(const QString &cachePath, const CardDatabaseData &data, const QByteArray &sourceHash)
{
QSaveFile file(cachePath);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_6_4);
// Header
out << CACHE_MAGIC;
out << CACHE_VERSION;
out << sourceHash;
// Sets
out << static_cast<quint32>(data.sets.size());
for (const CardSetPtr &set : data.sets) {
writeSet(out, set);
}
// Cards
const quint32 cardCount = static_cast<quint32>(data.cards.size());
out << cardCount;
for (const CardInfoPtr &card : data.cards) {
writeCard(out, card);
}
// Formats
out << static_cast<quint32>(data.formats.size());
for (const FormatRulesPtr &format : data.formats) {
writeFormat(out, format);
}
return file.commit();
}
bool CardDatabaseCache::read(const QString &cachePath,
CardDatabaseData &data,
const QByteArray &sourceHash,
ICardSetPriorityController *priorityController)
{
QFile file(cachePath);
if (!file.open(QIODevice::ReadOnly)) {
return false;
}
// Read the whole cache into memory up front; deserialization then works on an
// in-memory buffer with no further disk I/O.
QByteArray raw = file.readAll();
if (raw.isEmpty()) {
return false;
}
QBuffer buffer(&raw);
buffer.open(QIODevice::ReadOnly);
QDataStream in(&buffer);
in.setVersion(QDataStream::Qt_6_4);
quint32 magic = 0;
quint32 version = 0;
QByteArray storedHash;
in >> magic >> version >> storedHash;
if (magic != CACHE_MAGIC || version != CACHE_VERSION || storedHash != sourceHash) {
return false;
}
QElapsedTimer deserializeTimer;
deserializeTimer.start();
// Sets
quint32 setCount = 0;
in >> setCount;
if (in.status() != QDataStream::Ok) {
return false;
}
for (quint32 i = 0; i < setCount; ++i) {
CardSetPtr set = readSet(in, priorityController);
data.sets.insert(set->getShortName(), set);
}
// Cards
quint32 cardCount = 0;
in >> cardCount;
if (in.status() != QDataStream::Ok) {
return false;
}
if (cardCount > 0) {
data.cards.reserve(static_cast<int>(cardCount));
for (quint32 i = 0; i < cardCount; ++i) {
CardInfoPtr card = readCard(in, data.sets);
if (card == nullptr) {
return false;
}
data.cards.insert(card->getName(), card);
data.simpleNameCards.insert(card->getSimpleName(), card);
}
for (const CardInfoPtr &card : data.cards) {
for (const auto &printings : card->getSets()) {
for (const PrintingInfo &printing : printings) {
if (auto set = printing.getSet()) {
set->append(card);
break;
}
}
}
}
}
// Formats
quint32 formatCount = 0;
in >> formatCount;
if (in.status() != QDataStream::Ok) {
return false;
}
for (quint32 i = 0; i < formatCount; ++i) {
FormatRulesPtr format = readFormat(in);
if (format == nullptr) {
return false;
}
data.formats.insert(format->formatName.toLower(), format);
}
qCInfo(CardDatabaseLoadingLog) << "[cache] read + deserialize" << deserializeTimer.elapsed() << "ms for"
<< cardCount << "cards";
if (in.status() != QDataStream::Ok) {
return false;
}
return file.error() == QFile::NoError;
}

View file

@ -0,0 +1,34 @@
#ifndef CARDDATABASE_CACHE_H
#define CARDDATABASE_CACHE_H
#include "card_database_data.h"
#include <QByteArray>
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
namespace CardDatabaseCache
{
/**
* @brief Writes a fully-built database snapshot to a binary cache file.
* @param cachePath Path of the cache file to write.
* @param data The snapshot to serialize.
* @param sourceHash Hash of the source XML used to validate the cache later.
* @return True if the cache was written successfully.
*/
bool write(const QString &cachePath, const CardDatabaseData &data, const QByteArray &sourceHash);
/**
* @brief Reads a database snapshot from a binary cache file.
* @param cachePath Path of the cache file to read.
* @param data Snapshot to populate.
* @param sourceHash Expected hash of the source XML; read fails if it differs.
* @param priorityController Controller used to reconstruct CardSet state.
* @return True if a valid, up-to-date cache was read.
*/
bool read(const QString &cachePath,
CardDatabaseData &data,
const QByteArray &sourceHash,
ICardSetPriorityController *priorityController);
} // namespace CardDatabaseCache
#endif // CARDDATABASE_CACHE_H

View file

@ -0,0 +1,24 @@
#ifndef CARDDATABASE_DATA_H
#define CARDDATABASE_DATA_H
#include <libcockatrice/card/card_info.h>
/**
* @struct CardDatabaseData
* @ingroup CardDatabase
* @brief Value type holding the full parsed state of a card database.
*
* Parsers fill a CardDatabaseData off the UI thread; the finished snapshot is
* then swapped into the live CardDatabase atomically. Keeping this separate from
* the live container avoids emitting per-card change notifications and prevents
* readers from observing a half-loaded database.
*/
struct CardDatabaseData
{
CardNameMap cards;
CardNameMap simpleNameCards;
SetNameMap sets;
FormatRulesNameMap formats;
};
#endif // CARDDATABASE_DATA_H

View file

@ -1,12 +1,18 @@
#include "card_database_loader.h"
#include "card_database.h"
#include "card_database_cache.h"
#include "parser/card_database_parser.h"
#include "parser/cockatrice_xml_3.h"
#include "parser/cockatrice_xml_4.h"
#include <QByteArray>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDebug>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QTime>
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
@ -14,18 +20,14 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
ICardDatabasePathProvider *_pathProvider,
ICardPreferenceProvider *_preferenceProvider,
ICardSetPriorityController *_priorityController)
: QObject(parent), database(db), pathProvider(_pathProvider)
: QObject(parent), database(db), pathProvider(_pathProvider), priorityController(_priorityController)
{
// instantiate available parsers here and connect them to the database
// instantiate available parsers here
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
availableParsers << new CockatriceXml3Parser(_priorityController);
for (auto *p : availableParsers) {
// connect parser outputs to the database adders
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection);
}
// The load path parses into a snapshot and never emits per-card signals;
// the finished snapshot is swapped into the live database on the GUI thread.
// when SettingsCache's path changes, trigger reloads
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
@ -38,7 +40,7 @@ CardDatabaseLoader::~CardDatabaseLoader()
availableParsers.clear();
}
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName, CardDatabaseData &data)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
@ -49,7 +51,7 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
file.reset();
if (parser->getCanParseFile(fileName, file)) {
file.reset();
parser->parseFile(file);
parser->parseFileInto(file, data);
return Ok;
}
}
@ -57,24 +59,29 @@ LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
return Invalid;
}
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path)
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabaseData &data)
{
auto startTime = QTime::currentTime();
LoadStatus tempLoadStatus = NotLoaded;
if (!path.isEmpty()) {
QMutexLocker locker(loadFromFileMutex);
tempLoadStatus = loadFromFile(path);
tempLoadStatus = loadFromFile(path, data);
}
int msecs = startTime.msecsTo(QTime::currentTime());
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
<< "Cards =" << (database ? database->cards.size() : 0)
<< "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs);
<< "Cards =" << data.cards.size() << "Sets =" << data.sets.size()
<< QString("%1ms").arg(msecs);
return tempLoadStatus;
}
LoadStatus CardDatabaseLoader::loadCardDatabases()
{
return doLoadCardDatabases();
}
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
{
QMutexLocker locker(reloadDatabaseMutex);
@ -86,31 +93,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
emit loadingStarted();
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
database->clear(); // remove old db
ICardDatabaseParser::clearSetlist();
LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database
loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database
CardDatabaseData data;
LoadStatus loadStatus = NotLoaded;
// Try the binary cache first: a cache hit avoids re-parsing the (large) XML.
const QStringList customPaths = collectCustomDatabasePaths();
const QByteArray sourceHash = computeSourceHash(customPaths);
if (loadFromCache(data, sourceHash)) {
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
loadStatus = Ok;
} else {
loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath(), data); // main card database
if (loadStatus == Ok) {
loadCardDatabase(pathProvider->getTokenDatabasePath(), data); // tokens database
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath(), data); // spoilers database
// find all custom card databases, recursively & following symlinks
// then load them alphabetically
const QStringList customPaths = collectCustomDatabasePaths();
for (int i = 0; i < customPaths.size(); ++i) {
const auto &p = customPaths.at(i);
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
loadCardDatabase(p);
loadCardDatabase(p, data);
}
// AFTER all the cards have been loaded
// resolve the reverse-related tags
database->refreshCachedReverseRelatedCards();
if (!saveToCache(data, sourceHash)) {
qCWarning(CardDatabaseLoadingLog) << "Failed to write binary cache to" << cachePath();
}
}
}
// AFTER all the cards have been loaded: resolve the reverse-related tags
// against the fully-built snapshot.
if (loadStatus == Ok) {
database->checkUnknownSets(); // update deck editors, etc
database->refreshCachedReverseRelatedCards(data.cards);
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
emit databaseDataReady(std::move(data));
emit loadingFinished();
// During the front-loaded parse in main() this runs before MainWindow
// exists, so cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled
// would be emitted with no receivers. Skip the check on the first load;
// MainWindow::startupConfigCheck() calls checkUnknownSets() once its
// signal connections are live. On subsequent reloads (e.g. path change)
// MainWindow exists and signals have live receivers.
if (initialLoadComplete) {
database->checkUnknownSets();
}
initialLoadComplete = true;
} else {
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
emit loadingFailed(); // bring up the settings dialog
@ -119,6 +150,55 @@ LoadStatus CardDatabaseLoader::loadCardDatabases()
return loadStatus;
}
QString CardDatabaseLoader::cachePath() const
{
return pathProvider->getCardDatabasePath() + ".cache";
}
QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths) const
{
// Hash over the paths, sizes and modification times of every input file so
// the cache invalidates when any source changes. Cheap (no content read).
QCryptographicHash hash(QCryptographicHash::Sha256);
// Include the application version so parser changes automatically invalidate
// old caches even when the XML files are byte-identical.
hash.addData(QCoreApplication::applicationVersion().toUtf8());
hash.addData(QByteArray(1, '\0'));
const QStringList inputs = QStringList()
<< pathProvider->getCardDatabasePath() << pathProvider->getTokenDatabasePath()
<< pathProvider->getSpoilerCardDatabasePath() << customPaths;
for (const QString &path : inputs) {
QFileInfo info(path);
if (info.exists()) {
hash.addData(path.toUtf8());
hash.addData(QByteArray(1, '\0'));
hash.addData(QByteArray::number(info.size()));
hash.addData(QByteArray(1, '\0'));
hash.addData(QByteArray::number(info.lastModified().toSecsSinceEpoch()));
hash.addData(QByteArray(1, '\0'));
}
}
return hash.result();
}
bool CardDatabaseLoader::loadFromCache(CardDatabaseData &data, const QByteArray &sourceHash)
{
if (sourceHash.isEmpty()) {
return false;
}
return CardDatabaseCache::read(cachePath(), data, sourceHash, priorityController);
}
bool CardDatabaseLoader::saveToCache(const CardDatabaseData &data, const QByteArray &sourceHash)
{
if (sourceHash.isEmpty()) {
return false;
}
return CardDatabaseCache::write(cachePath(), data, sourceHash);
}
QStringList CardDatabaseLoader::collectCustomDatabasePaths() const
{
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,

View file

@ -4,6 +4,7 @@
#include <QBasicMutex>
#include <QList>
#include <QLoggingCategory>
#include <libcockatrice/card/database/card_database_data.h>
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
@ -62,16 +63,20 @@ public:
public slots:
/**
* @brief Loads all configured card databases.
*
* Runs synchronously on the calling thread. The caller should ensure
* that any signal receivers are already connected before invoking this.
* @return Status of the main database load.
*/
LoadStatus loadCardDatabases();
/**
* @brief Loads a single card database file.
* @brief Loads a single card database file into the given snapshot.
* @param path Path to the database file.
* @param data Snapshot to populate.
* @return LoadStatus indicating success or failure.
*/
LoadStatus loadCardDatabase(const QString &path);
LoadStatus loadCardDatabase(const QString &path, CardDatabaseData &data);
/**
* @brief Saves custom tokens to the user-defined custom database path.
@ -89,6 +94,12 @@ signals:
/** @brief Emitted when loading fails. */
void loadingFailed();
/**
* @brief Emitted with the fully-built snapshot once background loading
* completes. The receiver swaps it into the live database.
*/
void databaseDataReady(CardDatabaseData data);
/**
* @brief Emitted when new sets are discovered during loading.
* @param numSets Number of new sets.
@ -101,11 +112,18 @@ signals:
private:
/**
* @brief Loads a database from a single file using the available parsers.
* @brief Parses a single file into the given snapshot using the available parsers.
* @param fileName Path to the database file.
* @param data Snapshot to populate.
* @return LoadStatus indicating success or failure.
*/
LoadStatus loadFromFile(const QString &fileName);
LoadStatus loadFromFile(const QString &fileName, CardDatabaseData &data);
/**
* @brief Performs the actual load work synchronously on the calling thread.
* @return Status of the main database load.
*/
LoadStatus doLoadCardDatabases();
/**
* @brief Collects custom card database paths recursively.
@ -113,13 +131,43 @@ private:
*/
[[nodiscard]] QStringList collectCustomDatabasePaths() const;
/**
* @brief Computes a hash identifying the current set of input database files.
* @return Hash over the paths, sizes and modification times of all inputs.
*/
[[nodiscard]] QByteArray computeSourceHash(const QStringList &customPaths) const;
/**
* @brief Path of the binary cache file derived from the main card database path.
* @return Cache file path.
*/
[[nodiscard]] QString cachePath() const;
/**
* @brief Attempts to populate the snapshot from the binary cache.
* @param data Snapshot to populate.
* @param sourceHash Pre-computed hash identifying the current input files.
* @return True if a valid, up-to-date cache was read.
*/
bool loadFromCache(CardDatabaseData &data, const QByteArray &sourceHash);
/**
* @brief Writes the snapshot to the binary cache for the current inputs.
* @param data Snapshot to serialize.
* @param sourceHash Pre-computed hash identifying the current input files.
* @return True if the cache was written successfully.
*/
bool saveToCache(const CardDatabaseData &data, const QByteArray &sourceHash);
private:
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
ICardSetPriorityController *priorityController; /**< Controller for reconstructing set state. */
QList<ICardDatabaseParser *> availableParsers; /**< List of available parsers for different formats. */
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
bool initialLoadComplete = false; /**< Set after the first successful load. */
};
#endif // COCKATRICE_CARD_DATABASE_LOADER_H

View file

@ -30,6 +30,10 @@ CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName,
newSet->setPriority(priority);
sets.insert(setName, newSet);
if (targetData) {
targetData->sets.insert(setName, newSet);
} else {
emit addSet(newSet);
}
return newSet;
}

View file

@ -2,6 +2,7 @@
#define CARDDATABASE_PARSER_H
#include "../../card_info.h"
#include "../card_database_data.h"
#include <QIODevice>
#include <QString>
@ -37,6 +38,15 @@ public:
*/
virtual void parseFile(QIODevice &device) = 0;
/**
* @brief Parses a database file into the given snapshot, without emitting
* any signals. Used for background loads that swap the snapshot into
* the live database once complete.
* @param device QIODevice representing the file content.
* @param data Target snapshot to populate.
*/
virtual void parseFileInto(QIODevice &device, CardDatabaseData &data) = 0;
/**
* @brief Saves card and set data to a file.
* @param _formats
@ -62,6 +72,21 @@ protected:
static SetNameMap sets;
ICardSetPriorityController *cardSetPriorityController;
/**
* @brief Snapshot the current parse is filling, or nullptr when emitting signals.
*
* This is an implicit-inheritance-via-member-variable pattern: parseFileInto()
* sets this before delegating to parseFile(), and loadCardsFromXml() /
* loadFormats() check it to decide between direct insertion and signal
* emission. Safe under the current model because loadFromFileMutex serialises
* all parser access and parseFile() is never re-entered. If a future change
* adds parallel parsing or a second parseFileInto() call within parseFile(),
* this pointer would race -- at that point refactor to pass CardDatabaseData*
* through the call chain instead. However, we are mostly RAM, not CPU bound on
* DB startup at this point so there's not much point to parallel parsing.
*/
CardDatabaseData *targetData = nullptr;
/**
* @brief Internal helper to add a set to the global set cache.
* @param setName Short set name.

View file

@ -79,6 +79,13 @@ void CockatriceXml3Parser::parseFile(QIODevice &device)
}
}
void CockatriceXml3Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
{
targetData = &data;
parseFile(device);
targetData = nullptr;
}
void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
@ -222,25 +229,27 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
// behaviour. Without this check, disabling a set has no effect on v3 databases.
if (set->getEnabled()) {
PrintingInfo setInfo(set);
QVariantHash printingProps;
if (attrs.hasAttribute("muId")) {
setInfo.setProperty("muid", attrs.value("muId").toString());
printingProps.insert("muid", attrs.value("muId").toString());
}
if (attrs.hasAttribute("uuId")) {
setInfo.setProperty("uuid", attrs.value("uuId").toString());
printingProps.insert("uuid", attrs.value("uuId").toString());
}
if (attrs.hasAttribute("picURL")) {
setInfo.setProperty("picurl", attrs.value("picURL").toString());
printingProps.insert("picurl", attrs.value("picURL").toString());
}
if (attrs.hasAttribute("num")) {
setInfo.setProperty("num", attrs.value("num").toString());
printingProps.insert("num", attrs.value("num").toString());
}
if (attrs.hasAttribute("rarity")) {
setInfo.setProperty("rarity", attrs.value("rarity").toString());
printingProps.insert("rarity", attrs.value("rarity").toString());
}
setInfo.setProperties(printingProps);
_sets[setName].append(setInfo);
}
// related cards
@ -299,9 +308,22 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
.upsideDownArt = upsideDown};
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
reverseRelatedCards, _sets, attributes);
if (targetData) {
if (auto existing = targetData->cards.value(name)) {
for (const auto &printings : newCard->getSets()) {
for (const auto &printing : printings) {
existing->addToSet(printing.getSet(), printing);
}
}
} else {
targetData->cards.insert(name, newCard);
targetData->simpleNameCards.insert(newCard->getSimpleName(), newCard);
}
} else {
emit addCard(newCard);
}
}
}
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)

View file

@ -43,6 +43,13 @@ public:
*/
void parseFile(QIODevice &device) override;
/**
* @brief Parse the XML database into the given snapshot.
* @param device Open QIODevice positioned at start of file.
* @param data Target snapshot to populate.
*/
void parseFileInto(QIODevice &device, CardDatabaseData &data) override;
/**
* @brief Save sets and cards back to an XML3 file.
*/

View file

@ -82,6 +82,13 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
}
}
void CockatriceXml4Parser::parseFileInto(QIODevice &device, CardDatabaseData &data)
{
targetData = &data;
parseFile(device);
targetData = nullptr;
}
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
{
auto rulesPtr = FormatRulesPtr(new FormatRules());
@ -187,9 +194,13 @@ void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
if (xml.name().toString() == "format") {
auto rulesPtr = parseFormat(xml);
if (targetData) {
targetData->formats.insert(rulesPtr->formatName.toLower(), rulesPtr);
} else {
emit addFormat(rulesPtr);
}
}
}
}
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
@ -304,13 +315,15 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
auto set = internalAddSet(setName);
if (set->getEnabled()) {
PrintingInfo printingInfo(set);
QVariantHash printingProps;
for (QXmlStreamAttribute attr : attrs) {
QString attrName = attr.name().toString();
if (attrName == "picURL") {
attrName = "picurl";
}
printingInfo.setProperty(attrName, attr.value().toString());
printingProps.insert(attrName, attr.value().toString());
}
printingInfo.setProperties(printingProps);
// This is very much a hack and not the right place to
// put this check, as it requires a reload of Cockatrice
@ -389,9 +402,24 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
.upsideDownArt = upsideDown};
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
reverseRelatedCards, _sets, attributes);
if (targetData) {
// Mirror CardDatabase::addCard: if a card with this name already
// exists, merge the new printings into it instead of replacing.
if (auto existing = targetData->cards.value(name)) {
for (const auto &printings : newCard->getSets()) {
for (const auto &printing : printings) {
existing->addToSet(printing.getSet(), printing);
}
}
} else {
targetData->cards.insert(name, newCard);
targetData->simpleNameCards.insert(newCard->getSimpleName(), newCard);
}
} else {
emit addCard(newCard);
}
}
}
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer<FormatRules> &rulesPtr)

View file

@ -47,6 +47,13 @@ public:
*/
void parseFile(QIODevice &device) override;
/**
* @brief Parse the XML database into the given snapshot.
* @param device Open QIODevice positioned at start of file.
* @param data Target snapshot to populate.
*/
void parseFileInto(QIODevice &device, CardDatabaseData &data) override;
/**
* @brief Save sets and cards back to an XML4 file.
*/

View file

@ -2,19 +2,56 @@
#include "../set/card_set.h"
#include <QDataStream>
#include <QIODevice>
PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set)
{
}
void PrintingInfo::ensurePropertiesLoaded() const
{
QMutexLocker lock(propertiesMutex.data());
if (propertiesLoaded) {
return;
}
propertiesCache.clear();
if (!propertiesBlob.isEmpty()) {
QDataStream in(propertiesBlob);
in.setVersion(QDataStream::Qt_6_4);
in >> propertiesCache;
}
propertiesLoaded = true;
}
void PrintingInfo::setProperty(const QString &_name, const QString &_value)
{
ensurePropertiesLoaded();
if (propertiesCache.value(_name).toString() == _value) {
return;
}
propertiesCache.insert(_name, _value);
setProperties(propertiesCache);
}
void PrintingInfo::setProperties(const QVariantHash &_props)
{
ensurePropertiesLoaded();
propertiesCache = _props;
QDataStream out(&propertiesBlob, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_6_4);
out << propertiesCache;
}
/**
* Gets the uuid property of the printing, or an empty string if the property isn't present
*/
QString PrintingInfo::getUuid() const
{
return properties.value("uuid").toString();
return getPropertiesHash().value("uuid").toString();
}
QString PrintingInfo::getFlavorName() const
{
return properties.value("flavorName").toString();
return getPropertiesHash().value("flavorName").toString();
}

View file

@ -5,6 +5,8 @@
#include <QList>
#include <QMap>
#include <QMutex>
#include <QSharedPointer>
#include <QVariant>
class PrintingInfo;
@ -51,7 +53,7 @@ public:
*/
bool operator==(const PrintingInfo &other) const
{
return this->set == other.set && this->properties == other.properties;
return this->set == other.set && this->getPropertiesHash() == other.getPropertiesHash();
}
/**
@ -61,12 +63,22 @@ public:
*/
bool isEmpty() const
{
return set == nullptr && properties.isEmpty();
return set == nullptr && getPropertiesHash().isEmpty();
}
private:
CardSetPtr set; ///< The set this variation belongs to.
QVariantHash properties; ///< Key-value store for variation-specific attributes.
// Properties are stored as a pre-serialized blob (cheap to load) and the
// QVariantHash is materialized on first query. This avoids constructing
// thousands of QVariants per card at database-load time.
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
mutable QVariantHash propertiesCache; ///< Materialized properties (query form).
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
mutable QSharedPointer<QBasicMutex> propertiesMutex =
QSharedPointer<QBasicMutex>::create(); ///< Guards lazy materialization.
void ensurePropertiesLoaded() const;
public:
/**
@ -86,7 +98,13 @@ public:
*/
[[nodiscard]] QStringList getProperties() const
{
return properties.keys();
return getPropertiesHash().keys();
}
[[nodiscard]] const QVariantHash &getPropertiesHash() const
{
ensurePropertiesLoaded();
return propertiesCache;
}
/**
@ -97,7 +115,7 @@ public:
*/
[[nodiscard]] QString getProperty(const QString &propertyName) const
{
return properties.value(propertyName).toString();
return getPropertiesHash().value(propertyName).toString();
}
/**
@ -108,9 +126,21 @@ public:
* @param _name The name of the property.
* @param _value The string value to assign.
*/
void setProperty(const QString &_name, const QString &_value)
void setProperty(const QString &_name, const QString &_value);
void setProperties(const QVariantHash &_props);
/**
* @brief Stores the pre-serialized properties blob and marks the materialized
* cache as invalid. Used by the binary cache reader to avoid building a
* QVariantHash at load time.
* @param _blob The serialized properties (as written by the cache writer).
*/
void setPropertiesBlob(QByteArray _blob) const
{
properties.insert(_name, _value);
QMutexLocker lock(propertiesMutex.data());
propertiesBlob = std::move(_blob);
propertiesLoaded = false;
propertiesCache.clear();
}
/**

View file

@ -42,9 +42,10 @@ QString CardSet::getCorrectedShortName() const
void CardSet::loadSetOptions()
{
sortKey = priorityController->getSortKey(shortName);
enabled = priorityController->isEnabled(shortName);
isknown = priorityController->isKnown(shortName);
const ICardSetPriorityController::SetOptions options = priorityController->getSetOptions(shortName);
sortKey = options.sortKey;
enabled = options.enabled;
isknown = options.isKnown;
}
void CardSet::setSortKey(unsigned int _sortKey)

View file

@ -13,6 +13,22 @@ public:
bool enabled;
};
/**
* @brief Bundled per-set priority options, returned in a single call.
*
* Reading these individually (getSortKey/isEnabled/isKnown) is cheap for the
* noop controller but extremely expensive when backed by QSettings, which
* re-opens and re-parses the whole .ini on every access. Callers that need
* more than one option for the same set (notably CardSet construction during
* database load) should use getSetOptions() to avoid that cost.
*/
struct SetOptions
{
unsigned int sortKey = 0;
bool enabled = false;
bool isKnown = false;
};
virtual ~ICardSetPriorityController() = default;
virtual void setSortKey(QString shortName, unsigned int sortKey) = 0;
@ -23,6 +39,13 @@ public:
virtual bool isEnabled(QString shortName) const = 0;
virtual bool isKnown(QString shortName) const = 0;
/**
* @brief Returns the bundled priority options for a set in a single call.
* @param shortName The set's short name.
* @return The sort key, enabled and known flags for the set.
*/
virtual SetOptions getSetOptions(QString shortName) const = 0;
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
};

View file

@ -29,6 +29,11 @@ public:
return true;
}
SetOptions getSetOptions(QString /* shortName */) const override
{
return {0, true, true};
}
void saveSets(const QVector<SetSaveData> & /* data */) override
{
}

View file

@ -12,6 +12,7 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved);
connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this,
&CardDatabaseModel::cardDatabaseEnabledSetsChanged);
connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseModel::resetCardList);
cardDatabaseEnabledSetsChanged();
}
@ -151,3 +152,18 @@ void CardDatabaseModel::cardRemoved(CardInfoPtr card)
cardList.removeAt(row);
endRemoveRows();
}
void CardDatabaseModel::resetCardList()
{
beginResetModel();
cardList.clear();
cardListSet.clear();
for (const CardInfoPtr &card : db->getCardList()) {
if (checkCardHasAtLeastOneEnabledSet(card)) {
cardList.append(card);
cardListSet.insert(card);
connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged);
}
}
endResetModel();
}

View file

@ -57,6 +57,7 @@ private slots:
void cardRemoved(CardInfoPtr card);
void cardInfoChanged(const CardInfoPtr &card);
void cardDatabaseEnabledSetsChanged();
void resetCardList();
};
#endif

View file

@ -1,5 +1,7 @@
#include "card_database_settings.h"
#include <QMutexLocker>
CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent)
: SettingsManager(settingPath + "cardDatabase.ini", QString(), QString(), parent)
{
@ -7,32 +9,73 @@ CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *
void CardDatabaseSettings::setSortKey(QString shortName, unsigned int sortKey)
{
setValue(sortKey, "sortkey", "sets", std::move(shortName));
setValue(sortKey, "sortkey", "sets", shortName);
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
setOptionsCache[shortName].sortKey = sortKey;
}
void CardDatabaseSettings::setEnabled(QString shortName, bool enabled)
{
setValue(enabled, "enabled", "sets", std::move(shortName));
setValue(enabled, "enabled", "sets", shortName);
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
setOptionsCache[shortName].enabled = enabled;
}
void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown)
{
setValue(isknown, "isknown", "sets", std::move(shortName));
setValue(isknown, "isknown", "sets", shortName);
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
setOptionsCache[shortName].isKnown = isknown;
}
void CardDatabaseSettings::ensureSetOptionsLoaded() const
{
if (setOptionsLoaded) {
return;
}
QSettings settings(getSettings());
settings.beginGroup("sets");
const QStringList groups = settings.childGroups();
for (const QString &group : groups) {
settings.beginGroup(group);
SetOptions &o = setOptionsCache[group];
o.sortKey = settings.value("sortkey", 0).toUInt();
o.enabled = settings.value("enabled", true).toBool();
o.isKnown = settings.value("isknown", true).toBool();
settings.endGroup();
}
setOptionsLoaded = true;
}
unsigned int CardDatabaseSettings::getSortKey(QString shortName) const
{
return getValue("sortkey", "sets", std::move(shortName)).toUInt();
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
return setOptionsCache.value(shortName).sortKey;
}
bool CardDatabaseSettings::isEnabled(QString shortName) const
{
return getValue("enabled", "sets", std::move(shortName)).toBool();
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
return setOptionsCache.value(shortName).enabled;
}
bool CardDatabaseSettings::isKnown(QString shortName) const
{
return getValue("isknown", "sets", std::move(shortName)).toBool();
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
return setOptionsCache.value(shortName).isKnown;
}
ICardSetPriorityController::SetOptions CardDatabaseSettings::getSetOptions(QString shortName) const
{
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
return setOptionsCache.value(shortName);
}
void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::SetSaveData> &data)
@ -47,4 +90,12 @@ void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::Se
}
s.endGroup();
});
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
for (const auto &entry : data) {
SetOptions &o = setOptionsCache[entry.shortName];
o.sortKey = entry.sortKey;
o.enabled = entry.enabled;
}
}

View file

@ -10,6 +10,8 @@
#include "settings_manager.h"
#include <QHash>
#include <QMutex>
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController
@ -26,10 +28,23 @@ public:
bool isEnabled(QString shortName) const override;
bool isKnown(QString shortName) const override;
SetOptions getSetOptions(QString shortName) const override;
void saveSets(const QVector<SetSaveData> &data) override;
private:
explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr);
// In-memory cache of set priority options. QSettings re-opens and re-parses
// the whole .ini on every access, which is catastrophic when CardSet
// construction calls getSortKey/isEnabled/isKnown once per set during the
// database load (thousands of file parses). The cache is populated from a
// single QSettings session and kept consistent with the writes below.
mutable QMutex setOptionsMutex;
mutable QHash<QString, SetOptions> setOptionsCache;
mutable bool setOptionsLoaded = false;
void ensureSetOptionsLoaded() const;
};
#endif // CARDDATABASESETTINGS_H

View file

@ -291,12 +291,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QList
// per-set properties
PrintingInfo printingInfo = PrintingInfo(currentSet);
QVariantHash printingProps;
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
QString mtgjsonProperty = i.key();
QString xmlPropertyName = i.value();
QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty);
if (!propertyValue.isEmpty()) {
printingInfo.setProperty(xmlPropertyName, propertyValue);
printingProps.insert(xmlPropertyName, propertyValue);
}
}
@ -304,7 +305,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QList
QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName");
QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName");
if (!flavorName.isEmpty()) {
printingInfo.setProperty("flavorName", flavorName);
printingProps.insert("flavorName", flavorName);
}
// Identifiers
@ -313,10 +314,12 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QList
QString xmlPropertyName = i.value();
QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty);
if (!propertyValue.isEmpty()) {
printingInfo.setProperty(xmlPropertyName, propertyValue);
printingProps.insert(xmlPropertyName, propertyValue);
}
}
printingInfo.setProperties(printingProps);
QString numComponent;
const QString numProperty = printingInfo.getProperty("num");
const QChar lastChar = numProperty.isEmpty() ? QChar() : numProperty.back();

View file

@ -21,6 +21,24 @@ target_link_libraries(
add_test(NAME carddatabase_test COMMAND carddatabase_test)
# ------------------------
# Load Benchmark (manual, not run in CI)
# ------------------------
add_executable(load_benchmark ${MOCKS_SOURCES} ${VERSION_STRING_CPP} load_benchmark.cpp mocks.cpp)
target_link_libraries(
load_benchmark
PRIVATE libcockatrice_card
PRIVATE libcockatrice_models
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
if(NOT GTEST_FOUND)
add_dependencies(load_benchmark gtest)
endif()
# ------------------------
# Filter String Test
# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt)

View file

@ -0,0 +1,235 @@
/*
* Standalone benchmark for card database loading.
*
* Measures wall-clock cost and model churn:
* - Cold load (XML -> write cache)
* - Warm load (read cache)
*
* Run:
* load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]
*
* All arguments are optional; they default to the XDG Cockatrice data location
* (~/.local/share/Cockatrice/Cockatrice/...).
*/
#include "mocks.h"
#include "gtest/gtest.h"
#include <QAbstractItemModel>
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QFile>
#include <QLoggingCategory>
#include <QStringList>
#include <QTimer>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_loader.h>
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/models/database/card_database_model.h>
static QString defaultXdgPath(const QString &file)
{
QString base = QDir::homePath() + "/.local/share/Cockatrice/Cockatrice/";
return base + file;
}
class CliCardDatabasePathProvider : public ICardDatabasePathProvider
{
public:
QString cardDb;
QString tokens;
QString spoilers;
QString custom;
QString getCardDatabasePath() const override
{
return cardDb;
}
QString getTokenDatabasePath() const override
{
return tokens;
}
QString getSpoilerCardDatabasePath() const override
{
return spoilers;
}
QString getCustomCardDatabasePath() const override
{
return custom;
}
};
namespace
{
struct ChurnCounters
{
qint64 cardAddedEmissions = 0;
qint64 cardRemovedEmissions = 0;
qint64 modelRowsInserted = 0;
qint64 modelRowsRemoved = 0;
qint64 loadingFinishedSignals = 0;
qint64 databaseResetSignals = 0;
qint64 modelResets = 0;
};
struct RunResult
{
qint64 elapsedMs = 0;
qint64 cardCount = 0;
qint64 setCount = 0;
LoadStatus status = NotLoaded;
ChurnCounters churn;
qint64 propsQueried = 0;
qint64 propTotal = 0;
};
void deleteCache(const QString &cardDbPath)
{
const QString cachePath = cardDbPath + ".cache";
if (QFile::exists(cachePath)) {
QFile::remove(cachePath);
}
}
RunResult runLoad(CardDatabase *db, ChurnCounters &counters)
{
const ChurnCounters before = counters;
QElapsedTimer timer;
timer.start();
db->loadCardDatabases();
RunResult result;
result.elapsedMs = timer.elapsed();
result.cardCount = db->getCardList().size();
result.setCount = db->getSetList().size();
result.status = db->getLoadStatus();
result.churn.cardAddedEmissions = counters.cardAddedEmissions - before.cardAddedEmissions;
result.churn.cardRemovedEmissions = counters.cardRemovedEmissions - before.cardRemovedEmissions;
result.churn.modelRowsInserted = counters.modelRowsInserted - before.modelRowsInserted;
result.churn.modelRowsRemoved = counters.modelRowsRemoved - before.modelRowsRemoved;
result.churn.loadingFinishedSignals = counters.loadingFinishedSignals - before.loadingFinishedSignals;
result.churn.databaseResetSignals = counters.databaseResetSignals - before.databaseResetSignals;
result.churn.modelResets = counters.modelResets - before.modelResets;
for (const CardInfoPtr &card : db->getCardList()) {
result.propTotal += card->getPropertiesHash().size();
if (!card->getProperty("colors").isEmpty()) {
++result.propsQueried;
}
}
return result;
}
void printResult(const QString &label, const RunResult &r)
{
qInfo() << "----------------------------------------";
qInfo() << label;
qInfo() << " Load wall-clock time :" << r.elapsedMs << "ms";
qInfo() << " Cards loaded :" << r.cardCount;
qInfo() << " Sets loaded :" << r.setCount;
qInfo() << " Load status :" << r.status;
qInfo() << " Properties queried :" << r.propsQueried << "/" << r.cardCount
<< " cards, total props:" << r.propTotal;
qInfo() << " CHURN during load:";
qInfo() << " cardAdded emissions :" << r.churn.cardAddedEmissions;
qInfo() << " cardRemoved emissions :" << r.churn.cardRemovedEmissions;
qInfo() << " model rowsInserted :" << r.churn.modelRowsInserted;
qInfo() << " model rowsRemoved :" << r.churn.modelRowsRemoved;
qInfo() << " loadingFinished signals :" << r.churn.loadingFinishedSignals;
qInfo() << " databaseReset signals :" << r.churn.databaseResetSignals;
qInfo() << " model resets (batch) :" << r.churn.modelResets;
}
} // namespace
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QStringList args = app.arguments();
CliCardDatabasePathProvider pathProvider;
pathProvider.cardDb = defaultXdgPath("cards.xml");
pathProvider.tokens = defaultXdgPath("tokens.xml");
pathProvider.spoilers = defaultXdgPath("spoiler.xml");
pathProvider.custom = defaultXdgPath("customsets/");
for (int i = 1; i < args.size(); ++i) {
const QString &a = args.at(i);
if (a == "--carddb" && i + 1 < args.size()) {
pathProvider.cardDb = args.at(++i);
} else if (a == "--tokens" && i + 1 < args.size()) {
pathProvider.tokens = args.at(++i);
} else if (a == "--spoilers" && i + 1 < args.size()) {
pathProvider.spoilers = args.at(++i);
} else if (a == "--custom" && i + 1 < args.size()) {
pathProvider.custom = args.at(++i);
} else if (a == "--help" || a == "-h") {
qInfo() << "Usage: load_benchmark [--carddb PATH] [--tokens PATH] [--spoilers PATH] [--custom DIR]";
return 0;
}
}
QLoggingCategory::setFilterRules("card_database.loading=true\n"
"card_database.loading.success=true\n");
qInfo() << "=== Card Database Load Benchmark ===";
qInfo() << "carddb :" << pathProvider.cardDb;
qInfo() << "tokens :" << pathProvider.tokens;
qInfo() << "spoilers :" << pathProvider.spoilers;
qInfo() << "custom :" << pathProvider.custom;
qInfo() << "";
ChurnCounters counters;
auto runScenario = [&](bool deleteCacheBefore) -> RunResult {
if (deleteCacheBefore) {
deleteCache(pathProvider.cardDb);
}
// Fresh database each scenario so signals don't accumulate across scenarios
// for the churn counters, but we keep the same counters object to get a
// running total if desired.
CardDatabase db(nullptr, new NoopCardPreferenceProvider(), &pathProvider, new NoopCardSetPriorityController());
// Re-wire churn counters for this db instance
QObject::connect(&db, &CardDatabase::cardAdded, &app,
[&counters](const CardInfoPtr &) { ++counters.cardAddedEmissions; });
QObject::connect(&db, &CardDatabase::cardRemoved, &app,
[&counters](CardInfoPtr) { ++counters.cardRemovedEmissions; });
QObject::connect(&db, &CardDatabase::cardDatabaseLoadingFinished, &app,
[&counters]() { ++counters.loadingFinishedSignals; });
QObject::connect(&db, &CardDatabase::cardDatabaseReset, &app,
[&counters]() { ++counters.databaseResetSignals; });
CardDatabaseModel model(&db, false);
QObject::connect(&model, &QAbstractItemModel::rowsInserted, &app,
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsInserted; });
QObject::connect(&model, &QAbstractItemModel::rowsRemoved, &app,
[&counters](const QModelIndex &, int, int) { ++counters.modelRowsRemoved; });
QObject::connect(&model, &QAbstractItemModel::modelReset, &app, [&counters]() { ++counters.modelResets; });
return runLoad(&db, counters);
};
// ---- Scenario 1: Cold load (XML -> write cache) ----
printResult("Scenario 1: Cold load (XML -> cache)", runScenario(true));
// ---- Scenario 2: Warm load (read cache) ----
printResult("Scenario 2: Warm load (read cache)", runScenario(false));
// Teardown
deleteCache(pathProvider.cardDb);
qInfo() << "----------------------------------------";
qInfo() << "Done.";
return 0;
}