diff --git a/gfx/2d/IterableArena.h b/gfx/2d/IterableArena.h new file mode 100644 index 000000000000..fde5ea2e3b4f --- /dev/null +++ b/gfx/2d/IterableArena.h @@ -0,0 +1,188 @@ +#ifndef MOZILLA_GFX_ITERABLEARENA_H_ +#define MOZILLA_GFX_ITERABLEARENA_H_ + +#include "mozilla/Move.h" +#include "mozilla/Assertions.h" +#include "mozilla/gfx/Logging.h" + +#include +#include +#include +#include + +namespace mozilla { +namespace gfx { + +/// A simple pool allocator for plain data structures. +/// +/// Beware that the pool will not attempt to run the destructors. It is the +/// responsibility of the user of this class to either use objects with no +/// destructor or to manually call the allocated objects destructors. +/// If the pool is growable, its allocated objects must be safely moveable in +/// in memory (through memcpy). +class IterableArena { +protected: + struct Header + { + size_t mBlocSize; + }; +public: + enum ArenaType { + FIXED_SIZE, + GROWABLE + }; + + IterableArena(ArenaType aType, size_t aStorageSize) + : mSize(aStorageSize) + , mCursor(0) + , mIsGrowable(aType == GROWABLE) + { + if (mSize == 0) { + mSize = 128; + } + + mStorage = (uint8_t*)malloc(mSize); + if (mStorage == nullptr) { + gfxCriticalError() << "Not enough Memory allocate a memory pool of size " << aStorageSize; + MOZ_CRASH(); + } + } + + ~IterableArena() + { + free(mStorage); + } + + /// Constructs a new item in the pool and returns a positive offset in case of + /// success. + /// + /// The offset never changes even if the storage is reallocated, so users + /// of this class should prefer storing offsets rather than direct pointers + /// to the allocated objects. + /// Alloc can cause the storage to be reallocated if the pool was initialized + /// with IterableArena::GROWABLE. + /// If for any reason the pool fails to allocate enough space for the new item + /// Alloc returns a negative offset and the object's constructor is not called. + template + ptrdiff_t + Alloc(Args&&... aArgs) + { + void* storage = nullptr; + auto offset = AllocRaw(sizeof(T), &storage); + if (offset < 0) { + return offset; + } + new (storage) T(Forward(aArgs)...); + return offset; + } + + ptrdiff_t AllocRaw(size_t aSize, void** aOutPtr = nullptr) + { + const size_t blocSize = AlignedSize(sizeof(Header) + aSize); + + if (AlignedSize(mCursor + blocSize) > mSize) { + if (!mIsGrowable) { + return -1; + } + + size_t newSize = mSize * 2; + while (AlignedSize(mCursor + blocSize) > newSize) { + newSize *= 2; + } + + uint8_t* newStorage = (uint8_t*)realloc(mStorage, newSize); + if (!newStorage) { + gfxCriticalError() << "Not enough Memory to grow the memory pool, size: " << newSize; + return -1; + } + + mStorage = newStorage; + mSize = newSize; + } + ptrdiff_t offset = mCursor; + GetHeader(offset)->mBlocSize = blocSize; + mCursor += blocSize; + if (aOutPtr) { + *aOutPtr = GetStorage(offset); + } + return offset; + } + + /// Get access to an allocated item at a given offset (only use offsets returned + /// by Alloc or AllocRaw). + /// + /// If the pool is growable, the returned pointer is only valid temporarily. The + /// underlying storage can be reallocated in Alloc or AllocRaw, so do not keep + /// these pointers around and store the offset instead. + void* GetStorage(ptrdiff_t offset = 0) + { + MOZ_ASSERT(offset >= 0); + MOZ_ASSERT(offset < mCursor); + return offset >= 0 ? mStorage + offset + sizeof(Header) : nullptr; + } + + /// Clears the storage without running any destructor and without deallocating it. + void Clear() + { + mCursor = 0; + } + + /// Iterate over the elements allocated in this pool. + /// + /// Takes a lambda or function object accepting a void* as parameter. + template + void ForEach(Func cb) + { + Iterator it; + while (void* ptr = it.Next(this)) { + cb(ptr); + } + } + + /// A simple iterator over an arena. + class Iterator { + public: + Iterator() + : mCursor(0) + {} + + void* Next(IterableArena* aArena) + { + if (mCursor >= aArena->mCursor) { + return nullptr; + } + void* result = aArena->GetStorage(mCursor); + const size_t blocSize = aArena->GetHeader(mCursor)->mBlocSize; + MOZ_ASSERT(blocSize != 0); + mCursor += blocSize; + return result; + } + + private: + ptrdiff_t mCursor; + }; + +protected: + Header* GetHeader(ptrdiff_t offset) + { + return (Header*) (mStorage + offset); + } + + size_t AlignedSize(size_t aSize) const + { + const size_t alignment = sizeof(uintptr_t); + return aSize + (alignment - (aSize % alignment)) % alignment; + } + + uint8_t* mStorage; + uint32_t mSize; + ptrdiff_t mCursor; + bool mIsGrowable; + + friend class Iterator; +}; + +} // namespace +} // namespace + +#endif diff --git a/gfx/2d/moz.build b/gfx/2d/moz.build index e2618bc89f39..8af91f7bc5da 100644 --- a/gfx/2d/moz.build +++ b/gfx/2d/moz.build @@ -25,6 +25,7 @@ EXPORTS.mozilla.gfx += [ 'Filters.h', 'Helpers.h', 'HelpersCairo.h', + 'IterableArena.h', 'Logging.h', 'Matrix.h', 'NumericTools.h', diff --git a/gfx/tests/gtest/TestArena.cpp b/gfx/tests/gtest/TestArena.cpp new file mode 100644 index 000000000000..fc310e54a4a7 --- /dev/null +++ b/gfx/tests/gtest/TestArena.cpp @@ -0,0 +1,188 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/gfx/IterableArena.h" +#include + +using namespace mozilla; +using namespace mozilla::gfx; + +#ifdef A +#undef A +#endif + +#ifdef B +#undef B +#endif + +// to avoid having symbols that collide easily like A and B in the global namespace +namespace test_arena { + +class A; +class B; + +class Base { +public: + virtual ~Base() {} + virtual A* AsA() { return nullptr; } + virtual B* AsB() { return nullptr; } +}; + +static int sDtorItemA = 0; +static int sDtorItemB = 0; + +class A : public Base { +public: + virtual A* AsA() override { return this; } + + A(uint64_t val) : mVal(val) {} + ~A() { ++sDtorItemA; } + + uint64_t mVal; +}; + +class B : public Base { +public: + virtual B* AsB() override { return this; } + + B(const string& str) : mVal(str) {} + ~B() { ++sDtorItemB; } + + std::string mVal; +}; + +struct BigStruct { + uint64_t mVal; + uint8_t data[120]; + + BigStruct(uint64_t val) : mVal(val) {} +}; + +void TestArenaAlloc(IterableArena::ArenaType aType) +{ + sDtorItemA = 0; + sDtorItemB = 0; + IterableArena arena(aType, 256); + + // An empty arena has no items to iterate over. + { + int iterations = 0; + arena.ForEach([&](void* item){ + iterations++; + }); + ASSERT_EQ(iterations, 0); + } + + auto a1 = arena.Alloc(42); + auto b1 = arena.Alloc("Obladi oblada"); + auto a2 = arena.Alloc(1337); + auto b2 = arena.Alloc("Yellow submarine"); + auto b3 = arena.Alloc("She's got a ticket to ride"); + + // Alloc returns a non-negative offset if the allocation succeeded. + ASSERT_TRUE(a1 >= 0); + ASSERT_TRUE(a2 >= 0); + ASSERT_TRUE(b1 >= 0); + ASSERT_TRUE(b2 >= 0); + ASSERT_TRUE(b3 >= 0); + + ASSERT_TRUE(arena.GetStorage(a1) != nullptr); + ASSERT_TRUE(arena.GetStorage(a2) != nullptr); + ASSERT_TRUE(arena.GetStorage(b1) != nullptr); + ASSERT_TRUE(arena.GetStorage(b2) != nullptr); + ASSERT_TRUE(arena.GetStorage(b3) != nullptr); + + ASSERT_TRUE(((Base*)arena.GetStorage(a1))->AsA() != nullptr); + ASSERT_TRUE(((Base*)arena.GetStorage(a2))->AsA() != nullptr); + + ASSERT_TRUE(((Base*)arena.GetStorage(b1))->AsB() != nullptr); + ASSERT_TRUE(((Base*)arena.GetStorage(b2))->AsB() != nullptr); + ASSERT_TRUE(((Base*)arena.GetStorage(b3))->AsB() != nullptr); + + ASSERT_EQ(((Base*)arena.GetStorage(a1))->AsA()->mVal, (uint64_t)42); + ASSERT_EQ(((Base*)arena.GetStorage(a2))->AsA()->mVal, (uint64_t)1337); + + ASSERT_EQ(((Base*)arena.GetStorage(b1))->AsB()->mVal, std::string("Obladi oblada")); + ASSERT_EQ(((Base*)arena.GetStorage(b2))->AsB()->mVal, std::string("Yellow submarine")); + ASSERT_EQ(((Base*)arena.GetStorage(b3))->AsB()->mVal, std::string("She's got a ticket to ride")); + + { + int iterations = 0; + arena.ForEach([&](void* item){ + iterations++; + }); + ASSERT_EQ(iterations, 5); + } + + // Typically, running the destructors of the elements in the arena will is done + // manually like this: + arena.ForEach([](void* item){ + ((Base*)item)->~Base(); + }); + arena.Clear(); + ASSERT_EQ(sDtorItemA, 2); + ASSERT_EQ(sDtorItemB, 3); + + // An empty arena has no items to iterate over (we just cleared it). + { + int iterations = 0; + arena.ForEach([&](void* item){ + iterations++; + }); + ASSERT_EQ(iterations, 0); + } + +} + +void TestArenaLimit(IterableArena::ArenaType aType, bool aShouldReachLimit) +{ + IterableArena arena(aType, 128); + + // A non-growable arena should return a negative offset when running out + // of space, without crashing. + // We should not run out of space with a growable arena (unless the os is + // running out of memory but this isn't expected for this test). + bool reachedLimit = false; + for (int i = 0; i < 100; ++i) { + auto offset = arena.Alloc(42); + if (offset < 0) { + reachedLimit = true; + break; + } + } + ASSERT_EQ(reachedLimit, aShouldReachLimit); +} + +} // namespace test_arena + +using namespace test_arena; + +TEST(Moz2D, FixedArena) { + TestArenaAlloc(IterableArena::FIXED_SIZE); + TestArenaLimit(IterableArena::FIXED_SIZE, true); +} + +TEST(Moz2D, GrowableArena) { + TestArenaAlloc(IterableArena::GROWABLE); + TestArenaLimit(IterableArena::GROWABLE, false); + + IterableArena arena(IterableArena::GROWABLE, 16); + // sizeof(BigStruct) is more than twice the initial capacity, make sure that + // this doesn't blow everything up, since the arena doubles its storage size each + // time it grows (until it finds a size that fits). + auto a = arena.Alloc(1); + auto b = arena.Alloc(2); + auto c = arena.Alloc(3); + + // Offsets should also still point to the appropriate values after reallocation. + ASSERT_EQ(((BigStruct*)arena.GetStorage(a))->mVal, (uint64_t)1); + ASSERT_EQ(((BigStruct*)arena.GetStorage(b))->mVal, (uint64_t)2); + ASSERT_EQ(((BigStruct*)arena.GetStorage(c))->mVal, (uint64_t)3); + + arena.Clear(); +} diff --git a/gfx/tests/gtest/moz.build b/gfx/tests/gtest/moz.build index 89193f6d99d8..3ac61e8e4edd 100644 --- a/gfx/tests/gtest/moz.build +++ b/gfx/tests/gtest/moz.build @@ -8,6 +8,7 @@ UNIFIED_SOURCES += [ 'gfxSurfaceRefCountTest.cpp', # Disabled on suspicion of causing bug 904227 #'gfxWordCacheTest.cpp', + 'TestArena.cpp', 'TestAsyncPanZoomController.cpp', 'TestBufferRotation.cpp', 'TestColorNames.cpp',