Coverage Report

Created: 2025-12-12 07:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/hermes/include/hermes/VM/CardTableNC.h
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
#ifndef HERMES_VM_CARDTABLE_H
9
#define HERMES_VM_CARDTABLE_H
10
11
#include "hermes/Support/ErrorHandling.h"
12
#include "hermes/Support/OSCompat.h"
13
#include "hermes/Support/OptValue.h"
14
#include "hermes/VM/AlignedStorage.h"
15
#include "hermes/VM/ExpectedPageSize.h"
16
#include "hermes/VM/GCCell.h"
17
#include "hermes/VM/PointerBase.h"
18
19
#include "llvh/Support/MathExtras.h"
20
21
#include <cassert>
22
23
namespace hermes {
24
namespace vm {
25
26
/// The card table optimizes young gen collections by restricting the amount of
27
/// heap belonging to the old gen that must be scanned.  The card table expects
28
/// to be constructed inside an AlignedHeapSegment's storage, at some position
29
/// before the allocation region, and covers the extent of that storage's
30
/// memory.
31
///
32
/// Also supports the following query:  Given a card in the heap that intersects
33
/// with the used portion of its segment, find its "crossing object" -- the
34
/// object whose extent [obj-start, end) contains the start of the card.  This
35
/// allows us to scan dirty cards: finding the crossing object allows us to then
36
/// do object-to-object traversal to the end of the card.
37
class CardTable {
38
 public:
39
  /// Points at the start of a card.
40
  class Boundary {
41
    friend class CardTable;
42
43
   public:
44
    Boundary() = default;
45
46
    /// Move boundary to the edge at the next highest address.
47
    inline void bump();
48
49
    /// The index for the card starting at \c address(), in the table covering
50
    /// that address.
51
    inline size_t index() const;
52
53
    /// The (inclusive) start address of the card.
54
    inline const char *address() const;
55
56
   private:
57
    inline Boundary(size_t index, const char *address);
58
59
    size_t index_{0};
60
    const char *address_{nullptr};
61
  };
62
63
  /// The size (and base-two log of the size) of cards used in the card table.
64
  static constexpr size_t kLogCardSize = 9; // ==> 512-byte cards.
65
  static constexpr size_t kCardSize = 1 << kLogCardSize; // ==> 512-byte cards.
66
67
  /// The number of valid indices into the card table.
68
  static constexpr size_t kValidIndices =
69
      AlignedStorage::size() >> kLogCardSize;
70
71
  /// The size of the card table.
72
  static constexpr size_t kCardTableSize = kValidIndices;
73
74
  /// For convenience, this is a conversion factor to determine how many bytes
75
  /// in the heap correspond to a single byte in the card table. This is
76
  /// distinct from kCardSize, which tells us how many bytes in the heap
77
  /// correspond to a single byte in the card table. However, since each index
78
  /// corresponds to a single byte for now, they are the same value. This is
79
  /// guaranteed by a static_assert below.
80
  static constexpr size_t kHeapBytesPerCardByte = kCardSize;
81
82
  /// A prefix of every segment is occupied by auxilary data
83
  /// structures.  The card table is the first such data structure.
84
  /// The card table maps to the segment.  Only the suffix of the card
85
  /// table that maps to the suffix of entire segment that is used for
86
  /// allocation is ever used; the prefix that maps to the card table
87
  /// itself is not used.  (Nor is the portion that of the card table
88
  /// that maps to the other auxiliary data structure, the mark bit
89
  /// array, but we don't attempt to calculate that here.)
90
  /// It is useful to know the size of this unused region of
91
  /// the card table, so it can be used for other purposes.
92
  /// Note that the total size of the card table is 2 times
93
  /// kCardTableSize, since the CardTable contains two byte arrays of
94
  /// that size (cards_ and _boundaries_).
95
  static constexpr size_t kFirstUsedIndex =
96
      (2 * kCardTableSize) >> kLogCardSize;
97
98
226
  CardTable() = default;
99
  /// CardTable is not copyable or movable: It must be constructed in-place.
100
  CardTable(const CardTable &) = delete;
101
  CardTable(CardTable &&) = delete;
102
  CardTable &operator=(const CardTable &) = delete;
103
  CardTable &operator=(CardTable &&) = delete;
104
105
  /// Returns the card table index corresponding to a byte at the given address.
106
  /// \pre \p addr must be within the bounds of the segment owning this card
107
  /// table or at most 1 card after it, that is to say
108
  ///
109
  ///    segment.lowLim() <= addr < segment.hiLim() + kCardSize
110
  ///
111
  /// Note that we allow the extra card after the segment in order to simplify
112
  /// the logic for callers that are using this function to generate an open
113
  /// interval of card indices. See \c dirtyCardsForAddressRange for an example
114
  /// of how this is used.
115
  inline size_t addressToIndex(const void *addr) const LLVM_NO_SANITIZE("null");
116
117
  /// Returns the address corresponding to the given card table
118
  /// index.
119
  ///
120
  /// \pre \p index is bounded:
121
  ///
122
  ///     0 <= index <= kValidIndices
123
  inline const char *indexToAddress(size_t index) const;
124
125
  /// Make the card table entry for the given address dirty.
126
  /// \pre \p addr is required to be an address covered by the card table.
127
  inline void dirtyCardForAddress(const void *addr);
128
129
  /// Make the card table entries for cards that intersect the given address
130
  /// range dirty.  The range is a closed interval [low, high].
131
  /// \pre \p low and \p high are required to be addresses covered by the card
132
  /// table.
133
  void dirtyCardsForAddressRange(const void *low, const void *high);
134
135
  /// Returns whether the card table entry for the given address is dirty.
136
  /// \pre \p addr is required to be an address covered by the card table.
137
  inline bool isCardForAddressDirty(const void *addr) const;
138
139
  /// Returns whether the card table entry for the given index is dirty.
140
  /// \pre \p index is required to be a valid card table index.
141
  inline bool isCardForIndexDirty(const size_t index) const;
142
143
  /// If there is a dirty card at or after \p fromIndex, at an index less than
144
  /// \p endIndex, returns the index of the dirty card, else returns none.
145
  inline OptValue<size_t> findNextDirtyCard(size_t fromIndex, size_t endIndex)
146
      const;
147
148
  /// If there is a card card at or after \p fromIndex, at an index less than
149
  /// \p endIndex, returns the index of the clean card, else returns none.
150
  inline OptValue<size_t> findNextCleanCard(size_t fromIndex, size_t endIndex)
151
      const;
152
153
  /// \return The first boundary that could be crossed by a suitably large
154
  /// allocation starting at \p level.
155
  inline Boundary nextBoundary(const char *level) const;
156
157
  /// Clears the card table.
158
  void clear();
159
160
  /// Modify the card table to be (conservatively) valid after old generation
161
  /// compaction: dirty all cards containing objects after compaction (up to \p
162
  /// newLevel), clean all now-unoccupied cards.  This keeps us from having to
163
  /// track what cards contain old-to-young pointers after compaction -- we
164
  /// assume any card might.
165
  ///
166
  /// TODO (T26751833) figure out if this is a performance problem, and do
167
  /// better if necessary.
168
  void updateAfterCompaction(const void *newLevel);
169
170
  /// An allocation of [start, end) has crossed at least one card boundary.
171
  /// Update the boundaries table appropriately to describe the allocation.
172
  ///
173
  /// \pre boundary is not null.
174
  /// \pre [start, end) must be covered by this table.
175
  /// \pre boundary's index must correspond to its address in this table.
176
  /// \pre [start, end) crosses \p *boundary.
177
  ///
178
  /// \post for all card indices I s.t. start <= indexToAddress(I) < end,
179
  ///   firstObjForCard(I) == start
180
  ///
181
  /// \post *boundary == nextBoundary(end)
182
  void updateBoundaries(Boundary *boundary, const char *start, const char *end);
183
184
  /// Returns the start address of the first object that extends onto the card
185
  /// with the given index.  (If an object is allocated at a card boundary, that
186
  /// is the first object.)
187
  GCCell *firstObjForCard(unsigned index) const;
188
189
#ifdef HERMES_EXTRA_DEBUG
190
  /// Temporary debugging hack: yield the numeric value of the boundaries_ array
191
  /// for the given \p index.
192
  /// TODO(T48709128): remove this when the problem is diagnosed.
193
0
  int8_t cardObjectTableValue(unsigned index) const {
194
0
    return boundaries_[index];
195
0
  }
196
197
  /// These methods protect and unprotect, respectively, the memory
198
  /// that comprises the card boundary table.  They require that the
199
  /// start of the boundary table is page-aligned, and its size is a
200
  /// multiple of the page size.
201
  /// TODO(T48709128): remove this when the problem is diagnosed.
202
  void protectBoundaryTable();
203
  void unprotectBoundaryTable();
204
#endif // HERMES_EXTRA_DEBUG
205
206
#ifdef HERMES_SLOW_DEBUG
207
  /// Asserts that for every card covering [start, level), what we believe to
208
  /// be its "crossing object"
209
  ///
210
  ///  1. Is valid.
211
  ///  2. Starts on or before the start of the card, and ends after the start of
212
  ///     the card.
213
  ///
214
  /// \pre start is card-aligned.
215
  void verifyBoundaries(char *start, char *level) const;
216
#endif // HERMES_SLOW_DEBUG
217
218
 private:
219
  enum class CardStatus : char { Clean = 0, Dirty = 1 };
220
221
  /// \return The lowest address whose card can be dirtied in this array. i.e.
222
  ///     The smallest address such that
223
  ///
224
  ///     addressToIndex(base()) == 0
225
  ///
226
  /// Note that the base address will be strictly less than the address
227
  /// corresponding to the start of the allocation region (by at least the width
228
  /// of the card table).
229
  inline const char *base() const LLVM_NO_SANITIZE("null");
230
231
  /// The encoding scheme for the logarithmic-time object boundary queries for
232
  /// large objects.  encodeExp encodes an exponent to a (negative) table value,
233
  /// and decodeExp is the inverse function.
234
  static inline int8_t encodeExp(int8_t exp);
235
  static inline int8_t decodeExp(int8_t encodedVal);
236
237
  /// Returns true iff ptr is card-aligned.
238
  static inline bool isCardAligned(const void *ptr);
239
240
  /// \return the index of the first card in the range [fromIndex, endIndex)
241
  /// with value \p status, or none if one does not exist.
242
  OptValue<size_t> findNextCardWithStatus(
243
      CardStatus status,
244
      size_t fromIndex,
245
      size_t endIndex) const;
246
247
  /// Clean, or dirty, the indicated index ranges in the card table.
248
  /// Ranges are half-open: [from, to).
249
  void cleanRange(size_t from, size_t to);
250
  void dirtyRange(size_t from, size_t to);
251
252
  void cleanOrDirtyRange(size_t from, size_t to, CardStatus cleanOrDirty);
253
254
  /// This needs to be atomic so that the background thread in Hades can safely
255
  /// dirty cards when compacting.
256
  std::array<AtomicIfConcurrentGC<CardStatus>, kCardTableSize> cards_{};
257
258
  /// See the comment at kHeapBytesPerCardByte above to see why this is
259
  /// necessary.
260
  static_assert(
261
      sizeof(cards_[0]) == 1,
262
      "Validate assumption that card table entries are one byte");
263
264
  /// Each card has a corresponding signed byte in the boundaries_ table.  A
265
  /// non-negative entry, K, indicates that the crossing object starts K *
266
  /// HeapAlign bytes before the start of the card. A negative entry, L,
267
  /// indicates that we must seek backwards by 2^(-L-1) indices and consult the
268
  /// entry there.
269
  ///
270
  /// This scheme allows the start of a large object to be found in logarithmic
271
  /// time:  If we allocate a large object that crosses many cards, the first
272
  /// crossed cards gets a non-negative value, and each subsequent one uses the
273
  /// maximum exponent that stays within the card range for the object.
274
  int8_t boundaries_[kCardTableSize];
275
};
276
277
/// Implementations of inlines.
278
942k
inline void CardTable::Boundary::bump() {
279
942k
  index_++;
280
942k
  address_ += kCardSize;
281
942k
}
282
283
962k
inline size_t CardTable::Boundary::index() const {
284
962k
  return index_;
285
962k
}
286
287
1.27M
inline const char *CardTable::Boundary::address() const {
288
1.27M
  return address_;
289
1.27M
}
290
291
inline CardTable::Boundary::Boundary(size_t index, const char *address)
292
258k
    : index_(index), address_(address) {}
293
294
1.86M
inline size_t CardTable::addressToIndex(const void *addr) const {
295
1.86M
  auto addrPtr = reinterpret_cast<const char *>(addr);
296
1.86M
  assert(
297
1.86M
      base() <= addrPtr &&
298
1.86M
      addrPtr < (static_cast<const char *>(AlignedStorage::end(base())) +
299
1.86M
                 kCardSize) &&
300
1.86M
      "address is required to be within range.");
301
1.86M
  return (addrPtr - base()) >> kLogCardSize;
302
1.86M
}
303
304
1.92M
inline const char *CardTable::indexToAddress(size_t index) const {
305
1.92M
  assert(index <= kValidIndices && "index must be within the index range");
306
1.92M
  const char *res = base() + (index << kLogCardSize);
307
1.92M
  assert(
308
1.92M
      base() <= res && res <= AlignedStorage::end(base()) &&
309
1.92M
      "result must be within the covered range");
310
1.92M
  return res;
311
1.92M
}
312
313
1.58M
inline void CardTable::dirtyCardForAddress(const void *addr) {
314
1.58M
  cards_[addressToIndex(addr)].store(
315
1.58M
      CardStatus::Dirty, std::memory_order_relaxed);
316
1.58M
}
317
318
695
inline bool CardTable::isCardForAddressDirty(const void *addr) const {
319
695
  return isCardForIndexDirty(addressToIndex(addr));
320
695
}
321
322
865
inline bool CardTable::isCardForIndexDirty(size_t index) const {
323
865
  assert(index < kValidIndices && "index is required to be in range.");
324
865
  return cards_[index].load(std::memory_order_relaxed) == CardStatus::Dirty;
325
865
}
326
327
inline OptValue<size_t> CardTable::findNextDirtyCard(
328
    size_t fromIndex,
329
137
    size_t endIndex) const {
330
137
  return findNextCardWithStatus(CardStatus::Dirty, fromIndex, endIndex);
331
137
}
332
333
inline OptValue<size_t> CardTable::findNextCleanCard(
334
    size_t fromIndex,
335
85
    size_t endIndex) const {
336
85
  return findNextCardWithStatus(CardStatus::Clean, fromIndex, endIndex);
337
85
}
338
339
258k
inline CardTable::Boundary CardTable::nextBoundary(const char *level) const {
340
258k
  assert(level != nullptr);
341
258k
  size_t ix = addressToIndex(level - 1) + 1;
342
258k
  const char *addr = indexToAddress(ix);
343
344
258k
  return {ix, addr};
345
258k
}
346
347
11.4M
inline const char *CardTable::base() const {
348
  // As we know the card table is laid out inline before the allocation region
349
  // of its aligned heap segment, we can use its own this pointer as the base
350
  // address.
351
11.4M
  return reinterpret_cast<const char *>(this);
352
11.4M
}
353
354
923k
/* static */ inline int8_t CardTable::encodeExp(int8_t exp) {
355
923k
  return -(exp + 1);
356
923k
}
357
358
4.52M
/* static */ inline int8_t CardTable::decodeExp(int8_t encodedVal) {
359
4.52M
  return -encodedVal - 1;
360
4.52M
}
361
362
104
/* static */ inline bool CardTable::isCardAligned(const void *ptr) {
363
104
  return (reinterpret_cast<intptr_t>(ptr) & (kCardSize - 1)) == 0;
364
104
}
365
366
} // namespace vm
367
} // namespace hermes
368
369
#endif // HERMES_VM_CARDTABLE_H