Skip to content
File

Blob: src/workerd/jsg/wrappable.c++

22.3 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 "wrappable.h"
6 
7#include "jsg.h"
8#include "setup.h"
9 
10#include <workerd/util/thread-scopes.h>
11 
12#include <cppgc/allocation.h>
13#include <cppgc/garbage-collected.h>
14#include <v8-cppgc.h>
15 
16#include <kj/async.h>
17#include <kj/debug.h>
18#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
19#include <sanitizer/asan_interface.h>
20#endif
21 
22namespace workerd::jsg {
23 
24namespace {
25 
26static thread_local bool inCppgcShimDestructor = false;
27 
28};
29 
30bool HeapTracer::isInCppgcDestructor() {
31 return inCppgcShimDestructor;
32}
33 
34void HeapTracer::clearWrappers() {
35 // When clearing wrappers (at isolate shutdown), we may be destroying objects that were recently
36 // determined to be unreachable, but the CppgcShim destructors haven't been run yet. We need to
37 // treat this case as if we are running CppgcShim destructors, that is, assume any
38 // TracedReferences we destroy have already been collected so cannot be touched.
39 // TODO(cleanup): Rename `inCppgcShimDestructor` to `possiblyCollectingUnreachableObject`?
40 KJ_ASSERT(!inCppgcShimDestructor);
41 inCppgcShimDestructor = true;
42 KJ_DEFER(inCppgcShimDestructor = false);
43 
44 while (!wrappers.empty()) {
45 // Don't freelist the shim because we're shutting down anyway.
46 auto& wrappable = wrappers.front();
47 auto ownWrappable = wrappable.detachWrapper(false);
48 // Clear the isolate pointer so that any code that later tries to use it (e.g.,
49 // maybeDeferDestruction() or GcVisitor) will see that the isolate is gone and skip
50 // V8 operations. Without this, objects that outlive their isolate (like WebSockets
51 // stored in a HibernationManager) would have a dangling isolate pointer and crash
52 // when trying to check v8::Locker::IsLocked() or create V8 handles.
53 ownWrappable->isolate = nullptr;
54 }
55 clearFreelistedShims();
56}
57 
58using JSGWrappable = workerd::jsg::Wrappable;
59 
60// V8's GC integrates with cppgc, aka "oilpan", a garbage collector for C++ objects. We want to
61// integrate with the GC in order to receive GC visitation callbacks, so that the GC is able to
62// trace through our C++ objects to find what is reachable through them. The only way for us to
63// support this is by integrating with cppgc.
64//
65// However, workerd was written using KJ idioms long before cppgc existed. Rewriting all our code
66// to use cppgc allocation instead would be a highly invasive change. Maybe we'll do it someday,
67// but today is not the day. So, our API objects continue to be allocated on the regular (non-GC)
68// C++ heap.
69//
70// CppgcShim provides a compromise. For each API object that has been wrapped for use from JS,
71// we create a CppgcShim object on the cppgc heap. This basically just contains a pointer to the
72// regular old C++ object. This lets us get our GC visitation without fully integrating with
73// cppgc.
74//
75// There is an additional trick here: As of this writing, cppgc objects cannot be collected
76// during V8's minor GC passes ("scavenge" passes). Only full GCs ("trace" passes) can collect
77// them. But we do want our API objects to be collectable during minor GC. We integrate with V8's
78// EmbedderRootsHandler to get notification when these objects can be collected. But when they
79// are, what happens to the CppgcShim object we allocated? We can't force it to be collected
80// early. We could just discard it and let it be collected during the next major GC, but that
81// would mean accumulating a lot of garbage shims. Instead, we freelist the objects: when a
82// wrapper is collected during minor GC, the CppgcShim is placed in a freelist and can be
83// reused for a future allocation, if that allocation occurs before the next major GC. When a
84// major GC occurs, the freelist is cleared, since any unreachable CppgcShim objects are likely
85// condemned after that point and will be deleted shortly thereafter.
86class Wrappable::CppgcShim final: public v8::Object::Wrappable {
87 public:
88 CppgcShim(JSGWrappable& wrappable): state(Active{kj::addRef(wrappable)}) {
89 KJ_DASSERT(wrappable.cppgcShim == kj::none);
90 wrappable.cppgcShim = *this;
91 }
92 
93 ~CppgcShim() {
94 // (Unlike most KJ destructors, we don't mark this noexcept(false) because it's called from
95 // V8 which doesn't support exceptions.)
96 
97 KJ_DASSERT(!inCppgcShimDestructor);
98 inCppgcShimDestructor = true;
99 KJ_DEFER(inCppgcShimDestructor = false);
100 
101 KJ_SWITCH_ONEOF(state) {
102 KJ_CASE_ONEOF(active, Active) {
103 KJ_DASSERT(&KJ_ASSERT_NONNULL(active.wrappable->cppgcShim) == this);
104 KJ_DASSERT(active.wrappable->strongWrapper.IsEmpty());
105 active.wrappable->detachWrapper(false);
106 }
107 KJ_CASE_ONEOF(freelisted, Freelisted) {
108 KJ_DASSERT(&KJ_ASSERT_NONNULL(*freelisted.prev) == this);
109 *freelisted.prev = freelisted.next;
110 KJ_IF_SOME(next, freelisted.next) {
111 KJ_DASSERT(next.state.get<Freelisted>().prev == &freelisted.next);
112 next.state.get<Freelisted>().prev = freelisted.prev;
113 }
114 }
115 KJ_CASE_ONEOF(d, Dead) {}
116 }
117 }
118 
119 void Trace(cppgc::Visitor* visitor) const override {
120 KJ_SWITCH_ONEOF(state) {
121 KJ_CASE_ONEOF(active, Active) {
122 active.wrappable->traceFromV8(*visitor);
123 }
124 KJ_CASE_ONEOF(freelisted, Freelisted) {
125 // We're tracing a shim for an object that was collected in minor GC. This could happen
126 // due to conservative GC or due to incremental marking. Unfortunately the shim won't be
127 // collected on this pass but hopefully it can be on the next pass.
128 }
129 KJ_CASE_ONEOF(d, Dead) {}
130 }
131 }
132 
133 const char* GetHumanReadableName() const override {
134 return "CppgcShim";
135 }
136 
137 struct Active {
138 kj::Own<JSGWrappable> wrappable;
139 };
140 
141 // The JavaScript wrapper using this shim was collected in a minor GC. cppgc objects can only
142 // be collected in full GC, so we freelist the shim object in the meantime.
143 struct Freelisted {
144 kj::Maybe<JSGWrappable::CppgcShim&> next;
145 kj::Maybe<JSGWrappable::CppgcShim&>* prev;
146 // kj::List doesn't quite work here because the list link is inside a OneOf. Also we want a
147 // LIFO list anyway so we don't need a tail pointer, which makes things easier. So we do it
148 // manually.
149 };
150 struct Dead {};
151 
152 kj::StringPtr jsgGetMemoryName() const {
153 return "CppgcShim"_kjc;
154 }
155 size_t jsgGetMemorySelfSize() const {
156 return sizeof(CppgcShim);
157 }
158 void jsgGetMemoryInfo(MemoryTracker& tracker) const {
159 KJ_IF_SOME(active, state.tryGet<Active>()) {
160 tracker.trackField("wrappable", active.wrappable);
161 }
162 }
163 bool jsgGetMemoryInfoIsRootNode() const {
164 return false;
165 }
166 
167 mutable kj::OneOf<Active, Freelisted, Dead> state;
168 // This is `mutable` because `Trace()` is const. We configure V8 to perform traces atomically in
169 // the main thread so concurrency is not a concern.
170};
171 
172void HeapTracer::addToFreelist(JSGWrappable::CppgcShim& shim) {
173 auto& freelisted = shim.state.init<JSGWrappable::CppgcShim::Freelisted>();
174 freelisted.next = freelistedShims;
175 KJ_IF_SOME(next, freelisted.next) {
176 next.state.get<JSGWrappable::CppgcShim::Freelisted>().prev = &freelisted.next;
177 }
178 freelisted.prev = &freelistedShims;
179 freelistedShims = shim;
180}
181 
182JSGWrappable::CppgcShim* HeapTracer::allocateShim(JSGWrappable& wrappable) {
183 // Under gc-stress, skip freelist reuse so each shim has a unique address in logs.
184 if (!isGcStressModeForTest()) {
185 KJ_IF_SOME(shim, freelistedShims) {
186 freelistedShims = shim.state.get<JSGWrappable::CppgcShim::Freelisted>().next;
187 KJ_IF_SOME(next, freelistedShims) {
188 next.state.get<JSGWrappable::CppgcShim::Freelisted>().prev = &freelistedShims;
189 }
190 shim.state = JSGWrappable::CppgcShim::Active{kj::addRef(wrappable)};
191 KJ_DASSERT(wrappable.cppgcShim == kj::none);
192 wrappable.cppgcShim = shim;
193 return &shim;
194 }
195 }
196 auto& cppgcAllocHandle = isolate->GetCppHeap()->GetAllocationHandle();
197 auto* shim = cppgc::MakeGarbageCollected<JSGWrappable::CppgcShim>(cppgcAllocHandle, wrappable);
198 return shim;
199}
200 
201void HeapTracer::clearFreelistedShims() {
202 for (;;) {
203 KJ_IF_SOME(shim, freelistedShims) {
204 freelistedShims = shim.state.get<JSGWrappable::CppgcShim::Freelisted>().next;
205 shim.state = JSGWrappable::CppgcShim::Dead{};
206 } else {
207 break;
208 }
209 }
210}
211 
212void HeapTracer::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const {
213 for (const auto& wrapper: wrappers) {
214 tracker.trackField("wrapper", wrapper);
215 }
216 // TODO(soon): Track the other fields here?
217}
218 
219kj::Own<JSGWrappable> JSGWrappable::detachWrapper(bool shouldFreelistShim) {
220 KJ_IF_SOME(shim, cppgcShim) {
221#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
222 // There's a possibility that the CppgcShim has already been found to be unreachable by a GC
223 // pass, but has not actually been destroyed yet. For some reason, cppgc likes to delay the
224 // calling of actual destructors. However, in ASAN builds, cppgc will poison the memory in the
225 // meantime, because it figures that we "shouldn't" be accessing unreachable memory. This
226 // assumption makes sense in the abstract, but not for our specific use case, where we are
227 // essentially maintaining a weak pointer to the CppgcShim. If the destructor had been called,
228 // then `cppgcShim` here would have been nulled out at that time. We're expecting that until
229 // the destructor is called, we can still safely access the object to detach the wrapper.
230 //
231 // So to work around cppgc's incorrect assumption, we manually unpoison the memory.
232 //
233 // Note: An alternative strategy could have been for CppgcShim itself to allocate a separate
234 // C++ heap object to store its own state in, so that that state could be modified even while
235 // the CppgcShim object itself is poisoned. In this case `Wrappable::cppgcShim` would change to
236 // point at this state object, not to the `CppgcShim` itself. However, this approach would
237 // require extra heap allocation for everyone, just to satisfy ASAN, which seems undesirable.
238 ASAN_UNPOISON_MEMORY_REGION(&shim, sizeof(shim));
239#endif
240 
241 auto& tracer = HeapTracer::getTracer(isolate);
242 auto result =
243 kj::mv(KJ_ASSERT_NONNULL(shim.state.tryGet<JSGWrappable::CppgcShim::Active>()).wrappable);
244 if (shouldFreelistShim) {
245 tracer.addToFreelist(shim);
246 } else {
247 shim.state = JSGWrappable::CppgcShim::Dead{};
248 }
249 wrapper = kj::none;
250 cppgcShim = kj::none;
251 strongWrapper.Reset();
252 tracer.removeWrapper({}, *this);
253 if (strongRefcount > 0) {
254 // Need to visit child references in order to convert them to strong references, since we
255 // no longer have an intervening wrapper.
256 GcVisitor visitor(*this, kj::none);
257 jsgVisitForGc(visitor);
258 }
259 return result;
260 } else {
261 return {};
262 }
263}
264 
265v8::Local<v8::Object> Wrappable::getHandle(v8::Isolate* isolate) {
266 return KJ_REQUIRE_NONNULL(tryGetHandle(isolate));
267}
268 
269void Wrappable::addStrongRef() {
270 // The `isolate == nullptr` check here ensures that `js.alloc<T>()` can be used with no
271 // isolate, simply allocating the object as a normal C++ heap object.
272 KJ_DREQUIRE(isolate == nullptr || v8::Isolate::TryGetCurrent() != nullptr,
273 "referencing wrapper without isolate lock");
274 if (strongRefcount++ == 0) {
275 // This object previously had no strong references, but now it has one.
276 KJ_IF_SOME(w, wrapper) {
277 // Copy the traced reference into the strong reference.
278 v8::HandleScope scope(isolate);
279 strongWrapper.Reset(isolate, w.Get(isolate));
280 } else {
281 // Since we have no JS wrapper, we're forced to recursively mark all references reachable
282 // through this wrapper as strong.
283 GcVisitor visitor(*this, kj::none);
284 jsgVisitForGc(visitor);
285 }
286 }
287}
288void Wrappable::removeStrongRef() {
289 KJ_DREQUIRE(isolate == nullptr || v8::Isolate::TryGetCurrent() == isolate,
290 "destroying wrapper without isolate lock");
291 if (--strongRefcount == 0) {
292 // This was the last strong reference.
293 if (wrapper == kj::none) {
294 // We have no wrapper. We need to mark all references held by this object as weak.
295 if (isolate != nullptr) {
296 // But only if the current isolate isn't null. If strong ref count is zero,
297 // the wrapper is empty, and isolate is null, then the child handles it has will
298 // be released anyway (since we're about to be destroyed), thus this visitation
299 // isn't required (and may be buggy, since it may happen outside the isolate lock).
300 GcVisitor visitor(*this, kj::none);
301 jsgVisitForGc(visitor);
302 }
303 } else {
304 // Just clear the strong ref.
305 strongWrapper.Reset();
306 }
307 }
308}
309 
310void Wrappable::maybeDeferDestruction(bool strong, kj::Own<void> ownSelf, Wrappable* self) {
311 DISALLOW_KJ_IO_DESTRUCTORS_SCOPE;
312 
313 auto item = IsolateBase::RefToDelete(strong, kj::mv(ownSelf), self);
314 
315 if (isolate == nullptr || v8::Locker::IsLocked(isolate)) {
316 // If we never attached a wrapper and were never traced, or the isolate is already locked, then
317 // we can just destroy the Wrappable immediately.
318 auto drop = kj::mv(item);
319 } else {
320 // Otherwise, we have a wrapper and we don't have the isolate locked.
321 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
322 jsgIsolate.deferDestruction(kj::mv(item));
323 }
324}
325 
326void Wrappable::traceFromV8(cppgc::Visitor& cppgcVisitor) {
327 cppgcVisitor.Trace(KJ_ASSERT_NONNULL(wrapper));
328 GcVisitor visitor(*this, cppgcVisitor);
329 jsgVisitForGc(visitor);
330}
331 
332void Wrappable::attachWrapper(
333 v8::Isolate* isolate, v8::Local<v8::Object> object, bool needsGcTracing) {
334 auto& tracer = HeapTracer::getTracer(isolate);
335 
336 KJ_REQUIRE(wrapper == kj::none);
337 KJ_REQUIRE(strongWrapper.IsEmpty());
338 
339 // The C++ Wrappable object must hold a TracedReference to its own JavaScript wrapper, while
340 // such a wrapper exists. This way, if the object is reached through C++ again later, we can
341 // return the same object to JavaScript.
342 //
343 // This reference is special: it is marked as "droppable". This tells V8 that we know how to
344 // recreate this wrapper on-demand (from the C++ object). This is an optimization: If the
345 // application drops all of its direct references to the wrapper, such that object is only
346 // reachable implicitly through C++ objects, then V8 can drop the wrapper entirely and have us
347 // recreate it later, when JS needs it again.
348 //
349 // For example, consider a Request object that contains a Headers object. Say the application
350 // accesses the Headers briefly, like `request.headers.get("foo")` -- it doesn't keep around a
351 // direct reference to the Headers. But it DOES keep around a reference to the Request, and the
352 // C++ API object backing the Request keeps a `jsg::Ref<Headers>`. In this case, we do not really
353 // need the JavaScript wrapper for `Headers` to stick around. We know we can create a new one if
354 // and when it is needed. So we tell V8 that our internal reference is "droppable", so that it
355 // will go ahead and drop it in this scenario. (Specifically, v8 calls
356 // `EmbedderRootsHandler::ResetRoot()`, which is implemented by our `HeapTracer`, to tell us that
357 // it is dropping the wrapper.)
358 //
359 // Note that there are things that the application might do which actually make it unsafe for us
360 // to drop and recreate the wrapper. For example, the application could add a property to the
361 // wrapper object itself, like `request.headers.foo = 123`. Later on, when the app accesses
362 // `request.headers.foo` again, it expects the property will still be there. But if we dropped
363 // our wrapper and recreated it, the property would be gone. Luckily, V8 already handles this
364 // for us! V8 knows not to drop our wrapper if the application has done anything with it such
365 // that a recreated wrapper would no longer be equivalent.
366 wrapper.emplace(isolate, object, v8::TracedReference<v8::Object>::IsDroppable());
367 this->isolate = isolate;
368 
369 // Add to list of objects to force-clean at isolate shutdown.
370 tracer.addWrapper({}, *this);
371 
372 // Set up internal fields for a newly-allocated object.
373 KJ_REQUIRE(object->InternalFieldCount() == Wrappable::INTERNAL_FIELD_COUNT);
374 // The third argument is the type tag, a small integer that should be
375 // different for every pointer type to avoid type confusion attacks. We just
376 // use the slot index for now, since we have a different pointer type for
377 // each slot.
378 auto tagAddress = const_cast<uint16_t*>(&WORKERD_WRAPPABLE_TAG);
379 object->SetAlignedPointerInInternalField(WRAPPABLE_TAG_FIELD_INDEX, tagAddress,
380 static_cast<v8::EmbedderDataTypeTag>(WRAPPABLE_TAG_FIELD_INDEX));
381 object->SetAlignedPointerInInternalField(WRAPPED_OBJECT_FIELD_INDEX, this,
382 static_cast<v8::EmbedderDataTypeTag>(WRAPPED_OBJECT_FIELD_INDEX));
383 
384 v8::Object::Wrap<WRAPPABLE_TAG>(isolate, object, tracer.allocateShim(*this));
385 
386 if (strongRefcount > 0) {
387 strongWrapper.Reset(isolate, object);
388 
389 // This object has untraced references, but didn't have a wrapper. That means that any refs
390 // transitively reachable through the reference are strong. Now that a wrapper exists, the
391 // refs will be traced when the wrapper is traced, so they should be converted to traced
392 // references. Performing a visitation pass will update them.
393 GcVisitor visitor(*this, kj::none);
394 jsgVisitForGc(visitor);
395 }
396}
397 
398void Wrappable::jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const {
399 tracker.trackField("cppgcshim", cppgcShim);
400}
401 
402v8::Local<v8::Object> Wrappable::attachOpaqueWrapper(
403 v8::Local<v8::Context> context, bool needsGcTracing) {
404 auto isolate = v8::Isolate::GetCurrent();
405 auto object =
406 jsg::check(IsolateBase::getOpaqueTemplate(isolate)->InstanceTemplate()->NewInstance(context));
407 attachWrapper(isolate, object, needsGcTracing);
408 return object;
409}
410 
411kj::Maybe<Wrappable&> Wrappable::tryUnwrapOpaque(
412 v8::Isolate* isolate, v8::Local<v8::Value> handle) {
413 if (handle->IsObject()) {
414 v8::Local<v8::Object> instance =
415 v8::Local<v8::Object>::Cast(handle)->FindInstanceInPrototypeChain(
416 IsolateBase::getOpaqueTemplate(isolate));
417 if (!instance.IsEmpty()) {
418 return *reinterpret_cast<Wrappable*>(
419 instance->GetAlignedPointerFromInternalField(WRAPPED_OBJECT_FIELD_INDEX,
420 static_cast<v8::EmbedderDataTypeTag>(WRAPPED_OBJECT_FIELD_INDEX)));
421 }
422 }
423 
424 return kj::none;
425}
426 
427void Wrappable::jsgVisitForGc(GcVisitor& visitor) {
428 // Nothing; subclasses that need tracing will override.
429}
430 
431void Wrappable::visitRef(GcVisitor& visitor, kj::Maybe<Wrappable&>& refParent, bool& refStrong) {
432 KJ_IF_SOME(p, refParent) {
433 KJ_ASSERT(&p == &visitor.parent);
434 } else {
435 refParent = visitor.parent;
436 }
437 
438 if (isolate == nullptr) {
439 isolate = visitor.parent.isolate;
440 }
441 
442 // Make ref strength match the parent.
443 if (visitor.parent.strongRefcount > 0 && visitor.parent.wrapper == kj::none) {
444 // This reference should be strong, because the parent has strong refs and does not have its
445 // own wrapper that will be traced.
446 
447 if (!refStrong) {
448 // Ref transitions from weak to strong.
449 //
450 // This should never happen during a GC pass, since we should only be visiting traced
451 // references then.
452 KJ_ASSERT(visitor.cppgcVisitor == kj::none);
453 addStrongRef();
454 refStrong = true;
455 }
456 } else {
457 if (refStrong) {
458 // Ref transitions from strong to weak.
459 //
460 // Note that a Ref can become weak here as part of a GC pass. Specifically, the Ref might
461 // have previously been added to an object that already had a JS wrapper before the Ref was
462 // added. In this case, we won't detect that the Ref is traced until the next GC pass reaches
463 // it.
464 refStrong = false;
465 removeStrongRef();
466 }
467 }
468 
469 KJ_IF_SOME(cgv, visitor.cppgcVisitor) {
470 // We're visiting for the purpose of a GC trace.
471 KJ_IF_SOME(w, wrapper) {
472 cgv.Trace(w);
473 } else {
474 // This object doesn't currently have a wrapper, so traces must transitively trace through
475 // it. However, as an optimization, we can skip the trace if we've already been traced in
476 // this trace pass.
477 GcVisitor subVisitor(*this, visitor.cppgcVisitor);
478 jsgVisitForGc(subVisitor);
479 }
480 }
481}
482 
483void GcVisitor::visit(Data& value) {
484 if (!value.handle.IsEmpty()) {
485 // Make ref strength match the parent.
486 if (parent.strongRefcount > 0 && parent.wrapper == kj::none) {
487 // This is directly reachable by a strong ref, so mark the handle strong.
488 if (value.tracedHandle != kj::none) {
489 // Convert the handle back to strong and discard the traced reference.
490 value.handle.ClearWeak<void>();
491 value.tracedHandle = kj::none;
492 }
493 } else {
494 // This is only reachable via traced objects, so the handle should be weak, and we should
495 // hold a TracedReference alongside it.
496 if (value.tracedHandle == kj::none) {
497 // Create the TracedReference.
498 v8::HandleScope scope(parent.isolate);
499 value.tracedHandle =
500 v8::TracedReference<v8::Data>(parent.isolate, value.handle.Get(parent.isolate));
501 
502 // Set the handle weak.
503 value.handle.SetWeak();
504 }
505 }
506 
507 KJ_IF_SOME(c, cppgcVisitor) {
508 KJ_IF_SOME(t, value.tracedHandle) {
509 c.Trace(t);
510 }
511 }
512 }
513}
514 
515void GcVisitor::visit(v8::Global<v8::Value>& strong, v8::TracedReference<v8::Data>& traced) {
516 if (strong.IsEmpty()) {
517 return;
518 }
519 
520 // Mirror visit(Data&): make handle strength match the parent.
521 //
522 // The `parent.wrapper == kj::none` check mirrors the same condition in
523 // visit(Data&): even when strongRefcount > 0, if the JS wrapper already
524 // exists we must keep the handle in traced mode so cppgc can follow edges
525 // from it. Only when there is no wrapper yet (object not yet exported to JS)
526 // does a positive strongRefcount alone justify keeping the handle strong.
527 if (parent.strongRefcount > 0 && parent.wrapper == kj::none) {
528 // Parent has strong Rust refs and no JS wrapper — keep handle strong,
529 // discard any traced ref.
530 if (!traced.IsEmpty()) {
531 strong.ClearWeak<void>();
532 traced.Reset();
533 }
534 } else {
535 // Parent is only reachable via GC tracing — downgrade to a TracedReference.
536 if (traced.IsEmpty()) {
537 v8::HandleScope scope(parent.isolate);
538 traced.Reset(parent.isolate, strong.Get(parent.isolate));
539 strong.SetWeak();
540 }
541 }
542 
543 KJ_IF_SOME(c, cppgcVisitor) {
544 if (!traced.IsEmpty()) {
545 c.Trace(traced);
546 }
547 }
548}
549 
550} // namespace workerd::jsg