diff options
Diffstat (limited to '')
-rw-r--r-- | xpcom/tests/gtest/TestJSHolderMap.cpp | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/xpcom/tests/gtest/TestJSHolderMap.cpp b/xpcom/tests/gtest/TestJSHolderMap.cpp new file mode 100644 index 0000000000..2255e2e773 --- /dev/null +++ b/xpcom/tests/gtest/TestJSHolderMap.cpp @@ -0,0 +1,345 @@ +/* -*- 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/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: + 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; + } +}; + +static size_t CountEntries(JSHolderMap& map) { + size_t count = 0; + for (JSHolderMap::Iter i(map); !i.Done(); i.Next()) { + MOZ_RELEASE_ASSERT(i->mHolder); + MOZ_RELEASE_ASSERT(i->mTracer); + count++; + } + return count; +} + +JS::Zone* DummyZone = reinterpret_cast<JS::Zone*>(1); + +JS::Zone* ZoneForKind(HolderKind kind) { + return kind == MultiZone ? nullptr : DummyZone; +} + +TEST(JSHolderMap, Empty) +{ + JSHolderMap map; + ASSERT_EQ(CountEntries(map), 0u); +} + +static void TestAddAndRemove(HolderKind kind) { + JSHolderMap map; + + MyHolder holder(kind); + nsScriptObjectTracer* tracer = &holder; + + ASSERT_FALSE(map.Has(&holder)); + ASSERT_EQ(map.Extract(&holder), nullptr); + + map.Put(&holder, tracer, ZoneForKind(kind)); + ASSERT_TRUE(map.Has(&holder)); + ASSERT_EQ(CountEntries(map), 1u); + ASSERT_EQ(map.Get(&holder), tracer); + + ASSERT_EQ(map.Extract(&holder), tracer); + ASSERT_EQ(map.Extract(&holder), nullptr); + ASSERT_FALSE(map.Has(&holder)); + ASSERT_EQ(CountEntries(map), 0u); +} + +TEST(JSHolderMap, AddAndRemove) +{ + TestAddAndRemove(SingleZone); + TestAddAndRemove(MultiZone); +} + +static void TestIterate(HolderKind kind) { + JSHolderMap map; + + MyHolder holder(kind, 0); + nsScriptObjectTracer* tracer = &holder; + + Maybe<JSHolderMap::Iter> iter; + + // Iterate an empty map. + iter.emplace(map); + ASSERT_TRUE(iter->Done()); + iter.reset(); + + // Iterate a map with one entry. + map.Put(&holder, tracer, ZoneForKind(kind)); + iter.emplace(map); + ASSERT_FALSE(iter->Done()); + ASSERT_EQ(iter->Get().mHolder, &holder); + iter->Next(); + ASSERT_TRUE(iter->Done()); + iter.reset(); + + // Iterate a map with 10 entries. + constexpr size_t count = 10; + Vector<UniquePtr<MyHolder>, 0, InfallibleAllocPolicy> holders; + bool seen[count] = {}; + for (size_t i = 1; i < count; i++) { + MOZ_ALWAYS_TRUE( + holders.emplaceBack(mozilla::MakeUnique<MyHolder>(kind, i))); + map.Put(holders.back().get(), tracer, ZoneForKind(kind)); + } + for (iter.emplace(map); !iter->Done(); iter->Next()) { + MyHolder* holder = static_cast<MyHolder*>(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); +} + +static void TestAddRemoveMany(HolderKind kind, size_t count) { + JSHolderMap map; + + Vector<UniquePtr<MyHolder>, 0, InfallibleAllocPolicy> holders; + for (size_t i = 0; i < count; i++) { + MOZ_ALWAYS_TRUE(holders.emplaceBack(mozilla::MakeUnique<MyHolder>(kind))); + } + + for (size_t i = 0; i < count; i++) { + MyHolder* holder = holders[i].get(); + map.Put(holder, holder, ZoneForKind(kind)); + } + + ASSERT_EQ(CountEntries(map), count); + + for (size_t i = 0; i < count; i++) { + MyHolder* holder = holders[i].get(); + ASSERT_EQ(map.Extract(holder), holder); + } + + ASSERT_EQ(CountEntries(map), 0u); +} + +TEST(JSHolderMap, TestAddRemoveMany) +{ + TestAddRemoveMany(SingleZone, 10000); + TestAddRemoveMany(MultiZone, 10000); +} + +static void TestRemoveWhileIterating(HolderKind kind, size_t count) { + JSHolderMap map; + Vector<UniquePtr<MyHolder>, 0, InfallibleAllocPolicy> holders; + Maybe<JSHolderMap::Iter> iter; + + for (size_t i = 0; i < count; i++) { + MOZ_ALWAYS_TRUE(holders.emplaceBack(MakeUnique<MyHolder>(kind))); + } + + // Iterate a map with one entry but remove it before we get to it. + MyHolder* holder = holders[0].get(); + map.Put(holder, holder, ZoneForKind(kind)); + iter.emplace(map); + ASSERT_FALSE(iter->Done()); + ASSERT_EQ(map.Extract(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(); + map.Put(holder, holder, ZoneForKind(kind)); + } + + iter.emplace(map); + 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(map.Extract(holder), holder); + } + + iter->UpdateForRemovals(); + + ASSERT_TRUE(iter->Done()); + iter.reset(); + + ASSERT_EQ(CountEntries(map), 0u); +} + +TEST(JSHolderMap, TestRemoveWhileIterating) +{ + TestRemoveWhileIterating(SingleZone, 10000); + TestRemoveWhileIterating(MultiZone, 10000); +} + +class ObjectHolder final { + public: + ObjectHolder() { HoldJSObjects(this); } + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ObjectHolder) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(ObjectHolder) + + 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)); + } + + private: + JS::Heap<JSObject*> mObject; + + ~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 + +// Test GC things stored in JS holders are marked as gray roots by the GC. +static void TestHoldersAreMarkedGray(JSContext* cx) { + RefPtr holder(new ObjectHolder); + + 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. +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<ObjectHolder> holder(new ObjectHolder); + 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)); +} + +TEST(JSHolderMap, GCIntegration) +{ + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + ASSERT_NE(ccjscx, nullptr); + JSContext* cx = ccjscx->Context(); + ASSERT_NE(cx, nullptr); + + static const JSClass GlobalClass = {"global", JSCLASS_GLOBAL_FLAGS, + &JS::DefaultGlobalClassOps}; + + JS::RealmOptions options; + JS::RootedObject global(cx); + global = JS_NewGlobalObject(cx, &GlobalClass, nullptr, + JS::FireOnNewGlobalHook, options); + ASSERT_NE(global, nullptr); + + JSAutoRealm ar(cx, global); + + TestHoldersAreMarkedGray(cx); + TestHoldersAreMoved(cx, true); + TestHoldersAreMoved(cx, false); +} |