// Copyright 2016 The Fuchsia Authors // // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT #include #include #include #include using fbl::Arena; struct TestObj { int xx, yy, zz; }; static bool init_null_name_succeeds() { BEGIN_TEST; Arena arena; EXPECT_EQ(ZX_OK, arena.Init(nullptr, sizeof(TestObj), 16), ""); END_TEST; } static bool init_zero_ob_size_fails() { BEGIN_TEST; Arena arena; EXPECT_EQ(ZX_ERR_INVALID_ARGS, arena.Init("name", 0, 16), ""); END_TEST; } static bool init_large_ob_size_fails() { BEGIN_TEST; Arena arena; EXPECT_EQ(ZX_ERR_INVALID_ARGS, arena.Init("name", PAGE_SIZE + 1, 16), ""); END_TEST; } static bool init_zero_count_fails() { BEGIN_TEST; Arena arena; EXPECT_EQ(ZX_ERR_INVALID_ARGS, arena.Init("name", sizeof(TestObj), 0), ""); END_TEST; } static bool start_and_end_look_good() { BEGIN_TEST; static const size_t num_slots = (2 * PAGE_SIZE) / sizeof(TestObj); static const size_t expected_size = num_slots * sizeof(TestObj); Arena arena; EXPECT_EQ(ZX_OK, arena.Init("name", sizeof(TestObj), num_slots), ""); EXPECT_NONNULL(arena.start(), ""); EXPECT_NONNULL(arena.end(), ""); auto start = reinterpret_cast(arena.start()); auto end = reinterpret_cast(arena.end()); EXPECT_LT(start, end, ""); EXPECT_GE(end - start, expected_size, ""); END_TEST; } static bool in_range_tests() { BEGIN_TEST; static const size_t num_slots = (2 * PAGE_SIZE) / sizeof(TestObj); Arena arena; EXPECT_EQ(ZX_OK, arena.Init("name", sizeof(TestObj), num_slots), ""); auto start = reinterpret_cast(arena.start()); // Nothing is allocated yet, so not even the start address // should be in range. EXPECT_FALSE(arena.in_range(start), ""); // Allocate some objects, and check that each is within range. static const int nobjs = 16; void* objs[nobjs]; for (int i = 0; i < nobjs; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%d]", i); objs[i] = arena.Alloc(); EXPECT_NONNULL(objs[i], msg); // The allocated object should be in range. EXPECT_TRUE(arena.in_range(objs[i]), msg); // The slot just after this object should not be in range. // FRAGILE: assumes that objects are allocated in increasing order. EXPECT_FALSE( arena.in_range(reinterpret_cast(objs[i]) + 1), msg); // The count should correspond to the number of times we have // allocated. EXPECT_EQ(static_cast(i + 1), arena.DiagnosticCount(), msg); } // Deallocate the objects and check whether they're in range. for (int i = nobjs - 1; i >= 0; i--) { char msg[32]; snprintf(msg, sizeof(msg), "[%d]", i); // The object should still be in range. EXPECT_TRUE(arena.in_range(objs[i]), msg); arena.Free(objs[i]); // The free slot will still be in range. // NOTE: If Arena ever learns to coalesce and decommit whole pages of // free objects, this test will need to change. EXPECT_TRUE(arena.in_range(objs[i]), msg); // The count should correspond to the number of times we have // deallocated. EXPECT_EQ(static_cast(i), arena.DiagnosticCount(), msg); } END_TEST; } static bool out_of_memory() { BEGIN_TEST; static const size_t num_slots = (2 * PAGE_SIZE) / sizeof(TestObj); Arena arena; EXPECT_EQ(ZX_OK, arena.Init("name", sizeof(TestObj), num_slots), ""); // Allocate all of the data objects. fbl::AllocChecker ac; fbl::unique_ptr objs = fbl::unique_ptr(new (&ac) void*[num_slots]); EXPECT_TRUE(ac.check(), ""); void** top = &objs[0]; for (size_t i = 0; i < num_slots; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%zu]", i); *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], msg); } // Any further allocations should return nullptr. EXPECT_NULL(arena.Alloc(), ""); EXPECT_NULL(arena.Alloc(), ""); EXPECT_NULL(arena.Alloc(), ""); EXPECT_NULL(arena.Alloc(), ""); // Free two objects. arena.Free(*--top); arena.Free(*--top); // Two allocations should succeed; any further should fail. *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], ""); *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], ""); EXPECT_NULL(arena.Alloc(), ""); EXPECT_NULL(arena.Alloc(), ""); // Free all objects. // Nothing much to check except that it doesn't crash. while (top > objs.get()) { arena.Free(*--top); } END_TEST; } // Test helper. Counts the number of committed and uncommitted pages in the // range. Returns {*committed, *uncommitted} = {0, 0} if |start| doesn't // correspond to a live VmMapping. static bool count_committed_pages( vaddr_t start, vaddr_t end, size_t* committed, size_t* uncommitted) { BEGIN_TEST; // Not a test, but we need these guards to use ASSERT_* *committed = 0; *uncommitted = 0; // Find the VmMapping that covers |start|. Assume that it covers |end-1|. const auto region = VmAspace::kernel_aspace()->FindRegion(start); ASSERT_NONNULL(region, "FindRegion"); const auto mapping = region->as_vm_mapping(); if (mapping == nullptr) { // It's a VMAR, not a mapping, so no pages are committed. // Return 0/0. return true; } // Ask the VMO how many pages it's allocated within the range. auto start_off = ROUNDDOWN(start, PAGE_SIZE) - mapping->base(); auto end_off = ROUNDUP(end, PAGE_SIZE) - mapping->base(); *committed = mapping->vmo()->AllocatedPagesInRange( start_off + mapping->object_offset(), end_off - start_off); *uncommitted = (end_off - start_off) / PAGE_SIZE - *committed; END_TEST; } static bool committing_tests() { BEGIN_TEST; static const size_t num_slots = (64 * PAGE_SIZE) / sizeof(TestObj); Arena arena; EXPECT_EQ(ZX_OK, arena.Init("name", sizeof(TestObj), num_slots), ""); auto start = reinterpret_cast(arena.start()); auto end = reinterpret_cast(arena.end()); // Nothing is allocated yet, so no pages should be committed. size_t committed; size_t uncommitted; EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(0u, committed, ""); EXPECT_GT(uncommitted, 0u, ""); // Allocate an object. auto obj = reinterpret_cast(arena.Alloc()); EXPECT_NE(0u, obj, ""); size_t atotal = sizeof(TestObj); // The page containing the object should be committed. auto ps = ROUNDDOWN(obj, PAGE_SIZE); auto pe = ROUNDUP(obj + sizeof(TestObj), PAGE_SIZE); EXPECT_TRUE(count_committed_pages(ps, pe, &committed, &uncommitted), ""); EXPECT_GT(committed, 0u, ""); EXPECT_EQ(0u, uncommitted, ""); // The first handful of pages should also become committed, but the rest // should stay ucommitted. EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_GT(committed, 0u, ""); EXPECT_GT(uncommitted, 0u, ""); // Fill the committed pages with objects; the set of committed pages // shouldn't change. auto orig_committed = committed; auto orig_uncommitted = uncommitted; while (atotal + sizeof(TestObj) <= orig_committed * PAGE_SIZE) { EXPECT_NONNULL(arena.Alloc(), ""); atotal += sizeof(TestObj); } EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(orig_committed, committed, ""); EXPECT_EQ(orig_uncommitted, uncommitted, ""); // Allocating one more object should cause more pages to be committed. EXPECT_NONNULL(arena.Alloc(), ""); atotal += sizeof(TestObj); EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_LT(orig_committed, committed, ""); EXPECT_GT(orig_uncommitted, uncommitted, ""); // TODO(dbort): Test uncommitting if Arena ever learns to decommit pages. END_TEST; } // Friend class that can see inside an Arena. namespace fbl { class ArenaTestFriend { public: ArenaTestFriend(const Arena& arena) : arena_(arena) {} constexpr static size_t control_slot_size() { return sizeof(Arena::Node); } vaddr_t control_start() const { return reinterpret_cast(arena_.control_.start()); } vaddr_t control_end() const { return reinterpret_cast(arena_.control_.end()); } private: const Arena& arena_; }; } // namespace fbl using fbl::ArenaTestFriend; // Hit the decommit code path. We can't observe it without peeking inside the // control pool, since the data pool doesn't currently decommit. static bool uncommitting_tests() { BEGIN_TEST; // Create an arena with a 16-page control pool. static const size_t num_pages = 16; static const size_t num_slots = (num_pages * PAGE_SIZE) / ArenaTestFriend::control_slot_size(); Arena arena; // Use a small data slot size (1) to keep our memory usage down. EXPECT_EQ(ZX_OK, arena.Init("name", 1, num_slots), ""); // Get the extent of the control pool. vaddr_t start; vaddr_t end; { ArenaTestFriend atf(arena); start = reinterpret_cast(atf.control_start()); end = reinterpret_cast(atf.control_end()); } // Nothing is allocated yet, so no control pages should be committed. size_t committed; size_t uncommitted; EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_EQ(0u, committed, ""); EXPECT_GT(uncommitted, 0u, ""); // Allocate all of the data objects. Hold onto the pointers so we can free // them. fbl::AllocChecker ac; fbl::unique_ptr objs = fbl::unique_ptr(new (&ac) void*[num_slots]); EXPECT_TRUE(ac.check(), ""); void** top = &objs[0]; for (size_t i = 0; i < num_slots; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%zu]", i); *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], msg); } // We still shouldn't see any control pages committed, becase no objects // have been freed yet. EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_EQ(0u, committed, ""); EXPECT_GT(uncommitted, 0u, ""); // Demonstrate that we've allocated all of the data objects. void* no = arena.Alloc(); EXPECT_NULL(no, ""); // Free a data object. arena.Free(*--top); // We should now see some committed pages inside the control pool. EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_GT(committed, 0u, ""); EXPECT_GT(uncommitted, 0u, ""); // Free all of the data objects. while (top > objs.get()) { arena.Free(*--top); } // All of the control pages should be committed. EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_EQ(committed, num_pages, ""); EXPECT_EQ(uncommitted, 0u, ""); // Allocate half of the data objects, freeing up half of the free nodes // (and thus half of the control slots). auto orig_committed = committed; ASSERT_EQ(top, objs.get(), ""); for (size_t i = 0; i < num_slots / 2; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%zu]", i); *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], msg); } // The number of committed pages should have dropped. EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_LT(committed, orig_committed, ""); EXPECT_GT(uncommitted, 0u, ""); // Free more control slots (by allocating data objects) until we see more // unmapping happen. orig_committed = committed; for (size_t i = num_slots / 2; i < num_slots; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%zu]", i); *top++ = arena.Alloc(); EXPECT_NONNULL(top[-1], msg); EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); if (committed != orig_committed) { break; } } EXPECT_GT(orig_committed, committed, ""); // Allocating one more control slot (by freeing a data object) should not // cause the number of committed pages to change: there should be some // hysteresis built into the system to avoid flickering back and forth. orig_committed = committed; arena.Free(*--top); EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_EQ(committed, orig_committed, ""); EXPECT_GT(uncommitted, 0u, ""); // Same for freeing a couple more control slots (by allocating more data // objects). *top++ = arena.Alloc(); *top++ = arena.Alloc(); EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_EQ(num_pages, committed + uncommitted, ""); EXPECT_EQ(committed, orig_committed, ""); EXPECT_GT(uncommitted, 0u, ""); END_TEST; } // Checks that destroying an arena unmaps all of its pages. static bool memory_cleanup() { BEGIN_TEST; static const size_t num_slots = (16 * PAGE_SIZE) / sizeof(TestObj); fbl::AllocChecker ac; Arena* arena = new (&ac) Arena(); EXPECT_TRUE(ac.check(), ""); EXPECT_EQ(ZX_OK, arena->Init("name", sizeof(TestObj), num_slots), ""); auto start = reinterpret_cast(arena->start()); auto end = reinterpret_cast(arena->end()); // Allocate and leak a bunch of objects. for (size_t i = 0; i < num_slots; i++) { char msg[32]; snprintf(msg, sizeof(msg), "[%zu]", i); EXPECT_NONNULL(arena->Alloc(), msg); } // Should see some committed pages. size_t committed; size_t uncommitted; EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); EXPECT_GT(committed, 0u, ""); // Destroying the Arena should destroy the underlying VmMapping, // along with all of its pages. delete arena; EXPECT_TRUE( count_committed_pages(start, end, &committed, &uncommitted), ""); // 0/0 means "no mapping at this address". // FLAKY: Another thread could could come in and allocate a mapping at the // old location. EXPECT_EQ(committed, 0u, ""); EXPECT_EQ(uncommitted, 0u, ""); END_TEST; } // Basic checks that the contents of allocated objects stick around, aren't // stomped on. static bool content_preservation() { BEGIN_TEST; Arena arena; zx_status_t s = arena.Init("arena_tests", sizeof(TestObj), 1000); ASSERT_EQ(ZX_OK, s, "arena.Init()"); const int count = 30; for (int times = 0; times != 5; ++times) { TestObj* afp[count] = {0}; for (int ix = 0; ix != count; ++ix) { afp[ix] = reinterpret_cast(arena.Alloc()); ASSERT_NONNULL(afp[ix], "arena.Alloc()"); *afp[ix] = {17, 5, ix + 100}; } arena.Free(afp[3]); arena.Free(afp[4]); arena.Free(afp[5]); afp[3] = afp[4] = afp[5] = nullptr; afp[4] = reinterpret_cast(arena.Alloc()); ASSERT_NONNULL(afp[4], "arena.Alloc()"); *afp[4] = {17, 5, 104}; for (int ix = 0; ix != count; ++ix) { if (!afp[ix]) continue; EXPECT_EQ(17, afp[ix]->xx, ""); EXPECT_EQ(5, afp[ix]->yy, ""); EXPECT_EQ(ix + 100, afp[ix]->zz, ""); arena.Free(afp[ix]); } // Leak a few objects. for (int ix = 0; ix != 7; ++ix) { TestObj* leak = reinterpret_cast(arena.Alloc()); ASSERT_NONNULL(leak, "arena.Alloc()"); *leak = {2121, 77, 55}; } } END_TEST; } #define ARENA_UNITTEST(fname) UNITTEST(#fname, fname) UNITTEST_START_TESTCASE(arena_tests) ARENA_UNITTEST(init_null_name_succeeds) ARENA_UNITTEST(init_zero_ob_size_fails) ARENA_UNITTEST(init_large_ob_size_fails) ARENA_UNITTEST(init_zero_count_fails) ARENA_UNITTEST(start_and_end_look_good) ARENA_UNITTEST(in_range_tests) ARENA_UNITTEST(out_of_memory) ARENA_UNITTEST(committing_tests) ARENA_UNITTEST(uncommitting_tests) ARENA_UNITTEST(memory_cleanup) ARENA_UNITTEST(content_preservation) UNITTEST_END_TESTCASE(arena_tests, "arenatests", "Arena allocator test");