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 "jsapi-tests/tests.h"
11 #include "vm/Realm.h"
12 
13 using namespace js;
14 
15 JSObject* keyDelegate = nullptr;
16 
BEGIN_TEST(testWeakMap_basicOperations)17 BEGIN_TEST(testWeakMap_basicOperations) {
18   JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
19   CHECK(IsWeakMapObject(map));
20 
21   JS::RootedObject key(cx, newKey());
22   CHECK(key);
23   CHECK(!IsWeakMapObject(key));
24 
25   JS::RootedValue r(cx);
26   CHECK(GetWeakMapEntry(cx, map, key, &r));
27   CHECK(r.isUndefined());
28 
29   CHECK(checkSize(map, 0));
30 
31   JS::RootedValue val(cx, JS::Int32Value(1));
32   CHECK(SetWeakMapEntry(cx, map, key, val));
33 
34   CHECK(GetWeakMapEntry(cx, map, key, &r));
35   CHECK(r == val);
36   CHECK(checkSize(map, 1));
37 
38   JS_GC(cx);
39 
40   CHECK(GetWeakMapEntry(cx, map, key, &r));
41   CHECK(r == val);
42   CHECK(checkSize(map, 1));
43 
44   key = nullptr;
45   JS_GC(cx);
46 
47   CHECK(checkSize(map, 0));
48 
49   return true;
50 }
51 
newKey()52 JSObject* newKey() { return JS_NewPlainObject(cx); }
53 
checkSize(JS::HandleObject map,uint32_t expected)54 bool checkSize(JS::HandleObject map, uint32_t expected) {
55   JS::RootedObject keys(cx);
56   CHECK(JS_NondeterministicGetWeakMapKeys(cx, map, &keys));
57 
58   uint32_t length;
59   CHECK(JS::GetArrayLength(cx, keys, &length));
60   CHECK(length == expected);
61 
62   return true;
63 }
64 END_TEST(testWeakMap_basicOperations)
65 
BEGIN_TEST(testWeakMap_keyDelegates)66 BEGIN_TEST(testWeakMap_keyDelegates) {
67   AutoLeaveZeal nozeal(cx);
68 
69   AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
70   JS_GC(cx);
71   JS::RootedObject map(cx, JS::NewWeakMapObject(cx));
72   CHECK(map);
73 
74   JS::RootedObject delegate(cx, newDelegate());
75   JS::RootedObject key(cx, delegate);
76   if (!JS_WrapObject(cx, &key)) {
77     return false;
78   }
79   CHECK(key);
80   CHECK(delegate);
81 
82   keyDelegate = delegate;
83 
84   JS::RootedObject delegateRoot(cx);
85   {
86     JSAutoRealm ar(cx, delegate);
87     delegateRoot = JS_NewPlainObject(cx);
88     CHECK(delegateRoot);
89     JS::RootedValue delegateValue(cx, JS::ObjectValue(*delegate));
90     CHECK(JS_DefineProperty(cx, delegateRoot, "delegate", delegateValue, 0));
91   }
92   delegate = nullptr;
93 
94   /*
95    * Perform an incremental GC, introducing an unmarked CCW to force the map
96    * zone to finish marking before the delegate zone.
97    */
98   CHECK(newCCW(map, delegateRoot));
99   performIncrementalGC();
100 #ifdef DEBUG
101   CHECK(map->zone()->lastSweepGroupIndex() <
102         delegateRoot->zone()->lastSweepGroupIndex());
103 #endif
104 
105   /* Add our entry to the weakmap. */
106   JS::RootedValue val(cx, JS::Int32Value(1));
107   CHECK(SetWeakMapEntry(cx, map, key, val));
108   CHECK(checkSize(map, 1));
109 
110   /*
111    * Check the delegate keeps the entry alive even if the key is not reachable.
112    */
113   key = nullptr;
114   CHECK(newCCW(map, delegateRoot));
115   performIncrementalGC();
116   CHECK(checkSize(map, 1));
117 
118   /*
119    * Check that the zones finished marking at the same time, which is
120    * necessary because of the presence of the delegate and the CCW.
121    */
122 #ifdef DEBUG
123   CHECK(map->zone()->lastSweepGroupIndex() ==
124         delegateRoot->zone()->lastSweepGroupIndex());
125 #endif
126 
127   /* Check that when the delegate becomes unreachable the entry is removed. */
128   delegateRoot = nullptr;
129   keyDelegate = nullptr;
130   JS_GC(cx);
131   CHECK(checkSize(map, 0));
132 
133   return true;
134 }
135 
DelegateObjectMoved(JSObject * obj,JSObject * old)136 static size_t DelegateObjectMoved(JSObject* obj, JSObject* old) {
137   if (!keyDelegate) {
138     return 0;  // Object got moved before we set keyDelegate to point to it.
139   }
140 
141   MOZ_RELEASE_ASSERT(keyDelegate == old);
142   keyDelegate = obj;
143   return 0;
144 }
145 
newKey()146 JSObject* newKey() {
147   static const JSClass keyClass = {
148       "keyWithDelegate", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(1),
149       JS_NULL_CLASS_OPS, JS_NULL_CLASS_SPEC,
150       JS_NULL_CLASS_EXT, JS_NULL_OBJECT_OPS};
151 
152   JS::RootedObject key(cx, JS_NewObject(cx, &keyClass));
153   if (!key) {
154     return nullptr;
155   }
156 
157   return key;
158 }
159 
newCCW(JS::HandleObject sourceZone,JS::HandleObject destZone)160 JSObject* newCCW(JS::HandleObject sourceZone, JS::HandleObject destZone) {
161   /*
162    * Now ensure that this zone will be swept first by adding a cross
163    * compartment wrapper to a new object in the same zone as the
164    * delegate object.
165    */
166   JS::RootedObject object(cx);
167   {
168     JSAutoRealm ar(cx, destZone);
169     object = JS_NewPlainObject(cx);
170     if (!object) {
171       return nullptr;
172     }
173   }
174   {
175     JSAutoRealm ar(cx, sourceZone);
176     if (!JS_WrapObject(cx, &object)) {
177       return nullptr;
178     }
179   }
180 
181   // In order to test the SCC algorithm, we need the wrapper/wrappee to be
182   // tenured.
183   cx->runtime()->gc.evictNursery();
184 
185   return object;
186 }
187 
newDelegate()188 JSObject* newDelegate() {
189   static const JSClassOps delegateClassOps = {
190       nullptr,                   // addProperty
191       nullptr,                   // delProperty
192       nullptr,                   // enumerate
193       nullptr,                   // newEnumerate
194       nullptr,                   // resolve
195       nullptr,                   // mayResolve
196       nullptr,                   // finalize
197       nullptr,                   // call
198       nullptr,                   // hasInstance
199       nullptr,                   // construct
200       JS_GlobalObjectTraceHook,  // trace
201   };
202 
203   static const js::ClassExtension delegateClassExtension = {
204       DelegateObjectMoved,  // objectMovedOp
205   };
206 
207   static const JSClass delegateClass = {
208       "delegate",
209       JSCLASS_GLOBAL_FLAGS | JSCLASS_HAS_RESERVED_SLOTS(1),
210       &delegateClassOps,
211       JS_NULL_CLASS_SPEC,
212       &delegateClassExtension,
213       JS_NULL_OBJECT_OPS};
214 
215   /* Create the global object. */
216   JS::RealmOptions options;
217   JS::RootedObject global(cx,
218                           JS_NewGlobalObject(cx, &delegateClass, nullptr,
219                                              JS::FireOnNewGlobalHook, options));
220   if (!global) {
221     return nullptr;
222   }
223 
224   JS_SetReservedSlot(global, 0, JS::Int32Value(42));
225   return global;
226 }
227 
checkSize(JS::HandleObject map,uint32_t expected)228 bool checkSize(JS::HandleObject map, uint32_t expected) {
229   JS::RootedObject keys(cx);
230   CHECK(JS_NondeterministicGetWeakMapKeys(cx, map, &keys));
231 
232   uint32_t length;
233   CHECK(JS::GetArrayLength(cx, keys, &length));
234   CHECK(length == expected);
235 
236   return true;
237 }
238 
performIncrementalGC()239 void performIncrementalGC() {
240   JSRuntime* rt = cx->runtime();
241   js::SliceBudget budget(js::WorkBudget(1000));
242   rt->gc.startDebugGC(JS::GCOptions::Normal, budget);
243 
244   // Wait until we've started marking before finishing the GC
245   // non-incrementally.
246   while (rt->gc.state() == gc::State::Prepare) {
247     rt->gc.debugGCSlice(budget);
248   }
249   if (JS::IsIncrementalGCInProgress(cx)) {
250     rt->gc.finishGC(JS::GCReason::DEBUG_GC);
251   }
252 }
253 END_TEST(testWeakMap_keyDelegates)
254