Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
*/
/* 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
#include "gc/Zone.h"
#include "js/Array.h" // JS::GetArrayLength
#include "js/Exception.h" // JS_IsExceptionPending
#include "js/GlobalObject.h" // JS_NewGlobalObject
#include "js/PropertyAndElement.h" // JS_DefineProperty
#include "js/WeakMap.h"
#include "jsapi-tests/tests.h"
#include "vm/Realm.h"
using namespace js;
static bool checkSize(JSContext* cx, JS::HandleObject map, uint32_t expected) {
JS::RootedObject keys(cx);
if (!JS_NondeterministicGetWeakMapKeys(cx, map, &keys)) {
return false;
}
uint32_t length;
if (!JS::GetArrayLength(cx, keys, &length)) {
return false;
}
return length == expected;
}
JSObject* keyDelegate = nullptr;
BEGIN_TEST(testWeakMap_basicOperations) {
JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
CHECK(IsWeakMapObject(map));
JS::RootedValue key(cx, JS::ObjectOrNullValue(newKey()));
CHECK(!key.isNull());
CHECK(!JS::IsWeakMapObject(&key.toObject()));
JS::RootedValue r(cx);
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r.isUndefined());
CHECK(checkSize(cx, map, 0));
JS::RootedValue val(cx, JS::Int32Value(1));
CHECK(SetWeakMapEntry(cx, map, key, val));
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r == val);
CHECK(checkSize(cx, map, 1));
JS_GC(cx);
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r == val);
CHECK(checkSize(cx, map, 1));
key.setUndefined();
JS_GC(cx);
CHECK(checkSize(cx, map, 0));
return true;
}
JSObject* newKey() { return JS_NewPlainObject(cx); }
END_TEST(testWeakMap_basicOperations)
BEGIN_TEST(testWeakMap_setWeakMapEntry_invalid_key) {
JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
CHECK(IsWeakMapObject(map));
CHECK(checkSize(cx, map, 0));
JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test"));
// sym is a Symbol in global Symbol registry and hence can't be used as a key.
JS::RootedSymbol sym(cx, JS::GetSymbolFor(cx, test));
JS::RootedValue key(cx, JS::SymbolValue(sym));
CHECK(!key.isUndefined());
JS::RootedValue val(cx, JS::Int32Value(1));
CHECK(!JS_IsExceptionPending(cx));
CHECK(SetWeakMapEntry(cx, map, key, val) == false);
CHECK(JS_IsExceptionPending(cx));
JS::Rooted<JS::Value> exn(cx);
CHECK(JS_GetPendingException(cx, &exn));
JS::Rooted<JSObject*> obj(cx, &exn.toObject());
JSErrorReport* err = JS_ErrorFromException(cx, obj);
CHECK(err->exnType == JSEXN_TYPEERR);
JS_ClearPendingException(cx);
JS::RootedValue r(cx);
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r == JS::UndefinedValue());
CHECK(checkSize(cx, map, 0));
return true;
}
END_TEST(testWeakMap_setWeakMapEntry_invalid_key)
#ifdef NIGHTLY_BUILD
BEGIN_TEST(testWeakMap_basicOperations_symbols_as_keys) {
JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
CHECK(IsWeakMapObject(map));
JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test"));
JS::RootedSymbol sym(cx, JS::NewSymbol(cx, test));
CHECK(sym);
JS::RootedValue key(cx, JS::SymbolValue(sym));
JS::RootedValue r(cx);
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r.isUndefined());
CHECK(checkSize(cx, map, 0));
JS::RootedValue val(cx, JS::Int32Value(1));
CHECK(SetWeakMapEntry(cx, map, key, val));
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r == val);
CHECK(checkSize(cx, map, 1));
JS_GC(cx);
CHECK(GetWeakMapEntry(cx, map, key, &r));
CHECK(r == val);
CHECK(checkSize(cx, map, 1));
sym = nullptr;
key.setUndefined();
JS_GC(cx);
CHECK(checkSize(cx, map, 0));
return true;
}
END_TEST(testWeakMap_basicOperations_symbols_as_keys)
#endif
BEGIN_TEST(testWeakMap_keyDelegates) {
AutoLeaveZeal nozeal(cx);
AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
JS_GC(cx);
JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
CHECK(map);
JS::RootedObject delegate(cx, newDelegate());
JS::RootedObject key(cx, delegate);
if (!JS_WrapObject(cx, &key)) {
return false;
}
CHECK(key);
CHECK(delegate);
keyDelegate = delegate;
JS::RootedObject delegateRoot(cx);
{
JSAutoRealm ar(cx, delegate);
delegateRoot = JS_NewPlainObject(cx);
CHECK(delegateRoot);
JS::RootedValue delegateValue(cx, JS::ObjectValue(*delegate));
CHECK(JS_DefineProperty(cx, delegateRoot, "delegate", delegateValue, 0));
}
delegate = nullptr;
/*
* Perform an incremental GC, introducing an unmarked CCW to force the map
* zone to finish marking before the delegate zone.
*/
CHECK(newCCW(map, delegateRoot));
performIncrementalGC();
#ifdef DEBUG
CHECK(map->zone()->lastSweepGroupIndex() <
delegateRoot->zone()->lastSweepGroupIndex());
#endif
/* Add our entry to the weakmap. */
JS::RootedValue keyVal(cx, JS::ObjectValue(*key));
JS::RootedValue val(cx, JS::Int32Value(1));
CHECK(SetWeakMapEntry(cx, map, keyVal, val));
CHECK(checkSize(cx, map, 1));
/*
* Check the delegate keeps the entry alive even if the key is not reachable.
*/
key = nullptr;
keyVal.setUndefined();
CHECK(newCCW(map, delegateRoot));
performIncrementalGC();
CHECK(checkSize(cx, map, 1));
/*
* Check that the zones finished marking at the same time, which is
* necessary because of the presence of the delegate and the CCW.
*/
#ifdef DEBUG
CHECK(map->zone()->lastSweepGroupIndex() ==
delegateRoot->zone()->lastSweepGroupIndex());
#endif
/* Check that when the delegate becomes unreachable the entry is removed. */
delegateRoot = nullptr;
keyDelegate = nullptr;
JS_GC(cx);
CHECK(checkSize(cx, map, 0));
return true;
}
static size_t DelegateObjectMoved(JSObject* obj, JSObject* old) {
if (!keyDelegate) {
return 0; // Object got moved before we set keyDelegate to point to it.
}
MOZ_RELEASE_ASSERT(keyDelegate == old);
keyDelegate = obj;
return 0;
}
JSObject* newKey() {
static const JSClass keyClass = {
"keyWithDelegate", JSCLASS_HAS_RESERVED_SLOTS(1),
JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC,
JS_NULL_CLASS_EXT, JS_NULL_OBJECT_OPS,
};
JS::RootedObject key(cx, JS_NewObject(cx, &keyClass));
if (!key) {
return nullptr;
}
return key;
}
JSObject* newCCW(JS::HandleObject sourceZone, JS::HandleObject destZone) {
/*
* Now ensure that this zone will be swept first by adding a cross
* compartment wrapper to a new object in the same zone as the
* delegate object.
*/
JS::RootedObject object(cx);
{
JSAutoRealm ar(cx, destZone);
object = JS_NewPlainObject(cx);
if (!object) {
return nullptr;
}
}
{
JSAutoRealm ar(cx, sourceZone);
if (!JS_WrapObject(cx, &object)) {
return nullptr;
}
}
// In order to test the SCC algorithm, we need the wrapper/wrappee to be
// tenured.
cx->runtime()->gc.evictNursery();
return object;
}
JSObject* newDelegate() {
static const JSClassOps delegateClassOps = {
nullptr, // addProperty
nullptr, // delProperty
nullptr, // enumerate
nullptr, // newEnumerate
nullptr, // resolve
nullptr, // mayResolve
nullptr, // finalize
nullptr, // call
nullptr, // construct
JS_GlobalObjectTraceHook, // trace
};
static const js::ClassExtension delegateClassExtension = {
DelegateObjectMoved, // objectMovedOp
};
static const JSClass delegateClass = {
"delegate",
JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1),
&delegateClassOps,
JS_NULL_CLASS_SPEC,
&delegateClassExtension,
JS_NULL_OBJECT_OPS,
};
/* Create the global object. */
JS::RealmOptions options;
JS::RootedObject global(cx,
JS_NewGlobalObject(cx, &delegateClass, nullptr,
JS::FireOnNewGlobalHook, options));
if (!global) {
return nullptr;
}
JS_SetReservedSlot(global, 0, JS::Int32Value(42));
return global;
}
void performIncrementalGC() {
JSRuntime* rt = cx->runtime();
JS::SliceBudget budget(JS::WorkBudget(1000));
rt->gc.startDebugGC(JS::GCOptions::Normal, budget);
// Wait until we've started marking before finishing the GC
// non-incrementally.
while (rt->gc.state() == gc::State::Prepare) {
rt->gc.debugGCSlice(budget);
}
if (JS::IsIncrementalGCInProgress(cx)) {
rt->gc.finishGC(JS::GCReason::DEBUG_GC);
}
}
END_TEST(testWeakMap_keyDelegates)