From 3da5d9cf8e021d92d032a8776ac85de6716647d7 Mon Sep 17 00:00:00 2001 From: roytam1 Date: Sun, 23 Mar 2025 12:38:50 +0800 Subject: [PATCH] import from `custom` branch of UXP: ported from mozilla: Add UnorderedRemoveElement[s]At to nsTArray, r=froydnj (3fa9e61e) --- xpcom/glue/nsTArray-inl.h | 55 ++++++++++++++ xpcom/glue/nsTArray.h | 86 +++++++++++++++++++++- xpcom/tests/gtest/TestTArray.cpp | 120 +++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 1 deletion(-) diff --git a/xpcom/glue/nsTArray-inl.h b/xpcom/glue/nsTArray-inl.h index 1c6b61e30..d8fbf9412 100644 --- a/xpcom/glue/nsTArray-inl.h +++ b/xpcom/glue/nsTArray-inl.h @@ -306,6 +306,61 @@ nsTArray_base::ShiftData(index_type aStart, } } +template +template +void +nsTArray_base::SwapFromEnd(index_type aStart, + size_type aCount, + size_type aElemSize, + size_t aElemAlign) +{ + // This method is part of the implementation of + // nsTArray::SwapRemoveElement{s,}At. For more information, read the + // documentation on that method. + if (aCount == 0) { + return; + } + + // We are going to be removing aCount elements. Update our length to point to + // the new end of the array. + size_type oldLength = mHdr->mLength; + mHdr->mLength -= aCount; + + if (mHdr->mLength == 0) { + // If we have no elements remaining in the array, we can free our buffer. + ShrinkCapacity(aElemSize, aElemAlign); + return; + } + + // Determine how many elements we need to move from the end of the array into + // the now-removed section. This will either be the number of elements which + // were removed (if there are more elements in the tail of the array), or the + // entire tail of the array, whichever is smaller. + size_type relocCount = std::min(aCount, mHdr->mLength - aStart); + if (relocCount == 0) { + return; + } + + // Move the elements which are now stranded after the end of the array back + // into the now-vacated memory. + index_type sourceBytes = (oldLength - relocCount) * aElemSize; + index_type destBytes = aStart * aElemSize; + + // Perform the final copy. This is guaranteed to be a non-overlapping copy + // as our source contains only still-valid entries, and the destination + // contains only invalid entries which need to be overwritten. + MOZ_ASSERT(sourceBytes >= destBytes, + "The source should be after the destination."); + MOZ_ASSERT(sourceBytes - destBytes >= relocCount * aElemSize, + "The range should be nonoverlapping"); + + char* baseAddr = reinterpret_cast(mHdr + 1); + Copy::MoveNonOverlappingRegion(baseAddr + destBytes, + baseAddr + sourceBytes, + relocCount, + aElemSize); +} + template template typename ActualAlloc::ResultTypeProxy diff --git a/xpcom/glue/nsTArray.h b/xpcom/glue/nsTArray.h index 79d4324e3..6f2ad8e0e 100644 --- a/xpcom/glue/nsTArray.h +++ b/xpcom/glue/nsTArray.h @@ -559,6 +559,17 @@ protected: void ShiftData(index_type aStart, size_type aOldLen, size_type aNewLen, size_type aElemSize, size_t aElemAlign); + // This method may be called to swap elements from the end of the array to + // fill a "gap" in the array. If the resulting array has zero elements, then + // the array's memory is free'd. + // @param aStart The starting index of the gap. + // @param aCount The length of the gap. + // @param aElemSize The size of an array element. + // @param aElemAlign The alignment in bytes of an array element. + template + void SwapFromEnd(index_type aStart, size_type aCount, + size_type aElemSize, size_t aElemAlign); + // This method increments the length member of the array's header. // Note that mHdr may actually be sEmptyHdr in the case where a // zero-length array is inserted into our array. But then aNum should @@ -1853,7 +1864,58 @@ public: // A variation on the RemoveElementsAt method defined above. void RemoveElementAt(index_type aIndex) { RemoveElementsAt(aIndex, 1); } - // A variation on the RemoveElementsAt method defined above. + // This method performs index-based removals from an array without preserving + // the order of the array. This is useful if you are using the array as a + // set-like data structure. + // + // These removals are efficient, as they move as few elements as possible. At + // most N elements, where N is the number of removed elements, will have to + // be relocated. + // + // ## Examples + // + // When removing an element from the end of the array, it can be removed in + // place, by destroying it and decrementing the length. + // + // [ 1, 2, 3 ] => [ 1, 2 ] + // ^ + // + // When removing any other single element, it is removed by swapping it with + // the last element, and then decrementing the length as before. + // + // [ 1, 2, 3, 4, 5, 6 ] => [ 1, 6, 3, 4, 5 ] + // ^ + // + // This method also supports efficiently removing a range of elements. If they + // are at the end, then they can all be removed like in the one element case. + // + // [ 1, 2, 3, 4, 5, 6 ] => [ 1, 2 ] + // ^--------^ + // + // If more elements are removed than exist after the removed section, the + // remaining elements will be shifted down like in a normal removal. + // + // [ 1, 2, 3, 4, 5, 6, 7, 8 ] => [ 1, 2, 7, 8 ] + // ^--------^ + // + // And if fewer elements are removed than exist after the removed section, + // elements will be moved from the end of the array to fill the vacated space. + // + // [ 1, 2, 3, 4, 5, 6, 7, 8 ] => [ 1, 7, 8, 4, 5, 6 ] + // ^--^ + // + // @param aStart The starting index of the elements to remove. @param aCount + // The number of elements to remove. + void UnorderedRemoveElementsAt(index_type aStart, size_type aCount); + + // A variation on the UnorderedRemoveElementsAt method defined above to remove + // a single element. This operation is sometimes called `SwapRemove`. + // + // This method is O(1), but does not preserve the order of the elements. + void UnorderedRemoveElementAt(index_type aIndex) { + UnorderedRemoveElementsAt(aIndex, 1); + } + void Clear() { RemoveElementsAt(0, Length()); } // This method removes elements based on the return value of the @@ -2184,6 +2246,28 @@ nsTArray_Impl::RemoveElementsAt(index_type aStart, size_type aCount) MOZ_ALIGNOF(elem_type)); } +template +void +nsTArray_Impl::UnorderedRemoveElementsAt(index_type aStart, size_type aCount) +{ + MOZ_ASSERT(aCount == 0 || aStart < Length(), "Invalid aStart index"); + + mozilla::CheckedInt rangeEnd = aStart; + rangeEnd += aCount; + + if (MOZ_UNLIKELY(!rangeEnd.isValid() || rangeEnd.value() > Length())) { + InvalidArrayIndex_CRASH(aStart, Length()); + } + + // Destroy the elements which are being removed, and then swap elements in to + // replace them from the end. See the docs on the declaration of this + // function. + DestructRange(aStart, aCount); + this->template SwapFromEnd(aStart, aCount, + sizeof(elem_type), + MOZ_ALIGNOF(elem_type)); +} + template template void diff --git a/xpcom/tests/gtest/TestTArray.cpp b/xpcom/tests/gtest/TestTArray.cpp index 10e33664f..e867d73d3 100644 --- a/xpcom/tests/gtest/TestTArray.cpp +++ b/xpcom/tests/gtest/TestTArray.cpp @@ -203,4 +203,124 @@ TEST(TArray, CopyOverlappingBackwards) } } +TEST(TArray, UnorderedRemoveElements) +{ + // When removing an element from the end of the array, it can be removed in + // place, by destroying it and decrementing the length. + // + // [ 1, 2, 3 ] => [ 1, 2 ] + // ^ + { + nsTArray array{ 1, 2, 3 }; + array.UnorderedRemoveElementAt(2); + + nsTArray goal{ 1, 2 }; + ASSERT_EQ(array, goal); + } + + // When removing any other single element, it is removed by swapping it with + // the last element, and then decrementing the length as before. + // + // [ 1, 2, 3, 4, 5, 6 ] => [ 1, 6, 3, 4, 5 ] + // ^ + { + nsTArray array{1, 2, 3, 4, 5, 6}; + array.UnorderedRemoveElementAt(1); + + nsTArray goal{1, 6, 3, 4, 5}; + ASSERT_EQ(array, goal); + } + + // This method also supports efficiently removing a range of elements. If they + // are at the end, then they can all be removed like in the one element case. + // + // [ 1, 2, 3, 4, 5, 6 ] => [ 1, 2 ] + // ^--------^ + { + nsTArray array{1, 2, 3, 4, 5, 6}; + array.UnorderedRemoveElementsAt(2, 4); + + nsTArray goal{1, 2}; + ASSERT_EQ(array, goal); + } + + // If more elements are removed than exist after the removed section, the + // remaining elements will be shifted down like in a normal removal. + // + // [ 1, 2, 3, 4, 5, 6, 7, 8 ] => [ 1, 2, 7, 8 ] + // ^--------^ + { + nsTArray array{1, 2, 3, 4, 5, 6, 7, 8}; + array.UnorderedRemoveElementsAt(2, 4); + + nsTArray goal{1, 2, 7, 8}; + ASSERT_EQ(array, goal); + } + + // And if fewer elements are removed than exist after the removed section, + // elements will be moved from the end of the array to fill the vacated space. + // + // [ 1, 2, 3, 4, 5, 6, 7, 8 ] => [ 1, 7, 8, 4, 5, 6 ] + // ^--^ + { + nsTArray array{1, 2, 3, 4, 5, 6, 7, 8}; + array.UnorderedRemoveElementsAt(1, 2); + + nsTArray goal{1, 7, 8, 4, 5, 6}; + ASSERT_EQ(array, goal); + } + + // We should do the right thing if we drain the entire array. + { + nsTArray array{1, 2, 3, 4, 5}; + array.UnorderedRemoveElementsAt(0, 5); + + nsTArray goal{}; + ASSERT_EQ(array, goal); + } + + { + nsTArray array{1}; + array.UnorderedRemoveElementAt(0); + + nsTArray goal{}; + ASSERT_EQ(array, goal); + } + + // We should do the right thing if we remove the same number of elements that + // we have remaining. + { + nsTArray array{1, 2, 3, 4, 5, 6}; + array.UnorderedRemoveElementsAt(2, 2); + + nsTArray goal{1, 2, 5, 6}; + ASSERT_EQ(array, goal); + } + + { + nsTArray array{1, 2, 3}; + array.UnorderedRemoveElementAt(1); + + nsTArray goal{1, 3}; + ASSERT_EQ(array, goal); + } + + // We should be able to remove elements from the front without issue. + { + nsTArray array{1, 2, 3, 4, 5, 6}; + array.UnorderedRemoveElementsAt(0, 2); + + nsTArray goal{5, 6, 3, 4}; + ASSERT_EQ(array, goal); + } + + { + nsTArray array{1, 2, 3, 4}; + array.UnorderedRemoveElementAt(0); + + nsTArray goal{4, 2, 3}; + ASSERT_EQ(array, goal); + } +} + } // namespace TestTArray