/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * 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/. */ #include "vm/UnboxedObject-inl.h" #include "jit/BaselineIC.h" #include "jit/ExecutableAllocator.h" #include "jit/JitCommon.h" #include "jit/Linker.h" #include "jsobjinlines.h" #include "gc/Nursery-inl.h" #include "jit/MacroAssembler-inl.h" #include "vm/Shape-inl.h" using mozilla::ArrayLength; using mozilla::DebugOnly; using mozilla::PodCopy; using namespace js; ///////////////////////////////////////////////////////////////////// // UnboxedLayout ///////////////////////////////////////////////////////////////////// void UnboxedLayout::trace(JSTracer* trc) { for (size_t i = 0; i < properties_.length(); i++) TraceManuallyBarrieredEdge(trc, &properties_[i].name, "unboxed_layout_name"); if (newScript()) newScript()->trace(trc); TraceNullableEdge(trc, &nativeGroup_, "unboxed_layout_nativeGroup"); TraceNullableEdge(trc, &nativeShape_, "unboxed_layout_nativeShape"); TraceNullableEdge(trc, &allocationScript_, "unboxed_layout_allocationScript"); TraceNullableEdge(trc, &replacementGroup_, "unboxed_layout_replacementGroup"); TraceNullableEdge(trc, &constructorCode_, "unboxed_layout_constructorCode"); } size_t UnboxedLayout::sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { return mallocSizeOf(this) + properties_.sizeOfExcludingThis(mallocSizeOf) + (newScript() ? newScript()->sizeOfIncludingThis(mallocSizeOf) : 0) + mallocSizeOf(traceList()); } void UnboxedLayout::setNewScript(TypeNewScript* newScript, bool writeBarrier /* = true */) { if (newScript_ && writeBarrier) TypeNewScript::writeBarrierPre(newScript_); newScript_ = newScript; } // Constructor code returns a 0x1 value to indicate the constructor code should // be cleared. static const uintptr_t CLEAR_CONSTRUCTOR_CODE_TOKEN = 0x1; /* static */ bool UnboxedLayout::makeConstructorCode(JSContext* cx, HandleObjectGroup group) { gc::AutoSuppressGC suppress(cx); using namespace jit; if (!cx->compartment()->ensureJitCompartmentExists(cx)) return false; UnboxedLayout& layout = group->unboxedLayout(); MOZ_ASSERT(!layout.constructorCode()); UnboxedPlainObject* templateObject = UnboxedPlainObject::create(cx, group, TenuredObject); if (!templateObject) return false; JitContext jitContext(cx, nullptr); MacroAssembler masm; Register propertiesReg, newKindReg; #ifdef JS_CODEGEN_X86 propertiesReg = eax; newKindReg = ecx; masm.loadPtr(Address(masm.getStackPointer(), sizeof(void*)), propertiesReg); masm.loadPtr(Address(masm.getStackPointer(), 2 * sizeof(void*)), newKindReg); #else propertiesReg = IntArgReg0; newKindReg = IntArgReg1; #endif #ifdef JS_CODEGEN_ARM64 // ARM64 communicates stack address via sp, but uses a pseudo-sp for addressing. masm.initStackPtr(); #endif MOZ_ASSERT(propertiesReg.volatile_()); MOZ_ASSERT(newKindReg.volatile_()); AllocatableGeneralRegisterSet regs(GeneralRegisterSet::All()); regs.take(propertiesReg); regs.take(newKindReg); Register object = regs.takeAny(), scratch1 = regs.takeAny(), scratch2 = regs.takeAny(); LiveGeneralRegisterSet savedNonVolatileRegisters = SavedNonVolatileRegisters(regs); masm.PushRegsInMask(savedNonVolatileRegisters); // The scratch double register might be used by MacroAssembler methods. if (ScratchDoubleReg.volatile_()) masm.push(ScratchDoubleReg); Label failure, tenuredObject, allocated; masm.branch32(Assembler::NotEqual, newKindReg, Imm32(GenericObject), &tenuredObject); masm.branchTest32(Assembler::NonZero, AbsoluteAddress(group->addressOfFlags()), Imm32(OBJECT_FLAG_PRE_TENURE), &tenuredObject); // Allocate an object in the nursery masm.createGCObject(object, scratch1, templateObject, gc::DefaultHeap, &failure, /* initFixedSlots = */ false); masm.jump(&allocated); masm.bind(&tenuredObject); // Allocate an object in the tenured heap. masm.createGCObject(object, scratch1, templateObject, gc::TenuredHeap, &failure, /* initFixedSlots = */ false); // If any of the properties being stored are in the nursery, add a store // buffer entry for the new object. Label postBarrier; for (size_t i = 0; i < layout.properties().length(); i++) { const UnboxedLayout::Property& property = layout.properties()[i]; if (property.type == JSVAL_TYPE_OBJECT) { Address valueAddress(propertiesReg, i * sizeof(IdValuePair) + offsetof(IdValuePair, value)); Label notObject; masm.branchTestObject(Assembler::NotEqual, valueAddress, ¬Object); Register valueObject = masm.extractObject(valueAddress, scratch1); masm.branchPtrInNurseryChunk(Assembler::Equal, valueObject, scratch2, &postBarrier); masm.bind(¬Object); } } masm.jump(&allocated); masm.bind(&postBarrier); LiveGeneralRegisterSet liveVolatileRegisters; liveVolatileRegisters.add(propertiesReg); if (object.volatile_()) liveVolatileRegisters.add(object); masm.PushRegsInMask(liveVolatileRegisters); masm.mov(ImmPtr(cx->runtime()), scratch1); masm.setupUnalignedABICall(scratch2); masm.passABIArg(scratch1); masm.passABIArg(object); masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, PostWriteBarrier)); masm.PopRegsInMask(liveVolatileRegisters); masm.bind(&allocated); ValueOperand valueOperand; #ifdef JS_NUNBOX32 valueOperand = ValueOperand(scratch1, scratch2); #else valueOperand = ValueOperand(scratch1); #endif Label failureStoreOther, failureStoreObject; for (size_t i = 0; i < layout.properties().length(); i++) { const UnboxedLayout::Property& property = layout.properties()[i]; Address valueAddress(propertiesReg, i * sizeof(IdValuePair) + offsetof(IdValuePair, value)); Address targetAddress(object, UnboxedPlainObject::offsetOfData() + property.offset); masm.loadValue(valueAddress, valueOperand); if (property.type == JSVAL_TYPE_OBJECT) { HeapTypeSet* types = group->maybeGetProperty(IdToTypeId(NameToId(property.name))); Label notObject; masm.branchTestObject(Assembler::NotEqual, valueOperand, types->mightBeMIRType(MIRType::Null) ? ¬Object : &failureStoreObject); Register payloadReg = masm.extractObject(valueOperand, scratch1); if (!types->hasType(TypeSet::AnyObjectType())) { Register scratch = (payloadReg == scratch1) ? scratch2 : scratch1; masm.guardObjectType(payloadReg, types, scratch, &failureStoreObject); } masm.storeUnboxedProperty(targetAddress, JSVAL_TYPE_OBJECT, TypedOrValueRegister(MIRType::Object, AnyRegister(payloadReg)), nullptr); if (notObject.used()) { Label done; masm.jump(&done); masm.bind(¬Object); masm.branchTestNull(Assembler::NotEqual, valueOperand, &failureStoreOther); masm.storeUnboxedProperty(targetAddress, JSVAL_TYPE_OBJECT, NullValue(), nullptr); masm.bind(&done); } } else { masm.storeUnboxedProperty(targetAddress, property.type, ConstantOrRegister(valueOperand), &failureStoreOther); } } Label done; masm.bind(&done); if (object != ReturnReg) masm.movePtr(object, ReturnReg); // Restore non-volatile registers which were saved on entry. if (ScratchDoubleReg.volatile_()) masm.pop(ScratchDoubleReg); masm.PopRegsInMask(savedNonVolatileRegisters); masm.abiret(); masm.bind(&failureStoreOther); // There was a failure while storing a value which cannot be stored at all // in the unboxed object. Initialize the object so it is safe for GC and // return null. masm.initUnboxedObjectContents(object, templateObject); masm.bind(&failure); masm.movePtr(ImmWord(0), object); masm.jump(&done); masm.bind(&failureStoreObject); // There was a failure while storing a value to an object slot of the // unboxed object. If the value is storable, the failure occurred due to // incomplete type information in the object, so return a token to trigger // regeneration of the jitcode after a new object is created in the VM. { Label isObject; masm.branchTestObject(Assembler::Equal, valueOperand, &isObject); masm.branchTestNull(Assembler::NotEqual, valueOperand, &failureStoreOther); masm.bind(&isObject); } // Initialize the object so it is safe for GC. masm.initUnboxedObjectContents(object, templateObject); masm.movePtr(ImmWord(CLEAR_CONSTRUCTOR_CODE_TOKEN), object); masm.jump(&done); Linker linker(masm); AutoFlushICache afc("UnboxedObject"); JitCode* code = linker.newCode(cx, OTHER_CODE); if (!code) return false; layout.setConstructorCode(code); return true; } void UnboxedLayout::detachFromCompartment() { if (isInList()) remove(); } ///////////////////////////////////////////////////////////////////// // UnboxedPlainObject ///////////////////////////////////////////////////////////////////// bool UnboxedPlainObject::setValue(ExclusiveContext* cx, const UnboxedLayout::Property& property, const Value& v) { uint8_t* p = &data_[property.offset]; return SetUnboxedValue(cx, this, NameToId(property.name), p, property.type, v, /* preBarrier = */ true); } Value UnboxedPlainObject::getValue(const UnboxedLayout::Property& property, bool maybeUninitialized /* = false */) { uint8_t* p = &data_[property.offset]; return GetUnboxedValue(p, property.type, maybeUninitialized); } void UnboxedPlainObject::trace(JSTracer* trc, JSObject* obj) { if (obj->as().expando_) { TraceManuallyBarrieredEdge(trc, reinterpret_cast(&obj->as().expando_), "unboxed_expando"); } const UnboxedLayout& layout = obj->as().layoutDontCheckGeneration(); const int32_t* list = layout.traceList(); if (!list) return; uint8_t* data = obj->as().data(); while (*list != -1) { GCPtrString* heap = reinterpret_cast(data + *list); TraceEdge(trc, heap, "unboxed_string"); list++; } list++; while (*list != -1) { GCPtrObject* heap = reinterpret_cast(data + *list); TraceNullableEdge(trc, heap, "unboxed_object"); list++; } // Unboxed objects don't have Values to trace. MOZ_ASSERT(*(list + 1) == -1); } /* static */ UnboxedExpandoObject* UnboxedPlainObject::ensureExpando(JSContext* cx, Handle obj) { if (obj->expando_) return obj->expando_; UnboxedExpandoObject* expando = NewObjectWithGivenProto(cx, nullptr, gc::AllocKind::OBJECT4); if (!expando) return nullptr; // Don't track property types for expando objects. This allows Baseline // and Ion AddSlot ICs to guard on the unboxed group without guarding on // the expando group. MarkObjectGroupUnknownProperties(cx, expando->group()); // If the expando is tenured then the original object must also be tenured. // Otherwise barriers triggered on the original object for writes to the // expando (as can happen in the JIT) won't see the tenured->nursery edge. // See WholeCellEdges::mark. MOZ_ASSERT_IF(!IsInsideNursery(expando), !IsInsideNursery(obj)); // As with setValue(), we need to manually trigger post barriers on the // whole object. If we treat the field as a GCPtrObject and later // convert the object to its native representation, we will end up with a // corrupted store buffer entry. if (IsInsideNursery(expando) && !IsInsideNursery(obj)) cx->runtime()->gc.storeBuffer.putWholeCell(obj); obj->expando_ = expando; return expando; } bool UnboxedPlainObject::containsUnboxedOrExpandoProperty(ExclusiveContext* cx, jsid id) const { if (layout().lookup(id)) return true; if (maybeExpando() && maybeExpando()->containsShapeOrElement(cx, id)) return true; return false; } static bool PropagatePropertyTypes(JSContext* cx, jsid id, ObjectGroup* oldGroup, ObjectGroup* newGroup) { HeapTypeSet* typeProperty = oldGroup->maybeGetProperty(id); TypeSet::TypeList types; if (!typeProperty->enumerateTypes(&types)) { ReportOutOfMemory(cx); return false; } for (size_t j = 0; j < types.length(); j++) AddTypePropertyId(cx, newGroup, nullptr, id, types[j]); return true; } static PlainObject* MakeReplacementTemplateObject(JSContext* cx, HandleObjectGroup group, const UnboxedLayout &layout) { PlainObject* obj = NewObjectWithGroup(cx, group, layout.getAllocKind(), TenuredObject); if (!obj) return nullptr; for (size_t i = 0; i < layout.properties().length(); i++) { const UnboxedLayout::Property& property = layout.properties()[i]; if (!obj->addDataProperty(cx, NameToId(property.name), i, JSPROP_ENUMERATE)) return nullptr; MOZ_ASSERT(obj->slotSpan() == i + 1); MOZ_ASSERT(!obj->inDictionaryMode()); } return obj; } /* static */ bool UnboxedLayout::makeNativeGroup(JSContext* cx, ObjectGroup* group) { AutoEnterAnalysis enter(cx); UnboxedLayout& layout = group->unboxedLayout(); Rooted proto(cx, group->proto()); MOZ_ASSERT(!layout.nativeGroup()); RootedObjectGroup replacementGroup(cx); const Class* clasp = layout.isArray() ? &ArrayObject::class_ : &PlainObject::class_; // Immediately clear any new script on the group. This is done by replacing // the existing new script with one for a replacement default new group. // This is done so that the size of the replacment group's objects is the // same as that for the unboxed group, so that we do not see polymorphic // slot accesses later on for sites that see converted objects from this // group and objects that were allocated using the replacement new group. if (layout.newScript()) { MOZ_ASSERT(!layout.isArray()); replacementGroup = ObjectGroupCompartment::makeGroup(cx, &PlainObject::class_, proto); if (!replacementGroup) return false; PlainObject* templateObject = MakeReplacementTemplateObject(cx, replacementGroup, layout); if (!templateObject) return false; TypeNewScript* replacementNewScript = TypeNewScript::makeNativeVersion(cx, layout.newScript(), templateObject); if (!replacementNewScript) return false; replacementGroup->setNewScript(replacementNewScript); gc::TraceTypeNewScript(replacementGroup); group->clearNewScript(cx, replacementGroup); } // Similarly, if this group is keyed to an allocation site, replace its // entry with a new group that has no unboxed layout. if (layout.allocationScript()) { RootedScript script(cx, layout.allocationScript()); jsbytecode* pc = layout.allocationPc(); replacementGroup = ObjectGroupCompartment::makeGroup(cx, clasp, proto); if (!replacementGroup) return false; PlainObject* templateObject = &script->getObject(pc)->as(); replacementGroup->addDefiniteProperties(cx, templateObject->lastProperty()); JSProtoKey key = layout.isArray() ? JSProto_Array : JSProto_Object; cx->compartment()->objectGroups.replaceAllocationSiteGroup(script, pc, key, replacementGroup); // Clear any baseline information at this opcode which might use the old group. if (script->hasBaselineScript()) { jit::ICEntry& entry = script->baselineScript()->icEntryFromPCOffset(script->pcToOffset(pc)); jit::ICFallbackStub* fallback = entry.fallbackStub(); for (jit::ICStubIterator iter = fallback->beginChain(); !iter.atEnd(); iter++) iter.unlink(cx); if (fallback->isNewObject_Fallback()) fallback->toNewObject_Fallback()->setTemplateObject(nullptr); else if (fallback->isNewArray_Fallback()) fallback->toNewArray_Fallback()->setTemplateGroup(replacementGroup); } } size_t nfixed = layout.isArray() ? 0 : gc::GetGCKindSlots(layout.getAllocKind()); if (layout.isArray()) { // The length shape to use for arrays is cached via a modified initial // shape for array objects. Create an array now to make sure this entry // is instantiated. if (!NewDenseEmptyArray(cx)) return false; } RootedShape shape(cx, EmptyShape::getInitialShape(cx, clasp, proto, nfixed, 0)); if (!shape) return false; MOZ_ASSERT_IF(layout.isArray(), !shape->isEmptyShape() && shape->slotSpan() == 0); // Add shapes for each property, if this is for a plain object. for (size_t i = 0; i < layout.properties().length(); i++) { const UnboxedLayout::Property& property = layout.properties()[i]; Rooted child(cx, StackShape(shape->base()->unowned(), NameToId(property.name), i, JSPROP_ENUMERATE, 0)); shape = cx->zone()->propertyTree.getChild(cx, shape, child); if (!shape) return false; } ObjectGroup* nativeGroup = ObjectGroupCompartment::makeGroup(cx, clasp, proto, group->flags() & OBJECT_FLAG_DYNAMIC_MASK); if (!nativeGroup) return false; // No sense propagating if we don't know what we started with. if (!group->unknownProperties()) { // Propagate all property types from the old group to the new group. if (layout.isArray()) { if (!PropagatePropertyTypes(cx, JSID_VOID, group, nativeGroup)) return false; } else { for (size_t i = 0; i < layout.properties().length(); i++) { const UnboxedLayout::Property& property = layout.properties()[i]; jsid id = NameToId(property.name); if (!PropagatePropertyTypes(cx, id, group, nativeGroup)) return false; // If we are OOM we may not be able to propagate properties. if (nativeGroup->unknownProperties()) break; HeapTypeSet* nativeProperty = nativeGroup->maybeGetProperty(id); if (nativeProperty && nativeProperty->canSetDefinite(i)) nativeProperty->setDefinite(i); } } } else { // If we skip, though, the new group had better agree. MOZ_ASSERT(nativeGroup->unknownProperties()); } layout.nativeGroup_ = nativeGroup; layout.nativeShape_ = shape; layout.replacementGroup_ = replacementGroup; nativeGroup->setOriginalUnboxedGroup(group); group->markStateChange(cx); return true; } /* static */ bool UnboxedPlainObject::convertToNative(JSContext* cx, JSObject* obj) { const UnboxedLayout& layout = obj->as().layout(); UnboxedExpandoObject* expando = obj->as().maybeExpando(); if (!layout.nativeGroup()) { if (!UnboxedLayout::makeNativeGroup(cx, obj->group())) return false; // makeNativeGroup can reentrantly invoke this method. if (obj->is()) return true; } AutoValueVector values(cx); for (size_t i = 0; i < layout.properties().length(); i++) { // We might be reading properties off the object which have not been // initialized yet. Make sure any double values we read here are // canonicalized. if (!values.append(obj->as().getValue(layout.properties()[i], true))) return false; } // We are eliminating the expando edge with the conversion, so trigger a // pre barrier. JSObject::writeBarrierPre(expando); // Additionally trigger a post barrier on the expando itself. Whole cell // store buffer entries can be added on the original unboxed object for // writes to the expando (see WholeCellEdges::trace), so after conversion // we need to make sure the expando itself will still be traced. if (expando && !IsInsideNursery(expando)) cx->runtime()->gc.storeBuffer.putWholeCell(expando); obj->setGroup(layout.nativeGroup()); obj->as().setLastPropertyMakeNative(cx, layout.nativeShape()); for (size_t i = 0; i < values.length(); i++) obj->as().initSlotUnchecked(i, values[i]); if (expando) { // Add properties from the expando object to the object, in order. // Suppress GC here, so that callers don't need to worry about this // method collecting. The stuff below can only fail due to OOM, in // which case the object will not have been completely filled back in. gc::AutoSuppressGC suppress(cx); Vector ids(cx); for (Shape::Range r(expando->lastProperty()); !r.empty(); r.popFront()) { if (!ids.append(r.front().propid())) return false; } for (size_t i = 0; i < expando->getDenseInitializedLength(); i++) { if (!expando->getDenseElement(i).isMagic(JS_ELEMENTS_HOLE)) { if (!ids.append(INT_TO_JSID(i))) return false; } } ::Reverse(ids.begin(), ids.end()); RootedPlainObject nobj(cx, &obj->as()); Rooted nexpando(cx, expando); RootedId id(cx); Rooted desc(cx); for (size_t i = 0; i < ids.length(); i++) { id = ids[i]; if (!GetOwnPropertyDescriptor(cx, nexpando, id, &desc)) return false; ObjectOpResult result; if (!DefineProperty(cx, nobj, id, desc, result)) return false; MOZ_ASSERT(result.ok()); } } return true; } /* static */ UnboxedPlainObject* UnboxedPlainObject::create(ExclusiveContext* cx, HandleObjectGroup group, NewObjectKind newKind) { AutoSetNewObjectMetadata metadata(cx); MOZ_ASSERT(group->clasp() == &class_); gc::AllocKind allocKind = group->unboxedLayout().getAllocKind(); UnboxedPlainObject* res = NewObjectWithGroup(cx, group, allocKind, newKind); if (!res) return nullptr; // Overwrite the dummy shape which was written to the object's expando field. res->initExpando(); // Initialize reference fields of the object. All fields in the object will // be overwritten shortly, but references need to be safe for the GC. const int32_t* list = res->layout().traceList(); if (list) { uint8_t* data = res->data(); while (*list != -1) { GCPtrString* heap = reinterpret_cast(data + *list); heap->init(cx->names().empty); list++; } list++; while (*list != -1) { GCPtrObject* heap = reinterpret_cast(data + *list); heap->init(nullptr); list++; } // Unboxed objects don't have Values to initialize. MOZ_ASSERT(*(list + 1) == -1); } return res; } /* static */ JSObject* UnboxedPlainObject::createWithProperties(ExclusiveContext* cx, HandleObjectGroup group, NewObjectKind newKind, IdValuePair* properties) { MOZ_ASSERT(newKind == GenericObject || newKind == TenuredObject); UnboxedLayout& layout = group->unboxedLayout(); if (layout.constructorCode()) { MOZ_ASSERT(cx->isJSContext()); typedef JSObject* (*ConstructorCodeSignature)(IdValuePair*, NewObjectKind); ConstructorCodeSignature function = reinterpret_cast(layout.constructorCode()->raw()); JSObject* obj; { JS::AutoSuppressGCAnalysis nogc; obj = reinterpret_cast(CALL_GENERATED_2(function, properties, newKind)); } if (obj > reinterpret_cast(CLEAR_CONSTRUCTOR_CODE_TOKEN)) return obj; if (obj == reinterpret_cast(CLEAR_CONSTRUCTOR_CODE_TOKEN)) layout.setConstructorCode(nullptr); } UnboxedPlainObject* obj = UnboxedPlainObject::create(cx, group, newKind); if (!obj) return nullptr; for (size_t i = 0; i < layout.properties().length(); i++) { if (!obj->setValue(cx, layout.properties()[i], properties[i].value)) return NewPlainObjectWithProperties(cx, properties, layout.properties().length(), newKind); } #ifndef JS_CODEGEN_NONE if (cx->isJSContext() && !group->unknownProperties() && !layout.constructorCode() && cx->asJSContext()->runtime()->jitSupportsFloatingPoint && jit::CanLikelyAllocateMoreExecutableMemory()) { if (!UnboxedLayout::makeConstructorCode(cx->asJSContext(), group)) return nullptr; } #endif return obj; } /* static */ bool UnboxedPlainObject::obj_lookupProperty(JSContext* cx, HandleObject obj, HandleId id, MutableHandleObject objp, MutableHandle propp) { if (obj->as().containsUnboxedOrExpandoProperty(cx, id)) { propp.setNonNativeProperty(); objp.set(obj); return true; } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { objp.set(nullptr); propp.setNotFound(); return true; } return LookupProperty(cx, proto, id, objp, propp); } /* static */ bool UnboxedPlainObject::obj_defineProperty(JSContext* cx, HandleObject obj, HandleId id, Handle desc, ObjectOpResult& result) { const UnboxedLayout& layout = obj->as().layout(); if (const UnboxedLayout::Property* property = layout.lookup(id)) { if (!desc.getter() && !desc.setter() && desc.attributes() == JSPROP_ENUMERATE) { // This define is equivalent to setting an existing property. if (obj->as().setValue(cx, *property, desc.value())) return result.succeed(); } // Trying to incompatibly redefine an existing property requires the // object to be converted to a native object. if (!convertToNative(cx, obj)) return false; return DefineProperty(cx, obj, id, desc, result); } // Define the property on the expando object. Rooted expando(cx, ensureExpando(cx, obj.as())); if (!expando) return false; // Update property types on the unboxed object as well. AddTypePropertyId(cx, obj, id, desc.value()); return DefineProperty(cx, expando, id, desc, result); } /* static */ bool UnboxedPlainObject::obj_hasProperty(JSContext* cx, HandleObject obj, HandleId id, bool* foundp) { if (obj->as().containsUnboxedOrExpandoProperty(cx, id)) { *foundp = true; return true; } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { *foundp = false; return true; } return HasProperty(cx, proto, id, foundp); } /* static */ bool UnboxedPlainObject::obj_getProperty(JSContext* cx, HandleObject obj, HandleValue receiver, HandleId id, MutableHandleValue vp) { const UnboxedLayout& layout = obj->as().layout(); if (const UnboxedLayout::Property* property = layout.lookup(id)) { vp.set(obj->as().getValue(*property)); return true; } if (UnboxedExpandoObject* expando = obj->as().maybeExpando()) { if (expando->containsShapeOrElement(cx, id)) { RootedObject nexpando(cx, expando); return GetProperty(cx, nexpando, receiver, id, vp); } } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { vp.setUndefined(); return true; } return GetProperty(cx, proto, receiver, id, vp); } /* static */ bool UnboxedPlainObject::obj_setProperty(JSContext* cx, HandleObject obj, HandleId id, HandleValue v, HandleValue receiver, ObjectOpResult& result) { const UnboxedLayout& layout = obj->as().layout(); if (const UnboxedLayout::Property* property = layout.lookup(id)) { if (receiver.isObject() && obj == &receiver.toObject()) { if (obj->as().setValue(cx, *property, v)) return result.succeed(); if (!convertToNative(cx, obj)) return false; return SetProperty(cx, obj, id, v, receiver, result); } return SetPropertyByDefining(cx, id, v, receiver, result); } if (UnboxedExpandoObject* expando = obj->as().maybeExpando()) { if (expando->containsShapeOrElement(cx, id)) { // Update property types on the unboxed object as well. AddTypePropertyId(cx, obj, id, v); RootedObject nexpando(cx, expando); return SetProperty(cx, nexpando, id, v, receiver, result); } } return SetPropertyOnProto(cx, obj, id, v, receiver, result); } /* static */ bool UnboxedPlainObject::obj_getOwnPropertyDescriptor(JSContext* cx, HandleObject obj, HandleId id, MutableHandle desc) { const UnboxedLayout& layout = obj->as().layout(); if (const UnboxedLayout::Property* property = layout.lookup(id)) { desc.value().set(obj->as().getValue(*property)); desc.setAttributes(JSPROP_ENUMERATE); desc.object().set(obj); return true; } if (UnboxedExpandoObject* expando = obj->as().maybeExpando()) { if (expando->containsShapeOrElement(cx, id)) { RootedObject nexpando(cx, expando); if (!GetOwnPropertyDescriptor(cx, nexpando, id, desc)) return false; if (desc.object() == nexpando) desc.object().set(obj); return true; } } desc.object().set(nullptr); return true; } /* static */ bool UnboxedPlainObject::obj_deleteProperty(JSContext* cx, HandleObject obj, HandleId id, ObjectOpResult& result) { if (!convertToNative(cx, obj)) return false; return DeleteProperty(cx, obj, id, result); } /* static */ bool UnboxedPlainObject::obj_enumerate(JSContext* cx, HandleObject obj, AutoIdVector& properties, bool enumerableOnly) { // Ignore expando properties here, they are special-cased by the property // enumeration code. const UnboxedLayout::PropertyVector& unboxed = obj->as().layout().properties(); for (size_t i = 0; i < unboxed.length(); i++) { if (!properties.append(NameToId(unboxed[i].name))) return false; } return true; } const Class UnboxedExpandoObject::class_ = { "UnboxedExpandoObject", 0 }; static const ClassOps UnboxedPlainObjectClassOps = { nullptr, /* addProperty */ nullptr, /* delProperty */ nullptr, /* getProperty */ nullptr, /* setProperty */ nullptr, /* enumerate */ nullptr, /* resolve */ nullptr, /* mayResolve */ nullptr, /* finalize */ nullptr, /* call */ nullptr, /* hasInstance */ nullptr, /* construct */ UnboxedPlainObject::trace, }; static const ObjectOps UnboxedPlainObjectObjectOps = { UnboxedPlainObject::obj_lookupProperty, UnboxedPlainObject::obj_defineProperty, UnboxedPlainObject::obj_hasProperty, UnboxedPlainObject::obj_getProperty, UnboxedPlainObject::obj_setProperty, UnboxedPlainObject::obj_getOwnPropertyDescriptor, UnboxedPlainObject::obj_deleteProperty, nullptr, /* getElements */ UnboxedPlainObject::obj_enumerate, nullptr /* funToString */ }; const Class UnboxedPlainObject::class_ = { js_Object_str, Class::NON_NATIVE | JSCLASS_HAS_CACHED_PROTO(JSProto_Object) | JSCLASS_DELAY_METADATA_BUILDER, &UnboxedPlainObjectClassOps, JS_NULL_CLASS_SPEC, JS_NULL_CLASS_EXT, &UnboxedPlainObjectObjectOps }; ///////////////////////////////////////////////////////////////////// // UnboxedArrayObject ///////////////////////////////////////////////////////////////////// template DenseElementResult AppendUnboxedDenseElements(UnboxedArrayObject* obj, uint32_t initlen, MutableHandle> values) { for (size_t i = 0; i < initlen; i++) values.infallibleAppend(obj->getElementSpecific(i)); return DenseElementResult::Success; } DefineBoxedOrUnboxedFunctor3(AppendUnboxedDenseElements, UnboxedArrayObject*, uint32_t, MutableHandle>); /* static */ bool UnboxedArrayObject::convertToNativeWithGroup(ExclusiveContext* cx, JSObject* obj, ObjectGroup* group, Shape* shape) { size_t length = obj->as().length(); size_t initlen = obj->as().initializedLength(); Rooted> values(cx, GCVector(cx)); if (!values.reserve(initlen)) return false; AppendUnboxedDenseElementsFunctor functor(&obj->as(), initlen, &values); DebugOnly result = CallBoxedOrUnboxedSpecialization(functor, obj); MOZ_ASSERT(result.value == DenseElementResult::Success); obj->setGroup(group); ArrayObject* aobj = &obj->as(); aobj->setLastPropertyMakeNative(cx, shape); // Make sure there is at least one element, so that this array does not // use emptyObjectElements / emptyObjectElementsShared. if (!aobj->ensureElements(cx, Max(initlen, 1))) return false; MOZ_ASSERT(!aobj->getDenseInitializedLength()); aobj->setDenseInitializedLength(initlen); aobj->initDenseElements(0, values.begin(), initlen); aobj->setLengthInt32(length); return true; } /* static */ bool UnboxedArrayObject::convertToNative(JSContext* cx, JSObject* obj) { const UnboxedLayout& layout = obj->as().layout(); if (!layout.nativeGroup()) { if (!UnboxedLayout::makeNativeGroup(cx, obj->group())) return false; } return convertToNativeWithGroup(cx, obj, layout.nativeGroup(), layout.nativeShape()); } bool UnboxedArrayObject::convertInt32ToDouble(ExclusiveContext* cx, ObjectGroup* group) { MOZ_ASSERT(elementType() == JSVAL_TYPE_INT32); MOZ_ASSERT(group->unboxedLayout().elementType() == JSVAL_TYPE_DOUBLE); Vector values(cx); if (!values.reserve(initializedLength())) return false; for (size_t i = 0; i < initializedLength(); i++) values.infallibleAppend(getElementSpecific(i).toInt32()); uint8_t* newElements; if (hasInlineElements()) { newElements = AllocateObjectBuffer(cx, this, capacity() * sizeof(double)); } else { newElements = ReallocateObjectBuffer(cx, this, elements(), capacity() * sizeof(int32_t), capacity() * sizeof(double)); } if (!newElements) return false; setGroup(group); elements_ = newElements; for (size_t i = 0; i < initializedLength(); i++) setElementNoTypeChangeSpecific(i, DoubleValue(values[i])); return true; } /* static */ UnboxedArrayObject* UnboxedArrayObject::create(ExclusiveContext* cx, HandleObjectGroup group, uint32_t length, NewObjectKind newKind, uint32_t maxLength) { MOZ_ASSERT(length <= MaximumCapacity); MOZ_ASSERT(group->clasp() == &class_); uint32_t elementSize = UnboxedTypeSize(group->unboxedLayout().elementType()); uint32_t capacity = Min(length, maxLength); uint32_t nbytes = offsetOfInlineElements() + elementSize * capacity; UnboxedArrayObject* res; if (nbytes <= JSObject::MAX_BYTE_SIZE) { gc::AllocKind allocKind = gc::GetGCObjectKindForBytes(nbytes); // If there was no provided length information, pick an allocation kind // to accommodate small arrays (as is done for normal native arrays). if (capacity == 0) allocKind = gc::AllocKind::OBJECT8; res = NewObjectWithGroup(cx, group, allocKind, newKind); if (!res) return nullptr; res->setInitializedLengthNoBarrier(0); res->setInlineElements(); size_t actualCapacity = (GetGCKindBytes(allocKind) - offsetOfInlineElements()) / elementSize; MOZ_ASSERT(actualCapacity >= capacity); res->setCapacityIndex(exactCapacityIndex(actualCapacity)); } else { res = NewObjectWithGroup(cx, group, gc::AllocKind::OBJECT0, newKind); if (!res) return nullptr; res->setInitializedLengthNoBarrier(0); uint32_t capacityIndex = (capacity == length) ? CapacityMatchesLengthIndex : chooseCapacityIndex(capacity, length); uint32_t actualCapacity = computeCapacity(capacityIndex, length); res->elements_ = AllocateObjectBuffer(cx, res, actualCapacity * elementSize); if (!res->elements_) { // Make the object safe for GC. res->setInlineElements(); return nullptr; } res->setCapacityIndex(capacityIndex); } res->setLength(cx, length); return res; } bool UnboxedArrayObject::setElement(ExclusiveContext* cx, size_t index, const Value& v) { MOZ_ASSERT(index < initializedLength()); uint8_t* p = elements() + index * elementSize(); return SetUnboxedValue(cx, this, JSID_VOID, p, elementType(), v, /* preBarrier = */ true); } bool UnboxedArrayObject::initElement(ExclusiveContext* cx, size_t index, const Value& v) { MOZ_ASSERT(index < initializedLength()); uint8_t* p = elements() + index * elementSize(); return SetUnboxedValue(cx, this, JSID_VOID, p, elementType(), v, /* preBarrier = */ false); } void UnboxedArrayObject::initElementNoTypeChange(size_t index, const Value& v) { MOZ_ASSERT(index < initializedLength()); uint8_t* p = elements() + index * elementSize(); if (UnboxedTypeNeedsPreBarrier(elementType())) *reinterpret_cast(p) = nullptr; SetUnboxedValueNoTypeChange(this, p, elementType(), v, /* preBarrier = */ false); } Value UnboxedArrayObject::getElement(size_t index) { MOZ_ASSERT(index < initializedLength()); uint8_t* p = elements() + index * elementSize(); return GetUnboxedValue(p, elementType(), /* maybeUninitialized = */ false); } /* static */ void UnboxedArrayObject::trace(JSTracer* trc, JSObject* obj) { JSValueType type = obj->as().elementType(); if (!UnboxedTypeNeedsPreBarrier(type)) return; MOZ_ASSERT(obj->as().elementSize() == sizeof(uintptr_t)); size_t initlen = obj->as().initializedLength(); void** elements = reinterpret_cast(obj->as().elements()); switch (type) { case JSVAL_TYPE_OBJECT: for (size_t i = 0; i < initlen; i++) { GCPtrObject* heap = reinterpret_cast(elements + i); TraceNullableEdge(trc, heap, "unboxed_object"); } break; case JSVAL_TYPE_STRING: for (size_t i = 0; i < initlen; i++) { GCPtrString* heap = reinterpret_cast(elements + i); TraceEdge(trc, heap, "unboxed_string"); } break; default: MOZ_CRASH(); } } /* static */ void UnboxedArrayObject::objectMoved(JSObject* obj, const JSObject* old) { UnboxedArrayObject& dst = obj->as(); const UnboxedArrayObject& src = old->as(); // Fix up possible inline data pointer. if (src.hasInlineElements()) dst.setInlineElements(); } /* static */ void UnboxedArrayObject::finalize(FreeOp* fop, JSObject* obj) { MOZ_ASSERT(!IsInsideNursery(obj)); if (!obj->as().hasInlineElements()) js_free(obj->as().elements()); } /* static */ size_t UnboxedArrayObject::objectMovedDuringMinorGC(JSTracer* trc, JSObject* dst, JSObject* src, gc::AllocKind allocKind) { UnboxedArrayObject* ndst = &dst->as(); UnboxedArrayObject* nsrc = &src->as(); MOZ_ASSERT(ndst->elements() == nsrc->elements()); Nursery& nursery = trc->runtime()->gc.nursery; if (!nursery.isInside(nsrc->elements())) { nursery.removeMallocedBuffer(nsrc->elements()); return 0; } // Determine if we can use inline data for the target array. If this is // possible, the nursery will have picked an allocation size that is large // enough. size_t nbytes = nsrc->capacity() * nsrc->elementSize(); if (offsetOfInlineElements() + nbytes <= GetGCKindBytes(allocKind)) { ndst->setInlineElements(); } else { MOZ_ASSERT(allocKind == gc::AllocKind::OBJECT0); AutoEnterOOMUnsafeRegion oomUnsafe; uint8_t* data = nsrc->zone()->pod_malloc(nbytes); if (!data) oomUnsafe.crash("Failed to allocate unboxed array elements while tenuring."); ndst->elements_ = data; } PodCopy(ndst->elements(), nsrc->elements(), nsrc->initializedLength() * nsrc->elementSize()); // Set a forwarding pointer for the element buffers in case they were // preserved on the stack by Ion. bool direct = nsrc->capacity() * nsrc->elementSize() >= sizeof(uintptr_t); nursery.maybeSetForwardingPointer(trc, nsrc->elements(), ndst->elements(), direct); return ndst->hasInlineElements() ? 0 : nbytes; } // Possible capacities for unboxed arrays. Some of these capacities might seem // a little weird, but were chosen to allow the inline data of objects of each // size to be fully utilized for arrays of the various types on both 32 bit and // 64 bit platforms. // // To find the possible inline capacities, the following script was used: // // var fixedSlotCapacities = [0, 2, 4, 8, 12, 16]; // var dataSizes = [1, 4, 8]; // var header32 = 4 * 2 + 4 * 2; // var header64 = 8 * 2 + 4 * 2; // // for (var i = 0; i < fixedSlotCapacities.length; i++) { // var nfixed = fixedSlotCapacities[i]; // var size32 = 4 * 4 + 8 * nfixed - header32; // var size64 = 8 * 4 + 8 * nfixed - header64; // for (var j = 0; j < dataSizes.length; j++) { // print(size32 / dataSizes[j]); // print(size64 / dataSizes[j]); // } // } // /* static */ const uint32_t UnboxedArrayObject::CapacityArray[] = { UINT32_MAX, // For CapacityMatchesLengthIndex. 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 16, 17, 18, 24, 26, 32, 34, 40, 64, 72, 96, 104, 128, 136, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 3145728, 4194304, 5242880, 6291456, 7340032, 8388608, 9437184, 11534336, 13631488, 15728640, 17825792, 20971520, 24117248, 27262976, 31457280, 35651584, 40894464, 46137344, 52428800, 59768832, MaximumCapacity }; static const uint32_t Pow2CapacityIndexes[] = { 2, // 1 3, // 2 5, // 4 8, // 8 13, // 16 18, // 32 21, // 64 25, // 128 27, // 256 28, // 512 29, // 1024 30, // 2048 31, // 4096 32, // 8192 33, // 16384 34, // 32768 35, // 65536 36, // 131072 37, // 262144 38, // 524288 39 // 1048576 }; static const uint32_t MebiCapacityIndex = 39; /* static */ uint32_t UnboxedArrayObject::chooseCapacityIndex(uint32_t capacity, uint32_t length) { // Note: the structure and behavior of this method follow along with // NativeObject::goodAllocated. Changes to the allocation strategy in one // should generally be matched by the other. // Make sure we have enough space to store all possible values for the capacity index. // This ought to be a static_assert, but MSVC doesn't like that. MOZ_ASSERT(mozilla::ArrayLength(CapacityArray) - 1 <= (CapacityMask >> CapacityShift)); // The caller should have ensured the capacity is possible for an unboxed array. MOZ_ASSERT(capacity <= MaximumCapacity); static const uint32_t Mebi = 1024 * 1024; if (capacity <= Mebi) { capacity = mozilla::RoundUpPow2(capacity); // When the required capacity is close to the array length, then round // up to the array length itself, as for NativeObject. if (length >= capacity && capacity > (length / 3) * 2) return CapacityMatchesLengthIndex; if (capacity < MinimumDynamicCapacity) capacity = MinimumDynamicCapacity; uint32_t bit = mozilla::FloorLog2Size(capacity); MOZ_ASSERT(capacity == uint32_t(1 << bit)); MOZ_ASSERT(bit <= 20); MOZ_ASSERT(mozilla::ArrayLength(Pow2CapacityIndexes) == 21); uint32_t index = Pow2CapacityIndexes[bit]; MOZ_ASSERT(CapacityArray[index] == capacity); return index; } MOZ_ASSERT(CapacityArray[MebiCapacityIndex] == Mebi); for (uint32_t i = MebiCapacityIndex + 1;; i++) { if (CapacityArray[i] >= capacity) return i; } MOZ_CRASH("Invalid capacity"); } /* static */ uint32_t UnboxedArrayObject::exactCapacityIndex(uint32_t capacity) { for (size_t i = CapacityMatchesLengthIndex + 1; i < ArrayLength(CapacityArray); i++) { if (CapacityArray[i] == capacity) return i; } MOZ_CRASH(); } bool UnboxedArrayObject::growElements(ExclusiveContext* cx, size_t cap) { // The caller should have checked if this capacity is possible for an // unboxed array, so the only way this call can fail is from OOM. MOZ_ASSERT(cap <= MaximumCapacity); uint32_t oldCapacity = capacity(); uint32_t newCapacityIndex = chooseCapacityIndex(cap, length()); uint32_t newCapacity = computeCapacity(newCapacityIndex, length()); MOZ_ASSERT(oldCapacity < cap); MOZ_ASSERT(cap <= newCapacity); // The allocation size computation below cannot have integer overflows. JS_STATIC_ASSERT(MaximumCapacity < UINT32_MAX / sizeof(double)); uint8_t* newElements; if (hasInlineElements()) { newElements = AllocateObjectBuffer(cx, this, newCapacity * elementSize()); if (!newElements) return false; js_memcpy(newElements, elements(), initializedLength() * elementSize()); } else { newElements = ReallocateObjectBuffer(cx, this, elements(), oldCapacity * elementSize(), newCapacity * elementSize()); if (!newElements) return false; } elements_ = newElements; setCapacityIndex(newCapacityIndex); return true; } void UnboxedArrayObject::shrinkElements(ExclusiveContext* cx, size_t cap) { if (hasInlineElements()) return; uint32_t oldCapacity = capacity(); uint32_t newCapacityIndex = chooseCapacityIndex(cap, 0); uint32_t newCapacity = computeCapacity(newCapacityIndex, 0); MOZ_ASSERT(cap < oldCapacity); MOZ_ASSERT(cap <= newCapacity); if (newCapacity >= oldCapacity) return; uint8_t* newElements = ReallocateObjectBuffer(cx, this, elements(), oldCapacity * elementSize(), newCapacity * elementSize()); if (!newElements) return; elements_ = newElements; setCapacityIndex(newCapacityIndex); } bool UnboxedArrayObject::containsProperty(ExclusiveContext* cx, jsid id) { if (JSID_IS_INT(id) && uint32_t(JSID_TO_INT(id)) < initializedLength()) return true; if (JSID_IS_ATOM(id) && JSID_TO_ATOM(id) == cx->names().length) return true; return false; } /* static */ bool UnboxedArrayObject::obj_lookupProperty(JSContext* cx, HandleObject obj, HandleId id, MutableHandleObject objp, MutableHandle propp) { if (obj->as().containsProperty(cx, id)) { propp.setNonNativeProperty(); objp.set(obj); return true; } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { objp.set(nullptr); propp.setNotFound(); return true; } return LookupProperty(cx, proto, id, objp, propp); } /* static */ bool UnboxedArrayObject::obj_defineProperty(JSContext* cx, HandleObject obj, HandleId id, Handle desc, ObjectOpResult& result) { if (JSID_IS_INT(id) && !desc.getter() && !desc.setter() && desc.attributes() == JSPROP_ENUMERATE) { UnboxedArrayObject* nobj = &obj->as(); uint32_t index = JSID_TO_INT(id); if (index < nobj->initializedLength()) { if (nobj->setElement(cx, index, desc.value())) return result.succeed(); } else if (index == nobj->initializedLength() && index < MaximumCapacity) { if (nobj->initializedLength() == nobj->capacity()) { if (!nobj->growElements(cx, index + 1)) return false; } nobj->setInitializedLength(index + 1); if (nobj->initElement(cx, index, desc.value())) { if (nobj->length() <= index) nobj->setLengthInt32(index + 1); return result.succeed(); } nobj->setInitializedLengthNoBarrier(index); } } if (!convertToNative(cx, obj)) return false; return DefineProperty(cx, obj, id, desc, result); } /* static */ bool UnboxedArrayObject::obj_hasProperty(JSContext* cx, HandleObject obj, HandleId id, bool* foundp) { if (obj->as().containsProperty(cx, id)) { *foundp = true; return true; } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { *foundp = false; return true; } return HasProperty(cx, proto, id, foundp); } /* static */ bool UnboxedArrayObject::obj_getProperty(JSContext* cx, HandleObject obj, HandleValue receiver, HandleId id, MutableHandleValue vp) { if (obj->as().containsProperty(cx, id)) { if (JSID_IS_INT(id)) vp.set(obj->as().getElement(JSID_TO_INT(id))); else vp.set(Int32Value(obj->as().length())); return true; } RootedObject proto(cx, obj->staticPrototype()); if (!proto) { vp.setUndefined(); return true; } return GetProperty(cx, proto, receiver, id, vp); } /* static */ bool UnboxedArrayObject::obj_setProperty(JSContext* cx, HandleObject obj, HandleId id, HandleValue v, HandleValue receiver, ObjectOpResult& result) { if (obj->as().containsProperty(cx, id)) { if (receiver.isObject() && obj == &receiver.toObject()) { if (JSID_IS_INT(id)) { if (obj->as().setElement(cx, JSID_TO_INT(id), v)) return result.succeed(); } else { uint32_t len; if (!CanonicalizeArrayLengthValue(cx, v, &len)) return false; UnboxedArrayObject* nobj = &obj->as(); if (len < nobj->initializedLength()) { nobj->setInitializedLength(len); nobj->shrinkElements(cx, len); } nobj->setLength(cx, len); return result.succeed(); } if (!convertToNative(cx, obj)) return false; return SetProperty(cx, obj, id, v, receiver, result); } return SetPropertyByDefining(cx, id, v, receiver, result); } return SetPropertyOnProto(cx, obj, id, v, receiver, result); } /* static */ bool UnboxedArrayObject::obj_getOwnPropertyDescriptor(JSContext* cx, HandleObject obj, HandleId id, MutableHandle desc) { if (obj->as().containsProperty(cx, id)) { if (JSID_IS_INT(id)) { desc.value().set(obj->as().getElement(JSID_TO_INT(id))); desc.setAttributes(JSPROP_ENUMERATE); } else { desc.value().set(Int32Value(obj->as().length())); desc.setAttributes(JSPROP_PERMANENT); } desc.object().set(obj); return true; } desc.object().set(nullptr); return true; } /* static */ bool UnboxedArrayObject::obj_deleteProperty(JSContext* cx, HandleObject obj, HandleId id, ObjectOpResult& result) { if (obj->as().containsProperty(cx, id)) { size_t initlen = obj->as().initializedLength(); if (JSID_IS_INT(id) && JSID_TO_INT(id) == int32_t(initlen - 1)) { obj->as().setInitializedLength(initlen - 1); obj->as().shrinkElements(cx, initlen - 1); return result.succeed(); } } if (!convertToNative(cx, obj)) return false; return DeleteProperty(cx, obj, id, result); } /* static */ bool UnboxedArrayObject::obj_enumerate(JSContext* cx, HandleObject obj, AutoIdVector& properties, bool enumerableOnly) { for (size_t i = 0; i < obj->as().initializedLength(); i++) { if (!properties.append(INT_TO_JSID(i))) return false; } if (!enumerableOnly && !properties.append(NameToId(cx->names().length))) return false; return true; } static const ClassOps UnboxedArrayObjectClassOps = { nullptr, /* addProperty */ nullptr, /* delProperty */ nullptr, /* getProperty */ nullptr, /* setProperty */ nullptr, /* enumerate */ nullptr, /* resolve */ nullptr, /* mayResolve */ UnboxedArrayObject::finalize, nullptr, /* call */ nullptr, /* hasInstance */ nullptr, /* construct */ UnboxedArrayObject::trace, }; static const ClassExtension UnboxedArrayObjectClassExtension = { nullptr, /* weakmapKeyDelegateOp */ UnboxedArrayObject::objectMoved }; static const ObjectOps UnboxedArrayObjectObjectOps = { UnboxedArrayObject::obj_lookupProperty, UnboxedArrayObject::obj_defineProperty, UnboxedArrayObject::obj_hasProperty, UnboxedArrayObject::obj_getProperty, UnboxedArrayObject::obj_setProperty, UnboxedArrayObject::obj_getOwnPropertyDescriptor, UnboxedArrayObject::obj_deleteProperty, nullptr, /* getElements */ UnboxedArrayObject::obj_enumerate, nullptr /* funToString */ }; const Class UnboxedArrayObject::class_ = { "Array", Class::NON_NATIVE | JSCLASS_SKIP_NURSERY_FINALIZE | JSCLASS_BACKGROUND_FINALIZE, &UnboxedArrayObjectClassOps, JS_NULL_CLASS_SPEC, &UnboxedArrayObjectClassExtension, &UnboxedArrayObjectObjectOps }; ///////////////////////////////////////////////////////////////////// // API ///////////////////////////////////////////////////////////////////// static bool UnboxedTypeIncludes(JSValueType supertype, JSValueType subtype) { if (supertype == JSVAL_TYPE_DOUBLE && subtype == JSVAL_TYPE_INT32) return true; if (supertype == JSVAL_TYPE_OBJECT && subtype == JSVAL_TYPE_NULL) return true; return false; } static bool CombineUnboxedTypes(const Value& value, JSValueType* existing) { JSValueType type = value.isDouble() ? JSVAL_TYPE_DOUBLE : value.extractNonDoubleType(); if (*existing == JSVAL_TYPE_MAGIC || *existing == type || UnboxedTypeIncludes(type, *existing)) { *existing = type; return true; } if (UnboxedTypeIncludes(*existing, type)) return true; return false; } // Return whether the property names and types in layout are a subset of the // specified vector. static bool PropertiesAreSuperset(const UnboxedLayout::PropertyVector& properties, UnboxedLayout* layout) { for (size_t i = 0; i < layout->properties().length(); i++) { const UnboxedLayout::Property& layoutProperty = layout->properties()[i]; bool found = false; for (size_t j = 0; j < properties.length(); j++) { if (layoutProperty.name == properties[j].name) { found = (layoutProperty.type == properties[j].type); break; } } if (!found) return false; } return true; } static bool CombinePlainObjectProperties(PlainObject* obj, Shape* templateShape, UnboxedLayout::PropertyVector& properties) { // All preliminary objects must have been created with enough space to // fill in their unboxed data inline. This is ensured either by using // the largest allocation kind (which limits the maximum size of an // unboxed object), or by using an allocation kind that covers all // properties in the template, as the space used by unboxed properties // is less than or equal to that used by boxed properties. MOZ_ASSERT(gc::GetGCKindSlots(obj->asTenured().getAllocKind()) >= Min(NativeObject::MAX_FIXED_SLOTS, templateShape->slotSpan())); if (obj->lastProperty() != templateShape || obj->hasDynamicElements()) { // Only use an unboxed representation if all created objects match // the template shape exactly. return false; } for (size_t i = 0; i < templateShape->slotSpan(); i++) { Value val = obj->getSlot(i); JSValueType& existing = properties[i].type; if (!CombineUnboxedTypes(val, &existing)) return false; } return true; } static bool CombineArrayObjectElements(ExclusiveContext* cx, ArrayObject* obj, JSValueType* elementType) { if (obj->inDictionaryMode() || obj->lastProperty()->propid() != AtomToId(cx->names().length) || !obj->lastProperty()->previous()->isEmptyShape()) { // Only use an unboxed representation if the object has no properties. return false; } for (size_t i = 0; i < obj->getDenseInitializedLength(); i++) { Value val = obj->getDenseElement(i); // For now, unboxed arrays cannot have holes. if (val.isMagic(JS_ELEMENTS_HOLE)) return false; if (!CombineUnboxedTypes(val, elementType)) return false; } return true; } static size_t ComputePlainObjectLayout(ExclusiveContext* cx, Shape* templateShape, UnboxedLayout::PropertyVector& properties) { // Fill in the names for all the object's properties. for (Shape::Range r(templateShape); !r.empty(); r.popFront()) { size_t slot = r.front().slot(); MOZ_ASSERT(!properties[slot].name); properties[slot].name = JSID_TO_ATOM(r.front().propid())->asPropertyName(); } // Fill in all the unboxed object's property offsets. uint32_t offset = 0; // Search for an existing unboxed layout which is a subset of this one. // If there are multiple such layouts, use the largest one. If we're able // to find such a layout, use the same property offsets for the shared // properties, which will allow us to generate better code if the objects // have a subtype/supertype relation and are accessed at common sites. UnboxedLayout* bestExisting = nullptr; for (UnboxedLayout* existing : cx->compartment()->unboxedLayouts) { if (PropertiesAreSuperset(properties, existing)) { if (!bestExisting || existing->properties().length() > bestExisting->properties().length()) { bestExisting = existing; } } } if (bestExisting) { for (size_t i = 0; i < bestExisting->properties().length(); i++) { const UnboxedLayout::Property& existingProperty = bestExisting->properties()[i]; for (size_t j = 0; j < templateShape->slotSpan(); j++) { if (existingProperty.name == properties[j].name) { MOZ_ASSERT(existingProperty.type == properties[j].type); properties[j].offset = existingProperty.offset; } } } offset = bestExisting->size(); } // Order remaining properties from the largest down for the best space // utilization. static const size_t typeSizes[] = { 8, 4, 1 }; for (size_t i = 0; i < ArrayLength(typeSizes); i++) { size_t size = typeSizes[i]; for (size_t j = 0; j < templateShape->slotSpan(); j++) { if (properties[j].offset != UINT32_MAX) continue; JSValueType type = properties[j].type; if (UnboxedTypeSize(type) == size) { offset = JS_ROUNDUP(offset, size); properties[j].offset = offset; offset += size; } } } // The final offset is the amount of data needed by the object. return offset; } static bool SetLayoutTraceList(ExclusiveContext* cx, UnboxedLayout* layout) { // Figure out the offsets of any objects or string properties. Vector objectOffsets, stringOffsets; for (size_t i = 0; i < layout->properties().length(); i++) { const UnboxedLayout::Property& property = layout->properties()[i]; MOZ_ASSERT(property.offset != UINT32_MAX); if (property.type == JSVAL_TYPE_OBJECT) { if (!objectOffsets.append(property.offset)) return false; } else if (property.type == JSVAL_TYPE_STRING) { if (!stringOffsets.append(property.offset)) return false; } } // Construct the layout's trace list. if (!objectOffsets.empty() || !stringOffsets.empty()) { Vector entries; if (!entries.appendAll(stringOffsets) || !entries.append(-1) || !entries.appendAll(objectOffsets) || !entries.append(-1) || !entries.append(-1)) { return false; } int32_t* traceList = cx->zone()->pod_malloc(entries.length()); if (!traceList) return false; PodCopy(traceList, entries.begin(), entries.length()); layout->setTraceList(traceList); } return true; } static inline Value NextValue(Handle> values, size_t* valueCursor) { return values[(*valueCursor)++]; } static bool GetValuesFromPreliminaryArrayObject(ArrayObject* obj, MutableHandle> values) { if (!values.append(Int32Value(obj->length()))) return false; if (!values.append(Int32Value(obj->getDenseInitializedLength()))) return false; for (size_t i = 0; i < obj->getDenseInitializedLength(); i++) { if (!values.append(obj->getDenseElement(i))) return false; } return true; } void UnboxedArrayObject::fillAfterConvert(ExclusiveContext* cx, Handle> values, size_t* valueCursor) { MOZ_ASSERT(CapacityArray[1] == 0); setCapacityIndex(1); setInitializedLengthNoBarrier(0); setInlineElements(); setLength(cx, NextValue(values, valueCursor).toInt32()); int32_t initlen = NextValue(values, valueCursor).toInt32(); if (!initlen) return; AutoEnterOOMUnsafeRegion oomUnsafe; if (!growElements(cx, initlen)) oomUnsafe.crash("UnboxedArrayObject::fillAfterConvert"); setInitializedLength(initlen); for (size_t i = 0; i < size_t(initlen); i++) JS_ALWAYS_TRUE(initElement(cx, i, NextValue(values, valueCursor))); } static bool GetValuesFromPreliminaryPlainObject(PlainObject* obj, MutableHandle> values) { for (size_t i = 0; i < obj->slotSpan(); i++) { if (!values.append(obj->getSlot(i))) return false; } return true; } void UnboxedPlainObject::fillAfterConvert(ExclusiveContext* cx, Handle> values, size_t* valueCursor) { initExpando(); memset(data(), 0, layout().size()); for (size_t i = 0; i < layout().properties().length(); i++) JS_ALWAYS_TRUE(setValue(cx, layout().properties()[i], NextValue(values, valueCursor))); } bool js::TryConvertToUnboxedLayout(ExclusiveContext* cx, AutoEnterAnalysis& enter, Shape* templateShape, ObjectGroup* group, PreliminaryObjectArray* objects) { bool isArray = !templateShape; // Unboxed arrays are disabled for now. See bug 1153266. if (isArray) { return true; } else { if (jit::JitOptions.disableUnboxedObjects) return true; } MOZ_ASSERT_IF(templateShape, !templateShape->getObjectFlags()); if (group->runtimeFromAnyThread()->isSelfHostingGlobal(cx->global())) return true; if (!isArray && templateShape->slotSpan() == 0) return true; UnboxedLayout::PropertyVector properties; if (!isArray) { if (!properties.appendN(UnboxedLayout::Property(), templateShape->slotSpan())) return false; } JSValueType elementType = JSVAL_TYPE_MAGIC; size_t objectCount = 0; for (size_t i = 0; i < PreliminaryObjectArray::COUNT; i++) { JSObject* obj = objects->get(i); if (!obj) continue; if (obj->isSingleton() || obj->group() != group) return true; objectCount++; if (isArray) { if (!CombineArrayObjectElements(cx, &obj->as(), &elementType)) return true; } else { if (!CombinePlainObjectProperties(&obj->as(), templateShape, properties)) return true; } } size_t layoutSize = 0; if (isArray) { // Don't use an unboxed representation if we couldn't determine an // element type for the objects. if (UnboxedTypeSize(elementType) == 0) return true; } else { if (objectCount <= 1) { // If only one of the objects has been created, it is more likely // to have new properties added later. This heuristic is not used // for array objects, where we might want an unboxed representation // even if there is only one large array. return true; } for (size_t i = 0; i < templateShape->slotSpan(); i++) { // We can't use an unboxed representation if e.g. all the objects have // a null value for one of the properties, as we can't decide what type // it is supposed to have. if (UnboxedTypeSize(properties[i].type) == 0) return true; } // Make sure that all properties on the template shape are property // names, and not indexes. for (Shape::Range r(templateShape); !r.empty(); r.popFront()) { jsid id = r.front().propid(); uint32_t dummy; if (!JSID_IS_ATOM(id) || JSID_TO_ATOM(id)->isIndex(&dummy)) return true; } layoutSize = ComputePlainObjectLayout(cx, templateShape, properties); // The entire object must be allocatable inline. if (UnboxedPlainObject::offsetOfData() + layoutSize > JSObject::MAX_BYTE_SIZE) return true; } UniquePtr& layout = enter.unboxedLayoutToCleanUp; MOZ_ASSERT(!layout); layout = group->zone()->make_unique(); if (!layout) return false; if (isArray) { layout->initArray(elementType); } else { if (!layout->initProperties(properties, layoutSize)) return false; // The unboxedLayouts list only tracks layouts for plain objects. cx->compartment()->unboxedLayouts.insertFront(layout.get()); if (!SetLayoutTraceList(cx, layout.get())) return false; } // We've determined that all the preliminary objects can use the new layout // just constructed, so convert the existing group to use the unboxed class, // and update the preliminary objects to use the new layout. Do the // fallible stuff first before modifying any objects. // Get an empty shape which we can use for the preliminary objects. const Class* clasp = isArray ? &UnboxedArrayObject::class_ : &UnboxedPlainObject::class_; Shape* newShape = EmptyShape::getInitialShape(cx, clasp, group->proto(), 0); if (!newShape) { cx->recoverFromOutOfMemory(); return false; } // Accumulate a list of all the values in each preliminary object, and // update their shapes. Rooted> values(cx, GCVector(cx)); for (size_t i = 0; i < PreliminaryObjectArray::COUNT; i++) { JSObject* obj = objects->get(i); if (!obj) continue; bool ok; if (isArray) ok = GetValuesFromPreliminaryArrayObject(&obj->as(), &values); else ok = GetValuesFromPreliminaryPlainObject(&obj->as(), &values); if (!ok) { cx->recoverFromOutOfMemory(); return false; } } if (TypeNewScript* newScript = group->newScript()) layout->setNewScript(newScript); for (size_t i = 0; i < PreliminaryObjectArray::COUNT; i++) { if (JSObject* obj = objects->get(i)) obj->as().setLastPropertyMakeNonNative(newShape); } group->setClasp(clasp); group->setUnboxedLayout(layout.release()); size_t valueCursor = 0; for (size_t i = 0; i < PreliminaryObjectArray::COUNT; i++) { JSObject* obj = objects->get(i); if (!obj) continue; if (isArray) obj->as().fillAfterConvert(cx, values, &valueCursor); else obj->as().fillAfterConvert(cx, values, &valueCursor); } MOZ_ASSERT(valueCursor == values.length()); return true; } DefineBoxedOrUnboxedFunctor6(SetOrExtendBoxedOrUnboxedDenseElements, ExclusiveContext*, JSObject*, uint32_t, const Value*, uint32_t, ShouldUpdateTypes); DenseElementResult js::SetOrExtendAnyBoxedOrUnboxedDenseElements(ExclusiveContext* cx, JSObject* obj, uint32_t start, const Value* vp, uint32_t count, ShouldUpdateTypes updateTypes) { SetOrExtendBoxedOrUnboxedDenseElementsFunctor functor(cx, obj, start, vp, count, updateTypes); return CallBoxedOrUnboxedSpecialization(functor, obj); }; DefineBoxedOrUnboxedFunctor5(MoveBoxedOrUnboxedDenseElements, JSContext*, JSObject*, uint32_t, uint32_t, uint32_t); DenseElementResult js::MoveAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj, uint32_t dstStart, uint32_t srcStart, uint32_t length) { MoveBoxedOrUnboxedDenseElementsFunctor functor(cx, obj, dstStart, srcStart, length); return CallBoxedOrUnboxedSpecialization(functor, obj); } DefineBoxedOrUnboxedFunctorPair6(CopyBoxedOrUnboxedDenseElements, JSContext*, JSObject*, JSObject*, uint32_t, uint32_t, uint32_t); DenseElementResult js::CopyAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* dst, JSObject* src, uint32_t dstStart, uint32_t srcStart, uint32_t length) { CopyBoxedOrUnboxedDenseElementsFunctor functor(cx, dst, src, dstStart, srcStart, length); return CallBoxedOrUnboxedSpecialization(functor, dst, src); } DefineBoxedOrUnboxedFunctor3(SetBoxedOrUnboxedInitializedLength, JSContext*, JSObject*, size_t); void js::SetAnyBoxedOrUnboxedInitializedLength(JSContext* cx, JSObject* obj, size_t initlen) { SetBoxedOrUnboxedInitializedLengthFunctor functor(cx, obj, initlen); JS_ALWAYS_TRUE(CallBoxedOrUnboxedSpecialization(functor, obj) == DenseElementResult::Success); } DefineBoxedOrUnboxedFunctor3(EnsureBoxedOrUnboxedDenseElements, JSContext*, JSObject*, size_t); DenseElementResult js::EnsureAnyBoxedOrUnboxedDenseElements(JSContext* cx, JSObject* obj, size_t initlen) { EnsureBoxedOrUnboxedDenseElementsFunctor functor(cx, obj, initlen); return CallBoxedOrUnboxedSpecialization(functor, obj); }