mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
* [CardDB] Stream card properties from blobs Parsed and cached cards used to keep a materialized QHash<QString, QString> for their properties: LazyPropertiesHash only deferred building that hash until the first query, and the cache writer re-serialized it on save. - LazyPropertiesHash answers value(), contains(), size(), isEmpty() and keys() straight out of the serialized blob, so a property query no longer builds a hash. - serialize() is public, and the cache writer passes the stored blob through instead of serializing a hash it just rebuilt. - CardInfo and PrintingInfo expose getPropertiesBlob() and getPropertyCount(); PrintingInfo::operator== compares key by key without materializing either side. - The XML parsers build cards via newInstanceFromPropertiesBlob(), which derives the simple name and the alternate names that the parser used to keep next to the hash. The reload cycle fix stays out of this change; it depends on changing the CardInfo self pointer and is reviewed separately. * [CardDB] Address review comments on streaming properties * [CardDB] Address final review nits on property streaming Move BlobReader and the blob accessors into LazyPropertiesHash as private members so the parsing helper is declared with the class instead of hiding in the translation unit. Add LazyPropertiesHash::serializedFrom() and use it where a PrintingInfo is built from a fresh property hash, replacing the nested LazyPropertiesHash(LazyPropertiesHash::serialize(props)) construction. The pre-materialized QHash constructor stays: CardInfo's non-blob factory still uses it for the oracle import path, and the tests cover it directly. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
240 lines
8.5 KiB
C++
240 lines
8.5 KiB
C++
/*
|
|
* 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 "libcockatrice/card/card_info.h"
|
|
#include "libcockatrice/card/set/card_set_list.h"
|
|
|
|
#include <QAbstractItemModel>
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QElapsedTimer>
|
|
#include <QFile>
|
|
#include <QHash>
|
|
#include <QList>
|
|
#include <QLoggingCategory>
|
|
#include <QObject>
|
|
#include <QSharedPointer>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QtGlobal>
|
|
#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>
|
|
#include <qlogging.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->getProperties().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;
|
|
}
|