Skip to content

Commit d09005e

Browse files
David Detlefsfacebook-github-bot
David Detlefs
authored andcommitted
Properly handle JSWeakMap semantics.
Summary: We were not properly handling the semantics of JSWeakMap. The spec says (http://www.ecma-international.org/ecma-262/#sec-weakmap-objects) that if an object used a key is only reachable from the the corresponding value, then the key should not be considered reachable, and the key/value pair should (eventually) be removed from the table. This diff fixes that. We modify marking to recognize JSWeakMaps. When these are found reachable, we collect those in a vector, but do not mark through them. When the marking transitive closure is otherwise complete, we consider the JSWeakMaps. We find entries with reachable keys, and mark transitively from the corresponding values. This is done carefully to ensure we reach a full transitive closure. After this we clear the keys and values of entries with unreachable keys. Normal weak ref processing will mark the table as having freeable slots. A subsequent diffs may optimize this basic idea: if we consider a map multiple times, it is wasteful to scan all the entries. So we should keep track of entries whose keys are not yet known to be unreachable, and consider only them. Minor things: * Modify HermesValue::getWeakSize to check if a JSWeakMap has freeable slots, and free those before getting the size, to get a consistent value. * Factor out "CompleteMarkState::pushCell", now that there's a second use. Also, assert that the cell pushed is valid, which found a problem in my debugging. Reviewed By: dulinriley Differential Revision: D18651375 fbshipit-source-id: 79c06875330147ecf15434bea2c92d3d3141ced9
1 parent f651218 commit d09005e

10 files changed

+398
-13
lines changed

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ set(HERMES_LIT_TEST_PARAMS
542542
serialize_enabled=${HERMESVM_SERIALIZE}
543543
profiler=${HERMES_PROFILER_MODE_IN_LIT_TEST}
544544
use_js_library_implementation=${HERMESVM_USE_JS_LIBRARY_IMPLEMENTATION}
545+
gc=${HERMESVM_GCKIND}
545546
# TODO: Figure out how to tell if CMake is doing a UBSAN build.
546547
ubsan=OFF
547548
)

include/hermes/VM/CompleteMarkState.h

+12
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
namespace hermes {
1818
namespace vm {
1919

20+
/// Forward declartions.
2021
struct FullMSCUpdateAcceptor;
2122

23+
template <CellKind>
24+
class JSWeakMapImpl;
25+
using JSWeakMap = JSWeakMapImpl<CellKind::WeakMapKind>;
26+
2227
/// Intermediate state from marking.
2328
struct CompleteMarkState {
2429
/// Forward declaration of the Acceptor used to mark fields of marked objects.
@@ -32,6 +37,10 @@ struct CompleteMarkState {
3237
/// traversal).
3338
void markTransitive(void *ptr);
3439

40+
/// The pointer \p cell is assumed to point to a reachable object.
41+
/// Push it on the appropriate markStack.
42+
void pushCell(GCCell *cell);
43+
3544
/// Continually pops elements from the mark stack and scans their pointer
3645
/// fields. If such a field points to an unmarked object, mark it and push it
3746
/// on the mark stack. If such a push would overflow the mark stack, sets a
@@ -82,6 +91,9 @@ struct CompleteMarkState {
8291
/// Stores the current object whose fields are being scanned
8392
/// to be marked if needed.
8493
GCCell *currentParPointer = nullptr;
94+
95+
/// The WeakMap objects that have been discovered to be reachable.
96+
std::vector<JSWeakMap *> reachableWeakMaps_;
8597
};
8698

8799
/// Returns a heap acceptor for mark-sweep-compact pointer update.

include/hermes/VM/GenGCNC.h

+12
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,18 @@ class GenGC final : public GCBase {
666666
/// close the mark bits.
667667
void completeMarking();
668668

669+
/// In the first phase of marking, before this is called, we treat
670+
/// JSWeakMaps specially: when we mark a reachable JSWeakMap, we do
671+
/// not mark from it, but rather save a pointer to it in a vector.
672+
/// Then we call this method, which finds the keys that are
673+
/// reachable, and marks transitively from the corresponding value.
674+
/// This is done carefully, to reach a correct global transitive
675+
/// closure, in cases where keys are reachable only via values of
676+
/// other keys. When this marking is done, entries with unreachable
677+
/// keys are cleared. Normal WeakRef processing at the end of GC
678+
/// will delete the cleared entries from the map.
679+
void completeWeakMapMarking();
680+
669681
/// Does any work necessary for GC stats at the end of collection.
670682
/// Returns the number of allocated objects before collection starts.
671683
/// (In optimized builds, does nothing, and returns zero.)

include/hermes/VM/JSWeakMapImpl.h

+47
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ struct WeakRefKey {
3030
uint32_t hash;
3131

3232
WeakRefKey(WeakRef<JSObject> ref, uint32_t hash) : ref(ref), hash(hash) {}
33+
34+
/// Returns the object reference of ref; returns null if ref is not valid.
35+
/// Should only be called during GC; the \param gc argument is used only to
36+
/// verify this.
37+
JSObject *getObject(GC *gc) const;
3338
};
3439

3540
/// Enable using WeakRef<JSObject> in DenseMap.
@@ -159,6 +164,48 @@ class JSWeakMapImplBase : public JSObject {
159164
PointerBase *base,
160165
JSWeakMapImplBase *self);
161166

167+
/// An iterator over the keys of the map.
168+
struct KeyIterator {
169+
DenseMapT::iterator mapIter;
170+
171+
KeyIterator &operator++(int /*dummy*/) {
172+
mapIter++;
173+
return *this;
174+
}
175+
176+
WeakRefKey &operator*() {
177+
return mapIter->first;
178+
}
179+
180+
WeakRefKey *operator->() {
181+
return &mapIter->first;
182+
}
183+
184+
bool operator!=(const KeyIterator &other) {
185+
return mapIter != other.mapIter;
186+
}
187+
};
188+
189+
// Return begin and end iterators for the keys of the map.
190+
KeyIterator keys_begin();
191+
KeyIterator keys_end();
192+
193+
/// Returns the value corresponding to the given \p key. Returns
194+
/// the empty HermesValue if \p key is not in the map. May only be
195+
/// called during GC.
196+
/// \param gc Used to verify that the call is during GC, and provides
197+
/// a PointerBase.
198+
HermesValue getValueDirect(GC *gc, const WeakRefKey &key);
199+
200+
/// If the given \p key is in the map, clears the entry
201+
/// corresponding to \p key -- clears the slot of the WeakRef in
202+
/// key, and sets the value to the empty HermesValue. May only be
203+
/// called during GC.
204+
/// \param gc Used to verify that the call is during GC, and provides
205+
/// a PointerBase.
206+
/// \return whether the key was in the map.
207+
bool clearEntryDirect(GC *gc, const WeakRefKey &key);
208+
162209
protected:
163210
static void _finalizeImpl(GCCell *cell, GC *gc) {
164211
auto *self = vmcast<JSWeakMapImplBase>(cell);

include/hermes/VM/WeakRef.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ class WeakRef : public WeakRefBase {
6161
/// This is an unsafe function since the referenced object may be freed any
6262
/// time that GC occurs.
6363
OptValue<typename Traits::value_type> unsafeGetOptional() const {
64-
return isValid() ? Traits::decode(slot_->value()) : llvm::None;
64+
return isValid() ? Traits::decode(slot_->value())
65+
: OptValue<typename Traits::value_type>(llvm::None);
6566
}
6667

6768
/// Before calling this function the user must check whether weak reference is
@@ -84,6 +85,11 @@ class WeakRef : public WeakRefBase {
8485
assert(vmisa<T>(other.unsafeGetSlot()->value()) && "invalid WeakRef cast");
8586
return WeakRef<T>(other.unsafeGetSlot());
8687
}
88+
89+
/// Clear the slot to which the WeakRef refers.
90+
void clear() {
91+
unsafeGetSlot()->clearPointer();
92+
}
8793
};
8894

8995
} // namespace vm

lib/VM/JSWeakMapImpl.cpp

+41-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,29 @@ bool JSWeakMapImplBase::deleteValue(
7676
return true;
7777
}
7878

79+
// Only during GC.
80+
bool JSWeakMapImplBase::clearEntryDirect(GC *gc, const WeakRefKey &key) {
81+
assert(gc->inGC() && "Should only be used by the GC implementation.");
82+
DenseMapT::iterator it = map_.find(key);
83+
if (it == map_.end()) {
84+
return false;
85+
}
86+
it->first.ref.clear();
87+
valueStorage_.get(gc->getPointerBase())
88+
->at(it->second)
89+
.setNonPtr(HermesValue::encodeEmptyValue());
90+
return true;
91+
}
92+
93+
HermesValue JSWeakMapImplBase::getValueDirect(GC *gc, const WeakRefKey &key) {
94+
assert(gc->inGC() && "Should only be used by the GC implementation.");
95+
DenseMapT::iterator it = map_.find(key);
96+
if (it == map_.end()) {
97+
return HermesValue::encodeEmptyValue();
98+
}
99+
return valueStorage_.get(gc->getPointerBase())->at(it->second);
100+
}
101+
79102
/// \return true if the \p key exists in the map.
80103
bool JSWeakMapImplBase::hasValue(
81104
Handle<JSWeakMapImplBase> self,
@@ -111,6 +134,23 @@ uint32_t JSWeakMapImplBase::debugFreeSlotsAndGetSize(
111134
return self->map_.size();
112135
}
113136

137+
JSWeakMapImplBase::KeyIterator JSWeakMapImplBase::keys_begin() {
138+
return KeyIterator{map_.begin()};
139+
}
140+
141+
JSWeakMapImplBase::KeyIterator JSWeakMapImplBase::keys_end() {
142+
return KeyIterator{map_.end()};
143+
}
144+
145+
JSObject *detail::WeakRefKey::getObject(GC *gc) const {
146+
assert(gc->inGC() && "Should only be used by the GC implementation.");
147+
if (auto val = ref.unsafeGetOptional()) {
148+
return *val;
149+
} else {
150+
return nullptr;
151+
}
152+
}
153+
114154
void JSWeakMapImplBase::_markWeakImpl(GCCell *cell, WeakRefAcceptor &acceptor) {
115155
auto *self = reinterpret_cast<JSWeakMapImplBase *>(cell);
116156
for (auto it = self->map_.begin(); it != self->map_.end(); ++it) {
@@ -145,7 +185,7 @@ void JSWeakMapImplBase::deleteInternal(
145185
PointerBase *base,
146186
JSWeakMapImplBase::DenseMapT::iterator it) {
147187
assert(it != map_.end() && "Invalid iterator to deleteInternal");
148-
valueStorage_.get(base)
188+
valueStorage_.getNonNull(base)
149189
->at(it->second)
150190
.setNonPtr(HermesValue::encodeNativeUInt32(freeListHead_));
151191
freeListHead_ = it->second;

lib/VM/gcs/CompleteMarkState.cpp

+27-11
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
*/
77

88
#include "hermes/VM/CompleteMarkState.h"
9+
#include "hermes/VM/Casting.h"
910
#include "hermes/VM/CompleteMarkState-inline.h"
1011
#include "hermes/VM/GCBase-inline.h"
1112
#include "hermes/VM/GCBase.h"
13+
#include "hermes/VM/JSWeakMapImpl.h"
1214

1315
namespace hermes {
1416
namespace vm {
@@ -48,16 +50,22 @@ void CompleteMarkState::markTransitive(void *ptr) {
4850
// wouldn't make it past the earlier check for a false value if the
4951
// indices were equal.
5052
if (ptr < reinterpret_cast<void *>(currentParPointer)) {
51-
GCCell *cell = reinterpret_cast<GCCell *>(ptr);
52-
// Push cell to the correct mark stack.
53-
std::vector<GCCell *> *stack =
54-
(cell->isVariableSize() ? &varSizeMarkStack_ : &markStack_);
55-
if (stack->size() == kMarkStackLimit) {
56-
markStackOverflow_ = true;
57-
} else {
58-
stack->push_back(cell);
59-
numPtrsPushedByParent++;
60-
}
53+
pushCell(reinterpret_cast<GCCell *>(ptr));
54+
}
55+
}
56+
57+
void CompleteMarkState::pushCell(GCCell *cell) {
58+
// Push cell to the correct mark stack.
59+
#ifdef HERMES_SLOW_DEBUG
60+
assert(cell->isValid());
61+
#endif
62+
std::vector<GCCell *> *stack =
63+
(cell->isVariableSize() ? &varSizeMarkStack_ : &markStack_);
64+
if (stack->size() == kMarkStackLimit) {
65+
markStackOverflow_ = true;
66+
} else {
67+
stack->push_back(cell);
68+
numPtrsPushedByParent++;
6169
}
6270
}
6371

@@ -78,7 +86,15 @@ void CompleteMarkState::drainMarkStack(
7886
// skipped for fixed sized cells.
7987
numPtrsPushedByParent = 0;
8088

81-
GCBase::markCell(cell, gc, acceptor);
89+
// Don't mark through JSWeakMaps. (Presently, this check is very
90+
// specific. If there are ever other cell kinds that need special
91+
// weak marking handling, we could put a boolean in the vtable or
92+
// metadata table).
93+
if (cell->getKind() == CellKind::WeakMapKind) {
94+
reachableWeakMaps_.push_back(vmcast<JSWeakMap>(cell));
95+
} else {
96+
GCBase::markCell(cell, gc, acceptor);
97+
}
8298

8399
// All fields of a fixed-sized cell should be marked by this point, but var
84100
// sized GCCells may not. Pop if the last round of marking pushed nothing,

lib/VM/gcs/GenGCNC.cpp

+94
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "hermes/VM/GCPointer-inline.h"
2626
#include "hermes/VM/HeapSnapshot.h"
2727
#include "hermes/VM/HermesValue-inline.h"
28+
#include "hermes/VM/JSWeakMapImpl.h"
2829
#include "hermes/VM/Serializer.h"
2930
#include "hermes/VM/SlotAcceptorDefault-inline.h"
3031
#include "hermes/VM/StringPrimitive.h"
@@ -630,9 +631,102 @@ void GenGC::completeMarking() {
630631
if (markState_.markStackOverflow_)
631632
break;
632633
}
634+
// The marking loop above will have accumulated WeakMaps;
635+
// find things reachable from values of reachable keys.
636+
// (Note that this can also set markStackOverflow_).
637+
completeWeakMapMarking();
633638
} while (markState_.markStackOverflow_);
634639
}
635640

641+
/// For all reachable keys in \p weakMap, mark from the corresponding value
642+
/// using \p markState and the given \p acceptor, reaching a
643+
/// transitive closure (or setting \p markState.markStackOverflow_).
644+
/// Returns whether any previously-unmarked values were marked.
645+
static bool markFromReachableWeakMapKeys(
646+
GC *gc,
647+
JSWeakMap *weakMap,
648+
CompleteMarkState *markState,
649+
CompleteMarkState::FullMSCMarkTransitiveAcceptor &acceptor) {
650+
bool newlyMarkedValue = false;
651+
for (auto iter = weakMap->keys_begin(), end = weakMap->keys_end();
652+
iter != end;
653+
iter++) {
654+
GCCell *cell = iter->getObject(gc);
655+
if (!cell || !AlignedHeapSegment::getCellMarkBit(cell)) {
656+
continue;
657+
}
658+
HermesValue val = weakMap->getValueDirect(gc, *iter);
659+
if (val.isPointer()) {
660+
GCCell *valCell = reinterpret_cast<GCCell *>(val.getPointer());
661+
if (!AlignedHeapSegment::getCellMarkBit(valCell)) {
662+
AlignedHeapSegment::setCellMarkBit(valCell);
663+
markState->pushCell(valCell);
664+
markState->drainMarkStack(gc, acceptor);
665+
newlyMarkedValue = true;
666+
}
667+
}
668+
}
669+
return newlyMarkedValue;
670+
}
671+
672+
/// For all non-null keys in \p weakMap that are unreachable, clear
673+
/// the key (clear the pointer in the WeakRefSlot) and value (set it
674+
/// to undefined).
675+
static void clearEntriesWithUnreachableKeys(GenGC *gc, JSWeakMap *weakMap) {
676+
for (auto iter = weakMap->keys_begin(), end = weakMap->keys_end();
677+
iter != end;
678+
iter++) {
679+
JSObject *keyObj = iter->getObject(gc);
680+
if (keyObj && !AlignedHeapSegment::getCellMarkBit(keyObj)) {
681+
weakMap->clearEntryDirect(gc, *iter);
682+
}
683+
}
684+
}
685+
686+
void GenGC::completeWeakMapMarking() {
687+
CompleteMarkState::FullMSCMarkTransitiveAcceptor acceptor(*this, &markState_);
688+
689+
// Set the currentParPointer to a maximal value, so all pointers scanned
690+
// will be pushed on the mark stack.
691+
markState_.currentParPointer =
692+
reinterpret_cast<GCCell *>(static_cast<intptr_t>(-1));
693+
// Must declare this outside the loop, but the initial value doesn't matter:
694+
// we make it false at the start of each loop iteration.
695+
bool newReachableValueFound = true;
696+
do {
697+
newReachableValueFound = false;
698+
// Note that new reachable weak maps may be discovered during the loop, so
699+
// markState_.reachableWeakMaps_.size() may increase during the loop.
700+
for (unsigned i = 0; i < markState_.reachableWeakMaps_.size(); i++) {
701+
JSWeakMap *weakMap = markState_.reachableWeakMaps_[i];
702+
if (markFromReachableWeakMapKeys(this, weakMap, &markState_, acceptor)) {
703+
newReachableValueFound = true;
704+
}
705+
}
706+
} while (newReachableValueFound);
707+
for (auto *weakMap : markState_.reachableWeakMaps_) {
708+
clearEntriesWithUnreachableKeys(this, weakMap);
709+
// The argument for why this works is delicate. We need to call
710+
// markCell, because it marks fields of the WeakMap that are not
711+
// part of the table -- in particular, the pointer to the
712+
// valueStorage_ array. There would be a problem, however, if
713+
// this markCell/drain pair could cause any keys of any WeakMaps
714+
// to become newly reachable. If so, we'd need to scan their
715+
// corresponding values. This cannot happen, however. The
716+
// marking loop above ensured that all values in the valueStorage_
717+
// corresponding to reachable keys have been marked. The call
718+
// just above ensures that values corresponding to unreachable
719+
// keys have been cleared. So we'll mark *only* the valueStorage_
720+
// array; the drainMarkStack call should not do any work. Perhaps
721+
// we should assert this.
722+
GCBase::markCell(weakMap, this, acceptor);
723+
markState_.drainMarkStack(this, acceptor);
724+
}
725+
726+
markState_.currentParPointer = nullptr;
727+
markState_.reachableWeakMaps_.clear();
728+
}
729+
636730
void GenGC::finalizeUnreachableObjects() {
637731
youngGen_.finalizeUnreachableObjects();
638732
oldGen_.finalizeUnreachableObjects();

0 commit comments

Comments
 (0)