diff --git a/serialization/itemrepository.h b/serialization/itemrepository.h --- a/serialization/itemrepository.h +++ b/serialization/itemrepository.h @@ -467,29 +467,32 @@ /// @param modulo MUST be a multiple of ObjectMapSize, because (b-a) | (x * h1) => (b-a) | h2, where a|b means a is a multiple of b. /// This this allows efficiently computing the clashes using the local object map hash. - bool hasClashingItem(uint hash, uint modulo) { + enum ClashType { + NoClash, + LocalClash, + SearchClash, + }; - Q_ASSERT(modulo % ObjectMapSize == 0); + ClashType hasClashingItem(uint hash, uint searchMod = 0) { m_lastUsed = 0; - uint hashMod = hash % modulo; - unsigned short localHash = hash % m_objectMapSize; - unsigned short currentIndex = m_objectMap[localHash]; + ushort currentIndex = m_objectMap[hash % m_objectMapSize]; - if(currentIndex == 0) - return false; + if (!searchMod || !currentIndex) + return currentIndex ? LocalClash : NoClash; + + Q_ASSERT(searchMod % ObjectMapSize == 0); + const uint hashModSearch = hash % searchMod; while(currentIndex) { uint currentHash = itemFromIndex(currentIndex)->hash(); - Q_ASSERT(currentHash % m_objectMapSize == localHash); - - if(currentHash % modulo == hashMod) - return true; //Clash + if(currentHash % searchMod == hashModSearch) + return SearchClash; currentIndex = followerIndex(currentIndex); } - return false; + return LocalClash; } void countFollowerIndexLengths(uint& usedSlots, uint& lengths, uint& slotCount, uint& longestInBucketFollowerChain) { @@ -1389,19 +1392,32 @@ if (!previousBucketPtr) { // This bucket is linked in the m_firstBucketForHash array, find the next clashing bucket in the chain // There may be items in the chain that clash only with MyBucket::NextBucketHashSize, skipped here - m_firstBucketForHash[hash % bucketHashSize] = walkBucketChain(hash, [hash](ushort bucketIdx, MyBucket *bucketPtr){ - if (bucketPtr->hasClashingItem(hash, bucketHashSize)) { - return bucketIdx; + bool removedLastLocalClash = false; + m_firstBucketForHash[hash % bucketHashSize] = + walkBucketChain(hash, [=, &removedLastLocalClash](ushort bucketIdx, MyBucket *bucketPtr) -> ushort { + auto clashType = bucketPtr->hasClashingItem(hash, bucketHashSize); + if (clashType == MyBucket::SearchClash) { + return bucketIdx; // found bucket clashing with the root mod, this bucket becomes the new root } - return static_cast(0); + if (clashType == MyBucket::NoClash) { + // This should only be possible for the bucket from which the item was just deleted + // An item with no local clashes for a hash may not participate in the chain for it + Q_ASSERT(bucketIdx == bucket); + removedLastLocalClash = true; + } + return 0; }); - } else if(!bucketPtr->hasClashingItem(hash, MyBucket::NextBucketHashSize)) { + if (removedLastLocalClash) { + bucketPtr->setNextBucketForHash(hash, 0); + } + } else if(bucketPtr->hasClashingItem(hash) == MyBucket::NoClash) { // TODO: Skip clashing items reachable from m_firstBucketForHash // (see note in usePermissiveModuloWhenRemovingClashLinks() test) ENSURE_REACHABLE(bucket); previousBucketPtr->setNextBucketForHash(hash, bucketPtr->nextBucketForHash(hash)); + bucketPtr->setNextBucketForHash(hash, 0); Q_ASSERT(m_buckets[bucketPtr->nextBucketForHash(hash)] != previousBucketPtr); } @@ -1412,13 +1428,6 @@ //Convert the monster-bucket back to multiple normal buckets, and put them into the free list uint newBuckets = bucketPtr->monsterBucketExtent()+1; Q_ASSERT(bucketPtr->isEmpty()); - if (!previousBucketPtr) { - // see https://bugs.kde.org/show_bug.cgi?id=272408 - // the monster bucket will be deleted and new smaller ones created - // the next bucket for this hash is invalid anyways as done above - // but calling the below unconditionally leads to other issues... - bucketPtr->setNextBucketForHash(hash, 0); - } convertMonsterBucket(bucket, 0); for(uint created = bucket; created < bucket + newBuckets; ++created) { putIntoFreeList(created, bucketForIndex(created)); diff --git a/serialization/tests/test_itemrepository.cpp b/serialization/tests/test_itemrepository.cpp --- a/serialization/tests/test_itemrepository.cpp +++ b/serialization/tests/test_itemrepository.cpp @@ -260,6 +260,7 @@ // Choose sizes that ensure that the items fit in the desired buckets const uint bigItemSize = KDevelop::ItemRepositoryBucketSize * 0.55 - 1; const uint smallItemSize = KDevelop::ItemRepositoryBucketSize * 0.25 - 1; + const uint monsterItemSize = KDevelop::ItemRepositoryBucketSize * 1.1; // Will get placed in bucket 1 (bucket zero is invalid), so the root bucket table at position 'clashValue' will be '1' const QScopedPointer firstChainFirstLink(createItem(clashValue, bigItemSize)); @@ -350,6 +351,31 @@ * So TODO: Don't preserve links to items accessible from root buckets. This cannot * be done correctly using only Bucket::hasClashingItem as of now. */ + + // Re-using the first chain above, we link a monster to the end + const QScopedPointer firstChainSecondLinkMonster(createItem(bucketHashSize + clashValue, monsterItemSize)); + const uint firstChainSecondLinkMonsterIndex = repository.index(*firstChainSecondLinkMonster); + QCOMPARE(bucketForIndex(firstChainSecondLinkMonsterIndex), 3u); + + // And link another monster to that one + const QScopedPointer firstChainThirdLink(createItem(bucketHashSize * 2 + clashValue, monsterItemSize)); + const uint firstChainThirdLinkIndex = repository.index(*firstChainThirdLink); + QCOMPARE(bucketForIndex(firstChainThirdLinkIndex), 5u); + + /* + * Now we cut the second monster out of the chain. + * + * What this test really cares about is that the stale link has been removed before the monster is destroyed. + * + * Monsters that are destroyed assert that they have no further links. That wouldn't make sense, as monsters + * can only contain a single item, so removing it means it should be completely cut from the chain. + * + * If stale links are correctly detected and destroyed when items are removed, this will succeed. + * + * See also deleteClashingMonsterBucket() above, which tests this condition for root buckets + */ + repository.deleteItem(firstChainSecondLinkMonsterIndex); + QCOMPARE(repository.findIndex(*firstChainThirdLink), firstChainThirdLinkIndex); } };