Bug 835102 - Convert sufficiently sparse objects back to using dense elements, r=billm.
authorBrian Hackett <bhackett1024@gmail.com>
Tue, 29 Jan 2013 19:50:41 -0700
changeset 130197 7d45649de683d8ff3c27580d7856c5e1c92b22af
parent 130196 3125ccd01edb70257952f0a84ffa101a81ef6746
child 130198 8ef47aa7120f2202fc80c9004d7fcab95ef3c9a3
push id2323
push userbbajaj@mozilla.com
push dateMon, 01 Apr 2013 19:47:02 +0000
treeherdermozilla-beta@7712be144d91 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs835102
milestone21.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 835102 - Convert sufficiently sparse objects back to using dense elements, r=billm.
js/src/jsarray.h
js/src/jsinterpinlines.h
js/src/jsobj.cpp
js/src/jsobj.h
js/src/vm/Shape-inl.h
js/src/vm/Shape.cpp
js/src/vm/Shape.h
--- a/js/src/jsarray.h
+++ b/js/src/jsarray.h
@@ -10,37 +10,33 @@
  * JS Array interface.
  */
 #include "jscntxt.h"
 #include "jsprvtd.h"
 #include "jspubtd.h"
 #include "jsatom.h"
 #include "jsobj.h"
 
-/* Small arrays are dense, no matter what. */
-const unsigned MIN_SPARSE_INDEX = 512;
-
 namespace js {
 /* 2^32-2, inclusive */
 const uint32_t MAX_ARRAY_INDEX = 4294967294u;
 }
 
 inline JSBool
 js_IdIsIndex(jsid id, uint32_t *indexp)
 {
     if (JSID_IS_INT(id)) {
         int32_t i = JSID_TO_INT(id);
-        if (i < 0)
-            return JS_FALSE;
+        JS_ASSERT(i >= 0);
         *indexp = (uint32_t)i;
-        return JS_TRUE;
+        return true;
     }
 
     if (JS_UNLIKELY(!JSID_IS_STRING(id)))
-        return JS_FALSE;
+        return false;
 
     return js::StringIsArrayIndex(JSID_TO_ATOM(id), indexp);
 }
 
 extern JSObject *
 js_InitArrayClass(JSContext *cx, js::HandleObject obj);
 
 extern bool
--- a/js/src/jsinterpinlines.h
+++ b/js/src/jsinterpinlines.h
@@ -354,17 +354,17 @@ SetPropertyOperation(JSContext *cx, jsby
 #endif
 
             if (shape->hasDefaultSetter() && shape->hasSlot()) {
                 /* Fast path for, e.g., plain Object instance properties. */
                 JSObject::nativeSetSlotWithType(cx, obj, shape, rval);
             } else {
                 RootedValue rref(cx, rval);
                 bool strict = cx->stack.currentScript()->strict;
-                if (!js_NativeSet(cx, obj, obj, shape, false, strict, &rref))
+                if (!js_NativeSet(cx, obj, obj, shape, strict, &rref))
                     return false;
             }
             return true;
         }
 
         GET_NAME_FROM_BYTECODE(cx->stack.currentScript(), pc, 0, name);
     }
 
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -2519,36 +2519,149 @@ bool
 JSObject::willBeSparseElements(unsigned requiredCapacity, unsigned newElementsHint)
 {
     JS_ASSERT(isNative());
     JS_ASSERT(requiredCapacity > MIN_SPARSE_INDEX);
 
     unsigned cap = getDenseCapacity();
     JS_ASSERT(requiredCapacity >= cap);
 
-    if (requiredCapacity >= JSObject::NELEMENTS_LIMIT)
+    if (requiredCapacity >= NELEMENTS_LIMIT)
         return true;
 
-    unsigned minimalDenseCount = requiredCapacity / 4;
+    unsigned minimalDenseCount = requiredCapacity / SPARSE_DENSITY_RATIO;
     if (newElementsHint >= minimalDenseCount)
         return false;
     minimalDenseCount -= newElementsHint;
 
     if (minimalDenseCount > cap)
         return true;
 
     unsigned len = getDenseInitializedLength();
     const Value *elems = getDenseElements();
     for (unsigned i = 0; i < len; i++) {
         if (!elems[i].isMagic(JS_ELEMENTS_HOLE) && !--minimalDenseCount)
             return false;
     }
     return true;
 }
 
+/* static */ JSObject::EnsureDenseResult
+JSObject::maybeDensifySparseElements(JSContext *cx, HandleObject obj)
+{
+    /* This should only be called after adding a sparse index to an object. */
+    JS_ASSERT(JSID_IS_INT(obj->lastProperty()->propid()));
+
+    /*
+     * Wait until after the object goes into dictionary mode, which must happen
+     * when sparsely packing any array with more than MIN_SPARSE_INDEX elements
+     * (see PropertyTree::MAX_HEIGHT).
+     */
+    if (!obj->inDictionaryMode())
+        return ED_SPARSE;
+
+    /*
+     * Only measure the number of indexed properties every log(n) times when
+     * populating the object.
+     */
+    uint32_t slotSpan = obj->slotSpan();
+    if (slotSpan != RoundUpPow2(slotSpan))
+        return ED_SPARSE;
+
+    /* Watch for conditions under which an object's elements cannot be dense. */
+    if (!obj->isExtensible() || obj->watched())
+        return ED_SPARSE;
+
+    /*
+     * The indexes in the object need to be sufficiently dense before they can
+     * be converted to dense mode.
+     */
+    uint32_t numDenseElements = 0;
+    uint32_t newInitializedLength = 0;
+
+    RootedShape shape(cx, obj->lastProperty());
+    while (!shape->isEmptyShape()) {
+        uint32_t index;
+        if (js_IdIsIndex(shape->propid(), &index)) {
+            if (shape->attributes() == JSPROP_ENUMERATE &&
+                shape->hasDefaultGetter() &&
+                shape->hasDefaultSetter())
+            {
+                numDenseElements++;
+                newInitializedLength = Max(newInitializedLength, index + 1);
+            } else {
+                /*
+                 * For simplicity, only densify the object if all indexed
+                 * properties can be converted to dense elements.
+                 */
+                return ED_SPARSE;
+            }
+        }
+        shape = shape->previous();
+    }
+
+    if (numDenseElements * SPARSE_DENSITY_RATIO < newInitializedLength)
+        return ED_SPARSE;
+
+    if (newInitializedLength >= NELEMENTS_LIMIT)
+        return ED_SPARSE;
+
+    /*
+     * This object meets all necessary restrictions, convert all indexed
+     * properties into dense elements.
+     */
+
+    if (!obj->growElements(cx, newInitializedLength))
+        return ED_FAILED;
+
+    obj->ensureDenseInitializedLength(cx, newInitializedLength, 0);
+
+    RootedValue value(cx);
+
+    shape = obj->lastProperty();
+    while (!shape->isEmptyShape()) {
+        jsid id = shape->propid();
+        uint32_t index;
+        if (js_IdIsIndex(id, &index)) {
+            value = obj->getSlot(shape->slot());
+
+            /*
+             * When removing a property from a dictionary, the specified
+             * property will be removed from the dictionary list and the
+             * last property will then be changed due to reshaping the object.
+             * Compute the next shape in the traverse, watching for such
+             * removals from the list.
+             */
+            if (shape != obj->lastProperty()) {
+                shape = shape->previous();
+                if (!obj->removeProperty(cx, id))
+                    return ED_FAILED;
+            } else {
+                if (!obj->removeProperty(cx, id))
+                    return ED_FAILED;
+                shape = obj->lastProperty();
+            }
+
+            obj->setDenseElement(index, value);
+        } else {
+            shape = shape->previous();
+        }
+    }
+
+    /*
+     * All indexed properties on the object are now dense, clear the indexed
+     * flag so that we will not start using sparse indexes again if we need
+     * to grow the object.
+     */
+    if (!obj->clearFlag(cx, BaseShape::INDEXED))
+        return ED_FAILED;
+
+    return ED_OK;
+}
+
 bool
 JSObject::growElements(JSContext *cx, unsigned newcap)
 {
     size_t oldSize = Probes::objectResizeActive() ? computedSizeOfThisSlotsElements() : 0;
 
     if (!growElements(&cx->compartment->allocator, newcap)) {
         JS_ReportOutOfMemory(cx);
         return false;
@@ -3050,18 +3163,20 @@ static inline bool
 CallAddPropertyHook(JSContext *cx, Class *clasp, HandleObject obj, HandleShape shape,
                     HandleValue nominal)
 {
     if (clasp->addProperty != JS_PropertyStub) {
         /* Make a local copy of value so addProperty can mutate its inout parameter. */
         RootedValue value(cx, nominal);
 
         Rooted<jsid> id(cx, shape->propid());
-        if (!CallJSPropertyOp(cx, clasp->addProperty, obj, id, &value))
+        if (!CallJSPropertyOp(cx, clasp->addProperty, obj, id, &value)) {
+            obj->removeProperty(cx, shape->propid());
             return false;
+        }
         if (value.get() != nominal) {
             if (shape->hasSlot())
                 JSObject::nativeSetSlotWithType(cx, obj, shape, value);
         }
     }
     return true;
 }
 
@@ -3077,24 +3192,85 @@ CallAddPropertyHookDense(JSContext *cx, 
         return true;
     }
 
     if (clasp->addProperty != JS_PropertyStub) {
         /* Make a local copy of value so addProperty can mutate its inout parameter. */
         RootedValue value(cx, nominal);
 
         Rooted<jsid> id(cx, INT_TO_JSID(index));
-        if (!CallJSPropertyOp(cx, clasp->addProperty, obj, id, &value))
+        if (!CallJSPropertyOp(cx, clasp->addProperty, obj, id, &value)) {
+            JSObject::setDenseElementHole(cx, obj, index);
             return false;
+        }
         if (value.get() != nominal)
             JSObject::setDenseElementWithType(cx, obj, index, value);
     }
     return true;
 }
 
+static inline bool
+DefinePropertyOrElement(JSContext *cx, HandleObject obj, HandleId id,
+                        PropertyOp getter, StrictPropertyOp setter,
+                        unsigned attrs, unsigned flags, int shortid,
+                        HandleValue value, bool callSetterAfterwards, bool setterIsStrict)
+{
+    /* Use dense storage for new indexed properties where possible. */
+    if (JSID_IS_INT(id) &&
+        getter == JS_PropertyStub &&
+        setter == JS_StrictPropertyStub &&
+        attrs == JSPROP_ENUMERATE &&
+        (!obj->isIndexed() || !obj->nativeContains(cx, id)))
+    {
+        uint32_t index = JSID_TO_INT(id);
+        JSObject::EnsureDenseResult result = obj->ensureDenseElements(cx, index, 1);
+        if (result == JSObject::ED_FAILED)
+            return false;
+        if (result == JSObject::ED_OK) {
+            obj->setDenseElementMaybeConvertDouble(index, value);
+            return CallAddPropertyHookDense(cx, obj->getClass(), obj, index, value);
+        }
+    }
+
+    AutoRooterGetterSetter gsRoot(cx, attrs, &getter, &setter);
+
+    RootedShape shape(cx, JSObject::putProperty(cx, obj, id, getter, setter, SHAPE_INVALID_SLOT,
+                                                attrs, flags, shortid));
+    if (!shape)
+        return false;
+
+    if (shape->hasSlot())
+        obj->nativeSetSlot(shape->slot(), value);
+
+    /*
+     * Clear any existing dense index after adding a sparse indexed property,
+     * and investigate converting the object to dense indexes.
+     */
+    if (JSID_IS_INT(id)) {
+        uint32_t index = JSID_TO_INT(id);
+        JSObject::removeDenseElementForSparseIndex(cx, obj, index);
+        JSObject::EnsureDenseResult result = JSObject::maybeDensifySparseElements(cx, obj);
+        if (result == JSObject::ED_FAILED)
+            return false;
+        if (result == JSObject::ED_OK) {
+            JS_ASSERT(setter == JS_StrictPropertyStub);
+            return CallAddPropertyHookDense(cx, obj->getClass(), obj, index, value);
+        }
+    }
+
+    if (!CallAddPropertyHook(cx, obj->getClass(), obj, shape, value))
+        return false;
+
+    if (callSetterAfterwards && setter != JS_StrictPropertyStub) {
+        RootedValue nvalue(cx, value);
+        return js_NativeSet(cx, obj, obj, shape, setterIsStrict, &nvalue);
+    }
+    return true;
+}
+
 bool
 js::DefineNativeProperty(JSContext *cx, HandleObject obj, HandleId id, HandleValue value,
                          PropertyOp getter, StrictPropertyOp setter, unsigned attrs,
                          unsigned flags, int shortid, unsigned defineHow /* = 0 */)
 {
     JS_ASSERT((defineHow & ~(DNP_CACHE_RESULT | DNP_DONT_PURGE |
                              DNP_SKIP_TYPE)) == 0);
     JS_ASSERT(!(attrs & JSPROP_NATIVE_ACCESSORS));
@@ -3143,17 +3319,17 @@ js::DefineNativeProperty(JSContext *cx, 
         } else {
             shape = NULL;
         }
     }
 
     /*
      * Purge the property cache of any properties named by id that are about
      * to be shadowed in obj's scope chain unless it is known a priori that it
-     * is not possible. We do this before locking obj to avoid nesting locks.
+     * is not possible.
      */
     if (!(defineHow & DNP_DONT_PURGE)) {
         if (!js_PurgeScopeChain(cx, obj, id))
             return false;
     }
 
     /* Use the object's class getter and setter by default. */
     Class *clasp = obj->getClass();
@@ -3168,57 +3344,24 @@ js::DefineNativeProperty(JSContext *cx, 
          * initial value of the property.
          */
         AddTypePropertyId(cx, obj, id, value);
         if (attrs & JSPROP_READONLY)
             MarkTypePropertyConfigured(cx, obj, id);
     }
 
     if (!shape) {
-        /* Use dense storage for new indexed properties where possible. */
-        if (JSID_IS_INT(id) &&
-            getter == JS_PropertyStub &&
-            setter == JS_StrictPropertyStub &&
-            attrs == JSPROP_ENUMERATE &&
-            (!obj->isIndexed() || !obj->nativeContains(cx, id)))
-        {
-            uint32_t index = JSID_TO_INT(id);
-            JSObject::EnsureDenseResult result = obj->ensureDenseElements(cx, index, 1);
-            if (result == JSObject::ED_FAILED)
-                return false;
-            if (result == JSObject::ED_OK) {
-                obj->setDenseElementMaybeConvertDouble(index, value);
-                if (!CallAddPropertyHookDense(cx, clasp, obj, index, value)) {
-                    JSObject::setDenseElementHole(cx, obj, index);
-                    return false;
-                }
-                return true;
-            }
-        }
-
-        shape = JSObject::putProperty(cx, obj, id, getter, setter, SHAPE_INVALID_SLOT,
-                                      attrs, flags, shortid);
-        if (!shape)
-            return false;
-
-        /* Clear any existing dense index after adding a sparse indexed property. */
-        if (JSID_IS_INT(id))
-            JSObject::removeDenseElementForSparseIndex(cx, obj, JSID_TO_INT(id));
+        return DefinePropertyOrElement(cx, obj, id, getter, setter,
+                                       attrs, flags, shortid, value, false, false);
     }
 
-    /* Store valueCopy before calling addProperty, in case the latter GC's. */
     if (shape->hasSlot())
         obj->nativeSetSlot(shape->slot(), value);
 
-    if (!CallAddPropertyHook(cx, clasp, obj, shape, value)) {
-        obj->removeProperty(cx, id);
-        return false;
-    }
-
-    return shape;
+    return CallAddPropertyHook(cx, clasp, obj, shape, value);
 }
 
 /*
  * Call obj's resolve hook.
  *
  * cx, id, and flags are the parameters initially passed to the ongoing lookup;
  * objp and propp are its out parameters. obj is an object along the prototype
  * chain from where the lookup started.
@@ -3549,17 +3692,17 @@ JSBool
 js_NativeGet(JSContext *cx, Handle<JSObject*> obj, Handle<JSObject*> pobj, Handle<Shape*> shape,
              unsigned getHow, MutableHandle<Value> vp)
 {
     return NativeGetInline<CanGC>(cx, obj, obj, pobj, shape, getHow, vp);
 }
 
 JSBool
 js_NativeSet(JSContext *cx, Handle<JSObject*> obj, Handle<JSObject*> receiver,
-             HandleShape shape, bool added, bool strict, MutableHandleValue vp)
+             HandleShape shape, bool strict, MutableHandleValue vp)
 {
     JS_ASSERT(obj->isNative());
 
     if (shape->hasSlot()) {
         uint32_t slot = shape->slot();
 
         /* If shape has a stub setter, just store vp. */
         if (shape->hasDefaultSetter()) {
@@ -3893,17 +4036,16 @@ JSBool
 baseops::SetPropertyHelper(JSContext *cx, HandleObject obj, HandleObject receiver, HandleId id,
                            unsigned defineHow, MutableHandleValue vp, JSBool strict)
 {
     unsigned attrs, flags;
     int shortid;
     Class *clasp;
     PropertyOp getter;
     StrictPropertyOp setter;
-    bool added;
 
     JS_ASSERT((defineHow & ~(DNP_CACHE_RESULT | DNP_UNQUALIFIED)) == 0);
 
     if (JS_UNLIKELY(obj->watched())) {
         /* Fire watchpoints, if any. */
         WatchpointMap *wpmap = cx->compartment->watchpointMap;
         if (wpmap && !wpmap->triggerWatchpoint(cx, obj, id, vp))
             return false;
@@ -4033,83 +4175,41 @@ baseops::SetPropertyHelper(JSContext *cx
         }
     }
 
     if (IsImplicitDenseElement(shape)) {
         JSObject::setDenseElementWithType(cx, obj, JSID_TO_INT(id), vp);
         return true;
     }
 
-    added = false;
     if (!shape) {
         if (!obj->isExtensible()) {
             /* Error in strict mode code, warn with strict option, otherwise do nothing. */
             if (strict)
                 return obj->reportNotExtensible(cx);
             if (cx->hasStrictOption())
                 return obj->reportNotExtensible(cx, JSREPORT_STRICT | JSREPORT_WARNING);
-            return JS_TRUE;
+            return true;
         }
 
-        /* Use dense storage for new indexed properties where possible. */
-        if (JSID_IS_INT(id) &&
-            getter == JS_PropertyStub &&
-            setter == JS_StrictPropertyStub &&
-            attrs == JSPROP_ENUMERATE)
-        {
-            uint32_t index = JSID_TO_INT(id);
-            JSObject::EnsureDenseResult result = obj->ensureDenseElements(cx, index, 1);
-            if (result == JSObject::ED_FAILED)
-                return false;
-            if (result == JSObject::ED_OK) {
-                obj->setDenseElement(index, UndefinedValue());
-                if (!CallAddPropertyHookDense(cx, clasp, obj, index, vp)) {
-                    JSObject::setDenseElementHole(cx, obj, index);
-                    return false;
-                }
-                JSObject::setDenseElementWithType(cx, obj, index, vp);
-                return true;
-            }
-        }
-
-        /*
-         * Purge the property cache of now-shadowed id in obj's scope chain.
-         * Do this early, before locking obj to avoid nesting locks.
-         */
+        /* Purge the property cache of now-shadowed id in obj's scope chain. */
         if (!js_PurgeScopeChain(cx, obj, id))
-            return JS_FALSE;
-
-        shape = JSObject::putProperty(cx, obj, id, getter, setter, SHAPE_INVALID_SLOT,
-                                      attrs, flags, shortid);
-        if (!shape)
-            return JS_FALSE;
-
-        /* Clear any existing dense index after adding a sparse indexed property. */
-        if (JSID_IS_INT(id))
-            JSObject::removeDenseElementForSparseIndex(cx, obj, JSID_TO_INT(id));
-
-        /*
-         * Initialize the new property value (passed to setter) to undefined.
-         * Note that we store before calling addProperty, to match the order
-         * in DefineNativeProperty.
-         */
-        if (shape->hasSlot())
-            obj->nativeSetSlot(shape->slot(), UndefinedValue());
-
-        if (!CallAddPropertyHook(cx, clasp, obj, shape, vp)) {
-            obj->removeProperty(cx, id);
-            return JS_FALSE;
-        }
-        added = true;
+            return false;
+
+        if (getter == JS_PropertyStub)
+            AddTypePropertyId(cx, obj, id, vp);
+
+        return DefinePropertyOrElement(cx, obj, id, getter, setter,
+                                       attrs, flags, shortid, vp, true, strict);
     }
 
-    if ((defineHow & DNP_CACHE_RESULT) && !added)
+    if (defineHow & DNP_CACHE_RESULT)
         cx->propertyCache().fill(cx, obj, obj, shape);
 
-    return js_NativeSet(cx, obj, receiver, shape, added, strict, vp);
+    return js_NativeSet(cx, obj, receiver, shape, strict, vp);
 }
 
 JSBool
 baseops::SetElementHelper(JSContext *cx, HandleObject obj, HandleObject receiver, uint32_t index,
                           unsigned defineHow, MutableHandleValue vp, JSBool strict)
 {
     RootedId id(cx);
     if (!IndexToId(cx, index, &id))
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -362,16 +362,17 @@ class JSObject : public js::ObjectImpl
 
     enum GenerateShape {
         GENERATE_NONE,
         GENERATE_SHAPE
     };
 
     bool setFlag(JSContext *cx, /*BaseShape::Flag*/ uint32_t flag,
                  GenerateShape generateShape = GENERATE_NONE);
+    bool clearFlag(JSContext *cx, /*BaseShape::Flag*/ uint32_t flag);
 
   public:
     inline bool nativeEmpty() const;
 
     bool shadowingShapeChange(JSContext *cx, const js::Shape &shape);
 
     /*
      * Whether there may be indexed properties on this object, excluding any in
@@ -622,22 +623,37 @@ class JSObject : public js::ObjectImpl
     inline EnsureDenseResult extendDenseElements(CONTEXT *cx, unsigned requiredCapacity, unsigned extra);
 
     /* Convert a single dense element to a sparse property. */
     static bool sparsifyDenseElement(JSContext *cx, js::HandleObject obj, unsigned index);
 
     /* Convert all dense elements to sparse properties. */
     static bool sparsifyDenseElements(JSContext *cx, js::HandleObject obj);
 
+    /* Small objects are dense, no matter what. */
+    static const unsigned MIN_SPARSE_INDEX = 1000;
+
+    /*
+     * Element storage for an object will be sparse if fewer than 1/8 indexes
+     * are filled in.
+     */
+    static const unsigned SPARSE_DENSITY_RATIO = 8;
+
     /*
      * Check if after growing the object's elements will be too sparse.
      * newElementsHint is an estimated number of elements to be added.
      */
     bool willBeSparseElements(unsigned requiredCapacity, unsigned newElementsHint);
 
+    /*
+     * After adding a sparse index to obj, see if it should be converted to use
+     * dense elements.
+     */
+    static EnsureDenseResult maybeDensifySparseElements(JSContext *cx, js::HandleObject obj);
+
     /* Array specific accessors. */
     inline uint32_t getArrayLength() const;
     static inline void setArrayLength(JSContext *cx, js::HandleObject obj, uint32_t length);
     inline void setArrayLengthInt32(uint32_t length);
 
   public:
     /*
      * Date-specific getters and setters.
@@ -1326,17 +1342,17 @@ const unsigned JSGET_CACHE_RESULT = 1; /
  * scope containing shape unlocked.
  */
 extern JSBool
 js_NativeGet(JSContext *cx, js::Handle<JSObject*> obj, js::Handle<JSObject*> pobj,
              js::Handle<js::Shape*> shape, unsigned getHow, js::MutableHandle<js::Value> vp);
 
 extern JSBool
 js_NativeSet(JSContext *cx, js::Handle<JSObject*> obj, js::Handle<JSObject*> receiver,
-             js::Handle<js::Shape*> shape, bool added, bool strict, js::MutableHandleValue vp);
+             js::Handle<js::Shape*> shape, bool strict, js::MutableHandleValue vp);
 
 namespace js {
 
 bool
 GetPropertyHelper(JSContext *cx, HandleObject obj, HandleId id, uint32_t getHow, MutableHandleValue vp);
 
 inline bool
 GetPropertyHelper(JSContext *cx, HandleObject obj, PropertyName *name, uint32_t getHow, MutableHandleValue vp)
--- a/js/src/vm/Shape-inl.h
+++ b/js/src/vm/Shape-inl.h
@@ -153,18 +153,16 @@ StackBaseShape::updateGetterSetter(uint8
 inline void
 BaseShape::adoptUnowned(UnrootedUnownedBaseShape other)
 {
     /*
      * This is a base shape owned by a dictionary object, update it to reflect the
      * unowned base shape of a new last property.
      */
     JS_ASSERT(isOwned());
-    mozilla::DebugOnly<uint32_t> flags = getObjectFlags();
-    JS_ASSERT((flags & other->getObjectFlags()) == flags);
 
     uint32_t span = slotSpan();
     ShapeTable *table = &this->table();
 
     *this = *other;
     setOwned(other);
     setTable(table);
     setSlotSpan(span);
--- a/js/src/vm/Shape.cpp
+++ b/js/src/vm/Shape.cpp
@@ -1087,16 +1087,34 @@ JSObject::setFlag(JSContext *cx, /*BaseS
     UnrootedShape newShape = Shape::setObjectFlag(cx, flag, getTaggedProto(), lastProperty());
     if (!newShape)
         return false;
 
     self->shape_ = newShape;
     return true;
 }
 
+bool
+JSObject::clearFlag(JSContext *cx, /*BaseShape::Flag*/ uint32_t flag)
+{
+    JS_ASSERT(inDictionaryMode());
+    JS_ASSERT(lastProperty()->getObjectFlags() & flag);
+
+    RootedObject self(cx, this);
+
+    StackBaseShape base(self->lastProperty());
+    base.flags &= ~flag;
+    UnrootedUnownedBaseShape nbase = BaseShape::getUnowned(cx, base);
+    if (!nbase)
+        return false;
+
+    self->lastProperty()->base()->adoptUnowned(nbase);
+    return true;
+}
+
 /* static */ UnrootedShape
 Shape::setObjectFlag(JSContext *cx, BaseShape::Flag flag, TaggedProto proto, Shape *last)
 {
     if (last->getObjectFlags() & flag)
         return last;
 
     StackBaseShape base(last);
     base.flags |= flag;
--- a/js/src/vm/Shape.h
+++ b/js/src/vm/Shape.h
@@ -240,18 +240,19 @@ class BaseShape : public js::gc::Cell
         OWNED_SHAPE        = 0x1,
 
         /* getterObj/setterObj are active in unions below. */
         HAS_GETTER_OBJECT  = 0x2,
         HAS_SETTER_OBJECT  = 0x4,
 
         /*
          * Flags set which describe the referring object. Once set these cannot
-         * be unset, and are transferred from shape to shape as the object's
-         * last property changes.
+         * be unset (except during object densification of sparse indexes), and
+         * are transferred from shape to shape as the object's last property
+         * changes.
          */
 
         DELEGATE           =    0x8,
         NOT_EXTENSIBLE     =   0x10,
         INDEXED            =   0x20,
         BOUND_FUNCTION     =   0x40,
         VAROBJ             =   0x80,
         WATCHED            =  0x100,