File indexing completed on 2024-05-12 05:26:26

0001 #include <QTest>
0002 
0003 #include "dummy_generated.h"
0004 
0005 #include "hawd/dataset.h"
0006 #include "hawd/formatter.h"
0007 #include "common/storage.h"
0008 #include "common/storage/key.h"
0009 #include "common/log.h"
0010 
0011 #include <definitions.h>
0012 #include <fstream>
0013 
0014 #include <QDebug>
0015 #include <QString>
0016 #include <QTime>
0017 
0018 using namespace Sink::ApplicationDomain::Buffer;
0019 using namespace flatbuffers;
0020 
0021 static QByteArray createEntity()
0022 {
0023     static const size_t attachmentSize = 1024 * 2; // 2KB
0024     static uint8_t rawData[attachmentSize];
0025     static FlatBufferBuilder fbb;
0026     fbb.Clear();
0027     {
0028         uint8_t *rawDataPtr = Q_NULLPTR;
0029         auto summary = fbb.CreateString("summary");
0030         auto data = fbb.CreateUninitializedVector<uint8_t>(attachmentSize, &rawDataPtr);
0031         DummyBuilder builder(fbb);
0032         builder.add_summary(summary);
0033         builder.add_attachment(data);
0034         FinishDummyBuffer(fbb, builder.Finish());
0035         memcpy((void *)rawDataPtr, rawData, attachmentSize);
0036     }
0037 
0038     return QByteArray::fromRawData(reinterpret_cast<const char *>(fbb.GetBufferPointer()), fbb.GetSize());
0039 }
0040 
0041 /**
0042  * Benchmark the storage implementation.
0043  */
0044 class StorageBenchmark : public QObject
0045 {
0046     Q_OBJECT
0047 private:
0048     // This should point to a directory on disk and not a ramdisk (since we're measuring performance)
0049     QString testDataPath;
0050     QString dbName;
0051     QString filePath;
0052     const int count = 50000;
0053 
0054 private slots:
0055     void initTestCase()
0056     {
0057         // testDataPath = Sink::storageLocation() + "/testdb";
0058         testDataPath = "./testdb";
0059         dbName = "test";
0060         filePath = testDataPath + "buffer.fb";
0061     }
0062 
0063     void cleanupTestCase()
0064     {
0065         Sink::Storage::DataStore store(testDataPath, dbName);
0066         store.removeFromDisk();
0067     }
0068 
0069     void testWriteRead()
0070     {
0071         auto entity = createEntity();
0072 
0073         QScopedPointer<Sink::Storage::DataStore> store(new Sink::Storage::DataStore(testDataPath, dbName, Sink::Storage::DataStore::ReadWrite));
0074 
0075         const char *keyPrefix = "key";
0076 
0077         QTime time;
0078         time.start();
0079         // Test db write time
0080         {
0081             auto transaction = store->createTransaction(Sink::Storage::DataStore::ReadWrite);
0082             for (int i = 0; i < count; i++) {
0083                 transaction.openDatabase().write(keyPrefix + QByteArray::number(i), entity);
0084                 if ((i % 10000) == 0) {
0085                     transaction.commit();
0086                     transaction = store->createTransaction(Sink::Storage::DataStore::ReadWrite);
0087                 }
0088             }
0089             transaction.commit();
0090         }
0091         qreal dbWriteDuration = time.restart();
0092         qreal dbWriteOpsPerMs = count / dbWriteDuration;
0093 
0094         // Test file write time
0095         {
0096             std::ofstream myfile;
0097             myfile.open(filePath.toStdString());
0098             for (int i = 0; i < count; i++) {
0099                 myfile << entity.toStdString();
0100             }
0101             myfile.close();
0102         }
0103         qreal fileWriteDuration = time.restart();
0104         qreal fileWriteOpsPerMs = count / fileWriteDuration;
0105 
0106         // Db read time
0107         {
0108             auto transaction = store->createTransaction(Sink::Storage::DataStore::ReadOnly);
0109             auto db = transaction.openDatabase();
0110             for (int i = 0; i < count; i++) {
0111                 db.scan(keyPrefix + QByteArray::number(i), [](const QByteArray &key, const QByteArray &value) -> bool { return true; });
0112             }
0113         }
0114         qreal readDuration = time.restart();
0115         qreal readOpsPerMs = count / readDuration;
0116 
0117         HAWD::Dataset dataset("storage_readwrite", m_hawdState);
0118         HAWD::Dataset::Row row = dataset.row();
0119         row.setValue("rows", count);
0120         row.setValue("dbWrite", dbWriteOpsPerMs);
0121         row.setValue("fileWrite", fileWriteOpsPerMs);
0122         row.setValue("dbRead", readOpsPerMs);
0123         dataset.insertRow(row);
0124         HAWD::Formatter::print(dataset);
0125 
0126         {
0127             Sink::Storage::DataStore store(testDataPath, dbName);
0128             QFileInfo fileInfo(filePath);
0129 
0130             HAWD::Dataset dataset("storage_sizes", m_hawdState);
0131             HAWD::Dataset::Row row = dataset.row();
0132             row.setValue("rows", count);
0133             row.setValue("dbSize", store.diskUsage() / 1024);
0134             row.setValue("fileSize", fileInfo.size() / 1024);
0135             dataset.insertRow(row);
0136             HAWD::Formatter::print(dataset);
0137         }
0138     }
0139 
0140     void testScan()
0141     {
0142         QScopedPointer<Sink::Storage::DataStore> store(new Sink::Storage::DataStore(testDataPath, dbName, Sink::Storage::DataStore::ReadOnly));
0143 
0144         QBENCHMARK {
0145             int hit = 0;
0146             store->createTransaction(Sink::Storage::DataStore::ReadOnly)
0147                 .openDatabase()
0148                 .scan("", [&](const QByteArray &key, const QByteArray &value) -> bool {
0149                     if (key == "key10000") {
0150                         // qDebug() << "hit";
0151                         hit++;
0152                     }
0153                     return true;
0154                 });
0155             QCOMPARE(hit, 1);
0156         }
0157     }
0158 
0159     void testKeyLookup()
0160     {
0161         QScopedPointer<Sink::Storage::DataStore> store(new Sink::Storage::DataStore(testDataPath, dbName, Sink::Storage::DataStore::ReadOnly));
0162         auto transaction = store->createTransaction(Sink::Storage::DataStore::ReadOnly);
0163         auto db = transaction.openDatabase();
0164 
0165         QBENCHMARK {
0166             int hit = 0;
0167             db.scan("key40000", [&](const QByteArray &key, const QByteArray &value) -> bool {
0168                 hit++;
0169                 return true;
0170             });
0171             QCOMPARE(hit, 1);
0172         }
0173     }
0174 
0175     void testFindLatest()
0176     {
0177         QScopedPointer<Sink::Storage::DataStore> store(new Sink::Storage::DataStore(testDataPath, dbName, Sink::Storage::DataStore::ReadOnly));
0178         auto transaction = store->createTransaction(Sink::Storage::DataStore::ReadOnly);
0179         auto db = transaction.openDatabase();
0180 
0181         QBENCHMARK {
0182             int hit = 0;
0183             db.findLatest("key40000", [&](const QByteArray &key, const QByteArray &value) -> bool {
0184                 hit++;
0185                 return true;
0186             });
0187             QCOMPARE(hit, 1);
0188         }
0189     }
0190 
0191     void testBufferCreation()
0192     {
0193         HAWD::Dataset dataset("buffer_creation", m_hawdState);
0194         HAWD::Dataset::Row row = dataset.row();
0195 
0196         QTime time;
0197         time.start();
0198 
0199         for (int i = 0; i < count; i++) {
0200             auto entity = createEntity();
0201             Q_UNUSED(entity);
0202         }
0203 
0204         qreal bufferDuration = time.restart();
0205         qreal opsPerMs = count / bufferDuration;
0206         row.setValue("numBuffers", count);
0207         row.setValue("time", bufferDuration);
0208         row.setValue("ops", opsPerMs);
0209         dataset.insertRow(row);
0210         HAWD::Formatter::print(dataset);
0211     }
0212 
0213     /*
0214      * This benchmark is supposed to roughly emulate the workload we get during a large sync.
0215      *
0216      * Observed behavior is that without a concurrent read-only transaction free pages are mostly reused,
0217      * but commits start to take longer and longer.
0218      * With a concurrent read-only transaction free pages are kept from being reused and commits a greatly sped up.
0219      *
0220      * NOTE: At least in podman there seems to be a massive performance difference whether we use a path within the home directory,
0221      * or in the bind-mounted build directory. This is probably because the home directory is a fuse-overlayfs, and the build directory is
0222      * just the underlying ext4 volume. I suppose the overlayfs is not suitable for this specific workload.
0223      */
0224     void testWrite()
0225     {
0226         Sink::Storage::DataStore(testDataPath, dbName).removeFromDisk();
0227 
0228         using namespace Sink::Storage;
0229 
0230         auto entity = createEntity();
0231         qWarning() << "entity size " << entity.size();
0232         Sink::Storage::DataStore store(testDataPath, dbName, Sink::Storage::DataStore::ReadWrite);
0233 
0234         QTime time;
0235         time.start();
0236         // Test db write time
0237         {
0238             //This read-only transaction will make commits fast but cause a quick buildup of free pages.
0239             // auto rotransaction = store.createTransaction(Sink::Storage::DataStore::ReadOnly);
0240             auto transaction = store.createTransaction(Sink::Storage::DataStore::ReadWrite);
0241             for (int i = 0; i < count; i++) {
0242                 time.start();
0243                 const auto identifier = Identifier::fromDisplayByteArray(Sink::Storage::DataStore::generateUid());
0244                 const qint64 newRevision = Sink::Storage::DataStore::maxRevision(transaction) + 1;
0245                 const auto key = Sink::Storage::Key(identifier, newRevision);
0246 
0247                 DataStore::mainDatabase(transaction, "mail")
0248                     .write(newRevision, entity,
0249                         [&](const DataStore::Error &error) { qWarning() << "Failed to write entity" << identifier << newRevision; });
0250 
0251                 DataStore::setMaxRevision(transaction, newRevision);
0252                 DataStore::recordRevision(transaction, newRevision, identifier, "mail");
0253                 DataStore::recordUid(transaction, identifier, "mail");
0254 
0255                 auto db = transaction.openDatabase("mail.index.messageId" , std::function<void(const Sink::Storage::DataStore::Error &)>(), Sink::Storage::AllowDuplicates);
0256                 db.write("messageid", key.toInternalByteArray(), [&] (const Sink::Storage::DataStore::Error &error) {
0257                     qWarning() << "Error while writing value" << error;
0258                 });
0259 
0260                 if ((i % 100) == 0) {
0261                     transaction.commit();
0262                     qWarning() << "Commit " <<  i << time.elapsed() << "[ms]";
0263                     transaction = store.createTransaction(Sink::Storage::DataStore::ReadWrite);
0264                     // rotransaction = store.createTransaction(Sink::Storage::DataStore::ReadOnly);
0265                     qWarning() << "Free pages " << store.createTransaction(Sink::Storage::DataStore::ReadOnly).stat(false).freePages;
0266                 }
0267             }
0268             transaction.commit();
0269         }
0270         qreal dbWriteDuration = time.elapsed();
0271         qreal dbWriteOpsPerMs = count / dbWriteDuration;
0272         {
0273             auto transaction = store.createTransaction(Sink::Storage::DataStore::ReadOnly);
0274             auto stat = transaction.stat(false);
0275             qWarning() << "Write duration " << dbWriteDuration;
0276             qWarning() << "Write per ms " << dbWriteOpsPerMs;
0277             qWarning() << "free " << stat.freePages;
0278             qWarning() << "total " << stat.totalPages;
0279 
0280             QList<QByteArray> databases = transaction.getDatabaseNames();
0281             for (const auto &databaseName : databases) {
0282                 auto db = transaction.openDatabase(databaseName);
0283                 qint64 size = db.getSize() / 1024;
0284                 qWarning() << QString("%1: %2 [kb]").arg(QString(databaseName)).arg(size);
0285             }
0286         }
0287     }
0288 
0289 private:
0290     HAWD::State m_hawdState;
0291 };
0292 
0293 QTEST_MAIN(StorageBenchmark)
0294 #include "storagebenchmark.moc"