testGCHooks.cpp (10288B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "mozilla/ScopeExit.h" 6 #include "mozilla/UniquePtr.h" 7 8 #include "jsapi-tests/tests.h" 9 10 static unsigned gSliceCallbackCount = 0; 11 static bool gSawAllSliceCallbacks; 12 static bool gSawAllGCCallbacks; 13 14 static void NonIncrementalGCSliceCallback(JSContext* cx, 15 JS::GCProgress progress, 16 const JS::GCDescription& desc) { 17 using namespace JS; 18 static GCProgress expect[] = {GC_CYCLE_BEGIN, GC_SLICE_BEGIN, GC_SLICE_END, 19 GC_CYCLE_END}; 20 21 MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); 22 MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount++]); 23 MOZ_RELEASE_ASSERT(desc.isZone_ == false); 24 MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); 25 MOZ_RELEASE_ASSERT(desc.reason_ == JS::GCReason::API); 26 if (progress == GC_CYCLE_END) { 27 mozilla::UniquePtr<char16_t> summary(desc.formatSummaryMessage(cx)); 28 mozilla::UniquePtr<char16_t> message(desc.formatSliceMessage(cx)); 29 } 30 } 31 32 BEGIN_TEST(testGCSliceCallback) { 33 gSliceCallbackCount = 0; 34 JS::SetGCSliceCallback(cx, NonIncrementalGCSliceCallback); 35 JS_GC(cx); 36 JS::SetGCSliceCallback(cx, nullptr); 37 CHECK(gSliceCallbackCount == 4); 38 return true; 39 } 40 END_TEST(testGCSliceCallback) 41 42 static void RootsRemovedGCSliceCallback(JSContext* cx, JS::GCProgress progress, 43 const JS::GCDescription& desc) { 44 using namespace JS; 45 46 static constexpr struct { 47 GCProgress progress; 48 GCReason reason; 49 } expect[] = { 50 // Explicitly requested a full GC. 51 {GC_CYCLE_BEGIN, GCReason::DEBUG_GC}, 52 {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, 53 {GC_SLICE_END, GCReason::DEBUG_GC}, 54 {GC_SLICE_BEGIN, GCReason::DEBUG_GC}, 55 {GC_SLICE_END, GCReason::DEBUG_GC}, 56 {GC_CYCLE_END, GCReason::DEBUG_GC}, 57 // Shutdown GC with ROOTS_REMOVED. 58 {GC_CYCLE_BEGIN, GCReason::ROOTS_REMOVED}, 59 {GC_SLICE_BEGIN, GCReason::ROOTS_REMOVED}, 60 {GC_SLICE_END, GCReason::ROOTS_REMOVED}, 61 {GC_CYCLE_END, GCReason::ROOTS_REMOVED} 62 // All done. 63 }; 64 65 MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expect)); 66 MOZ_RELEASE_ASSERT(progress == expect[gSliceCallbackCount].progress); 67 MOZ_RELEASE_ASSERT(desc.isZone_ == false); 68 MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Shrink); 69 MOZ_RELEASE_ASSERT(desc.reason_ == expect[gSliceCallbackCount].reason); 70 gSliceCallbackCount++; 71 } 72 73 BEGIN_TEST(testGCRootsRemoved) { 74 AutoLeaveZeal nozeal(cx); 75 76 AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); 77 78 gSliceCallbackCount = 0; 79 JS::SetGCSliceCallback(cx, RootsRemovedGCSliceCallback); 80 auto byebye = mozilla::MakeScopeExit( 81 [=, this] { JS::SetGCSliceCallback(cx, nullptr); }); 82 83 JS::RootedObject obj(cx, JS_NewPlainObject(cx)); 84 CHECK(obj); 85 86 JS::PrepareForFullGC(cx); 87 JS::SliceBudget budget(JS::WorkBudget(1)); 88 cx->runtime()->gc.startDebugGC(JS::GCOptions::Shrink, budget); 89 CHECK(JS::IsIncrementalGCInProgress(cx)); 90 91 // Trigger another GC after the current one in shrinking / shutdown GCs. 92 cx->runtime()->gc.notifyRootsRemoved(); 93 94 JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); 95 CHECK(!JS::IsIncrementalGCInProgress(cx)); 96 97 return true; 98 } 99 END_TEST(testGCRootsRemoved) 100 101 #define ASSERT_MSG(cond, ...) \ 102 do { \ 103 if (!(cond)) { \ 104 fprintf(stderr, __VA_ARGS__); \ 105 MOZ_RELEASE_ASSERT(cond); \ 106 } \ 107 } while (false) 108 109 // Trigger some nested GCs to ensure that they get their own reasons and 110 // fullGCRequested state. 111 // 112 // The callbacks will form the following tree: 113 // 114 // Begin(DEBUG_GC) 115 // Begin(API) 116 // End(API) 117 // End(DEBUG_GC) 118 // Begin(MEM_PRESSURE) 119 // End(MEM_PRESSURE) 120 // Begin(DOM_WINDOW_UTILS) 121 // End(DOM_WINDOW_UTILS) 122 // 123 // JSGC_BEGIN and JSGC_END callbacks will be observed as a preorder traversal 124 // of the above tree. 125 // 126 // Note that the ordering of the *slice* callbacks don't match up simply to the 127 // ordering above. If a JSGC_BEGIN triggers another GC, we won't see the outer 128 // GC's GC_CYCLE_BEGIN until the inner one is done. The slice callbacks are 129 // reporting the actual order that the GCs are happening in. 130 // 131 // JSGC_END, on the other hand, won't be emitted until the GC is complete and 132 // the GC_CYCLE_BEGIN callback has fired. So its ordering is straightforward. 133 // 134 static void GCTreeCallback(JSContext* cx, JSGCStatus status, 135 JS::GCReason reason, void* data) { 136 using namespace JS; 137 138 static constexpr struct { 139 JSGCStatus expectedStatus; 140 JS::GCReason expectedReason; 141 bool fireGC; 142 JS::GCReason reason; 143 bool requestFullGC; 144 } invocations[] = { 145 {JSGC_BEGIN, GCReason::DEBUG_GC, true, GCReason::API, false}, 146 {JSGC_BEGIN, GCReason::API, false}, 147 {JSGC_END, GCReason::API, false}, 148 {JSGC_END, GCReason::DEBUG_GC, true, GCReason::MEM_PRESSURE, true}, 149 {JSGC_BEGIN, GCReason::MEM_PRESSURE, false}, 150 {JSGC_END, GCReason::MEM_PRESSURE, true, GCReason::DOM_WINDOW_UTILS, 151 false}, 152 {JSGC_BEGIN, GCReason::DOM_WINDOW_UTILS, false}, 153 {JSGC_END, GCReason::DOM_WINDOW_UTILS, false}}; 154 155 static size_t i = 0; 156 MOZ_RELEASE_ASSERT(i < std::size(invocations)); 157 auto& invocation = invocations[i++]; 158 if (i == std::size(invocations)) { 159 gSawAllGCCallbacks = true; 160 } 161 ASSERT_MSG(status == invocation.expectedStatus, 162 "GC callback #%zu: got status %d expected %d\n", i, status, 163 invocation.expectedStatus); 164 ASSERT_MSG(reason == invocation.expectedReason, 165 "GC callback #%zu: got reason %s expected %s\n", i, 166 ExplainGCReason(reason), 167 ExplainGCReason(invocation.expectedReason)); 168 if (invocation.fireGC) { 169 if (invocation.requestFullGC) { 170 JS::PrepareForFullGC(cx); 171 } 172 JS::SliceBudget budget = JS::SliceBudget(JS::WorkBudget(1)); 173 cx->runtime()->gc.startGC(GCOptions::Normal, invocation.reason, budget); 174 MOZ_RELEASE_ASSERT(JS::IsIncrementalGCInProgress(cx)); 175 176 JS::FinishIncrementalGC(cx, invocation.reason); 177 MOZ_RELEASE_ASSERT(!JS::IsIncrementalGCInProgress(cx)); 178 } 179 } 180 181 static void GCTreeSliceCallback(JSContext* cx, JS::GCProgress progress, 182 const JS::GCDescription& desc) { 183 using namespace JS; 184 185 static constexpr struct { 186 GCProgress progress; 187 GCReason reason; 188 bool isZonal; 189 } expectations[] = { 190 // JSGC_BEGIN triggers a new GC before we get any slice callbacks from the 191 // original outer GC. So the very first reason observed is API, not 192 // DEBUG_GC. 193 {GC_CYCLE_BEGIN, GCReason::API, true}, 194 {GC_SLICE_BEGIN, GCReason::API, true}, 195 {GC_SLICE_END, GCReason::API, true}, 196 {GC_SLICE_BEGIN, GCReason::API, true}, 197 {GC_SLICE_END, GCReason::API, true}, 198 {GC_CYCLE_END, GCReason::API, true}, 199 // Now the "outer" GC runs. It requested a full GC. 200 {GC_CYCLE_BEGIN, GCReason::DEBUG_GC, false}, 201 {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, 202 {GC_SLICE_END, GCReason::DEBUG_GC, false}, 203 {GC_SLICE_BEGIN, GCReason::DEBUG_GC, false}, 204 {GC_SLICE_END, GCReason::DEBUG_GC, false}, 205 {GC_CYCLE_END, GCReason::DEBUG_GC, false}, 206 // The outer JSGC_DEBUG GC's end callback triggers a full MEM_PRESSURE 207 // GC, which runs next. (Its JSGC_BEGIN does not run a GC.) 208 {GC_CYCLE_BEGIN, GCReason::MEM_PRESSURE, false}, 209 {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, 210 {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, 211 {GC_SLICE_BEGIN, GCReason::MEM_PRESSURE, false}, 212 {GC_SLICE_END, GCReason::MEM_PRESSURE, false}, 213 {GC_CYCLE_END, GCReason::MEM_PRESSURE, false}, 214 // The MEM_PRESSURE's GC's end callback now triggers a (zonal) 215 // DOM_WINDOW_UTILS GC. 216 {GC_CYCLE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, 217 {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, 218 {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, 219 {GC_SLICE_BEGIN, GCReason::DOM_WINDOW_UTILS, true}, 220 {GC_SLICE_END, GCReason::DOM_WINDOW_UTILS, true}, 221 {GC_CYCLE_END, GCReason::DOM_WINDOW_UTILS, true}, 222 // All done. 223 }; 224 225 MOZ_RELEASE_ASSERT(gSliceCallbackCount < std::size(expectations)); 226 auto& expect = expectations[gSliceCallbackCount]; 227 ASSERT_MSG(progress == expect.progress, "iteration %d: wrong progress\n", 228 gSliceCallbackCount); 229 ASSERT_MSG(desc.reason_ == expect.reason, 230 "iteration %d: expected %s got %s\n", gSliceCallbackCount, 231 JS::ExplainGCReason(expect.reason), 232 JS::ExplainGCReason(desc.reason_)); 233 ASSERT_MSG(desc.isZone_ == expect.isZonal, "iteration %d: wrong zonal\n", 234 gSliceCallbackCount); 235 MOZ_RELEASE_ASSERT(desc.options_ == JS::GCOptions::Normal); 236 gSliceCallbackCount++; 237 if (gSliceCallbackCount == std::size(expectations)) { 238 gSawAllSliceCallbacks = true; 239 } 240 } 241 242 BEGIN_TEST(testGCTree) { 243 AutoLeaveZeal nozeal(cx); 244 245 AutoGCParameter param1(cx, JSGC_INCREMENTAL_GC_ENABLED, true); 246 247 gSliceCallbackCount = 0; 248 gSawAllSliceCallbacks = false; 249 gSawAllGCCallbacks = false; 250 JS::SetGCSliceCallback(cx, GCTreeSliceCallback); 251 JS_SetGCCallback(cx, GCTreeCallback, nullptr); 252 253 // Automate the callback clearing. Otherwise if a CHECK fails, it will get 254 // cluttered with additional failures from the callback unexpectedly firing 255 // during the final shutdown GC. 256 auto byebye = mozilla::MakeScopeExit([=, this] { 257 JS::SetGCSliceCallback(cx, nullptr); 258 JS_SetGCCallback(cx, nullptr, nullptr); 259 }); 260 261 JS::RootedObject obj(cx, JS_NewPlainObject(cx)); 262 CHECK(obj); 263 264 // Outer GC is a full GC. 265 JS::PrepareForFullGC(cx); 266 JS::SliceBudget budget(JS::WorkBudget(1)); 267 cx->runtime()->gc.startDebugGC(JS::GCOptions::Normal, budget); 268 CHECK(JS::IsIncrementalGCInProgress(cx)); 269 270 JS::FinishIncrementalGC(cx, JS::GCReason::DEBUG_GC); 271 CHECK(!JS::IsIncrementalGCInProgress(cx)); 272 CHECK(gSawAllSliceCallbacks); 273 CHECK(gSawAllGCCallbacks); 274 275 return true; 276 } 277 END_TEST(testGCTree)