Skip to content
File

Blob: src/workerd/jsg/tracing-test.c++

8.6 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "jsg-test.h"
6 
7namespace workerd::jsg::test {
8namespace {
9 
10V8System v8System({"--expose-gc"_kj});
11 
12class NumberBoxHolder: public Object {
13 // An object that holds a NumberBox and implements GC visitation correctly.
14 //
15 // This differs from BoxBox (in jsg-test.h) in that this just holds the exact object you give
16 // it, whereas BoxBox likes to create new objects.
17 
18 public:
19 explicit NumberBoxHolder(Ref<NumberBox> inner): inner(kj::mv(inner)) {}
20 
21 Ref<NumberBox> inner;
22 
23 static Ref<NumberBoxHolder> constructor(jsg::Lock& js, Ref<NumberBox> inner) {
24 return js.alloc<NumberBoxHolder>(kj::mv(inner));
25 }
26 
27 Ref<NumberBox> getInner() {
28 return inner.addRef();
29 }
30 
31 JSG_RESOURCE_TYPE(NumberBoxHolder) {
32 JSG_READONLY_PROTOTYPE_PROPERTY(inner, getInner);
33 }
34 
35 private:
36 void visitForGc(GcVisitor& visitor) {
37 visitor.visit(inner);
38 }
39};
40 
41class GcDetector: public jsg::Object {
42 // Object which comes in pairs where one member of the pair can detect if the other has been
43 // collected.
44 
45 public:
46 ~GcDetector() noexcept(false) {
47 KJ_IF_SOME(s, sibling) s.sibling = kj::none;
48 }
49 
50 kj::Maybe<GcDetector&> sibling;
51 
52 bool getSiblingCollected() {
53 return sibling == kj::none;
54 }
55 
56 bool touch() {
57 return true;
58 }
59 
60 JSG_RESOURCE_TYPE(GcDetector) {
61 // NOTE: Using an instance property instead of a prototype property causes V8 to refuse to
62 // collect the wrapper during minor GCs, as it always thinks the wrapper is "modified".
63 JSG_READONLY_PROTOTYPE_PROPERTY(siblingCollected, getSiblingCollected);
64 JSG_METHOD(touch);
65 }
66};
67 
68class GcDetectorBox: public jsg::Object {
69 // Contains a GcDetector. Useful for testing tracing scenarios.
70 
71 public:
72 GcDetectorBox(jsg::Lock& js): inner(js.alloc<GcDetector>()) {}
73 jsg::Ref<GcDetector> inner;
74 
75 jsg::Ref<GcDetector> getInner() {
76 return inner.addRef();
77 }
78 
79 JSG_RESOURCE_TYPE(GcDetectorBox) {
80 JSG_READONLY_PROTOTYPE_PROPERTY(inner, getInner);
81 }
82 
83 private:
84 void visitForGc(GcVisitor& visitor) {
85 visitor.visit(inner);
86 }
87};
88 
89class ValueBox: public jsg::Object {
90 // Contains an arbitrary value.
91 
92 public:
93 ValueBox(jsg::Value inner): inner(kj::mv(inner)) {}
94 
95 static jsg::Ref<ValueBox> constructor(jsg::Lock& js, jsg::Value inner) {
96 return js.alloc<ValueBox>(kj::mv(inner));
97 }
98 
99 jsg::Value inner;
100 
101 jsg::Value getInner(jsg::Lock& lock) {
102 return inner.addRef(lock);
103 }
104 
105 JSG_RESOURCE_TYPE(ValueBox) {
106 JSG_READONLY_PROTOTYPE_PROPERTY(inner, getInner);
107 }
108 
109 private:
110 void visitForGc(GcVisitor& visitor) {
111 visitor.visit(inner);
112 }
113};
114 
115struct TraceTestContext: public Object, public ContextGlobal {
116 kj::Maybe<jsg::Ref<NumberBox>> strongRef;
117 // A strong reference to a NumberBox which may be get and set.
118 
119 jsg::Ref<NumberBox> getStrongRef() {
120 return KJ_REQUIRE_NONNULL(strongRef).addRef();
121 }
122 
123 void setStrongRef(jsg::Ref<NumberBox> ref) {
124 strongRef = kj::mv(ref);
125 }
126 
127 kj::Array<jsg::Ref<GcDetector>> makeGcDetectorPair(jsg::Lock& js) {
128 auto obj1 = js.alloc<GcDetector>();
129 auto obj2 = js.alloc<GcDetector>();
130 obj1->sibling = *obj2;
131 obj2->sibling = *obj1;
132 return kj::arr(kj::mv(obj1), kj::mv(obj2));
133 }
134 
135 kj::Array<jsg::Ref<GcDetectorBox>> makeGcDetectorBoxPair(jsg::Lock& js) {
136 auto obj1 = js.alloc<GcDetectorBox>(js);
137 auto obj2 = js.alloc<GcDetectorBox>(js);
138 obj1->inner->sibling = *obj2->inner;
139 obj2->inner->sibling = *obj1->inner;
140 return kj::arr(kj::mv(obj1), kj::mv(obj2));
141 }
142 
143 void assert_(bool condition, jsg::Optional<kj::String> message) {
144 JSG_ASSERT(condition, Error, message.orDefault(nullptr));
145 }
146 
147 JSG_RESOURCE_TYPE(TraceTestContext) {
148 JSG_NESTED_TYPE(NumberBox);
149 JSG_NESTED_TYPE(NumberBoxHolder);
150 JSG_NESTED_TYPE(GcDetector);
151 JSG_NESTED_TYPE(ValueBox);
152 JSG_METHOD(makeGcDetectorPair);
153 JSG_METHOD(makeGcDetectorBoxPair);
154 JSG_METHOD_NAMED(assert, assert_);
155 JSG_PROTOTYPE_PROPERTY(strongRef, getStrongRef, setStrongRef);
156 }
157};
158 
159JSG_DECLARE_ISOLATE_TYPE(TraceTestIsolate,
160 TraceTestContext,
161 NumberBox,
162 NumberBoxHolder,
163 GcDetector,
164 GcDetectorBox,
165 ValueBox);
166 
167KJ_TEST("GC collects objects when expected") {
168 Evaluator<TraceTestContext, TraceTestIsolate> e(v8System);
169 
170 // Test that a full GC can collect native objects.
171 e.expectEval(R"(
172 let pair = makeGcDetectorPair();
173 let a = pair[0];
174 let b = pair[1];
175 pair = null;
176 a = null;
177 gc();
178 assert(b.siblingCollected, "full GC did not collect native objects");
179 )",
180 "undefined", "undefined");
181 
182 // Test that a full GC can collect native cyclic objects.
183 e.expectEval(R"(
184 let pair = makeGcDetectorBoxPair();
185 let a = pair[0];
186 let b = pair[1].inner;
187 pair = null;
188 a.inner.cycle = a; // create cycle involving a jsg::Ref and a V8 native reference
189 gc();
190 assert(!b.siblingCollected);
191 a = null;
192 gc();
193 assert(b.siblingCollected, "full GC did not collect cycles");
194 )",
195 "undefined", "undefined");
196 
197 // Test that minor GC can collect native objects.
198 e.expectEval(R"(
199 let pair = makeGcDetectorPair();
200 let a = pair[0];
201 let b = pair[1];
202 pair = null;
203 a = null;
204 gc({type: "minor"});
205 assert(b.siblingCollected, "minor GC did not collect native objects");
206 )",
207 "undefined", "undefined");
208 
209 // Test that minor GC does not collect native objects whose wrappers have been "modified".
210 //
211 // This verifies our assumptions about how V8's EmbedderRootHandler works.
212 e.expectEval(R"(
213 let pair = makeGcDetectorPair();
214 let a = pair[0];
215 let b = pair[1];
216 pair = null;
217 a.foo = 123; // modify the wrapper
218 a = null;
219 gc({type: "minor"});
220 assert(!b.siblingCollected, "minor GC collected modified native object");
221 )",
222 "undefined", "undefined");
223 
224 // Test that minor GC collects a native object contained in another native object.
225 e.expectEval(R"(
226 let pair = makeGcDetectorBoxPair();
227 let a = pair[0];
228 let b = pair[1].inner;
229 pair = null;
230 let inner = a.inner;
231 // If I don't wrap `inner.touch()` in an IIFE then `inner` doesn't get collected (even with a
232 // full GC). I guess when invoking a method on a native object, V8 ends up putting a handle on
233 // the stack which doesn't get released until the end of the function? Weird but whatever.
234 (() => {
235 assert(inner.touch()); // make sure inner wrapper is initialized
236 })();
237 inner = null;
238 a = null;
239 gc({type: "minor"});
240 assert(b.siblingCollected, "minor GC did not collect transitive native objects");
241 )",
242 "undefined", "undefined");
243 
244 // Test that minor GC can collect unreachable jsg::Value.
245 e.expectEval(R"(
246 let pair = makeGcDetectorPair();
247 let a = pair[0];
248 let b = pair[1];
249 pair = null;
250 
251 // Without the IIFE here, a hidden reference gets left on the stack or something.
252 (() => {
253 a = new ValueBox(a);
254 })();
255 
256 a = null;
257 
258 // We need two minor GC passes to fully collect the object. This is because the first GC pass
259 // collects the `ValueBox`, thus destroying its `jsg::Value inner` member, but V8's GC doesn't
260 // actually notice that this makes the inner object unreachable until a second pass.
261 // TODO(perf): When V8 implements "unified young-generation", circle back and see if we can
262 // improved this.
263 gc({type: "minor"});
264 gc({type: "minor"});
265 
266 assert(b.siblingCollected, "minor GC did not collect jsg::Value");
267 )",
268 "undefined", "undefined");
269}
270 
271KJ_TEST("TracedReference usage does not lead to crashes") {
272 Evaluator<TraceTestContext, TraceTestIsolate> e(v8System);
273 
274 e.expectEval(
275 // Create an object holding another object.
276 "let holder = new NumberBoxHolder(new NumberBox(123));\n"
277 
278 // Do a GC pass to make sure traced wrappers are allocated.
279 "gc();\n"
280 
281 // Now put the NumberBox into the context's strongRef, and remove the holder. So now
282 // the object is only reachable via a strong ref.
283 "strongRef = holder.inner;\n"
284 "holder = null;\n"
285 
286 // Invoke GC. Since the NumberBox is not reachable via tracing from any other object, its
287 // tracedWrapper will not be marked and will therefore become invalid.
288 "gc();\n"
289 
290 // Now create a new holder which holds the NumberBox.
291 "holder = new NumberBoxHolder(strongRef);\n"
292 
293 // Invoke GC. The new holder will be traced, finding the NumberBox. It had better not crash
294 // on the tracedWrapper having been collected!
295 "gc();\n"
296 
297 // Verify the value is still there...
298 "holder.inner.value",
299 "number", "123");
300}
301 
302} // namespace
303} // namespace workerd::jsg::test