/* * 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 { 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; const LRTSegmentState cookie = kLRTFirstSegment; CheckDump(&lrt, 0, 0); if (check_jni) { IndirectRef bad_iref = (IndirectRef) 0x11110; EXPECT_FALSE(lrt.Remove(cookie, bad_iref)) << "unexpectedly successful removal"; } // Add three, check, remove in the order in which they were added. IndirectRef iref0 = lrt.Add(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); IndirectRef iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 2, 2); IndirectRef iref2 = lrt.Add(cookie, 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(cookie, iref0)); CheckDump(&lrt, 2, 2); EXPECT_TRUE(lrt.Remove(cookie, iref1)); CheckDump(&lrt, 1, 1); EXPECT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(cookie, obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); CheckDump(&lrt, 3, 3); ASSERT_TRUE(lrt.Remove(cookie, iref2)); CheckDump(&lrt, 2, 2); ASSERT_TRUE(lrt.Remove(cookie, iref1)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(cookie, obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); CheckDump(&lrt, 3, 3); ASSERT_EQ(3U, lrt.Capacity()); ASSERT_TRUE(lrt.Remove(cookie, iref1)); CheckDump(&lrt, 2, 2); if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie, 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(cookie, iref2)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); iref2 = lrt.Add(cookie, obj2.Get(), &error_msg); EXPECT_TRUE(iref2 != nullptr); IndirectRef iref3 = lrt.Add(cookie, obj3.Get(), &error_msg); EXPECT_TRUE(iref3 != nullptr); CheckDump(&lrt, 4, 4); ASSERT_TRUE(lrt.Remove(cookie, iref1)); CheckDump(&lrt, 3, 3); iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); ASSERT_EQ(4U, lrt.Capacity()) << "hole not filled"; CheckDump(&lrt, 4, 4); ASSERT_TRUE(lrt.Remove(cookie, iref1)); CheckDump(&lrt, 3, 3); ASSERT_TRUE(lrt.Remove(cookie, iref3)); CheckDump(&lrt, 2, 2); ASSERT_EQ(3U, lrt.Capacity()) << "should be 3 after two deletions"; ASSERT_TRUE(lrt.Remove(cookie, iref2)); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, iref0)); CheckDump(&lrt, 0, 0); iref1 = lrt.Add(cookie, obj1.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 1, 1); if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie, iref0)) << "mismatched del succeeded"; CheckDump(&lrt, 1, 1); } ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, iref0)); CheckDump(&lrt, 0, 0); iref1 = lrt.Add(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref1 != nullptr); CheckDump(&lrt, 1, 1); if (iref0 != iref1) { // Try 0, should not work. ASSERT_FALSE(lrt.Remove(cookie, iref0)) << "temporal del succeeded"; } ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); EXPECT_TRUE(iref0 != nullptr); CheckDump(&lrt, 1, 1); ASSERT_TRUE(lrt.Remove(cookie, 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(cookie, obj0.Get(), &error_msg); ASSERT_TRUE(manyRefs[i] != nullptr) << "Failed adding " << i; CheckDump(&lrt, i + 1, 1); } // ...this one causes overflow. iref0 = lrt.Add(cookie, 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(cookie, 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(cookie, 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; const LRTSegmentState cookie0 = kLRTFirstSegment; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(cookie0, obj0.Get(), &error_msg); IndirectRef iref1 = lrt.Add(cookie0, obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(cookie0, obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie0, iref1)); // New segment. const LRTSegmentState cookie1 = lrt.GetSegmentState(); IndirectRef iref3 = lrt.Add(cookie1, 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); 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; const LRTSegmentState cookie0 = kLRTFirstSegment; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(cookie0, obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie1 = lrt.GetSegmentState(); IndirectRef iref1 = lrt.Add(cookie1, obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(cookie1, obj2.Get(), &error_msg); IndirectRef iref3 = lrt.Add(cookie1, obj3.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie1, iref2)); // Pop segment. lrt.SetSegmentState(cookie1); IndirectRef iref4 = lrt.Add(cookie1, 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; const LRTSegmentState cookie0 = kLRTFirstSegment; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(cookie0, obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie1 = lrt.GetSegmentState(); IndirectRef iref1 = lrt.Add(cookie1, obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(cookie1, obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie1, iref1)); // New segment. const LRTSegmentState cookie2 = lrt.GetSegmentState(); IndirectRef iref3 = lrt.Add(cookie2, obj3.Get(), &error_msg); // Pop segment. lrt.SetSegmentState(cookie2); IndirectRef iref4 = lrt.Add(cookie1, obj4.Get(), &error_msg); EXPECT_EQ(lrt.Capacity(), 3u); if (check_jni) { EXPECT_FALSE(lrt.IsValidReference(iref1, &error_msg)); } CheckDump(&lrt, 3, 3); 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; const LRTSegmentState cookie0 = kLRTFirstSegment; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(cookie0, obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie1 = lrt.GetSegmentState(); IndirectRef iref1 = lrt.Add(cookie1, obj1.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie1, iref1)); // Emptied segment, push new one. const LRTSegmentState cookie2 = lrt.GetSegmentState(); IndirectRef iref2 = lrt.Add(cookie1, obj1.Get(), &error_msg); IndirectRef iref3 = lrt.Add(cookie1, obj2.Get(), &error_msg); IndirectRef iref4 = lrt.Add(cookie1, obj3.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie1, iref3)); // Pop segment. UNUSED(cookie2); lrt.SetSegmentState(cookie1); IndirectRef iref5 = lrt.Add(cookie1, obj4.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, 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; const LRTSegmentState cookie0 = kLRTFirstSegment; CheckDump(&lrt, 0, 0); IndirectRef iref0 = lrt.Add(cookie0, obj0.Get(), &error_msg); // New segment. const LRTSegmentState cookie1 = lrt.GetSegmentState(); IndirectRef iref1 = lrt.Add(cookie1, obj1.Get(), &error_msg); IndirectRef iref2 = lrt.Add(cookie1, obj1.Get(), &error_msg); IndirectRef iref3 = lrt.Add(cookie1, obj2.Get(), &error_msg); EXPECT_TRUE(lrt.Remove(cookie1, iref2)); // Pop segment. lrt.SetSegmentState(cookie1); // Push segment. const LRTSegmentState cookie1_second = lrt.GetSegmentState(); UNUSED(cookie1_second); IndirectRef iref4 = lrt.Add(cookie1, 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); const LRTSegmentState cookie = kLRTFirstSegment; for (size_t i = 0; i != max_count + 1; ++i) { lrt.Add(cookie, 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=*/ kPageSize / 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=*/ kPageSize / 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; const LRTSegmentState cookie0 = kLRTFirstSegment; for (size_t i = 0; i != fill_count; ++i) { IndirectRef iref = lrt.Add(cookie0, 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, cookie, obj, expected_capacity) \ do { \ (iref) = lrt.Add(cookie, (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(cookie, iref, expected_capacity) \ do { \ ASSERT_TRUE(lrt.Remove(cookie, iref)); \ ASSERT_EQ(fill_count + (expected_capacity), lrt.Capacity()); \ } while (false) #define POP_SEGMENT(cookie, expected_capacity) \ do { \ lrt.SetSegmentState(cookie); \ ASSERT_EQ(fill_count + (expected_capacity), lrt.Capacity()); \ } while (false) const LRTSegmentState cookie1 = lrt.GetSegmentState(); ADD_REF(iref0, cookie1, obj0, 1u); ADD_REF(iref1, cookie1, obj1, 2u); REMOVE_REF(cookie1, iref1, 1u); // Remove top entry. if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie1, iref1)); } ADD_REF(iref1, cookie1, obj1x, 2u); REMOVE_REF(cookie1, iref0, 2u); // Create hole. IndirectRef obsolete_iref0 = iref0; if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie1, iref0)); } ADD_REF(iref0, cookie1, obj0x, 2u); // Reuse hole if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie1, obsolete_iref0)); } // Test addition to the second segment without a hole in the first segment. // Also test removal from the wrong segment here. LRTSegmentState cookie2 = lrt.GetSegmentState(); // Create second segment. ASSERT_FALSE(lrt.Remove(cookie2, iref0)); // Cannot remove from inactive segment. ADD_REF(iref2, cookie2, obj2, 3u); POP_SEGMENT(cookie2, 2u); // Pop the second segment. if (check_jni) { ASSERT_FALSE(lrt.Remove(cookie1, 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, cookie2, obj2x, 3u); REMOVE_REF(cookie1, iref1, 3u); // Create hole. cookie2 = lrt.GetSegmentState(); // Create second segment. ADD_REF(iref3, cookie2, obj3, 4u); POP_SEGMENT(cookie2, 3u); // Pop the second segment. REMOVE_REF(cookie1, iref2, 1u); // Remove top entry, prune previous entry. ADD_REF(iref1, cookie1, obj1, 2u); cookie2 = lrt.GetSegmentState(); // Create second segment. ADD_REF(iref2, cookie2, obj2, 3u); ADD_REF(iref3, cookie2, obj3, 4u); REMOVE_REF(cookie2, iref2, 4u); // Create hole in second segment. POP_SEGMENT(cookie2, 2u); // Pop the second segment with hole. ADD_REF(iref2, cookie1, obj2x, 3u); // Prune free list, use new entry. REMOVE_REF(cookie1, iref2, 2u); REMOVE_REF(cookie1, iref0, 2u); // Create hole. cookie2 = lrt.GetSegmentState(); // Create second segment. ADD_REF(iref2, cookie2, obj2, 3u); ADD_REF(iref3, cookie2, obj3x, 4u); REMOVE_REF(cookie2, iref2, 4u); // Create hole in second segment. POP_SEGMENT(cookie2, 2u); // Pop the second segment with hole. ADD_REF(iref0, cookie1, obj0, 2u); // Prune free list, use remaining entry from free list. REMOVE_REF(cookie1, iref0, 2u); // Create hole. cookie2 = lrt.GetSegmentState(); // Create second segment. ADD_REF(iref2, cookie2, obj2x, 3u); ADD_REF(iref3, cookie2, obj3, 4u); REMOVE_REF(cookie2, iref2, 4u); // Create hole in second segment. REMOVE_REF(cookie2, iref3, 2u); // Remove top entry, prune previous entry, keep hole above. POP_SEGMENT(cookie2, 2u); // Pop the empty second segment. ADD_REF(iref0, cookie1, obj0x, 2u); // Reuse hole. #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; const LRTSegmentState cookie0 = kLRTFirstSegment; #define ADD_REF(iref, cookie, obj) \ do { \ (iref) = lrt.Add(cookie, (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], cookie0, objs[i]); ASSERT_EQ(i + 1u, lrt.Capacity()); } for (size_t i = 0; i != deleted_at_start; ++i) { ASSERT_TRUE(lrt.Remove(cookie0, irefs[i])); if (lrt.IsCheckJniEnabled()) { ASSERT_FALSE(lrt.Remove(cookie0, irefs[i])); } ASSERT_EQ(split, lrt.Capacity()); } lrt.SetCheckJniEnabled(!start_check_jni); // Check top index instead of `Capacity()` after changing the CheckJNI setting. uint32_t split_top_index = lrt.GetSegmentState().top_index; uint32_t last_top_index = split_top_index; for (size_t i = split; i != total; ++i) { ADD_REF(irefs[i], cookie0, objs[i]); ASSERT_LT(last_top_index, lrt.GetSegmentState().top_index); last_top_index = lrt.GetSegmentState().top_index; } for (size_t i = split; i != total; ++i) { ASSERT_TRUE(lrt.Remove(cookie0, irefs[i])); if (lrt.IsCheckJniEnabled()) { ASSERT_FALSE(lrt.Remove(cookie0, irefs[i])); } if (i + 1u != total) { ASSERT_LE(last_top_index, lrt.GetSegmentState().top_index); } else { ASSERT_GT(last_top_index, lrt.GetSegmentState().top_index); ASSERT_LE(split_top_index, lrt.GetSegmentState().top_index); } } } } } #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(); // Create the first segment with two references. const LRTSegmentState cookie0 = kLRTFirstSegment; IndirectRef ref0 = lrt.Add(cookie0, c, &error_msg); ASSERT_TRUE(ref0 != nullptr); IndirectRef ref1 = lrt.Add(cookie0, c, &error_msg); ASSERT_TRUE(ref1 != nullptr); // Create a second segment with a hole, then pop it. const LRTSegmentState cookieA = lrt.GetSegmentState(); IndirectRef ref2a = lrt.Add(cookieA, c, &error_msg); ASSERT_TRUE(ref2a != nullptr); IndirectRef ref3a = lrt.Add(cookieA, c, &error_msg); ASSERT_TRUE(ref3a != nullptr); EXPECT_TRUE(lrt.Remove(cookieA, ref2a)); lrt.SetSegmentState(cookieA); // 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(cookie0, ref0)); // Create a second segment again and overwite the old hole, then pop the segment. const LRTSegmentState cookieB = lrt.GetSegmentState(); ASSERT_EQ(cookieB.top_index, cookieA.top_index); IndirectRef ref2b = lrt.Add(cookieB, c, &error_msg); ASSERT_TRUE(ref2b != nullptr); lrt.SetSegmentState(cookieB); // Reuse the hole in first segment. IndirectRef reused0 = lrt.Add(cookie0, c, &error_msg); ASSERT_TRUE(reused0 != nullptr); // Add a new reference. IndirectRef new_ref = lrt.Add(cookie0, 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 LRTSegmentState cookie0 = kLRTFirstSegment; constexpr size_t kRefsPerPage = kPageSize / sizeof(LrtEntry); std::vector refs; for (size_t i = 0; i != 2 * kRefsPerPage; ++i) { refs.push_back(lrt.Add(cookie0, 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. const LRTSegmentState cookie0 = kLRTFirstSegment; constexpr size_t kRefsPerPage = kPageSize / sizeof(LrtEntry); std::vector refs0; for (size_t i = 0; i != kRefsPerPage; ++i) { refs0.push_back(lrt.Add(cookie0, 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.GetSegmentState(); for (size_t i = 0; i != kRefsPerPage; ++i) { refs1.push_back(lrt.Add(cookie1, 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(cookie1, 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.SetSegmentState(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.GetSegmentState(); // Push a new segment. for (size_t i = 0; i != 2 * kRefsPerPage; ++i) { refs1.push_back(lrt.Add(cookie1, 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.GetSegmentState(); for (size_t i = 0; i != kRefsPerPage; ++i) { refs2.push_back(lrt.Add(cookie2, 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.SetSegmentState(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.SetSegmentState(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.SetSegmentState(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. for (size_t i = 0; i != kRefsPerPage + 1u; ++i) { refs0.push_back(lrt.Add(cookie0, c, &error_msg)); ASSERT_TRUE(refs0.back() != nullptr); } cookie1 = lrt.GetSegmentState(); // Push a new segment. for (size_t i = 0; i != 3u * kRefsPerPage - 1u; ++i) { refs1.push_back(lrt.Add(cookie1, 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.SetSegmentState(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, kRefsPerPage - 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(kRefsPerPage - 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 = kLRTFirstSegment; constexpr size_t kRefsPerPage = kPageSize / sizeof(LrtEntry); std::vector refs; for (size_t i = 0; i != 2 * kRefsPerPage; ++i) { refs.push_back(lrt.Add(cookie0, 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(cookie0, removed)); // Pop the entire segment and trim. Small tables are not pruned. lrt.SetSegmentState(cookie0); lrt.Trim(); for (IndirectRef ref : ArrayRef(refs).SubArray(0u, kRefsPerPage)) { ASSERT_FALSE(IndirectReferenceTable::ClearIndirectRefKind(ref)->IsNull()); } for (IndirectRef ref : ArrayRef(refs).SubArray(kRefsPerPage)) { 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(cookie0, c, &error_msg); ASSERT_TRUE(new_ref != nullptr); ASSERT_NE(new_ref, removed); ASSERT_EQ(new_ref, refs[0]); } } // namespace jni } // namespace art