/src/hermes/lib/VM/gcs/HadesGC.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) Meta Platforms, Inc. and affiliates. |
3 | | * |
4 | | * This source code is licensed under the MIT license found in the |
5 | | * LICENSE file in the root directory of this source tree. |
6 | | */ |
7 | | |
8 | | #include "hermes/VM/HadesGC.h" |
9 | | |
10 | | #include "hermes/Support/Compiler.h" |
11 | | #include "hermes/Support/ErrorHandling.h" |
12 | | #include "hermes/VM/AllocResult.h" |
13 | | #include "hermes/VM/CheckHeapWellFormedAcceptor.h" |
14 | | #include "hermes/VM/FillerCell.h" |
15 | | #include "hermes/VM/GCBase-inline.h" |
16 | | #include "hermes/VM/GCPointer.h" |
17 | | #include "hermes/VM/HermesValue-inline.h" |
18 | | #include "hermes/VM/RootAndSlotAcceptorDefault.h" |
19 | | #include "hermes/VM/SmallHermesValue-inline.h" |
20 | | |
21 | | #include <array> |
22 | | #include <functional> |
23 | | #include <stack> |
24 | | |
25 | | #pragma GCC diagnostic push |
26 | | |
27 | | #ifdef HERMES_COMPILER_SUPPORTS_WSHORTEN_64_TO_32 |
28 | | #pragma GCC diagnostic ignored "-Wshorten-64-to-32" |
29 | | #endif |
30 | | |
31 | | namespace hermes { |
32 | | namespace vm { |
33 | | |
34 | | static const char *kGCName = |
35 | | kConcurrentGC ? "hades (concurrent)" : "hades (incremental)"; |
36 | | |
37 | | static const char *kCompacteeNameForCrashMgr = "COMPACT"; |
38 | | |
39 | | // We have a target max pause time of 50ms. |
40 | | static constexpr size_t kTargetMaxPauseMs = 50; |
41 | | |
42 | | // Assert that it is always safe to construct a cell that is as large as the |
43 | | // entire segment. This lets us always assume that contiguous regions in a |
44 | | // segment can be safely turned into a single FreelistCell. |
45 | | static_assert(HadesGC::HeapSegment::maxSize() <= HadesGC::maxAllocationSize()); |
46 | | |
47 | | // A free list cell is always variable-sized. |
48 | | const VTable HadesGC::OldGen::FreelistCell::vt{ |
49 | | CellKind::FreelistKind, |
50 | | /*variableSize*/ 0}; |
51 | | |
52 | 1 | void FreelistBuildMeta(const GCCell *cell, Metadata::Builder &mb) { |
53 | 1 | mb.setVTable(&HadesGC::OldGen::FreelistCell::vt); |
54 | 1 | } |
55 | | |
56 | 240k | GCCell *HadesGC::OldGen::finishAlloc(GCCell *cell, uint32_t sz) { |
57 | | // Track the number of allocated bytes in a segment. |
58 | 240k | incrementAllocatedBytes(sz); |
59 | | // Write a mark bit so this entry doesn't get free'd by the sweeper. |
60 | 240k | HeapSegment::setCellMarkBit(cell); |
61 | | // Could overwrite the VTable, but the allocator will write a new one in |
62 | | // anyway. |
63 | 240k | return cell; |
64 | 240k | } |
65 | | |
66 | 110 | void HadesGC::OldGen::SegmentBucket::addToFreelist(SegmentBucket *dummyHead) { |
67 | 110 | auto *oldHead = dummyHead->next; |
68 | 110 | if (oldHead) |
69 | 0 | oldHead->prev = this; |
70 | 110 | prev = dummyHead; |
71 | 110 | next = oldHead; |
72 | 110 | dummyHead->next = this; |
73 | 110 | } |
74 | | |
75 | 6 | void HadesGC::OldGen::SegmentBucket::removeFromFreelist() const { |
76 | 6 | if (next) |
77 | 0 | next->prev = prev; |
78 | 6 | prev->next = next; |
79 | 6 | } |
80 | | |
81 | | void HadesGC::OldGen::addCellToFreelist( |
82 | | void *addr, |
83 | | uint32_t sz, |
84 | 94 | SegmentBucket *segBucket) { |
85 | 94 | assert( |
86 | 94 | sz >= sizeof(FreelistCell) && |
87 | 94 | "Cannot construct a FreelistCell into an allocation in the OG"); |
88 | 94 | FreelistCell *newFreeCell = constructCell<FreelistCell>(addr, sz); |
89 | 94 | HeapSegment::setCellHead(static_cast<GCCell *>(addr), sz); |
90 | 94 | addCellToFreelist(newFreeCell, segBucket); |
91 | 94 | } |
92 | | |
93 | | void HadesGC::OldGen::addCellToFreelist( |
94 | | FreelistCell *cell, |
95 | 99 | SegmentBucket *segBucket) { |
96 | 99 | const size_t sz = cell->getAllocatedSize(); |
97 | | // Push onto the size-specific free list for this bucket and segment. |
98 | 99 | CompressedPointer oldHead = segBucket->head; |
99 | 99 | cell->next_ = oldHead; |
100 | 99 | segBucket->head = |
101 | 99 | CompressedPointer::encodeNonNull(cell, gc_.getPointerBase()); |
102 | | |
103 | | // If this SegmentBucket was not already in the freelist, add it. |
104 | 99 | if (!oldHead) { |
105 | 98 | uint32_t bucket = getFreelistBucket(sz); |
106 | 98 | auto *dummyHead = &buckets_[bucket]; |
107 | 98 | segBucket->addToFreelist(dummyHead); |
108 | | |
109 | | // Set a bit indicating that there are now available blocks for this bucket. |
110 | 98 | freelistBucketBitArray_.set(bucket, true); |
111 | 98 | } |
112 | | |
113 | | // In ASAN builds, poison the memory outside of the FreelistCell so that |
114 | | // accesses are flagged as illegal while it is in the freelist. |
115 | | // Here, and in other places where FreelistCells are poisoned, use +1 on the |
116 | | // pointer to skip towards the memory region directly after the FreelistCell |
117 | | // header of a cell. This way the header is always intact and readable, and |
118 | | // only the contents of the cell are poisoned. |
119 | 99 | __asan_poison_memory_region(cell + 1, sz - sizeof(FreelistCell)); |
120 | 99 | } |
121 | | |
122 | | void HadesGC::OldGen::addCellToFreelistFromSweep( |
123 | | char *freeRangeStart, |
124 | | char *freeRangeEnd, |
125 | | SegmentBuckets &segBuckets, |
126 | 71 | bool setHead) { |
127 | 71 | assert( |
128 | 71 | gc_.concurrentPhase_ == Phase::Sweep && |
129 | 71 | "addCellToFreelistFromSweep should only be called during sweeping."); |
130 | 71 | size_t newCellSize = freeRangeEnd - freeRangeStart; |
131 | | // While coalescing, sweeping may generate new cells, so make sure the cell |
132 | | // head is updated. |
133 | 71 | if (setHead) |
134 | 4 | HeapSegment::setCellHead( |
135 | 4 | reinterpret_cast<GCCell *>(freeRangeStart), newCellSize); |
136 | 71 | auto *newCell = constructCell<FreelistCell>(freeRangeStart, newCellSize); |
137 | | // Get the size bucket for the cell being added; |
138 | 71 | const uint32_t bucket = getFreelistBucket(newCellSize); |
139 | | // Push onto the size-specific free list for this bucket and segment. |
140 | 71 | auto *segBucket = &segBuckets[bucket]; |
141 | 71 | newCell->next_ = segBucket->head; |
142 | 71 | segBucket->head = |
143 | 71 | CompressedPointer::encodeNonNull(newCell, gc_.getPointerBase()); |
144 | 71 | __asan_poison_memory_region(newCell + 1, newCellSize - sizeof(FreelistCell)); |
145 | 71 | } |
146 | | |
147 | | HadesGC::OldGen::FreelistCell *HadesGC::OldGen::removeCellFromFreelist( |
148 | | size_t bucket, |
149 | 3 | SegmentBucket *segBucket) { |
150 | 3 | return removeCellFromFreelist(&segBucket->head, bucket, segBucket); |
151 | 3 | } |
152 | | |
153 | | HadesGC::OldGen::FreelistCell *HadesGC::OldGen::removeCellFromFreelist( |
154 | | AssignableCompressedPointer *prevLoc, |
155 | | size_t bucket, |
156 | 8 | SegmentBucket *segBucket) { |
157 | 8 | FreelistCell *cell = |
158 | 8 | vmcast<FreelistCell>(prevLoc->getNonNull(gc_.getPointerBase())); |
159 | 8 | assert(cell && "Cannot get a null cell from freelist"); |
160 | | |
161 | | // Update whatever was pointing to the cell we are removing. |
162 | 8 | *prevLoc = cell->next_; |
163 | | // Update the bit arrays if the given freelist is now empty. |
164 | 8 | if (!segBucket->head) { |
165 | 5 | segBucket->removeFromFreelist(); |
166 | | |
167 | | // If setting the bit to 0 above made this bucket empty for all segments, |
168 | | // set the bucket bit to 0 as well. |
169 | 5 | freelistBucketBitArray_.set(bucket, buckets_[bucket].next); |
170 | 5 | } |
171 | | |
172 | | // Unpoison the memory so that the mutator can use it. |
173 | 8 | __asan_unpoison_memory_region( |
174 | 8 | cell + 1, cell->getAllocatedSize() - sizeof(FreelistCell)); |
175 | 8 | return cell; |
176 | 8 | } |
177 | | |
178 | | /* static */ |
179 | | void HadesGC::HeapSegment::setCellHead( |
180 | | const GCCell *cellStart, |
181 | 240k | const size_t sz) { |
182 | 240k | const char *start = reinterpret_cast<const char *>(cellStart); |
183 | 240k | const char *end = start + sz; |
184 | 240k | CardTable *cards = cardTableCovering(start); |
185 | 240k | auto boundary = cards->nextBoundary(start); |
186 | | // If this object crosses a card boundary, then update boundaries |
187 | | // appropriately. |
188 | 240k | if (boundary.address() < end) { |
189 | 17.4k | cards->updateBoundaries(&boundary, start, end); |
190 | 17.4k | } |
191 | 240k | } |
192 | | |
193 | 56 | GCCell *HadesGC::HeapSegment::getFirstCellHead(size_t cardIdx) { |
194 | 56 | CardTable &cards = cardTable(); |
195 | 56 | GCCell *cell = cards.firstObjForCard(cardIdx); |
196 | 56 | assert(cell->isValid() && "Object head doesn't point to a valid object"); |
197 | 56 | return cell; |
198 | 56 | } |
199 | | |
200 | | template <typename CallbackFunction> |
201 | 360 | void HadesGC::HeapSegment::forAllObjs(CallbackFunction callback) { |
202 | 2.91M | for (GCCell *cell : cells()) { |
203 | | // Skip free-list entries. |
204 | 2.91M | if (!vmisa<OldGen::FreelistCell>(cell)) { |
205 | 2.91M | callback(cell); |
206 | 2.91M | } |
207 | 2.91M | } |
208 | 360 | } HadesGC.cpp:void hermes::vm::HadesGC::HeapSegment::forAllObjs<hermes::vm::HadesGC::finalizeAll()::$_0>(hermes::vm::HadesGC::finalizeAll()::$_0) Line | Count | Source | 201 | 94 | void HadesGC::HeapSegment::forAllObjs(CallbackFunction callback) { | 202 | 240k | for (GCCell *cell : cells()) { | 203 | | // Skip free-list entries. | 204 | 240k | if (!vmisa<OldGen::FreelistCell>(cell)) { | 205 | 240k | callback(cell); | 206 | 240k | } | 207 | 240k | } | 208 | 94 | } |
void hermes::vm::HadesGC::HeapSegment::forAllObjs<std::__1::function<void (hermes::vm::GCCell*)> >(std::__1::function<void (hermes::vm::GCCell*)>) Line | Count | Source | 201 | 266 | void HadesGC::HeapSegment::forAllObjs(CallbackFunction callback) { | 202 | 2.67M | for (GCCell *cell : cells()) { | 203 | | // Skip free-list entries. | 204 | 2.67M | if (!vmisa<OldGen::FreelistCell>(cell)) { | 205 | 2.67M | callback(cell); | 206 | 2.67M | } | 207 | 2.67M | } | 208 | 266 | } |
Unexecuted instantiation: HadesGC.cpp:void hermes::vm::HadesGC::HeapSegment::forAllObjs<hermes::vm::HadesGC::forAllObjs(std::__1::function<void (hermes::vm::GCCell*)> const&)::$_0>(hermes::vm::HadesGC::forAllObjs(std::__1::function<void (hermes::vm::GCCell*)> const&)::$_0) Unexecuted instantiation: HadesGC.cpp:void hermes::vm::HadesGC::HeapSegment::forAllObjs<hermes::vm::HadesGC::promoteYoungGenToOldGen()::$_0>(hermes::vm::HadesGC::promoteYoungGenToOldGen()::$_0) |
209 | | |
210 | | template <typename CallbackFunction> |
211 | | void HadesGC::HeapSegment::forCompactedObjs( |
212 | | CallbackFunction callback, |
213 | 0 | PointerBase &base) { |
214 | 0 | void *const stop = level(); |
215 | 0 | GCCell *cell = reinterpret_cast<GCCell *>(start()); |
216 | 0 | while (cell < stop) { |
217 | 0 | if (cell->hasMarkedForwardingPointer()) { |
218 | | // This cell has been evacuated, do nothing. |
219 | 0 | cell = reinterpret_cast<GCCell *>( |
220 | 0 | reinterpret_cast<char *>(cell) + |
221 | 0 | cell->getMarkedForwardingPointer() |
222 | 0 | .getNonNull(base) |
223 | 0 | ->getAllocatedSize()); |
224 | 0 | } else { |
225 | | // This cell is being compacted away, call the callback on it. |
226 | | // NOTE: We do not check if it is a FreelistCell here in order to avoid |
227 | | // the extra overhead of that check in YG. The callback should add that |
228 | | // check if required. |
229 | 0 | callback(cell); |
230 | 0 | cell = cell->nextCell(); |
231 | 0 | } |
232 | 0 | } |
233 | 0 | } Unexecuted instantiation: HadesGC.cpp:void hermes::vm::HadesGC::HeapSegment::forCompactedObjs<hermes::vm::HadesGC::finalizeAll()::$_0>(hermes::vm::HadesGC::finalizeAll()::$_0, hermes::vm::PointerBase&) Unexecuted instantiation: HadesGC.cpp:void hermes::vm::HadesGC::HeapSegment::forCompactedObjs<hermes::vm::HadesGC::youngGenCollection(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, bool)::$_1>(hermes::vm::HadesGC::youngGenCollection(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, bool)::$_1, hermes::vm::PointerBase&) |
234 | | |
235 | 240k | GCCell *HadesGC::OldGen::FreelistCell::carve(uint32_t sz) { |
236 | 240k | const auto origSize = getAllocatedSize(); |
237 | 240k | assert( |
238 | 240k | origSize >= sz + minAllocationSize() && |
239 | 240k | "Can't split if it would leave too small of a second cell"); |
240 | 240k | const auto finalSize = origSize - sz; |
241 | 240k | char *newCellAddress = reinterpret_cast<char *>(this) + finalSize; |
242 | 240k | GCCell *const newCell = reinterpret_cast<GCCell *>(newCellAddress); |
243 | 240k | setSizeFromGC(finalSize); |
244 | 240k | HeapSegment::setCellHead(newCell, sz); |
245 | 240k | return newCell; |
246 | 240k | } |
247 | | |
248 | | class HadesGC::CollectionStats final { |
249 | | public: |
250 | | using Clock = std::chrono::steady_clock; |
251 | | using TimePoint = std::chrono::time_point<Clock>; |
252 | | using Duration = std::chrono::microseconds; |
253 | | |
254 | | CollectionStats(HadesGC &gc, std::string cause, std::string collectionType) |
255 | 35 | : gc_{gc}, |
256 | 35 | cause_{std::move(cause)}, |
257 | 35 | collectionType_{std::move(collectionType)} {} |
258 | 35 | ~CollectionStats() { |
259 | 35 | assert(usedDbg_ && "Stats not submitted."); |
260 | 35 | } |
261 | | |
262 | 1 | void addCollectionType(std::string collectionType) { |
263 | 1 | if (std::find(tags_.begin(), tags_.end(), collectionType) == tags_.end()) |
264 | 1 | tags_.emplace_back(std::move(collectionType)); |
265 | 1 | } |
266 | | |
267 | | /// Record the allocated bytes in the heap and its size before a collection |
268 | | /// begins. |
269 | 34 | void setBeforeSizes(uint64_t allocated, uint64_t external, uint64_t sz) { |
270 | 34 | allocatedBefore_ = allocated; |
271 | 34 | externalBefore_ = external; |
272 | 34 | sizeBefore_ = sz; |
273 | 34 | } |
274 | | |
275 | | /// Record how many bytes were swept during the collection. |
276 | 34 | void setSweptBytes(uint64_t sweptBytes) { |
277 | 34 | sweptBytes_ = sweptBytes; |
278 | 34 | } |
279 | | |
280 | 34 | void setSweptExternalBytes(uint64_t externalBytes) { |
281 | 34 | sweptExternalBytes_ = externalBytes; |
282 | 34 | } |
283 | | |
284 | 34 | void setAfterSize(uint64_t sz) { |
285 | 34 | sizeAfter_ = sz; |
286 | 34 | } |
287 | | |
288 | | /// Record that a collection is beginning right now. |
289 | 35 | void setBeginTime() { |
290 | 35 | assert(beginTime_ == Clock::time_point{} && "Begin time already set"); |
291 | 35 | beginTime_ = Clock::now(); |
292 | 35 | } |
293 | | |
294 | | /// Record that a collection is ending right now. |
295 | 35 | void setEndTime() { |
296 | 35 | assert(endTime_ == Clock::time_point{} && "End time already set"); |
297 | 35 | endTime_ = Clock::now(); |
298 | 35 | } |
299 | | |
300 | 33 | std::chrono::milliseconds getElapsedTime() { |
301 | 33 | return std::chrono::duration_cast<std::chrono::milliseconds>( |
302 | 33 | Clock::now() - beginTime_); |
303 | 33 | } |
304 | | |
305 | | /// Record this amount of CPU time was taken. |
306 | | /// Call begin/end in each thread that does work to correctly count CPU time. |
307 | | /// NOTE: Can only be used by one thread at a time. |
308 | 35 | void beginCPUTimeSection() { |
309 | 35 | assert( |
310 | 35 | cpuTimeSectionStart_ == Duration{} && |
311 | 35 | "Must end cpu time section before starting another"); |
312 | 35 | cpuTimeSectionStart_ = oscompat::thread_cpu_time(); |
313 | 35 | } |
314 | 35 | void endCPUTimeSection() { |
315 | 35 | cpuDuration_ += oscompat::thread_cpu_time() - cpuTimeSectionStart_; |
316 | 35 | cpuTimeSectionStart_ = {}; |
317 | 35 | } |
318 | | |
319 | 2 | uint64_t beforeAllocatedBytes() const { |
320 | 2 | return allocatedBefore_; |
321 | 2 | } |
322 | | |
323 | | /// Since Hades allows allocations during an old gen collection, use the |
324 | | /// initially allocated bytes and the swept bytes to determine the actual |
325 | | /// impact of the GC. |
326 | 103 | uint64_t afterAllocatedBytes() const { |
327 | 103 | assert(sweptBytes_ <= allocatedBefore_); |
328 | 103 | return allocatedBefore_ - sweptBytes_; |
329 | 103 | } |
330 | | |
331 | 70 | uint64_t afterExternalBytes() const { |
332 | 70 | assert(sweptExternalBytes_ <= externalBefore_); |
333 | 70 | return externalBefore_ - sweptExternalBytes_; |
334 | 70 | } |
335 | | |
336 | 35 | double survivalRatio() const { |
337 | 35 | return allocatedBefore_ |
338 | 35 | ? static_cast<double>(afterAllocatedBytes()) / allocatedBefore_ |
339 | 35 | : 0; |
340 | 35 | } |
341 | | |
342 | 35 | void markUsed() { |
343 | 35 | assert(!std::exchange(usedDbg_, true) && "markUsed called twice"); |
344 | 35 | } |
345 | | |
346 | 35 | GCAnalyticsEvent getEvent() && { |
347 | 35 | markUsed(); |
348 | 35 | return GCAnalyticsEvent{ |
349 | 35 | gc_.getName(), |
350 | 35 | gc_.getKindAsStr(), |
351 | 35 | collectionType_, |
352 | 35 | std::move(cause_), |
353 | 35 | std::chrono::duration_cast<std::chrono::milliseconds>( |
354 | 35 | endTime_ - beginTime_), |
355 | 35 | std::chrono::duration_cast<std::chrono::milliseconds>(cpuDuration_), |
356 | 35 | /*allocated*/ BeforeAndAfter{allocatedBefore_, afterAllocatedBytes()}, |
357 | 35 | /*size*/ BeforeAndAfter{sizeBefore_, sizeAfter_}, |
358 | 35 | /*external*/ BeforeAndAfter{externalBefore_, afterExternalBytes()}, |
359 | 35 | /*survivalRatio*/ survivalRatio(), |
360 | 35 | /*tags*/ std::move(tags_)}; |
361 | 35 | } |
362 | | |
363 | | private: |
364 | | HadesGC &gc_; |
365 | | std::string cause_; |
366 | | std::string collectionType_; |
367 | | std::vector<std::string> tags_; |
368 | | TimePoint beginTime_{}; |
369 | | TimePoint endTime_{}; |
370 | | Duration cpuTimeSectionStart_{}; |
371 | | Duration cpuDuration_{}; |
372 | | uint64_t allocatedBefore_{0}; |
373 | | uint64_t externalBefore_{0}; |
374 | | uint64_t sizeBefore_{0}; |
375 | | uint64_t sizeAfter_{0}; |
376 | | uint64_t sweptBytes_{0}; |
377 | | uint64_t sweptExternalBytes_{0}; |
378 | | |
379 | | #ifndef NDEBUG |
380 | | bool usedDbg_{false}; |
381 | | #endif |
382 | | }; |
383 | | |
384 | | template <typename T> |
385 | 6.67k | static T convertPtr(PointerBase &, CompressedPointer cp) { |
386 | 6.67k | return cp; |
387 | 6.67k | } |
388 | | template <> |
389 | 119 | /* static */ GCCell *convertPtr(PointerBase &base, CompressedPointer cp) { |
390 | 119 | return cp.get(base); |
391 | 119 | } |
392 | | template <typename T> |
393 | 1.98k | static T convertPtr(PointerBase &, GCCell *p) { |
394 | 1.98k | return p; |
395 | 1.98k | } |
396 | | template <> |
397 | 8.55k | /* static */ CompressedPointer convertPtr(PointerBase &base, GCCell *a) { |
398 | 8.55k | return CompressedPointer::encodeNonNull(a, base); |
399 | 8.55k | } |
400 | | |
401 | | template <bool CompactionEnabled> |
402 | | class HadesGC::EvacAcceptor final : public RootAndSlotAcceptor, |
403 | | public WeakRootAcceptor { |
404 | | public: |
405 | | EvacAcceptor(HadesGC &gc) |
406 | 33 | : gc{gc}, |
407 | 33 | pointerBase_{gc.getPointerBase()}, |
408 | 33 | copyListHead_{nullptr}, |
409 | 33 | isTrackingIDs_{gc.isTrackingIDs()} {}Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::EvacAcceptor(hermes::vm::HadesGC&) hermes::vm::HadesGC::EvacAcceptor<false>::EvacAcceptor(hermes::vm::HadesGC&) Line | Count | Source | 406 | 33 | : gc{gc}, | 407 | 33 | pointerBase_{gc.getPointerBase()}, | 408 | 33 | copyListHead_{nullptr}, | 409 | 33 | isTrackingIDs_{gc.isTrackingIDs()} {} |
|
410 | | |
411 | 33 | ~EvacAcceptor() override {}Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::~EvacAcceptor() hermes::vm::HadesGC::EvacAcceptor<false>::~EvacAcceptor() Line | Count | Source | 411 | 33 | ~EvacAcceptor() override {} |
|
412 | | |
413 | | // TODO: Implement a purely CompressedPointer version of this. That will let |
414 | | // us avoid decompressing pointers altogether if they point outside the |
415 | | // YG/compactee. |
416 | 119k | inline bool shouldForward(const void *ptr) const { |
417 | 119k | return gc.inYoungGen(ptr) || |
418 | 116k | (CompactionEnabled && gc.compactee_.evacContains(ptr)); |
419 | 119k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::shouldForward(void const*) const hermes::vm::HadesGC::EvacAcceptor<false>::shouldForward(void const*) const Line | Count | Source | 416 | 119k | inline bool shouldForward(const void *ptr) const { | 417 | 119k | return gc.inYoungGen(ptr) || | 418 | 116k | (CompactionEnabled && gc.compactee_.evacContains(ptr)); | 419 | 119k | } |
|
420 | 225k | inline bool shouldForward(CompressedPointer ptr) const { |
421 | 225k | return gc.inYoungGen(ptr) || |
422 | 210k | (CompactionEnabled && gc.compactee_.evacContains(ptr)); |
423 | 225k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::shouldForward(hermes::vm::CompressedPointer) const hermes::vm::HadesGC::EvacAcceptor<false>::shouldForward(hermes::vm::CompressedPointer) const Line | Count | Source | 420 | 225k | inline bool shouldForward(CompressedPointer ptr) const { | 421 | 225k | return gc.inYoungGen(ptr) || | 422 | 210k | (CompactionEnabled && gc.compactee_.evacContains(ptr)); | 423 | 225k | } |
|
424 | | |
425 | 98.8k | LLVM_NODISCARD GCCell *acceptRoot(GCCell *ptr) { |
426 | 98.8k | if (shouldForward(ptr)) |
427 | 1.32k | return forwardCell<GCCell *>(ptr); |
428 | 97.5k | return ptr; |
429 | 98.8k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::acceptRoot(hermes::vm::GCCell*) hermes::vm::HadesGC::EvacAcceptor<false>::acceptRoot(hermes::vm::GCCell*) Line | Count | Source | 425 | 98.8k | LLVM_NODISCARD GCCell *acceptRoot(GCCell *ptr) { | 426 | 98.8k | if (shouldForward(ptr)) | 427 | 1.32k | return forwardCell<GCCell *>(ptr); | 428 | 97.5k | return ptr; | 429 | 98.8k | } |
|
430 | | |
431 | 791 | LLVM_NODISCARD GCCell *acceptHeap(GCCell *ptr, void *heapLoc) { |
432 | 791 | if (shouldForward(ptr)) { |
433 | 773 | assert( |
434 | 773 | HeapSegment::getCellMarkBit(ptr) && |
435 | 773 | "Should only evacuate marked objects."); |
436 | 773 | return forwardCell<GCCell *>(ptr); |
437 | 773 | } |
438 | 18 | if (CompactionEnabled && gc.compactee_.contains(ptr)) { |
439 | | // If a compaction is about to take place, dirty the card for any newly |
440 | | // evacuated cells, since the marker may miss them. |
441 | 0 | HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); |
442 | 0 | } |
443 | 18 | return ptr; |
444 | 791 | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::acceptHeap(hermes::vm::GCCell*, void*) hermes::vm::HadesGC::EvacAcceptor<false>::acceptHeap(hermes::vm::GCCell*, void*) Line | Count | Source | 431 | 791 | LLVM_NODISCARD GCCell *acceptHeap(GCCell *ptr, void *heapLoc) { | 432 | 791 | if (shouldForward(ptr)) { | 433 | 773 | assert( | 434 | 773 | HeapSegment::getCellMarkBit(ptr) && | 435 | 773 | "Should only evacuate marked objects."); | 436 | 773 | return forwardCell<GCCell *>(ptr); | 437 | 773 | } | 438 | 18 | if (CompactionEnabled && gc.compactee_.contains(ptr)) { | 439 | | // If a compaction is about to take place, dirty the card for any newly | 440 | | // evacuated cells, since the marker may miss them. | 441 | 0 | HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); | 442 | 0 | } | 443 | 18 | return ptr; | 444 | 791 | } |
|
445 | | |
446 | | LLVM_NODISCARD CompressedPointer |
447 | 225k | acceptHeap(CompressedPointer cptr, void *heapLoc) { |
448 | 225k | if (shouldForward(cptr)) { |
449 | 15.2k | GCCell *ptr = cptr.getNonNull(pointerBase_); |
450 | 15.2k | assert( |
451 | 15.2k | HeapSegment::getCellMarkBit(ptr) && |
452 | 15.2k | "Should only evacuate marked objects."); |
453 | 15.2k | return forwardCell<CompressedPointer>(ptr); |
454 | 15.2k | } |
455 | 210k | if (CompactionEnabled && gc.compactee_.contains(cptr)) { |
456 | | // If a compaction is about to take place, dirty the card for any newly |
457 | | // evacuated cells, since the marker may miss them. |
458 | 0 | HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); |
459 | 0 | } |
460 | 210k | return cptr; |
461 | 225k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::acceptHeap(hermes::vm::CompressedPointer, void*) hermes::vm::HadesGC::EvacAcceptor<false>::acceptHeap(hermes::vm::CompressedPointer, void*) Line | Count | Source | 447 | 225k | acceptHeap(CompressedPointer cptr, void *heapLoc) { | 448 | 225k | if (shouldForward(cptr)) { | 449 | 15.2k | GCCell *ptr = cptr.getNonNull(pointerBase_); | 450 | 15.2k | assert( | 451 | 15.2k | HeapSegment::getCellMarkBit(ptr) && | 452 | 15.2k | "Should only evacuate marked objects."); | 453 | 15.2k | return forwardCell<CompressedPointer>(ptr); | 454 | 15.2k | } | 455 | 210k | if (CompactionEnabled && gc.compactee_.contains(cptr)) { | 456 | | // If a compaction is about to take place, dirty the card for any newly | 457 | | // evacuated cells, since the marker may miss them. | 458 | 0 | HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); | 459 | 0 | } | 460 | 210k | return cptr; | 461 | 225k | } |
|
462 | | |
463 | | template <typename T> |
464 | 17.3k | LLVM_NODISCARD T forwardCell(GCCell *const cell) { |
465 | 17.3k | assert( |
466 | 17.3k | HeapSegment::getCellMarkBit(cell) && "Cannot forward unmarked object"); |
467 | 17.3k | if (cell->hasMarkedForwardingPointer()) { |
468 | | // Get the forwarding pointer from the header of the object. |
469 | 6.79k | CompressedPointer forwardedCell = cell->getMarkedForwardingPointer(); |
470 | 6.79k | assert( |
471 | 6.79k | forwardedCell.getNonNull(pointerBase_)->isValid() && |
472 | 6.79k | "Cell was forwarded incorrectly"); |
473 | 6.79k | return convertPtr<T>(pointerBase_, forwardedCell); |
474 | 6.79k | } |
475 | 17.3k | assert(cell->isValid() && "Encountered an invalid cell"); |
476 | 10.5k | const auto cellSize = cell->getAllocatedSize(); |
477 | | // Newly discovered cell, first forward into the old gen. |
478 | 10.5k | GCCell *const newCell = gc.oldGen_.alloc(cellSize); |
479 | 10.5k | HERMES_SLOW_ASSERT( |
480 | 10.5k | gc.inOldGen(newCell) && "Evacuated cell not in the old gen"); |
481 | 10.5k | assert( |
482 | 10.5k | HeapSegment::getCellMarkBit(newCell) && |
483 | 10.5k | "Cell must be marked when it is allocated into the old gen"); |
484 | | // Copy the contents of the existing cell over before modifying it. |
485 | 10.5k | std::memcpy(newCell, cell, cellSize); |
486 | 10.5k | assert(newCell->isValid() && "Cell was copied incorrectly"); |
487 | 10.5k | evacuatedBytes_ += cellSize; |
488 | 10.5k | CopyListCell *const copyCell = static_cast<CopyListCell *>(cell); |
489 | | // Set the forwarding pointer in the old spot |
490 | 10.5k | copyCell->setMarkedForwardingPointer( |
491 | 10.5k | CompressedPointer::encodeNonNull(newCell, pointerBase_)); |
492 | 10.5k | if (isTrackingIDs_) { |
493 | 0 | gc.moveObject(cell, cellSize, newCell, cellSize); |
494 | 0 | } |
495 | | // Push onto the copied list. |
496 | 10.5k | push(copyCell); |
497 | 10.5k | return convertPtr<T>(pointerBase_, newCell); |
498 | 10.5k | } Unexecuted instantiation: hermes::vm::GCCell* hermes::vm::HadesGC::EvacAcceptor<true>::forwardCell<hermes::vm::GCCell*>(hermes::vm::GCCell*) Unexecuted instantiation: hermes::vm::CompressedPointer hermes::vm::HadesGC::EvacAcceptor<true>::forwardCell<hermes::vm::CompressedPointer>(hermes::vm::GCCell*) hermes::vm::GCCell* hermes::vm::HadesGC::EvacAcceptor<false>::forwardCell<hermes::vm::GCCell*>(hermes::vm::GCCell*) Line | Count | Source | 464 | 2.10k | LLVM_NODISCARD T forwardCell(GCCell *const cell) { | 465 | 2.10k | assert( | 466 | 2.10k | HeapSegment::getCellMarkBit(cell) && "Cannot forward unmarked object"); | 467 | 2.10k | if (cell->hasMarkedForwardingPointer()) { | 468 | | // Get the forwarding pointer from the header of the object. | 469 | 119 | CompressedPointer forwardedCell = cell->getMarkedForwardingPointer(); | 470 | 119 | assert( | 471 | 119 | forwardedCell.getNonNull(pointerBase_)->isValid() && | 472 | 119 | "Cell was forwarded incorrectly"); | 473 | 119 | return convertPtr<T>(pointerBase_, forwardedCell); | 474 | 119 | } | 475 | 2.10k | assert(cell->isValid() && "Encountered an invalid cell"); | 476 | 1.98k | const auto cellSize = cell->getAllocatedSize(); | 477 | | // Newly discovered cell, first forward into the old gen. | 478 | 1.98k | GCCell *const newCell = gc.oldGen_.alloc(cellSize); | 479 | 1.98k | HERMES_SLOW_ASSERT( | 480 | 1.98k | gc.inOldGen(newCell) && "Evacuated cell not in the old gen"); | 481 | 1.98k | assert( | 482 | 1.98k | HeapSegment::getCellMarkBit(newCell) && | 483 | 1.98k | "Cell must be marked when it is allocated into the old gen"); | 484 | | // Copy the contents of the existing cell over before modifying it. | 485 | 1.98k | std::memcpy(newCell, cell, cellSize); | 486 | 1.98k | assert(newCell->isValid() && "Cell was copied incorrectly"); | 487 | 1.98k | evacuatedBytes_ += cellSize; | 488 | 1.98k | CopyListCell *const copyCell = static_cast<CopyListCell *>(cell); | 489 | | // Set the forwarding pointer in the old spot | 490 | 1.98k | copyCell->setMarkedForwardingPointer( | 491 | 1.98k | CompressedPointer::encodeNonNull(newCell, pointerBase_)); | 492 | 1.98k | if (isTrackingIDs_) { | 493 | 0 | gc.moveObject(cell, cellSize, newCell, cellSize); | 494 | 0 | } | 495 | | // Push onto the copied list. | 496 | 1.98k | push(copyCell); | 497 | 1.98k | return convertPtr<T>(pointerBase_, newCell); | 498 | 1.98k | } |
hermes::vm::CompressedPointer hermes::vm::HadesGC::EvacAcceptor<false>::forwardCell<hermes::vm::CompressedPointer>(hermes::vm::GCCell*) Line | Count | Source | 464 | 15.2k | LLVM_NODISCARD T forwardCell(GCCell *const cell) { | 465 | 15.2k | assert( | 466 | 15.2k | HeapSegment::getCellMarkBit(cell) && "Cannot forward unmarked object"); | 467 | 15.2k | if (cell->hasMarkedForwardingPointer()) { | 468 | | // Get the forwarding pointer from the header of the object. | 469 | 6.67k | CompressedPointer forwardedCell = cell->getMarkedForwardingPointer(); | 470 | 6.67k | assert( | 471 | 6.67k | forwardedCell.getNonNull(pointerBase_)->isValid() && | 472 | 6.67k | "Cell was forwarded incorrectly"); | 473 | 6.67k | return convertPtr<T>(pointerBase_, forwardedCell); | 474 | 6.67k | } | 475 | 15.2k | assert(cell->isValid() && "Encountered an invalid cell"); | 476 | 8.55k | const auto cellSize = cell->getAllocatedSize(); | 477 | | // Newly discovered cell, first forward into the old gen. | 478 | 8.55k | GCCell *const newCell = gc.oldGen_.alloc(cellSize); | 479 | 8.55k | HERMES_SLOW_ASSERT( | 480 | 8.55k | gc.inOldGen(newCell) && "Evacuated cell not in the old gen"); | 481 | 8.55k | assert( | 482 | 8.55k | HeapSegment::getCellMarkBit(newCell) && | 483 | 8.55k | "Cell must be marked when it is allocated into the old gen"); | 484 | | // Copy the contents of the existing cell over before modifying it. | 485 | 8.55k | std::memcpy(newCell, cell, cellSize); | 486 | 8.55k | assert(newCell->isValid() && "Cell was copied incorrectly"); | 487 | 8.55k | evacuatedBytes_ += cellSize; | 488 | 8.55k | CopyListCell *const copyCell = static_cast<CopyListCell *>(cell); | 489 | | // Set the forwarding pointer in the old spot | 490 | 8.55k | copyCell->setMarkedForwardingPointer( | 491 | 8.55k | CompressedPointer::encodeNonNull(newCell, pointerBase_)); | 492 | 8.55k | if (isTrackingIDs_) { | 493 | 0 | gc.moveObject(cell, cellSize, newCell, cellSize); | 494 | 0 | } | 495 | | // Push onto the copied list. | 496 | 8.55k | push(copyCell); | 497 | 8.55k | return convertPtr<T>(pointerBase_, newCell); | 498 | 8.55k | } |
|
499 | | |
500 | 1.86k | void accept(GCCell *&ptr) override { |
501 | 1.86k | ptr = acceptRoot(ptr); |
502 | 1.86k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::GCCell*&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::GCCell*&) Line | Count | Source | 500 | 1.86k | void accept(GCCell *&ptr) override { | 501 | 1.86k | ptr = acceptRoot(ptr); | 502 | 1.86k | } |
|
503 | | |
504 | 35.0k | void accept(GCPointerBase &ptr) override { |
505 | 35.0k | ptr.setInGC(acceptHeap(ptr, &ptr)); |
506 | 35.0k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::GCPointerBase&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::GCPointerBase&) Line | Count | Source | 504 | 35.0k | void accept(GCPointerBase &ptr) override { | 505 | 35.0k | ptr.setInGC(acceptHeap(ptr, &ptr)); | 506 | 35.0k | } |
|
507 | | |
508 | 97.7k | void accept(PinnedHermesValue &hv) override { |
509 | 97.7k | assert((!hv.isPointer() || hv.getPointer()) && "Value is not nullable."); |
510 | 97.7k | acceptNullable(hv); |
511 | 97.7k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::PinnedHermesValue&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::PinnedHermesValue&) Line | Count | Source | 508 | 97.7k | void accept(PinnedHermesValue &hv) override { | 509 | 97.7k | assert((!hv.isPointer() || hv.getPointer()) && "Value is not nullable."); | 510 | 97.7k | acceptNullable(hv); | 511 | 97.7k | } |
|
512 | | |
513 | 98.0k | void acceptNullable(PinnedHermesValue &hv) override { |
514 | 98.0k | if (hv.isPointer()) { |
515 | 96.9k | GCCell *forwardedPtr = acceptRoot(static_cast<GCCell *>(hv.getPointer())); |
516 | 96.9k | hv.setInGC(hv.updatePointer(forwardedPtr), gc); |
517 | 96.9k | } |
518 | 98.0k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::acceptNullable(hermes::vm::PinnedHermesValue&) hermes::vm::HadesGC::EvacAcceptor<false>::acceptNullable(hermes::vm::PinnedHermesValue&) Line | Count | Source | 513 | 98.0k | void acceptNullable(PinnedHermesValue &hv) override { | 514 | 98.0k | if (hv.isPointer()) { | 515 | 96.9k | GCCell *forwardedPtr = acceptRoot(static_cast<GCCell *>(hv.getPointer())); | 516 | 96.9k | hv.setInGC(hv.updatePointer(forwardedPtr), gc); | 517 | 96.9k | } | 518 | 98.0k | } |
|
519 | | |
520 | 512k | void accept(GCHermesValue &hv) override { |
521 | 512k | if (hv.isPointer()) { |
522 | 791 | GCCell *forwardedPtr = |
523 | 791 | acceptHeap(static_cast<GCCell *>(hv.getPointer()), &hv); |
524 | 791 | hv.setInGC(hv.updatePointer(forwardedPtr), gc); |
525 | 791 | } |
526 | 512k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::GCHermesValueBase<hermes::vm::HermesValue>&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::GCHermesValueBase<hermes::vm::HermesValue>&) Line | Count | Source | 520 | 512k | void accept(GCHermesValue &hv) override { | 521 | 512k | if (hv.isPointer()) { | 522 | 791 | GCCell *forwardedPtr = | 523 | 791 | acceptHeap(static_cast<GCCell *>(hv.getPointer()), &hv); | 524 | 791 | hv.setInGC(hv.updatePointer(forwardedPtr), gc); | 525 | 791 | } | 526 | 512k | } |
|
527 | | |
528 | 196k | void accept(GCSmallHermesValue &hv) override { |
529 | 196k | if (hv.isPointer()) { |
530 | 190k | CompressedPointer forwardedPtr = acceptHeap(hv.getPointer(), &hv); |
531 | 190k | hv.setInGC(hv.updatePointer(forwardedPtr), gc); |
532 | 190k | } |
533 | 196k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::GCHermesValueBase<hermes::vm::HermesValue32>&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::GCHermesValueBase<hermes::vm::HermesValue32>&) Line | Count | Source | 528 | 196k | void accept(GCSmallHermesValue &hv) override { | 529 | 196k | if (hv.isPointer()) { | 530 | 190k | CompressedPointer forwardedPtr = acceptHeap(hv.getPointer(), &hv); | 531 | 190k | hv.setInGC(hv.updatePointer(forwardedPtr), gc); | 532 | 190k | } | 533 | 196k | } |
|
534 | | |
535 | 19.4k | void acceptWeak(WeakRootBase &wr) override { |
536 | | // It's safe to not do a read barrier here since this is happening in the GC |
537 | | // and does not extend the lifetime of the referent. |
538 | 19.4k | GCCell *const ptr = wr.getNoBarrierUnsafe(pointerBase_); |
539 | | |
540 | 19.4k | if (!shouldForward(ptr)) |
541 | 19.3k | return; |
542 | | |
543 | 160 | if (ptr->hasMarkedForwardingPointer()) { |
544 | | // Get the forwarding pointer from the header of the object. |
545 | 24 | CompressedPointer forwardedCell = ptr->getMarkedForwardingPointer(); |
546 | 24 | assert( |
547 | 24 | forwardedCell.getNonNull(pointerBase_)->isValid() && |
548 | 24 | "Cell was forwarded incorrectly"); |
549 | | // Assign back to the input pointer location. |
550 | 24 | wr = forwardedCell; |
551 | 136 | } else { |
552 | 136 | wr = nullptr; |
553 | 136 | } |
554 | 160 | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::acceptWeak(hermes::vm::WeakRootBase&) hermes::vm::HadesGC::EvacAcceptor<false>::acceptWeak(hermes::vm::WeakRootBase&) Line | Count | Source | 535 | 19.4k | void acceptWeak(WeakRootBase &wr) override { | 536 | | // It's safe to not do a read barrier here since this is happening in the GC | 537 | | // and does not extend the lifetime of the referent. | 538 | 19.4k | GCCell *const ptr = wr.getNoBarrierUnsafe(pointerBase_); | 539 | | | 540 | 19.4k | if (!shouldForward(ptr)) | 541 | 19.3k | return; | 542 | | | 543 | 160 | if (ptr->hasMarkedForwardingPointer()) { | 544 | | // Get the forwarding pointer from the header of the object. | 545 | 24 | CompressedPointer forwardedCell = ptr->getMarkedForwardingPointer(); | 546 | 24 | assert( | 547 | 24 | forwardedCell.getNonNull(pointerBase_)->isValid() && | 548 | 24 | "Cell was forwarded incorrectly"); | 549 | | // Assign back to the input pointer location. | 550 | 24 | wr = forwardedCell; | 551 | 136 | } else { | 552 | 136 | wr = nullptr; | 553 | 136 | } | 554 | 160 | } |
|
555 | | |
556 | 0 | void accept(const RootSymbolID &sym) override {}Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::RootSymbolID const&) Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::RootSymbolID const&) |
557 | 76.5k | void accept(const GCSymbolID &sym) override {}Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::accept(hermes::vm::GCSymbolID const&) hermes::vm::HadesGC::EvacAcceptor<false>::accept(hermes::vm::GCSymbolID const&) Line | Count | Source | 557 | 76.5k | void accept(const GCSymbolID &sym) override {} |
|
558 | | |
559 | 33 | uint64_t evacuatedBytes() const { |
560 | 33 | return evacuatedBytes_; |
561 | 33 | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::evacuatedBytes() const hermes::vm::HadesGC::EvacAcceptor<false>::evacuatedBytes() const Line | Count | Source | 559 | 33 | uint64_t evacuatedBytes() const { | 560 | 33 | return evacuatedBytes_; | 561 | 33 | } |
|
562 | | |
563 | 10.5k | CopyListCell *pop() { |
564 | 10.5k | if (!copyListHead_) { |
565 | 33 | return nullptr; |
566 | 10.5k | } else { |
567 | 10.5k | CopyListCell *const cell = |
568 | 10.5k | static_cast<CopyListCell *>(copyListHead_.getNonNull(pointerBase_)); |
569 | 10.5k | assert(HeapSegment::getCellMarkBit(cell) && "Discovered unmarked object"); |
570 | 10.5k | copyListHead_ = cell->next_; |
571 | 10.5k | return cell; |
572 | 10.5k | } |
573 | 10.5k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::pop() hermes::vm::HadesGC::EvacAcceptor<false>::pop() Line | Count | Source | 563 | 10.5k | CopyListCell *pop() { | 564 | 10.5k | if (!copyListHead_) { | 565 | 33 | return nullptr; | 566 | 10.5k | } else { | 567 | 10.5k | CopyListCell *const cell = | 568 | 10.5k | static_cast<CopyListCell *>(copyListHead_.getNonNull(pointerBase_)); | 569 | 10.5k | assert(HeapSegment::getCellMarkBit(cell) && "Discovered unmarked object"); | 570 | 10.5k | copyListHead_ = cell->next_; | 571 | 10.5k | return cell; | 572 | 10.5k | } | 573 | 10.5k | } |
|
574 | | |
575 | | private: |
576 | | HadesGC &gc; |
577 | | PointerBase &pointerBase_; |
578 | | /// The copy list is managed implicitly in the body of each copied YG object. |
579 | | AssignableCompressedPointer copyListHead_; |
580 | | const bool isTrackingIDs_; |
581 | | uint64_t evacuatedBytes_{0}; |
582 | | |
583 | 10.5k | void push(CopyListCell *cell) { |
584 | 10.5k | cell->next_ = copyListHead_; |
585 | 10.5k | copyListHead_ = CompressedPointer::encodeNonNull(cell, pointerBase_); |
586 | 10.5k | } Unexecuted instantiation: hermes::vm::HadesGC::EvacAcceptor<true>::push(hermes::vm::HadesGC::CopyListCell*) hermes::vm::HadesGC::EvacAcceptor<false>::push(hermes::vm::HadesGC::CopyListCell*) Line | Count | Source | 583 | 10.5k | void push(CopyListCell *cell) { | 584 | 10.5k | cell->next_ = copyListHead_; | 585 | 10.5k | copyListHead_ = CompressedPointer::encodeNonNull(cell, pointerBase_); | 586 | 10.5k | } |
|
587 | | }; |
588 | | |
589 | | class MarkWorklist { |
590 | | private: |
591 | | /// Like std::vector but has a fixed capacity specified by N to reduce memory |
592 | | /// allocation. |
593 | | template <typename T, size_t N> |
594 | | class FixedCapacityVector { |
595 | | public: |
596 | 1 | explicit FixedCapacityVector() : FixedCapacityVector(0) {} |
597 | 1 | explicit FixedCapacityVector(size_t sz) : size_(sz) {} |
598 | | explicit FixedCapacityVector(const FixedCapacityVector &vec) |
599 | | : data_(vec.data_), size_(vec.size_) {} |
600 | | |
601 | 1 | size_t size() const { |
602 | 1 | return size_; |
603 | 1 | } |
604 | | |
605 | 0 | constexpr size_t capacity() const { |
606 | 0 | return N; |
607 | 0 | } |
608 | | |
609 | 2 | bool empty() const { |
610 | 2 | return size_ == 0; |
611 | 2 | } |
612 | | |
613 | 0 | void push_back(T elem) { |
614 | 0 | assert( |
615 | 0 | size_ < N && "Trying to push off the end of a FixedCapacityVector"); |
616 | 0 | data_[size_++] = elem; |
617 | 0 | } |
618 | | |
619 | 2 | T *data() { |
620 | 2 | return data_.data(); |
621 | 2 | } |
622 | | |
623 | 1 | void clear() { |
624 | 1 | #ifndef NDEBUG |
625 | | // Fill the push chunk with bad memory in debug modes to catch invalid |
626 | | // reads. |
627 | 1 | std::memset(data_.data(), kInvalidHeapValue, sizeof(GCCell *) * N); |
628 | 1 | #endif |
629 | 1 | size_ = 0; |
630 | 1 | } |
631 | | |
632 | | private: |
633 | | std::array<T, N> data_; |
634 | | size_t size_; |
635 | | }; |
636 | | |
637 | | public: |
638 | | /// Adds an element to the end of the queue. |
639 | 0 | void enqueue(GCCell *cell) { |
640 | 0 | pushChunk_.push_back(cell); |
641 | 0 | if (pushChunk_.size() == pushChunk_.capacity()) { |
642 | | // Once the chunk has reached its max size, move it to the pull chunks. |
643 | 0 | flushPushChunk(); |
644 | 0 | } |
645 | 0 | } |
646 | | |
647 | | /// Empty and return the current worklist |
648 | 17 | llvh::SmallVector<GCCell *, 0> drain() { |
649 | 17 | llvh::SmallVector<GCCell *, 0> cells; |
650 | | // Move the list (in O(1) time) to a local variable, and then release the |
651 | | // lock. This unblocks the mutator faster. |
652 | 17 | std::lock_guard<Mutex> lk{mtx_}; |
653 | 17 | std::swap(cells, worklist_); |
654 | 17 | assert(worklist_.empty() && "worklist isn't cleared out"); |
655 | | // Keep the previously allocated size to minimise allocations |
656 | 17 | worklist_.reserve(cells.size()); |
657 | 17 | return cells; |
658 | 17 | } |
659 | | |
660 | | /// While the world is stopped, move the push chunk to the list of pull chunks |
661 | | /// to finish draining the mark worklist. |
662 | | /// WARN: This can only be called by the mutator or when the world is stopped. |
663 | 1 | void flushPushChunk() { |
664 | 1 | std::lock_guard<Mutex> lk{mtx_}; |
665 | 1 | worklist_.insert( |
666 | 1 | worklist_.end(), |
667 | 1 | pushChunk_.data(), |
668 | 1 | pushChunk_.data() + pushChunk_.size()); |
669 | | // Set the size back to 0 and refill the same buffer. |
670 | 1 | pushChunk_.clear(); |
671 | 1 | } |
672 | | |
673 | | /// \return true if there is still some work to be drained, with the exception |
674 | | /// of the push chunk. |
675 | 0 | bool hasPendingWork() { |
676 | 0 | std::lock_guard<Mutex> lk{mtx_}; |
677 | 0 | return !worklist_.empty(); |
678 | 0 | } |
679 | | |
680 | | #ifndef NDEBUG |
681 | | /// WARN: This can only be called when the world is stopped. |
682 | 2 | bool empty() { |
683 | 2 | std::lock_guard<Mutex> lk{mtx_}; |
684 | 2 | return pushChunk_.empty() && worklist_.empty(); |
685 | 2 | } |
686 | | #endif |
687 | | |
688 | | private: |
689 | | Mutex mtx_; |
690 | | static constexpr size_t kChunkSize = 128; |
691 | | using Chunk = FixedCapacityVector<GCCell *, kChunkSize>; |
692 | | Chunk pushChunk_; |
693 | | // Use a SmallVector of size 0 since it is more aggressive with PODs |
694 | | llvh::SmallVector<GCCell *, 0> worklist_; |
695 | | }; |
696 | | |
697 | | class HadesGC::MarkAcceptor final : public RootAndSlotAcceptor { |
698 | | public: |
699 | | MarkAcceptor(HadesGC &gc) |
700 | 1 | : gc{gc}, |
701 | 1 | pointerBase_{gc.getPointerBase()}, |
702 | 1 | markedSymbols_{gc.gcCallbacks_.getSymbolsEnd()}, |
703 | 1 | writeBarrierMarkedSymbols_{gc.gcCallbacks_.getSymbolsEnd()} {} |
704 | | |
705 | 3.69k | void acceptHeap(GCCell *cell, const void *heapLoc) { |
706 | 3.69k | assert(cell && "Cannot pass null pointer to acceptHeap"); |
707 | 3.69k | assert(!gc.inYoungGen(heapLoc) && "YG slot found in OG marking"); |
708 | 3.69k | if (gc.compactee_.contains(cell) && !gc.compactee_.contains(heapLoc)) { |
709 | | // This is a pointer in the heap pointing into the compactee, dirty the |
710 | | // corresponding card. |
711 | 0 | HeapSegment::cardTableCovering(heapLoc)->dirtyCardForAddress(heapLoc); |
712 | 0 | } |
713 | 3.69k | if (HeapSegment::getCellMarkBit(cell)) { |
714 | | // Points to an already marked object, do nothing. |
715 | 2.04k | return; |
716 | 2.04k | } |
717 | | // This must be done after checking the cell mark bit, to avoid reading the |
718 | | // metadata of cells in the YG, which we do not have the lock for. |
719 | 3.69k | assert(cell->isValid() && "Encountered an invalid cell"); |
720 | 1.64k | push(cell); |
721 | 1.64k | } |
722 | | |
723 | 863 | void acceptRoot(GCCell *cell) { |
724 | 863 | assert(cell->isValid() && "Encountered an invalid cell"); |
725 | 863 | if (!HeapSegment::getCellMarkBit(cell)) |
726 | 836 | push(cell); |
727 | 863 | } |
728 | | |
729 | 484 | void accept(GCCell *&ptr) override { |
730 | 484 | if (ptr) |
731 | 484 | acceptRoot(ptr); |
732 | 484 | } |
733 | | |
734 | 4.09k | void accept(GCPointerBase &ptr) override { |
735 | 4.09k | if (auto cp = concurrentRead<CompressedPointer>(ptr)) |
736 | 2.47k | acceptHeap(cp.getNonNull(pointerBase_), &ptr); |
737 | 4.09k | } |
738 | | |
739 | 38 | void accept(GCHermesValue &hvRef) override { |
740 | 38 | HermesValue hv = concurrentRead<HermesValue>(hvRef); |
741 | 38 | if (hv.isPointer()) { |
742 | 35 | acceptHeap(static_cast<GCCell *>(hv.getPointer()), &hvRef); |
743 | 35 | } else if (hv.isSymbol()) { |
744 | 0 | acceptSym(hv.getSymbol()); |
745 | 0 | } |
746 | 38 | } |
747 | | |
748 | 480 | void accept(PinnedHermesValue &hv) override { |
749 | | // PinnedHermesValue is a root type and cannot live in the heap. Therefore |
750 | | // there's no risk of a concurrent access. |
751 | 480 | if (hv.isPointer()) { |
752 | 377 | acceptRoot(static_cast<GCCell *>(hv.getPointer())); |
753 | 377 | } else if (hv.isSymbol()) { |
754 | 0 | acceptSym(hv.getSymbol()); |
755 | 0 | } |
756 | 480 | } |
757 | 3 | void acceptNullable(PinnedHermesValue &hv) override { |
758 | 3 | if (hv.isPointer()) { |
759 | 2 | if (void *ptr = hv.getPointer()) |
760 | 2 | acceptRoot(static_cast<GCCell *>(ptr)); |
761 | 2 | } else if (hv.isSymbol()) { |
762 | 0 | acceptSym(hv.getSymbol()); |
763 | 0 | } |
764 | 3 | } |
765 | | |
766 | 1.88k | void accept(GCSmallHermesValue &hvRef) override { |
767 | 1.88k | const SmallHermesValue hv = concurrentRead<SmallHermesValue>(hvRef); |
768 | 1.88k | if (hv.isPointer()) { |
769 | 1.18k | acceptHeap(hv.getPointer(pointerBase_), &hvRef); |
770 | 1.18k | } else if (hv.isSymbol()) { |
771 | 10 | acceptSym(hv.getSymbol()); |
772 | 10 | } |
773 | 1.88k | } |
774 | | |
775 | 1.38k | void acceptSym(SymbolID sym) { |
776 | 1.38k | const uint32_t idx = sym.unsafeGetIndex(); |
777 | 1.38k | if (sym.isInvalid() || idx >= markedSymbols_.size()) { |
778 | | // Ignore symbols that aren't valid or are pointing outside of the range |
779 | | // when the collection began. |
780 | 2 | return; |
781 | 2 | } |
782 | 1.38k | markedSymbols_[idx] = true; |
783 | 1.38k | } |
784 | | |
785 | 123 | void accept(const RootSymbolID &sym) override { |
786 | 123 | acceptSym(sym); |
787 | 123 | } |
788 | 1.25k | void accept(const GCSymbolID &sym) override { |
789 | 1.25k | acceptSym(concurrentRead<SymbolID>(sym)); |
790 | 1.25k | } |
791 | | |
792 | | /// Interface for symbols marked by a write barrier. |
793 | 0 | void markSymbol(SymbolID sym) { |
794 | 0 | assert( |
795 | 0 | !gc.calledByBackgroundThread() && |
796 | 0 | "Write barrier invoked by background thread."); |
797 | 0 | const uint32_t idx = sym.unsafeGetIndex(); |
798 | 0 | if (sym.isInvalid() || idx >= writeBarrierMarkedSymbols_.size()) { |
799 | | // Ignore symbols that aren't valid or are pointing outside of the range |
800 | | // when the collection began. |
801 | 0 | return; |
802 | 0 | } |
803 | 0 | writeBarrierMarkedSymbols_[idx] = true; |
804 | 0 | } |
805 | | |
806 | | /// Set the drain rate that'll be used for any future calls to drain APIs. |
807 | 0 | void setDrainRate(size_t rate) { |
808 | 0 | assert(!kConcurrentGC && "Drain rate is only used by incremental GC."); |
809 | 0 | byteDrainRate_ = rate; |
810 | 0 | } |
811 | | |
812 | | /// \return the total number of bytes marked so far. |
813 | 1 | uint64_t markedBytes() const { |
814 | 1 | return markedBytes_; |
815 | 1 | } |
816 | | |
817 | | /// Drain the mark stack of cells to be processed. |
818 | | /// \post localWorklist is empty. Any flushed values in the global worklist at |
819 | | /// the start of the call are drained. |
820 | 2 | void drainAllWork() { |
821 | | // This should only be called from the mutator. This means no write barriers |
822 | | // should occur, and there's no need to check the global worklist more than |
823 | | // once. |
824 | 2 | drainSomeWork(std::numeric_limits<size_t>::max()); |
825 | 2 | assert(localWorklist_.empty() && "Some work left that wasn't completed"); |
826 | 2 | } |
827 | | |
828 | | /// Drains some of the worklist, using a drain rate specified by |
829 | | /// \c setDrainRate or kConcurrentMarkLimit. |
830 | | /// \return true if there is any remaining work in the local worklist. |
831 | 15 | bool drainSomeWork() { |
832 | | // See the comment in setDrainRate for why the drain rate isn't used for |
833 | | // concurrent collections. |
834 | 15 | constexpr size_t kConcurrentMarkLimit = 8192; |
835 | 15 | return drainSomeWork(kConcurrentGC ? kConcurrentMarkLimit : byteDrainRate_); |
836 | 15 | } |
837 | | |
838 | | /// Drain some of the work to be done for marking. |
839 | | /// \param markLimit Only mark up to this many bytes from the local |
840 | | /// worklist. |
841 | | /// NOTE: This does *not* guarantee that the marker thread |
842 | | /// has upper bounds on the amount of work it does before reading from the |
843 | | /// global worklist. Any individual cell can be quite large (such as an |
844 | | /// ArrayStorage). |
845 | | /// \return true if there is any remaining work in the local worklist. |
846 | 17 | bool drainSomeWork(const size_t markLimit) { |
847 | 17 | assert(gc.gcMutex_ && "Must hold the GC lock while accessing mark bits."); |
848 | | // Pull any new items off the global worklist. |
849 | 17 | auto cells = globalWorklist_.drain(); |
850 | 17 | for (GCCell *cell : cells) { |
851 | 0 | assert( |
852 | 0 | cell->isValid() && "Invalid cell received off the global worklist"); |
853 | 0 | assert( |
854 | 0 | !gc.inYoungGen(cell) && |
855 | 0 | "Shouldn't ever traverse a YG object in this loop"); |
856 | 0 | HERMES_SLOW_ASSERT( |
857 | 0 | gc.dbgContains(cell) && "Non-heap cell found in global worklist"); |
858 | 0 | if (!HeapSegment::getCellMarkBit(cell)) { |
859 | | // Cell has not yet been marked. |
860 | 0 | push(cell); |
861 | 0 | } |
862 | 0 | } |
863 | | |
864 | 17 | size_t numMarkedBytes = 0; |
865 | 17 | assert(markLimit && "markLimit must be non-zero!"); |
866 | 2.50k | while (!localWorklist_.empty() && numMarkedBytes < markLimit) { |
867 | 2.48k | GCCell *const cell = localWorklist_.top(); |
868 | 2.48k | localWorklist_.pop(); |
869 | 2.48k | assert(cell->isValid() && "Invalid cell in marking"); |
870 | 2.48k | assert(HeapSegment::getCellMarkBit(cell) && "Discovered unmarked object"); |
871 | 2.48k | assert( |
872 | 2.48k | !gc.inYoungGen(cell) && |
873 | 2.48k | "Shouldn't ever traverse a YG object in this loop"); |
874 | 2.48k | HERMES_SLOW_ASSERT( |
875 | 2.48k | gc.dbgContains(cell) && "Non-heap object discovered during marking"); |
876 | 2.48k | const auto sz = cell->getAllocatedSize(); |
877 | 2.48k | numMarkedBytes += sz; |
878 | 2.48k | gc.markCell(cell, *this); |
879 | 2.48k | } |
880 | 17 | markedBytes_ += numMarkedBytes; |
881 | 17 | return !localWorklist_.empty(); |
882 | 17 | } |
883 | | |
884 | 1 | bool isLocalWorklistEmpty() const { |
885 | 1 | return localWorklist_.empty(); |
886 | 1 | } |
887 | | |
888 | 3 | MarkWorklist &globalWorklist() { |
889 | 3 | return globalWorklist_; |
890 | 3 | } |
891 | | |
892 | | /// Merge the symbols marked by the MarkAcceptor and by the write barrier, |
893 | | /// then return a reference to it. |
894 | | /// WARN: This should only be called when the mutator is paused, as |
895 | | /// otherwise there is a race condition between reading this and a symbol |
896 | | /// write barrier getting executed. |
897 | 1 | llvh::BitVector &markedSymbols() { |
898 | 1 | assert(gc.gcMutex_ && "Cannot call markedSymbols without a lock"); |
899 | 1 | markedSymbols_ |= writeBarrierMarkedSymbols_; |
900 | | // No need to clear writeBarrierMarkedSymbols_, or'ing it again won't change |
901 | | // the bit vector. |
902 | 1 | return markedSymbols_; |
903 | 1 | } |
904 | | |
905 | | private: |
906 | | HadesGC &gc; |
907 | | PointerBase &pointerBase_; |
908 | | |
909 | | /// A worklist local to the marking thread, that is only pushed onto by the |
910 | | /// marking thread. If this is empty, the global worklist must be consulted |
911 | | /// to ensure that pointers modified in write barriers are handled. |
912 | | std::stack<GCCell *, std::vector<GCCell *>> localWorklist_; |
913 | | |
914 | | /// A worklist that other threads may add to as objects to be marked and |
915 | | /// considered alive. These objects will *not* have their mark bits set, |
916 | | /// because the mutator can't be modifying mark bits at the same time as the |
917 | | /// marker thread. |
918 | | MarkWorklist globalWorklist_; |
919 | | |
920 | | /// markedSymbols_ represents which symbols have been proven live so far in |
921 | | /// a collection. True means that it is live, false means that it could |
922 | | /// possibly be garbage. The SymbolID's internal value is used as the index |
923 | | /// into this vector. Once the collection is finished, this vector is passed |
924 | | /// to IdentifierTable so that it can free symbols. If any new symbols are |
925 | | /// allocated after the collection began, assume they are live. |
926 | | llvh::BitVector markedSymbols_; |
927 | | |
928 | | /// A vector the same size as markedSymbols_ that will collect all symbols |
929 | | /// marked by write barriers. Merge this with markedSymbols_ to have complete |
930 | | /// information about marked symbols. Kept separate to avoid synchronization. |
931 | | llvh::BitVector writeBarrierMarkedSymbols_; |
932 | | |
933 | | /// The number of bytes to drain per call to drainSomeWork. A higher rate |
934 | | /// means more objects will be marked. |
935 | | /// Only used by incremental collections. |
936 | | size_t byteDrainRate_{0}; |
937 | | |
938 | | /// The number of bytes that have been marked so far. |
939 | | uint64_t markedBytes_{0}; |
940 | | |
941 | 2.48k | void push(GCCell *cell) { |
942 | 2.48k | assert( |
943 | 2.48k | !HeapSegment::getCellMarkBit(cell) && |
944 | 2.48k | "A marked object should never be pushed onto a worklist"); |
945 | 2.48k | assert( |
946 | 2.48k | !gc.inYoungGen(cell) && |
947 | 2.48k | "Shouldn't ever push a YG object onto the worklist"); |
948 | 2.48k | HeapSegment::setCellMarkBit(cell); |
949 | | // There could be a race here: however, the mutator will never change a |
950 | | // cell's kind after initialization. The GC thread might to a free cell, but |
951 | | // only during sweeping, not concurrently with this operation. Therefore |
952 | | // there's no need for any synchronization here. |
953 | 2.48k | localWorklist_.push(cell); |
954 | 2.48k | } |
955 | | |
956 | | template <typename T> |
957 | 7.26k | T concurrentReadImpl(const T &valRef) { |
958 | 7.26k | using Storage = |
959 | 7.26k | typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; |
960 | 7.26k | static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); |
961 | 7.26k | union { |
962 | 7.26k | Storage storage{}; |
963 | 7.26k | T val; |
964 | 7.26k | } ret{}; |
965 | | |
966 | | // There is a benign data race here, as the GC can read a pointer while |
967 | | // it's being modified by the mutator; however, the following rules we |
968 | | // obey prevent it from being a problem: |
969 | | // * The only things being modified that the GC reads are the GCPointers |
970 | | // and GCHermesValue in an object. All other fields are ignored. |
971 | | // * Those fields are fewer than 64 bits. |
972 | | // * Therefore, on 64-bit platforms, those fields are atomic |
973 | | // automatically. |
974 | | // * On 32-bit platforms, we don't run this code concurrently, and |
975 | | // instead yield cooperatively with the mutator. |
976 | | // * Thanks to the write barrier, if something is modified its old value |
977 | | // is placed in the globalWorklist, so we don't miss marking it. |
978 | | // * Since the global worklist is pushed onto *before* the write |
979 | | // happens, we know that there's no way the loop will exit unless it |
980 | | // reads the old value. |
981 | | // * If it observes the old value (pre-write-barrier value) here, the |
982 | | // new value will still be live, either by being pre-marked by the |
983 | | // allocator, or because something else's write barrier will catch |
984 | | // the modification. |
985 | 7.26k | TsanIgnoreReadsBegin(); |
986 | | |
987 | | // The cast to a volatile variable forces a read from valRef, since |
988 | | // reads from volatile variables are considered observable behaviour. This |
989 | | // prevents the compiler from optimizing away the returned value, |
990 | | // guaranteeing that we will not observe changes to the underlying value |
991 | | // past this point. Not using volatile here could lead to a TOCTOU bug, |
992 | | // because the underlying value may change after a pointer check (in the |
993 | | // case of HermesValue) or a null check (for pointers). |
994 | 7.26k | ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); |
995 | 7.26k | TsanIgnoreReadsEnd(); |
996 | 7.26k | return ret.val; |
997 | 7.26k | } hermes::vm::CompressedPointer hermes::vm::HadesGC::MarkAcceptor::concurrentReadImpl<hermes::vm::CompressedPointer>(hermes::vm::CompressedPointer const&) Line | Count | Source | 957 | 4.09k | T concurrentReadImpl(const T &valRef) { | 958 | 4.09k | using Storage = | 959 | 4.09k | typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; | 960 | 4.09k | static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); | 961 | 4.09k | union { | 962 | 4.09k | Storage storage{}; | 963 | 4.09k | T val; | 964 | 4.09k | } ret{}; | 965 | | | 966 | | // There is a benign data race here, as the GC can read a pointer while | 967 | | // it's being modified by the mutator; however, the following rules we | 968 | | // obey prevent it from being a problem: | 969 | | // * The only things being modified that the GC reads are the GCPointers | 970 | | // and GCHermesValue in an object. All other fields are ignored. | 971 | | // * Those fields are fewer than 64 bits. | 972 | | // * Therefore, on 64-bit platforms, those fields are atomic | 973 | | // automatically. | 974 | | // * On 32-bit platforms, we don't run this code concurrently, and | 975 | | // instead yield cooperatively with the mutator. | 976 | | // * Thanks to the write barrier, if something is modified its old value | 977 | | // is placed in the globalWorklist, so we don't miss marking it. | 978 | | // * Since the global worklist is pushed onto *before* the write | 979 | | // happens, we know that there's no way the loop will exit unless it | 980 | | // reads the old value. | 981 | | // * If it observes the old value (pre-write-barrier value) here, the | 982 | | // new value will still be live, either by being pre-marked by the | 983 | | // allocator, or because something else's write barrier will catch | 984 | | // the modification. | 985 | 4.09k | TsanIgnoreReadsBegin(); | 986 | | | 987 | | // The cast to a volatile variable forces a read from valRef, since | 988 | | // reads from volatile variables are considered observable behaviour. This | 989 | | // prevents the compiler from optimizing away the returned value, | 990 | | // guaranteeing that we will not observe changes to the underlying value | 991 | | // past this point. Not using volatile here could lead to a TOCTOU bug, | 992 | | // because the underlying value may change after a pointer check (in the | 993 | | // case of HermesValue) or a null check (for pointers). | 994 | 4.09k | ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); | 995 | 4.09k | TsanIgnoreReadsEnd(); | 996 | 4.09k | return ret.val; | 997 | 4.09k | } |
hermes::vm::HermesValue hermes::vm::HadesGC::MarkAcceptor::concurrentReadImpl<hermes::vm::HermesValue>(hermes::vm::HermesValue const&) Line | Count | Source | 957 | 38 | T concurrentReadImpl(const T &valRef) { | 958 | 38 | using Storage = | 959 | 38 | typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; | 960 | 38 | static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); | 961 | 38 | union { | 962 | 38 | Storage storage{}; | 963 | 38 | T val; | 964 | 38 | } ret{}; | 965 | | | 966 | | // There is a benign data race here, as the GC can read a pointer while | 967 | | // it's being modified by the mutator; however, the following rules we | 968 | | // obey prevent it from being a problem: | 969 | | // * The only things being modified that the GC reads are the GCPointers | 970 | | // and GCHermesValue in an object. All other fields are ignored. | 971 | | // * Those fields are fewer than 64 bits. | 972 | | // * Therefore, on 64-bit platforms, those fields are atomic | 973 | | // automatically. | 974 | | // * On 32-bit platforms, we don't run this code concurrently, and | 975 | | // instead yield cooperatively with the mutator. | 976 | | // * Thanks to the write barrier, if something is modified its old value | 977 | | // is placed in the globalWorklist, so we don't miss marking it. | 978 | | // * Since the global worklist is pushed onto *before* the write | 979 | | // happens, we know that there's no way the loop will exit unless it | 980 | | // reads the old value. | 981 | | // * If it observes the old value (pre-write-barrier value) here, the | 982 | | // new value will still be live, either by being pre-marked by the | 983 | | // allocator, or because something else's write barrier will catch | 984 | | // the modification. | 985 | 38 | TsanIgnoreReadsBegin(); | 986 | | | 987 | | // The cast to a volatile variable forces a read from valRef, since | 988 | | // reads from volatile variables are considered observable behaviour. This | 989 | | // prevents the compiler from optimizing away the returned value, | 990 | | // guaranteeing that we will not observe changes to the underlying value | 991 | | // past this point. Not using volatile here could lead to a TOCTOU bug, | 992 | | // because the underlying value may change after a pointer check (in the | 993 | | // case of HermesValue) or a null check (for pointers). | 994 | 38 | ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); | 995 | 38 | TsanIgnoreReadsEnd(); | 996 | 38 | return ret.val; | 997 | 38 | } |
hermes::vm::HermesValue32 hermes::vm::HadesGC::MarkAcceptor::concurrentReadImpl<hermes::vm::HermesValue32>(hermes::vm::HermesValue32 const&) Line | Count | Source | 957 | 1.88k | T concurrentReadImpl(const T &valRef) { | 958 | 1.88k | using Storage = | 959 | 1.88k | typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; | 960 | 1.88k | static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); | 961 | 1.88k | union { | 962 | 1.88k | Storage storage{}; | 963 | 1.88k | T val; | 964 | 1.88k | } ret{}; | 965 | | | 966 | | // There is a benign data race here, as the GC can read a pointer while | 967 | | // it's being modified by the mutator; however, the following rules we | 968 | | // obey prevent it from being a problem: | 969 | | // * The only things being modified that the GC reads are the GCPointers | 970 | | // and GCHermesValue in an object. All other fields are ignored. | 971 | | // * Those fields are fewer than 64 bits. | 972 | | // * Therefore, on 64-bit platforms, those fields are atomic | 973 | | // automatically. | 974 | | // * On 32-bit platforms, we don't run this code concurrently, and | 975 | | // instead yield cooperatively with the mutator. | 976 | | // * Thanks to the write barrier, if something is modified its old value | 977 | | // is placed in the globalWorklist, so we don't miss marking it. | 978 | | // * Since the global worklist is pushed onto *before* the write | 979 | | // happens, we know that there's no way the loop will exit unless it | 980 | | // reads the old value. | 981 | | // * If it observes the old value (pre-write-barrier value) here, the | 982 | | // new value will still be live, either by being pre-marked by the | 983 | | // allocator, or because something else's write barrier will catch | 984 | | // the modification. | 985 | 1.88k | TsanIgnoreReadsBegin(); | 986 | | | 987 | | // The cast to a volatile variable forces a read from valRef, since | 988 | | // reads from volatile variables are considered observable behaviour. This | 989 | | // prevents the compiler from optimizing away the returned value, | 990 | | // guaranteeing that we will not observe changes to the underlying value | 991 | | // past this point. Not using volatile here could lead to a TOCTOU bug, | 992 | | // because the underlying value may change after a pointer check (in the | 993 | | // case of HermesValue) or a null check (for pointers). | 994 | 1.88k | ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); | 995 | 1.88k | TsanIgnoreReadsEnd(); | 996 | 1.88k | return ret.val; | 997 | 1.88k | } |
hermes::vm::SymbolID hermes::vm::HadesGC::MarkAcceptor::concurrentReadImpl<hermes::vm::SymbolID>(hermes::vm::SymbolID const&) Line | Count | Source | 957 | 1.25k | T concurrentReadImpl(const T &valRef) { | 958 | 1.25k | using Storage = | 959 | 1.25k | typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type; | 960 | 1.25k | static_assert(sizeof(T) == sizeof(Storage), "Sizes must match"); | 961 | 1.25k | union { | 962 | 1.25k | Storage storage{}; | 963 | 1.25k | T val; | 964 | 1.25k | } ret{}; | 965 | | | 966 | | // There is a benign data race here, as the GC can read a pointer while | 967 | | // it's being modified by the mutator; however, the following rules we | 968 | | // obey prevent it from being a problem: | 969 | | // * The only things being modified that the GC reads are the GCPointers | 970 | | // and GCHermesValue in an object. All other fields are ignored. | 971 | | // * Those fields are fewer than 64 bits. | 972 | | // * Therefore, on 64-bit platforms, those fields are atomic | 973 | | // automatically. | 974 | | // * On 32-bit platforms, we don't run this code concurrently, and | 975 | | // instead yield cooperatively with the mutator. | 976 | | // * Thanks to the write barrier, if something is modified its old value | 977 | | // is placed in the globalWorklist, so we don't miss marking it. | 978 | | // * Since the global worklist is pushed onto *before* the write | 979 | | // happens, we know that there's no way the loop will exit unless it | 980 | | // reads the old value. | 981 | | // * If it observes the old value (pre-write-barrier value) here, the | 982 | | // new value will still be live, either by being pre-marked by the | 983 | | // allocator, or because something else's write barrier will catch | 984 | | // the modification. | 985 | 1.25k | TsanIgnoreReadsBegin(); | 986 | | | 987 | | // The cast to a volatile variable forces a read from valRef, since | 988 | | // reads from volatile variables are considered observable behaviour. This | 989 | | // prevents the compiler from optimizing away the returned value, | 990 | | // guaranteeing that we will not observe changes to the underlying value | 991 | | // past this point. Not using volatile here could lead to a TOCTOU bug, | 992 | | // because the underlying value may change after a pointer check (in the | 993 | | // case of HermesValue) or a null check (for pointers). | 994 | 1.25k | ret.storage = *reinterpret_cast<Storage const volatile *>(&valRef); | 995 | 1.25k | TsanIgnoreReadsEnd(); | 996 | 1.25k | return ret.val; | 997 | 1.25k | } |
|
998 | | |
999 | | template <typename T> |
1000 | 7.26k | T concurrentRead(const T &ref) { |
1001 | 7.26k | return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; |
1002 | 7.26k | } hermes::vm::CompressedPointer hermes::vm::HadesGC::MarkAcceptor::concurrentRead<hermes::vm::CompressedPointer>(hermes::vm::CompressedPointer const&) Line | Count | Source | 1000 | 4.09k | T concurrentRead(const T &ref) { | 1001 | 4.09k | return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; | 1002 | 4.09k | } |
hermes::vm::HermesValue hermes::vm::HadesGC::MarkAcceptor::concurrentRead<hermes::vm::HermesValue>(hermes::vm::HermesValue const&) Line | Count | Source | 1000 | 38 | T concurrentRead(const T &ref) { | 1001 | 38 | return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; | 1002 | 38 | } |
hermes::vm::HermesValue32 hermes::vm::HadesGC::MarkAcceptor::concurrentRead<hermes::vm::HermesValue32>(hermes::vm::HermesValue32 const&) Line | Count | Source | 1000 | 1.88k | T concurrentRead(const T &ref) { | 1001 | 1.88k | return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; | 1002 | 1.88k | } |
hermes::vm::SymbolID hermes::vm::HadesGC::MarkAcceptor::concurrentRead<hermes::vm::SymbolID>(hermes::vm::SymbolID const&) Line | Count | Source | 1000 | 1.25k | T concurrentRead(const T &ref) { | 1001 | 1.25k | return kConcurrentGC ? concurrentReadImpl<T>(ref) : ref; | 1002 | 1.25k | } |
|
1003 | | }; |
1004 | | |
1005 | | /// Mark weak roots separately from the MarkAcceptor since this is done while |
1006 | | /// the world is stopped. |
1007 | | /// Don't use the default weak root acceptor because fine-grained control of |
1008 | | /// writes of compressed pointers is important. |
1009 | | class HadesGC::MarkWeakRootsAcceptor final : public WeakRootAcceptor { |
1010 | | public: |
1011 | 1 | MarkWeakRootsAcceptor(HadesGC &gc) : gc_{gc} {} |
1012 | | |
1013 | 607 | void acceptWeak(WeakRootBase &wr) override { |
1014 | 607 | if (!wr) { |
1015 | 1 | return; |
1016 | 1 | } |
1017 | 606 | GCCell *const cell = wr.getNoBarrierUnsafe(gc_.getPointerBase()); |
1018 | 606 | HERMES_SLOW_ASSERT(gc_.dbgContains(cell) && "ptr not in heap"); |
1019 | 606 | if (HeapSegment::getCellMarkBit(cell)) { |
1020 | | // If the cell is marked, no need to do any writes. |
1021 | 531 | return; |
1022 | 531 | } |
1023 | | // Reset weak root if target GCCell is dead. |
1024 | 75 | wr = nullptr; |
1025 | 75 | } |
1026 | | |
1027 | | private: |
1028 | | HadesGC &gc_; |
1029 | | }; |
1030 | | |
1031 | | class HadesGC::Executor { |
1032 | | public: |
1033 | 94 | Executor() : thread_([this] { worker(); }) {} |
1034 | 94 | ~Executor() { |
1035 | 94 | { |
1036 | 94 | std::lock_guard<std::mutex> lk(mtx_); |
1037 | 94 | shutdown_ = true; |
1038 | 94 | cv_.notify_one(); |
1039 | 94 | } |
1040 | 94 | thread_.join(); |
1041 | 94 | } |
1042 | | |
1043 | | /// Enqueue the function \p fn for execution on the executor thread. |
1044 | | template <typename Fn> |
1045 | 1 | void add(Fn &&fn) { |
1046 | 1 | std::lock_guard<std::mutex> lk(mtx_); |
1047 | 1 | queue_.emplace_back(std::forward<Fn>(fn)); |
1048 | 1 | cv_.notify_one(); |
1049 | 1 | } |
1050 | | |
1051 | | /// Get the ID of the executor thread. |
1052 | 5.13M | std::thread::id getThreadId() const { |
1053 | 5.13M | return thread_.get_id(); |
1054 | 5.13M | } |
1055 | | |
1056 | | private: |
1057 | | /// Drain enqueued tasks from the queue and run them. |
1058 | 94 | void worker() { |
1059 | 94 | oscompat::set_thread_name("hades"); |
1060 | 94 | std::unique_lock<std::mutex> lk(mtx_); |
1061 | 189 | while (!shutdown_) { |
1062 | 190 | cv_.wait(lk, [this]() { return !queue_.empty() || shutdown_; }); |
1063 | 96 | while (!queue_.empty()) { |
1064 | 1 | auto fn = std::move(queue_.front()); |
1065 | 1 | queue_.pop_front(); |
1066 | 1 | lk.unlock(); |
1067 | 1 | fn(); |
1068 | 1 | lk.lock(); |
1069 | 1 | } |
1070 | 95 | } |
1071 | 94 | } |
1072 | | |
1073 | | std::mutex mtx_; |
1074 | | std::condition_variable cv_; |
1075 | | std::deque<std::function<void()>> queue_; |
1076 | | bool shutdown_{false}; |
1077 | | std::thread thread_; |
1078 | | }; |
1079 | | |
1080 | 1 | bool HadesGC::OldGen::sweepNext(bool backgroundThread) { |
1081 | | // Check if there are any more segments to sweep. Note that in the case where |
1082 | | // OG has zero segments, this also skips updating the stats and survival ratio |
1083 | | // at the end of this function, since they are not required. |
1084 | 1 | if (!sweepIterator_.segNumber) |
1085 | 0 | return false; |
1086 | 1 | assert(gc_.gcMutex_ && "gcMutex_ must be held while sweeping."); |
1087 | | |
1088 | 1 | sweepIterator_.segNumber--; |
1089 | | |
1090 | 1 | const bool isTracking = gc_.isTrackingIDs(); |
1091 | | // Re-evaluate this start point each time, as releasing the gcMutex_ allows |
1092 | | // allocations into the old gen, which might boost the credited memory. |
1093 | 1 | const uint64_t externalBytesBefore = externalBytes(); |
1094 | | |
1095 | 1 | auto &segBuckets = segmentBuckets_[sweepIterator_.segNumber]; |
1096 | | |
1097 | | // Clear the head pointers and remove this segment from the segment level |
1098 | | // freelists, so that we can construct a new freelist. The |
1099 | | // freelistBucketBitArray_ will be updated after the segment is swept. The |
1100 | | // bits will be inconsistent with the actual freelist for the duration of |
1101 | | // sweeping, but this is fine because gcMutex_ is held during the entire |
1102 | | // period. |
1103 | 268 | for (size_t bucket = 0; bucket < kNumFreelistBuckets; bucket++) { |
1104 | 267 | auto *segBucket = &segBuckets[bucket]; |
1105 | 267 | if (segBucket->head) { |
1106 | 1 | segBucket->removeFromFreelist(); |
1107 | 1 | segBucket->head = nullptr; |
1108 | 1 | } |
1109 | 267 | } |
1110 | | |
1111 | 1 | char *freeRangeStart = nullptr, *freeRangeEnd = nullptr; |
1112 | 1 | size_t mergedCells = 0; |
1113 | 1 | int32_t segmentSweptBytes = 0; |
1114 | 2.56k | for (GCCell *cell : segments_[sweepIterator_.segNumber].cells()) { |
1115 | 2.56k | assert(cell->isValid() && "Invalid cell in sweeping"); |
1116 | 2.56k | if (HeapSegment::getCellMarkBit(cell)) { |
1117 | | // Cannot concurrently trim storage. Technically just checking |
1118 | | // backgroundThread would suffice, but the kConcurrentGC lets us compile |
1119 | | // away this check in incremental mode. |
1120 | 2.48k | if (kConcurrentGC && backgroundThread) |
1121 | 0 | continue; |
1122 | 2.48k | const uint32_t cellSize = cell->getAllocatedSize(); |
1123 | 2.48k | const uint32_t trimmedSize = |
1124 | 2.48k | cell->getVT()->getTrimmedSize(cell, cellSize); |
1125 | 2.48k | assert(cellSize >= trimmedSize && "Growing objects is not supported."); |
1126 | 2.48k | assert( |
1127 | 2.48k | trimmedSize >= minAllocationSize() && |
1128 | 2.48k | "Trimmed object must still meet minimum size."); |
1129 | 2.48k | assert( |
1130 | 2.48k | isSizeHeapAligned(trimmedSize) && |
1131 | 2.48k | "Trimmed size must also be heap aligned"); |
1132 | 2.48k | const uint32_t trimmableBytes = cellSize - trimmedSize; |
1133 | | |
1134 | | // If this cell has extra space we can trim, trim it. |
1135 | 2.48k | if (LLVM_UNLIKELY(trimmableBytes >= minAllocationSize())) { |
1136 | 10 | static_cast<VariableSizeRuntimeCell *>(cell)->setSizeFromGC( |
1137 | 10 | trimmedSize); |
1138 | 10 | GCCell *newCell = cell->nextCell(); |
1139 | | // Just create a FillerCell, the next iteration will free it. |
1140 | 10 | constructCell<FillerCell>(newCell, trimmableBytes); |
1141 | 10 | assert( |
1142 | 10 | !HeapSegment::getCellMarkBit(newCell) && |
1143 | 10 | "Trimmed space cannot be marked"); |
1144 | 10 | HeapSegment::setCellHead(newCell, trimmableBytes); |
1145 | 10 | #ifndef NDEBUG |
1146 | 10 | sweepIterator_.trimmedBytes += trimmableBytes; |
1147 | 10 | #endif |
1148 | 10 | } |
1149 | 2.48k | continue; |
1150 | 2.48k | } |
1151 | | |
1152 | 78 | const auto sz = cell->getAllocatedSize(); |
1153 | 78 | char *const cellCharPtr = reinterpret_cast<char *>(cell); |
1154 | | |
1155 | 78 | if (freeRangeEnd != cellCharPtr) { |
1156 | 71 | assert( |
1157 | 71 | freeRangeEnd < cellCharPtr && |
1158 | 71 | "Should not overshoot the start of an object"); |
1159 | | // We are starting a new free range, flush the previous one. |
1160 | 71 | if (LLVM_LIKELY(freeRangeStart)) |
1161 | 70 | addCellToFreelistFromSweep( |
1162 | 70 | freeRangeStart, freeRangeEnd, segBuckets, mergedCells > 1); |
1163 | | |
1164 | 71 | mergedCells = 0; |
1165 | 71 | freeRangeEnd = freeRangeStart = cellCharPtr; |
1166 | 71 | } |
1167 | | // Expand the current free range to include the current cell. |
1168 | 78 | freeRangeEnd += sz; |
1169 | 78 | mergedCells++; |
1170 | | |
1171 | 78 | if (vmisa<FreelistCell>(cell)) |
1172 | 1 | continue; |
1173 | | |
1174 | 77 | segmentSweptBytes += sz; |
1175 | | // Cell is dead, run its finalizer first if it has one. |
1176 | 77 | cell->getVT()->finalizeIfExists(cell, gc_); |
1177 | 77 | if (isTracking && !vmisa<FillerCell>(cell)) { |
1178 | 0 | gc_.untrackObject(cell, sz); |
1179 | 0 | } |
1180 | 77 | } |
1181 | | |
1182 | | // Flush any free range that was left over. |
1183 | 1 | if (freeRangeStart) |
1184 | 1 | addCellToFreelistFromSweep( |
1185 | 1 | freeRangeStart, freeRangeEnd, segBuckets, mergedCells > 1); |
1186 | | |
1187 | | // Update the segment level freelists for any buckets that this segment has |
1188 | | // free cells for. |
1189 | 268 | for (size_t bucket = 0; bucket < kNumFreelistBuckets; ++bucket) { |
1190 | 267 | auto *segBucket = &segBuckets[bucket]; |
1191 | 267 | if (segBucket->head) |
1192 | 12 | segBucket->addToFreelist(&buckets_[bucket]); |
1193 | | |
1194 | | // In case sweeping has changed the availability of a bucket, update the |
1195 | | // overall bit array. Note that this is necessary even if segBucket->head is |
1196 | | // null, as the bits were not updated when the freelist for this segment was |
1197 | | // erased prior to sweeping. |
1198 | 267 | freelistBucketBitArray_.set(bucket, buckets_[bucket].next); |
1199 | 267 | } |
1200 | | |
1201 | | // Correct the allocated byte count. |
1202 | 1 | incrementAllocatedBytes(-segmentSweptBytes); |
1203 | 1 | sweepIterator_.sweptBytes += segmentSweptBytes; |
1204 | 1 | sweepIterator_.sweptExternalBytes += externalBytesBefore - externalBytes(); |
1205 | | |
1206 | | // There are more iterations to go. |
1207 | 1 | if (sweepIterator_.segNumber) |
1208 | 0 | return true; |
1209 | | |
1210 | | // This was the last sweep iteration, finish the collection. |
1211 | 1 | auto &stats = *gc_.ogCollectionStats_; |
1212 | | |
1213 | 1 | auto sweptBytes = sweepIterator_.sweptBytes; |
1214 | 1 | auto preAllocated = stats.beforeAllocatedBytes(); |
1215 | 1 | if (sweptBytes > preAllocated) { |
1216 | | // Only trimming can result in freeing more memory than was allocated at the |
1217 | | // start of the collection, since we may trim cells that were allocated |
1218 | | // after the collection started. |
1219 | 0 | assert(sweepIterator_.trimmedBytes >= (sweptBytes - preAllocated)); |
1220 | | // We can't precisely calculate how much of the trimmed memory came from |
1221 | | // cells allocated during the collection, so just cap the swept bytes at the |
1222 | | // number of initially allocated bytes. |
1223 | 0 | sweptBytes = preAllocated; |
1224 | 0 | } |
1225 | 1 | stats.setSweptBytes(sweptBytes); |
1226 | 1 | stats.setSweptExternalBytes(sweepIterator_.sweptExternalBytes); |
1227 | 1 | const uint64_t targetSizeBytes = |
1228 | 1 | (stats.afterAllocatedBytes() + stats.afterExternalBytes()) / |
1229 | 1 | gc_.occupancyTarget_; |
1230 | | |
1231 | | // In a very large heap, use the configured max heap size as a backstop to |
1232 | | // prevent the target size crossing it (which would delay collection and cause |
1233 | | // an OOM). This is just an approximation, a precise accounting would subtract |
1234 | | // segment metadata and YG memory. |
1235 | 1 | uint64_t clampedSizeBytes = std::min(targetSizeBytes, gc_.maxHeapSize_); |
1236 | 1 | targetSizeBytes_.update(clampedSizeBytes); |
1237 | 1 | sweepIterator_ = {}; |
1238 | 1 | return false; |
1239 | 1 | } |
1240 | | |
1241 | 1 | void HadesGC::OldGen::initializeSweep() { |
1242 | 1 | assert( |
1243 | 1 | !sweepIterator_.segNumber && !sweepIterator_.sweptBytes && |
1244 | 1 | !sweepIterator_.sweptExternalBytes && "Sweep is already in progress."); |
1245 | 1 | sweepIterator_.segNumber = segments_.size(); |
1246 | 1 | } |
1247 | | |
1248 | 0 | size_t HadesGC::OldGen::sweepSegmentsRemaining() const { |
1249 | 0 | return sweepIterator_.segNumber; |
1250 | 0 | } |
1251 | | |
1252 | 0 | size_t HadesGC::OldGen::getMemorySize() const { |
1253 | 0 | size_t memorySize = segments_.size() * sizeof(HeapSegment); |
1254 | 0 | memorySize += segmentBuckets_.size() * sizeof(SegmentBuckets); |
1255 | 0 | return memorySize; |
1256 | 0 | } |
1257 | | |
1258 | | // Assume about 30% of the YG will survive initially. |
1259 | | constexpr double kYGInitialSurvivalRatio = 0.3; |
1260 | | |
1261 | 94 | HadesGC::OldGen::OldGen(HadesGC &gc) : gc_(gc) {} |
1262 | | |
1263 | | HadesGC::HadesGC( |
1264 | | GCCallbacks &gcCallbacks, |
1265 | | PointerBase &pointerBase, |
1266 | | const GCConfig &gcConfig, |
1267 | | std::shared_ptr<CrashManager> crashMgr, |
1268 | | std::shared_ptr<StorageProvider> provider, |
1269 | | experiments::VMExperimentFlags vmExperimentFlags) |
1270 | 94 | : GCBase( |
1271 | 94 | gcCallbacks, |
1272 | 94 | pointerBase, |
1273 | 94 | gcConfig, |
1274 | 94 | std::move(crashMgr), |
1275 | 94 | HeapKind::HadesGC), |
1276 | 94 | maxHeapSize_{std::max<uint64_t>( |
1277 | 94 | gcConfig.getMaxHeapSize(), |
1278 | | // At least one YG segment and one OG segment. |
1279 | 94 | 2 * AlignedStorage::size())}, |
1280 | 94 | provider_(std::move(provider)), |
1281 | 94 | oldGen_{*this}, |
1282 | 94 | backgroundExecutor_{ |
1283 | 94 | kConcurrentGC ? std::make_unique<Executor>() : nullptr}, |
1284 | 94 | promoteYGToOG_{!gcConfig.getAllocInYoung()}, |
1285 | 94 | revertToYGAtTTI_{gcConfig.getRevertToYGAtTTI()}, |
1286 | 94 | overwriteDeadYGObjects_{gcConfig.getOverwriteDeadYGObjects()}, |
1287 | 94 | occupancyTarget_(gcConfig.getOccupancyTarget()), |
1288 | 94 | ygAverageSurvivalBytes_{/*weight*/ 0.5, |
1289 | 94 | /*init*/ kYGInitialSizeFactor * |
1290 | 94 | HeapSegment::maxSize() * |
1291 | 94 | kYGInitialSurvivalRatio} { |
1292 | 94 | (void)vmExperimentFlags; |
1293 | 94 | std::lock_guard<Mutex> lk(gcMutex_); |
1294 | 94 | crashMgr_->setCustomData("HermesGC", getKindAsStr().c_str()); |
1295 | | // createSegment relies on member variables and should not be called until |
1296 | | // they are initialised. |
1297 | 94 | llvh::ErrorOr<HeapSegment> newYoungGen = createSegment(); |
1298 | 94 | if (!newYoungGen) |
1299 | 0 | hermes_fatal("Failed to initialize the young gen", newYoungGen.getError()); |
1300 | 94 | setYoungGen(std::move(newYoungGen.get())); |
1301 | 94 | const size_t initHeapSize = std::max<uint64_t>( |
1302 | 94 | {gcConfig.getMinHeapSize(), |
1303 | 94 | gcConfig.getInitHeapSize(), |
1304 | 94 | HeapSegment::maxSize()}); |
1305 | 94 | oldGen_.setTargetSizeBytes(initHeapSize - HeapSegment::maxSize()); |
1306 | 94 | } |
1307 | | |
1308 | 94 | HadesGC::~HadesGC() { |
1309 | | // finalizeAll calls waitForCollectionToFinish, so there should be no ongoing |
1310 | | // collection. |
1311 | 94 | assert( |
1312 | 94 | concurrentPhase_ == Phase::None && |
1313 | 94 | "Must call finalizeAll before destructor."); |
1314 | 94 | } |
1315 | | |
1316 | 0 | void HadesGC::getHeapInfo(HeapInfo &info) { |
1317 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1318 | 0 | GCBase::getHeapInfo(info); |
1319 | 0 | info.allocatedBytes = allocatedBytes(); |
1320 | | // Heap size includes fragmentation, which means every segment is fully used. |
1321 | 0 | info.heapSize = (oldGen_.numSegments() + 1) * AlignedStorage::size(); |
1322 | | // If YG isn't empty, its bytes haven't been accounted for yet, add them here. |
1323 | 0 | info.totalAllocatedBytes = totalAllocatedBytes_ + youngGen().used(); |
1324 | 0 | info.va = info.heapSize; |
1325 | 0 | info.externalBytes = oldGen_.externalBytes() + getYoungGenExternalBytes(); |
1326 | 0 | info.youngGenStats = ygCumulativeStats_; |
1327 | 0 | info.fullStats = ogCumulativeStats_; |
1328 | 0 | info.numCompactions = numCompactions_; |
1329 | 0 | } |
1330 | | |
1331 | 0 | void HadesGC::getHeapInfoWithMallocSize(HeapInfo &info) { |
1332 | | // Get the usual heap info. |
1333 | 0 | getHeapInfo(info); |
1334 | | // Get GCBase's malloc size estimate. |
1335 | 0 | GCBase::getHeapInfoWithMallocSize(info); |
1336 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1337 | | // First add the usage by the runtime's roots. |
1338 | 0 | info.mallocSizeEstimate += gcCallbacks_.mallocSize(); |
1339 | | // Scan all objects for their malloc size. This operation is what makes |
1340 | | // getHeapInfoWithMallocSize O(heap size). |
1341 | 0 | forAllObjs([&info](GCCell *cell) { |
1342 | 0 | info.mallocSizeEstimate += cell->getVT()->getMallocSize(cell); |
1343 | 0 | }); |
1344 | 0 | } |
1345 | | |
1346 | | void HadesGC::getCrashManagerHeapInfo( |
1347 | 0 | CrashManager::HeapInformation &crashInfo) { |
1348 | 0 | HeapInfo info; |
1349 | 0 | getHeapInfo(info); |
1350 | 0 | crashInfo.size_ = info.heapSize; |
1351 | 0 | crashInfo.used_ = info.allocatedBytes; |
1352 | 0 | } |
1353 | | |
1354 | | #ifdef HERMES_MEMORY_INSTRUMENTATION |
1355 | 0 | void HadesGC::createSnapshot(llvh::raw_ostream &os, bool captureNumericValue) { |
1356 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1357 | | // No allocations are allowed throughout the entire heap snapshot process. |
1358 | 0 | NoAllocScope scope{*this}; |
1359 | | // Let any existing collections complete before taking the snapshot. |
1360 | 0 | waitForCollectionToFinish("snapshot"); |
1361 | 0 | { |
1362 | 0 | GCCycle cycle{*this, "GC Heap Snapshot"}; |
1363 | 0 | GCBase::createSnapshot(*this, os, captureNumericValue); |
1364 | 0 | } |
1365 | 0 | } |
1366 | | |
1367 | 0 | void HadesGC::snapshotAddGCNativeNodes(HeapSnapshot &snap) { |
1368 | 0 | GCBase::snapshotAddGCNativeNodes(snap); |
1369 | 0 | if (nativeIDs_.ygFinalizables == IDTracker::kInvalidNode) { |
1370 | 0 | nativeIDs_.ygFinalizables = getIDTracker().nextNativeID(); |
1371 | 0 | } |
1372 | 0 | if (nativeIDs_.og == IDTracker::kInvalidNode) { |
1373 | 0 | nativeIDs_.og = getIDTracker().nextNativeID(); |
1374 | 0 | } |
1375 | 0 | snap.beginNode(); |
1376 | 0 | snap.endNode( |
1377 | 0 | HeapSnapshot::NodeType::Native, |
1378 | 0 | "std::vector<GCCell *>", |
1379 | 0 | nativeIDs_.ygFinalizables, |
1380 | 0 | youngGenFinalizables_.capacity() * sizeof(GCCell *), |
1381 | 0 | 0); |
1382 | 0 | snap.beginNode(); |
1383 | 0 | snap.endNode( |
1384 | 0 | HeapSnapshot::NodeType::Native, |
1385 | 0 | "OldGen", |
1386 | 0 | nativeIDs_.og, |
1387 | 0 | sizeof(oldGen_) + oldGen_.getMemorySize(), |
1388 | 0 | 0); |
1389 | 0 | } |
1390 | | |
1391 | 0 | void HadesGC::snapshotAddGCNativeEdges(HeapSnapshot &snap) { |
1392 | 0 | GCBase::snapshotAddGCNativeEdges(snap); |
1393 | | // All native ids should be lazily initialized in snapshotAddGCNativeNodes, |
1394 | | // because that is called first. |
1395 | 0 | assert(nativeIDs_.ygFinalizables != IDTracker::kInvalidNode); |
1396 | 0 | assert(nativeIDs_.og != IDTracker::kInvalidNode); |
1397 | 0 | snap.addNamedEdge( |
1398 | 0 | HeapSnapshot::EdgeType::Internal, |
1399 | 0 | "youngGenFinalizables", |
1400 | 0 | nativeIDs_.ygFinalizables); |
1401 | 0 | snap.addNamedEdge(HeapSnapshot::EdgeType::Internal, "oldGen", nativeIDs_.og); |
1402 | 0 | } |
1403 | | |
1404 | | void HadesGC::enableHeapProfiler( |
1405 | | std::function<void( |
1406 | | uint64_t, |
1407 | | std::chrono::microseconds, |
1408 | | std::vector<GCBase::AllocationLocationTracker::HeapStatsUpdate>)> |
1409 | 0 | fragmentCallback) { |
1410 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1411 | | // Let any existing collections complete before enabling the profiler. |
1412 | 0 | waitForCollectionToFinish("heap profiler enable"); |
1413 | 0 | GCBase::enableHeapProfiler(std::move(fragmentCallback)); |
1414 | 0 | } |
1415 | | |
1416 | 0 | void HadesGC::disableHeapProfiler() { |
1417 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1418 | | // Let any existing collections complete before disabling the profiler. |
1419 | 0 | waitForCollectionToFinish("heap profiler disable"); |
1420 | 0 | GCBase::disableHeapProfiler(); |
1421 | 0 | } |
1422 | | |
1423 | | void HadesGC::enableSamplingHeapProfiler( |
1424 | | size_t samplingInterval, |
1425 | 0 | int64_t seed) { |
1426 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1427 | | // Let any existing collections complete before enabling the profiler. |
1428 | 0 | waitForCollectionToFinish("sampling heap profiler enable"); |
1429 | 0 | GCBase::enableSamplingHeapProfiler(samplingInterval, seed); |
1430 | 0 | } |
1431 | | |
1432 | 0 | void HadesGC::disableSamplingHeapProfiler(llvh::raw_ostream &os) { |
1433 | 0 | std::lock_guard<Mutex> lk{gcMutex_}; |
1434 | | // Let any existing collections complete before disabling the profiler. |
1435 | 0 | waitForCollectionToFinish("sampling heap profiler disable"); |
1436 | 0 | GCBase::disableSamplingHeapProfiler(os); |
1437 | 0 | } |
1438 | | #endif // HERMES_MEMORY_INSTRUMENTATION |
1439 | | |
1440 | 0 | void HadesGC::printStats(JSONEmitter &json) { |
1441 | 0 | GCBase::printStats(json); |
1442 | 0 | json.emitKey("specific"); |
1443 | 0 | json.openDict(); |
1444 | 0 | json.emitKeyValue("collector", getKindAsStr()); |
1445 | 0 | json.emitKey("stats"); |
1446 | 0 | json.openDict(); |
1447 | 0 | json.emitKeyValue("Num compactions", numCompactions_); |
1448 | 0 | json.closeDict(); |
1449 | 0 | json.closeDict(); |
1450 | 0 | } |
1451 | | |
1452 | 129 | std::string HadesGC::getKindAsStr() const { |
1453 | 129 | return kGCName; |
1454 | 129 | } |
1455 | | |
1456 | 1 | void HadesGC::collect(std::string cause, bool /*canEffectiveOOM*/) { |
1457 | 1 | { |
1458 | | // Wait for any existing collections to finish before starting a new one. |
1459 | 1 | std::lock_guard<Mutex> lk{gcMutex_}; |
1460 | | // Disable the YG promotion mode. A forced GC via collect will do a full |
1461 | | // collection immediately anyway, so there's no need to avoid collecting YG. |
1462 | | // This is especially important when the forced GC is a memory warning. |
1463 | 1 | promoteYGToOG_ = false; |
1464 | 1 | waitForCollectionToFinish(cause); |
1465 | 1 | } |
1466 | | // This function should block until a collection finishes. |
1467 | | // YG needs to be empty in order to do an OG collection. |
1468 | 1 | youngGenCollection(cause, /*forceOldGenCollection*/ true); |
1469 | 1 | { |
1470 | | // Wait for the collection to complete. |
1471 | 1 | std::lock_guard<Mutex> lk{gcMutex_}; |
1472 | 1 | waitForCollectionToFinish(cause); |
1473 | 1 | } |
1474 | | // Start a second YG collection to complete any pending compaction. |
1475 | | // Since YG is empty, this will only be evacuating the compactee. |
1476 | | // Note that it's possible for this call to start another OG collection if the |
1477 | | // occupancy target is >= 75%. That doesn't break the contract of this |
1478 | | // function though, and we don't want to bother with waiting for that |
1479 | | // collection to complete because it won't find any garbage anyway. |
1480 | 1 | youngGenCollection(std::move(cause), /*forceOldGenCollection*/ false); |
1481 | 1 | } |
1482 | | |
1483 | 2 | void HadesGC::waitForCollectionToFinish(std::string cause) { |
1484 | 2 | assert( |
1485 | 2 | gcMutex_ && |
1486 | 2 | "gcMutex_ must be held before calling waitForCollectionToFinish"); |
1487 | 2 | if (concurrentPhase_ == Phase::None) { |
1488 | 1 | return; |
1489 | 1 | } |
1490 | 1 | GCCycle cycle{*this, "GC Old Gen (Direct)"}; |
1491 | | |
1492 | 1 | assert(!ygCollectionStats_ && "Cannot collect OG during a YG collection"); |
1493 | 1 | CollectionStats waitingStats(*this, std::move(cause), "waiting"); |
1494 | 1 | waitingStats.beginCPUTimeSection(); |
1495 | 1 | waitingStats.setBeginTime(); |
1496 | | |
1497 | 18 | while (concurrentPhase_ != Phase::None) |
1498 | 17 | incrementalCollect(false); |
1499 | | |
1500 | 1 | waitingStats.endCPUTimeSection(); |
1501 | 1 | waitingStats.setEndTime(); |
1502 | 1 | recordGCStats(std::move(waitingStats).getEvent(), true); |
1503 | 1 | } |
1504 | | |
1505 | 1 | void HadesGC::oldGenCollection(std::string cause, bool forceCompaction) { |
1506 | | // Full collection: |
1507 | | // * Mark all live objects by iterating through a worklist. |
1508 | | // * Sweep dead objects onto the free lists. |
1509 | | // This function must be called while the gcMutex_ is held. |
1510 | 1 | assert(gcMutex_ && "gcMutex_ must be held when starting an OG collection"); |
1511 | 1 | assert( |
1512 | 1 | gcMutex_.depth() == 1 && |
1513 | 1 | "Need ability to release mutex in oldGenCollection."); |
1514 | 1 | assert( |
1515 | 1 | concurrentPhase_ == Phase::None && |
1516 | 1 | "Starting a second old gen collection"); |
1517 | | // Wait for any lingering background task to finish. |
1518 | 1 | if (kConcurrentGC) { |
1519 | | // This is just making sure that any leftover work is completed before |
1520 | | // starting a new collection. Since concurrentPhase_ == None here, there is |
1521 | | // no collection ongoing. However, the background task may need to acquire |
1522 | | // the lock in order to observe the value of concurrentPhase_. |
1523 | 1 | std::unique_lock<Mutex> lk{gcMutex_, std::adopt_lock}; |
1524 | 1 | ogPauseCondVar_.wait(lk, [this] { return !backgroundTaskActive_; }); |
1525 | 1 | lk.release(); |
1526 | 1 | } |
1527 | | // We know ygCollectionStats_ exists because oldGenCollection is only called |
1528 | | // by youngGenCollection. |
1529 | 1 | ygCollectionStats_->addCollectionType("old gen start"); |
1530 | 1 | #ifdef HERMES_SLOW_DEBUG |
1531 | 1 | checkWellFormed(); |
1532 | 1 | #endif |
1533 | 1 | ogCollectionStats_ = |
1534 | 1 | std::make_unique<CollectionStats>(*this, std::move(cause), "old"); |
1535 | | // NOTE: Leave CPU time as zero if the collection isn't concurrent, as the |
1536 | | // times aren't useful. |
1537 | 1 | if (kConcurrentGC) |
1538 | 1 | ogCollectionStats_->beginCPUTimeSection(); |
1539 | 1 | ogCollectionStats_->setBeginTime(); |
1540 | 1 | ogCollectionStats_->setBeforeSizes( |
1541 | 1 | oldGen_.allocatedBytes(), oldGen_.externalBytes(), segmentFootprint()); |
1542 | | |
1543 | | // If we've reached the first OG collection, switch back to YG mode. |
1544 | 1 | promoteYGToOG_ = false; |
1545 | | |
1546 | | // First, clear any mark bits that were set by a previous collection or |
1547 | | // direct-to-OG allocation, they aren't needed anymore. |
1548 | 1 | for (HeapSegment &seg : oldGen_) |
1549 | 1 | seg.markBitArray().clear(); |
1550 | | |
1551 | | // Unmark all symbols in the identifier table, as Symbol liveness will be |
1552 | | // determined during the collection. |
1553 | 1 | gcCallbacks_.unmarkSymbols(); |
1554 | | |
1555 | | // Mark phase: discover all pointers that are live. |
1556 | | // This assignment will reset any leftover memory from the last collection. We |
1557 | | // leave the last marker alive to avoid a race condition with setting |
1558 | | // concurrentPhase_, oldGenMarker_ and the write barrier. |
1559 | 1 | oldGenMarker_.reset(new MarkAcceptor{*this}); |
1560 | 1 | { |
1561 | | // Roots are marked before a marking thread is spun up, so that the root |
1562 | | // marking is atomic. |
1563 | 1 | DroppingAcceptor<MarkAcceptor> nameAcceptor{*oldGenMarker_}; |
1564 | 1 | markRoots(nameAcceptor, /*markLongLived*/ true); |
1565 | | // Do not call markWeakRoots here, as weak roots can only be cleared |
1566 | | // after liveness is known. |
1567 | 1 | } |
1568 | | |
1569 | 1 | concurrentPhase_ = Phase::Mark; |
1570 | | // Before the thread starts up, make sure that any write barriers are aware |
1571 | | // that concurrent marking is happening. |
1572 | 1 | ogMarkingBarriers_ = true; |
1573 | | // prepareCompactee must be called before the new thread is spawned, in order |
1574 | | // to ensure that write barriers start operating immediately, and that any |
1575 | | // objects promoted during an intervening YG collection are correctly scanned. |
1576 | 1 | prepareCompactee(forceCompaction); |
1577 | | |
1578 | | // Setup the sweep iterator when collection begins, because the number of |
1579 | | // segments can change if a YG collection interleaves. There's no need to |
1580 | | // sweep those extra segments since they are full of newly promoted |
1581 | | // objects from YG (which have all their mark bits set), thus the sweep |
1582 | | // iterator doesn't need to be updated. We also don't need to sweep any |
1583 | | // segments made since the start of the collection, since they won't have |
1584 | | // any unmarked objects anyway. |
1585 | | // NOTE: this must be called after prepareCompactee, which may remove segments |
1586 | | // from the heap. |
1587 | 1 | oldGen_.initializeSweep(); |
1588 | | |
1589 | 1 | if (!kConcurrentGC) { |
1590 | | // 32-bit system: 64-bit HermesValues cannot be updated in one atomic |
1591 | | // instruction. Have YG collections interleave marking work. |
1592 | | // In this version of the system, the mark stack will be drained in batches |
1593 | | // during each YG collection. Once the mark stack is fully drained, the rest |
1594 | | // of the collection finishes while blocking a YG GC. This allows |
1595 | | // incremental work without actually using multiple threads. |
1596 | | |
1597 | | // Initialize the drain rate. |
1598 | 0 | oldGenMarker_->setDrainRate(getDrainRate()); |
1599 | 0 | return; |
1600 | 0 | } |
1601 | 1 | ogCollectionStats_->endCPUTimeSection(); |
1602 | | // 64-bit system: 64-bit HermesValues can be updated in one atomic |
1603 | | // instruction. Start up a separate thread for doing marking work. |
1604 | | // NOTE: Since the "this" value (the HadesGC instance) is copied to the |
1605 | | // executor, the GC cannot be destructed until the new thread completes. This |
1606 | | // means that before destroying the GC, waitForCollectionToFinish must be |
1607 | | // called. |
1608 | 1 | collectOGInBackground(); |
1609 | | // Use concurrentPhase_ to be able to tell when the collection finishes. |
1610 | 1 | } |
1611 | | |
1612 | 1 | void HadesGC::collectOGInBackground() { |
1613 | 1 | assert(gcMutex_ && "Must hold GC mutex when scheduling background work."); |
1614 | 1 | assert( |
1615 | 1 | !backgroundTaskActive_ && "Should only have one active task at a time"); |
1616 | 1 | assert(kConcurrentGC && "Background work can only be done in concurrent GC"); |
1617 | | |
1618 | 1 | backgroundTaskActive_ = true; |
1619 | | |
1620 | 1 | backgroundExecutor_->add([this]() { |
1621 | 1 | std::unique_lock<Mutex> lk(gcMutex_); |
1622 | 1 | while (true) { |
1623 | | // If the mutator has requested the background task to stop and yield |
1624 | | // gcMutex_, wait on ogPauseCondVar_ until the mutator acquires the mutex |
1625 | | // and signals that we may resume. |
1626 | 1 | ogPauseCondVar_.wait( |
1627 | 1 | lk, [this] { return !ogPaused_.load(std::memory_order_relaxed); }); |
1628 | 1 | assert( |
1629 | 1 | backgroundTaskActive_ && |
1630 | 1 | "backgroundTaskActive_ must be true when the background task is in the loop."); |
1631 | 1 | incrementalCollect(true); |
1632 | 1 | if (concurrentPhase_ == Phase::None || |
1633 | 1 | concurrentPhase_ == Phase::CompleteMarking) { |
1634 | 1 | backgroundTaskActive_ = false; |
1635 | | // Signal the mutator to let it know that this task is complete, in case |
1636 | | // it is waiting. |
1637 | 1 | ogPauseCondVar_.notify_one(); |
1638 | 1 | break; |
1639 | 1 | } |
1640 | 1 | } |
1641 | 1 | }); |
1642 | 1 | } |
1643 | | |
1644 | 230k | std::lock_guard<Mutex> HadesGC::pauseBackgroundTask() { |
1645 | 230k | assert(kConcurrentGC && "Should not be called in incremental mode"); |
1646 | 230k | assert(!calledByBackgroundThread() && "Must be called from mutator"); |
1647 | | // Signal to the background thread that it should stop and wait on |
1648 | | // ogPauseCondVar_. |
1649 | 230k | ogPaused_.store(true, std::memory_order_relaxed); |
1650 | | // Acquire gcMutex_ as soon as it is released by the background thread. |
1651 | 230k | gcMutex_.lock(); |
1652 | | // Signal to the background thread that it may resume. Note that it will just |
1653 | | // go to wait on gcMutex_, since it is currently held by this thread. |
1654 | 230k | ogPaused_.store(false, std::memory_order_relaxed); |
1655 | 230k | ogPauseCondVar_.notify_one(); |
1656 | 230k | return std::lock_guard(gcMutex_, std::adopt_lock); |
1657 | 230k | } |
1658 | | |
1659 | 18 | void HadesGC::incrementalCollect(bool backgroundThread) { |
1660 | 18 | assert(gcMutex_ && "Must hold the GC mutex when calling incrementalCollect"); |
1661 | 18 | switch (concurrentPhase_) { |
1662 | 1 | case Phase::None: |
1663 | 1 | break; |
1664 | 15 | case Phase::Mark: |
1665 | 15 | if (!kConcurrentGC && ygCollectionStats_) |
1666 | 0 | ygCollectionStats_->addCollectionType("marking"); |
1667 | | // Drain some work from the mark worklist. If the work has finished |
1668 | | // completely, move on to CompleteMarking. |
1669 | 15 | if (!oldGenMarker_->drainSomeWork()) |
1670 | 1 | concurrentPhase_ = Phase::CompleteMarking; |
1671 | 15 | break; |
1672 | 1 | case Phase::CompleteMarking: |
1673 | | // Background task should exit, the mutator will restart it after the STW |
1674 | | // pause. |
1675 | 1 | if (!backgroundThread) { |
1676 | 1 | if (ygCollectionStats_) |
1677 | 0 | ygCollectionStats_->addCollectionType("complete marking"); |
1678 | 1 | completeMarking(); |
1679 | 1 | concurrentPhase_ = Phase::Sweep; |
1680 | 1 | } |
1681 | 1 | break; |
1682 | 1 | case Phase::Sweep: |
1683 | 1 | if (!kConcurrentGC && ygCollectionStats_) |
1684 | 0 | ygCollectionStats_->addCollectionType("sweeping"); |
1685 | | // Calling oldGen_.sweepNext() will sweep the next segment. |
1686 | 1 | if (!oldGen_.sweepNext(backgroundThread)) { |
1687 | | // Finish any collection bookkeeping. |
1688 | 1 | ogCollectionStats_->setEndTime(); |
1689 | 1 | ogCollectionStats_->setAfterSize(segmentFootprint()); |
1690 | 1 | concurrentPhase_ = Phase::None; |
1691 | 1 | if (!backgroundThread) |
1692 | 1 | checkTripwireAndSubmitStats(); |
1693 | 1 | } |
1694 | 1 | break; |
1695 | 0 | default: |
1696 | 0 | llvm_unreachable("No other possible state between iterations"); |
1697 | 18 | } |
1698 | 18 | } |
1699 | | |
1700 | 1 | void HadesGC::prepareCompactee(bool forceCompaction) { |
1701 | 1 | assert(gcMutex_); |
1702 | 1 | assert( |
1703 | 1 | compactee_.empty() && |
1704 | 1 | "Ongoing compaction at the start of an OG collection."); |
1705 | 1 | if (promoteYGToOG_) |
1706 | 0 | return; |
1707 | | |
1708 | | #ifdef HERMESVM_SANITIZE_HANDLES |
1709 | | // Handle-SAN forces a compaction to move some OG objects. |
1710 | | if (sanitizeRate_) |
1711 | | forceCompaction = true; |
1712 | | #endif |
1713 | | |
1714 | | // To avoid compacting too often, keep a buffer of one segment or 5% of the |
1715 | | // heap (whichever is greater). Since the selected segment will be removed |
1716 | | // from the heap, we only want to compact if there are at least 2 segments in |
1717 | | // the OG. |
1718 | 1 | uint64_t buffer = std::max<uint64_t>( |
1719 | 1 | oldGen_.targetSizeBytes() / 20, HeapSegment::maxSize()); |
1720 | 1 | uint64_t threshold = oldGen_.targetSizeBytes() + buffer; |
1721 | 1 | uint64_t totalBytes = oldGen_.size() + oldGen_.externalBytes(); |
1722 | 1 | if ((forceCompaction || totalBytes > threshold) && |
1723 | 1 | oldGen_.numSegments() > 1) { |
1724 | 0 | compactee_.segment = std::make_shared<HeapSegment>(oldGen_.popSegment()); |
1725 | 0 | addSegmentExtentToCrashManager( |
1726 | 0 | *compactee_.segment, kCompacteeNameForCrashMgr); |
1727 | 0 | compactee_.start = compactee_.segment->lowLim(); |
1728 | 0 | compactee_.startCP = CompressedPointer::encodeNonNull( |
1729 | 0 | reinterpret_cast<GCCell *>(compactee_.segment->lowLim()), |
1730 | 0 | getPointerBase()); |
1731 | 0 | } |
1732 | 1 | } |
1733 | | |
1734 | 0 | void HadesGC::finalizeCompactee() { |
1735 | 0 | char *stop = compactee_.segment->level(); |
1736 | 0 | char *cur = compactee_.segment->start(); |
1737 | 0 | PointerBase &base = getPointerBase(); |
1738 | | // Calculate the total number of bytes that were allocated in the compactee at |
1739 | | // the start of compaction. |
1740 | 0 | int32_t preAllocated = 0; |
1741 | 0 | while (cur < stop) { |
1742 | 0 | auto *cell = reinterpret_cast<GCCell *>(cur); |
1743 | 0 | if (cell->hasMarkedForwardingPointer()) { |
1744 | 0 | auto size = cell->getMarkedForwardingPointer() |
1745 | 0 | .getNonNull(base) |
1746 | 0 | ->getAllocatedSize(); |
1747 | 0 | preAllocated += size; |
1748 | 0 | cur += size; |
1749 | 0 | } else { |
1750 | 0 | auto size = cell->getAllocatedSize(); |
1751 | 0 | if (!vmisa<OldGen::FreelistCell>(cell)) { |
1752 | 0 | cell->getVT()->finalizeIfExists(cell, *this); |
1753 | 0 | preAllocated += size; |
1754 | 0 | } |
1755 | 0 | cur += size; |
1756 | 0 | } |
1757 | 0 | } |
1758 | | // At this point, any cells that survived compaction are already accounted for |
1759 | | // separately in the counter, so we just need to subtract the number of bytes |
1760 | | // allocated in the compactee. |
1761 | 0 | oldGen_.incrementAllocatedBytes(-preAllocated); |
1762 | |
|
1763 | 0 | const size_t segIdx = |
1764 | 0 | SegmentInfo::segmentIndexFromStart(compactee_.segment->lowLim()); |
1765 | 0 | segmentIndices_.push_back(segIdx); |
1766 | 0 | removeSegmentExtentFromCrashManager(std::to_string(segIdx)); |
1767 | 0 | removeSegmentExtentFromCrashManager(kCompacteeNameForCrashMgr); |
1768 | 0 | compactee_ = {}; |
1769 | 0 | } |
1770 | | |
1771 | 1 | void HadesGC::updateOldGenThreshold() { |
1772 | 1 | const double markedBytes = oldGenMarker_->markedBytes(); |
1773 | 1 | const double preAllocated = ogCollectionStats_->beforeAllocatedBytes(); |
1774 | 1 | assert(markedBytes <= preAllocated && "Cannot mark more than was allocated"); |
1775 | 1 | const double postAllocated = oldGen_.allocatedBytes(); |
1776 | 1 | assert(postAllocated >= preAllocated && "Cannot free memory during marking"); |
1777 | | |
1778 | | // Calculate the number of bytes marked for each byte allocated into the old |
1779 | | // generation. Note that this is skewed heavily by the number of young gen |
1780 | | // collections that occur during marking, and therefore the size of the heap. |
1781 | | // 1. In small heaps, this underestimates the true mark rate, because marking |
1782 | | // may finish shortly after it starts, but we have to wait until the next YG |
1783 | | // collection is complete. This is desirable, because we need a more |
1784 | | // conservative margin in small heaps to avoid running over the heap limit. |
1785 | | // 2. In large heaps, this approaches the true mark rate, because there will |
1786 | | // be several young gen collections, giving us more accurate and finer grained |
1787 | | // information on the allocation rate. |
1788 | 1 | const auto concurrentMarkRate = |
1789 | 1 | markedBytes / std::max(postAllocated - preAllocated, 1.0); |
1790 | | |
1791 | | // If the heap is completely full and we are running into blocking |
1792 | | // collections, then it is possible that almost nothing is allocated into the |
1793 | | // OG during the mark phase. Without correction, can become a self-reinforcing |
1794 | | // cycle because it will cause the mark rate to be overestimated, making |
1795 | | // collections start later, further increasing the odds of a blocking |
1796 | | // collection. Empirically, the mark rate can be much higher than the below |
1797 | | // limit, but we get diminishing returns with increasing mark rate, since the |
1798 | | // threshold just asymptotically approaches 1. |
1799 | 1 | const auto clampedRate = std::min(concurrentMarkRate, 20.0); |
1800 | | |
1801 | | // Update the collection threshold using the newly computed mark rate. To add |
1802 | | // a margin of safety, we assume everything in the heap at the time we hit the |
1803 | | // threshold needs to be marked. This margin allows for variance in the |
1804 | | // marking rate, and gives time for sweeping to start. The update is |
1805 | | // calculated by viewing the threshold as the bytes to mark and the remainder |
1806 | | // after the threshold as the bytes to fill. We can then solve for it using: |
1807 | | // MarkRate = Threshold / (1 - Threshold) |
1808 | | // This has some nice properties: |
1809 | | // 1. As the threshold increases, the safety margin also increases (since the |
1810 | | // safety margin is just the difference between the threshold and the |
1811 | | // occupancy ratio). |
1812 | | // 2. It neatly fits the range [0, 1) for a mark rate in [0, infinity). There |
1813 | | // is no risk of division by zero. |
1814 | 1 | ogThreshold_.update(clampedRate / (clampedRate + 1)); |
1815 | 1 | } |
1816 | | |
1817 | 1 | void HadesGC::markWeakMapEntrySlots() { |
1818 | 1 | bool newlyMarkedValue; |
1819 | 1 | do { |
1820 | 1 | newlyMarkedValue = false; |
1821 | 1 | weakMapEntrySlots_.forEach([this](WeakMapEntrySlot &slot) { |
1822 | 0 | if (!slot.key || !slot.owner) |
1823 | 0 | return; |
1824 | 0 | GCCell *ownerMapCell = slot.owner.getNoBarrierUnsafe(getPointerBase()); |
1825 | | // If the owner structure isn't reachable, no need to mark the values. |
1826 | 0 | if (!HeapSegment::getCellMarkBit(ownerMapCell)) |
1827 | 0 | return; |
1828 | 0 | GCCell *cell = slot.key.getNoBarrierUnsafe(getPointerBase()); |
1829 | | // The WeakRef object must be marked for the mapped value to |
1830 | | // be marked (unless there are other strong refs to the value). |
1831 | 0 | if (!HeapSegment::getCellMarkBit(cell)) |
1832 | 0 | return; |
1833 | 0 | oldGenMarker_->accept(slot.mappedValue); |
1834 | 0 | }); |
1835 | 1 | newlyMarkedValue = !oldGenMarker_->isLocalWorklistEmpty(); |
1836 | 1 | oldGenMarker_->drainAllWork(); |
1837 | 1 | } while (newlyMarkedValue); |
1838 | | |
1839 | | // If either a key or its owning map is dead, set the mapped value to Empty. |
1840 | 1 | weakMapEntrySlots_.forEach([this](WeakMapEntrySlot &slot) { |
1841 | 0 | if (!slot.key || !slot.owner) { |
1842 | 0 | slot.mappedValue = HermesValue::encodeEmptyValue(); |
1843 | 0 | return; |
1844 | 0 | } |
1845 | 0 | GCCell *cell = slot.key.getNoBarrierUnsafe(getPointerBase()); |
1846 | 0 | GCCell *ownerMapCell = slot.owner.getNoBarrierUnsafe(getPointerBase()); |
1847 | 0 | if (!HeapSegment::getCellMarkBit(cell) || |
1848 | 0 | !HeapSegment::getCellMarkBit(ownerMapCell)) { |
1849 | 0 | slot.mappedValue = HermesValue::encodeEmptyValue(); |
1850 | 0 | } |
1851 | 0 | }); |
1852 | 1 | } |
1853 | | |
1854 | 1 | void HadesGC::completeMarking() { |
1855 | 1 | assert(inGC() && "inGC_ must be set during the STW pause"); |
1856 | | // Update the collection threshold before marking anything more, so that only |
1857 | | // the concurrently marked bytes are part of the calculation. |
1858 | 1 | updateOldGenThreshold(); |
1859 | 1 | ogMarkingBarriers_ = false; |
1860 | | // No locks are needed here because the world is stopped and there is only 1 |
1861 | | // active thread. |
1862 | 1 | oldGenMarker_->globalWorklist().flushPushChunk(); |
1863 | 1 | { |
1864 | | // Remark any roots that may have changed without executing barriers. |
1865 | 1 | DroppingAcceptor<MarkAcceptor> nameAcceptor{*oldGenMarker_}; |
1866 | 1 | gcCallbacks_.markRootsForCompleteMarking(nameAcceptor); |
1867 | 1 | } |
1868 | | // Drain the marking queue. |
1869 | 1 | oldGenMarker_->drainAllWork(); |
1870 | 1 | assert( |
1871 | 1 | oldGenMarker_->globalWorklist().empty() && |
1872 | 1 | "Marking worklist wasn't drained"); |
1873 | 1 | markWeakMapEntrySlots(); |
1874 | | // Update the compactee tracking pointers so that the next YG collection will |
1875 | | // do a compaction. |
1876 | 1 | compactee_.evacStart = compactee_.start; |
1877 | 1 | compactee_.evacStartCP = compactee_.startCP; |
1878 | 1 | assert( |
1879 | 1 | oldGenMarker_->globalWorklist().empty() && |
1880 | 1 | "Marking worklist wasn't drained"); |
1881 | | // Reset weak roots to null after full reachability has been |
1882 | | // determined. |
1883 | 1 | MarkWeakRootsAcceptor acceptor{*this}; |
1884 | 1 | markWeakRoots(acceptor, /*markLongLived*/ true); |
1885 | | |
1886 | | // Now free symbols and weak refs. |
1887 | 1 | gcCallbacks_.freeSymbols(oldGenMarker_->markedSymbols()); |
1888 | | |
1889 | | // Nothing needs oldGenMarker_ from this point onward. |
1890 | 1 | oldGenMarker_.reset(); |
1891 | 1 | } |
1892 | | |
1893 | 94 | void HadesGC::finalizeAll() { |
1894 | 94 | std::lock_guard<Mutex> lk{gcMutex_}; |
1895 | | // Terminate any existing OG collection. |
1896 | 94 | concurrentPhase_ = Phase::None; |
1897 | 94 | if (ogCollectionStats_) |
1898 | 0 | ogCollectionStats_->markUsed(); |
1899 | | // In case of an OOM, we may be in the middle of a YG collection. |
1900 | 94 | if (ygCollectionStats_) |
1901 | 0 | ygCollectionStats_->markUsed(); |
1902 | | // Now finalize the heap. |
1903 | | // We might be in the middle of a YG collection, with some objects promoted to |
1904 | | // the OG, and some not. Only finalize objects that have not been promoted to |
1905 | | // OG, and let the OG finalize the promoted objects. |
1906 | 94 | finalizeYoungGenObjects(); |
1907 | | |
1908 | | // If we are in the middle of a YG collection, some objects may have already |
1909 | | // been promoted to the OG. Assume that any remaining external memory in the |
1910 | | // YG belongs to those objects. |
1911 | 94 | transferExternalMemoryToOldGen(); |
1912 | | |
1913 | 240k | const auto finalizeCallback = [this](GCCell *cell) { |
1914 | 240k | assert(cell->isValid() && "Invalid cell in finalizeAll"); |
1915 | 240k | cell->getVT()->finalizeIfExists(cell, *this); |
1916 | 240k | }; |
1917 | 94 | if (compactee_.segment) |
1918 | 0 | compactee_.segment->forCompactedObjs(finalizeCallback, getPointerBase()); |
1919 | | |
1920 | 94 | for (HeapSegment &seg : oldGen_) |
1921 | 94 | seg.forAllObjs(finalizeCallback); |
1922 | 94 | } |
1923 | | |
1924 | 1.26k | void HadesGC::creditExternalMemory(GCCell *cell, uint32_t sz) { |
1925 | 1.26k | assert(canAllocExternalMemory(sz) && "Precondition"); |
1926 | 1.26k | if (inYoungGen(cell)) { |
1927 | 1.24k | size_t newYGExtBytes = getYoungGenExternalBytes() + sz; |
1928 | 1.24k | setYoungGenExternalBytes(newYGExtBytes); |
1929 | 1.24k | auto adj = std::min<size_t>(sz, youngGen_.available()); |
1930 | 1.24k | youngGen_.setEffectiveEnd(youngGen_.effectiveEnd() - adj); |
1931 | 1.24k | } else { |
1932 | 19 | std::lock_guard<Mutex> lk{gcMutex_}; |
1933 | 19 | oldGen_.creditExternalMemory(sz); |
1934 | 19 | uint64_t totalBytes = oldGen_.allocatedBytes() + oldGen_.externalBytes(); |
1935 | 19 | if (totalBytes > oldGen_.targetSizeBytes()) |
1936 | 0 | youngGen_.setEffectiveEnd(youngGen_.level()); |
1937 | 19 | } |
1938 | 1.26k | } |
1939 | | |
1940 | 205 | void HadesGC::debitExternalMemory(GCCell *cell, uint32_t sz) { |
1941 | 205 | if (inYoungGen(cell)) { |
1942 | 162 | size_t oldYGExtBytes = getYoungGenExternalBytes(); |
1943 | 162 | assert( |
1944 | 162 | oldYGExtBytes >= sz && "Debiting more native memory than was credited"); |
1945 | 162 | setYoungGenExternalBytes(oldYGExtBytes - sz); |
1946 | | // Don't modify the effective end here. creditExternalMemory is in charge |
1947 | | // of tracking this. We don't expect many debits to not be from finalizers |
1948 | | // anyway. |
1949 | 162 | } else { |
1950 | 43 | std::lock_guard<Mutex> lk{gcMutex_}; |
1951 | 43 | oldGen_.debitExternalMemory(sz); |
1952 | 43 | } |
1953 | 205 | } |
1954 | | |
1955 | 0 | void HadesGC::writeBarrierSlow(const GCHermesValue *loc, HermesValue value) { |
1956 | 0 | if (ogMarkingBarriers_) { |
1957 | 0 | snapshotWriteBarrierInternal(*loc); |
1958 | 0 | } |
1959 | 0 | if (!value.isPointer()) { |
1960 | 0 | return; |
1961 | 0 | } |
1962 | 0 | relocationWriteBarrier(loc, value.getPointer()); |
1963 | 0 | } |
1964 | | |
1965 | | void HadesGC::writeBarrierSlow( |
1966 | | const GCSmallHermesValue *loc, |
1967 | 619k | SmallHermesValue value) { |
1968 | 619k | if (ogMarkingBarriers_) { |
1969 | 0 | snapshotWriteBarrierInternal(*loc); |
1970 | 0 | } |
1971 | 619k | if (!value.isPointer()) { |
1972 | 124k | return; |
1973 | 124k | } |
1974 | 494k | relocationWriteBarrier(loc, value.getPointer(getPointerBase())); |
1975 | 494k | } |
1976 | | |
1977 | 755k | void HadesGC::writeBarrierSlow(const GCPointerBase *loc, const GCCell *value) { |
1978 | 755k | if (*loc && ogMarkingBarriers_) |
1979 | 0 | snapshotWriteBarrierInternal(*loc); |
1980 | | // Always do the non-snapshot write barrier in order for YG to be able to |
1981 | | // scan cards. |
1982 | 755k | relocationWriteBarrier(loc, value); |
1983 | 755k | } |
1984 | | |
1985 | | void HadesGC::constructorWriteBarrierSlow( |
1986 | | const GCHermesValue *loc, |
1987 | 0 | HermesValue value) { |
1988 | | // A constructor never needs to execute a SATB write barrier, since its |
1989 | | // previous value was definitely not live. |
1990 | 0 | if (!value.isPointer()) { |
1991 | 0 | return; |
1992 | 0 | } |
1993 | 0 | relocationWriteBarrier(loc, value.getPointer()); |
1994 | 0 | } |
1995 | | |
1996 | | void HadesGC::constructorWriteBarrierSlow( |
1997 | | const GCSmallHermesValue *loc, |
1998 | 0 | SmallHermesValue value) { |
1999 | | // A constructor never needs to execute a SATB write barrier, since its |
2000 | | // previous value was definitely not live. |
2001 | 0 | if (!value.isPointer()) { |
2002 | 0 | return; |
2003 | 0 | } |
2004 | 0 | relocationWriteBarrier(loc, value.getPointer(getPointerBase())); |
2005 | 0 | } |
2006 | | |
2007 | | void HadesGC::constructorWriteBarrierRangeSlow( |
2008 | | const GCHermesValue *start, |
2009 | 0 | uint32_t numHVs) { |
2010 | 0 | assert( |
2011 | 0 | AlignedStorage::containedInSame(start, start + numHVs) && |
2012 | 0 | "Range must start and end within a heap segment."); |
2013 | | |
2014 | | // Most constructors should be running in the YG, so in the common case, we |
2015 | | // can avoid doing anything for the whole range. If the range is in the OG, |
2016 | | // then just dirty all the cards corresponding to it, and we can scan them for |
2017 | | // pointers later. This is less precise but makes the write barrier faster. |
2018 | | |
2019 | 0 | AlignedHeapSegment::cardTableCovering(start)->dirtyCardsForAddressRange( |
2020 | 0 | start, start + numHVs); |
2021 | 0 | } |
2022 | | |
2023 | | void HadesGC::constructorWriteBarrierRangeSlow( |
2024 | | const GCSmallHermesValue *start, |
2025 | 0 | uint32_t numHVs) { |
2026 | 0 | assert( |
2027 | 0 | AlignedStorage::containedInSame(start, start + numHVs) && |
2028 | 0 | "Range must start and end within a heap segment."); |
2029 | 0 | AlignedHeapSegment::cardTableCovering(start)->dirtyCardsForAddressRange( |
2030 | 0 | start, start + numHVs); |
2031 | 0 | } |
2032 | | |
2033 | | void HadesGC::snapshotWriteBarrierRangeSlow( |
2034 | | const GCHermesValue *start, |
2035 | 0 | uint32_t numHVs) { |
2036 | 0 | for (uint32_t i = 0; i < numHVs; ++i) { |
2037 | 0 | snapshotWriteBarrierInternal(start[i]); |
2038 | 0 | } |
2039 | 0 | } |
2040 | | |
2041 | | void HadesGC::snapshotWriteBarrierRangeSlow( |
2042 | | const GCSmallHermesValue *start, |
2043 | 0 | uint32_t numHVs) { |
2044 | 0 | for (uint32_t i = 0; i < numHVs; ++i) { |
2045 | 0 | snapshotWriteBarrierInternal(start[i]); |
2046 | 0 | } |
2047 | 0 | } |
2048 | | |
2049 | 0 | void HadesGC::snapshotWriteBarrierInternal(GCCell *oldValue) { |
2050 | 0 | assert( |
2051 | 0 | (oldValue->isValid()) && |
2052 | 0 | "Invalid cell encountered in snapshotWriteBarrier"); |
2053 | 0 | if (!inYoungGen(oldValue)) { |
2054 | 0 | HERMES_SLOW_ASSERT( |
2055 | 0 | dbgContains(oldValue) && |
2056 | 0 | "Non-heap pointer encountered in snapshotWriteBarrier"); |
2057 | 0 | oldGenMarker_->globalWorklist().enqueue(oldValue); |
2058 | 0 | } |
2059 | 0 | } |
2060 | | |
2061 | 0 | void HadesGC::snapshotWriteBarrierInternal(CompressedPointer oldValue) { |
2062 | 0 | assert( |
2063 | 0 | (oldValue.get(getPointerBase())->isValid()) && |
2064 | 0 | "Invalid cell encountered in snapshotWriteBarrier"); |
2065 | 0 | if (!inYoungGen(oldValue)) { |
2066 | 0 | GCCell *ptr = oldValue.get(getPointerBase()); |
2067 | 0 | HERMES_SLOW_ASSERT( |
2068 | 0 | dbgContains(ptr) && |
2069 | 0 | "Non-heap pointer encountered in snapshotWriteBarrier"); |
2070 | 0 | oldGenMarker_->globalWorklist().enqueue(ptr); |
2071 | 0 | } |
2072 | 0 | } |
2073 | | |
2074 | 0 | void HadesGC::snapshotWriteBarrierInternal(HermesValue oldValue) { |
2075 | 0 | if (oldValue.isPointer()) { |
2076 | 0 | snapshotWriteBarrierInternal(static_cast<GCCell *>(oldValue.getPointer())); |
2077 | 0 | } else if (oldValue.isSymbol()) { |
2078 | | // Symbols need snapshot write barriers. |
2079 | 0 | snapshotWriteBarrierInternal(oldValue.getSymbol()); |
2080 | 0 | } |
2081 | 0 | } |
2082 | | |
2083 | 0 | void HadesGC::snapshotWriteBarrierInternal(SmallHermesValue oldValue) { |
2084 | 0 | if (oldValue.isPointer()) { |
2085 | 0 | snapshotWriteBarrierInternal(oldValue.getPointer()); |
2086 | 0 | } else if (oldValue.isSymbol()) { |
2087 | | // Symbols need snapshot write barriers. |
2088 | 0 | snapshotWriteBarrierInternal(oldValue.getSymbol()); |
2089 | 0 | } |
2090 | 0 | } |
2091 | | |
2092 | 0 | void HadesGC::snapshotWriteBarrierInternal(SymbolID symbol) { |
2093 | 0 | HERMES_SLOW_ASSERT( |
2094 | 0 | ogMarkingBarriers_ && |
2095 | 0 | "snapshotWriteBarrier should only be called while the OG is marking"); |
2096 | 0 | oldGenMarker_->markSymbol(symbol); |
2097 | 0 | } |
2098 | | |
2099 | 1.30M | void HadesGC::relocationWriteBarrier(const void *loc, const void *value) { |
2100 | 1.30M | assert(!inYoungGen(loc) && "Pre-condition from other callers"); |
2101 | | // Do not dirty cards for compactee->compactee, yg->yg, or yg->compactee |
2102 | | // pointers. But do dirty cards for compactee->yg pointers, since compaction |
2103 | | // may not happen in the next YG. |
2104 | 1.30M | if (AlignedStorage::containedInSame(loc, value)) { |
2105 | 127k | return; |
2106 | 127k | } |
2107 | 1.17M | if (inYoungGen(value) || compactee_.contains(value)) { |
2108 | | // Only dirty a card if it's an old-to-young or old-to-compactee pointer. |
2109 | | // This is fine to do since the GC never modifies card tables outside of |
2110 | | // allocation. |
2111 | | // Note that this *only* applies since the boundaries are updated separately |
2112 | | // from the card table being marked itself. |
2113 | 1.17M | HeapSegment::cardTableCovering(loc)->dirtyCardForAddress(loc); |
2114 | 1.17M | } |
2115 | 1.17M | } |
2116 | | |
2117 | 0 | void HadesGC::weakRefReadBarrier(HermesValue value) { |
2118 | 0 | assert( |
2119 | 0 | !calledByBackgroundThread() && |
2120 | 0 | "Read barrier invoked by background thread."); |
2121 | 0 | if (ogMarkingBarriers_) |
2122 | 0 | snapshotWriteBarrierInternal(value); |
2123 | 0 | } |
2124 | | |
2125 | 365k | void HadesGC::weakRefReadBarrier(GCCell *value) { |
2126 | 365k | assert( |
2127 | 365k | !calledByBackgroundThread() && |
2128 | 365k | "Read barrier invoked by background thread."); |
2129 | | // If the GC is marking, conservatively mark the value as live. |
2130 | 365k | if (ogMarkingBarriers_) |
2131 | 0 | snapshotWriteBarrierInternal(value); |
2132 | | |
2133 | | // Otherwise, if no GC is active at all, the weak ref must be alive. |
2134 | | // During sweeping there's no special handling either. |
2135 | 365k | } |
2136 | | |
2137 | 2.52k | bool HadesGC::canAllocExternalMemory(uint32_t size) { |
2138 | 2.52k | return size <= maxHeapSize_; |
2139 | 2.52k | } |
2140 | | |
2141 | 133 | void HadesGC::forAllObjs(const std::function<void(GCCell *)> &callback) { |
2142 | 133 | std::lock_guard<Mutex> lk{gcMutex_}; |
2143 | 133 | youngGen().forAllObjs(callback); |
2144 | | |
2145 | 133 | const auto skipGarbageCallback = [callback](GCCell *cell) { |
2146 | | // If we're doing this check during sweeping, or between sweeping and |
2147 | | // compaction, there might be some objects that are dead, and could |
2148 | | // potentially have garbage in them. There's no need to check the |
2149 | | // pointers of those objects. |
2150 | 0 | if (HeapSegment::getCellMarkBit(cell)) { |
2151 | 0 | callback(cell); |
2152 | 0 | } |
2153 | 0 | }; |
2154 | 133 | for (HeapSegment &seg : oldGen_) { |
2155 | 133 | if (concurrentPhase_ != Phase::Sweep) |
2156 | 133 | seg.forAllObjs(callback); |
2157 | 0 | else |
2158 | 0 | seg.forAllObjs(skipGarbageCallback); |
2159 | 133 | } |
2160 | 133 | if (compactee_.segment) { |
2161 | 0 | if (!compactee_.evacActive()) |
2162 | 0 | compactee_.segment->forAllObjs(callback); |
2163 | 0 | else |
2164 | 0 | compactee_.segment->forAllObjs(skipGarbageCallback); |
2165 | 0 | } |
2166 | 133 | } |
2167 | | |
2168 | 0 | void HadesGC::ttiReached() { |
2169 | 0 | if (revertToYGAtTTI_) |
2170 | 0 | promoteYGToOG_ = false; |
2171 | 0 | } |
2172 | | |
2173 | | #ifndef NDEBUG |
2174 | | |
2175 | 5.13M | bool HadesGC::calledByBackgroundThread() const { |
2176 | | // If the background thread is active, check if this thread matches the |
2177 | | // background thread. |
2178 | 5.13M | return kConcurrentGC && |
2179 | 5.13M | backgroundExecutor_->getThreadId() == std::this_thread::get_id(); |
2180 | 5.13M | } |
2181 | | |
2182 | 5.29M | bool HadesGC::validPointer(const void *p) const { |
2183 | 5.29M | return dbgContains(p) && static_cast<const GCCell *>(p)->isValid(); |
2184 | 5.29M | } |
2185 | | |
2186 | 5.29M | bool HadesGC::dbgContains(const void *p) const { |
2187 | 5.29M | return inYoungGen(p) || inOldGen(p); |
2188 | 5.29M | } |
2189 | | |
2190 | 0 | void HadesGC::trackReachable(CellKind kind, unsigned sz) {} |
2191 | | |
2192 | 367k | bool HadesGC::needsWriteBarrier(void *loc, GCCell *value) { |
2193 | 367k | return !inYoungGen(loc); |
2194 | 367k | } |
2195 | | #endif |
2196 | | |
2197 | 31 | void *HadesGC::allocSlow(uint32_t sz) { |
2198 | 31 | AllocResult res; |
2199 | | // Failed to alloc in young gen, do a young gen collection. |
2200 | 31 | youngGenCollection( |
2201 | 31 | kNaturalCauseForAnalytics, /*forceOldGenCollection*/ false); |
2202 | 31 | res = youngGen().alloc(sz); |
2203 | 31 | if (res.success) |
2204 | 31 | return res.ptr; |
2205 | | |
2206 | | // Still fails after YG collection, perhaps it is a large alloc, try growing |
2207 | | // the YG to full size. |
2208 | 0 | youngGen().clearExternalMemoryCharge(); |
2209 | 0 | res = youngGen().alloc(sz); |
2210 | 0 | if (res.success) |
2211 | 0 | return res.ptr; |
2212 | | |
2213 | | // A YG collection is guaranteed to fully evacuate, leaving all the space |
2214 | | // available, so the only way this could fail is if sz is greater than |
2215 | | // a segment size. |
2216 | | // This would be an error in VM code to ever allow such a size to be |
2217 | | // allocated, and thus there's an assert at the top of this function to |
2218 | | // disallow that. This case is for production, if we miss a test case. |
2219 | 0 | oom(make_error_code(OOMError::SuperSegmentAlloc)); |
2220 | 0 | } |
2221 | | |
2222 | 230k | void *HadesGC::allocLongLived(uint32_t sz) { |
2223 | 230k | assert( |
2224 | 230k | isSizeHeapAligned(sz) && |
2225 | 230k | "Call to allocLongLived must use a size aligned to HeapAlign"); |
2226 | 230k | assert(gcMutex_ && "GC mutex must be held when calling allocLongLived"); |
2227 | 230k | totalAllocatedBytes_ += sz; |
2228 | | // Alloc directly into the old gen. |
2229 | 230k | return oldGen_.alloc(sz); |
2230 | 230k | } |
2231 | | |
2232 | 240k | GCCell *HadesGC::OldGen::alloc(uint32_t sz) { |
2233 | 240k | assert( |
2234 | 240k | isSizeHeapAligned(sz) && |
2235 | 240k | "Should be aligned before entering this function"); |
2236 | 240k | assert(sz >= minAllocationSize() && "Allocating too small of an object"); |
2237 | 240k | assert(sz <= maxAllocationSize() && "Allocating too large of an object"); |
2238 | 240k | assert(gc_.gcMutex_ && "gcMutex_ must be held before calling oldGenAlloc"); |
2239 | 240k | if (GCCell *cell = search(sz)) { |
2240 | 240k | return cell; |
2241 | 240k | } |
2242 | | // Before waiting for a collection to finish, check if we're below the max |
2243 | | // heap size and can simply allocate another segment. This will prevent |
2244 | | // blocking the YG unnecessarily. |
2245 | 94 | llvh::ErrorOr<HeapSegment> seg = gc_.createSegment(); |
2246 | 94 | if (seg) { |
2247 | | // Complete this allocation using a bump alloc. |
2248 | 94 | AllocResult res = seg->alloc(sz); |
2249 | 94 | assert( |
2250 | 94 | res.success && |
2251 | 94 | "A newly created segment should always be able to allocate"); |
2252 | | // Set the cell head for any successful alloc, so that write barriers can |
2253 | | // move from dirty cards to the head of the object. |
2254 | 94 | seg->setCellHead(static_cast<GCCell *>(res.ptr), sz); |
2255 | | // Add the segment to segments_ and add the remainder of the segment to the |
2256 | | // free list. |
2257 | 94 | addSegment(std::move(seg.get())); |
2258 | 94 | GCCell *newObj = static_cast<GCCell *>(res.ptr); |
2259 | 94 | HeapSegment::setCellMarkBit(newObj); |
2260 | 94 | return newObj; |
2261 | 94 | } |
2262 | | |
2263 | | // TODO(T109282643): Block on any pending OG collections here in case they |
2264 | | // free up space. |
2265 | | |
2266 | | // Repeat the search in case the collection did free memory. |
2267 | 0 | if (GCCell *cell = search(sz)) { |
2268 | 0 | return cell; |
2269 | 0 | } |
2270 | | |
2271 | | // The GC didn't recover enough memory, OOM. |
2272 | | // Re-use the error code from the earlier heap segment allocation, because |
2273 | | // it's either that the max heap size was reached, or that segment failed to |
2274 | | // allocate. |
2275 | 0 | gc_.oom(seg.getError()); |
2276 | 0 | } |
2277 | | |
2278 | 962k | uint32_t HadesGC::OldGen::getFreelistBucket(uint32_t size) { |
2279 | | // If the size corresponds to the "small" portion of the freelist, then the |
2280 | | // bucket is just (size) / (heap alignment) |
2281 | 962k | if (size < kMinSizeForLargeBlock) { |
2282 | 480k | auto bucket = size >> LogHeapAlign; |
2283 | 480k | assert( |
2284 | 480k | bucket < kNumSmallFreelistBuckets && |
2285 | 480k | "Small blocks must be within the small free list range"); |
2286 | 480k | return bucket; |
2287 | 480k | } |
2288 | | // Otherwise, index into the large portion of the freelist, which is based on |
2289 | | // powers of 2 |
2290 | | |
2291 | 482k | auto bucket = |
2292 | 482k | kNumSmallFreelistBuckets + llvh::Log2_32(size) - kLogMinSizeForLargeBlock; |
2293 | 482k | assert( |
2294 | 482k | bucket < kNumFreelistBuckets && |
2295 | 482k | "Block size must be within the freelist range!"); |
2296 | 482k | return bucket; |
2297 | 482k | } |
2298 | | |
2299 | 240k | GCCell *HadesGC::OldGen::search(uint32_t sz) { |
2300 | 240k | size_t bucket = getFreelistBucket(sz); |
2301 | 240k | if (bucket < kNumSmallFreelistBuckets) { |
2302 | | // Fast path: There already exists a size bucket for this alloc. Check if |
2303 | | // there's a free cell to take and exit. |
2304 | 240k | if (auto *segBucket = buckets_[bucket].next) { |
2305 | 3 | assert(freelistBucketBitArray_.at(bucket)); |
2306 | 3 | FreelistCell *cell = removeCellFromFreelist(bucket, segBucket); |
2307 | 3 | assert( |
2308 | 3 | cell->getAllocatedSize() == sz && |
2309 | 3 | "Size bucket should be an exact match"); |
2310 | 3 | return finishAlloc(cell, sz); |
2311 | 3 | } |
2312 | | // Make sure we start searching at the smallest possible size that could fit |
2313 | 240k | bucket = getFreelistBucket(sz + minAllocationSize()); |
2314 | 240k | } |
2315 | | // Once we're examining the rest of the free list, it's a first-fit algorithm. |
2316 | | // This approach approximates finding the smallest possible fit. |
2317 | 240k | bucket = freelistBucketBitArray_.findNextSetBitFrom(bucket); |
2318 | 240k | for (; bucket < kNumFreelistBuckets; |
2319 | 240k | bucket = freelistBucketBitArray_.findNextSetBitFrom(bucket + 1)) { |
2320 | 240k | auto *segBucket = buckets_[bucket].next; |
2321 | 240k | do { |
2322 | | // Need to track the previous entry in order to change the next pointer. |
2323 | 240k | AssignableCompressedPointer *prevLoc = &segBucket->head; |
2324 | 240k | AssignableCompressedPointer cellCP = segBucket->head; |
2325 | | |
2326 | 240k | do { |
2327 | 240k | auto *cell = |
2328 | 240k | vmcast<FreelistCell>(cellCP.getNonNull(gc_.getPointerBase())); |
2329 | 240k | assert( |
2330 | 240k | cellCP == *prevLoc && |
2331 | 240k | "prevLoc should be updated in each iteration"); |
2332 | 240k | assert( |
2333 | 240k | (!cell->next_ || |
2334 | 240k | cell->next_.getNonNull(gc_.getPointerBase())->isValid()) && |
2335 | 240k | "Next pointer points to an invalid cell"); |
2336 | 240k | const auto cellSize = cell->getAllocatedSize(); |
2337 | 240k | assert( |
2338 | 240k | getFreelistBucket(cellSize) == bucket && |
2339 | 240k | "Found an incorrectly sized block in this bucket"); |
2340 | | // Check if the size is large enough that the cell could be split. |
2341 | 240k | if (cellSize >= sz + minAllocationSize()) { |
2342 | | // Split the free cell. In order to avoid initializing |
2343 | | // soon-to-be-unused values like the size and the next pointer, copy |
2344 | | // the return path here. |
2345 | 240k | auto newCell = cell->carve(sz); |
2346 | | // Since the size of cell has changed, we may need to add it to a |
2347 | | // different free list bucket. |
2348 | 240k | auto newBucket = getFreelistBucket(cell->getAllocatedSize()); |
2349 | 240k | assert(newBucket <= bucket && "Split cell must be smaller."); |
2350 | 240k | if (newBucket != bucket) { |
2351 | 5 | removeCellFromFreelist(prevLoc, bucket, segBucket); |
2352 | | // Since the buckets for each segment are stored contiguously in |
2353 | | // memory, we can compute the address of the SegmentBucket for |
2354 | | // newBucket in this segment relative to the current SegmentBucket. |
2355 | 5 | auto diff = bucket - newBucket; |
2356 | 5 | addCellToFreelist(cell, segBucket - diff); |
2357 | 5 | } |
2358 | | // Because we carved newCell out before removing cell from the |
2359 | | // freelist, newCell is still poisoned (regardless of whether the |
2360 | | // conditional above executed). Unpoison it. |
2361 | 240k | __asan_unpoison_memory_region(newCell, sz); |
2362 | 240k | return finishAlloc(newCell, sz); |
2363 | 240k | } else if (cellSize == sz) { |
2364 | | // Exact match, take it. |
2365 | 0 | removeCellFromFreelist(prevLoc, bucket, segBucket); |
2366 | 0 | return finishAlloc(cell, sz); |
2367 | 0 | } |
2368 | | // Non-exact matches, or anything just barely too small to fit, will |
2369 | | // need to find another block. |
2370 | | // NOTE: This is due to restrictions on the minimum cell size to keep |
2371 | | // the heap parseable, especially in debug mode. If this minimum size |
2372 | | // becomes smaller (smaller header, size becomes part of it |
2373 | | // automatically, debug magic field is handled differently), this |
2374 | | // decisions can be re-examined. An example alternative is to make a |
2375 | | // special fixed-size cell that is only as big as an empty GCCell. That |
2376 | | // alternative only works if the empty is small enough to fit in any gap |
2377 | | // in the heap. That's not true in debug modes currently. |
2378 | 0 | prevLoc = &cell->next_; |
2379 | 0 | cellCP = cell->next_; |
2380 | 0 | } while (cellCP); |
2381 | 0 | segBucket = segBucket->next; |
2382 | 0 | } while (segBucket); |
2383 | 240k | } |
2384 | 94 | return nullptr; |
2385 | 240k | } |
2386 | | |
2387 | | template <typename Acceptor> |
2388 | 33 | void HadesGC::youngGenEvacuateImpl(Acceptor &acceptor, bool doCompaction) { |
2389 | | // Marking each object puts it onto an embedded free list. |
2390 | 33 | { |
2391 | 33 | DroppingAcceptor<Acceptor> nameAcceptor{acceptor}; |
2392 | 33 | markRoots(nameAcceptor, /*markLongLived*/ doCompaction); |
2393 | | |
2394 | | // Mark the values in WeakMap entries as roots for the purposes of young gen |
2395 | | // collection. This is slightly suboptimal since some of the keys or maps |
2396 | | // may be dead, but we still end up evacuating their corresponding value. |
2397 | | // This is done for simplicity, since it lets us avoid needing to iterate to |
2398 | | // a fixed point as is done during a full collection. |
2399 | 33 | weakMapEntrySlots_.forEach([&nameAcceptor](WeakMapEntrySlot &slot) { |
2400 | 0 | nameAcceptor.accept(slot.mappedValue); |
2401 | 0 | }); Unexecuted instantiation: hermes::vm::HadesGC::youngGenEvacuateImpl<hermes::vm::HadesGC::EvacAcceptor<true> >(hermes::vm::HadesGC::EvacAcceptor<true>&, bool)::{lambda(hermes::vm::WeakMapEntrySlot&)#1}::operator()(hermes::vm::WeakMapEntrySlot&) constUnexecuted instantiation: hermes::vm::HadesGC::youngGenEvacuateImpl<hermes::vm::HadesGC::EvacAcceptor<false> >(hermes::vm::HadesGC::EvacAcceptor<false>&, bool)::{lambda(hermes::vm::WeakMapEntrySlot&)#1}::operator()(hermes::vm::WeakMapEntrySlot&) const |
2402 | 33 | } |
2403 | | // Find old-to-young pointers, as they are considered roots for YG |
2404 | | // collection. |
2405 | 33 | scanDirtyCards(acceptor); |
2406 | | // Iterate through the copy list to find new pointers. |
2407 | 10.5k | while (CopyListCell *const copyCell = acceptor.pop()) { |
2408 | 10.5k | assert( |
2409 | 10.5k | copyCell->hasMarkedForwardingPointer() && "Discovered unmarked object"); |
2410 | 10.5k | assert( |
2411 | 10.5k | (inYoungGen(copyCell) || compactee_.evacContains(copyCell)) && |
2412 | 10.5k | "Unexpected object in YG collection"); |
2413 | | // Update the pointers inside the forwarded object, since the old |
2414 | | // object is only there for the forwarding pointer. |
2415 | 10.5k | GCCell *const cell = |
2416 | 10.5k | copyCell->getMarkedForwardingPointer().getNonNull(getPointerBase()); |
2417 | 10.5k | markCell(cell, acceptor); |
2418 | 10.5k | } |
2419 | | |
2420 | | // Mark weak roots. We only need to update the long lived weak roots if we are |
2421 | | // evacuating part of the OG. |
2422 | 33 | markWeakRoots(acceptor, /*markLongLived*/ doCompaction); |
2423 | 33 | } Unexecuted instantiation: void hermes::vm::HadesGC::youngGenEvacuateImpl<hermes::vm::HadesGC::EvacAcceptor<true> >(hermes::vm::HadesGC::EvacAcceptor<true>&, bool) void hermes::vm::HadesGC::youngGenEvacuateImpl<hermes::vm::HadesGC::EvacAcceptor<false> >(hermes::vm::HadesGC::EvacAcceptor<false>&, bool) Line | Count | Source | 2388 | 33 | void HadesGC::youngGenEvacuateImpl(Acceptor &acceptor, bool doCompaction) { | 2389 | | // Marking each object puts it onto an embedded free list. | 2390 | 33 | { | 2391 | 33 | DroppingAcceptor<Acceptor> nameAcceptor{acceptor}; | 2392 | 33 | markRoots(nameAcceptor, /*markLongLived*/ doCompaction); | 2393 | | | 2394 | | // Mark the values in WeakMap entries as roots for the purposes of young gen | 2395 | | // collection. This is slightly suboptimal since some of the keys or maps | 2396 | | // may be dead, but we still end up evacuating their corresponding value. | 2397 | | // This is done for simplicity, since it lets us avoid needing to iterate to | 2398 | | // a fixed point as is done during a full collection. | 2399 | 33 | weakMapEntrySlots_.forEach([&nameAcceptor](WeakMapEntrySlot &slot) { | 2400 | 33 | nameAcceptor.accept(slot.mappedValue); | 2401 | 33 | }); | 2402 | 33 | } | 2403 | | // Find old-to-young pointers, as they are considered roots for YG | 2404 | | // collection. | 2405 | 33 | scanDirtyCards(acceptor); | 2406 | | // Iterate through the copy list to find new pointers. | 2407 | 10.5k | while (CopyListCell *const copyCell = acceptor.pop()) { | 2408 | 10.5k | assert( | 2409 | 10.5k | copyCell->hasMarkedForwardingPointer() && "Discovered unmarked object"); | 2410 | 10.5k | assert( | 2411 | 10.5k | (inYoungGen(copyCell) || compactee_.evacContains(copyCell)) && | 2412 | 10.5k | "Unexpected object in YG collection"); | 2413 | | // Update the pointers inside the forwarded object, since the old | 2414 | | // object is only there for the forwarding pointer. | 2415 | 10.5k | GCCell *const cell = | 2416 | 10.5k | copyCell->getMarkedForwardingPointer().getNonNull(getPointerBase()); | 2417 | 10.5k | markCell(cell, acceptor); | 2418 | 10.5k | } | 2419 | | | 2420 | | // Mark weak roots. We only need to update the long lived weak roots if we are | 2421 | | // evacuating part of the OG. | 2422 | 33 | markWeakRoots(acceptor, /*markLongLived*/ doCompaction); | 2423 | 33 | } |
|
2424 | | |
2425 | | void HadesGC::youngGenCollection( |
2426 | | std::string cause, |
2427 | 33 | bool forceOldGenCollection) { |
2428 | 33 | ygCollectionStats_ = std::make_unique<CollectionStats>(*this, cause, "young"); |
2429 | 33 | ygCollectionStats_->beginCPUTimeSection(); |
2430 | 33 | ygCollectionStats_->setBeginTime(); |
2431 | | // Acquire the GC lock for the duration of the YG collection. |
2432 | 33 | auto lk = ensureBackgroundTaskPaused(); |
2433 | | // The YG is not parseable while a collection is occurring. |
2434 | 33 | assert(!inGC() && "Cannot be in GC at the start of YG!"); |
2435 | 33 | GCCycle cycle{*this, "GC Young Gen"}; |
2436 | 33 | #ifdef HERMES_SLOW_DEBUG |
2437 | 33 | checkWellFormed(); |
2438 | | // Check that the card tables are well-formed before the collection. |
2439 | 33 | verifyCardTable(); |
2440 | 33 | #endif |
2441 | 33 | assert( |
2442 | 33 | youngGen().markBitArray().findNextUnmarkedBitFrom(0) == |
2443 | 33 | youngGen().markBitArray().size() && |
2444 | 33 | "Young gen segment must have all mark bits set"); |
2445 | 33 | struct { |
2446 | 33 | uint64_t before{0}; |
2447 | 33 | uint64_t after{0}; |
2448 | 33 | } heapBytes, externalBytes; |
2449 | 33 | heapBytes.before = youngGen().used(); |
2450 | 33 | externalBytes.before = getYoungGenExternalBytes(); |
2451 | | // YG is about to be emptied, add all of the allocations. |
2452 | 33 | totalAllocatedBytes_ += youngGen().used(); |
2453 | 33 | const bool doCompaction = compactee_.evacActive(); |
2454 | | // Attempt to promote the YG segment to OG if the flag is set. If this call |
2455 | | // fails for any reason, proceed with a GC. |
2456 | 33 | if (promoteYoungGenToOldGen()) { |
2457 | | // Leave sweptBytes and sweptExternalBytes as defaults (which are 0). |
2458 | | // Don't update the average YG survival ratio since no liveness was |
2459 | | // calculated for the promotion case. |
2460 | 0 | ygCollectionStats_->setBeforeSizes( |
2461 | 0 | heapBytes.before, externalBytes.before, segmentFootprint()); |
2462 | 0 | ygCollectionStats_->addCollectionType("promotion"); |
2463 | 0 | assert(!doCompaction && "Cannot do compactions during YG promotions."); |
2464 | 33 | } else { |
2465 | 33 | auto &yg = youngGen(); |
2466 | | |
2467 | 33 | if (compactee_.segment) { |
2468 | 0 | EvacAcceptor<true> acceptor{*this}; |
2469 | 0 | youngGenEvacuateImpl(acceptor, doCompaction); |
2470 | | // The remaining bytes after the collection is just the number of bytes |
2471 | | // that were evacuated. |
2472 | 0 | heapBytes.after = acceptor.evacuatedBytes(); |
2473 | 33 | } else { |
2474 | 33 | EvacAcceptor<false> acceptor{*this}; |
2475 | 33 | youngGenEvacuateImpl(acceptor, false); |
2476 | 33 | heapBytes.after = acceptor.evacuatedBytes(); |
2477 | 33 | } |
2478 | | // Inform trackers about objects that died during this YG collection. |
2479 | 33 | if (isTrackingIDs()) { |
2480 | 0 | auto trackerCallback = [this](GCCell *cell) { |
2481 | | // The compactee might have free list cells, which are not tracked. |
2482 | | // untrackObject requires the object to have been tracked previously. |
2483 | | // So skip free list cells here. |
2484 | 0 | if (!vmisa<OldGen::FreelistCell>(cell)) { |
2485 | 0 | untrackObject(cell, cell->getAllocatedSize()); |
2486 | 0 | } |
2487 | 0 | }; |
2488 | 0 | yg.forCompactedObjs(trackerCallback, getPointerBase()); |
2489 | 0 | if (doCompaction) { |
2490 | 0 | compactee_.segment->forCompactedObjs(trackerCallback, getPointerBase()); |
2491 | 0 | } |
2492 | 0 | } |
2493 | | // Run finalizers for young gen objects. |
2494 | 33 | finalizeYoungGenObjects(); |
2495 | | // This was modified by debitExternalMemoryFromFinalizer, called by |
2496 | | // finalizers. The difference in the value before to now was the swept bytes |
2497 | 33 | externalBytes.after = getYoungGenExternalBytes(); |
2498 | | |
2499 | 33 | if (overwriteDeadYGObjects_) |
2500 | 0 | memset(yg.start(), kInvalidHeapValue, yg.used()); |
2501 | | |
2502 | | // Now the copy list is drained, and all references point to the old |
2503 | | // gen. Clear the level of the young gen. |
2504 | 33 | yg.resetLevel(); |
2505 | 33 | assert( |
2506 | 33 | yg.markBitArray().findNextUnmarkedBitFrom(0) == |
2507 | 33 | yg.markBitArray().size() && |
2508 | 33 | "Young gen segment must have all mark bits set"); |
2509 | | |
2510 | 33 | if (doCompaction) { |
2511 | 0 | numCompactions_++; |
2512 | 0 | ygCollectionStats_->addCollectionType("compact"); |
2513 | | // We can use the total amount of external memory in the OG before and |
2514 | | // after running finalizers to measure how much external memory has been |
2515 | | // released. |
2516 | 0 | uint64_t ogExternalBefore = oldGen_.externalBytes(); |
2517 | | // Similarly, finalizeCompactee will update the allocated bytes counter to |
2518 | | // remove bytes allocated in the compactee. |
2519 | 0 | uint64_t ogAllocatedBefore = oldGen_.allocatedBytes(); |
2520 | 0 | finalizeCompactee(); |
2521 | 0 | heapBytes.before += ogAllocatedBefore - oldGen_.allocatedBytes(); |
2522 | 0 | const uint64_t externalCompactedBytes = |
2523 | 0 | ogExternalBefore - oldGen_.externalBytes(); |
2524 | | // Since we can't track the actual number of external bytes that were in |
2525 | | // this segment, just use the swept external byte count. |
2526 | 0 | externalBytes.before += externalCompactedBytes; |
2527 | 0 | } |
2528 | | |
2529 | | // Move external memory accounting from YG to OG as well. |
2530 | 33 | transferExternalMemoryToOldGen(); |
2531 | | |
2532 | | // Potentially resize the YG if this collection did not meet our pause time |
2533 | | // goals. Exclude compacting collections and the portion of YG time spent on |
2534 | | // incremental OG collections, since they distort pause times and are |
2535 | | // unaffected by YG size. |
2536 | 33 | if (!doCompaction) |
2537 | 33 | updateYoungGenSizeFactor(); |
2538 | | |
2539 | | // The effective end of our YG is no longer accurate for multiple reasons: |
2540 | | // 1. transferExternalMemoryToOldGen resets the effectiveEnd to be the end. |
2541 | | // 2. Creating a large alloc in the YG can increase the effectiveEnd. |
2542 | | // 3. The duration of this collection may not have met our pause time goals. |
2543 | 33 | youngGen().setEffectiveEnd( |
2544 | 33 | youngGen().start() + |
2545 | 33 | static_cast<size_t>(ygSizeFactor_ * HeapSegment::maxSize())); |
2546 | | |
2547 | | // We have to set these after the collection, in case a compaction took |
2548 | | // place and updated these metrics. |
2549 | 33 | ygCollectionStats_->setBeforeSizes( |
2550 | 33 | heapBytes.before, externalBytes.before, segmentFootprint()); |
2551 | | |
2552 | | // The swept bytes are just the bytes that were not evacuated. |
2553 | 33 | ygCollectionStats_->setSweptBytes(heapBytes.before - heapBytes.after); |
2554 | 33 | ygCollectionStats_->setSweptExternalBytes( |
2555 | 33 | externalBytes.before - externalBytes.after); |
2556 | 33 | ygCollectionStats_->setAfterSize(segmentFootprint()); |
2557 | | // If this is not a compacting YG, update the average survival bytes. |
2558 | | // In a compacting YG, since the evacuatedBytes counter tracks both |
2559 | | // segments, this value is not a useful predictor of future collections. |
2560 | 33 | if (!doCompaction) |
2561 | 33 | ygAverageSurvivalBytes_.update( |
2562 | 33 | ygCollectionStats_->afterAllocatedBytes() + |
2563 | 33 | ygCollectionStats_->afterExternalBytes()); |
2564 | 33 | } |
2565 | 33 | #ifdef HERMES_SLOW_DEBUG |
2566 | | // Check that the card tables are well-formed after the collection. |
2567 | 33 | verifyCardTable(); |
2568 | 33 | #endif |
2569 | | // Perform any pending work for an ongoing OG collection. |
2570 | | // Do this before starting a new collection in case we need collections |
2571 | | // back-to-back. Also, don't check this after starting a collection to avoid |
2572 | | // waiting for something that is both unlikely, and will increase the pause |
2573 | | // time if it does happen. |
2574 | 33 | yieldToOldGen(); |
2575 | | // We can only consider starting a new OG collection if any previous OG |
2576 | | // collection is fully completed and has not left a pending compaction. This |
2577 | | // handles the rare case where an OG collection was completed during this YG |
2578 | | // collection, and the compaction will therefore only be completed in the next |
2579 | | // collection. |
2580 | 33 | if (concurrentPhase_ == Phase::None && !compactee_.evacActive()) { |
2581 | | // There is no OG collection running, check the tripwire in case this is the |
2582 | | // first YG after an OG completed. |
2583 | 33 | checkTripwireAndSubmitStats(); |
2584 | 33 | if (forceOldGenCollection) { |
2585 | | // If an OG collection is being forced, it's because something called |
2586 | | // collect directly, most likely from a memory warning. In order to |
2587 | | // respond to memory pressure effectively, the OG should compact. |
2588 | 1 | oldGenCollection(std::move(cause), /*forceCompaction*/ true); |
2589 | 32 | } else { |
2590 | | // If the OG is sufficiently full after the collection finishes, begin |
2591 | | // an OG collection. |
2592 | | // External bytes are part of the numerator and denominator, because they |
2593 | | // should not be included as part of determining the heap's occupancy, but |
2594 | | // instead just influence when collections begin. |
2595 | 32 | const uint64_t totalAllocated = |
2596 | 32 | oldGen_.allocatedBytes() + oldGen_.externalBytes(); |
2597 | 32 | const uint64_t totalBytes = oldGen_.targetSizeBytes(); |
2598 | 32 | double allocatedRatio = static_cast<double>(totalAllocated) / totalBytes; |
2599 | 32 | if (allocatedRatio >= ogThreshold_) { |
2600 | 0 | oldGenCollection(kNaturalCauseForAnalytics, /*forceCompaction*/ false); |
2601 | 0 | } |
2602 | 32 | } |
2603 | 33 | } |
2604 | 33 | #ifdef HERMES_SLOW_DEBUG |
2605 | | // Run a well-formed check before exiting. |
2606 | 33 | checkWellFormed(); |
2607 | 33 | #endif |
2608 | 33 | ygCollectionStats_->setEndTime(); |
2609 | 33 | ygCollectionStats_->endCPUTimeSection(); |
2610 | 33 | auto statsEvent = std::move(*ygCollectionStats_).getEvent(); |
2611 | 33 | recordGCStats(statsEvent, true); |
2612 | 33 | recordGCStats(statsEvent, &ygCumulativeStats_, true); |
2613 | 33 | ygCollectionStats_.reset(); |
2614 | 33 | } |
2615 | | |
2616 | 33 | bool HadesGC::promoteYoungGenToOldGen() { |
2617 | 33 | if (!promoteYGToOG_) { |
2618 | 33 | return false; |
2619 | 33 | } |
2620 | | |
2621 | | // Attempt to create a new segment, if that fails, turn off the flag to |
2622 | | // disable GC and return false so we proceed with a GC. |
2623 | | // TODO: Add more stringent criteria for turning off this flag, for instance, |
2624 | | // once the heap reaches a certain size. That would avoid growing the heap to |
2625 | | // the maximum possible size before stopping promotions. |
2626 | 0 | llvh::ErrorOr<HeapSegment> newYoungGen = createSegment(); |
2627 | 0 | if (!newYoungGen) { |
2628 | 0 | promoteYGToOG_ = false; |
2629 | 0 | return false; |
2630 | 0 | } |
2631 | | |
2632 | | // Move the external memory costs to the OG. Needs to happen here so that the |
2633 | | // YG segment moved to OG is not left with an effective end. |
2634 | 0 | transferExternalMemoryToOldGen(); |
2635 | | // The flag is on to prevent GC until TTI. Promote the whole YG segment |
2636 | | // directly into OG. |
2637 | | // Before promoting it, set the cell heads correctly for the segment |
2638 | | // going into OG. This could be done at allocation time, but at a cost |
2639 | | // to YG alloc times for a case that might not come up. |
2640 | 0 | youngGen_.forAllObjs([this](GCCell *cell) { |
2641 | 0 | youngGen_.setCellHead(cell, cell->getAllocatedSize()); |
2642 | 0 | }); |
2643 | | // It is important that this operation is just a move of pointers to |
2644 | | // segments. The addresses have to stay the same or else it would |
2645 | | // require a marking pass through all objects. |
2646 | | // This will also rename the segment in the crash data. |
2647 | 0 | oldGen_.addSegment(setYoungGen(std::move(newYoungGen.get()))); |
2648 | |
|
2649 | 0 | return true; |
2650 | 0 | } |
2651 | | |
2652 | 94 | HadesGC::HeapSegment HadesGC::setYoungGen(HeapSegment seg) { |
2653 | 94 | addSegmentExtentToCrashManager(seg, "YG"); |
2654 | 94 | youngGenFinalizables_.clear(); |
2655 | 94 | std::swap(youngGen_, seg); |
2656 | 94 | youngGenCP_ = CompressedPointer::encodeNonNull( |
2657 | 94 | reinterpret_cast<GCCell *>(youngGen_.lowLim()), getPointerBase()); |
2658 | 94 | return seg; |
2659 | 94 | } |
2660 | | |
2661 | 34 | void HadesGC::checkTripwireAndSubmitStats() { |
2662 | 34 | assert( |
2663 | 34 | gcMutex_ && |
2664 | 34 | "gcMutex must be held before calling checkTripwireAndSubmitStats"); |
2665 | 34 | assert( |
2666 | 34 | concurrentPhase_ == Phase::None && |
2667 | 34 | "Cannot check stats while OG collection is ongoing."); |
2668 | 34 | if (!ogCollectionStats_) { |
2669 | 33 | return; |
2670 | 33 | } |
2671 | 1 | const auto usedBytes = ogCollectionStats_->afterAllocatedBytes() + |
2672 | 1 | ogCollectionStats_->afterExternalBytes(); |
2673 | | // We use the amount of live data from after a GC completed as the minimum |
2674 | | // bound of what is live. |
2675 | 1 | checkTripwire(usedBytes); |
2676 | 1 | auto event = std::move(*ogCollectionStats_).getEvent(); |
2677 | 1 | recordGCStats(event, false); |
2678 | 1 | recordGCStats(event, &ogCumulativeStats_, false); |
2679 | 1 | ogCollectionStats_.reset(); |
2680 | 1 | } |
2681 | | |
2682 | 127 | void HadesGC::transferExternalMemoryToOldGen() { |
2683 | 127 | oldGen_.creditExternalMemory(getYoungGenExternalBytes()); |
2684 | 127 | setYoungGenExternalBytes(0); |
2685 | 127 | youngGen_.clearExternalMemoryCharge(); |
2686 | 127 | } |
2687 | | |
2688 | 33 | void HadesGC::updateYoungGenSizeFactor() { |
2689 | 33 | assert( |
2690 | 33 | ygSizeFactor_ <= 1.0 && ygSizeFactor_ >= 0.25 && "YG size out of range."); |
2691 | 33 | const auto ygDuration = ygCollectionStats_->getElapsedTime().count(); |
2692 | | // If the YG collection has taken less than 20% of our budgeted time, increase |
2693 | | // the size of the YG by 10%. |
2694 | 33 | if (ygDuration < kTargetMaxPauseMs * 0.2) |
2695 | 30 | ygSizeFactor_ = std::min(ygSizeFactor_ * 1.1, 1.0); |
2696 | | // If the YG collection has taken more than 40% of our budgeted time, decrease |
2697 | | // the size of the YG by 10%. This is meant to leave some time for OG work. |
2698 | | // However, don't let the YG size drop below 25% of the segment size. |
2699 | 3 | else if (ygDuration > kTargetMaxPauseMs * 0.4) |
2700 | 0 | ygSizeFactor_ = std::max(ygSizeFactor_ * 0.9, 0.25); |
2701 | 33 | } |
2702 | | |
2703 | | template <bool CompactionEnabled> |
2704 | | void HadesGC::scanDirtyCardsForSegment( |
2705 | | SlotVisitor<EvacAcceptor<CompactionEnabled>> &visitor, |
2706 | 33 | HeapSegment &seg) { |
2707 | 33 | const auto &cardTable = seg.cardTable(); |
2708 | | // Use level instead of end in case the OG segment is still in bump alloc |
2709 | | // mode. |
2710 | 33 | const char *const origSegLevel = seg.level(); |
2711 | 33 | size_t from = cardTable.addressToIndex(seg.start()); |
2712 | 33 | const size_t to = cardTable.addressToIndex(origSegLevel - 1) + 1; |
2713 | | |
2714 | | // Do not scan unmarked objects in the OG if we are currently sweeping. We do |
2715 | | // this for three reasons: |
2716 | | // 1. If an object is unmarked during sweeping, that means it is dead, so it |
2717 | | // is wasteful to scan it and promote objects it refers to. |
2718 | | // 2. During compaction, these unmarked objects may point to dead objects in |
2719 | | // the compactee. Promoting these objects is dangerous, since they may |
2720 | | // contain references to other dead objects that are or will be freed. |
2721 | | // 3. If a compaction was completed during this cycle, unmarked objects may |
2722 | | // contain references into the now freed compactee. These pointers are not |
2723 | | // safe to visit. |
2724 | 33 | const bool visitUnmarked = concurrentPhase_ != Phase::Sweep; |
2725 | | |
2726 | 89 | while (const auto oiBegin = cardTable.findNextDirtyCard(from, to)) { |
2727 | 56 | const auto iBegin = *oiBegin; |
2728 | | |
2729 | 56 | const auto oiEnd = cardTable.findNextCleanCard(iBegin, to); |
2730 | 56 | const auto iEnd = oiEnd ? *oiEnd : to; |
2731 | | |
2732 | 56 | assert( |
2733 | 56 | (iEnd == to || !cardTable.isCardForIndexDirty(iEnd)) && |
2734 | 56 | cardTable.isCardForIndexDirty(iEnd - 1) && |
2735 | 56 | "end should either be the end of the card table, or the first " |
2736 | 56 | "non-dirty card after a sequence of dirty cards"); |
2737 | 56 | assert(iBegin < iEnd && "Indices must be apart by at least one"); |
2738 | | |
2739 | 56 | const char *const begin = cardTable.indexToAddress(iBegin); |
2740 | 56 | const char *const end = cardTable.indexToAddress(iEnd); |
2741 | | // Don't try to mark any cell past the original boundary of the segment. |
2742 | 56 | const void *const boundary = std::min(end, origSegLevel); |
2743 | | |
2744 | | // Use the object heads rather than the card table to discover the head |
2745 | | // of the object. |
2746 | 56 | GCCell *const firstObj = seg.getFirstCellHead(iBegin); |
2747 | 56 | GCCell *obj = firstObj; |
2748 | | // Throughout this loop, objects are being marked which could promote |
2749 | | // other objects into the OG. Such objects might be promoted onto a dirty |
2750 | | // card, and be visited a second time. This is only a problem if the |
2751 | | // acceptor isn't idempotent. Luckily, EvacAcceptor happens to be |
2752 | | // idempotent, and so there's no correctness issue with visiting an object |
2753 | | // multiple times. If EvacAcceptor wasn't idempotent, we'd have to be able |
2754 | | // to identify objects promoted from YG in this loop, which would be |
2755 | | // expensive. |
2756 | | |
2757 | | // Mark the first object with respect to the dirty card boundaries. |
2758 | 56 | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) |
2759 | 56 | markCellWithinRange(visitor, obj, obj->getKind(), begin, end); |
2760 | | |
2761 | 56 | obj = obj->nextCell(); |
2762 | | // If there are additional objects in this card, scan them. |
2763 | 56 | if (LLVM_LIKELY(obj < boundary)) { |
2764 | | // Mark the objects that are entirely contained within the dirty card |
2765 | | // boundaries. In a given iteration, obj is the start of a given object, |
2766 | | // and next is the start of the next object. Iterate until the last |
2767 | | // object where next is within the card. |
2768 | 8.62k | for (GCCell *next = obj->nextCell(); next < boundary; |
2769 | 8.56k | next = next->nextCell()) { |
2770 | 8.56k | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) |
2771 | 8.56k | markCell(visitor, obj, obj->getKind()); |
2772 | 8.56k | obj = next; |
2773 | 8.56k | } |
2774 | | |
2775 | | // Mark the final object in the range with respect to the dirty card |
2776 | | // boundaries. |
2777 | 54 | assert( |
2778 | 54 | obj < boundary && obj->nextCell() >= boundary && |
2779 | 54 | "Last object in card must touch or cross cross the card boundary"); |
2780 | 54 | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) |
2781 | 54 | markCellWithinRange(visitor, obj, obj->getKind(), begin, end); |
2782 | 54 | } |
2783 | | |
2784 | 56 | from = iEnd; |
2785 | 56 | } |
2786 | 33 | } Unexecuted instantiation: void hermes::vm::HadesGC::scanDirtyCardsForSegment<true>(hermes::vm::SlotVisitor<hermes::vm::HadesGC::EvacAcceptor<true> >&, hermes::vm::HadesGC::HeapSegment&) void hermes::vm::HadesGC::scanDirtyCardsForSegment<false>(hermes::vm::SlotVisitor<hermes::vm::HadesGC::EvacAcceptor<false> >&, hermes::vm::HadesGC::HeapSegment&) Line | Count | Source | 2706 | 33 | HeapSegment &seg) { | 2707 | 33 | const auto &cardTable = seg.cardTable(); | 2708 | | // Use level instead of end in case the OG segment is still in bump alloc | 2709 | | // mode. | 2710 | 33 | const char *const origSegLevel = seg.level(); | 2711 | 33 | size_t from = cardTable.addressToIndex(seg.start()); | 2712 | 33 | const size_t to = cardTable.addressToIndex(origSegLevel - 1) + 1; | 2713 | | | 2714 | | // Do not scan unmarked objects in the OG if we are currently sweeping. We do | 2715 | | // this for three reasons: | 2716 | | // 1. If an object is unmarked during sweeping, that means it is dead, so it | 2717 | | // is wasteful to scan it and promote objects it refers to. | 2718 | | // 2. During compaction, these unmarked objects may point to dead objects in | 2719 | | // the compactee. Promoting these objects is dangerous, since they may | 2720 | | // contain references to other dead objects that are or will be freed. | 2721 | | // 3. If a compaction was completed during this cycle, unmarked objects may | 2722 | | // contain references into the now freed compactee. These pointers are not | 2723 | | // safe to visit. | 2724 | 33 | const bool visitUnmarked = concurrentPhase_ != Phase::Sweep; | 2725 | | | 2726 | 89 | while (const auto oiBegin = cardTable.findNextDirtyCard(from, to)) { | 2727 | 56 | const auto iBegin = *oiBegin; | 2728 | | | 2729 | 56 | const auto oiEnd = cardTable.findNextCleanCard(iBegin, to); | 2730 | 56 | const auto iEnd = oiEnd ? *oiEnd : to; | 2731 | | | 2732 | 56 | assert( | 2733 | 56 | (iEnd == to || !cardTable.isCardForIndexDirty(iEnd)) && | 2734 | 56 | cardTable.isCardForIndexDirty(iEnd - 1) && | 2735 | 56 | "end should either be the end of the card table, or the first " | 2736 | 56 | "non-dirty card after a sequence of dirty cards"); | 2737 | 56 | assert(iBegin < iEnd && "Indices must be apart by at least one"); | 2738 | | | 2739 | 56 | const char *const begin = cardTable.indexToAddress(iBegin); | 2740 | 56 | const char *const end = cardTable.indexToAddress(iEnd); | 2741 | | // Don't try to mark any cell past the original boundary of the segment. | 2742 | 56 | const void *const boundary = std::min(end, origSegLevel); | 2743 | | | 2744 | | // Use the object heads rather than the card table to discover the head | 2745 | | // of the object. | 2746 | 56 | GCCell *const firstObj = seg.getFirstCellHead(iBegin); | 2747 | 56 | GCCell *obj = firstObj; | 2748 | | // Throughout this loop, objects are being marked which could promote | 2749 | | // other objects into the OG. Such objects might be promoted onto a dirty | 2750 | | // card, and be visited a second time. This is only a problem if the | 2751 | | // acceptor isn't idempotent. Luckily, EvacAcceptor happens to be | 2752 | | // idempotent, and so there's no correctness issue with visiting an object | 2753 | | // multiple times. If EvacAcceptor wasn't idempotent, we'd have to be able | 2754 | | // to identify objects promoted from YG in this loop, which would be | 2755 | | // expensive. | 2756 | | | 2757 | | // Mark the first object with respect to the dirty card boundaries. | 2758 | 56 | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) | 2759 | 56 | markCellWithinRange(visitor, obj, obj->getKind(), begin, end); | 2760 | | | 2761 | 56 | obj = obj->nextCell(); | 2762 | | // If there are additional objects in this card, scan them. | 2763 | 56 | if (LLVM_LIKELY(obj < boundary)) { | 2764 | | // Mark the objects that are entirely contained within the dirty card | 2765 | | // boundaries. In a given iteration, obj is the start of a given object, | 2766 | | // and next is the start of the next object. Iterate until the last | 2767 | | // object where next is within the card. | 2768 | 8.62k | for (GCCell *next = obj->nextCell(); next < boundary; | 2769 | 8.56k | next = next->nextCell()) { | 2770 | 8.56k | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) | 2771 | 8.56k | markCell(visitor, obj, obj->getKind()); | 2772 | 8.56k | obj = next; | 2773 | 8.56k | } | 2774 | | | 2775 | | // Mark the final object in the range with respect to the dirty card | 2776 | | // boundaries. | 2777 | 54 | assert( | 2778 | 54 | obj < boundary && obj->nextCell() >= boundary && | 2779 | 54 | "Last object in card must touch or cross cross the card boundary"); | 2780 | 54 | if (visitUnmarked || HeapSegment::getCellMarkBit(obj)) | 2781 | 54 | markCellWithinRange(visitor, obj, obj->getKind(), begin, end); | 2782 | 54 | } | 2783 | | | 2784 | 56 | from = iEnd; | 2785 | 56 | } | 2786 | 33 | } |
|
2787 | | |
2788 | | template <bool CompactionEnabled> |
2789 | 33 | void HadesGC::scanDirtyCards(EvacAcceptor<CompactionEnabled> &acceptor) { |
2790 | 33 | SlotVisitor<EvacAcceptor<CompactionEnabled>> visitor{acceptor}; |
2791 | 33 | const bool preparingCompaction = |
2792 | 33 | CompactionEnabled && !compactee_.evacActive(); |
2793 | | // The acceptors in this loop can grow the old gen by adding another |
2794 | | // segment, if there's not enough room to evac the YG objects discovered. |
2795 | | // Since segments are always placed at the end, we can use indices instead |
2796 | | // of iterators, which aren't invalidated. It's ok to not scan newly added |
2797 | | // segments, since they are going to be handled from the rest of YG |
2798 | | // collection. |
2799 | 33 | const auto segEnd = oldGen_.numSegments(); |
2800 | 66 | for (size_t i = 0; i < segEnd; ++i) { |
2801 | | // It is safe to hold this reference across a push_back into |
2802 | | // oldGen_.segments_ since references into a deque are not invalidated. |
2803 | 33 | HeapSegment &seg = oldGen_[i]; |
2804 | 33 | scanDirtyCardsForSegment(visitor, seg); |
2805 | | // Do not clear the card table if the OG thread is currently marking to |
2806 | | // prepare for a compaction. Note that we should clear the card tables if |
2807 | | // the compaction is currently ongoing. |
2808 | 33 | if (!preparingCompaction) |
2809 | 33 | seg.cardTable().clear(); |
2810 | 33 | } |
2811 | | |
2812 | | // No need to search dirty cards in the compactee segment if it is |
2813 | | // currently being evacuated, since it will be scanned fully. |
2814 | 33 | if (preparingCompaction) |
2815 | 0 | scanDirtyCardsForSegment(visitor, *compactee_.segment); |
2816 | 33 | } Unexecuted instantiation: void hermes::vm::HadesGC::scanDirtyCards<true>(hermes::vm::HadesGC::EvacAcceptor<true>&) void hermes::vm::HadesGC::scanDirtyCards<false>(hermes::vm::HadesGC::EvacAcceptor<false>&) Line | Count | Source | 2789 | 33 | void HadesGC::scanDirtyCards(EvacAcceptor<CompactionEnabled> &acceptor) { | 2790 | 33 | SlotVisitor<EvacAcceptor<CompactionEnabled>> visitor{acceptor}; | 2791 | 33 | const bool preparingCompaction = | 2792 | 33 | CompactionEnabled && !compactee_.evacActive(); | 2793 | | // The acceptors in this loop can grow the old gen by adding another | 2794 | | // segment, if there's not enough room to evac the YG objects discovered. | 2795 | | // Since segments are always placed at the end, we can use indices instead | 2796 | | // of iterators, which aren't invalidated. It's ok to not scan newly added | 2797 | | // segments, since they are going to be handled from the rest of YG | 2798 | | // collection. | 2799 | 33 | const auto segEnd = oldGen_.numSegments(); | 2800 | 66 | for (size_t i = 0; i < segEnd; ++i) { | 2801 | | // It is safe to hold this reference across a push_back into | 2802 | | // oldGen_.segments_ since references into a deque are not invalidated. | 2803 | 33 | HeapSegment &seg = oldGen_[i]; | 2804 | 33 | scanDirtyCardsForSegment(visitor, seg); | 2805 | | // Do not clear the card table if the OG thread is currently marking to | 2806 | | // prepare for a compaction. Note that we should clear the card tables if | 2807 | | // the compaction is currently ongoing. | 2808 | 33 | if (!preparingCompaction) | 2809 | 33 | seg.cardTable().clear(); | 2810 | 33 | } | 2811 | | | 2812 | | // No need to search dirty cards in the compactee segment if it is | 2813 | | // currently being evacuated, since it will be scanned fully. | 2814 | 33 | if (preparingCompaction) | 2815 | 0 | scanDirtyCardsForSegment(visitor, *compactee_.segment); | 2816 | 33 | } |
|
2817 | | |
2818 | 127 | void HadesGC::finalizeYoungGenObjects() { |
2819 | 1.66k | for (GCCell *cell : youngGenFinalizables_) { |
2820 | 1.66k | if (!cell->hasMarkedForwardingPointer()) { |
2821 | 1.61k | cell->getVT()->finalize(cell, *this); |
2822 | 1.61k | } |
2823 | 1.66k | } |
2824 | 127 | youngGenFinalizables_.clear(); |
2825 | 127 | } |
2826 | | |
2827 | 0 | uint64_t HadesGC::allocatedBytes() const { |
2828 | | // This can be called very early in initialization, before YG is initialized. |
2829 | 0 | return (youngGen_ ? youngGen_.used() : 0) + oldGen_.allocatedBytes(); |
2830 | 0 | } |
2831 | | |
2832 | 188 | uint64_t HadesGC::externalBytes() const { |
2833 | 188 | return getYoungGenExternalBytes() + oldGen_.externalBytes(); |
2834 | 188 | } |
2835 | | |
2836 | 256 | uint64_t HadesGC::segmentFootprint() const { |
2837 | 256 | size_t totalSegments = oldGen_.numSegments() + (youngGen_ ? 1 : 0) + |
2838 | 256 | (compactee_.segment ? 1 : 0); |
2839 | 256 | return totalSegments * AlignedStorage::size(); |
2840 | 256 | } |
2841 | | |
2842 | 188 | uint64_t HadesGC::heapFootprint() const { |
2843 | 188 | return segmentFootprint() + externalBytes(); |
2844 | 188 | } |
2845 | | |
2846 | 53 | uint64_t HadesGC::OldGen::allocatedBytes() const { |
2847 | 53 | return allocatedBytes_; |
2848 | 53 | } |
2849 | | |
2850 | 240k | void HadesGC::OldGen::incrementAllocatedBytes(int32_t incr) { |
2851 | 240k | allocatedBytes_ += incr; |
2852 | 240k | assert(allocatedBytes_ <= size() && "Invalid increment"); |
2853 | 240k | } |
2854 | | |
2855 | 243 | uint64_t HadesGC::OldGen::externalBytes() const { |
2856 | 243 | assert(gc_.gcMutex_ && "OG external bytes must be accessed under gcMutex_."); |
2857 | 243 | return externalBytes_; |
2858 | 243 | } |
2859 | | |
2860 | 240k | uint64_t HadesGC::OldGen::size() const { |
2861 | 240k | size_t totalSegments = numSegments() + (gc_.compactee_.segment ? 1 : 0); |
2862 | 240k | return totalSegments * HeapSegment::maxSize(); |
2863 | 240k | } |
2864 | | |
2865 | 53 | uint64_t HadesGC::OldGen::targetSizeBytes() const { |
2866 | 53 | assert(gc_.gcMutex_ && "Must hold gcMutex_ when accessing targetSizeBytes_."); |
2867 | 53 | return targetSizeBytes_; |
2868 | 53 | } |
2869 | | |
2870 | 1.79k | size_t HadesGC::getYoungGenExternalBytes() const { |
2871 | 1.79k | assert( |
2872 | 1.79k | !calledByBackgroundThread() && |
2873 | 1.79k | "ygExternalBytes_ is only accessible from the mutator."); |
2874 | 1.79k | return ygExternalBytes_; |
2875 | 1.79k | } |
2876 | 1.53k | void HadesGC::setYoungGenExternalBytes(size_t sz) { |
2877 | 1.53k | assert( |
2878 | 1.53k | !calledByBackgroundThread() && |
2879 | 1.53k | "ygExternalBytes_ is only accessible from the mutator."); |
2880 | 1.53k | ygExternalBytes_ = sz; |
2881 | 1.53k | } |
2882 | | |
2883 | 0 | llvh::ErrorOr<size_t> HadesGC::getVMFootprintForTest() const { |
2884 | | // Start by adding the YG. |
2885 | 0 | size_t footprint = 0; |
2886 | 0 | auto ygFootprint = |
2887 | 0 | hermes::oscompat::vm_footprint(youngGen_.start(), youngGen_.hiLim()); |
2888 | | // Check if the call failed. |
2889 | 0 | if (!ygFootprint) |
2890 | 0 | return ygFootprint; |
2891 | | |
2892 | | // Add each OG segment. |
2893 | 0 | for (const HeapSegment &seg : oldGen_) { |
2894 | 0 | auto segFootprint = |
2895 | 0 | hermes::oscompat::vm_footprint(seg.start(), seg.hiLim()); |
2896 | 0 | if (!segFootprint) |
2897 | 0 | return segFootprint; |
2898 | 0 | footprint += *segFootprint; |
2899 | 0 | } |
2900 | 0 | return footprint; |
2901 | 0 | } |
2902 | | |
2903 | 294 | std::deque<HadesGC::HeapSegment>::iterator HadesGC::OldGen::begin() { |
2904 | 294 | return segments_.begin(); |
2905 | 294 | } |
2906 | | |
2907 | 294 | std::deque<HadesGC::HeapSegment>::iterator HadesGC::OldGen::end() { |
2908 | 294 | return segments_.end(); |
2909 | 294 | } |
2910 | | |
2911 | | std::deque<HadesGC::HeapSegment>::const_iterator HadesGC::OldGen::begin() |
2912 | 3.82M | const { |
2913 | 3.82M | return segments_.begin(); |
2914 | 3.82M | } |
2915 | | |
2916 | 3.82M | std::deque<HadesGC::HeapSegment>::const_iterator HadesGC::OldGen::end() const { |
2917 | 3.82M | return segments_.end(); |
2918 | 3.82M | } |
2919 | | |
2920 | 241k | size_t HadesGC::OldGen::numSegments() const { |
2921 | 241k | return segments_.size(); |
2922 | 241k | } |
2923 | | |
2924 | 33 | HadesGC::HeapSegment &HadesGC::OldGen::operator[](size_t i) { |
2925 | 33 | return segments_[i]; |
2926 | 33 | } |
2927 | | |
2928 | 188 | llvh::ErrorOr<HadesGC::HeapSegment> HadesGC::createSegment() { |
2929 | | // No heap size limit when Handle-SAN is on, to allow the heap enough room to |
2930 | | // keep moving things around. |
2931 | 188 | if (!sanitizeRate_ && heapFootprint() >= maxHeapSize_) |
2932 | 0 | return make_error_code(OOMError::MaxHeapReached); |
2933 | | |
2934 | 188 | auto res = AlignedStorage::create(provider_.get(), "hades-segment"); |
2935 | 188 | if (!res) { |
2936 | 0 | return res.getError(); |
2937 | 0 | } |
2938 | 188 | HeapSegment seg(std::move(res.get())); |
2939 | | // Even if compressed pointers are off, we still use the segment index for |
2940 | | // crash manager indices. |
2941 | 188 | size_t segIdx; |
2942 | 188 | if (segmentIndices_.size()) { |
2943 | 0 | segIdx = segmentIndices_.back(); |
2944 | 0 | segmentIndices_.pop_back(); |
2945 | 188 | } else { |
2946 | 188 | segIdx = ++numSegments_; |
2947 | 188 | } |
2948 | 188 | pointerBase_.setSegment(segIdx, seg.lowLim()); |
2949 | 188 | addSegmentExtentToCrashManager(seg, std::to_string(segIdx)); |
2950 | 188 | seg.markBitArray().markAll(); |
2951 | 188 | return llvh::ErrorOr<HadesGC::HeapSegment>(std::move(seg)); |
2952 | 188 | } |
2953 | | |
2954 | 94 | void HadesGC::OldGen::addSegment(HeapSegment seg) { |
2955 | 94 | segments_.emplace_back(std::move(seg)); |
2956 | 94 | HeapSegment &newSeg = segments_.back(); |
2957 | 94 | incrementAllocatedBytes(newSeg.used()); |
2958 | | // Add a set of freelist buckets for this segment. |
2959 | 94 | segmentBuckets_.emplace_back(); |
2960 | | |
2961 | 94 | assert( |
2962 | 94 | segmentBuckets_.size() == segments_.size() && |
2963 | 94 | "Must have as many freelists as segments."); |
2964 | | |
2965 | | // Add the remainder of the segment to the freelist. |
2966 | 94 | uint32_t sz = newSeg.available(); |
2967 | 94 | if (sz >= minAllocationSize()) { |
2968 | 94 | auto res = newSeg.alloc(sz); |
2969 | 94 | assert(res.success); |
2970 | 94 | auto bucket = getFreelistBucket(sz); |
2971 | 94 | addCellToFreelist(res.ptr, sz, &segmentBuckets_.back()[bucket]); |
2972 | 94 | } |
2973 | | |
2974 | 94 | gc_.addSegmentExtentToCrashManager(newSeg, std::to_string(numSegments())); |
2975 | 94 | } |
2976 | | |
2977 | 0 | HadesGC::HeapSegment HadesGC::OldGen::popSegment() { |
2978 | 0 | const auto &segBuckets = segmentBuckets_.back(); |
2979 | 0 | for (size_t bucket = 0; bucket < kNumFreelistBuckets; ++bucket) { |
2980 | 0 | if (segBuckets[bucket].head) { |
2981 | 0 | segBuckets[bucket].removeFromFreelist(); |
2982 | 0 | freelistBucketBitArray_.set(bucket, buckets_[bucket].next); |
2983 | 0 | } |
2984 | 0 | } |
2985 | 0 | segmentBuckets_.pop_back(); |
2986 | |
|
2987 | 0 | auto oldSeg = std::move(segments_.back()); |
2988 | 0 | segments_.pop_back(); |
2989 | 0 | return oldSeg; |
2990 | 0 | } |
2991 | | |
2992 | 94 | void HadesGC::OldGen::setTargetSizeBytes(size_t targetSizeBytes) { |
2993 | 94 | assert(gc_.gcMutex_ && "Must hold gcMutex_ when accessing targetSizeBytes_."); |
2994 | 94 | assert(!targetSizeBytes_ && "Should only initialise targetSizeBytes_ once."); |
2995 | 94 | targetSizeBytes_ = ExponentialMovingAverage(0.5, targetSizeBytes); |
2996 | 94 | } |
2997 | | |
2998 | 3.82M | bool HadesGC::inOldGen(const void *p) const { |
2999 | | // If it isn't in any OG segment or the compactee, then this pointer is not in |
3000 | | // the OG. |
3001 | 3.82M | return compactee_.contains(p) || |
3002 | 3.82M | std::any_of(oldGen_.begin(), oldGen_.end(), [p](const HeapSegment &seg) { |
3003 | 3.82M | return seg.contains(p); |
3004 | 3.82M | }); |
3005 | 3.82M | } |
3006 | | |
3007 | 33 | void HadesGC::yieldToOldGen() { |
3008 | 33 | assert(inGC() && "Must be in GC when yielding to old gen"); |
3009 | 33 | if (!kConcurrentGC && concurrentPhase_ != Phase::None) { |
3010 | | // If there is an ongoing collection, update the drain rate before |
3011 | | // collecting. |
3012 | 0 | if (concurrentPhase_ == Phase::Mark) |
3013 | 0 | oldGenMarker_->setDrainRate(getDrainRate()); |
3014 | |
|
3015 | 0 | constexpr uint32_t kYGIncrementalCollectBudget = kTargetMaxPauseMs / 2; |
3016 | 0 | const auto initialPhase = concurrentPhase_; |
3017 | | // If the phase hasn't changed and we are still under 25ms after the first |
3018 | | // iteration, then we can be reasonably sure that the next iteration will |
3019 | | // also be <25ms, keeping us within 50ms even in the worst case. |
3020 | 0 | do { |
3021 | 0 | incrementalCollect(false); |
3022 | 0 | } while (concurrentPhase_ == initialPhase && |
3023 | 0 | ygCollectionStats_->getElapsedTime().count() < |
3024 | 0 | kYGIncrementalCollectBudget); |
3025 | |
|
3026 | 33 | } else if (concurrentPhase_ == Phase::CompleteMarking) { |
3027 | 0 | incrementalCollect(false); |
3028 | 0 | collectOGInBackground(); |
3029 | 0 | } |
3030 | 33 | } |
3031 | | |
3032 | 0 | size_t HadesGC::getDrainRate() { |
3033 | | // Concurrent collections don't need to use the drain rate because they |
3034 | | // only yield the lock periodically to be interrupted, but otherwise will |
3035 | | // continuously churn through work regardless of the rate. |
3036 | | // Non-concurrent collections, on the other hand, can only make progress |
3037 | | // at YG collection intervals, so they need to be configured to mark the |
3038 | | // OG faster than it fills up. |
3039 | 0 | assert(!kConcurrentGC); |
3040 | | |
3041 | | // Set a fixed floor on the mark rate, regardless of the pause time budget. |
3042 | | // yieldToOldGen may operate in multiples of this drain rate if it fits in the |
3043 | | // budget. Pinning the mark rate in this way helps us keep the dynamically |
3044 | | // computed OG collection threshold in a reasonable range. On a slow device, |
3045 | | // where we can only do one iteration of this drain rate, the OG threshold |
3046 | | // will be ~75%. And by not increasing the drain rate when the threshold is |
3047 | | // high, we avoid having a one-way ratchet effect that hurts pause times. |
3048 | 0 | constexpr size_t baseMarkRate = 3; |
3049 | 0 | uint64_t drainRate = baseMarkRate * ygAverageSurvivalBytes_; |
3050 | | // In case the allocation rate is extremely low, set a lower bound to ensure |
3051 | | // the collection eventually ends. |
3052 | 0 | constexpr uint64_t byteDrainRateMin = 8192; |
3053 | 0 | return std::max(drainRate, byteDrainRateMin); |
3054 | 0 | } |
3055 | | |
3056 | | void HadesGC::addSegmentExtentToCrashManager( |
3057 | | const HeapSegment &seg, |
3058 | 376 | const std::string &extraName) { |
3059 | 376 | assert(!extraName.empty() && "extraName can't be empty"); |
3060 | 376 | if (!crashMgr_) { |
3061 | 0 | return; |
3062 | 0 | } |
3063 | 376 | const std::string segmentName = name_ + ":HeapSegment:" + extraName; |
3064 | | // Pointers need at most 18 characters for 0x + 16 digits for a 64-bit |
3065 | | // pointer. |
3066 | 376 | constexpr unsigned kAddressMaxSize = 18; |
3067 | 376 | char segmentAddressBuffer[kAddressMaxSize]; |
3068 | 376 | snprintf(segmentAddressBuffer, kAddressMaxSize, "%p", seg.lowLim()); |
3069 | 376 | crashMgr_->setContextualCustomData(segmentName.c_str(), segmentAddressBuffer); |
3070 | | |
3071 | | #ifdef HERMESVM_PLATFORM_LOGGING |
3072 | | hermesLog( |
3073 | | "HermesGC", |
3074 | | "Added segment extent: %s = %s", |
3075 | | segmentName.c_str(), |
3076 | | segmentAddressBuffer); |
3077 | | #endif |
3078 | 376 | } |
3079 | | |
3080 | | void HadesGC::removeSegmentExtentFromCrashManager( |
3081 | 0 | const std::string &extraName) { |
3082 | 0 | assert(!extraName.empty() && "extraName can't be empty"); |
3083 | 0 | if (!crashMgr_) { |
3084 | 0 | return; |
3085 | 0 | } |
3086 | 0 | const std::string segmentName = name_ + ":HeapSegment:" + extraName; |
3087 | 0 | crashMgr_->removeContextualCustomData(segmentName.c_str()); |
3088 | 0 | } |
3089 | | |
3090 | | #ifdef HERMES_SLOW_DEBUG |
3091 | | |
3092 | 67 | void HadesGC::checkWellFormed() { |
3093 | 67 | CheckHeapWellFormedAcceptor acceptor(*this); |
3094 | 67 | { |
3095 | 67 | DroppingAcceptor<CheckHeapWellFormedAcceptor> nameAcceptor{acceptor}; |
3096 | 67 | markRoots(nameAcceptor, true); |
3097 | 67 | } |
3098 | 67 | markWeakRoots(acceptor, /*markLongLived*/ true); |
3099 | 1.34M | forAllObjs([this, &acceptor](GCCell *cell) { |
3100 | 1.34M | assert(cell->isValid() && "Invalid cell encountered in heap"); |
3101 | 1.34M | markCell(cell, acceptor); |
3102 | 1.34M | }); |
3103 | | |
3104 | 67 | weakMapEntrySlots_.forEach([](WeakMapEntrySlot &slot) { |
3105 | 0 | if (slot.mappedValue.isEmpty()) { |
3106 | 0 | assert( |
3107 | 0 | !slot.key || |
3108 | 0 | !slot.owner && "Reachable entry should have not have Empty value."); |
3109 | 0 | } |
3110 | 0 | }); |
3111 | 67 | } |
3112 | | |
3113 | 66 | void HadesGC::verifyCardTable() { |
3114 | 66 | assert(inGC() && "Must be in GC to call verifyCardTable"); |
3115 | 66 | struct VerifyCardDirtyAcceptor final : public SlotAcceptor { |
3116 | 66 | HadesGC &gc; |
3117 | | |
3118 | 66 | explicit VerifyCardDirtyAcceptor(HadesGC &gc) : gc(gc) {} |
3119 | | |
3120 | 1.75M | void acceptHelper(void *valuePtr, void *locPtr) { |
3121 | 1.75M | const bool crossRegionCompacteePtr = |
3122 | 1.75M | !gc.compactee_.evacContains(locPtr) && |
3123 | 1.75M | gc.compactee_.evacContains(valuePtr); |
3124 | 1.75M | if (!gc.inYoungGen(locPtr) && |
3125 | 988k | (gc.inYoungGen(valuePtr) || crossRegionCompacteePtr)) { |
3126 | 523 | assert( |
3127 | 523 | HeapSegment::cardTableCovering(locPtr)->isCardForAddressDirty( |
3128 | 523 | locPtr)); |
3129 | 523 | } |
3130 | 1.75M | } |
3131 | | |
3132 | 605k | void accept(GCPointerBase &ptr) override { |
3133 | 605k | acceptHelper(ptr.get(gc.getPointerBase()), &ptr); |
3134 | 605k | } |
3135 | | |
3136 | 4.15M | void accept(GCHermesValue &hv) override { |
3137 | 4.15M | if (hv.isPointer()) |
3138 | 6.71k | acceptHelper(hv.getPointer(), &hv); |
3139 | 4.15M | } |
3140 | 2.13M | void accept(GCSmallHermesValue &hv) override { |
3141 | 2.13M | if (hv.isPointer()) |
3142 | 1.14M | acceptHelper(hv.getPointer(gc.getPointerBase()), &hv); |
3143 | 2.13M | } |
3144 | | |
3145 | 471k | void accept(const GCSymbolID &hv) override {} |
3146 | 66 | }; |
3147 | | |
3148 | 66 | VerifyCardDirtyAcceptor acceptor{*this}; |
3149 | 1.33M | forAllObjs([this, &acceptor](GCCell *cell) { markCell(cell, acceptor); }); |
3150 | | |
3151 | 66 | for (const HeapSegment &seg : oldGen_) { |
3152 | 66 | seg.cardTable().verifyBoundaries(seg.start(), seg.level()); |
3153 | 66 | } |
3154 | 66 | } |
3155 | | |
3156 | | #endif |
3157 | | |
3158 | | } // namespace vm |
3159 | | } // namespace hermes |