File indexing completed on 2024-05-12 03:54:54
0001 /* 0002 This file is part of the KDE project. 0003 0004 SPDX-FileCopyrightText: 2010, 2012 Michael Pyne <mpyne@kde.org> 0005 SPDX-FileCopyrightText: 2012 Ralf Jung <ralfjung-e@gmx.de> 0006 0007 SPDX-License-Identifier: LGPL-2.0-only 0008 */ 0009 0010 #include "kshareddatacache.h" 0011 #include "kcoreaddons_debug.h" 0012 #include "ksdcmapping_p.h" 0013 #include "ksdcmemory_p.h" 0014 0015 #include "kshareddatacache_p.h" // Various auxiliary support code 0016 0017 #include <QByteArray> 0018 #include <QDir> 0019 #include <QFile> 0020 #include <QRandomGenerator> 0021 #include <QStandardPaths> 0022 0023 // The per-instance private data, such as map size, whether 0024 // attached or not, pointer to shared memory, etc. 0025 class Q_DECL_HIDDEN KSharedDataCache::Private 0026 { 0027 public: 0028 Private(const QString &name, unsigned defaultCacheSize, unsigned expectedItemSize) 0029 : m_cacheName(name) 0030 , shm(nullptr) 0031 , m_mapping(nullptr) 0032 , m_defaultCacheSize(defaultCacheSize) 0033 , m_expectedItemSize(expectedItemSize) 0034 { 0035 createMemoryMapping(); 0036 } 0037 0038 void createMemoryMapping() 0039 { 0040 shm = nullptr; 0041 m_mapping.reset(); 0042 0043 // 0-sized caches are fairly useless. 0044 unsigned cacheSize = qMax(m_defaultCacheSize, uint(SharedMemory::MINIMUM_CACHE_SIZE)); 0045 unsigned pageSize = SharedMemory::equivalentPageSize(m_expectedItemSize); 0046 0047 // Ensure that the cache is sized such that there is a minimum number of 0048 // pages available. (i.e. a cache consisting of only 1 page is fairly 0049 // useless and probably crash-prone). 0050 cacheSize = qMax(pageSize * 256, cacheSize); 0051 0052 // The m_cacheName is used to find the file to store the cache in. 0053 const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); 0054 QString cacheName = cacheDir + QLatin1String("/") + m_cacheName + QLatin1String(".kcache"); 0055 QFile file(cacheName); 0056 QFileInfo fileInfo(file); 0057 if (!QDir().mkpath(fileInfo.absolutePath())) { 0058 qCWarning(KCOREADDONS_DEBUG) << "Failed to create cache dir" << fileInfo.absolutePath(); 0059 } 0060 0061 // The basic idea is to open the file that we want to map into shared 0062 // memory, and then actually establish the mapping. Once we have mapped the 0063 // file into shared memory we can close the file handle, the mapping will 0064 // still be maintained (unless the file is resized to be shorter than 0065 // expected, which we don't handle yet :-( ) 0066 0067 // size accounts for the overhead over the desired cacheSize 0068 uint size = SharedMemory::totalSize(cacheSize, pageSize); 0069 Q_ASSERT(size >= cacheSize); 0070 0071 // Open the file and resize to some sane value if the file is too small. 0072 if (file.open(QIODevice::ReadWrite) && (file.size() >= size || (ensureFileAllocated(file.handle(), size) && file.resize(size)))) { 0073 try { 0074 m_mapping.reset(new KSDCMapping(&file, size, cacheSize, pageSize)); 0075 shm = m_mapping->m_mapped; 0076 } catch (KSDCCorrupted) { 0077 shm = nullptr; 0078 m_mapping.reset(); 0079 0080 qCWarning(KCOREADDONS_DEBUG) << "Deleting corrupted cache" << cacheName; 0081 file.remove(); 0082 QFile file(cacheName); 0083 if (file.open(QIODevice::ReadWrite) && ensureFileAllocated(file.handle(), size) && file.resize(size)) { 0084 try { 0085 m_mapping.reset(new KSDCMapping(&file, size, cacheSize, pageSize)); 0086 } catch (KSDCCorrupted) { 0087 m_mapping.reset(); 0088 qCCritical(KCOREADDONS_DEBUG) << "Even a brand-new cache starts off corrupted, something is" 0089 << "seriously wrong. :-("; 0090 } 0091 } 0092 } 0093 } 0094 0095 if (!m_mapping) { 0096 m_mapping.reset(new KSDCMapping(nullptr, size, cacheSize, pageSize)); 0097 shm = m_mapping->m_mapped; 0098 } 0099 } 0100 0101 // Called whenever the cache is apparently corrupt (for instance, a timeout trying to 0102 // lock the cache). In this situation it is safer just to destroy it all and try again. 0103 void recoverCorruptedCache() 0104 { 0105 qCWarning(KCOREADDONS_DEBUG) << "Deleting corrupted cache" << m_cacheName; 0106 0107 KSharedDataCache::deleteCache(m_cacheName); 0108 0109 createMemoryMapping(); 0110 } 0111 0112 class CacheLocker 0113 { 0114 mutable Private *d; 0115 0116 bool cautiousLock() 0117 { 0118 int lockCount = 0; 0119 0120 // Locking can fail due to a timeout. If it happens too often even though 0121 // we're taking corrective action assume there's some disastrous problem 0122 // and give up. 0123 while (!d->m_mapping->lock() && !d->m_mapping->isLockedCacheSafe()) { 0124 d->recoverCorruptedCache(); 0125 0126 if (!d->m_mapping->isValid()) { 0127 qCWarning(KCOREADDONS_DEBUG) << "Lost the connection to shared memory for cache" << d->m_cacheName; 0128 return false; 0129 } 0130 0131 if (lockCount++ > 4) { 0132 qCCritical(KCOREADDONS_DEBUG) << "There is a very serious problem with the KDE data cache" << d->m_cacheName 0133 << "giving up trying to access cache."; 0134 return false; 0135 } 0136 } 0137 0138 return true; 0139 } 0140 0141 public: 0142 CacheLocker(const Private *_d) 0143 : d(const_cast<Private *>(_d)) 0144 { 0145 if (Q_UNLIKELY(!d || !cautiousLock())) { 0146 d = nullptr; 0147 } 0148 } 0149 0150 ~CacheLocker() 0151 { 0152 if (d) { 0153 d->m_mapping->unlock(); 0154 } 0155 } 0156 0157 CacheLocker(const CacheLocker &) = delete; 0158 CacheLocker &operator=(const CacheLocker &) = delete; 0159 0160 bool failed() const 0161 { 0162 return !d; 0163 } 0164 }; 0165 0166 QString m_cacheName; 0167 SharedMemory *shm; 0168 std::unique_ptr<KSDCMapping> m_mapping; 0169 uint m_defaultCacheSize; 0170 uint m_expectedItemSize; 0171 }; 0172 0173 KSharedDataCache::KSharedDataCache(const QString &cacheName, unsigned defaultCacheSize, unsigned expectedItemSize) 0174 : d(nullptr) 0175 { 0176 try { 0177 d = new Private(cacheName, defaultCacheSize, expectedItemSize); 0178 } catch (KSDCCorrupted) { 0179 qCCritical(KCOREADDONS_DEBUG) << "Failed to initialize KSharedDataCache!"; 0180 d = nullptr; // Just in case 0181 } 0182 } 0183 0184 KSharedDataCache::~KSharedDataCache() 0185 { 0186 if (!d) { 0187 return; 0188 } 0189 0190 delete d; 0191 } 0192 0193 bool KSharedDataCache::insert(const QString &key, const QByteArray &data) 0194 { 0195 try { 0196 Private::CacheLocker lock(d); 0197 if (lock.failed()) { 0198 return false; 0199 } 0200 0201 QByteArray encodedKey = key.toUtf8(); 0202 uint keyHash = SharedMemory::generateHash(encodedKey); 0203 uint position = keyHash % d->shm->indexTableSize(); 0204 0205 // See if we're overwriting an existing entry. 0206 IndexTableEntry *indices = d->shm->indexTable(); 0207 0208 // In order to avoid the issue of a very long-lived cache having items 0209 // with a use count of 1 near-permanently, we attempt to artifically 0210 // reduce the use count of long-lived items when there is high load on 0211 // the cache. We do this randomly, with a weighting that makes the event 0212 // impossible if load < 0.5, and guaranteed if load >= 0.96. 0213 const static double startCullPoint = 0.5l; 0214 const static double mustCullPoint = 0.96l; 0215 0216 // cacheAvail is in pages, cacheSize is in bytes. 0217 double loadFactor = 1.0 - (1.0l * d->shm->cacheAvail * d->shm->cachePageSize() / d->shm->cacheSize); 0218 bool cullCollisions = false; 0219 0220 if (Q_UNLIKELY(loadFactor >= mustCullPoint)) { 0221 cullCollisions = true; 0222 } else if (loadFactor > startCullPoint) { 0223 const int tripWireValue = RAND_MAX * (loadFactor - startCullPoint) / (mustCullPoint - startCullPoint); 0224 if (QRandomGenerator::global()->bounded(RAND_MAX) >= tripWireValue) { 0225 cullCollisions = true; 0226 } 0227 } 0228 0229 // In case of collisions in the index table (i.e. identical positions), use 0230 // quadratic chaining to attempt to find an empty slot. The equation we use 0231 // is: 0232 // position = (hash + (i + i*i) / 2) % size, where i is the probe number. 0233 uint probeNumber = 1; 0234 while (indices[position].useCount > 0 && probeNumber < SharedMemory::MAX_PROBE_COUNT) { 0235 // If we actually stumbled upon an old version of the key we are 0236 // overwriting, then use that position, do not skip over it. 0237 0238 if (Q_UNLIKELY(indices[position].fileNameHash == keyHash)) { 0239 break; 0240 } 0241 0242 // If we are "culling" old entries, see if this one is old and if so 0243 // reduce its use count. If it reduces to zero then eliminate it and 0244 // use its old spot. 0245 0246 if (cullCollisions && (::time(nullptr) - indices[position].lastUsedTime) > 60) { 0247 indices[position].useCount >>= 1; 0248 if (indices[position].useCount == 0) { 0249 qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing old cached entry due to collision."; 0250 d->shm->removeEntry(position); // Remove it first 0251 break; 0252 } 0253 } 0254 0255 position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % d->shm->indexTableSize(); 0256 probeNumber++; 0257 } 0258 0259 if (indices[position].useCount > 0 && indices[position].firstPage >= 0) { 0260 qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing cached entry due to collision."; 0261 d->shm->removeEntry(position); // Remove it first 0262 } 0263 0264 // Data will be stored as fileNamefoo\0PNGimagedata..... 0265 // So total size required is the length of the encoded file name + 1 0266 // for the trailing null, and then the length of the image data. 0267 uint fileNameLength = 1 + encodedKey.length(); 0268 uint requiredSize = fileNameLength + data.size(); 0269 uint pagesNeeded = SharedMemory::intCeil(requiredSize, d->shm->cachePageSize()); 0270 uint firstPage(-1); 0271 0272 if (pagesNeeded >= d->shm->pageTableSize()) { 0273 qCWarning(KCOREADDONS_DEBUG) << key << "is too large to be cached."; 0274 return false; 0275 } 0276 0277 // If the cache has no room, or the fragmentation is too great to find 0278 // the required number of consecutive free pages, take action. 0279 if (pagesNeeded > d->shm->cacheAvail || (firstPage = d->shm->findEmptyPages(pagesNeeded)) >= d->shm->pageTableSize()) { 0280 // If we have enough free space just defragment 0281 uint freePagesDesired = 3 * qMax(1u, pagesNeeded / 2); 0282 0283 if (d->shm->cacheAvail > freePagesDesired) { 0284 // TODO: How the hell long does this actually take on real 0285 // caches? 0286 d->shm->defragment(); 0287 firstPage = d->shm->findEmptyPages(pagesNeeded); 0288 } else { 0289 // If we already have free pages we don't want to remove a ton 0290 // extra. However we can't rely on the return value of 0291 // removeUsedPages giving us a good location since we're not 0292 // passing in the actual number of pages that we need. 0293 d->shm->removeUsedPages(qMin(2 * freePagesDesired, d->shm->pageTableSize()) - d->shm->cacheAvail); 0294 firstPage = d->shm->findEmptyPages(pagesNeeded); 0295 } 0296 0297 if (firstPage >= d->shm->pageTableSize() || d->shm->cacheAvail < pagesNeeded) { 0298 qCCritical(KCOREADDONS_DEBUG) << "Unable to free up memory for" << key; 0299 return false; 0300 } 0301 } 0302 0303 // Update page table 0304 PageTableEntry *table = d->shm->pageTable(); 0305 for (uint i = 0; i < pagesNeeded; ++i) { 0306 table[firstPage + i].index = position; 0307 } 0308 0309 // Update index 0310 indices[position].fileNameHash = keyHash; 0311 indices[position].totalItemSize = requiredSize; 0312 indices[position].useCount = 1; 0313 indices[position].addTime = ::time(nullptr); 0314 indices[position].lastUsedTime = indices[position].addTime; 0315 indices[position].firstPage = firstPage; 0316 0317 // Update cache 0318 d->shm->cacheAvail -= pagesNeeded; 0319 0320 // Actually move the data in place 0321 void *dataPage = d->shm->page(firstPage); 0322 if (Q_UNLIKELY(!dataPage)) { 0323 throw KSDCCorrupted(); 0324 } 0325 0326 // Verify it will all fit 0327 d->m_mapping->verifyProposedMemoryAccess(dataPage, requiredSize); 0328 0329 // Cast for byte-sized pointer arithmetic 0330 uchar *startOfPageData = reinterpret_cast<uchar *>(dataPage); 0331 ::memcpy(startOfPageData, encodedKey.constData(), fileNameLength); 0332 ::memcpy(startOfPageData + fileNameLength, data.constData(), data.size()); 0333 0334 return true; 0335 } catch (KSDCCorrupted) { 0336 d->recoverCorruptedCache(); 0337 return false; 0338 } 0339 } 0340 0341 bool KSharedDataCache::find(const QString &key, QByteArray *destination) const 0342 { 0343 try { 0344 Private::CacheLocker lock(d); 0345 if (lock.failed()) { 0346 return false; 0347 } 0348 0349 // Search in the index for our data, hashed by key; 0350 QByteArray encodedKey = key.toUtf8(); 0351 qint32 entry = d->shm->findNamedEntry(encodedKey); 0352 0353 if (entry >= 0) { 0354 const IndexTableEntry *header = &d->shm->indexTable()[entry]; 0355 const void *resultPage = d->shm->page(header->firstPage); 0356 if (Q_UNLIKELY(!resultPage)) { 0357 throw KSDCCorrupted(); 0358 } 0359 0360 d->m_mapping->verifyProposedMemoryAccess(resultPage, header->totalItemSize); 0361 0362 header->useCount++; 0363 header->lastUsedTime = ::time(nullptr); 0364 0365 // Our item is the key followed immediately by the data, so skip 0366 // past the key. 0367 const char *cacheData = reinterpret_cast<const char *>(resultPage); 0368 cacheData += encodedKey.size(); 0369 cacheData++; // Skip trailing null -- now we're pointing to start of data 0370 0371 if (destination) { 0372 *destination = QByteArray(cacheData, header->totalItemSize - encodedKey.size() - 1); 0373 } 0374 0375 return true; 0376 } 0377 } catch (KSDCCorrupted) { 0378 d->recoverCorruptedCache(); 0379 } 0380 0381 return false; 0382 } 0383 0384 void KSharedDataCache::clear() 0385 { 0386 try { 0387 Private::CacheLocker lock(d); 0388 0389 if (!lock.failed()) { 0390 d->shm->clear(); 0391 } 0392 } catch (KSDCCorrupted) { 0393 d->recoverCorruptedCache(); 0394 } 0395 } 0396 0397 bool KSharedDataCache::contains(const QString &key) const 0398 { 0399 try { 0400 Private::CacheLocker lock(d); 0401 if (lock.failed()) { 0402 return false; 0403 } 0404 0405 return d->shm->findNamedEntry(key.toUtf8()) >= 0; 0406 } catch (KSDCCorrupted) { 0407 d->recoverCorruptedCache(); 0408 return false; 0409 } 0410 } 0411 0412 void KSharedDataCache::deleteCache(const QString &cacheName) 0413 { 0414 QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") + cacheName + QLatin1String(".kcache"); 0415 0416 // Note that it is important to simply unlink the file, and not truncate it 0417 // smaller first to avoid SIGBUS errors and similar with shared memory 0418 // attached to the underlying inode. 0419 qCDebug(KCOREADDONS_DEBUG) << "Removing cache at" << cachePath; 0420 QFile::remove(cachePath); 0421 } 0422 0423 unsigned KSharedDataCache::totalSize() const 0424 { 0425 try { 0426 Private::CacheLocker lock(d); 0427 if (lock.failed()) { 0428 return 0u; 0429 } 0430 0431 return d->shm->cacheSize; 0432 } catch (KSDCCorrupted) { 0433 d->recoverCorruptedCache(); 0434 return 0u; 0435 } 0436 } 0437 0438 unsigned KSharedDataCache::freeSize() const 0439 { 0440 try { 0441 Private::CacheLocker lock(d); 0442 if (lock.failed()) { 0443 return 0u; 0444 } 0445 0446 return d->shm->cacheAvail * d->shm->cachePageSize(); 0447 } catch (KSDCCorrupted) { 0448 d->recoverCorruptedCache(); 0449 return 0u; 0450 } 0451 } 0452 0453 KSharedDataCache::EvictionPolicy KSharedDataCache::evictionPolicy() const 0454 { 0455 if (d && d->shm) { 0456 return static_cast<EvictionPolicy>(d->shm->evictionPolicy.fetchAndAddAcquire(0)); 0457 } 0458 0459 return NoEvictionPreference; 0460 } 0461 0462 void KSharedDataCache::setEvictionPolicy(EvictionPolicy newPolicy) 0463 { 0464 if (d && d->shm) { 0465 d->shm->evictionPolicy.fetchAndStoreRelease(static_cast<int>(newPolicy)); 0466 } 0467 } 0468 0469 unsigned KSharedDataCache::timestamp() const 0470 { 0471 if (d && d->shm) { 0472 return static_cast<unsigned>(d->shm->cacheTimestamp.fetchAndAddAcquire(0)); 0473 } 0474 0475 return 0; 0476 } 0477 0478 void KSharedDataCache::setTimestamp(unsigned newTimestamp) 0479 { 0480 if (d && d->shm) { 0481 d->shm->cacheTimestamp.fetchAndStoreRelease(static_cast<int>(newTimestamp)); 0482 } 0483 }