Coverage Report

Created: 2025-12-11 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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&) const
Unexecuted 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