Files
Cockatrice/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp
T
749223c2dc [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>
2026-07-27 23:12:53 +02:00

238 lines
8.5 KiB
C++

#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,
CardDatabase *db,
ICardDatabasePathProvider *_pathProvider,
ICardPreferenceProvider *_preferenceProvider,
ICardSetPriorityController *_priorityController)
: QObject(parent), database(db), pathProvider(_pathProvider), priorityController(_priorityController)
{
// instantiate available parsers here
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
availableParsers << new CockatriceXml3Parser(_priorityController);
// 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,
&CardDatabaseLoader::loadCardDatabases);
}
CardDatabaseLoader::~CardDatabaseLoader()
{
qDeleteAll(availableParsers);
availableParsers.clear();
}
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName, CardDatabaseData &data)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
return FileError;
}
for (auto parser : availableParsers) {
file.reset();
if (parser->getCanParseFile(fileName, file)) {
file.reset();
parser->parseFileInto(file, data);
return Ok;
}
}
return Invalid;
}
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabaseData &data)
{
auto startTime = QTime::currentTime();
LoadStatus tempLoadStatus = NotLoaded;
if (!path.isEmpty()) {
QMutexLocker locker(loadFromFileMutex);
tempLoadStatus = loadFromFile(path, data);
}
int msecs = startTime.msecsTo(QTime::currentTime());
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
<< "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);
if (!database) {
qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer";
emit loadingFailed();
return FileError;
}
emit loadingStarted();
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
ICardDatabaseParser::clearSetlist();
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
for (int i = 0; i < customPaths.size(); ++i) {
const auto &p = customPaths.at(i);
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
loadCardDatabase(p, data);
}
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->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
}
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,
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
QStringList paths;
while (it.hasNext()) {
paths << it.next();
}
paths.sort();
return paths;
}
bool CardDatabaseLoader::saveCustomTokensToFile()
{
if (!database) {
qCWarning(CardDatabaseLog) << "saveCustomTokensToFile: database pointer missing";
return false;
}
QString fileName = pathProvider->getCustomCardDatabasePath() + "/" + CardSet::TOKENS_SETNAME + ".xml";
SetNameMap tmpSets;
CardSetPtr customTokensSet = database->getSet(CardSet::TOKENS_SETNAME);
tmpSets.insert(CardSet::TOKENS_SETNAME, customTokensSet);
CardNameMap tmpCards;
for (const CardInfoPtr &card : database->cards) {
if (card->getSets().contains(CardSet::TOKENS_SETNAME)) {
tmpCards.insert(card->getName(), card);
}
}
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
return true;
}