diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/vm/Shape.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | js/src/vm/Shape.cpp | 2224 |
1 files changed, 2224 insertions, 0 deletions
diff --git a/js/src/vm/Shape.cpp b/js/src/vm/Shape.cpp new file mode 100644 index 0000000000..406af24c3a --- /dev/null +++ b/js/src/vm/Shape.cpp @@ -0,0 +1,2224 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* JS symbol tables. */ + +#include "vm/Shape-inl.h" + +#include "mozilla/MathAlgorithms.h" +#include "mozilla/PodOperations.h" + +#include "gc/FreeOp.h" +#include "gc/HashUtil.h" +#include "gc/Policy.h" +#include "gc/PublicIterators.h" +#include "js/HashTable.h" +#include "js/UniquePtr.h" +#include "util/Text.h" +#include "vm/JSAtom.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" + +#include "vm/Caches-inl.h" +#include "vm/JSContext-inl.h" +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" +#include "vm/Realm-inl.h" + +using namespace js; + +using mozilla::CeilingLog2Size; +using mozilla::PodZero; + +using JS::AutoCheckCannotGC; + +Shape* const ShapeTable::Entry::SHAPE_REMOVED = + (Shape*)ShapeTable::Entry::SHAPE_COLLISION; + +bool ShapeIC::init(JSContext* cx) { + size_ = MAX_SIZE; + entries_.reset(cx->pod_calloc<Entry>(size_)); + return (!entries_) ? false : true; +} + +bool ShapeTable::init(JSContext* cx, Shape* lastProp) { + uint32_t sizeLog2 = CeilingLog2Size(entryCount_); + uint32_t size = Bit(sizeLog2); + if (entryCount_ >= size - (size >> 2)) { + sizeLog2++; + } + if (sizeLog2 < MIN_SIZE_LOG2) { + sizeLog2 = MIN_SIZE_LOG2; + } + + size = Bit(sizeLog2); + entries_.reset(cx->pod_calloc<Entry>(size)); + if (!entries_) { + return false; + } + + MOZ_ASSERT(sizeLog2 <= HASH_BITS); + hashShift_ = HASH_BITS - sizeLog2; + + for (Shape::Range<NoGC> r(lastProp); !r.empty(); r.popFront()) { + Shape& shape = r.front(); + Entry& entry = searchUnchecked<MaybeAdding::Adding>(shape.propid()); + + /* + * Beware duplicate args and arg vs. var conflicts: the youngest shape + * (nearest to lastProp) must win. See bug 600067. + */ + if (!entry.shape()) { + entry.setPreservingCollision(&shape); + } + } + + MOZ_ASSERT(capacity() == size); + MOZ_ASSERT(size >= MIN_SIZE); + MOZ_ASSERT(!needsToGrow()); + return true; +} + +void Shape::removeFromDictionary(NativeObject* obj) { + MOZ_ASSERT(inDictionary()); + MOZ_ASSERT(obj->inDictionaryMode()); + MOZ_ASSERT(!dictNext.isNone()); + + MOZ_ASSERT(obj->shape()->inDictionary()); + MOZ_ASSERT(obj->shape()->dictNext.toObject() == obj); + + if (parent) { + parent->setDictionaryNextPtr(dictNext); + } + *dictNext.prevPtr() = parent; + clearDictionaryNextPtr(); + + obj->shape()->clearCachedBigEnoughForShapeTable(); +} + +void Shape::insertIntoDictionaryBefore(DictionaryShapeLink next) { + // Don't assert inDictionaryMode() here because we may be called from + // NativeObject::toDictionaryMode via Shape::initDictionaryShape. + MOZ_ASSERT(inDictionary()); + MOZ_ASSERT(dictNext.isNone()); + + Shape* prev = *next.prevPtr(); + +#ifdef DEBUG + if (prev) { + MOZ_ASSERT(prev->inDictionary()); + MOZ_ASSERT(prev->dictNext == next); + MOZ_ASSERT(zone() == prev->zone()); + } +#endif + + setParent(prev); + if (parent) { + parent->setNextDictionaryShape(this); + } + + setDictionaryNextPtr(next); + *dictNext.prevPtr() = this; +} + +bool Shape::makeOwnBaseShape(JSContext* cx) { + MOZ_ASSERT(!base()->isOwned()); + MOZ_ASSERT(cx->zone() == zone()); + + BaseShape* nbase = Allocate<BaseShape, NoGC>(cx); + if (!nbase) { + return false; + } + + new (nbase) BaseShape(StackBaseShape(this)); + nbase->setOwned(base()->toUnowned()); + + setBase(nbase); + + return true; +} + +void Shape::handoffTableTo(Shape* shape) { + MOZ_ASSERT(inDictionary() && shape->inDictionary()); + + if (this == shape) { + return; + } + + MOZ_ASSERT(base()->isOwned() && !shape->base()->isOwned()); + + BaseShape* nbase = base(); + + setBase(nbase->baseUnowned()); + nbase->adoptUnowned(shape->base()->toUnowned()); + + shape->setBase(nbase); +} + +/* static */ +bool Shape::hashify(JSContext* cx, Shape* shape) { + MOZ_ASSERT(!shape->hasTable()); + + if (!shape->ensureOwnBaseShape(cx)) { + return false; + } + + UniquePtr<ShapeTable> table = + cx->make_unique<ShapeTable>(shape->entryCount()); + if (!table) { + return false; + } + + if (!table->init(cx, shape)) { + return false; + } + + BaseShape* base = shape->base(); + base->maybePurgeCache(cx->defaultFreeOp()); + base->setTable(table.release()); + // TODO: The contents of ShapeTable is not currently tracked, only the object + // itself. + AddCellMemory(base, sizeof(ShapeTable), MemoryUse::ShapeCache); + return true; +} + +void ShapeCachePtr::maybePurgeCache(JSFreeOp* fop, BaseShape* base) { + if (isTable()) { + ShapeTable* table = getTablePointer(); + if (table->freeList() == SHAPE_INVALID_SLOT) { + fop->delete_(base, getTablePointer(), MemoryUse::ShapeCache); + p = 0; + } + } else if (isIC()) { + fop->delete_<ShapeIC>(base, getICPointer(), MemoryUse::ShapeCache); + p = 0; + } +} + +/* static */ +bool Shape::cachify(JSContext* cx, Shape* shape) { + MOZ_ASSERT(!shape->hasTable() && !shape->hasIC()); + + if (!shape->ensureOwnBaseShape(cx)) { + return false; + } + + UniquePtr<ShapeIC> ic = cx->make_unique<ShapeIC>(); + if (!ic) { + return false; + } + + if (!ic->init(cx)) { + return false; + } + + shape->base()->setIC(ic.release()); + AddCellMemory(shape->base(), sizeof(ShapeIC), MemoryUse::ShapeCache); + return true; +} + +bool ShapeTable::change(JSContext* cx, int log2Delta) { + MOZ_ASSERT(entries_); + MOZ_ASSERT(-1 <= log2Delta && log2Delta <= 1); + + /* + * Grow, shrink, or compress by changing this->entries_. + */ + uint32_t oldLog2 = HASH_BITS - hashShift_; + uint32_t newLog2 = oldLog2 + log2Delta; + uint32_t oldSize = Bit(oldLog2); + uint32_t newSize = Bit(newLog2); + Entry* newTable = cx->maybe_pod_calloc<Entry>(newSize); + if (!newTable) { + return false; + } + + /* Now that we have newTable allocated, update members. */ + MOZ_ASSERT(newLog2 <= HASH_BITS); + hashShift_ = HASH_BITS - newLog2; + removedCount_ = 0; + Entry* oldTable = entries_.release(); + entries_.reset(newTable); + + /* Copy only live entries, leaving removed and free ones behind. */ + AutoCheckCannotGC nogc; + for (Entry* oldEntry = oldTable; oldSize != 0; oldEntry++) { + if (Shape* shape = oldEntry->shape()) { + Entry& entry = search<MaybeAdding::Adding>(shape->propid(), nogc); + MOZ_ASSERT(entry.isFree()); + entry.setShape(shape); + } + oldSize--; + } + + MOZ_ASSERT(capacity() == newSize); + + /* Finally, free the old entries storage. */ + js_free(oldTable); + return true; +} + +bool ShapeTable::grow(JSContext* cx) { + MOZ_ASSERT(needsToGrow()); + + uint32_t size = capacity(); + int delta = removedCount_ < (size >> 2); + + MOZ_ASSERT(entryCount_ + removedCount_ <= size - 1); + + if (!change(cx, delta)) { + if (entryCount_ + removedCount_ == size - 1) { + ReportOutOfMemory(cx); + return false; + } + } + + return true; +} + +void ShapeCachePtr::trace(JSTracer* trc) { + if (isIC()) { + getICPointer()->trace(trc); + } else if (isTable()) { + getTablePointer()->trace(trc); + } +} + +void ShapeIC::trace(JSTracer* trc) { + for (size_t i = 0; i < entryCount(); i++) { + Entry& entry = entries_[i]; + if (entry.shape_) { + TraceManuallyBarrieredEdge(trc, &entry.shape_, "ShapeIC shape"); + } + } +} + +void ShapeTable::trace(JSTracer* trc) { + for (size_t i = 0; i < capacity(); i++) { + Entry& entry = getEntry(i); + Shape* shape = entry.shape(); + if (shape) { + TraceManuallyBarrieredEdge(trc, &shape, "ShapeTable shape"); + if (shape != entry.shape()) { + entry.setPreservingCollision(shape); + } + } + } +} + +inline void ShapeCachePtr::destroy(JSFreeOp* fop, BaseShape* base) { + if (isTable()) { + fop->delete_(base, getTablePointer(), MemoryUse::ShapeCache); + } else if (isIC()) { + fop->delete_(base, getICPointer(), MemoryUse::ShapeCache); + } + p = 0; +} + +#ifdef JSGC_HASH_TABLE_CHECKS + +void ShapeCachePtr::checkAfterMovingGC() { + if (isIC()) { + getICPointer()->checkAfterMovingGC(); + } else if (isTable()) { + getTablePointer()->checkAfterMovingGC(); + } +} + +void ShapeIC::checkAfterMovingGC() { + for (size_t i = 0; i < entryCount(); i++) { + Entry& entry = entries_[i]; + Shape* shape = entry.shape_; + if (shape) { + CheckGCThingAfterMovingGC(shape); + } + } +} + +void ShapeTable::checkAfterMovingGC() { + for (size_t i = 0; i < capacity(); i++) { + Entry& entry = getEntry(i); + Shape* shape = entry.shape(); + if (shape) { + CheckGCThingAfterMovingGC(shape); + } + } +} + +#endif + +/* static */ +Shape* Shape::replaceLastProperty(JSContext* cx, StackBaseShape& base, + TaggedProto proto, HandleShape shape) { + MOZ_ASSERT(!shape->inDictionary()); + + if (!shape->parent) { + /* Treat as resetting the initial property of the shape hierarchy. */ + gc::AllocKind kind = gc::GetGCObjectKind(shape->numFixedSlots()); + return EmptyShape::getInitialShape( + cx, base.clasp, proto, kind, base.flags & BaseShape::OBJECT_FLAG_MASK); + } + + UnownedBaseShape* nbase = BaseShape::getUnowned(cx, base); + if (!nbase) { + return nullptr; + } + + Rooted<StackShape> child(cx, StackShape(shape)); + child.setBase(nbase); + + return cx->zone()->propertyTree().getChild(cx, shape->parent, child); +} + +/* + * Get or create a property-tree or dictionary child property of |parent|, + * which must be lastProperty() if inDictionaryMode(), else parent must be + * one of lastProperty() or lastProperty()->parent. + */ +/* static */ MOZ_ALWAYS_INLINE Shape* NativeObject::getChildDataProperty( + JSContext* cx, HandleNativeObject obj, HandleShape parent, + MutableHandle<StackShape> child) { + MOZ_ASSERT(child.isDataProperty()); + + if (child.hasMissingSlot()) { + uint32_t slot; + if (obj->inDictionaryMode()) { + if (!allocDictionarySlot(cx, obj, &slot)) { + return nullptr; + } + } else { + slot = obj->slotSpan(); + MOZ_ASSERT(slot >= JSSLOT_FREE(obj->getClass())); + // Objects with many properties are converted to dictionary + // mode, so we can't overflow SHAPE_MAXIMUM_SLOT here. + MOZ_ASSERT(slot < + JSSLOT_FREE(obj->getClass()) + PropertyTree::MAX_HEIGHT); + MOZ_ASSERT(slot < SHAPE_MAXIMUM_SLOT); + } + child.setSlot(slot); + } else { + /* + * Slots can only be allocated out of order on objects in + * dictionary mode. Otherwise the child's slot must be after the + * parent's slot (if it has one), because slot number determines + * slot span for objects with that shape. Usually child slot + * *immediately* follows parent slot, but there may be a slot gap + * when the object uses some -- but not all -- of its reserved + * slots to store properties. + */ + MOZ_ASSERT(obj->inDictionaryMode() || parent->hasMissingSlot() || + child.slot() == parent->maybeSlot() + 1 || + (parent->maybeSlot() + 1 < JSSLOT_FREE(obj->getClass()) && + child.slot() == JSSLOT_FREE(obj->getClass()))); + } + + if (obj->inDictionaryMode()) { + MOZ_ASSERT(parent == obj->lastProperty()); + Shape* shape = Allocate<Shape>(cx); + if (!shape) { + return nullptr; + } + if (child.slot() >= obj->slotSpan()) { + if (!obj->ensureSlotsForDictionaryObject(cx, child.slot() + 1)) { + new (shape) Shape(obj->lastProperty()->base()->unowned(), 0); + return nullptr; + } + } + shape->initDictionaryShape(child, obj->numFixedSlots(), + DictionaryShapeLink(obj)); + return shape; + } + + Shape* shape = cx->zone()->propertyTree().inlinedGetChild(cx, parent, child); + if (!shape) { + return nullptr; + } + + MOZ_ASSERT(shape->parent == parent); + MOZ_ASSERT_IF(parent != obj->lastProperty(), + parent == obj->lastProperty()->parent); + + if (!obj->setLastProperty(cx, shape)) { + return nullptr; + } + return shape; +} + +/* static */ MOZ_ALWAYS_INLINE Shape* NativeObject::getChildAccessorProperty( + JSContext* cx, HandleNativeObject obj, HandleShape parent, + MutableHandle<StackShape> child) { + MOZ_ASSERT(!child.isDataProperty()); + + // Accessor properties have no slot, but slot_ will reflect that of parent. + child.setSlot(parent->maybeSlot()); + + if (obj->inDictionaryMode()) { + MOZ_ASSERT(parent == obj->lastProperty()); + Shape* shape = Allocate<AccessorShape>(cx); + if (!shape) { + return nullptr; + } + shape->initDictionaryShape(child, obj->numFixedSlots(), + DictionaryShapeLink(obj)); + return shape; + } + + Shape* shape = cx->zone()->propertyTree().inlinedGetChild(cx, parent, child); + if (!shape) { + return nullptr; + } + + MOZ_ASSERT(shape->parent == parent); + MOZ_ASSERT_IF(parent != obj->lastProperty(), + parent == obj->lastProperty()->parent); + + if (!obj->setLastProperty(cx, shape)) { + return nullptr; + } + return shape; +} + +/* static */ +bool js::NativeObject::toDictionaryMode(JSContext* cx, HandleNativeObject obj) { + MOZ_ASSERT(!obj->inDictionaryMode()); + MOZ_ASSERT(cx->isInsideCurrentCompartment(obj)); + + uint32_t span = obj->slotSpan(); + + // Clone the shapes into a new dictionary list. Don't update the last + // property of this object until done, otherwise a GC triggered while + // creating the dictionary will get the wrong slot span for this object. + RootedShape root(cx); + RootedShape dictionaryShape(cx); + + RootedShape shape(cx, obj->lastProperty()); + while (shape) { + MOZ_ASSERT(!shape->inDictionary()); + + Shape* dprop = shape->isAccessorShape() ? Allocate<AccessorShape>(cx) + : Allocate<Shape>(cx); + if (!dprop) { + ReportOutOfMemory(cx); + return false; + } + + DictionaryShapeLink next; + if (dictionaryShape) { + next.setShape(dictionaryShape); + } + StackShape child(shape); + dprop->initDictionaryShape(child, obj->numFixedSlots(), next); + + if (!dictionaryShape) { + root = dprop; + } + + MOZ_ASSERT(!dprop->hasTable()); + dictionaryShape = dprop; + shape = shape->previous(); + } + + if (!Shape::hashify(cx, root)) { + ReportOutOfMemory(cx); + return false; + } + + if (IsInsideNursery(obj) && + !cx->nursery().queueDictionaryModeObjectToSweep(obj)) { + ReportOutOfMemory(cx); + return false; + } + + MOZ_ASSERT(root->dictNext.isNone()); + root->setDictionaryObject(obj); + obj->setShape(root); + + MOZ_ASSERT(obj->inDictionaryMode()); + obj->setDictionaryModeSlotSpan(span); + + return true; +} + +static bool ShouldConvertToDictionary(NativeObject* obj) { + /* + * Use a lower limit if this object is likely a hashmap (SETELEM was used + * to set properties). + */ + if (obj->hadElementsAccess()) { + return obj->lastProperty()->entryCount() >= + PropertyTree::MAX_HEIGHT_WITH_ELEMENTS_ACCESS; + } + return obj->lastProperty()->entryCount() >= PropertyTree::MAX_HEIGHT; +} + +static MOZ_ALWAYS_INLINE UnownedBaseShape* GetBaseShapeForNewShape( + JSContext* cx, HandleShape last, HandleId id) { + uint32_t index; + bool indexed = IdIsIndex(id, &index); + bool interestingSymbol = + JSID_IS_SYMBOL(id) && JSID_TO_SYMBOL(id)->isInterestingSymbol(); + + if (MOZ_LIKELY(!indexed && !interestingSymbol)) { + return last->base()->unowned(); + } + + StackBaseShape base(last->base()); + if (indexed) { + base.flags |= BaseShape::INDEXED; + } else if (interestingSymbol) { + base.flags |= BaseShape::HAS_INTERESTING_SYMBOL; + } + return BaseShape::getUnowned(cx, base); +} + +namespace js { + +class MOZ_RAII AutoCheckShapeConsistency { +#ifdef DEBUG + HandleNativeObject obj_; +#endif + + public: + explicit AutoCheckShapeConsistency(HandleNativeObject obj) +#ifdef DEBUG + : obj_(obj) +#endif + { + } + +#ifdef DEBUG + ~AutoCheckShapeConsistency() { obj_->checkShapeConsistency(); } +#endif +}; + +} // namespace js + +/* static */ MOZ_ALWAYS_INLINE bool +NativeObject::maybeConvertToOrGrowDictionaryForAdd( + JSContext* cx, HandleNativeObject obj, HandleId id, ShapeTable** table, + ShapeTable::Entry** entry, const AutoKeepShapeCaches& keep) { + MOZ_ASSERT(!!*table == !!*entry); + + // The code below deals with either converting obj to dictionary mode or + // growing an object that's already in dictionary mode. + if (!obj->inDictionaryMode()) { + if (!ShouldConvertToDictionary(obj)) { + return true; + } + if (!toDictionaryMode(cx, obj)) { + return false; + } + *table = obj->lastProperty()->maybeTable(keep); + } else { + if (!(*table)->needsToGrow()) { + return true; + } + if (!(*table)->grow(cx)) { + return false; + } + } + + *entry = &(*table)->search<MaybeAdding::Adding>(id, keep); + MOZ_ASSERT(!(*entry)->shape()); + return true; +} + +MOZ_ALWAYS_INLINE void Shape::updateDictionaryTable( + ShapeTable* table, ShapeTable::Entry* entry, + const AutoKeepShapeCaches& keep) { + MOZ_ASSERT(table); + MOZ_ASSERT(entry); + MOZ_ASSERT(inDictionary()); + + // Store this Shape in the table entry. + entry->setPreservingCollision(this); + table->incEntryCount(); + + // Pass the table along to the new last property, namely *this. + MOZ_ASSERT(parent->maybeTable(keep) == table); + parent->handoffTableTo(this); +} + +static void AssertValidPropertyOp(NativeObject* obj, GetterOp getter, + SetterOp setter, unsigned attrs) { + // We only support PropertyOp accessors on ArrayObject and ArgumentsObject + // and we don't want to add more of these properties (bug 1404885). + +#ifdef DEBUG + if ((getter && !(attrs & JSPROP_GETTER)) || + (setter && !(attrs & JSPROP_SETTER))) { + MOZ_ASSERT(obj->is<ArrayObject>() || obj->is<ArgumentsObject>()); + } +#endif +} + +/* static */ +Shape* NativeObject::addAccessorPropertyInternal( + JSContext* cx, HandleNativeObject obj, HandleId id, GetterOp getter, + SetterOp setter, unsigned attrs, ShapeTable* table, + ShapeTable::Entry* entry, const AutoKeepShapeCaches& keep) { + AutoCheckShapeConsistency check(obj); + AutoRooterGetterSetter gsRoot(cx, attrs, &getter, &setter); + + AssertValidPropertyOp(obj, getter, setter, attrs); + + if (!maybeConvertToOrGrowDictionaryForAdd(cx, obj, id, &table, &entry, + keep)) { + return nullptr; + } + + // Find or create a property tree node labeled by our arguments. + RootedShape shape(cx); + { + RootedShape last(cx, obj->lastProperty()); + Rooted<UnownedBaseShape*> nbase(cx, GetBaseShapeForNewShape(cx, last, id)); + if (!nbase) { + return nullptr; + } + + Rooted<StackShape> child(cx, + StackShape(nbase, id, SHAPE_INVALID_SLOT, attrs)); + child.updateGetterSetter(getter, setter); + shape = getChildAccessorProperty(cx, obj, last, &child); + if (!shape) { + return nullptr; + } + } + + MOZ_ASSERT(shape == obj->lastProperty()); + + if (table) { + shape->updateDictionaryTable(table, entry, keep); + } + + return shape; +} + +/* static */ +Shape* NativeObject::addDataPropertyInternal(JSContext* cx, + HandleNativeObject obj, + HandleId id, uint32_t slot, + unsigned attrs, ShapeTable* table, + ShapeTable::Entry* entry, + const AutoKeepShapeCaches& keep) { + AutoCheckShapeConsistency check(obj); + + // The slot, if any, must be a reserved slot. + MOZ_ASSERT(slot == SHAPE_INVALID_SLOT || + slot < JSCLASS_RESERVED_SLOTS(obj->getClass())); + + if (!maybeConvertToOrGrowDictionaryForAdd(cx, obj, id, &table, &entry, + keep)) { + return nullptr; + } + + // Find or create a property tree node labeled by our arguments. + RootedShape shape(cx); + { + RootedShape last(cx, obj->lastProperty()); + Rooted<UnownedBaseShape*> nbase(cx, GetBaseShapeForNewShape(cx, last, id)); + if (!nbase) { + return nullptr; + } + + Rooted<StackShape> child(cx, StackShape(nbase, id, slot, attrs)); + shape = getChildDataProperty(cx, obj, last, &child); + if (!shape) { + return nullptr; + } + } + + MOZ_ASSERT(shape == obj->lastProperty()); + + if (table) { + shape->updateDictionaryTable(table, entry, keep); + } + + return shape; +} + +static MOZ_ALWAYS_INLINE Shape* PropertyTreeReadBarrier(JSContext* cx, + Shape* parent, + Shape* shape) { + JS::Zone* zone = shape->zone(); + if (zone->needsIncrementalBarrier()) { + // We need a read barrier for the shape tree, since these are weak + // pointers. + Shape* tmp = shape; + TraceManuallyBarrieredEdge(zone->barrierTracer(), &tmp, "read barrier"); + MOZ_ASSERT(tmp == shape); + return shape; + } + + if (MOZ_LIKELY(!zone->isGCSweepingOrCompacting() || + !IsAboutToBeFinalizedUnbarriered(&shape))) { + if (shape->isMarkedGray()) { + UnmarkGrayShapeRecursively(shape); + } + return shape; + } + + // The shape we've found is unreachable and due to be finalized, so + // remove our weak reference to it and don't use it. + MOZ_ASSERT(parent->isMarkedAny()); + parent->removeChild(cx->defaultFreeOp(), shape); + + return nullptr; +} + +/* static */ +Shape* NativeObject::addEnumerableDataProperty(JSContext* cx, + HandleNativeObject obj, + HandleId id) { + // Like addProperty(Internal), but optimized for the common case of adding a + // new enumerable data property. + + AutoCheckShapeConsistency check(obj); + + // Fast path for non-dictionary shapes with a single child. + do { + AutoCheckCannotGC nogc; + + Shape* lastProperty = obj->lastProperty(); + if (lastProperty->inDictionary()) { + break; + } + + ShapeChildren* childp = &lastProperty->children; + if (!childp->isSingleShape()) { + break; + } + + Shape* child = childp->toSingleShape(); + MOZ_ASSERT(!child->inDictionary()); + + if (child->propidRaw() != id || child->isAccessorShape() || + child->attributes() != JSPROP_ENUMERATE || + child->base()->unowned() != lastProperty->base()->unowned()) { + break; + } + + MOZ_ASSERT(child->isDataProperty()); + + child = PropertyTreeReadBarrier(cx, lastProperty, child); + if (!child) { + break; + } + + if (!obj->setLastProperty(cx, child)) { + return nullptr; + } + return child; + } while (0); + + AutoKeepShapeCaches keep(cx); + ShapeTable* table = nullptr; + ShapeTable::Entry* entry = nullptr; + + if (!obj->inDictionaryMode()) { + if (MOZ_UNLIKELY(ShouldConvertToDictionary(obj))) { + if (!toDictionaryMode(cx, obj)) { + return nullptr; + } + table = obj->lastProperty()->maybeTable(keep); + entry = &table->search<MaybeAdding::Adding>(id, keep); + } + } else { + table = obj->lastProperty()->ensureTableForDictionary(cx, keep); + if (!table) { + return nullptr; + } + if (table->needsToGrow()) { + if (!table->grow(cx)) { + return nullptr; + } + } + entry = &table->search<MaybeAdding::Adding>(id, keep); + MOZ_ASSERT(!entry->shape()); + } + + MOZ_ASSERT(!!table == !!entry); + + /* Find or create a property tree node labeled by our arguments. */ + RootedShape last(cx, obj->lastProperty()); + UnownedBaseShape* nbase = GetBaseShapeForNewShape(cx, last, id); + if (!nbase) { + return nullptr; + } + + Shape* shape; + if (obj->inDictionaryMode()) { + uint32_t slot; + if (!allocDictionarySlot(cx, obj, &slot)) { + return nullptr; + } + + Rooted<StackShape> child(cx, StackShape(nbase, id, slot, JSPROP_ENUMERATE)); + + MOZ_ASSERT(last == obj->lastProperty()); + shape = Allocate<Shape>(cx); + if (!shape) { + return nullptr; + } + if (slot >= obj->slotSpan()) { + if (MOZ_UNLIKELY(!obj->ensureSlotsForDictionaryObject(cx, slot + 1))) { + new (shape) Shape(obj->lastProperty()->base()->unowned(), 0); + return nullptr; + } + } + shape->initDictionaryShape(child, obj->numFixedSlots(), + DictionaryShapeLink(obj)); + } else { + uint32_t slot = obj->slotSpan(); + MOZ_ASSERT(slot >= JSSLOT_FREE(obj->getClass())); + // Objects with many properties are converted to dictionary + // mode, so we can't overflow SHAPE_MAXIMUM_SLOT here. + MOZ_ASSERT(slot < JSSLOT_FREE(obj->getClass()) + PropertyTree::MAX_HEIGHT); + MOZ_ASSERT(slot < SHAPE_MAXIMUM_SLOT); + + Rooted<StackShape> child(cx, StackShape(nbase, id, slot, JSPROP_ENUMERATE)); + shape = cx->zone()->propertyTree().inlinedGetChild(cx, last, child); + if (!shape) { + return nullptr; + } + if (!obj->setLastProperty(cx, shape)) { + return nullptr; + } + } + + MOZ_ASSERT(shape == obj->lastProperty()); + + if (table) { + shape->updateDictionaryTable(table, entry, keep); + } + + return shape; +} + +/* + * Assert some invariants that should hold when changing properties. It's the + * responsibility of the callers to ensure these hold. + */ +static void AssertCanChangeAttrs(Shape* shape, unsigned attrs) { +#ifdef DEBUG + if (shape->configurable()) { + return; + } + + /* A permanent property must stay permanent. */ + MOZ_ASSERT(attrs & JSPROP_PERMANENT); + + /* Reject attempts to remove a slot from the permanent data property. */ + MOZ_ASSERT_IF(shape->isDataProperty(), + !(attrs & (JSPROP_GETTER | JSPROP_SETTER))); +#endif +} + +static void AssertValidArrayIndex(NativeObject* obj, jsid id) { +#ifdef DEBUG + if (obj->is<ArrayObject>()) { + ArrayObject* arr = &obj->as<ArrayObject>(); + uint32_t index; + if (IdIsIndex(id, &index)) { + MOZ_ASSERT(index < arr->length() || arr->lengthIsWritable()); + } + } +#endif +} + +/* static */ +bool NativeObject::maybeToDictionaryModeForPut(JSContext* cx, + HandleNativeObject obj, + MutableHandleShape shape) { + // Overwriting a non-last property requires switching to dictionary mode. + // The shape tree is shared immutable, and we can't removeProperty and then + // addAccessorPropertyInternal because a failure under add would lose data. + + if (shape == obj->lastProperty() || obj->inDictionaryMode()) { + return true; + } + + if (!toDictionaryMode(cx, obj)) { + return false; + } + + AutoCheckCannotGC nogc; + ShapeTable* table = obj->lastProperty()->maybeTable(nogc); + MOZ_ASSERT(table); + shape.set( + table->search<MaybeAdding::NotAdding>(shape->propid(), nogc).shape()); + return true; +} + +/* static */ +Shape* NativeObject::putDataProperty(JSContext* cx, HandleNativeObject obj, + HandleId id, unsigned attrs) { + MOZ_ASSERT(!JSID_IS_VOID(id)); + + AutoCheckShapeConsistency check(obj); + AssertValidArrayIndex(obj, id); + + // Search for id in order to claim its entry if table has been allocated. + AutoKeepShapeCaches keep(cx); + RootedShape shape(cx); + { + ShapeTable* table; + ShapeTable::Entry* entry; + if (!Shape::search<MaybeAdding::Adding>(cx, obj->lastProperty(), id, keep, + shape.address(), &table, &entry)) { + return nullptr; + } + + if (!shape) { + MOZ_ASSERT( + obj->isExtensible() || + (JSID_IS_INT(id) && obj->containsDenseElement(JSID_TO_INT(id))), + "Can't add new property to non-extensible object"); + return addDataPropertyInternal(cx, obj, id, SHAPE_INVALID_SLOT, attrs, + table, entry, keep); + } + + // Property exists: search must have returned a valid entry. + MOZ_ASSERT_IF(entry, !entry->isRemoved()); + } + + AssertCanChangeAttrs(shape, attrs); + + // If the caller wants to allocate a slot, but doesn't care which slot, + // copy the existing shape's slot into slot so we can match shape, if all + // other members match. + bool hadSlot = shape->isDataProperty(); + uint32_t oldSlot = shape->maybeSlot(); + uint32_t slot = hadSlot ? oldSlot : SHAPE_INVALID_SLOT; + + Rooted<UnownedBaseShape*> nbase(cx); + { + RootedShape shape(cx, obj->lastProperty()); + nbase = GetBaseShapeForNewShape(cx, shape, id); + if (!nbase) { + return nullptr; + } + } + + // Now that we've possibly preserved slot, check whether all members match. + // If so, this is a redundant "put" and we can return without more work. + if (shape->matchesParamsAfterId(nbase, slot, attrs, nullptr, nullptr)) { + return shape; + } + + if (!maybeToDictionaryModeForPut(cx, obj, &shape)) { + return nullptr; + } + + MOZ_ASSERT_IF(shape->isDataProperty(), shape->slot() == slot); + + if (obj->inDictionaryMode()) { + // Updating some property in a dictionary-mode object. Create a new + // shape for the existing property, and also generate a new shape for + // the last property of the dictionary (unless the modified property + // is also the last property). + bool updateLast = (shape == obj->lastProperty()); + shape = NativeObject::replaceWithNewEquivalentShape( + cx, obj, shape, nullptr, + /* accessorShape = */ false); + if (!shape) { + return nullptr; + } + if (!updateLast && !NativeObject::generateOwnShape(cx, obj)) { + return nullptr; + } + + if (slot == SHAPE_INVALID_SLOT) { + if (!allocDictionarySlot(cx, obj, &slot)) { + return nullptr; + } + } + + if (updateLast) { + shape->base()->adoptUnowned(nbase); + } else { + shape->setBase(nbase); + } + + shape->setSlot(slot); + shape->attrs = uint8_t(attrs); + shape->immutableFlags &= ~Shape::ACCESSOR_SHAPE; + shape->immutableFlags |= Shape::IN_DICTIONARY; + } else { + // Updating the last property in a non-dictionary-mode object. Find an + // alternate shared child of the last property's previous shape. + + MOZ_ASSERT(shape == obj->lastProperty()); + + // Find or create a property tree node labeled by our arguments. + Rooted<StackShape> child(cx, StackShape(nbase, id, slot, attrs)); + RootedShape parent(cx, shape->parent); + shape = getChildDataProperty(cx, obj, parent, &child); + if (!shape) { + return nullptr; + } + } + + MOZ_ASSERT(shape->isDataProperty()); + return shape; +} + +/* static */ +Shape* NativeObject::putAccessorProperty(JSContext* cx, HandleNativeObject obj, + HandleId id, GetterOp getter, + SetterOp setter, unsigned attrs) { + MOZ_ASSERT(!JSID_IS_VOID(id)); + + AutoCheckShapeConsistency check(obj); + AssertValidArrayIndex(obj, id); + AssertValidPropertyOp(obj, getter, setter, attrs); + + AutoRooterGetterSetter gsRoot(cx, attrs, &getter, &setter); + + // Search for id in order to claim its entry if table has been allocated. + AutoKeepShapeCaches keep(cx); + RootedShape shape(cx); + { + ShapeTable* table; + ShapeTable::Entry* entry; + if (!Shape::search<MaybeAdding::Adding>(cx, obj->lastProperty(), id, keep, + shape.address(), &table, &entry)) { + return nullptr; + } + + if (!shape) { + MOZ_ASSERT( + obj->isExtensible() || + (JSID_IS_INT(id) && obj->containsDenseElement(JSID_TO_INT(id))), + "Can't add new property to non-extensible object"); + return addAccessorPropertyInternal(cx, obj, id, getter, setter, attrs, + table, entry, keep); + } + + // Property exists: search must have returned a valid entry. + MOZ_ASSERT_IF(entry, !entry->isRemoved()); + } + + AssertCanChangeAttrs(shape, attrs); + + bool hadSlot = shape->isDataProperty(); + uint32_t oldSlot = shape->maybeSlot(); + + Rooted<UnownedBaseShape*> nbase(cx); + { + RootedShape shape(cx, obj->lastProperty()); + nbase = GetBaseShapeForNewShape(cx, shape, id); + if (!nbase) { + return nullptr; + } + } + + // Check whether all members match. If so, this is a redundant "put" and we + // can return without more work. + if (shape->matchesParamsAfterId(nbase, SHAPE_INVALID_SLOT, attrs, getter, + setter)) { + return shape; + } + + if (!maybeToDictionaryModeForPut(cx, obj, &shape)) { + return nullptr; + } + + if (obj->inDictionaryMode()) { + // Updating some property in a dictionary-mode object. Create a new + // shape for the existing property, and also generate a new shape for + // the last property of the dictionary (unless the modified property + // is also the last property). + bool updateLast = (shape == obj->lastProperty()); + shape = + NativeObject::replaceWithNewEquivalentShape(cx, obj, shape, nullptr, + /* accessorShape = */ true); + if (!shape) { + return nullptr; + } + if (!updateLast && !NativeObject::generateOwnShape(cx, obj)) { + return nullptr; + } + + if (updateLast) { + shape->base()->adoptUnowned(nbase); + } else { + shape->setBase(nbase); + } + + shape->setSlot(SHAPE_INVALID_SLOT); + shape->attrs = uint8_t(attrs); + shape->immutableFlags |= Shape::IN_DICTIONARY | Shape::ACCESSOR_SHAPE; + + AccessorShape& accShape = shape->asAccessorShape(); + accShape.rawGetter = getter; + accShape.rawSetter = setter; + GetterSetterPostWriteBarrier(&accShape); + } else { + // Updating the last property in a non-dictionary-mode object. Find an + // alternate shared child of the last property's previous shape. + + MOZ_ASSERT(shape == obj->lastProperty()); + + // Find or create a property tree node labeled by our arguments. + Rooted<StackShape> child(cx, + StackShape(nbase, id, SHAPE_INVALID_SLOT, attrs)); + child.updateGetterSetter(getter, setter); + RootedShape parent(cx, shape->parent); + shape = getChildAccessorProperty(cx, obj, parent, &child); + if (!shape) { + return nullptr; + } + } + + // Can't fail now, so free the previous incarnation's slot. But we do not + // need to free oldSlot (and must not, as trying to will botch an assertion + // in NativeObject::freeSlot) if the new last property (shape here) has a + // slotSpan that does not cover it. + if (hadSlot && oldSlot < obj->slotSpan()) { + obj->freeSlot(cx, oldSlot); + } + + MOZ_ASSERT(!shape->isDataProperty()); + return shape; +} + +/* static */ +Shape* NativeObject::changeProperty(JSContext* cx, HandleNativeObject obj, + HandleShape shape, unsigned attrs, + GetterOp getter, SetterOp setter) { + MOZ_ASSERT(obj->containsPure(shape)); + + AutoCheckShapeConsistency check(obj); + + /* Allow only shared (slotless) => unshared (slotful) transition. */ +#ifdef DEBUG + bool needSlot = Shape::isDataProperty(attrs, getter, setter); + MOZ_ASSERT_IF(shape->isDataProperty() != needSlot, needSlot); +#endif + + AssertCanChangeAttrs(shape, attrs); + + if (shape->attrs == attrs && shape->getter() == getter && + shape->setter() == setter) { + return shape; + } + + RootedId propid(cx, shape->propid()); + return putAccessorProperty(cx, obj, propid, getter, setter, attrs); +} + +/* static */ +bool NativeObject::removeProperty(JSContext* cx, HandleNativeObject obj, + jsid id_) { + RootedId id(cx, id_); + + AutoKeepShapeCaches keep(cx); + ShapeTable* table; + ShapeTable::Entry* entry; + RootedShape shape(cx); + if (!Shape::search(cx, obj->lastProperty(), id, keep, shape.address(), &table, + &entry)) { + return false; + } + + if (!shape) { + return true; + } + + /* + * If shape is not the last property added, or the last property cannot + * be removed, switch to dictionary mode. + */ + if (!obj->inDictionaryMode() && + (shape != obj->lastProperty() || !obj->canRemoveLastProperty())) { + if (!toDictionaryMode(cx, obj)) { + return false; + } + table = obj->lastProperty()->maybeTable(keep); + MOZ_ASSERT(table); + entry = &table->search<MaybeAdding::NotAdding>(shape->propid(), keep); + shape = entry->shape(); + } + + /* + * If in dictionary mode, get a new shape for the last property after the + * removal. We need a fresh shape for all dictionary deletions, even of + * the last property. Otherwise, a shape could replay and caches might + * return deleted DictionaryShapes! See bug 595365. Do this before changing + * the object or table, so the remaining removal is infallible. + */ + RootedShape spare(cx); + if (obj->inDictionaryMode()) { + /* For simplicity, always allocate an accessor shape for now. */ + spare = Allocate<AccessorShape>(cx); + if (!spare) { + return false; + } + new (spare) Shape(shape->base()->unowned(), 0); + if (shape == obj->lastProperty()) { + /* + * Get an up to date unowned base shape for the new last property + * when removing the dictionary's last property. Information in + * base shapes for non-last properties may be out of sync with the + * object's state. + */ + RootedShape previous(cx, obj->lastProperty()->parent); + StackBaseShape base(obj->lastProperty()->base()); + BaseShape* nbase = BaseShape::getUnowned(cx, base); + if (!nbase) { + return false; + } + previous->setBase(nbase); + } + } + + /* If shape has a slot, free its slot number. */ + if (shape->isDataProperty()) { + obj->freeSlot(cx, shape->slot()); + } + + /* + * A dictionary-mode object owns mutable, unique shapes on a non-circular + * doubly linked list, hashed by lastProperty()->table. So we can edit the + * list and hash in place. + */ + if (obj->inDictionaryMode()) { + MOZ_ASSERT(obj->lastProperty()->maybeTable(keep) == table); + + if (entry->hadCollision()) { + entry->setRemoved(); + table->decEntryCount(); + table->incRemovedCount(); + } else { + entry->setFree(); + table->decEntryCount(); + +#ifdef DEBUG + /* + * Check the consistency of the table but limit the number of + * checks not to alter significantly the complexity of the + * delete in debug builds, see bug 534493. + */ + Shape* aprop = obj->lastProperty(); + for (int n = 50; --n >= 0 && aprop->parent; aprop = aprop->parent) { + MOZ_ASSERT_IF(aprop != shape, obj->contains(cx, aprop)); + } +#endif + } + + { + /* Remove shape from its non-circular doubly linked list. */ + Shape* oldLastProp = obj->lastProperty(); + shape->removeFromDictionary(obj); + + /* Hand off table from the old to new last property. */ + oldLastProp->handoffTableTo(obj->lastProperty()); + } + + /* Generate a new shape for the object, infallibly. */ + MOZ_ALWAYS_TRUE(NativeObject::generateOwnShape(cx, obj, spare)); + + /* Consider shrinking table if its load factor is <= .25. */ + uint32_t size = table->capacity(); + if (size > ShapeTable::MIN_SIZE && table->entryCount() <= size >> 2) { + (void)table->change(cx, -1); + } + } else { + /* + * Non-dictionary-mode shape tables are shared immutables, so all we + * need do is retract the last property and we'll either get or else + * lazily make via a later hashify the exact table for the new property + * lineage. + */ + MOZ_ASSERT(shape == obj->lastProperty()); + obj->removeLastProperty(cx); + } + + obj->checkShapeConsistency(); + return true; +} + +/* static */ +Shape* NativeObject::replaceWithNewEquivalentShape(JSContext* cx, + HandleNativeObject obj, + Shape* oldShape, + Shape* newShape, + bool accessorShape) { + MOZ_ASSERT(cx->isInsideCurrentZone(oldShape)); + MOZ_ASSERT_IF(oldShape != obj->lastProperty(), + obj->inDictionaryMode() && + obj->lookup(cx, oldShape->propidRef()) == oldShape); + + if (!obj->inDictionaryMode()) { + RootedShape newRoot(cx, newShape); + if (!toDictionaryMode(cx, obj)) { + return nullptr; + } + oldShape = obj->lastProperty(); + newShape = newRoot; + } + + if (!newShape) { + RootedShape oldRoot(cx, oldShape); + newShape = (oldShape->isAccessorShape() || accessorShape) + ? Allocate<AccessorShape>(cx) + : Allocate<Shape>(cx); + if (!newShape) { + return nullptr; + } + new (newShape) Shape(oldRoot->base()->unowned(), 0); + oldShape = oldRoot; + } + + AutoCheckCannotGC nogc; + ShapeTable* table = obj->lastProperty()->ensureTableForDictionary(cx, nogc); + if (!table) { + return nullptr; + } + + ShapeTable::Entry* entry = + oldShape->isEmptyShape() + ? nullptr + : &table->search<MaybeAdding::NotAdding>(oldShape->propidRef(), nogc); + + /* + * Splice the new shape into the same position as the old shape, preserving + * enumeration order (see bug 601399). + */ + StackShape nshape(oldShape); + newShape->initDictionaryShape(nshape, obj->numFixedSlots(), + oldShape->dictNext); + + MOZ_ASSERT(newShape->parent == oldShape); + oldShape->removeFromDictionary(obj); + + if (newShape == obj->lastProperty()) { + oldShape->handoffTableTo(newShape); + } + + if (entry) { + entry->setPreservingCollision(newShape); + } + return newShape; +} + +/* static */ +bool JSObject::setFlags(JSContext* cx, HandleObject obj, BaseShape::Flag flags, + GenerateShape generateShape) { + MOZ_ASSERT(cx->compartment() == obj->compartment()); + + if (obj->hasAllFlags(flags)) { + return true; + } + + Shape* existingShape = obj->shape(); + if (!existingShape) { + return false; + } + + if (obj->isNative() && obj->as<NativeObject>().inDictionaryMode()) { + if (generateShape == GENERATE_SHAPE) { + if (!NativeObject::generateOwnShape(cx, obj.as<NativeObject>())) { + return false; + } + } + StackBaseShape base(obj->as<NativeObject>().lastProperty()); + base.flags |= flags; + UnownedBaseShape* nbase = BaseShape::getUnowned(cx, base); + if (!nbase) { + return false; + } + + obj->as<NativeObject>().lastProperty()->base()->adoptUnowned(nbase); + return true; + } + + Shape* newShape = + Shape::setObjectFlags(cx, flags, obj->taggedProto(), existingShape); + if (!newShape) { + return false; + } + + obj->as<JSObject>().setShape(newShape); + return true; +} + +/* static */ +bool NativeObject::clearFlag(JSContext* cx, HandleNativeObject obj, + BaseShape::Flag flag) { + MOZ_ASSERT(obj->lastProperty()->getObjectFlags() & flag); + + if (!obj->inDictionaryMode()) { + if (!toDictionaryMode(cx, obj)) { + return false; + } + } + + StackBaseShape base(obj->lastProperty()); + base.flags &= ~flag; + UnownedBaseShape* nbase = BaseShape::getUnowned(cx, base); + if (!nbase) { + return false; + } + + obj->lastProperty()->base()->adoptUnowned(nbase); + return true; +} + +/* static */ +Shape* Shape::setObjectFlags(JSContext* cx, BaseShape::Flag flags, + TaggedProto proto, Shape* last) { + if ((last->getObjectFlags() & flags) == flags) { + return last; + } + + StackBaseShape base(last); + base.flags |= flags; + + RootedShape lastRoot(cx, last); + return replaceLastProperty(cx, base, proto, lastRoot); +} + +inline BaseShape::BaseShape(const StackBaseShape& base) + : TenuredCellWithNonGCPointer(base.clasp), + flags(base.flags), + unowned_(nullptr) {} + +/* static */ +void BaseShape::copyFromUnowned(BaseShape& dest, UnownedBaseShape& src) { + dest.setHeaderPtr(src.clasp()); + dest.unowned_ = &src; + dest.flags = src.flags | OWNED_SHAPE; +} + +inline void BaseShape::adoptUnowned(UnownedBaseShape* other) { + // This is a base shape owned by a dictionary object, update it to reflect the + // unowned base shape of a new last property. + MOZ_ASSERT(isOwned()); + + BaseShape::copyFromUnowned(*this, *other); + + assertConsistency(); +} + +/* static */ +UnownedBaseShape* BaseShape::getUnowned(JSContext* cx, StackBaseShape& base) { + auto& table = cx->zone()->baseShapes(); + + auto p = MakeDependentAddPtr(cx, table, base); + if (p) { + return *p; + } + + BaseShape* nbase_ = Allocate<BaseShape>(cx); + if (!nbase_) { + return nullptr; + } + + new (nbase_) BaseShape(base); + + UnownedBaseShape* nbase = static_cast<UnownedBaseShape*>(nbase_); + + if (!p.add(cx, table, base, nbase)) { + return nullptr; + } + + return nbase; +} + +void BaseShape::assertConsistency() { +#ifdef DEBUG + if (isOwned()) { + UnownedBaseShape* unowned = baseUnowned(); + MOZ_ASSERT(getObjectFlags() == unowned->getObjectFlags()); + } +#endif +} + +void BaseShape::traceChildren(JSTracer* trc) { + traceChildrenSkipShapeCache(trc); + traceShapeCache(trc); +} + +void BaseShape::traceChildrenSkipShapeCache(JSTracer* trc) { + if (isOwned()) { + TraceEdge(trc, &unowned_, "base"); + } + + assertConsistency(); +} + +void BaseShape::traceShapeCache(JSTracer* trc) { + AutoCheckCannotGC nogc; + cache_.trace(trc); +} + +#ifdef DEBUG +bool BaseShape::canSkipMarkingShapeCache(Shape* lastShape) { + // Check that every shape in the shape table will be marked by marking + // |lastShape|. + AutoCheckCannotGC nogc; + ShapeCachePtr cache = getCache(nogc); + if (!cache.isTable()) { + return true; + } + + uint32_t count = 0; + for (Shape::Range<NoGC> r(lastShape); !r.empty(); r.popFront()) { + Shape* shape = &r.front(); + ShapeTable::Entry& entry = + cache.getTablePointer()->search<MaybeAdding::NotAdding>(shape->propid(), + nogc); + if (entry.isLive()) { + count++; + } + } + + return count == cache.getTablePointer()->entryCount(); +} +#endif + +#ifdef JSGC_HASH_TABLE_CHECKS + +void Zone::checkBaseShapeTableAfterMovingGC() { + for (auto r = baseShapes().all(); !r.empty(); r.popFront()) { + UnownedBaseShape* base = r.front().unbarrieredGet(); + CheckGCThingAfterMovingGC(base); + + BaseShapeSet::Ptr ptr = baseShapes().lookup(base); + MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); + } +} + +#endif // JSGC_HASH_TABLE_CHECKS + +void BaseShape::finalize(JSFreeOp* fop) { + if (cache_.isInitialized()) { + cache_.destroy(fop, this); + } +} + +inline InitialShapeEntry::InitialShapeEntry() : shape(nullptr), proto() {} + +inline InitialShapeEntry::InitialShapeEntry(Shape* shape, + const TaggedProto& proto) + : shape(shape), proto(proto) {} + +#ifdef JSGC_HASH_TABLE_CHECKS + +void Zone::checkInitialShapesTableAfterMovingGC() { + /* + * Assert that the postbarriers have worked and that nothing is left in + * initialShapes that points into the nursery, and that the hash table + * entries are discoverable. + */ + for (auto r = initialShapes().all(); !r.empty(); r.popFront()) { + InitialShapeEntry entry = r.front(); + TaggedProto proto = entry.proto.unbarrieredGet(); + Shape* shape = entry.shape.unbarrieredGet(); + + CheckGCThingAfterMovingGC(shape); + if (proto.isObject()) { + CheckGCThingAfterMovingGC(proto.toObject()); + } + + using Lookup = InitialShapeEntry::Lookup; + Lookup lookup(shape->getObjectClass(), proto, shape->numFixedSlots(), + shape->getObjectFlags()); + InitialShapeSet::Ptr ptr = initialShapes().lookup(lookup); + MOZ_RELEASE_ASSERT(ptr.found() && &*ptr == &r.front()); + } +} + +#endif // JSGC_HASH_TABLE_CHECKS + +Shape* EmptyShape::new_(JSContext* cx, Handle<UnownedBaseShape*> base, + uint32_t nfixed) { + Shape* shape = Allocate<Shape>(cx); + if (!shape) { + ReportOutOfMemory(cx); + return nullptr; + } + + new (shape) EmptyShape(base, nfixed); + return shape; +} + +MOZ_ALWAYS_INLINE HashNumber ShapeHasher::hash(const Lookup& l) { + return l.hash(); +} + +MOZ_ALWAYS_INLINE bool ShapeHasher::match(const Key k, const Lookup& l) { + return k->matches(l); +} + +static ShapeSet* MakeShapeSet(Shape* child1, Shape* child2) { + auto hash = MakeUnique<ShapeSet>(); + if (!hash || !hash->reserve(2)) { + return nullptr; + } + + hash->putNewInfallible(StackShape(child1), child1); + hash->putNewInfallible(StackShape(child2), child2); + return hash.release(); +} + +bool PropertyTree::insertChild(JSContext* cx, Shape* parent, Shape* child) { + MOZ_ASSERT(!parent->inDictionary()); + MOZ_ASSERT(!child->parent); + MOZ_ASSERT(!child->inDictionary()); + MOZ_ASSERT(child->zone() == parent->zone()); + MOZ_ASSERT(cx->zone() == zone_); + + ShapeChildren* childp = &parent->children; + + if (childp->isNone()) { + child->setParent(parent); + childp->setSingleShape(child); + return true; + } + + if (childp->isSingleShape()) { + Shape* shape = childp->toSingleShape(); + MOZ_ASSERT(shape != child); + MOZ_ASSERT(!shape->matches(child)); + + ShapeSet* hash = MakeShapeSet(shape, child); + if (!hash) { + ReportOutOfMemory(cx); + return false; + } + childp->setShapeSet(hash); + AddCellMemory(parent, sizeof(ShapeSet), MemoryUse::ShapeChildren); + child->setParent(parent); + return true; + } + + if (!childp->toShapeSet()->putNew(StackShape(child), child)) { + ReportOutOfMemory(cx); + return false; + } + + child->setParent(parent); + return true; +} + +void Shape::removeChild(JSFreeOp* fop, Shape* child) { + MOZ_ASSERT(!child->inDictionary()); + MOZ_ASSERT(child->parent == this); + + ShapeChildren* childp = &children; + + if (childp->isSingleShape()) { + MOZ_ASSERT(childp->toSingleShape() == child); + childp->setNone(); + child->parent = nullptr; + return; + } + + // There must be at least two shapes in a set otherwise + // childp->isSingleShape() should be true. + ShapeSet* set = childp->toShapeSet(); + MOZ_ASSERT(set->count() >= 2); + +#ifdef DEBUG + size_t oldCount = set->count(); +#endif + + set->remove(StackShape(child)); + child->parent = nullptr; + + MOZ_ASSERT(set->count() == oldCount - 1); + + if (set->count() == 1) { + // Convert from set form back to single shape form. + ShapeSet::Range r = set->all(); + Shape* otherChild = r.front(); + MOZ_ASSERT((r.popFront(), r.empty())); // No more elements! + childp->setSingleShape(otherChild); + fop->delete_(this, set, MemoryUse::ShapeChildren); + } +} + +MOZ_ALWAYS_INLINE Shape* PropertyTree::inlinedGetChild( + JSContext* cx, Shape* parent, Handle<StackShape> childSpec) { + MOZ_ASSERT(parent); + + Shape* existingShape = nullptr; + + /* + * The property tree has extremely low fan-out below its root in + * popular embeddings with real-world workloads. Patterns such as + * defining closures that capture a constructor's environment as + * getters or setters on the new object that is passed in as + * |this| can significantly increase fan-out below the property + * tree root -- see bug 335700 for details. + */ + ShapeChildren* childp = &parent->children; + if (childp->isSingleShape()) { + Shape* child = childp->toSingleShape(); + if (child->matches(childSpec)) { + existingShape = child; + } + } else if (childp->isShapeSet()) { + if (ShapeSet::Ptr p = childp->toShapeSet()->lookup(childSpec)) { + existingShape = *p; + } + } else { + /* If childp->isNone(), we always insert. */ + } + + if (existingShape) { + existingShape = PropertyTreeReadBarrier(cx, parent, existingShape); + if (existingShape) { + return existingShape; + } + } + + RootedShape parentRoot(cx, parent); + Shape* shape = Shape::new_(cx, childSpec, parentRoot->numFixedSlots()); + if (!shape) { + return nullptr; + } + + if (!insertChild(cx, parentRoot, shape)) { + return nullptr; + } + + return shape; +} + +Shape* PropertyTree::getChild(JSContext* cx, Shape* parent, + Handle<StackShape> child) { + return inlinedGetChild(cx, parent, child); +} + +void Shape::sweep(JSFreeOp* fop) { + /* + * We detach the child from the parent if the parent is reachable. + * + * This test depends on shape arenas not being freed until after we finish + * incrementally sweeping them. If that were not the case the parent pointer + * could point to a marked cell that had been deallocated and then + * reallocated, since allocating a cell in a zone that is being marked will + * set the mark bit for that cell. + */ + + MOZ_ASSERT(zone()->isGCSweeping()); + MOZ_ASSERT_IF(parent, parent->zone() == zone()); + + if (parent && parent->isMarkedAny()) { + if (inDictionary()) { + if (parent->dictNext == DictionaryShapeLink(this)) { + parent->dictNext.setNone(); + } + } else { + parent->removeChild(fop, this); + } + } +} + +void Shape::finalize(JSFreeOp* fop) { + if (!inDictionary() && children.isShapeSet()) { + fop->delete_(this, children.toShapeSet(), MemoryUse::ShapeChildren); + } +} + +void Shape::fixupDictionaryShapeAfterMovingGC() { + if (dictNext.isShape()) { + Shape* shape = dictNext.toShape(); + if (gc::IsForwarded(shape)) { + dictNext.setShape(gc::Forwarded(shape)); + } + } else if (dictNext.isObject()) { + JSObject* obj = dictNext.toObject(); + if (gc::IsForwarded(obj)) { + dictNext.setObject(gc::Forwarded(obj)); + } + } else { + MOZ_ASSERT(dictNext.isNone()); + } +} + +void Shape::fixupShapeTreeAfterMovingGC() { + if (children.isNone()) { + return; + } + + if (children.isSingleShape()) { + if (gc::IsForwarded(children.toSingleShape())) { + children.setSingleShape(gc::Forwarded(children.toSingleShape())); + } + return; + } + + MOZ_ASSERT(children.isShapeSet()); + ShapeSet* set = children.toShapeSet(); + for (ShapeSet::Enum e(*set); !e.empty(); e.popFront()) { + Shape* key = MaybeForwarded(e.front()); + BaseShape* base = MaybeForwarded(key->base()); + UnownedBaseShape* unowned = MaybeForwarded(base->unowned()); + + GetterOp getter = key->getter(); + if (key->hasGetterObject()) { + getter = GetterOp(MaybeForwarded(key->getterObject())); + } + + SetterOp setter = key->setter(); + if (key->hasSetterObject()) { + setter = SetterOp(MaybeForwarded(key->setterObject())); + } + + StackShape lookup(unowned, const_cast<Shape*>(key)->propidRef(), + key->immutableFlags & Shape::SLOT_MASK, key->attrs); + lookup.updateGetterSetter(getter, setter); + e.rekeyFront(lookup, key); + } +} + +void Shape::fixupAfterMovingGC() { + if (inDictionary()) { + fixupDictionaryShapeAfterMovingGC(); + } else { + fixupShapeTreeAfterMovingGC(); + } +} + +void NurseryShapesRef::trace(JSTracer* trc) { + auto& shapes = zone_->nurseryShapes(); + for (auto shape : shapes) { + shape->fixupGetterSetterForBarrier(trc); + } + shapes.clearAndFree(); +} + +void Shape::fixupGetterSetterForBarrier(JSTracer* trc) { + if (!hasGetterValue() && !hasSetterValue()) { + return; + } + + JSObject* priorGetter = asAccessorShape().getterObj; + JSObject* priorSetter = asAccessorShape().setterObj; + if (!priorGetter && !priorSetter) { + return; + } + + JSObject* postGetter = priorGetter; + JSObject* postSetter = priorSetter; + if (priorGetter) { + TraceManuallyBarrieredEdge(trc, &postGetter, "getterObj"); + } + if (priorSetter) { + TraceManuallyBarrieredEdge(trc, &postSetter, "setterObj"); + } + if (priorGetter == postGetter && priorSetter == postSetter) { + return; + } + + if (parent && !parent->inDictionary() && parent->children.isShapeSet()) { + // Relocating the getterObj or setterObj will have changed our location in + // our parent's ShapeSet, so take care to update it. We must do this before + // we update the shape itself, since the shape is used to match the original + // entry in the hash set. + + StackShape original(this); + StackShape updated(this); + updated.rawGetter = reinterpret_cast<GetterOp>(postGetter); + updated.rawSetter = reinterpret_cast<SetterOp>(postSetter); + + ShapeSet* set = parent->children.toShapeSet(); + MOZ_ALWAYS_TRUE(set->rekeyAs(original, updated, this)); + } + + asAccessorShape().getterObj = postGetter; + asAccessorShape().setterObj = postSetter; + + MOZ_ASSERT_IF( + parent && !parent->inDictionary() && parent->children.isShapeSet(), + parent->children.toShapeSet()->has(StackShape(this))); +} + +#ifdef DEBUG + +void ShapeChildren::checkHasChild(Shape* child) const { + if (isSingleShape()) { + MOZ_ASSERT(toSingleShape() == child); + } else { + MOZ_ASSERT(isShapeSet()); + ShapeSet* set = toShapeSet(); + ShapeSet::Ptr ptr = set->lookup(StackShape(child)); + MOZ_ASSERT(*ptr == child); + } +} + +void Shape::dump(js::GenericPrinter& out) const { + jsid propid = this->propid(); + + MOZ_ASSERT(!JSID_IS_VOID(propid)); + + if (JSID_IS_INT(propid)) { + out.printf("[%ld]", (long)JSID_TO_INT(propid)); + } else if (JSID_IS_ATOM(propid)) { + if (JSLinearString* str = JSID_TO_ATOM(propid)) { + EscapedStringPrinter(out, str, '"'); + } else { + out.put("<error>"); + } + } else { + MOZ_ASSERT(JSID_IS_SYMBOL(propid)); + JSID_TO_SYMBOL(propid)->dump(out); + } + + out.printf(" g/s %p/%p slot %d attrs %x ", + JS_FUNC_TO_DATA_PTR(void*, getter()), + JS_FUNC_TO_DATA_PTR(void*, setter()), + isDataProperty() ? int32_t(slot()) : -1, attrs); + + if (attrs) { + int first = 1; + out.putChar('('); +# define DUMP_ATTR(name, display) \ + if (attrs & JSPROP_##name) out.put(&(" " #display)[first]), first = 0 + DUMP_ATTR(ENUMERATE, enumerate); + DUMP_ATTR(READONLY, readonly); + DUMP_ATTR(PERMANENT, permanent); + DUMP_ATTR(GETTER, getter); + DUMP_ATTR(SETTER, setter); +# undef DUMP_ATTR + out.putChar(')'); + } + + out.printf("immutableFlags %x ", immutableFlags); + if (immutableFlags) { + int first = 1; + out.putChar('('); +# define DUMP_FLAG(name, display) \ + if (immutableFlags & name) out.put(&(" " #display)[first]), first = 0 + DUMP_FLAG(IN_DICTIONARY, in_dictionary); +# undef DUMP_FLAG + out.putChar(')'); + } +} + +void Shape::dump() const { + Fprinter out(stderr); + dump(out); +} + +void Shape::dumpSubtree(int level, js::GenericPrinter& out) const { + if (!parent) { + MOZ_ASSERT(level == 0); + MOZ_ASSERT(JSID_IS_EMPTY(propid_)); + out.printf("class %s emptyShape\n", getObjectClass()->name); + } else { + out.printf("%*sid ", level, ""); + dump(out); + } + + if (!children.isNone()) { + ++level; + if (children.isSingleShape()) { + Shape* child = children.toSingleShape(); + MOZ_ASSERT(child->parent == this); + child->dumpSubtree(level, out); + } else { + const ShapeSet& set = *children.toShapeSet(); + for (ShapeSet::Range range = set.all(); !range.empty(); + range.popFront()) { + Shape* child = range.front(); + + MOZ_ASSERT(child->parent == this); + child->dumpSubtree(level, out); + } + } + } +} + +#endif + +/* static */ +Shape* EmptyShape::getInitialShape(JSContext* cx, const JSClass* clasp, + TaggedProto proto, size_t nfixed, + uint32_t objectFlags) { + MOZ_ASSERT_IF(proto.isObject(), + cx->isInsideCurrentCompartment(proto.toObject())); + + auto& table = cx->zone()->initialShapes(); + + using Lookup = InitialShapeEntry::Lookup; + auto protoPointer = + MakeDependentAddPtr(cx, table, Lookup(clasp, proto, nfixed, objectFlags)); + if (protoPointer) { + return protoPointer->shape; + } + + Rooted<TaggedProto> protoRoot(cx, proto); + StackBaseShape base(clasp, objectFlags); + Rooted<UnownedBaseShape*> nbase(cx, BaseShape::getUnowned(cx, base)); + if (!nbase) { + return nullptr; + } + + RootedShape shape(cx, EmptyShape::new_(cx, nbase, nfixed)); + if (!shape) { + return nullptr; + } + + Lookup lookup(clasp, protoRoot, nfixed, objectFlags); + if (!protoPointer.add(cx, table, lookup, + InitialShapeEntry(shape, protoRoot))) { + return nullptr; + } + + return shape; +} + +/* static */ +Shape* EmptyShape::getInitialShape(JSContext* cx, const JSClass* clasp, + TaggedProto proto, gc::AllocKind kind, + uint32_t objectFlags) { + return getInitialShape(cx, clasp, proto, GetGCKindSlots(kind, clasp), + objectFlags); +} + +void NewObjectCache::invalidateEntriesForShape(JSContext* cx, HandleShape shape, + HandleObject proto) { + const JSClass* clasp = shape->getObjectClass(); + + gc::AllocKind kind = gc::GetGCObjectKind(shape->numFixedSlots()); + if (CanChangeToBackgroundAllocKind(kind, clasp)) { + kind = ForegroundToBackgroundAllocKind(kind); + } + + RootedObjectGroup group( + cx, ObjectGroup::defaultNewGroup(cx, clasp, TaggedProto(proto))); + if (!group) { + purge(); + cx->recoverFromOutOfMemory(); + return; + } + + EntryIndex entry; + for (RealmsInZoneIter realm(shape->zone()); !realm.done(); realm.next()) { + if (GlobalObject* global = realm->unsafeUnbarrieredMaybeGlobal()) { + if (lookupGlobal(clasp, global, kind, &entry)) { + PodZero(&entries[entry]); + } + } + } + if (!proto->is<GlobalObject>() && lookupProto(clasp, proto, kind, &entry)) { + PodZero(&entries[entry]); + } + if (lookupGroup(group, kind, &entry)) { + PodZero(&entries[entry]); + } +} + +/* static */ +void EmptyShape::insertInitialShape(JSContext* cx, HandleShape shape, + HandleObject proto) { + using Lookup = InitialShapeEntry::Lookup; + Lookup lookup(shape->getObjectClass(), TaggedProto(proto), + shape->numFixedSlots(), shape->getObjectFlags()); + + InitialShapeSet::Ptr p = cx->zone()->initialShapes().lookup(lookup); + MOZ_ASSERT(p); + + InitialShapeEntry& entry = const_cast<InitialShapeEntry&>(*p); + + // The metadata callback can end up causing redundant changes of the initial + // shape. + if (entry.shape == shape) { + return; + } + + // The new shape had better be rooted at the old one. +#ifdef DEBUG + Shape* nshape = shape; + while (!nshape->isEmptyShape()) { + nshape = nshape->previous(); + } + MOZ_ASSERT(nshape == entry.shape); +#endif + + entry.shape = WeakHeapPtrShape(shape); + + /* + * This affects the shape that will be produced by the various NewObject + * methods, so clear any cache entry referring to the old shape. This is + * not required for correctness: the NewObject must always check for a + * nativeEmpty() result and generate the appropriate properties if found. + * Clearing the cache entry avoids this duplicate regeneration. + * + * Clearing is not necessary when this context is running off + * thread, as it will not use the new object cache for allocations. + */ + if (!cx->isHelperThreadContext()) { + cx->caches().newObjectCache.invalidateEntriesForShape(cx, shape, proto); + } +} + +void Zone::fixupInitialShapeTable() { + for (InitialShapeSet::Enum e(initialShapes()); !e.empty(); e.popFront()) { + // The shape may have been moved, but we can update that in place. + Shape* shape = e.front().shape.unbarrieredGet(); + if (IsForwarded(shape)) { + shape = Forwarded(shape); + e.mutableFront().shape.set(shape); + } + shape->updateBaseShapeAfterMovingGC(); + + // If the prototype has moved we have to rekey the entry. + InitialShapeEntry entry = e.front(); + // Use unbarrieredGet() to prevent triggering read barrier while collecting. + const TaggedProto& proto = entry.proto.unbarrieredGet(); + if (proto.isObject() && IsForwarded(proto.toObject())) { + entry.proto = TaggedProto(Forwarded(proto.toObject())); + using Lookup = InitialShapeEntry::Lookup; + Lookup relookup(shape->getObjectClass(), proto, shape->numFixedSlots(), + shape->getObjectFlags()); + e.rekeyFront(relookup, entry); + } + } +} + +void AutoRooterGetterSetter::Inner::trace(JSTracer* trc) { + if ((attrs & JSPROP_GETTER) && *pgetter) { + TraceRoot(trc, (JSObject**)pgetter, "AutoRooterGetterSetter getter"); + } + if ((attrs & JSPROP_SETTER) && *psetter) { + TraceRoot(trc, (JSObject**)psetter, "AutoRooterGetterSetter setter"); + } +} + +JS::ubi::Node::Size JS::ubi::Concrete<js::Shape>::size( + mozilla::MallocSizeOf mallocSizeOf) const { + Size size = js::gc::Arena::thingSize(get().asTenured().getAllocKind()); + + AutoCheckCannotGC nogc; + if (ShapeTable* table = get().maybeTable(nogc)) { + size += table->sizeOfIncludingThis(mallocSizeOf); + } + + if (!get().inDictionary() && get().children.isShapeSet()) { + size += + get().children.toShapeSet()->shallowSizeOfIncludingThis(mallocSizeOf); + } + + return size; +} + +JS::ubi::Node::Size JS::ubi::Concrete<js::BaseShape>::size( + mozilla::MallocSizeOf mallocSizeOf) const { + return js::gc::Arena::thingSize(get().asTenured().getAllocKind()); +} + +void PropertyResult::trace(JSTracer* trc) { + if (isNativeProperty()) { + TraceRoot(trc, &shape_, "PropertyResult::shape_"); + } +} |