summaryrefslogtreecommitdiffstats
path: root/js/src/jsapi-tests/testGCHeapBarriers.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/jsapi-tests/testGCHeapBarriers.cpp
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jsapi-tests/testGCHeapBarriers.cpp')
-rw-r--r--js/src/jsapi-tests/testGCHeapBarriers.cpp828
1 files changed, 828 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testGCHeapBarriers.cpp b/js/src/jsapi-tests/testGCHeapBarriers.cpp
new file mode 100644
index 0000000000..9493049e16
--- /dev/null
+++ b/js/src/jsapi-tests/testGCHeapBarriers.cpp
@@ -0,0 +1,828 @@
+/* -*- 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+
+#include "gc/AllocKind.h"
+#include "gc/Cell.h"
+#include "gc/GCInternals.h"
+#include "gc/GCRuntime.h"
+#include "js/ArrayBuffer.h" // JS::NewArrayBuffer
+#include "js/experimental/TypedData.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty
+#include "js/RootingAPI.h"
+#include "jsapi-tests/tests.h"
+#include "vm/Runtime.h"
+#include "vm/TypedArrayObject.h"
+
+#include "vm/JSContext-inl.h"
+
+using namespace js;
+
+static js::gc::CellColor GetColor(JSObject* obj) { return obj->color(); }
+static js::gc::CellColor GetColor(const JS::ArrayBufferOrView& view) {
+ return view.asObjectUnbarriered()->color();
+}
+
+static MOZ_MAYBE_UNUSED bool IsInsideNursery(gc::Cell* cell) {
+ return !cell->isTenured();
+}
+static MOZ_MAYBE_UNUSED bool IsInsideNursery(
+ const JS::ArrayBufferOrView& view) {
+ return IsInsideNursery(view.asObjectUnbarriered());
+}
+
+// A heap-allocated structure containing one of our barriered pointer wrappers
+// to test.
+template <typename W, typename T>
+struct TestStruct {
+ W wrapper;
+
+ void trace(JSTracer* trc) {
+ TraceNullableEdge(trc, &wrapper, "TestStruct::wrapper");
+ }
+
+ TestStruct() {}
+ explicit TestStruct(T init) : wrapper(init) {}
+};
+
+template <typename T>
+static T CreateNurseryGCThing(JSContext* cx) = delete;
+
+template <>
+JSObject* CreateNurseryGCThing(JSContext* cx) {
+ JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return nullptr;
+ }
+ JS_DefineProperty(cx, obj, "x", 42, 0);
+ MOZ_ASSERT(IsInsideNursery(obj));
+ return obj;
+}
+
+template <>
+JSFunction* CreateNurseryGCThing(JSContext* cx) {
+ /*
+ * We don't actually use the function as a function, so here we cheat and
+ * cast a JSObject.
+ */
+ return static_cast<JSFunction*>(CreateNurseryGCThing<JSObject*>(cx));
+}
+
+template <>
+JS::Uint8Array CreateNurseryGCThing(JSContext* cx) {
+ JS::Rooted<JS::Uint8Array> arr(cx, JS::Uint8Array::create(cx, 100));
+ JS::RootedObject obj(cx, arr.asObject());
+ JS_DefineProperty(cx, obj, "x", 42, 0);
+ MOZ_ASSERT(IsInsideNursery(obj));
+ return arr;
+}
+
+template <typename T>
+static T CreateTenuredGCThing(JSContext* cx) = delete;
+
+template <>
+JSObject* CreateTenuredGCThing(JSContext* cx) {
+ // Use ArrayBuffers because they have finalizers, which allows using them in
+ // TenuredHeap<> without awkward conversations about nursery allocatability.
+ // Note that at some point ArrayBuffers might become nursery allocated at
+ // which point this test will have to change.
+ JSObject* obj = JS::NewArrayBuffer(cx, 20);
+ MOZ_ASSERT(!IsInsideNursery(obj));
+ MOZ_ASSERT(obj->getClass()->hasFinalize() &&
+ !(obj->getClass()->flags & JSCLASS_SKIP_NURSERY_FINALIZE));
+ return obj;
+}
+
+template <>
+JS::ArrayBuffer CreateTenuredGCThing(JSContext* cx) {
+ return JS::ArrayBuffer::fromObject(CreateTenuredGCThing<JSObject*>(cx));
+}
+
+template <>
+JS::Uint8Array CreateTenuredGCThing(JSContext* cx) {
+ // Use internal APIs that lets us specify the InitialHeap so we can ensure
+ // that this is tenured.
+ JSObject* obj = js::NewUint8ArrayWithLength(cx, 100, gc::Heap::Tenured);
+ MOZ_ASSERT(!IsInsideNursery(obj));
+ return JS::Uint8Array::fromObject(obj);
+}
+
+template <typename T>
+void* CreateHiddenTenuredGCThing(JSContext* cx) {
+ return CreateTenuredGCThing<T>(cx);
+}
+
+template <>
+void* CreateHiddenTenuredGCThing<JS::ArrayBuffer>(JSContext* cx) {
+ return CreateTenuredGCThing<JS::ArrayBuffer>(cx).asObjectUnbarriered();
+}
+
+template <>
+void* CreateHiddenTenuredGCThing<JS::Uint8Array>(JSContext* cx) {
+ return CreateTenuredGCThing<JS::Uint8Array>(cx).asObjectUnbarriered();
+}
+
+static uintptr_t UnbarrieredCastToInt(gc::Cell* cell) {
+ return reinterpret_cast<uintptr_t>(cell);
+}
+static uintptr_t UnbarrieredCastToInt(const JS::ArrayBufferOrView& view) {
+ return UnbarrieredCastToInt(view.asObjectUnbarriered());
+}
+
+template <typename T>
+T RecoverHiddenGCThing(void* ptr) {
+ return reinterpret_cast<T>(ptr);
+}
+
+template <>
+JS::ArrayBuffer RecoverHiddenGCThing(void* ptr) {
+ return JS::ArrayBuffer::fromObject(RecoverHiddenGCThing<JSObject*>(ptr));
+}
+
+template <>
+JS::Uint8Array RecoverHiddenGCThing(void* ptr) {
+ return JS::Uint8Array::fromObject(RecoverHiddenGCThing<JSObject*>(ptr));
+}
+
+static void MakeGray(JSObject* obj) {
+ gc::TenuredCell* cell = &obj->asTenured();
+ cell->unmark();
+ cell->markIfUnmarked(gc::MarkColor::Gray);
+ MOZ_ASSERT(obj->isMarkedGray());
+}
+
+static void MakeGray(const JS::ArrayBufferOrView& view) {
+ MakeGray(view.asObjectUnbarriered());
+}
+
+// Test post-barrier implementation on wrapper types. The following wrapper
+// types have post barriers:
+// - JS::Heap
+// - GCPtr
+// - HeapPtr
+// - WeakHeapPtr
+BEGIN_TEST(testGCHeapPostBarriers) {
+ AutoLeaveZeal nozeal(cx);
+
+ /* Sanity check - objects start in the nursery and then become tenured. */
+ JS_GC(cx);
+ JS::RootedObject obj(cx, CreateNurseryGCThing<JSObject*>(cx));
+ CHECK(IsInsideNursery(obj.get()));
+ JS_GC(cx);
+ CHECK(!IsInsideNursery(obj.get()));
+ JS::RootedObject tenuredObject(cx, obj);
+
+ /* JSObject and JSFunction objects are nursery allocated. */
+ CHECK(TestHeapPostBarriersForType<JSObject*>());
+ CHECK(TestHeapPostBarriersForType<JSFunction*>());
+ CHECK(TestHeapPostBarriersForType<JS::Uint8Array>());
+ // Bug 1599378: Add string tests.
+
+ return true;
+}
+
+bool CanAccessObject(JSObject* obj) {
+ JS::RootedObject rootedObj(cx, obj);
+ JS::RootedValue value(cx);
+ CHECK(JS_GetProperty(cx, rootedObj, "x", &value));
+ CHECK(value.isInt32());
+ CHECK(value.toInt32() == 42);
+ return true;
+}
+bool CanAccessObject(const JS::ArrayBufferOrView& view) {
+ return CanAccessObject(view.asObject());
+}
+
+template <typename T>
+bool TestHeapPostBarriersForType() {
+ CHECK((TestHeapPostBarriersForWrapper<js::GCPtr, T>()));
+ CHECK((TestHeapPostBarriersForMovableWrapper<JS::Heap, T>()));
+ CHECK((TestHeapPostBarriersForMovableWrapper<js::HeapPtr, T>()));
+ CHECK((TestHeapPostBarriersForMovableWrapper<js::WeakHeapPtr, T>()));
+ return true;
+}
+
+template <template <typename> class W, typename T>
+bool TestHeapPostBarriersForMovableWrapper() {
+ CHECK((TestHeapPostBarriersForWrapper<W, T>()));
+ CHECK((TestHeapPostBarrierMoveConstruction<W<T>, T>()));
+ CHECK((TestHeapPostBarrierMoveAssignment<W<T>, T>()));
+ return true;
+}
+
+template <template <typename> class W, typename T>
+bool TestHeapPostBarriersForWrapper() {
+ CHECK((TestHeapPostBarrierConstruction<W<T>, T>()));
+ CHECK((TestHeapPostBarrierConstruction<const W<T>, T>()));
+ CHECK((TestHeapPostBarrierUpdate<W<T>, T>()));
+ if constexpr (!std::is_same_v<W<T>, GCPtr<T>>) {
+ // It is not allowed to delete heap memory containing GCPtrs on
+ // initialization failure like this and doing so will cause an assertion to
+ // fail in the GCPtr destructor (although we disable this in some places in
+ // this test).
+ CHECK((TestHeapPostBarrierInitFailure<W<T>, T>()));
+ CHECK((TestHeapPostBarrierInitFailure<const W<T>, T>()));
+ }
+ return true;
+}
+
+template <typename W, typename T>
+bool TestHeapPostBarrierConstruction() {
+ T initialObj = CreateNurseryGCThing<T>(cx);
+ CHECK(initialObj);
+ CHECK(IsInsideNursery(initialObj));
+ uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj);
+
+ {
+ // We don't root our structure because that would end up tracing it on minor
+ // GC and we're testing that heap post barrier works for things that aren't
+ // roots.
+ JS::AutoSuppressGCAnalysis noAnalysis(cx);
+
+ auto* testStruct = js_new<TestStruct<W, T>>(initialObj);
+ CHECK(testStruct);
+
+ auto& wrapper = testStruct->wrapper;
+ CHECK(wrapper == initialObj);
+
+ cx->minorGC(JS::GCReason::API);
+
+ CHECK(UnbarrieredCastToInt(wrapper.get()) != initialObjAsInt);
+ CHECK(!IsInsideNursery(wrapper.get()));
+ CHECK(CanAccessObject(wrapper.get()));
+
+ // Disable the check that GCPtrs are only destroyed by the GC. What happens
+ // on destruction isn't relevant to the test.
+ gc::AutoSetThreadIsFinalizing threadIsFinalizing;
+
+ js_delete(testStruct);
+ }
+
+ cx->minorGC(JS::GCReason::API);
+
+ return true;
+}
+
+template <typename W, typename T>
+bool TestHeapPostBarrierUpdate() {
+ // Normal case - allocate a heap object, write a nursery pointer into it and
+ // check that it gets updated on minor GC.
+
+ T initialObj = CreateNurseryGCThing<T>(cx);
+ CHECK(initialObj);
+ CHECK(IsInsideNursery(initialObj));
+ uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj);
+
+ {
+ // We don't root our structure because that would end up tracing it on minor
+ // GC and we're testing that heap post barrier works for things that aren't
+ // roots.
+ JS::AutoSuppressGCAnalysis noAnalysis(cx);
+
+ auto* testStruct = js_new<TestStruct<W, T>>();
+ CHECK(testStruct);
+
+ auto& wrapper = testStruct->wrapper;
+ CHECK(!wrapper.get());
+
+ wrapper = initialObj;
+ CHECK(wrapper == initialObj);
+
+ cx->minorGC(JS::GCReason::API);
+
+ CHECK(UnbarrieredCastToInt(wrapper.get()) != initialObjAsInt);
+ CHECK(!IsInsideNursery(wrapper.get()));
+ CHECK(CanAccessObject(wrapper.get()));
+
+ // Disable the check that GCPtrs are only destroyed by the GC. What happens
+ // on destruction isn't relevant to the test.
+ gc::AutoSetThreadIsFinalizing threadIsFinalizing;
+
+ js_delete(testStruct);
+ }
+
+ cx->minorGC(JS::GCReason::API);
+
+ return true;
+}
+
+template <typename W, typename T>
+bool TestHeapPostBarrierInitFailure() {
+ // Failure case - allocate a heap object, write a nursery pointer into it
+ // and fail to complete initialization.
+
+ T initialObj = CreateNurseryGCThing<T>(cx);
+ CHECK(initialObj);
+ CHECK(IsInsideNursery(initialObj));
+
+ {
+ // We don't root our structure because that would end up tracing it on minor
+ // GC and we're testing that heap post barrier works for things that aren't
+ // roots.
+ JS::AutoSuppressGCAnalysis noAnalysis(cx);
+
+ auto testStruct = cx->make_unique<TestStruct<W, T>>(initialObj);
+ CHECK(testStruct);
+
+ auto& wrapper = testStruct->wrapper;
+ CHECK(wrapper == initialObj);
+
+ // testStruct deleted here, as if we left this block due to an error.
+ }
+
+ cx->minorGC(JS::GCReason::API);
+
+ return true;
+}
+
+template <typename W, typename T>
+bool TestHeapPostBarrierMoveConstruction() {
+ T initialObj = CreateNurseryGCThing<T>(cx);
+ CHECK(initialObj);
+ CHECK(IsInsideNursery(initialObj));
+ uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj);
+
+ {
+ // We don't root our structure because that would end up tracing it on minor
+ // GC and we're testing that heap post barrier works for things that aren't
+ // roots.
+ JS::AutoSuppressGCAnalysis noAnalysis(cx);
+
+ W wrapper1(initialObj);
+ CHECK(wrapper1 == initialObj);
+
+ W wrapper2(std::move(wrapper1));
+ CHECK(wrapper2 == initialObj);
+
+ cx->minorGC(JS::GCReason::API);
+
+ CHECK(UnbarrieredCastToInt(wrapper1.get()) != initialObjAsInt);
+ CHECK(UnbarrieredCastToInt(wrapper2.get()) != initialObjAsInt);
+ CHECK(!IsInsideNursery(wrapper2.get()));
+ CHECK(CanAccessObject(wrapper2.get()));
+ }
+
+ cx->minorGC(JS::GCReason::API);
+
+ return true;
+}
+
+template <typename W, typename T>
+bool TestHeapPostBarrierMoveAssignment() {
+ T initialObj = CreateNurseryGCThing<T>(cx);
+ CHECK(initialObj);
+ CHECK(IsInsideNursery(initialObj));
+ uintptr_t initialObjAsInt = UnbarrieredCastToInt(initialObj);
+
+ {
+ // We don't root our structure because that would end up tracing it on minor
+ // GC and we're testing that heap post barrier works for things that aren't
+ // roots.
+ JS::AutoSuppressGCAnalysis noAnalysis(cx);
+
+ W wrapper1(initialObj);
+ CHECK(wrapper1 == initialObj);
+
+ W wrapper2;
+ wrapper2 = std::move(wrapper1);
+ CHECK(wrapper2 == initialObj);
+
+ cx->minorGC(JS::GCReason::API);
+
+ CHECK(UnbarrieredCastToInt(wrapper1.get()) != initialObjAsInt);
+ CHECK(UnbarrieredCastToInt(wrapper2.get()) != initialObjAsInt);
+ CHECK(!IsInsideNursery(wrapper2.get()));
+ CHECK(CanAccessObject(wrapper2.get()));
+ }
+
+ cx->minorGC(JS::GCReason::API);
+
+ return true;
+}
+
+END_TEST(testGCHeapPostBarriers)
+
+// Test read barrier implementation on wrapper types. The following wrapper
+// types have read barriers:
+// - JS::Heap
+// - JS::TenuredHeap
+// - WeakHeapPtr
+//
+// Also check that equality comparisons on wrappers do not trigger the read
+// barrier.
+BEGIN_TEST(testGCHeapReadBarriers) {
+ AutoLeaveZeal nozeal(cx);
+
+ CHECK((TestWrapperType<JS::Heap<JSObject*>, JSObject*>()));
+ CHECK((TestWrapperType<JS::TenuredHeap<JSObject*>, JSObject*>()));
+ CHECK((TestWrapperType<WeakHeapPtr<JSObject*>, JSObject*>()));
+ CHECK((TestWrapperType<JS::Heap<JS::ArrayBuffer>, JS::ArrayBuffer>()));
+ CHECK((TestWrapperType<JS::Heap<JS::Uint8Array>, JS::Uint8Array>()));
+
+ // JS::Heap has an additional barrier on its move and copy constructors.
+ CHECK((TestConstructorBarrier<JS::Heap<JSObject*>, JSObject*>()));
+ CHECK((TestConstructorBarrier<JS::Heap<JS::ArrayBuffer>, JS::ArrayBuffer>()));
+ CHECK((TestConstructorBarrier<JS::Heap<JS::Uint8Array>, JS::Uint8Array>()));
+
+ return true;
+}
+
+template <typename WrapperT, typename ObjectT>
+bool TestWrapperType() {
+ // Check that the read barrier normally marks gray things black.
+ CHECK((TestReadBarrierUnmarksGray<WrapperT, ObjectT>()));
+
+ // Check that the read barrier marks gray and white things black during an
+ // incremental GC.
+ CHECK((TestReadBarrierMarksBlack<WrapperT, ObjectT>(true)));
+ CHECK((TestReadBarrierMarksBlack<WrapperT, ObjectT>(false)));
+
+ // Allocate test objects and make them gray. We will make sure they stay
+ // gray. (For most reads, the barrier will unmark gray.)
+ Rooted<ObjectT> obj1(cx, CreateTenuredGCThing<ObjectT>(cx));
+ Rooted<ObjectT> obj2(cx, CreateTenuredGCThing<ObjectT>(cx));
+ MakeGray(obj1);
+ MakeGray(obj2);
+
+ WrapperT wrapper1(obj1);
+ WrapperT wrapper2(obj2);
+ const ObjectT constobj1 = obj1;
+ const ObjectT constobj2 = obj2;
+ CHECK((TestUnbarrieredOperations<WrapperT, ObjectT>(obj1, obj2, wrapper1,
+ wrapper2)));
+ CHECK((TestUnbarrieredOperations<WrapperT, ObjectT>(constobj1, constobj2,
+ wrapper1, wrapper2)));
+
+ return true;
+}
+
+template <typename WrapperT, typename ObjectT>
+void Access(const WrapperT& wrapper) {
+ if constexpr (std::is_base_of_v<JS::ArrayBufferOrView, ObjectT>) {
+ (void)wrapper.asObject();
+ } else {
+ (void)*wrapper;
+ }
+}
+
+template <typename WrapperT, typename ObjectT>
+bool TestReadBarrierUnmarksGray() {
+ Rooted<ObjectT> obj(cx, CreateTenuredGCThing<ObjectT>(cx));
+ WrapperT wrapper(obj);
+
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ Access<WrapperT, ObjectT>(wrapper);
+
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ MakeGray(obj);
+ Access<WrapperT, ObjectT>(wrapper);
+
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+}
+
+// Execute thunk |f| between two slices of an incremental GC controlled by zeal
+// mode |mode|.
+template <typename F>
+bool CallDuringIncrementalGC(uint32_t mode, F&& f) {
+#ifndef JS_GC_ZEAL
+ fprintf(stderr, "This test requires building with --enable-gczeal\n");
+#else
+ AutoGCParameter incremental(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
+
+ const int64_t BudgetMS = 10000; // 10S should be long enough for anyone.
+
+ JS_SetGCZeal(cx, mode, 0);
+ JS::PrepareZoneForGC(cx, js::GetContextZone(cx));
+ js::SliceBudget budget{TimeBudget(BudgetMS)};
+ JS::StartIncrementalGC(cx, JS::GCOptions(), JS::GCReason::DEBUG_GC, budget);
+ CHECK(JS::IsIncrementalGCInProgress(cx));
+
+ CHECK(f());
+
+ JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC);
+#endif
+
+ return true;
+}
+
+template <typename WrapperT, typename ObjectT>
+bool TestReadBarrierMarksBlack(bool fromWhite) {
+ AutoLeaveZeal noZeal(cx);
+
+ // Create an object and hide it from the hazard analysis.
+ void* ptr = CreateHiddenTenuredGCThing<ObjectT>(cx);
+ CHECK(ptr);
+
+ CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool {
+ CHECK(JS::IsIncrementalBarrierNeeded(cx));
+
+ auto obj = RecoverHiddenGCThing<ObjectT>(ptr);
+
+ WrapperT wrapper(obj);
+
+ CHECK(GetColor(obj) == gc::CellColor::White);
+ if (!fromWhite) {
+ MakeGray(obj);
+ }
+
+ Access<WrapperT, ObjectT>(wrapper);
+
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+ });
+
+ return true;
+}
+
+template <typename WrapperT, typename ObjectT>
+bool TestConstructorBarrier() {
+ AutoLeaveZeal noZeal(cx);
+
+ // Create an object and hide it from the hazard analysis.
+ void* ptr = CreateHiddenTenuredGCThing<ObjectT>(cx);
+ CHECK(ptr);
+
+ CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool {
+ CHECK(JS::IsIncrementalBarrierNeeded(cx));
+
+ auto obj = RecoverHiddenGCThing<ObjectT>(ptr);
+ WrapperT wrapper(obj);
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ WrapperT copiedWrapper(wrapper);
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+ });
+
+ ptr = CreateHiddenTenuredGCThing<ObjectT>(cx);
+ CHECK(ptr);
+
+ CallDuringIncrementalGC(9 /* YieldBeforeSweeping */, [&]() -> bool {
+ CHECK(JS::IsIncrementalBarrierNeeded(cx));
+
+ auto obj = RecoverHiddenGCThing<ObjectT>(ptr);
+ WrapperT wrapper(obj);
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ WrapperT movedWrapper(std::move(wrapper));
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+ });
+
+ return true;
+}
+
+template <typename WrapperT, typename ObjectT>
+bool TestUnbarrieredOperations(ObjectT obj, ObjectT obj2, WrapperT& wrapper,
+ WrapperT& wrapper2) {
+ (void)bool(wrapper);
+ (void)bool(wrapper2);
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+
+ int x = 0;
+
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += obj == obj2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += obj == wrapper2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += wrapper == obj2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += wrapper == wrapper2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+
+ CHECK(x == 0);
+
+ x += obj != obj2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += obj != wrapper2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += wrapper != obj2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+ x += wrapper != wrapper2;
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ CHECK(GetColor(obj2) == gc::CellColor::Gray);
+
+ CHECK(x == 4);
+
+ return true;
+}
+
+END_TEST(testGCHeapReadBarriers)
+
+using ObjectVector = Vector<JSObject*, 0, SystemAllocPolicy>;
+
+// Test pre-barrier implementation on wrapper types. The following wrapper types
+// have a pre-barrier:
+// - GCPtr
+// - HeapPtr
+// - PreBarriered
+BEGIN_TEST(testGCHeapPreBarriers) {
+ AutoLeaveZeal nozeal(cx);
+
+ AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
+
+ // Create a bunch of objects. These are unrooted and will be used to test
+ // whether barriers have fired by checking whether they have been marked
+ // black.
+ size_t objectCount = 100; // Increase this if necessary when adding tests.
+ ObjectVector testObjects;
+ for (size_t i = 0; i < objectCount; i++) {
+ JSObject* obj = CreateTenuredGCThing<JSObject*>(cx);
+ CHECK(obj);
+ CHECK(testObjects.append(obj));
+ }
+
+ // Start an incremental GC so we can detect if we cause barriers to fire, as
+ // these will mark objects black.
+ JS::PrepareForFullGC(cx);
+ SliceBudget budget(WorkBudget(1));
+ gc::GCRuntime* gc = &cx->runtime()->gc;
+ gc->startDebugGC(JS::GCOptions::Normal, budget);
+ while (gc->state() != gc::State::Mark) {
+ gc->debugGCSlice(budget);
+ }
+ MOZ_ASSERT(cx->zone()->needsIncrementalBarrier());
+
+ TestWrapper<HeapPtr<JSObject*>>(testObjects);
+ TestWrapper<PreBarriered<JSObject*>>(testObjects);
+
+ // GCPtr is different because 1) it doesn't support move operations as it's
+ // supposed to be part of a GC thing and 2) it doesn't perform a pre-barrier
+ // in its destructor because these are only destroyed as part of a GC where
+ // the barrier is unnecessary.
+ TestGCPtr(testObjects);
+
+ gc::FinishGC(cx, JS::GCReason::API);
+
+ return true;
+}
+
+template <typename Wrapper>
+bool TestWrapper(ObjectVector& testObjects) {
+ CHECK(TestCopyConstruction<Wrapper>(testObjects.popCopy()));
+ CHECK(TestMoveConstruction<Wrapper>(testObjects.popCopy()));
+ CHECK(TestAssignment<Wrapper>(testObjects.popCopy(), testObjects.popCopy()));
+ CHECK(TestMoveAssignment<Wrapper>(testObjects.popCopy(),
+ testObjects.popCopy()));
+ return true;
+}
+
+template <typename Wrapper>
+bool TestCopyConstruction(JSObject* obj) {
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ {
+ Wrapper wrapper1(obj);
+ Wrapper wrapper2(wrapper1);
+ CHECK(wrapper1 == obj);
+ CHECK(wrapper2 == obj);
+ CHECK(GetColor(obj) == gc::CellColor::White);
+ }
+
+ // Check destructor performs pre-barrier.
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+}
+
+template <typename Wrapper>
+bool TestMoveConstruction(JSObject* obj) {
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ {
+ Wrapper wrapper1(obj);
+ MakeGray(obj); // Check that we allow move of gray GC thing.
+ Wrapper wrapper2(std::move(wrapper1));
+ CHECK(!wrapper1);
+ CHECK(wrapper2 == obj);
+ CHECK(GetColor(obj) == gc::CellColor::Gray);
+ }
+
+ // Check destructor performs pre-barrier.
+ CHECK(GetColor(obj) == gc::CellColor::Black);
+
+ return true;
+}
+
+template <typename Wrapper>
+bool TestAssignment(JSObject* obj1, JSObject* obj2) {
+ CHECK(GetColor(obj1) == gc::CellColor::White);
+ CHECK(GetColor(obj2) == gc::CellColor::White);
+
+ {
+ Wrapper wrapper1(obj1);
+ Wrapper wrapper2(obj2);
+
+ wrapper2 = wrapper1;
+
+ CHECK(wrapper1 == obj1);
+ CHECK(wrapper2 == obj1);
+ CHECK(GetColor(obj1) == gc::CellColor::White); // No barrier fired.
+ CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired.
+ }
+
+ // Check destructor performs pre-barrier.
+ CHECK(GetColor(obj1) == gc::CellColor::Black);
+
+ return true;
+}
+
+template <typename Wrapper>
+bool TestMoveAssignment(JSObject* obj1, JSObject* obj2) {
+ CHECK(GetColor(obj1) == gc::CellColor::White);
+ CHECK(GetColor(obj2) == gc::CellColor::White);
+
+ {
+ Wrapper wrapper1(obj1);
+ Wrapper wrapper2(obj2);
+
+ MakeGray(obj1); // Check we allow move of gray thing.
+ wrapper2 = std::move(wrapper1);
+
+ CHECK(!wrapper1);
+ CHECK(wrapper2 == obj1);
+ CHECK(GetColor(obj1) == gc::CellColor::Gray); // No barrier fired.
+ CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired.
+ }
+
+ // Check destructor performs pre-barrier.
+ CHECK(GetColor(obj1) == gc::CellColor::Black);
+
+ return true;
+}
+
+bool TestGCPtr(ObjectVector& testObjects) {
+ CHECK(TestGCPtrCopyConstruction(testObjects.popCopy()));
+ CHECK(TestGCPtrAssignment(testObjects.popCopy(), testObjects.popCopy()));
+ return true;
+}
+
+bool TestGCPtrCopyConstruction(JSObject* obj) {
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ {
+ // Let us destroy GCPtrs ourselves for testing purposes.
+ gc::AutoSetThreadIsFinalizing threadIsFinalizing;
+
+ GCPtr<JSObject*> wrapper1(obj);
+ GCPtr<JSObject*> wrapper2(wrapper1);
+ CHECK(wrapper1 == obj);
+ CHECK(wrapper2 == obj);
+ CHECK(GetColor(obj) == gc::CellColor::White);
+ }
+
+ // GCPtr doesn't perform pre-barrier in destructor.
+ CHECK(GetColor(obj) == gc::CellColor::White);
+
+ return true;
+}
+
+bool TestGCPtrAssignment(JSObject* obj1, JSObject* obj2) {
+ CHECK(GetColor(obj1) == gc::CellColor::White);
+ CHECK(GetColor(obj2) == gc::CellColor::White);
+
+ {
+ // Let us destroy GCPtrs ourselves for testing purposes.
+ gc::AutoSetThreadIsFinalizing threadIsFinalizing;
+
+ GCPtr<JSObject*> wrapper1(obj1);
+ GCPtr<JSObject*> wrapper2(obj2);
+
+ wrapper2 = wrapper1;
+
+ CHECK(wrapper1 == obj1);
+ CHECK(wrapper2 == obj1);
+ CHECK(GetColor(obj1) == gc::CellColor::White); // No barrier fired.
+ CHECK(GetColor(obj2) == gc::CellColor::Black); // Pre barrier fired.
+ }
+
+ // GCPtr doesn't perform pre-barrier in destructor.
+ CHECK(GetColor(obj1) == gc::CellColor::White);
+
+ return true;
+}
+
+END_TEST(testGCHeapPreBarriers)