Coverage Report

Created: 2025-12-12 07:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/hermes/lib/VM/DictPropertyMap.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
#define DEBUG_TYPE "vm"
9
#include "hermes/VM/DictPropertyMap.h"
10
#include "hermes/Support/Statistic.h"
11
#include "hermes/VM/SymbolID-inline.h"
12
13
HERMES_SLOW_STATISTIC(NumDictLookups, "Number of dictionary lookups");
14
HERMES_SLOW_STATISTIC(NumExtraHashProbes, "Number of extra hash probes");
15
16
namespace hermes {
17
namespace vm {
18
19
struct DictPropertyMap::detail {
20
  /// The upper bound of the search when trying to find the maximum capacity
21
  /// of this object, given GC::maxAllocationSize().
22
  /// It was chosen to be a value that is certain to not fit into an allocation;
23
  /// at the same time we want to make it smaller, so/ we have arbitrarily
24
  /// chosen to divide the max allocation size by two, which is still guaranteed
25
  /// not to fit.
26
  static constexpr uint32_t kSearchUpperBound = GC::maxAllocationSize() / 2;
27
28
  static_assert(
29
      !DictPropertyMap::constWouldFitAllocation(kSearchUpperBound),
30
      "kSearchUpperBound should not fit into an allocation");
31
32
  /// The maximum capacity of DictPropertyMap, given GC::maxAllocationSize().
33
  static constexpr uint32_t kMaxCapacity =
34
      DictPropertyMap::constFindMaxCapacity(0, kSearchUpperBound);
35
36
  // Double-check that kMaxCapacity is reasonable.
37
  static_assert(
38
      DictPropertyMap::constApproxAllocSize64(kMaxCapacity) <=
39
          GC::maxAllocationSize(),
40
      "invalid kMaxCapacity");
41
42
  // Ensure that it is safe to double capacities without checking for overflow
43
  // until we exceed kMaxCapacity.
44
  static_assert(
45
      kMaxCapacity < (1u << 31),
46
      "kMaxCapacity is unrealistically large");
47
48
  static_assert(
49
      DictPropertyMap::HashPair::canStore(kMaxCapacity),
50
      "too few bits to store max possible descriptor index");
51
};
52
53
const VTable DictPropertyMap::vt{CellKind::DictPropertyMapKind, 0};
54
55
1
void DictPropertyMapBuildMeta(const GCCell *cell, Metadata::Builder &mb) {
56
1
  const auto *self = static_cast<const DictPropertyMap *>(cell);
57
1
  mb.setVTable(&DictPropertyMap::vt);
58
1
  mb.addArray(
59
1
      &self->getDescriptorPairs()->first,
60
1
      &self->numDescriptors_,
61
1
      sizeof(DictPropertyMap::DescriptorPair));
62
1
}
63
64
233k
DictPropertyMap::size_type DictPropertyMap::getMaxCapacity() {
65
233k
  return detail::kMaxCapacity;
66
233k
}
67
68
CallResult<PseudoHandle<DictPropertyMap>> DictPropertyMap::create(
69
    Runtime &runtime,
70
245k
    size_type capacity) {
71
245k
  if (LLVM_UNLIKELY(capacity > detail::kMaxCapacity)) {
72
0
    return runtime.raiseRangeError(
73
0
        TwineChar16("Property storage exceeds ") + detail::kMaxCapacity +
74
0
        " properties");
75
0
  }
76
245k
  size_type hashCapacity = calcHashCapacity(capacity);
77
245k
  auto *cell = runtime.makeAVariable<DictPropertyMap>(
78
245k
      allocationSize(capacity, hashCapacity), capacity, hashCapacity);
79
245k
  return createPseudoHandle(cell);
80
245k
}
81
82
std::pair<bool, DictPropertyMap::HashPair *> DictPropertyMap::lookupEntryFor(
83
    DictPropertyMap *self,
84
6.27M
    SymbolID symbolID) {
85
6.27M
  ++NumDictLookups;
86
87
6.27M
  size_type const mask = self->hashCapacity_ - 1;
88
6.27M
  size_type index = hash(symbolID) & mask;
89
90
  // Probing step.
91
6.27M
  size_type step = 1;
92
  // Save the address of the start of the table to avoid recalculating it.
93
6.27M
  HashPair *const tableStart = self->getHashPairs();
94
  // The first deleted entry we found.
95
6.27M
  HashPair *deleted = nullptr;
96
97
6.27M
  assert(symbolID.isValid() && "looking for an invalid SymbolID");
98
99
12.4M
  for (;;) {
100
12.4M
    HashPair *curEntry = tableStart + index;
101
102
12.4M
    if (curEntry->isValid()) {
103
8.81M
      if (self->isMatch(curEntry, symbolID))
104
2.67M
        return {true, curEntry};
105
8.81M
    } else if (curEntry->isEmpty()) {
106
      // If we encountered an empty pair, the search is over - we failed.
107
      // Return either this entry or a deleted one, if we encountered one.
108
109
3.59M
      return {false, deleted ? deleted : curEntry};
110
3.59M
    } else {
111
0
      assert(curEntry->isDeleted() && "unexpected HashPair state");
112
      // The first time we encounter a deleted entry, record it so we can
113
      // potentially reuse it for insertion.
114
0
      if (!deleted)
115
0
        deleted = curEntry;
116
0
    }
117
118
6.14M
    ++NumExtraHashProbes;
119
6.14M
    index = (index + step) & mask;
120
6.14M
    ++step;
121
6.14M
  }
122
6.27M
}
123
124
ExecutionStatus DictPropertyMap::grow(
125
    MutableHandle<DictPropertyMap> &selfHandleRef,
126
    Runtime &runtime,
127
11.7k
    size_type newCapacity) {
128
11.7k
  auto res = create(runtime, newCapacity);
129
11.7k
  if (LLVM_UNLIKELY(res == ExecutionStatus::EXCEPTION)) {
130
0
    return ExecutionStatus::EXCEPTION;
131
0
  }
132
11.7k
  auto *newSelf = res->get();
133
11.7k
  auto *self = *selfHandleRef;
134
135
11.7k
  selfHandleRef = newSelf;
136
137
11.7k
  auto *dst = newSelf->getDescriptorPairs();
138
11.7k
  size_type count = 0;
139
140
11.7k
  for (auto *src = self->getDescriptorPairs(),
141
11.7k
            *e = src + self->numDescriptors_.load(std::memory_order_relaxed);
142
369k
       src != e;
143
358k
       ++src) {
144
358k
    if (src->first.isInvalid())
145
0
      continue;
146
147
358k
    const SymbolID key = src->first;
148
149
    // The new property map doesn't have any valid symbols in it yet, use a
150
    // constructor instead of assignment to avoid an invalid write barrier.
151
358k
    new (&dst->first) GCSymbolID(key);
152
358k
    dst->second = src->second;
153
154
358k
    auto result = lookupEntryFor(newSelf, key);
155
358k
    assert(!result.first && "found duplicate entry while growing");
156
358k
    result.second->setDescIndex(count, key);
157
158
358k
    ++dst;
159
358k
    ++count;
160
358k
  }
161
162
11.7k
  assert(
163
11.7k
      count == self->numProperties_ && "numProperties mismatch when growing");
164
165
11.7k
  newSelf->numProperties_ = count;
166
167
  // Transfer the deleted list to the new instance.
168
11.7k
  auto deletedIndex = self->deletedListHead_;
169
11.7k
  if (deletedIndex != END_OF_LIST) {
170
0
    newSelf->deletedListHead_ = count;
171
0
    newSelf->deletedListSize_ = self->deletedListSize_;
172
173
0
    do {
174
0
      const auto *src = self->getDescriptorPairs() + deletedIndex;
175
0
      assert(
176
0
          src->first == SymbolID::deleted() &&
177
0
          "pair in the deleted list is not marked as deleted");
178
179
      // The new property map doesn't have any valid symbols in it yet, use a
180
      // constructor instead of assignment to avoid an invalid write barrier.
181
0
      new (&dst->first) GCSymbolID(SymbolID::deleted());
182
0
      dst->second.slot = src->second.slot;
183
184
0
      deletedIndex = getNextDeletedIndex(src);
185
0
      setNextDeletedIndex(
186
0
          dst, deletedIndex != END_OF_LIST ? count + 1 : END_OF_LIST);
187
188
0
      ++dst;
189
0
      ++count;
190
0
    } while (deletedIndex != END_OF_LIST);
191
0
  }
192
193
11.7k
  newSelf->numDescriptors_.store(count, std::memory_order_release);
194
11.7k
  assert(count <= newSelf->descriptorCapacity_);
195
11.7k
  return ExecutionStatus::RETURNED;
196
11.7k
}
197
198
CallResult<std::pair<NamedPropertyDescriptor *, bool>>
199
DictPropertyMap::findOrAdd(
200
    MutableHandle<DictPropertyMap> &selfHandleRef,
201
    Runtime &runtime,
202
544k
    SymbolID id) {
203
544k
  auto *self = *selfHandleRef;
204
544k
  auto numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed);
205
544k
  auto found = lookupEntryFor(self, id);
206
544k
  if (found.first) {
207
0
    return std::make_pair(
208
0
        &self->getDescriptorPairs()[found.second->getDescIndex()].second,
209
0
        false);
210
0
  }
211
212
  // We want to grow the hash table if the number of occupied hash entries
213
  // exceeds 75% of capacity or if the descriptor array is full. Since the
214
  // capacity of the table is 4/3 of the capacity of the descriptor array, it is
215
  // sufficient to only check for the latter.
216
217
544k
  if (numDescriptors == self->descriptorCapacity_) {
218
11.7k
    size_type newCapacity;
219
11.7k
    if (self->numProperties_ == self->descriptorCapacity_) {
220
      // Double the new capacity, up to kMaxCapacity. However make sure that
221
      // we try to allocate at least one extra property. If we are already
222
      // exactly at kMaxCapacity, there is nothing we can do, so grow() will
223
      // simply fail.
224
11.7k
      newCapacity = self->numProperties_ * 2;
225
11.7k
      if (newCapacity > detail::kMaxCapacity)
226
0
        newCapacity =
227
0
            std::max(toRValue(detail::kMaxCapacity), self->numProperties_ + 1);
228
11.7k
    } else {
229
      // Calculate the new capacity to be exactly as much as we need to
230
      // accommodate the deleted list plus one extra property. It it happens
231
      // to exceed kMaxCapacity, there is nothing we can do, so grow() will
232
      // raise an exception.
233
0
      newCapacity = self->numProperties_ + 1 + self->deletedListSize_;
234
0
    }
235
236
11.7k
    if (LLVM_UNLIKELY(
237
11.7k
            grow(selfHandleRef, runtime, newCapacity) ==
238
11.7k
            ExecutionStatus::EXCEPTION)) {
239
0
      return ExecutionStatus::EXCEPTION;
240
0
    }
241
242
11.7k
    self = *selfHandleRef;
243
11.7k
    numDescriptors = self->numDescriptors_.load(std::memory_order_relaxed);
244
245
11.7k
    found = lookupEntryFor(self, id);
246
11.7k
  }
247
248
544k
  ++self->numProperties_;
249
544k
  if (found.second->isDeleted())
250
0
    self->decDeletedHashCount();
251
252
544k
  found.second->setDescIndex(numDescriptors, id);
253
254
544k
  auto *descPair = self->getDescriptorPairs() + numDescriptors;
255
256
544k
  descPair->first.set(id, runtime.getHeap());
257
544k
  self->numDescriptors_.fetch_add(1, std::memory_order_acq_rel);
258
259
544k
  return std::make_pair(&descPair->second, true);
260
544k
}
261
262
void DictPropertyMap::erase(
263
    DictPropertyMap *self,
264
    Runtime &runtime,
265
0
    PropertyPos pos) {
266
0
  auto *hashPair = self->getHashPairs() + pos.hashPairIndex;
267
0
  auto descIndex = hashPair->getDescIndex();
268
0
  assert(
269
0
      descIndex < self->numDescriptors_.load(std::memory_order_relaxed) &&
270
0
      "descriptor index out of range");
271
272
0
  auto *descPair = self->getDescriptorPairs() + descIndex;
273
0
  assert(
274
0
      descPair->first != SymbolID::empty() &&
275
0
      "accessing deleted descriptor pair");
276
277
0
  hashPair->setDeleted();
278
0
  descPair->first.set(SymbolID::deleted(), runtime.getHeap());
279
  // Add the descriptor to the deleted list.
280
0
  setNextDeletedIndex(descPair, self->deletedListHead_);
281
0
  self->deletedListHead_ = descIndex;
282
0
  ++self->deletedListSize_;
283
284
0
  assert(self->numProperties_ != 0 && "num properties out of sync");
285
0
  --self->numProperties_;
286
0
  self->incDeletedHashCount();
287
0
}
288
289
SlotIndex DictPropertyMap::allocatePropertySlot(
290
    DictPropertyMap *self,
291
208k
    Runtime &runtime) {
292
  // If there are no deleted properties, the number of properties corresponds
293
  // exactly to the number of slots.
294
208k
  if (self->deletedListHead_ == END_OF_LIST)
295
208k
    return self->numProperties_;
296
297
0
  auto *deletedPair = self->getDescriptorPairs() + self->deletedListHead_;
298
0
  assert(
299
0
      deletedPair->first == SymbolID::deleted() &&
300
0
      "Head of deleted list is not deleted");
301
302
  // Remove the first element from the deleted list.
303
0
  self->deletedListHead_ = getNextDeletedIndex(deletedPair);
304
0
  --self->deletedListSize_;
305
306
  // Mark the pair as "invalid" instead of "deleted".
307
  // No need for symbol write barrier because the previous value was deleted.
308
0
  new (&deletedPair->first) GCSymbolID(SymbolID::empty());
309
310
0
  return deletedPair->second.slot;
311
0
}
312
313
0
void DictPropertyMap::dump() {
314
0
  auto &OS = llvh::errs();
315
316
0
  OS << "DictPropertyMap:" << getDebugAllocationId() << "\n";
317
0
  OS << "  HashPairs[" << hashCapacity_ << "]:\n";
318
0
  for (unsigned i = 0; i < hashCapacity_; ++i) {
319
0
    auto *pair = getHashPairs() + i;
320
0
    if (pair->isValid()) {
321
0
      OS << "    " << pair->getDescIndex() << "\n";
322
0
    } else if (pair->isEmpty()) {
323
0
      OS << "    (empty)\n";
324
0
    } else {
325
0
      assert(pair->isDeleted());
326
0
      OS << "    (deleted)\n";
327
0
    }
328
0
  }
329
0
  OS << "  Descriptors[" << descriptorCapacity_ << "]:\n";
330
0
  for (unsigned i = 0; i < descriptorCapacity_; ++i) {
331
0
    auto *pair = getDescriptorPairs() + i;
332
0
    OS << "    (" << pair->first << ", " << "(slot=" << pair->second.slot
333
0
       << "))\n";
334
0
  }
335
0
}
336
337
} // namespace vm
338
} // namespace hermes
339
340
#undef DEBUG_TYPE