File indexing completed on 2024-12-15 04:54:42

0001 /******************************************************************************
0002  *
0003  *  SPDX-FileCopyrightText: 2016 Daniel Vrátil <dvratil@kde.org>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  *
0007  *******************************************************************************/
0008 
0009 #include "threadingcache.h"
0010 #include "aggregation.h"
0011 #include "messagelist_debug.h"
0012 
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QStandardPaths>
0017 
0018 using namespace MessageList::Core;
0019 
0020 namespace
0021 {
0022 struct CacheHeader {
0023     int version;
0024     Aggregation::Grouping grouping;
0025     Aggregation::Threading threading;
0026     Aggregation::ThreadLeader threadLeader;
0027     int cacheSize;
0028 };
0029 
0030 QDebug operator<<(QDebug d, const CacheHeader &t)
0031 {
0032     d << " grouping " << t.grouping;
0033     d << " threading " << t.threading;
0034     d << " threadLeader " << t.threadLeader;
0035     d << " grouping " << t.grouping;
0036     d << " cacheSize " << t.cacheSize;
0037     return d;
0038 }
0039 
0040 QDataStream &operator<<(QDataStream &stream, const CacheHeader &header)
0041 {
0042     return stream << header.version << (qint8)header.grouping << (qint8)header.threading << (qint8)header.threadLeader << header.cacheSize;
0043 }
0044 
0045 QDataStream &operator>>(QDataStream &stream, CacheHeader &header)
0046 {
0047     return stream >> header.version >> (qint8 &)header.grouping >> (qint8 &)header.threading >> (qint8 &)header.threadLeader >> header.cacheSize;
0048 }
0049 }
0050 
0051 ThreadingCache::ThreadingCache() = default;
0052 
0053 ThreadingCache::~ThreadingCache()
0054 {
0055     if (mEnabled && !mCacheId.isEmpty()) {
0056         save();
0057     }
0058 }
0059 
0060 bool ThreadingCache::isEnabled() const
0061 {
0062     return mEnabled;
0063 }
0064 
0065 void ThreadingCache::setEnabled(bool enabled)
0066 {
0067     mEnabled = enabled;
0068 }
0069 
0070 void ThreadingCache::load(const QString &id, const Aggregation *aggregation)
0071 {
0072     mParentCache.clear();
0073     mItemCache.clear();
0074 
0075     mCacheId = id;
0076     mThreading = aggregation->threading();
0077     mThreadLeader = aggregation->threadLeader();
0078     mGrouping = aggregation->grouping();
0079     mEnabled = true;
0080 
0081     if (id.isEmpty()) {
0082         qCDebug(MESSAGELIST_LOG) << "Invalid collection: id is empty";
0083         return;
0084     }
0085 
0086     const QString cacheFileName =
0087         QStandardPaths::locate(QStandardPaths::CacheLocation, QStringLiteral("messagelist/threading/%1").arg(id), QStandardPaths::LocateFile);
0088     if (cacheFileName.isEmpty()) {
0089         qCDebug(MESSAGELIST_LOG) << "No threading cache file for collection" << id;
0090         return;
0091     }
0092     qCDebug(MESSAGELIST_LOG) << "Loading threading cache file" << cacheFileName;
0093 
0094     QFile cacheFile(cacheFileName);
0095     if (!cacheFile.open(QIODevice::ReadOnly)) {
0096         qCWarning(MESSAGELIST_LOG) << "Failed to open cache file" << cacheFileName << ":" << cacheFile.errorString();
0097         return;
0098     }
0099 
0100     QDataStream stream(&cacheFile);
0101     stream.setVersion(QDataStream::Qt_5_15);
0102     CacheHeader cacheHeader = {};
0103     stream >> cacheHeader;
0104 
0105     if (cacheHeader.version != 1) {
0106         // Unknown version
0107         qCDebug(MESSAGELIST_LOG) << "\tCache file unusable, unknown version";
0108         cacheFile.close();
0109         cacheFile.remove();
0110         return;
0111     }
0112 
0113     if (cacheHeader.grouping != mGrouping || cacheHeader.threading != mThreading || cacheHeader.threadLeader != mThreadLeader) {
0114         // The cache is valid, but for a different grouping/threading configuration.
0115         qCDebug(MESSAGELIST_LOG) << "\tCache file unusable, threading configuration mismatch";
0116         cacheFile.close();
0117         cacheFile.remove();
0118         return;
0119     }
0120     const qsizetype values = cacheHeader.cacheSize;
0121     mItemCache.reserve(values);
0122     mParentCache.reserve(values);
0123 
0124     for (int i = 0; i < values; ++i) {
0125         qint64 child;
0126         qint64 parent;
0127         stream >> child >> parent;
0128         if (stream.status() != QDataStream::Ok) {
0129             // Suspect corrupted cache
0130             qCDebug(MESSAGELIST_LOG) << "\tCache file unusable, data truncated";
0131             cacheFile.close();
0132             cacheFile.remove();
0133             mParentCache.clear();
0134             return;
0135         }
0136 
0137         mParentCache.insert(child, parent);
0138     }
0139 
0140     qCDebug(MESSAGELIST_LOG) << "Loaded" << values << "entries from threading cache";
0141 }
0142 
0143 void ThreadingCache::save()
0144 {
0145     if (mCacheId.isEmpty()) {
0146         return;
0147     }
0148     const QDir cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
0149     if (!cacheDir.exists(QStringLiteral("messagelist/threading"))) {
0150         if (!cacheDir.mkpath(QStringLiteral("messagelist/threading"))) {
0151             qCWarning(MESSAGELIST_LOG) << "Failed to create cache directory.";
0152             return;
0153         }
0154     }
0155 
0156     QFile cacheFile(cacheDir.filePath(QStringLiteral("messagelist/threading/%1").arg(mCacheId)));
0157     if (!cacheFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
0158         qCWarning(MESSAGELIST_LOG) << "Failed to create cache file:" << cacheFile.errorString() << " mCacheId " << mCacheId;
0159         return;
0160     }
0161 
0162     qCDebug(MESSAGELIST_LOG) << "Saving threading cache to" << cacheFile.fileName();
0163 
0164     QDataStream stream(&cacheFile);
0165     stream.setVersion(QDataStream::Qt_5_15);
0166     CacheHeader cacheHeader{1, // version
0167                             mGrouping,
0168                             mThreading,
0169                             mThreadLeader,
0170                             static_cast<int>(mParentCache.size())};
0171     stream << cacheHeader;
0172     cacheFile.flush();
0173 
0174     for (auto iter = mParentCache.cbegin(), end = mParentCache.cend(); iter != end; ++iter) {
0175         stream << iter.key() << iter.value();
0176     }
0177     qCDebug(MESSAGELIST_LOG) << "Saved" << mParentCache.count() << "cache entries";
0178 }
0179 
0180 void ThreadingCache::addItemToCache(MessageItem *mi)
0181 {
0182     if (mEnabled) {
0183         mItemCache.insert(mi->itemId(), mi);
0184     }
0185 }
0186 
0187 void ThreadingCache::updateParent(MessageItem *mi, MessageItem *parent)
0188 {
0189     if (mEnabled) {
0190         mParentCache.insert(mi->itemId(), parent ? parent->itemId() : 0);
0191     }
0192 }
0193 
0194 MessageItem *ThreadingCache::parentForItem(MessageItem *mi, qint64 &parentId) const
0195 {
0196     if (mEnabled) {
0197         parentId = mParentCache.value(mi->itemId(), -1);
0198         if (parentId > -1) {
0199             return mItemCache.value(parentId, nullptr);
0200         } else {
0201             return nullptr;
0202         }
0203     } else {
0204         return nullptr;
0205     }
0206 }
0207 
0208 void ThreadingCache::expireParent(MessageItem *item)
0209 {
0210     if (mEnabled) {
0211         mParentCache.remove(item->itemId());
0212         mItemCache.remove(item->itemId());
0213     }
0214 }