/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "local_reference_table-inl.h" #include "android-base/stringprintf.h" #include "class_root-inl.h" #include "common_runtime_test.h" #include "mirror/class-alloc-inl.h" #include "mirror/object-inl.h" #include "scoped_thread_state_change-inl.h" namespace art HIDDEN { namespace jni { using android::base::StringPrintf; class LocalReferenceTableTest : public CommonRuntimeTest { protected: LocalReferenceTableTest() { use_boot_image_ = true; // Make the Runtime creation cheaper. } static void CheckDump(LocalReferenceTable* lrt, size_t num_objects, size_t num_unique) REQUIRES_SHARED(Locks::mutator_lock_); void BasicTest(bool check_jni, size_t max_count); void BasicHolesTest(bool check_jni, size_t max_count); void BasicResizeTest(bool check_jni, size_t max_count); void TestAddRemove(bool check_jni, size_t max_count, size_t fill_count = 0u); void TestAddRemoveMixed(bool start_check_jni); }; void LocalReferenceTableTest::CheckDump( LocalReferenceTable* lrt, size_t num_objects, size_t num_unique) { std::ostringstream oss; lrt->Dump(oss); if (num_objects == 0) { EXPECT_EQ(oss.str().find("java.lang.Object"), std::string::npos) << oss.str(); } else if (num_objects == 1) { EXPECT_NE(oss.str().find("1 of java.lang.Object"), std::string::npos) << oss.str(); } else { EXPECT_NE(oss.str().find(StringPrintf("%zd of java.lang.Object (%zd unique instances)", num_objects, num_unique)), std::string::npos) << "\n Expected number of objects: " << num_objects << "\n Expected unique objects: " << num_unique << "\n" << oss.str(); } } void LocalReferenceTableTest::BasicTest(bool check_jni, size_t max_count) { // This will lead to error messages in the log. ScopedLogSeverity sls(LogSeverity::FATAL); ScopedObjectAccess soa(Thread::Current()); StackHandleScope<5> hs(soa.Self()); Handle c = hs.NewHandle(GetClassRoot()); ASSERT_TRUE(c != nullptr); Handle obj0 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj0 != nullptr); Handle obj1 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj1 != nullptr); Handle obj2 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj2 != nullptr); Handle obj3 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj3 != nullptr); std::string error_msg; LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); if (check_jni) { IndirectRef bad_iref = (IndirectRef) 0x11110; EXPECT_FALSE(lrt.Remove(bad_iref)) << "unexpectedly successful removal"; } // Add three, check, remove in the order in which they were added. IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 2, 2); IndirectRef iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); CheckDump(&lrt, 3, 3); EXPECT_OBJ_PTR_EQ(obj0.Get(), lrt.Get(iref0)); EXPECT_OBJ_PTR_EQ(obj1.Get(), lrt.Get(iref1)); EXPECT_OBJ_PTR_EQ(obj2.Get(), lrt.Get(iref2)); EXPECT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 2, 2); EXPECT_TRUE(lrt.Remove(iref1)); CheckDump(&lrt, 1, 1); EXPECT_TRUE(lrt.Remove(iref2)); CheckDump(&lrt, 0, 0); // Table should be empty now. EXPECT_EQ(0U, lrt.Capacity()); // Check that the entry off the end of the list is not valid. // (CheckJNI shall abort for such entries.) EXPECT_FALSE(lrt.IsValidReference(iref0, &error_msg)); // Add three, remove in the opposite order. iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); CheckDump(&lrt, 3, 3); ASSERT_TRUE(lrt.Remove(iref2)); CheckDump(&lrt, 2, 2); ASSERT_TRUE(lrt.Remove(iref1)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 0, 0); // Table should be empty now. ASSERT_EQ(0U, lrt.Capacity()); // Add three, remove middle / middle / bottom / top. (Second attempt // to remove middle should fail.) iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); CheckDump(&lrt, 3, 3); ASSERT_EQ(3U, lrt.Capacity()); ASSERT_TRUE(lrt.Remove(iref1)); CheckDump(&lrt, 2, 2); if (check_jni) { ASSERT_FALSE(lrt.Remove(iref1)); CheckDump(&lrt, 2, 2); } // Check that the reference to the hole is not valid. EXPECT_FALSE(lrt.IsValidReference(iref1, &error_msg)); ASSERT_TRUE(lrt.Remove(iref2)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 0, 0); // Table should be empty now. ASSERT_EQ(0U, lrt.Capacity()); // Add four entries. Remove #1, add new entry, verify that table size // is still 4 (i.e. holes are getting filled). Remove #1 and #3, verify // that we delete one and don't hole-compact the other. iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); IndirectRef iref3 = lrt.Add(obj3.Get(), &error_msg); EXPECT_TRUE(iref3 != nullptr); CheckDump(&lrt, 4, 4); ASSERT_TRUE(lrt.Remove(iref1)); CheckDump(&lrt, 3, 3); iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); ASSERT_EQ(4U, lrt.Capacity()) << "hole not filled"; CheckDump(&lrt, 4, 4); ASSERT_TRUE(lrt.Remove(iref1)); CheckDump(&lrt, 3, 3); ASSERT_TRUE(lrt.Remove(iref3)); CheckDump(&lrt, 2, 2); ASSERT_EQ(3U, lrt.Capacity()) << "should be 3 after two deletions"; ASSERT_TRUE(lrt.Remove(iref2)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 0, 0); ASSERT_EQ(0U, lrt.Capacity()) << "not empty after split remove"; // Add an entry, remove it, add a new entry, and try to use the original // iref. They have the same slot number but are for different objects. // With the extended checks in place, this should fail. iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 0, 0); iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 1, 1); if (check_jni) { ASSERT_FALSE(lrt.Remove(iref0)) << "mismatched del succeeded"; CheckDump(&lrt, 1, 1); } ASSERT_TRUE(lrt.Remove(iref1)) << "switched del failed"; ASSERT_EQ(0U, lrt.Capacity()) << "switching del not empty"; CheckDump(&lrt, 0, 0); // Same as above, but with the same object. A more rigorous checker // (e.g. with slot serialization) will catch this. iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); CheckDump(&lrt, 0, 0); iref1 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 1, 1); if (iref0 != iref1) { // Try 0, should not work. ASSERT_FALSE(lrt.Remove(iref0)) << "temporal del succeeded"; } ASSERT_TRUE(lrt.Remove(iref1)) << "temporal cleanup failed"; ASSERT_EQ(0U, lrt.Capacity()) << "temporal del not empty"; CheckDump(&lrt, 0, 0); // Stale reference is not valid. iref0 = lrt.Add(obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(iref0)); EXPECT_FALSE(lrt.IsValidReference(iref0, &error_msg)) << "stale lookup succeeded"; CheckDump(&lrt, 0, 0); // Test table resizing. // These ones fit... static const size_t kTableInitial = max_count / 2; IndirectRef manyRefs[kTableInitial]; for (size_t i = 0; i < kTableInitial; i++) { manyRefs[i] = lrt.Add(obj0.Get(), &error_msg); ASSERT_TRUE(manyRefs[i] != nullptr) << "Failed adding " << i; CheckDump(&lrt, i + 1, 1); } // ...this one causes overflow. iref0 = lrt.Add(obj0.Get(), &error_msg); ASSERT_TRUE(iref0 != nullptr); ASSERT_EQ(kTableInitial + 1, lrt.Capacity()); CheckDump(&lrt, kTableInitial + 1, 1); for (size_t i = 0; i < kTableInitial; i++) { ASSERT_TRUE(lrt.Remove(manyRefs[i])) << "failed removing " << i; CheckDump(&lrt, kTableInitial - i, 1); } // Because of removal order, should have 11 entries, 10 of them holes. ASSERT_EQ(kTableInitial + 1, lrt.Capacity()); ASSERT_TRUE(lrt.Remove(iref0)) << "multi-remove final failed"; ASSERT_EQ(0U, lrt.Capacity()) << "multi-del not empty"; CheckDump(&lrt, 0, 0); } TEST_F(LocalReferenceTableTest, BasicTest) { BasicTest(/*check_jni=*/ false, /*max_count=*/ 20u); BasicTest(/*check_jni=*/ false, /*max_count=*/ kSmallLrtEntries); BasicTest(/*check_jni=*/ false, /*max_count=*/ 2u * kSmallLrtEntries); } TEST_F(LocalReferenceTableTest, BasicTestCheckJNI) { BasicTest(/*check_jni=*/ true, /*max_count=*/ 20u); BasicTest(/*check_jni=*/ true, /*max_count=*/ kSmallLrtEntries); BasicTest(/*check_jni=*/ true, /*max_count=*/ 2u * kSmallLrtEntries); } void LocalReferenceTableTest::BasicHolesTest(bool check_jni, size_t max_count) { // Test the explicitly named cases from the LRT implementation: // // 1) Segment with holes (current_num_holes_ > 0), push new segment, add/remove reference // 2) Segment with holes (current_num_holes_ > 0), pop segment, add/remove reference // 3) Segment with holes (current_num_holes_ > 0), push new segment, pop segment, add/remove // reference // 4) Empty segment, push new segment, create a hole, pop a segment, add/remove a reference // 5) Base segment, push new segment, create a hole, pop a segment, push new segment, add/remove // reference ScopedObjectAccess soa(Thread::Current()); StackHandleScope<6> hs(soa.Self()); Handle c = hs.NewHandle(GetClassRoot()); ASSERT_TRUE(c != nullptr); Handle obj0 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj0 != nullptr); Handle obj1 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj1 != nullptr); Handle obj2 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj2 != nullptr); Handle obj3 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj3 != nullptr); Handle obj4 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj4 != nullptr); std::string error_msg; // 1) Segment with holes (current_num_holes_ > 0), push new segment, add/remove reference. { LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref1)); EXPECT_EQ(lrt.Capacity(), 3u); // New segment. const LRTSegmentState cookie = lrt.PushFrame(); IndirectRef iref3 = lrt.Add(obj3.Get(), &error_msg); // Must not have filled the previous hole. EXPECT_EQ(lrt.Capacity(), 4u); EXPECT_FALSE(lrt.IsValidReference(iref1, &error_msg)); CheckDump(&lrt, 3, 3); lrt.PopFrame(cookie); EXPECT_EQ(lrt.Capacity(), 3u); UNUSED(iref0, iref1, iref2, iref3); } // 2) Segment with holes (current_num_holes_ > 0), pop segment, add/remove reference { LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie = lrt.PushFrame(); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(obj2.Get(), &error_msg); IndirectRef iref3 = lrt.Add(obj3.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref2)); // Pop segment. lrt.PopFrame(cookie); IndirectRef iref4 = lrt.Add(obj4.Get(), &error_msg); EXPECT_EQ(lrt.Capacity(), 2u); EXPECT_FALSE(lrt.IsValidReference(iref2, &error_msg)); CheckDump(&lrt, 2, 2); UNUSED(iref0, iref1, iref2, iref3, iref4); } // 3) Segment with holes (current_num_holes_ > 0), push new segment, pop segment, add/remove // reference. { LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie0 = lrt.PushFrame(); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref1)); // New segment. const LRTSegmentState cookie1 = lrt.PushFrame(); IndirectRef iref3 = lrt.Add(obj3.Get(), &error_msg); // Pop segment. lrt.PopFrame(cookie1); IndirectRef iref4 = lrt.Add(obj4.Get(), &error_msg); EXPECT_EQ(lrt.Capacity(), 3u); if (check_jni) { EXPECT_FALSE(lrt.IsValidReference(iref1, &error_msg)); } CheckDump(&lrt, 3, 3); lrt.PopFrame(cookie0); CheckDump(&lrt, 1, 1); UNUSED(iref0, iref1, iref2, iref3, iref4); } // 4) Empty segment, push new segment, create a hole, pop a segment, add/remove a reference. { LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie0 = lrt.PushFrame(); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref1)); // Emptied segment, push new one. const LRTSegmentState cookie1 = lrt.PushFrame(); IndirectRef iref2 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref3 = lrt.Add(obj2.Get(), &error_msg); IndirectRef iref4 = lrt.Add(obj3.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref3)); // Pop segment. lrt.PopFrame(cookie1); IndirectRef iref5 = lrt.Add(obj4.Get(), &error_msg); EXPECT_EQ(lrt.Capacity(), 2u); EXPECT_FALSE(lrt.IsValidReference(iref3, &error_msg)); CheckDump(&lrt, 2, 2); // Pop segment. lrt.PopFrame(cookie0); CheckDump(&lrt, 1, 1); UNUSED(iref0, iref1, iref2, iref3, iref4, iref5); } // 5) Base segment, push new segment, create a hole, pop a segment, push new segment, add/remove // reference { LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie0 = lrt.PushFrame(); IndirectRef iref1 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(obj1.Get(), &error_msg); IndirectRef iref3 = lrt.Add(obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(iref2)); // Pop segment. lrt.PopFrame(cookie0); // Push segment. const LRTSegmentState cookie0_second = lrt.PushFrame(); EXPECT_EQ(cookie0.top_index, cookie0_second.top_index); IndirectRef iref4 = lrt.Add(obj3.Get(), &error_msg); EXPECT_EQ(lrt.Capacity(), 2u); EXPECT_FALSE(lrt.IsValidReference(iref3, &error_msg)); CheckDump(&lrt, 2, 2); UNUSED(iref0, iref1, iref2, iref3, iref4); } } TEST_F(LocalReferenceTableTest, BasicHolesTest) { BasicHolesTest(/*check_jni=*/ false, 20u); BasicHolesTest(/*check_jni=*/ false, /*max_count=*/ kSmallLrtEntries); BasicHolesTest(/*check_jni=*/ false, /*max_count=*/ 2u * kSmallLrtEntries); } TEST_F(LocalReferenceTableTest, BasicHolesTestCheckJNI) { BasicHolesTest(/*check_jni=*/ true, 20u); BasicHolesTest(/*check_jni=*/ true, /*max_count=*/ kSmallLrtEntries); BasicHolesTest(/*check_jni=*/ true, /*max_count=*/ 2u * kSmallLrtEntries); } void LocalReferenceTableTest::BasicResizeTest(bool check_jni, size_t max_count) { ScopedObjectAccess soa(Thread::Current()); StackHandleScope<2> hs(soa.Self()); Handle c = hs.NewHandle(GetClassRoot()); ASSERT_TRUE(c != nullptr); Handle obj0 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj0 != nullptr); std::string error_msg; LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; CheckDump(&lrt, 0, 0); for (size_t i = 0; i != max_count + 1; ++i) { lrt.Add(obj0.Get(), &error_msg); } EXPECT_EQ(lrt.Capacity(), max_count + 1); } TEST_F(LocalReferenceTableTest, BasicResizeTest) { BasicResizeTest(/*check_jni=*/ false, 20u); BasicResizeTest(/*check_jni=*/ false, /*max_count=*/ kSmallLrtEntries); BasicResizeTest(/*check_jni=*/ false, /*max_count=*/ 2u * kSmallLrtEntries); BasicResizeTest(/*check_jni=*/ false, /*max_count=*/ gPageSize / sizeof(LrtEntry)); } TEST_F(LocalReferenceTableTest, BasicResizeTestCheckJNI) { BasicResizeTest(/*check_jni=*/ true, 20u); BasicResizeTest(/*check_jni=*/ true, /*max_count=*/ kSmallLrtEntries); BasicResizeTest(/*check_jni=*/ true, /*max_count=*/ 2u * kSmallLrtEntries); BasicResizeTest(/*check_jni=*/ true, /*max_count=*/ gPageSize / sizeof(LrtEntry)); } void LocalReferenceTableTest::TestAddRemove(bool check_jni, size_t max_count, size_t fill_count) { // This will lead to error messages in the log. ScopedLogSeverity sls(LogSeverity::FATAL); ScopedObjectAccess soa(Thread::Current()); StackHandleScope<9> hs(soa.Self()); Handle c = hs.NewHandle(GetClassRoot()); ASSERT_TRUE(c != nullptr); Handle obj0 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj0 != nullptr); Handle obj0x = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj0x != nullptr); Handle obj1 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj1 != nullptr); Handle obj1x = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj1x != nullptr); Handle obj2 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj2 != nullptr); Handle obj2x = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj2x != nullptr); Handle obj3 = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj3 != nullptr); Handle obj3x = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(obj3x != nullptr); std::string error_msg; LocalReferenceTable lrt(check_jni); bool success = lrt.Initialize(max_count, &error_msg); ASSERT_TRUE(success) << error_msg; for (size_t i = 0; i != fill_count; ++i) { IndirectRef iref = lrt.Add(c.Get(), &error_msg); ASSERT_TRUE(iref != nullptr) << error_msg; ASSERT_EQ(i + 1u, lrt.Capacity()); EXPECT_OBJ_PTR_EQ(c.Get(), lrt.Get(iref)); } IndirectRef iref0, iref1, iref2, iref3; #define ADD_REF(iref, obj, expected_capacity) \ do { \ (iref) = lrt.Add((obj).Get(), &error_msg); \ ASSERT_TRUE((iref) != nullptr) << error_msg; \ ASSERT_EQ(fill_count + (expected_capacity), lrt.Capacity()); \ EXPECT_OBJ_PTR_EQ((obj).Get(), lrt.Get(iref)); \ } while (false) #define REMOVE_REF(iref, expected_capacity) \ do { \ ASSERT_TRUE(lrt.Remove(iref)); \ ASSERT_EQ(fill_count + (expected_capacity), lrt.Capacity()); \ } while (false) #define POP_SEGMENT(cookie, expected_capacity) \ do { \ lrt.PopFrame(cookie); \ ASSERT_EQ(fill_count + (expected_capacity), lrt.Capacity()); \ } while (false) const LRTSegmentState cookie0 = lrt.PushFrame(); ADD_REF(iref0, obj0, 1u); ADD_REF(iref1, obj1, 2u); REMOVE_REF(iref1, 1u); // Remove top entry. if (check_jni) { ASSERT_FALSE(lrt.Remove(iref1)); } ADD_REF(iref1, obj1x, 2u); REMOVE_REF(iref0, 2u); // Create hole. IndirectRef obsolete_iref0 = iref0; if (check_jni) { ASSERT_FALSE(lrt.Remove(iref0)); } ADD_REF(iref0, obj0x, 2u); // Reuse hole if (check_jni) { ASSERT_FALSE(lrt.Remove(obsolete_iref0)); } // Test addition to the second segment without a hole in the first segment. // Also test removal from the wrong segment here. LRTSegmentState cookie1 = lrt.PushFrame(); // Create second segment. ASSERT_FALSE(lrt.Remove(iref0)); // Cannot remove from inactive segment. ADD_REF(iref2, obj2, 3u); POP_SEGMENT(cookie1, 2u); // Pop the second segment. if (check_jni) { ASSERT_FALSE(lrt.Remove(iref2)); // Cannot remove from popped segment. } // Test addition to the second segment with a hole in the first. // Use one more reference in the first segment to allow hitting the small table // overflow path either above or here, based on the provided `fill_count`. ADD_REF(iref2, obj2x, 3u); REMOVE_REF(iref1, 3u); // Create hole. cookie1 = lrt.PushFrame(); // Create second segment. ADD_REF(iref3, obj3, 4u); POP_SEGMENT(cookie1, 3u); // Pop the second segment. REMOVE_REF(iref2, 1u); // Remove top entry, prune previous entry. ADD_REF(iref1, obj1, 2u); cookie1 = lrt.PushFrame(); // Create second segment. ADD_REF(iref2, obj2, 3u); ADD_REF(iref3, obj3, 4u); REMOVE_REF(iref2, 4u); // Create hole in second segment. POP_SEGMENT(cookie1, 2u); // Pop the second segment with hole. ADD_REF(iref2, obj2x, 3u); // Prune free list, use new entry. REMOVE_REF(iref2, 2u); REMOVE_REF(iref0, 2u); // Create hole. cookie1 = lrt.PushFrame(); // Create second segment. ADD_REF(iref2, obj2, 3u); ADD_REF(iref3, obj3x, 4u); REMOVE_REF(iref2, 4u); // Create hole in second segment. POP_SEGMENT(cookie1, 2u); // Pop the second segment with hole. ADD_REF(iref0, obj0, 2u); // Prune free list, use remaining entry from free list. REMOVE_REF(iref0, 2u); // Create hole. cookie1 = lrt.PushFrame(); // Create second segment. ADD_REF(iref2, obj2x, 3u); ADD_REF(iref3, obj3, 4u); REMOVE_REF(iref2, 4u); // Create hole in second segment. REMOVE_REF(iref3, 2u); // Remove top entry, prune previous entry, keep hole above. POP_SEGMENT(cookie1, 2u); // Pop the empty second segment. ADD_REF(iref0, obj0x, 2u); // Reuse hole. POP_SEGMENT(cookie0, 0u); // Pop the first segment. #undef REMOVE_REF #undef ADD_REF } TEST_F(LocalReferenceTableTest, TestAddRemove) { TestAddRemove(/*check_jni=*/ false, /*max_count=*/ 20u); TestAddRemove(/*check_jni=*/ false, /*max_count=*/ kSmallLrtEntries); TestAddRemove(/*check_jni=*/ false, /*max_count=*/ 2u * kSmallLrtEntries); static_assert(kSmallLrtEntries >= 4u); for (size_t fill_count = kSmallLrtEntries - 4u; fill_count != kSmallLrtEntries; ++fill_count) { TestAddRemove(/*check_jni=*/ false, /*max_count=*/ kSmallLrtEntries, fill_count); } } TEST_F(LocalReferenceTableTest, TestAddRemoveCheckJNI) { TestAddRemove(/*check_jni=*/ true, /*max_count=*/ 20u); TestAddRemove(/*check_jni=*/ true, /*max_count=*/ kSmallLrtEntries); TestAddRemove(/*check_jni=*/ true, /*max_count=*/ 2u * kSmallLrtEntries); static_assert(kSmallLrtEntries >= 4u); for (size_t fill_count = kSmallLrtEntries - 4u; fill_count != kSmallLrtEntries; ++fill_count) { TestAddRemove(/*check_jni=*/ true, /*max_count=*/ kSmallLrtEntries, fill_count); } } void LocalReferenceTableTest::TestAddRemoveMixed(bool start_check_jni) { // This will lead to error messages in the log. ScopedLogSeverity sls(LogSeverity::FATAL); ScopedObjectAccess soa(Thread::Current()); static constexpr size_t kMaxUniqueRefs = 16; StackHandleScope hs(soa.Self()); Handle c = hs.NewHandle(GetClassRoot()); ASSERT_TRUE(c != nullptr); std::array, kMaxUniqueRefs> objs; for (size_t i = 0u; i != kMaxUniqueRefs; ++i) { objs[i] = hs.NewHandle(c->AllocObject(soa.Self())); ASSERT_TRUE(objs[i] != nullptr); } std::string error_msg; std::array irefs; #define ADD_REF(iref, obj) \ do { \ (iref) = lrt.Add((obj).Get(), &error_msg); \ ASSERT_TRUE((iref) != nullptr) << error_msg; \ EXPECT_OBJ_PTR_EQ((obj).Get(), lrt.Get(iref)); \ } while (false) for (size_t split = 1u; split < kMaxUniqueRefs - 1u; ++split) { for (size_t total = split + 1u; total < kMaxUniqueRefs; ++total) { for (size_t deleted_at_start = 0u; deleted_at_start + 1u < split; ++deleted_at_start) { LocalReferenceTable lrt(/*check_jni=*/ start_check_jni); bool success = lrt.Initialize(kSmallLrtEntries, &error_msg); ASSERT_TRUE(success) << error_msg; for (size_t i = 0; i != split; ++i) { ADD_REF(irefs[i], objs[i]); ASSERT_EQ(i + 1u, lrt.Capacity()); } for (size_t i = 0; i != deleted_at_start; ++i) { ASSERT_TRUE(lrt.Remove(irefs[i])); if (lrt.IsCheckJniEnabled()) { ASSERT_FALSE(lrt.Remove(irefs[i])); } ASSERT_EQ(split, lrt.Capacity()); } lrt.SetCheckJniEnabled(!start_check_jni); // Check top index instead of `Capacity()` after changing the CheckJNI setting. auto get_segment_state = [&lrt]() { LRTSegmentState cookie0 = lrt.PushFrame(); LRTSegmentState cookie1 = lrt.PushFrame(); uint32_t result = cookie1.top_index; lrt.PopFrame(cookie1); lrt.PopFrame(cookie0); return result; }; uint32_t split_top_index = get_segment_state(); uint32_t last_top_index = split_top_index; for (size_t i = split; i != total; ++i) { ADD_REF(irefs[i], objs[i]); ASSERT_LT(last_top_index, get_segment_state()); last_top_index = get_segment_state(); } for (size_t i = split; i != total; ++i) { ASSERT_TRUE(lrt.Remove(irefs[i])); if (lrt.IsCheckJniEnabled()) { ASSERT_FALSE(lrt.Remove(irefs[i])); } if (i + 1u != total) { ASSERT_LE(last_top_index, get_segment_state()); } else { ASSERT_GT(last_top_index, get_segment_state()); ASSERT_LE(split_top_index, get_segment_state()); } } } } } #undef ADD_REF } TEST_F(LocalReferenceTableTest, TestAddRemoveMixed) { TestAddRemoveMixed(/*start_check_jni=*/ false); TestAddRemoveMixed(/*start_check_jni=*/ true); } TEST_F(LocalReferenceTableTest, RegressionTestB276210372) { LocalReferenceTable lrt(/*check_jni=*/ false); std::string error_msg; bool success = lrt.Initialize(kSmallLrtEntries, &error_msg); ASSERT_TRUE(success) << error_msg; ScopedObjectAccess soa(Thread::Current()); ObjPtr c = GetClassRoot(); auto get_previous_state = [&lrt]() { LRTSegmentState previous_state = lrt.PushFrame(); lrt.PopFrame(previous_state); return previous_state; }; // Create the first segment with two references. IndirectRef ref0 = lrt.Add(c, &error_msg); ASSERT_TRUE(ref0 != nullptr); IndirectRef ref1 = lrt.Add(c, &error_msg); ASSERT_TRUE(ref1 != nullptr); // Create a second segment with a hole, then pop it. const LRTSegmentState cookie0A = lrt.PushFrame(); const LRTSegmentState previous_state_A = get_previous_state(); IndirectRef ref2a = lrt.Add(c, &error_msg); ASSERT_TRUE(ref2a != nullptr); IndirectRef ref3a = lrt.Add(c, &error_msg); ASSERT_TRUE(ref3a != nullptr); EXPECT_TRUE(lrt.Remove(ref2a)); lrt.PopFrame(cookie0A); // Create a hole in the first segment. // There was previously a bug that `Remove()` would not prune the popped free entries, // so the new free entry would point to the hole in the popped segment. The code below // would then overwrite that hole with a new segment, pop that segment, reuse the good // free entry and then crash trying to prune the overwritten hole. b/276210372 EXPECT_TRUE(lrt.Remove(ref0)); // Create a second segment again and overwite the old hole, then pop the segment. const LRTSegmentState cookie0B = lrt.PushFrame(); const LRTSegmentState previous_state_B = get_previous_state(); ASSERT_EQ(cookie0B.top_index, cookie0A.top_index); ASSERT_EQ(previous_state_B.top_index, previous_state_A.top_index); IndirectRef ref2b = lrt.Add(c, &error_msg); ASSERT_TRUE(ref2b != nullptr); lrt.PopFrame(cookie0B); // Reuse the hole in first segment. IndirectRef reused0 = lrt.Add(c, &error_msg); ASSERT_TRUE(reused0 != nullptr); // Add a new reference. IndirectRef new_ref = lrt.Add(c, &error_msg); ASSERT_TRUE(new_ref != nullptr); } TEST_F(LocalReferenceTableTest, RegressionTestB276864369) { LocalReferenceTable lrt(/*check_jni=*/ false); std::string error_msg; bool success = lrt.Initialize(kSmallLrtEntries, &error_msg); ASSERT_TRUE(success) << error_msg; ScopedObjectAccess soa(Thread::Current()); ObjPtr c = GetClassRoot(); // Add refs to fill all small tables and one bigger table. const size_t refs_per_page = gPageSize / sizeof(LrtEntry); std::vector refs; for (size_t i = 0; i != 2 * refs_per_page; ++i) { refs.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs.back() != nullptr); } // We had a bug in `Trim()` where we would try to skip one more table than available // if the capacity was exactly at the end of table. If the next table was not allocated, // we would hit a `DCHECK()` in `dchecked_vector<>` in debug mode but in release // mode we would proceed to use memory outside the allocated chunk. b/276864369 lrt.Trim(); } TEST_F(LocalReferenceTableTest, Trim) { LocalReferenceTable lrt(/*check_jni=*/ false); std::string error_msg; bool success = lrt.Initialize(kSmallLrtEntries, &error_msg); ASSERT_TRUE(success) << error_msg; ScopedObjectAccess soa(Thread::Current()); ObjPtr c = GetClassRoot(); // Add refs to fill all small tables. LRTSegmentState cookie0 = lrt.PushFrame(); const size_t refs_per_page = gPageSize / sizeof(LrtEntry); std::vector refs0; for (size_t i = 0; i != refs_per_page; ++i) { refs0.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs0.back() != nullptr); } // Nothing to trim. lrt.Trim(); ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(refs0.back())->IsNull()); // Add refs to fill the next, page-sized table. std::vector refs1; LRTSegmentState cookie1 = lrt.PushFrame(); for (size_t i = 0; i != refs_per_page; ++i) { refs1.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs1.back() != nullptr); } // Nothing to trim. lrt.Trim(); ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(refs1.back())->IsNull()); // Pop one reference and try to trim, there is no page to trim. ASSERT_TRUE(lrt.Remove(refs1.back())); lrt.Trim(); ASSERT_FALSE( IndirectReferenceTable::ClearIndirectRefKind(refs1[refs1.size() - 2u])->IsNull()); // Pop the entire segment with the page-sized table and trim, clearing the page. lrt.PopFrame(cookie1); lrt.Trim(); for (IndirectRef ref : refs1) { ASSERT_TRUE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } refs1.clear(); // Add refs to fill the page-sized table and half of the next one. cookie1 = lrt.PushFrame(); // Push a new segment. for (size_t i = 0; i != 2 * refs_per_page; ++i) { refs1.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs1.back() != nullptr); } // Add refs to fill the other half of the table with two pages. std::vector refs2; const LRTSegmentState cookie2 = lrt.PushFrame(); for (size_t i = 0; i != refs_per_page; ++i) { refs2.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs2.back() != nullptr); } // Nothing to trim. lrt.Trim(); ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(refs1.back())->IsNull()); // Pop the last segment with one page worth of references and trim that page. lrt.PopFrame(cookie2); lrt.Trim(); for (IndirectRef ref : refs2) { ASSERT_TRUE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } refs2.clear(); for (IndirectRef ref : refs1) { ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } // Pop the middle segment with two pages worth of references, and trim those pages. lrt.PopFrame(cookie1); lrt.Trim(); for (IndirectRef ref : refs1) { ASSERT_TRUE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } refs1.clear(); // Pop the first segment with small tables and try to trim. Small tables are never trimmed. lrt.PopFrame(cookie0); lrt.Trim(); for (IndirectRef ref : refs0) { ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } refs0.clear(); // Fill small tables and one more reference, then another segment up to 4 pages. LRTSegmentState cookie0_second = lrt.PushFrame(); ASSERT_EQ(cookie0.top_index, cookie0_second.top_index); for (size_t i = 0; i != refs_per_page + 1u; ++i) { refs0.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs0.back() != nullptr); } cookie1 = lrt.PushFrame(); // Push a new segment. for (size_t i = 0; i != 3u * refs_per_page - 1u; ++i) { refs1.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs1.back() != nullptr); } // Nothing to trim. lrt.Trim(); ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(refs1.back())->IsNull()); // Pop the middle segment, trim two pages. lrt.PopFrame(cookie1); lrt.Trim(); for (IndirectRef ref : refs0) { ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } ASSERT_EQ(refs0.size(), lrt.Capacity()); for (IndirectRef ref : ArrayRef(refs1).SubArray(0u, refs_per_page - 1u)) { // Popped but not trimmed as these are at the same page as the last entry in `refs0`. ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } for (IndirectRef ref : ArrayRef(refs1).SubArray(refs_per_page - 1u)) { ASSERT_TRUE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } } TEST_F(LocalReferenceTableTest, PruneBeforeTrim) { LocalReferenceTable lrt(/*check_jni=*/ false); std::string error_msg; bool success = lrt.Initialize(kSmallLrtEntries, &error_msg); ASSERT_TRUE(success) << error_msg; ScopedObjectAccess soa(Thread::Current()); ObjPtr c = GetClassRoot(); // Add refs to fill all small tables and one bigger table. const LRTSegmentState cookie0 = lrt.PushFrame(); const size_t refs_per_page = gPageSize / sizeof(LrtEntry); std::vector refs; for (size_t i = 0; i != 2 * refs_per_page; ++i) { refs.push_back(lrt.Add(c, &error_msg)); ASSERT_TRUE(refs.back() != nullptr); } // Nothing to trim. lrt.Trim(); ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(refs.back())->IsNull()); // Create a hole in the last page. IndirectRef removed = refs[refs.size() - 2u]; ASSERT_TRUE(lrt.Remove(removed)); // Pop the entire segment and trim. Small tables are not pruned. lrt.PopFrame(cookie0); lrt.Trim(); for (IndirectRef ref : ArrayRef(refs).SubArray(0u, refs_per_page)) { ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } for (IndirectRef ref : ArrayRef(refs).SubArray(refs_per_page)) { ASSERT_TRUE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } // Add a new reference and check that it reused the first slot rather than the old hole. IndirectRef new_ref = lrt.Add(c, &error_msg); ASSERT_TRUE(new_ref != nullptr); ASSERT_NE(new_ref, removed); ASSERT_EQ(new_ref, refs[0]); } } // namespace jni } // namespace art