* This file is part of the KDE project.
*
* SPDX-FileCopyrightText: 2010 Michael Pyne <mpyne@kde.org>
* SPDX-License-Identifier: LGPL-2.0-only
*
* This library includes "MurmurHash" code from Austin Appleby, which is
* placed in the public domain. See http://sites.google.com/site/murmurhash/
*/
#include "kcoreaddons_debug.h"
#include "ksdcmemory_p.h"
#include <QByteArray>
static unsigned int MurmurHashAligned(const void *key, int len, unsigned int seed)
{
const unsigned int m = 0xc6a4a793;
const int r = 16;
const unsigned char *data = reinterpret_cast<const unsigned char *>(key);
unsigned int h = seed ^ (len * m);
int align = reinterpret_cast<quintptr>(data) & 3;
if (align && len >= 4) {
unsigned int t = 0;
unsigned int d = 0;
switch (align) {
case 1:
t |= data[2] << 16;
Q_FALLTHROUGH();
case 2:
t |= data[1] << 8;
Q_FALLTHROUGH();
case 3:
t |= data[0];
}
t <<= (8 * align);
data += 4 - align;
len -= 4 - align;
int sl = 8 * (4 - align);
int sr = 8 * align;
while (len >= 4) {
d = *reinterpret_cast<const unsigned int *>(data);
t = (t >> sr) | (d << sl);
h += t;
h *= m;
h ^= h >> r;
t = d;
data += 4;
len -= 4;
}
int pack = len < align ? len : align;
d = 0;
switch (pack) {
case 3:
d |= data[2] << 16;
Q_FALLTHROUGH();
case 2:
d |= data[1] << 8;
Q_FALLTHROUGH();
case 1:
d |= data[0];
Q_FALLTHROUGH();
case 0:
h += (t >> sr) | (d << sl);
h *= m;
h ^= h >> r;
}
data += pack;
len -= pack;
} else {
while (len >= 4) {
h += *reinterpret_cast<const unsigned int *>(data);
h *= m;
h ^= h >> r;
data += 4;
len -= 4;
}
}
switch (len) {
case 3:
h += data[2] << 16;
Q_FALLTHROUGH();
case 2:
h += data[1] << 8;
Q_FALLTHROUGH();
case 1:
h += data[0];
h *= m;
h ^= h >> r;
};
h *= m;
h ^= h >> 10;
h *= m;
h ^= h >> 17;
return h;
}
* This is the hash function used for our data to hopefully make the
* hashing used to place the QByteArrays as efficient as possible.
*/
quint32 SharedMemory::generateHash(const QByteArray &buffer)
{
return MurmurHashAligned(buffer.data(), buffer.size(), 0xF0F00F0F);
}
#ifndef ALIGNOF
#if defined(Q_CC_GNU) || defined(Q_CC_SUN)
#define ALIGNOF(x) (__alignof__(x))
#else
#include <stddef.h>
template<class T>
struct __alignmentHack {
char firstEntry;
T obj;
static const size_t size = offsetof(__alignmentHack, obj);
};
#define ALIGNOF(x) (__alignmentHack<x>::size)
#endif
#endif
template<class T>
T *alignTo(const void *start, uint size = ALIGNOF(T))
{
quintptr mask = size - 1;
quintptr basePointer = reinterpret_cast<quintptr>(start);
basePointer = (basePointer + mask) & ~mask;
return reinterpret_cast<T *>(basePointer);
}
* Returns a pointer to a const object of type T, assumed to be @p offset
* *BYTES* greater than the base address. Note that in order to meet alignment
* requirements for T, it is possible that the returned pointer points greater
* than @p offset into @p base.
*/
template<class T>
const T *offsetAs(const void *const base, qint32 offset)
{
const char *ptr = reinterpret_cast<const char *>(base);
return alignTo<const T>(ptr + offset);
}
template<class T>
T *offsetAs(void *const base, qint32 offset)
{
char *ptr = reinterpret_cast<char *>(base);
return alignTo<T>(ptr + offset);
}
* @return the smallest integer greater than or equal to (@p a / @p b).
* @param a Numerator, should be ≥ 0.
* @param b Denominator, should be > 0.
*/
unsigned SharedMemory::intCeil(unsigned a, unsigned b)
{
if (Q_UNLIKELY(b == 0 || ((a + b) < a))) {
throw KSDCCorrupted();
}
return (a + b - 1) / b;
}
* @return number of set bits in @p value (see also "Hamming weight")
*/
static unsigned countSetBits(unsigned value)
{
unsigned count = 0;
for (count = 0; value != 0; count++) {
value &= (value - 1);
}
return count;
}
* Converts the given average item size into an appropriate page size.
*/
unsigned SharedMemory::equivalentPageSize(unsigned itemSize)
{
if (itemSize == 0) {
return 4096;
}
int log2OfSize = 0;
while ((itemSize >>= 1) != 0) {
log2OfSize++;
}
log2OfSize = qBound(9, log2OfSize, 18);
return (1 << log2OfSize);
}
unsigned SharedMemory::cachePageSize() const
{
unsigned _pageSize = static_cast<unsigned>(pageSize.loadRelaxed());
static const unsigned validSizeMask = 0x7FE00u;
if (Q_UNLIKELY(countSetBits(_pageSize) != 1 || (_pageSize & ~validSizeMask))) {
throw KSDCCorrupted();
}
return _pageSize;
}
* This is effectively the class ctor. But since we're in shared memory,
* there's a few rules:
*
* 1. To allow for some form of locking in the initial-setup case, we
* use an atomic int, which will be initialized to 0 by mmap(). Then to
* take the lock we atomically increment the 0 to 1. If we end up calling
* the QAtomicInt constructor we can mess that up, so we can't use a
* constructor for this class either.
* 2. Any member variable you add takes up space in shared memory as well,
* so make sure you need it.
*/
bool SharedMemory::performInitialSetup(uint _cacheSize, uint _pageSize)
{
if (_cacheSize < MINIMUM_CACHE_SIZE) {
qCCritical(KCOREADDONS_DEBUG) << "Internal error: Attempted to create a cache sized < " << MINIMUM_CACHE_SIZE;
return false;
}
if (_pageSize == 0) {
qCCritical(KCOREADDONS_DEBUG) << "Internal error: Attempted to create a cache with 0-sized pages.";
return false;
}
shmLock.type = findBestSharedLock();
if (shmLock.type == LOCKTYPE_INVALID) {
qCCritical(KCOREADDONS_DEBUG) << "Unable to find an appropriate lock to guard the shared cache. "
<< "This *should* be essentially impossible. :(";
return false;
}
bool isProcessShared = false;
std::unique_ptr<KSDCLock> tempLock(createLockFromId(shmLock.type, shmLock));
if (!tempLock->initialize(isProcessShared)) {
qCCritical(KCOREADDONS_DEBUG) << "Unable to initialize the lock for the cache!";
return false;
}
if (!isProcessShared) {
qCWarning(KCOREADDONS_DEBUG) << "Cache initialized, but does not support being"
<< "shared across processes.";
}
cacheSize = _cacheSize;
pageSize = _pageSize;
version = PIXMAP_CACHE_VERSION;
cacheTimestamp = static_cast<unsigned>(::time(nullptr));
clearInternalTables();
return ready.ref();
}
void SharedMemory::clearInternalTables()
{
cacheAvail = pageTableSize();
PageTableEntry *table = pageTable();
for (uint i = 0; i < pageTableSize(); ++i) {
table[i].index = -1;
}
IndexTableEntry *indices = indexTable();
for (uint i = 0; i < indexTableSize(); ++i) {
indices[i].firstPage = -1;
indices[i].useCount = 0;
indices[i].fileNameHash = 0;
indices[i].totalItemSize = 0;
indices[i].addTime = 0;
indices[i].lastUsedTime = 0;
}
}
const IndexTableEntry *SharedMemory::indexTable() const
{
return offsetAs<IndexTableEntry>(this, sizeof(*this));
}
const PageTableEntry *SharedMemory::pageTable() const
{
const IndexTableEntry *base = indexTable();
base += indexTableSize();
return alignTo<PageTableEntry>(base);
}
const void *SharedMemory::cachePages() const
{
const PageTableEntry *tableStart = pageTable();
tableStart += pageTableSize();
return alignTo<void>(tableStart, cachePageSize());
}
const void *SharedMemory::page(pageID at) const
{
if (static_cast<uint>(at) >= pageTableSize()) {
return nullptr;
}
const char *pageStart = reinterpret_cast<const char *>(cachePages());
pageStart += (at * cachePageSize());
return reinterpret_cast<const void *>(pageStart);
}
IndexTableEntry *SharedMemory::indexTable()
{
const SharedMemory *that = const_cast<const SharedMemory *>(this);
return const_cast<IndexTableEntry *>(that->indexTable());
}
PageTableEntry *SharedMemory::pageTable()
{
const SharedMemory *that = const_cast<const SharedMemory *>(this);
return const_cast<PageTableEntry *>(that->pageTable());
}
void *SharedMemory::cachePages()
{
const SharedMemory *that = const_cast<const SharedMemory *>(this);
return const_cast<void *>(that->cachePages());
}
void *SharedMemory::page(pageID at)
{
const SharedMemory *that = const_cast<const SharedMemory *>(this);
return const_cast<void *>(that->page(at));
}
uint SharedMemory::pageTableSize() const
{
return cacheSize / cachePageSize();
}
uint SharedMemory::indexTableSize() const
{
return pageTableSize() / 2;
}
* @return the index of the first page, for the set of contiguous
* pages that can hold @p pagesNeeded PAGES.
*/
pageID SharedMemory::findEmptyPages(uint pagesNeeded) const
{
if (Q_UNLIKELY(pagesNeeded > pageTableSize())) {
return pageTableSize();
}
const PageTableEntry *table = pageTable();
uint contiguousPagesFound = 0;
pageID base = 0;
for (pageID i = 0; i < static_cast<int>(pageTableSize()); ++i) {
if (table[i].index < 0) {
if (contiguousPagesFound == 0) {
base = i;
}
contiguousPagesFound++;
} else {
contiguousPagesFound = 0;
}
if (contiguousPagesFound == pagesNeeded) {
return base;
}
}
return pageTableSize();
}
bool SharedMemory::lruCompare(const IndexTableEntry &l, const IndexTableEntry &r)
{
if (l.firstPage < 0 && r.firstPage >= 0) {
return false;
}
if (l.firstPage >= 0 && r.firstPage < 0) {
return true;
}
return l.lastUsedTime < r.lastUsedTime;
}
bool SharedMemory::seldomUsedCompare(const IndexTableEntry &l, const IndexTableEntry &r)
{
if (l.firstPage < 0 && r.firstPage >= 0) {
return false;
}
if (l.firstPage >= 0 && r.firstPage < 0) {
return true;
}
return l.useCount < r.useCount;
}
bool SharedMemory::ageCompare(const IndexTableEntry &l, const IndexTableEntry &r)
{
if (l.firstPage < 0 && r.firstPage >= 0) {
return false;
}
if (l.firstPage >= 0 && r.firstPage < 0) {
return true;
}
return l.addTime < r.addTime;
}
void SharedMemory::defragment()
{
if (cacheAvail * cachePageSize() == cacheSize) {
return;
}
qCDebug(KCOREADDONS_DEBUG) << "Defragmenting the shared cache";
pageID currentPage = 0;
pageID idLimit = static_cast<pageID>(pageTableSize());
PageTableEntry *pages = pageTable();
if (Q_UNLIKELY(!pages || idLimit <= 0)) {
throw KSDCCorrupted();
}
while (currentPage < idLimit && pages[currentPage].index >= 0) {
++currentPage;
}
pageID freeSpot = currentPage;
while (currentPage < idLimit) {
while (currentPage < idLimit && pages[currentPage].index < 0) {
++currentPage;
}
if (currentPage >= idLimit) {
break;
}
qint32 affectedIndex = pages[currentPage].index;
if (Q_UNLIKELY(affectedIndex < 0 || affectedIndex >= idLimit || indexTable()[affectedIndex].firstPage != currentPage)) {
throw KSDCCorrupted();
}
indexTable()[affectedIndex].firstPage = freeSpot;
while (currentPage < idLimit && pages[currentPage].index >= 0) {
const void *const sourcePage = page(currentPage);
void *const destinationPage = page(freeSpot);
if (Q_UNLIKELY(!sourcePage || !destinationPage || sourcePage < destinationPage)) {
throw KSDCCorrupted();
}
::memcpy(destinationPage, sourcePage, cachePageSize());
pages[freeSpot].index = affectedIndex;
pages[currentPage].index = -1;
++currentPage;
++freeSpot;
if (currentPage >= idLimit) {
break;
}
if (affectedIndex != pages[currentPage].index) {
indexTable()[pages[currentPage].index].firstPage = freeSpot;
}
affectedIndex = pages[currentPage].index;
}
}
}
* Finds the index entry for a given key.
* @param key UTF-8 encoded key to search for.
* @return The index of the entry in the cache named by @p key. Returns
* <0 if no such entry is present.
*/
qint32 SharedMemory::findNamedEntry(const QByteArray &key) const
{
uint keyHash = SharedMemory::generateHash(key);
uint position = keyHash % indexTableSize();
uint probeNumber = 1;
while (indexTable()[position].fileNameHash != keyHash && probeNumber < MAX_PROBE_COUNT) {
position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % indexTableSize();
probeNumber++;
}
if (indexTable()[position].fileNameHash == keyHash) {
pageID firstPage = indexTable()[position].firstPage;
if (firstPage < 0 || static_cast<uint>(firstPage) >= pageTableSize()) {
return -1;
}
const void *resultPage = page(firstPage);
if (Q_UNLIKELY(!resultPage)) {
throw KSDCCorrupted();
}
const char *utf8FileName = reinterpret_cast<const char *>(resultPage);
if (qstrncmp(utf8FileName, key.constData(), cachePageSize()) == 0) {
return position;
}
}
return -1;
}
void SharedMemory::deleteTable(IndexTableEntry *table)
{
delete[] table;
}
* Removes the requested number of pages.
*
* @param numberNeeded the number of pages required to fulfill a current request.
* This number should be <0 and <= the number of pages in the cache.
* @return The identifier of the beginning of a consecutive block of pages able
* to fill the request. Returns a value >= pageTableSize() if no such
* request can be filled.
* @internal
*/
uint SharedMemory::removeUsedPages(uint numberNeeded)
{
if (numberNeeded == 0) {
qCCritical(KCOREADDONS_DEBUG) << "Internal error: Asked to remove exactly 0 pages for some reason.";
throw KSDCCorrupted();
}
if (numberNeeded > pageTableSize()) {
qCCritical(KCOREADDONS_DEBUG) << "Internal error: Requested more space than exists in the cache.";
qCCritical(KCOREADDONS_DEBUG) << numberNeeded << "requested, " << pageTableSize() << "is the total possible.";
throw KSDCCorrupted();
}
qCDebug(KCOREADDONS_DEBUG) << "Removing old entries to free up" << numberNeeded << "pages," << cacheAvail << "are already theoretically available.";
if (cacheAvail > 3 * numberNeeded) {
defragment();
uint result = findEmptyPages(numberNeeded);
if (result < pageTableSize()) {
return result;
} else {
qCCritical(KCOREADDONS_DEBUG) << "Just defragmented a locked cache, but still there"
<< "isn't enough room for the current request.";
}
}
std::unique_ptr<IndexTableEntry, decltype(deleteTable) *> tablePtr(new IndexTableEntry[indexTableSize()], deleteTable);
if (!tablePtr) {
qCCritical(KCOREADDONS_DEBUG) << "Unable to allocate temporary memory for sorting the cache!";
clearInternalTables();
throw KSDCCorrupted();
}
IndexTableEntry *table = tablePtr.get();
::memcpy(table, indexTable(), sizeof(IndexTableEntry) * indexTableSize());
for (uint i = 0; i < indexTableSize(); ++i) {
table[i].firstPage = table[i].useCount > 0 ? static_cast<pageID>(i) : -1;
}
bool (*compareFunction)(const IndexTableEntry &, const IndexTableEntry &);
switch (evictionPolicy.loadRelaxed()) {
case KSharedDataCache::EvictLeastOftenUsed:
case KSharedDataCache::NoEvictionPreference:
default:
compareFunction = seldomUsedCompare;
break;
case KSharedDataCache::EvictLeastRecentlyUsed:
compareFunction = lruCompare;
break;
case KSharedDataCache::EvictOldest:
compareFunction = ageCompare;
break;
}
std::sort(table, table + indexTableSize(), compareFunction);
uint i = 0;
while (i < indexTableSize() && numberNeeded > cacheAvail) {
int curIndex = table[i++].firstPage;
if (curIndex < 0 || static_cast<uint>(curIndex) >= indexTableSize()) {
qCCritical(KCOREADDONS_DEBUG) << "Trying to remove index" << curIndex << "out-of-bounds for index table of size" << indexTableSize();
throw KSDCCorrupted();
}
qCDebug(KCOREADDONS_DEBUG) << "Removing entry of" << indexTable()[curIndex].totalItemSize << "size";
removeEntry(curIndex);
}
defragment();
pageID result = pageTableSize();
while (i < indexTableSize() && (static_cast<uint>(result = findEmptyPages(numberNeeded))) >= pageTableSize()) {
int curIndex = table[i++].firstPage;
if (curIndex < 0) {
defragment();
return findEmptyPages(numberNeeded);
}
if (Q_UNLIKELY(static_cast<uint>(curIndex) >= indexTableSize())) {
throw KSDCCorrupted();
}
removeEntry(curIndex);
}
return result;
}
uint SharedMemory::totalSize(uint cacheSize, uint effectivePageSize)
{
uint numberPages = intCeil(cacheSize, effectivePageSize);
uint indexTableSize = numberPages / 2;
IndexTableEntry *indexTableStart = offsetAs<IndexTableEntry>(static_cast<void *>(nullptr), sizeof(SharedMemory));
indexTableStart += indexTableSize;
PageTableEntry *pageTableStart = reinterpret_cast<PageTableEntry *>(indexTableStart);
pageTableStart = alignTo<PageTableEntry>(pageTableStart);
pageTableStart += numberPages;
char *cacheStart = reinterpret_cast<char *>(pageTableStart);
cacheStart += (numberPages * effectivePageSize);
cacheStart = alignTo<char>(cacheStart, ALIGNOF(void *));
return static_cast<uint>(reinterpret_cast<quintptr>(cacheStart));
}
uint SharedMemory::fileNameHash(const QByteArray &utf8FileName) const
{
return generateHash(utf8FileName) % indexTableSize();
}
void SharedMemory::clear()
{
clearInternalTables();
}
void SharedMemory::removeEntry(uint index)
{
if (index >= indexTableSize() || cacheAvail > pageTableSize()) {
throw KSDCCorrupted();
}
PageTableEntry *pageTableEntries = pageTable();
IndexTableEntry *entriesIndex = indexTable();
pageID firstPage = entriesIndex[index].firstPage;
if (firstPage < 0 || static_cast<quint32>(firstPage) >= pageTableSize()) {
qCDebug(KCOREADDONS_DEBUG) << "Trying to remove an entry which is already invalid. This "
<< "cache is likely corrupt.";
throw KSDCCorrupted();
}
if (index != static_cast<uint>(pageTableEntries[firstPage].index)) {
qCCritical(KCOREADDONS_DEBUG) << "Removing entry" << index << "but the matching data"
<< "doesn't link back -- cache is corrupt, clearing.";
throw KSDCCorrupted();
}
uint entriesToRemove = intCeil(entriesIndex[index].totalItemSize, cachePageSize());
uint savedCacheSize = cacheAvail;
for (uint i = firstPage; i < pageTableSize() && static_cast<uint>(pageTableEntries[i].index) == index; ++i) {
pageTableEntries[i].index = -1;
cacheAvail++;
}
if ((cacheAvail - savedCacheSize) != entriesToRemove) {
qCCritical(KCOREADDONS_DEBUG) << "We somehow did not remove" << entriesToRemove << "when removing entry" << index << ", instead we removed"
<< (cacheAvail - savedCacheSize);
throw KSDCCorrupted();
}
#ifdef NDEBUG
void *const startOfData = page(firstPage);
if (startOfData) {
QByteArray str((const char *)startOfData);
str.prepend(" REMOVED: ");
str.prepend(QByteArray::number(index));
str.prepend("ENTRY ");
::memcpy(startOfData, str.constData(), str.size() + 1);
}
#endif
entriesIndex[index].fileNameHash = 0;
entriesIndex[index].totalItemSize = 0;
entriesIndex[index].useCount = 0;
entriesIndex[index].lastUsedTime = 0;
entriesIndex[index].addTime = 0;
entriesIndex[index].firstPage = -1;
}