diff options
Diffstat (limited to 'js/src/jsapi-tests/testGCHooks.cpp')
-rw-r--r-- | js/src/jsapi-tests/testGCHooks.cpp | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testGCHooks.cpp b/js/src/jsapi-tests/testGCHooks.cpp new file mode 100644 index 0000000000..7c6a224995 --- /dev/null +++ b/js/src/jsapi-tests/testGCHooks.cpp @@ -0,0 +1,279 @@ +/* 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/ScopeExit.h" +#include "mozilla/UniquePtr.h" + +#include <iterator> + +#include "jsapi-tests/tests.h" + +static unsigned gSliceCallbackCount = 0; +static bool gSawAllSliceCallbacks; +static bool gSawAllGCCallbacks; + +static void NonIncrementalGCSliceCallback(JSContext* cx, + JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + static GCProgress expect[] = {GC_CYCLE_BEGIN, GC_SLICE_BEGIN, GC_SLICE_END, + GC_CYCLE_END}; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); + MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount++]); + MOZ_RELEASE_ASSERT(desc.isZone_ == false); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); + MOZ_RELEASE_ASSERT(desc.reason_ == JS::GCReason::API); + if (progress == GC_CYCLE_END) { + mozilla::UniquePtr<char16_t> summary(desc.formatSummaryMessage(cx)); + mozilla::UniquePtr<char16_t> message(desc.formatSliceMessage(cx)); + } +} + +BEGIN_TEST(testGCSliceCallback) { + gSliceCallbackCount = 0; + JS::SetGCSliceCallback(cx, NonIncrementalGCSliceCallback); + JS_GC(cx); + JS::SetGCSliceCallback(cx, nullptr); + CHECK(gSliceCallbackCount == 4); + return true; +} +END_TEST(testGCSliceCallback) + +static void RootsRemovedGCSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + + static constexpr struct { + GCProgress progress; + GCReason reason; + } expect[] = { + // Explicitly requested a full GC. + {GC_CYCLE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_END, GCReason::DEBUG_GC}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, + {GC_SLICE_END, GCReason::DEBUG_GC}, + {GC_CYCLE_END, GCReason::DEBUG_GC}, + // Shutdown GC with ROOTS_REMOVED. + {GC_CYCLE_BEGIN, GCReason::ROOTS_REMOVED}, + {GC_SLICE_BEGIN, GCReason::ROOTS_REMOVED}, + {GC_SLICE_END, GCReason::ROOTS_REMOVED}, + {GC_CYCLE_END, GCReason::ROOTS_REMOVED} + // All done. + }; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); + MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount].progress); + MOZ_RELEASE_ASSERT(desc.isZone_ == false); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Shrink); + MOZ_RELEASE_ASSERT(desc.reason_ == expect[gSliceCallbackCount].reason); + gSliceCallbackCount++; +} + +BEGIN_TEST(testGCRootsRemoved) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + gSliceCallbackCount = 0; + JS::SetGCSliceCallback(cx, RootsRemovedGCSliceCallback); + auto byebye = + mozilla::MakeScopeExit([=] { JS::SetGCSliceCallback(cx, nullptr); }); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + JS::PrepareForFullGC(cx); + js::SliceBudget budget(js::WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Shrink, budget); + CHECK(JS::IsIncrementalGCInProgress(cx)); + + // Trigger another GC after the current one in shrinking / shutdown GCs. + cx->runtime()->gc.notifyRootsRemoved(); + + JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); + CHECK(!JS::IsIncrementalGCInProgress(cx)); + + return true; +} +END_TEST(testGCRootsRemoved) + +#define ASSERT_MSG(cond, ...) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, __VA_ARGS__); \ + MOZ_RELEASE_ASSERT(cond); \ + } \ + } while (false) + +// Trigger some nested GCs to ensure that they get their own reasons and +// fullGCRequested state. +// +// The callbacks will form the following tree: +// +// Begin(DEBUG_GC) +// Begin(API) +// End(API) +// End(DEBUG_GC) +// Begin(MEM_PRESSURE) +// End(MEM_PRESSURE) +// Begin(DOM_WINDOW_UTILS) +// End(DOM_WINDOW_UTILS) +// +// JSGC_BEGIN and JSGC_END callbacks will be observed as a preorder traversal +// of the above tree. +// +// Note that the ordering of the *slice* callbacks don't match up simply to the +// ordering above. If a JSGC_BEGIN triggers another GC, we won't see the outer +// GC's GC_CYCLE_BEGIN until the inner one is done. The slice callbacks are +// reporting the actual order that the GCs are happening in. +// +// JSGC_END, on the other hand, won't be emitted until the GC is complete and +// the GC_CYCLE_BEGIN callback has fired. So its ordering is straightforward. +// +static void GCTreeCallback(JSContext* cx, JSGCStatus status, + JS::GCReason reason, void* data) { + using namespace JS; + + static constexpr struct { + JSGCStatus expectedStatus; + JS::GCReason expectedReason; + bool fireGC; + JS::GCReason reason; + bool requestFullGC; + } invocations[] = { + {JSGC_BEGIN, GCReason::DEBUG_GC, true, GCReason::API, false}, + {JSGC_BEGIN, GCReason::API, false}, + {JSGC_END, GCReason::API, false}, + {JSGC_END, GCReason::DEBUG_GC, true, GCReason::MEM_PRESSURE, true}, + {JSGC_BEGIN, GCReason::MEM_PRESSURE, false}, + {JSGC_END, GCReason::MEM_PRESSURE, true, GCReason::DOM_WINDOW_UTILS, + false}, + {JSGC_BEGIN, GCReason::DOM_WINDOW_UTILS, false}, + {JSGC_END, GCReason::DOM_WINDOW_UTILS, false}}; + + static size_t i = 0; + MOZ_RELEASE_ASSERT(i < std::size(invocations)); + auto& invocation = invocations[i++]; + if (i == std::size(invocations)) { + gSawAllGCCallbacks = true; + } + ASSERT_MSG(status == invocation.expectedStatus, + "GC callback #%zu: got status %d expected %d\n", i, status, + invocation.expectedStatus); + ASSERT_MSG(reason == invocation.expectedReason, + "GC callback #%zu: got reason %s expected %s\n", i, + ExplainGCReason(reason), + ExplainGCReason(invocation.expectedReason)); + if (invocation.fireGC) { + if (invocation.requestFullGC) { + JS::PrepareForFullGC(cx); + } + js::SliceBudget budget = js::SliceBudget(js::WorkBudget(1)); + cx->runtime()->gc.startGC(GCOptions::Normal, invocation.reason, budget); + MOZ_RELEASE_ASSERT(JS::IsIncrementalGCInProgress(cx)); + + JS::FinishIncrementalGC(cx, invocation.reason); + MOZ_RELEASE_ASSERT(!JS::IsIncrementalGCInProgress(cx)); + } +} + +static void GCTreeSliceCallback(JSContext* cx, JS::GCProgress progress, + const JS::GCDescription& desc) { + using namespace JS; + + static constexpr struct { + GCProgress progress; + GCReason reason; + bool isZonal; + } expectations[] = { + // JSGC_BEGIN triggers a new GC before we get any slice callbacks from the + // original outer GC. So the very first reason observed is API, not + // DEBUG_GC. + {GC_CYCLE_BEGIN, GCReason::API, true}, + {GC_SLICE_BEGIN, GCReason::API, true}, + {GC_SLICE_END, GCReason::API, true}, + {GC_SLICE_BEGIN, GCReason::API, true}, + {GC_SLICE_END, GCReason::API, true}, + {GC_CYCLE_END, GCReason::API, true}, + // Now the "outer" GC runs. It requested a full GC. + {GC_CYCLE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_END, GCReason::DEBUG_GC, false}, + {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, + {GC_SLICE_END, GCReason::DEBUG_GC, false}, + {GC_CYCLE_END, GCReason::DEBUG_GC, false}, + // The outer JSGC_DEBUG GC's end callback triggers a full MEM_PRESSURE + // GC, which runs next. (Its JSGC_BEGIN does not run a GC.) + {GC_CYCLE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, + {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, + {GC_CYCLE_END, GCReason::MEM_PRESSURE, false}, + // The MEM_PRESSURE's GC's end callback now triggers a (zonal) + // DOM_WINDOW_UTILS GC. + {GC_CYCLE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, + {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, + {GC_CYCLE_END, GCReason::DOM_WINDOW_UTILS, true}, + // All done. + }; + + MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expectations)); + auto& expect = expectations[gSliceCallbackCount]; + ASSERT_MSG(progress == expect.progress, "iteration %d: wrong progress\n", + gSliceCallbackCount); + ASSERT_MSG(desc.reason_ == expect.reason, + "iteration %d: expected %s got %s\n", gSliceCallbackCount, + JS::ExplainGCReason(expect.reason), + JS::ExplainGCReason(desc.reason_)); + ASSERT_MSG(desc.isZone_ == expect.isZonal, "iteration %d: wrong zonal\n", + gSliceCallbackCount); + MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); + gSliceCallbackCount++; + if (gSliceCallbackCount == std::size(expectations)) { + gSawAllSliceCallbacks = true; + } +} + +BEGIN_TEST(testGCTree) { + AutoLeaveZeal nozeal(cx); + + AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); + + gSliceCallbackCount = 0; + gSawAllSliceCallbacks = false; + gSawAllGCCallbacks = false; + JS::SetGCSliceCallback(cx, GCTreeSliceCallback); + JS_SetGCCallback(cx, GCTreeCallback, nullptr); + + // Automate the callback clearing. Otherwise if a CHECK fails, it will get + // cluttered with additional failures from the callback unexpectedly firing + // during the final shutdown GC. + auto byebye = mozilla::MakeScopeExit([=] { + JS::SetGCSliceCallback(cx, nullptr); + JS_SetGCCallback(cx, nullptr, nullptr); + }); + + JS::RootedObject obj(cx, JS_NewPlainObject(cx)); + CHECK(obj); + + // Outer GC is a full GC. + JS::PrepareForFullGC(cx); + js::SliceBudget budget(js::WorkBudget(1)); + cx->runtime()->gc.startDebugGC(JS::GCOptions::Normal, budget); + CHECK(JS::IsIncrementalGCInProgress(cx)); + + JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); + CHECK(!JS::IsIncrementalGCInProgress(cx)); + CHECK(gSawAllSliceCallbacks); + CHECK(gSawAllGCCallbacks); + + return true; +} +END_TEST(testGCTree) |