1
0
Fork 0
firefox/xpcom/tests/gtest/TestJSHolderMap.cpp
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

457 lines
13 KiB
C++

/* -*- 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 <typename Container>
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<JS::Zone*>(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 <typename Container>
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<JSHolderMap>(SingleZone);
TestAddAndRemove<JSHolderMap>(MultiZone);
}
TEST(JSHolderList, AddAndRemove)
{
TestAddAndRemove<JSHolderList>(MultiZone);
}
template <typename Container>
static void TestIterate(HolderKind kind) {
Container container;
MyHolder holder(kind, 0);
Maybe<typename Container::Iter> 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<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)));
Put(container, holders.back().get(), ZoneForKind(kind));
}
for (iter.emplace(container); !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<JSHolderMap>(SingleZone);
TestIterate<JSHolderMap>(MultiZone);
}
TEST(JSHolderList, Iterate)
{
TestIterate<JSHolderList>(MultiZone);
}
template <typename Container>
static void TestAddRemoveMany(HolderKind kind, size_t count) {
Container container;
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();
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<JSHolderMap>(SingleZone, 10000);
TestAddRemoveMany<JSHolderMap>(MultiZone, 10000);
}
TEST(JSHolderList, TestAddRemoveMany)
{
TestAddRemoveMany<JSHolderList>(MultiZone, 10000);
}
template <typename Container>
static void TestRemoveWhileIterating(HolderKind kind, size_t count) {
Container container;
Vector<UniquePtr<MyHolder>, 0, InfallibleAllocPolicy> holders;
Maybe<typename Container::Iter> iter;
for (size_t i = 0; i < count; i++) {
MOZ_ALWAYS_TRUE(holders.emplaceBack(MakeUnique<MyHolder>(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<JSHolderMap>(SingleZone, 10000);
TestRemoveWhileIterating<JSHolderMap>(MultiZone, 10000);
}
TEST(JSHolderList, TestRemoveWhileIterating)
{
TestRemoveWhileIterating<JSHolderList>(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<JSObject*> 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 <typename Holder>
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 <typename Holder>
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> 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<ObjectHolder>(cx);
TestHoldersAreMoved<ObjectHolder>(cx, true);
TestHoldersAreMoved<ObjectHolder>(cx, false);
}
TEST(JSHolderList, GCIntegration)
{
JSContext* cx;
GetJSContext(&cx);
JS::RootedObject global(cx);
CreateGlobal(cx, &global);
JSAutoRealm ar(cx, global);
TestHoldersAreMarkedGray<ObjectHolderWithKey>(cx);
TestHoldersAreMoved<ObjectHolderWithKey>(cx, true);
TestHoldersAreMoved<ObjectHolderWithKey>(cx, false);
}