/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/CycleCollectedJSRuntime.h" #include "mozilla/HoldDropJSObjects.h" #include "mozilla/Maybe.h" #include "mozilla/UniquePtr.h" #include "mozilla/Vector.h" #include "nsCycleCollectionParticipant.h" #include "nsCycleCollector.h" #include "js/GCAPI.h" #include "gtest/gtest.h" using namespace mozilla; enum HolderKind { SingleZone, MultiZone }; class MyHolder final : public nsScriptObjectTracer, public JSHolderBase { public: explicit MyHolder(HolderKind kind = SingleZone, size_t value = 0) : nsScriptObjectTracer(FlagsForKind(kind)), value(value) {} const size_t value; NS_IMETHOD_(void) Root(void*) override { MOZ_CRASH(); } NS_IMETHOD_(void) Unlink(void*) override { MOZ_CRASH(); } NS_IMETHOD_(void) Unroot(void*) override { MOZ_CRASH(); } NS_IMETHOD_(void) DeleteCycleCollectable(void*) override { MOZ_CRASH(); } NS_IMETHOD_(void) Trace(void* aPtr, const TraceCallbacks& aCb, void* aClosure) override { MOZ_CRASH(); } NS_IMETHOD TraverseNative(void* aPtr, nsCycleCollectionTraversalCallback& aCb) override { MOZ_CRASH(); } NS_DECL_CYCLE_COLLECTION_CLASS_NAME_METHOD(MyHolder) private: Flags FlagsForKind(HolderKind kind) { return kind == MultiZone ? FlagMultiZoneJSHolder : FlagMaybeSingleZoneJSHolder; } }; template static size_t CountEntries(Container& container) { size_t count = 0; for (typename Container::Iter i(container); !i.Done(); i.Next()) { MOZ_RELEASE_ASSERT(i->mHolder); MOZ_RELEASE_ASSERT(i->mTracer); count++; } return count; } JS::Zone* DummyZone = reinterpret_cast(1); JS::Zone* ZoneForKind(HolderKind kind) { return kind == MultiZone ? nullptr : DummyZone; } // Adapter functions to allow working with JSHolderMap and JSHolderList // interchangeably. static bool Has(JSHolderMap& map, MyHolder* holder) { return map.Has(holder); } static bool Has(JSHolderList& list, MyHolder* holder) { JSHolderKey* key = &holder->mJSHolderKey; return list.Has(key); } static void Put(JSHolderMap& map, MyHolder* holder, JS::Zone* zone) { nsScriptObjectTracer* tracer = holder; map.Put(holder, tracer, zone); } static void Put(JSHolderList& list, MyHolder* holder, JS::Zone* zone) { MOZ_RELEASE_ASSERT(!zone); nsScriptObjectTracer* tracer = holder; JSHolderKey* key = &holder->mJSHolderKey; list.Put(holder, tracer, key); } static nsScriptObjectTracer* Get(JSHolderMap& map, MyHolder* holder) { return map.Get(holder); } static nsScriptObjectTracer* Get(JSHolderList& list, MyHolder* holder) { JSHolderKey* key = &holder->mJSHolderKey; return list.Get(holder, key); } static nsScriptObjectTracer* Extract(JSHolderMap& map, MyHolder* holder) { return map.Extract(holder); } static nsScriptObjectTracer* Extract(JSHolderList& list, MyHolder* holder) { JSHolderKey* key = &holder->mJSHolderKey; return list.Extract(holder, key); } TEST(JSHolderMap, Empty) { JSHolderMap map; ASSERT_EQ(CountEntries(map), 0u); } TEST(JSHolderList, Empty) { JSHolderList list; ASSERT_EQ(CountEntries(list), 0u); } template static void TestAddAndRemove(HolderKind kind) { Container container; MyHolder holder(kind); nsScriptObjectTracer* tracer = &holder; ASSERT_FALSE(Has(container, &holder)); ASSERT_EQ(Extract(container, &holder), nullptr); Put(container, &holder, ZoneForKind(kind)); ASSERT_TRUE(Has(container, &holder)); ASSERT_EQ(CountEntries(container), 1u); ASSERT_EQ(Get(container, &holder), tracer); ASSERT_EQ(Extract(container, &holder), tracer); ASSERT_EQ(Extract(container, &holder), nullptr); ASSERT_FALSE(Has(container, &holder)); ASSERT_EQ(CountEntries(container), 0u); } TEST(JSHolderMap, AddAndRemove) { TestAddAndRemove(SingleZone); TestAddAndRemove(MultiZone); } TEST(JSHolderList, AddAndRemove) { TestAddAndRemove(MultiZone); } template static void TestIterate(HolderKind kind) { Container container; MyHolder holder(kind, 0); Maybe iter; // Iterate an empty container. iter.emplace(container); ASSERT_TRUE(iter->Done()); iter.reset(); // Iterate a container with one entry. Put(container, &holder, ZoneForKind(kind)); iter.emplace(container); ASSERT_FALSE(iter->Done()); ASSERT_EQ(iter->Get().mHolder, &holder); iter->Next(); ASSERT_TRUE(iter->Done()); iter.reset(); // Iterate a container with 10 entries. constexpr size_t count = 10; Vector, 0, InfallibleAllocPolicy> holders; bool seen[count] = {}; for (size_t i = 1; i < count; i++) { MOZ_ALWAYS_TRUE( holders.emplaceBack(mozilla::MakeUnique(kind, i))); Put(container, holders.back().get(), ZoneForKind(kind)); } for (iter.emplace(container); !iter->Done(); iter->Next()) { MyHolder* holder = static_cast(iter->Get().mHolder); size_t value = holder->value; ASSERT_TRUE(value < count); ASSERT_FALSE(seen[value]); seen[value] = true; } for (const auto& s : seen) { ASSERT_TRUE(s); } } TEST(JSHolderMap, Iterate) { TestIterate(SingleZone); TestIterate(MultiZone); } TEST(JSHolderList, Iterate) { TestIterate(MultiZone); } template static void TestAddRemoveMany(HolderKind kind, size_t count) { Container container; Vector, 0, InfallibleAllocPolicy> holders; for (size_t i = 0; i < count; i++) { MOZ_ALWAYS_TRUE(holders.emplaceBack(mozilla::MakeUnique(kind))); } for (size_t i = 0; i < count; i++) { MyHolder* holder = holders[i].get(); Put(container, holder, ZoneForKind(kind)); } ASSERT_EQ(CountEntries(container), count); for (size_t i = 0; i < count; i++) { MyHolder* holder = holders[i].get(); ASSERT_EQ(Extract(container, holder), holder); } ASSERT_EQ(CountEntries(container), 0u); } TEST(JSHolderMap, TestAddRemoveMany) { TestAddRemoveMany(SingleZone, 10000); TestAddRemoveMany(MultiZone, 10000); } TEST(JSHolderList, TestAddRemoveMany) { TestAddRemoveMany(MultiZone, 10000); } template static void TestRemoveWhileIterating(HolderKind kind, size_t count) { Container container; Vector, 0, InfallibleAllocPolicy> holders; Maybe iter; for (size_t i = 0; i < count; i++) { MOZ_ALWAYS_TRUE(holders.emplaceBack(MakeUnique(kind))); } // Iterate a container with one entry but remove it before we get to it. MyHolder* holder = holders[0].get(); Put(container, holder, ZoneForKind(kind)); iter.emplace(container); ASSERT_FALSE(iter->Done()); ASSERT_EQ(Extract(container, holder), holder); iter->UpdateForRemovals(); ASSERT_TRUE(iter->Done()); // Check UpdateForRemovals is safe to call on a done iterator. iter->UpdateForRemovals(); ASSERT_TRUE(iter->Done()); iter.reset(); // Add many holders and remove them mid way through iteration. for (size_t i = 0; i < count; i++) { MyHolder* holder = holders[i].get(); Put(container, holder, ZoneForKind(kind)); } iter.emplace(container); for (size_t i = 0; i < count / 2; i++) { iter->Next(); ASSERT_FALSE(iter->Done()); } for (size_t i = 0; i < count; i++) { MyHolder* holder = holders[i].get(); ASSERT_EQ(Extract(container, holder), holder); } iter->UpdateForRemovals(); ASSERT_TRUE(iter->Done()); iter.reset(); ASSERT_EQ(CountEntries(container), 0u); } TEST(JSHolderMap, TestRemoveWhileIterating) { TestRemoveWhileIterating(SingleZone, 10000); TestRemoveWhileIterating(MultiZone, 10000); } TEST(JSHolderList, TestRemoveWhileIterating) { TestRemoveWhileIterating(MultiZone, 10000); } class ObjectHolderBase { public: void SetObject(JSObject* aObject) { mObject = aObject; } void ClearObject() { mObject = nullptr; } JSObject* GetObject() const { return mObject; } JSObject* GetObjectUnbarriered() const { return mObject.unbarrieredGet(); } bool ObjectIsGray() const { JSObject* obj = mObject.unbarrieredGet(); MOZ_RELEASE_ASSERT(obj); return JS::GCThingIsMarkedGray(JS::GCCellPtr(obj)); } protected: JS::Heap mObject; }; class ObjectHolder final : public ObjectHolderBase { public: ObjectHolder() { HoldJSObjects(this); } NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ObjectHolder) NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(ObjectHolder) private: ~ObjectHolder() { DropJSObjects(this); } }; NS_IMPL_CYCLE_COLLECTION_CLASS(ObjectHolder) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ObjectHolder) tmp->ClearObject(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ObjectHolder) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ObjectHolder) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mObject) NS_IMPL_CYCLE_COLLECTION_TRACE_END class ObjectHolderWithKey final : public ObjectHolderBase, public JSHolderBase { public: ObjectHolderWithKey() { HoldJSObjectsWithKey(this); } NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ObjectHolderWithKey) NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(ObjectHolderWithKey) private: ~ObjectHolderWithKey() { DropJSObjectsWithKey(this); } }; NS_IMPL_CYCLE_COLLECTION_CLASS(ObjectHolderWithKey) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ObjectHolderWithKey) tmp->ClearObject(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ObjectHolderWithKey) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ObjectHolderWithKey) NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mObject) NS_IMPL_CYCLE_COLLECTION_TRACE_END // Test GC things stored in JS holders are marked as gray roots by the GC. template static void TestHoldersAreMarkedGray(JSContext* cx) { RefPtr holder(new Holder); JSObject* obj = JS_NewPlainObject(cx); ASSERT_TRUE(obj); holder->SetObject(obj); obj = nullptr; JS_GC(cx); ASSERT_TRUE(holder->ObjectIsGray()); } // Test GC things stored in JS holders are updated by compacting GC. template static void TestHoldersAreMoved(JSContext* cx, bool singleZone) { JS::RootedObject obj(cx, JS_NewPlainObject(cx)); ASSERT_TRUE(obj); // Set a property so we can check we have the same object at the end. const char* PropertyName = "answer"; const int32_t PropertyValue = 42; JS::RootedValue value(cx, JS::Int32Value(PropertyValue)); ASSERT_TRUE(JS_SetProperty(cx, obj, PropertyName, value)); // Ensure the object is tenured. JS_GC(cx); RefPtr holder(new Holder); holder->SetObject(obj); uintptr_t original = uintptr_t(obj.get()); if (singleZone) { JS::PrepareZoneForGC(cx, js::GetContextZone(cx)); } else { JS::PrepareForFullGC(cx); } JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::DEBUG_GC); // Shrinking DEBUG_GC should move all GC things. ASSERT_NE(uintptr_t(holder->GetObject()), original); // Both root and holder should have been updated. ASSERT_EQ(obj, holder->GetObject()); // Check it's the object we expect. value.setUndefined(); ASSERT_TRUE(JS_GetProperty(cx, obj, PropertyName, &value)); ASSERT_EQ(value, JS::Int32Value(PropertyValue)); } static const JSClass GlobalClass = {"global", JSCLASS_GLOBAL_FLAGS, &JS::DefaultGlobalClassOps}; static void GetJSContext(JSContext** aContextOut) { CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); ASSERT_NE(ccjscx, nullptr); JSContext* cx = ccjscx->Context(); ASSERT_NE(cx, nullptr); *aContextOut = cx; } static void CreateGlobal(JSContext* cx, JS::MutableHandleObject aGlobalOut) { JS::RealmOptions options; // dummy options.behaviors().setReduceTimerPrecisionCallerType( JS::RTPCallerTypeToken{0}); JSObject* global = JS_NewGlobalObject(cx, &GlobalClass, nullptr, JS::FireOnNewGlobalHook, options); ASSERT_NE(global, nullptr); aGlobalOut.set(global); } TEST(JSHolderMap, GCIntegration) { JSContext* cx; GetJSContext(&cx); JS::RootedObject global(cx); CreateGlobal(cx, &global); JSAutoRealm ar(cx, global); TestHoldersAreMarkedGray(cx); TestHoldersAreMoved(cx, true); TestHoldersAreMoved(cx, false); } TEST(JSHolderList, GCIntegration) { JSContext* cx; GetJSContext(&cx); JS::RootedObject global(cx); CreateGlobal(cx, &global); JSAutoRealm ar(cx, global); TestHoldersAreMarkedGray(cx); TestHoldersAreMoved(cx, true); TestHoldersAreMoved(cx, false); }