457 lines
13 KiB
C++
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);
|
|
}
|