testWeakMap.cpp (8784B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 */ 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 #include "gc/Zone.h" 9 #include "js/Array.h" // JS::GetArrayLength 10 #include "js/Exception.h" // JS_IsExceptionPending 11 #include "js/GlobalObject.h" // JS_NewGlobalObject 12 #include "js/PropertyAndElement.h" // JS_DefineProperty 13 #include "js/WeakMap.h" 14 #include "jsapi-tests/tests.h" 15 #include "vm/Realm.h" 16 17 using namespace js; 18 19 static bool checkSize(JSContext* cx, JS::HandleObject map, uint32_t expected) { 20 JS::RootedObject keys(cx); 21 if (!JS_NondeterministicGetWeakMapKeys(cx, map, &keys)) { 22 return false; 23 } 24 25 uint32_t length; 26 if (!JS::GetArrayLength(cx, keys, &length)) { 27 return false; 28 } 29 30 return length == expected; 31 } 32 33 JSObject* keyDelegate = nullptr; 34 35 BEGIN_TEST(testWeakMap_basicOperations) { 36 JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); 37 CHECK(IsWeakMapObject(map)); 38 39 JS::RootedValue key(cx, JS::ObjectOrNullValue(newKey())); 40 CHECK(!key.isNull()); 41 CHECK(!JS::IsWeakMapObject(&key.toObject())); 42 43 JS::RootedValue r(cx); 44 CHECK(GetWeakMapEntry(cx, map, key, &r)); 45 CHECK(r.isUndefined()); 46 47 CHECK(checkSize(cx, map, 0)); 48 49 JS::RootedValue val(cx, JS::Int32Value(1)); 50 CHECK(SetWeakMapEntry(cx, map, key, val)); 51 52 CHECK(GetWeakMapEntry(cx, map, key, &r)); 53 CHECK(r == val); 54 CHECK(checkSize(cx, map, 1)); 55 56 JS_GC(cx); 57 58 CHECK(GetWeakMapEntry(cx, map, key, &r)); 59 CHECK(r == val); 60 CHECK(checkSize(cx, map, 1)); 61 62 key.setUndefined(); 63 JS_GC(cx); 64 65 CHECK(checkSize(cx, map, 0)); 66 67 return true; 68 } 69 70 JSObject* newKey() { return JS_NewPlainObject(cx); } 71 END_TEST(testWeakMap_basicOperations) 72 73 BEGIN_TEST(testWeakMap_setWeakMapEntry_invalid_key) { 74 JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); 75 CHECK(IsWeakMapObject(map)); 76 CHECK(checkSize(cx, map, 0)); 77 78 JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test")); 79 // sym is a Symbol in global Symbol registry and hence can't be used as a key. 80 JS::RootedSymbol sym(cx, JS::GetSymbolFor(cx, test)); 81 JS::RootedValue key(cx, JS::SymbolValue(sym)); 82 CHECK(!key.isUndefined()); 83 84 JS::RootedValue val(cx, JS::Int32Value(1)); 85 86 CHECK(!JS_IsExceptionPending(cx)); 87 CHECK(SetWeakMapEntry(cx, map, key, val) == false); 88 89 CHECK(JS_IsExceptionPending(cx)); 90 JS::Rooted<JS::Value> exn(cx); 91 CHECK(JS_GetPendingException(cx, &exn)); 92 JS::Rooted<JSObject*> obj(cx, &exn.toObject()); 93 JSErrorReport* err = JS_ErrorFromException(cx, obj); 94 CHECK(err->exnType == JSEXN_TYPEERR); 95 96 JS_ClearPendingException(cx); 97 98 JS::RootedValue r(cx); 99 CHECK(GetWeakMapEntry(cx, map, key, &r)); 100 CHECK(r == JS::UndefinedValue()); 101 CHECK(checkSize(cx, map, 0)); 102 103 return true; 104 } 105 END_TEST(testWeakMap_setWeakMapEntry_invalid_key) 106 107 BEGIN_TEST(testWeakMap_basicOperations_symbols_as_keys) { 108 JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); 109 CHECK(IsWeakMapObject(map)); 110 111 JS::RootedString test(cx, JS_NewStringCopyZ(cx, "test")); 112 JS::RootedSymbol sym(cx, JS::NewSymbol(cx, test)); 113 CHECK(sym); 114 JS::RootedValue key(cx, JS::SymbolValue(sym)); 115 116 JS::RootedValue r(cx); 117 CHECK(GetWeakMapEntry(cx, map, key, &r)); 118 CHECK(r.isUndefined()); 119 120 CHECK(checkSize(cx, map, 0)); 121 122 JS::RootedValue val(cx, JS::Int32Value(1)); 123 CHECK(SetWeakMapEntry(cx, map, key, val)); 124 125 CHECK(GetWeakMapEntry(cx, map, key, &r)); 126 CHECK(r == val); 127 CHECK(checkSize(cx, map, 1)); 128 129 JS_GC(cx); 130 131 CHECK(GetWeakMapEntry(cx, map, key, &r)); 132 CHECK(r == val); 133 CHECK(checkSize(cx, map, 1)); 134 135 sym = nullptr; 136 key.setUndefined(); 137 JS_GC(cx); 138 139 CHECK(checkSize(cx, map, 0)); 140 141 return true; 142 } 143 END_TEST(testWeakMap_basicOperations_symbols_as_keys) 144 145 BEGIN_TEST(testWeakMap_keyDelegates) { 146 AutoLeaveZeal nozeal(cx); 147 148 AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true); 149 JS_GC(cx); 150 JS::RootedObject map(cx, JS::NewWeakMapObject(cx)); 151 CHECK(map); 152 153 JS::RootedObject delegate(cx, newDelegate()); 154 JS::RootedObject key(cx, delegate); 155 if (!JS_WrapObject(cx, &key)) { 156 return false; 157 } 158 CHECK(key); 159 CHECK(delegate); 160 161 keyDelegate = delegate; 162 163 JS::RootedObject delegateRoot(cx); 164 { 165 JSAutoRealm ar(cx, delegate); 166 delegateRoot = JS_NewPlainObject(cx); 167 CHECK(delegateRoot); 168 JS::RootedValue delegateValue(cx, JS::ObjectValue(*delegate)); 169 CHECK(JS_DefineProperty(cx, delegateRoot, "delegate", delegateValue, 0)); 170 } 171 delegate = nullptr; 172 173 /* 174 * Perform an incremental GC, introducing an unmarked CCW to force the map 175 * zone to finish marking before the delegate zone. 176 */ 177 CHECK(newCCW(map, delegateRoot)); 178 performIncrementalGC(); 179 #ifdef DEBUG 180 CHECK(map->zone()->lastSweepGroupIndex() < 181 delegateRoot->zone()->lastSweepGroupIndex()); 182 #endif 183 184 /* Add our entry to the weakmap. */ 185 JS::RootedValue keyVal(cx, JS::ObjectValue(*key)); 186 JS::RootedValue val(cx, JS::Int32Value(1)); 187 CHECK(SetWeakMapEntry(cx, map, keyVal, val)); 188 CHECK(checkSize(cx, map, 1)); 189 190 /* 191 * Check the delegate keeps the entry alive even if the key is not reachable. 192 */ 193 key = nullptr; 194 keyVal.setUndefined(); 195 CHECK(newCCW(map, delegateRoot)); 196 performIncrementalGC(); 197 CHECK(checkSize(cx, map, 1)); 198 199 /* 200 * Check that the zones finished marking at the same time, which is 201 * necessary because of the presence of the delegate and the CCW. 202 */ 203 #ifdef DEBUG 204 CHECK(map->zone()->lastSweepGroupIndex() == 205 delegateRoot->zone()->lastSweepGroupIndex()); 206 #endif 207 208 /* Check that when the delegate becomes unreachable the entry is removed. */ 209 delegateRoot = nullptr; 210 keyDelegate = nullptr; 211 JS_GC(cx); 212 CHECK(checkSize(cx, map, 0)); 213 214 return true; 215 } 216 217 static size_t DelegateObjectMoved(JSObject* obj, JSObject* old) { 218 if (!keyDelegate) { 219 return 0; // Object got moved before we set keyDelegate to point to it. 220 } 221 222 MOZ_RELEASE_ASSERT(keyDelegate == old); 223 keyDelegate = obj; 224 return 0; 225 } 226 227 JSObject* newKey() { 228 static const JSClass keyClass = { 229 "keyWithDelegate", JSCLASS_HAS_RESERVED_SLOTS(1), 230 JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC, 231 JS_NULL_CLASS_EXT, JS_NULL_OBJECT_OPS, 232 }; 233 234 JS::RootedObject key(cx, JS_NewObject(cx, &keyClass)); 235 if (!key) { 236 return nullptr; 237 } 238 239 return key; 240 } 241 242 JSObject* newCCW(JS::HandleObject sourceZone, JS::HandleObject destZone) { 243 /* 244 * Now ensure that this zone will be swept first by adding a cross 245 * compartment wrapper to a new object in the same zone as the 246 * delegate object. 247 */ 248 JS::RootedObject object(cx); 249 { 250 JSAutoRealm ar(cx, destZone); 251 object = JS_NewPlainObject(cx); 252 if (!object) { 253 return nullptr; 254 } 255 } 256 { 257 JSAutoRealm ar(cx, sourceZone); 258 if (!JS_WrapObject(cx, &object)) { 259 return nullptr; 260 } 261 } 262 263 // In order to test the SCC algorithm, we need the wrapper/wrappee to be 264 // tenured. 265 cx->runtime()->gc.evictNursery(); 266 267 return object; 268 } 269 270 JSObject* newDelegate() { 271 static const JSClassOps delegateClassOps = { 272 nullptr, // addProperty 273 nullptr, // delProperty 274 nullptr, // enumerate 275 nullptr, // newEnumerate 276 nullptr, // resolve 277 nullptr, // mayResolve 278 nullptr, // finalize 279 nullptr, // call 280 nullptr, // construct 281 JS_GlobalObjectTraceHook, // trace 282 }; 283 284 static const js::ClassExtension delegateClassExtension = { 285 DelegateObjectMoved, // objectMovedOp 286 }; 287 288 static const JSClass delegateClass = { 289 "delegate", 290 JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1), 291 &delegateClassOps, 292 JS_NULL_CLASS_SPEC, 293 &delegateClassExtension, 294 JS_NULL_OBJECT_OPS, 295 }; 296 297 /* Create the global object. */ 298 JS::RealmOptions options; 299 JS::RootedObject global(cx, 300 JS_NewGlobalObject(cx, &delegateClass, nullptr, 301 JS::FireOnNewGlobalHook, options)); 302 if (!global) { 303 return nullptr; 304 } 305 306 JS_SetReservedSlot(global, 0, JS::Int32Value(42)); 307 return global; 308 } 309 310 void performIncrementalGC() { 311 JSRuntime* rt = cx->runtime(); 312 JS::SliceBudget budget(JS::WorkBudget(1000)); 313 rt->gc.startDebugGC(JS::GCOptions::Normal, budget); 314 315 // Wait until we've started marking before finishing the GC 316 // non-incrementally. 317 while (rt->gc.state() == gc::State::Prepare) { 318 rt->gc.debugGCSlice(budget); 319 } 320 if (JS::IsIncrementalGCInProgress(cx)) { 321 rt->gc.finishGC(JS::GCReason::DEBUG_GC); 322 } 323 } 324 END_TEST(testWeakMap_keyDelegates)