Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions js/public/ExternalCompilerHooks.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
namespace js {
class AbstractGeneratorObject;
class NativeObject;
class ObjectFuse;
class SharedShape;
class RegExpShared;
class RunState;
class VectorMatchPairs;
Expand Down Expand Up @@ -87,6 +89,17 @@ struct ExternalCompilerHooks {
void (*propertyAdded)(JSContext* cx, js::NativeObject* obj,
JS::PropertyKey id, uint32_t slot,
uint32_t numFixedSlots);
// Slot placement. Before the engine adds a property to a shared-shape
// object whose word is nonzero, it asks here where to put it: set *result
// to a shape from js::ExternalShapeWithPropertyAtSlot, made from the
// object's current shape with this key and flags (the raw PropertyFlags
// byte), to place the property in that shape's slot, or leave it null to
// let the engine append the property at the slot span. Return false only
// with an exception pending. Engine-side caches keyed by (shape, key) are
// not filled with such shapes; the tier memoizes its own.
bool (*shapeForAdd)(JSContext* cx, JS::Handle<js::NativeObject*> obj,
JS::HandleId id, uint8_t flags,
js::SharedShape** result);

// Global object. A property was defined or deleted on a global, a data
// property of a global was written, or a global lexical binding now
Expand All @@ -96,6 +109,15 @@ struct ExternalCompilerHooks {
uint64_t valueBits);
void (*globalLexicalShadowAdded)(JSContext* cx, uint64_t idBits);

// Object fuses (vm/ObjectFuse.h). Wherever the engine invalidates the Ion
// code depending on a constant property of an object fuse's object, it
// reports the fuse and the property's slot here, or UINT32_MAX for every
// property (a proto mutation or swap). A tier that marks properties
// constant (ObjectFuse::tryOptimizeConstantProperty) and relies on them
// drops what it assumed. Must not GC.
void (*objectFuseInvalidated)(JSContext* cx, js::ObjectFuse* fuse,
uint32_t propSlot);

// Script entry. Every script carries a pointer-sized external word, zero
// at birth; the engine consults these only for scripts whose word is
// nonzero.
Expand Down Expand Up @@ -153,6 +175,26 @@ extern JS_PUBLIC_API void ExternalPropertyAdded(JSContext* cx,
NativeObject* obj,
JS::PropertyKey id,
uint32_t slot);
extern JS_PUBLIC_API bool ExternalShapeForAdd(JSContext* cx,
JS::Handle<NativeObject*> obj,
JS::HandleId id, uint8_t flags,
SharedShape** result);

// Custom slot placement (SharedShape::getShapeWithPropertyAtSlot): the shape
// that adds `id` with raw PropertyFlags `flags` to `shape` in slot `slot`, or
// null in *result (no exception) if that placement is not possible. A slot
// other than the span gives the shape ObjectFlag::PermutedSlots, which turns
// off the engine's fast paths that assume slot order is insertion order.
extern JS_PUBLIC_API bool ExternalShapeWithPropertyAtSlot(
JSContext* cx, JS::Handle<SharedShape*> shape, JS::HandleId id,
uint8_t flags, uint32_t slot, SharedShape** result);

// Add the property `newShape` adds to `obj`, whose shape it was made from
// (see NativeObject::addPropertyWithShape). The slot, holding undefined, is
// returned in *slot for the caller to initialize.
extern JS_PUBLIC_API bool ExternalAddPropertyWithShape(
JSContext* cx, JS::Handle<NativeObject*> obj, SharedShape* newShape,
uint32_t* slot);

} // namespace js

Expand Down
8 changes: 7 additions & 1 deletion js/src/builtin/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,12 @@ void PlainObjectAssignCache::assertValid() const {
if (fromPlain->getDenseInitializedLength() > 0 || fromPlain->isIndexed()) {
return true;
}

// The fast path copies slot i to slot i and assumes property i is in slot
// i, which permuted slots do not.
if (fromPlain->shape()->hasPermutedSlots()) {
return true;
}
MOZ_ASSERT(!fromPlain->getClass()->getNewEnumerate());
MOZ_ASSERT(!fromPlain->getClass()->getEnumerate());

Expand Down Expand Up @@ -1030,7 +1036,7 @@ void PlainObjectAssignCache::assertValid() const {
// more complicated slot layout (the slot numbers may not match the property
// definition order and the slots may contain holes).
if (toWasEmpty && hasOnlyEnumerableProps && !fromPlain->inDictionaryMode() &&
!toPlain->inDictionaryMode()) {
!toPlain->inDictionaryMode() && !toPlain->shape()->hasPermutedSlots()) {
PlainObjectAssignCache& cache = cx->realm()->plainObjectAssignCache;
cache.fill(&origToShape->asShared(), fromPlain->sharedShape(),
toPlain->sharedShape());
Expand Down
84 changes: 84 additions & 0 deletions js/src/builtin/TestingFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8545,6 +8545,75 @@ static bool NumAllocSitesPretenured(JSContext* cx, unsigned argc, Value* vp) {
return true;
}

#ifdef JS_EXTERNAL_COMPILER_HOOKS
// addPropertyAtSlot(obj, key, slot): add a writable, enumerable, configurable
// data property `key` (value undefined) to plain object `obj` in slot `slot`
// (SharedShape::getShapeWithPropertyAtSlot). Returns whether it was placed.
static bool AddPropertyAtSlot(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 3 || !args[0].isObject() ||
!args[0].toObject().is<PlainObject>() || !args[2].isInt32() ||
args[2].toInt32() < 0) {
JS_ReportErrorASCII(cx,
"addPropertyAtSlot: expected (plain object, key, "
"slot)");
return false;
}
Rooted<NativeObject*> obj(cx, &args[0].toObject().as<NativeObject>());
RootedId id(cx);
if (!ToPropertyKey(cx, args[1], &id)) {
return false;
}
if (id.isInt() || obj->inDictionaryMode() || !obj->isExtensible() ||
obj->containsPure(id)) {
args.rval().setBoolean(false);
return true;
}
Rooted<SharedShape*> shape(cx, obj->sharedShape());
SharedShape* newShape = nullptr;
if (!SharedShape::getShapeWithPropertyAtSlot(
cx, shape, id, PropertyFlags::defaultDataPropFlags,
uint32_t(args[2].toInt32()), &newShape)) {
return false;
}
if (!newShape) {
args.rval().setBoolean(false);
return true;
}
uint32_t slot;
if (!NativeObject::addPropertyWithShape(cx, obj, newShape, &slot)) {
return false;
}
args.rval().setBoolean(true);
return true;
}

// hasPermutedSlots(obj): whether obj's shape has ObjectFlag::PermutedSlots.
static bool HasPermutedSlots(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1 || !args[0].isObject()) {
JS_ReportErrorASCII(cx, "hasPermutedSlots: expected an object");
return false;
}
args.rval().setBoolean(args[0].toObject().shape()->hasPermutedSlots());
return true;
}

// objectSlotSpan(obj): obj's slot span, or -1 for a non-native object.
static bool ObjectSlotSpan(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1 || !args[0].isObject()) {
JS_ReportErrorASCII(cx, "objectSlotSpan: expected an object");
return false;
}
JSObject* obj = &args[0].toObject();
args.rval().setInt32(obj->is<NativeObject>()
? int32_t(obj->as<NativeObject>().slotSpan())
: -1);
return true;
}
#endif

static bool GetLcovInfo(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);

Expand Down Expand Up @@ -10986,6 +11055,21 @@ JS_FOR_WASM_FEATURES(WASM_FEATURE)
" Return the number of allocation sites that were pretenured for the current\n"
" global\n"),

#ifdef JS_EXTERNAL_COMPILER_HOOKS
JS_FN_HELP("addPropertyAtSlot", AddPropertyAtSlot, 3, 0,
"addPropertyAtSlot(obj, key, slot)",
" Add data property key (value undefined) to plain object obj in the given\n"
" slot, not at the slot span. Returns whether it was placed.\n"),

JS_FN_HELP("hasPermutedSlots", HasPermutedSlots, 1, 0,
"hasPermutedSlots(obj)",
" Return whether obj's slots may not follow property insertion order.\n"),

JS_FN_HELP("objectSlotSpan", ObjectSlotSpan, 1, 0,
"objectSlotSpan(obj)",
" Return obj's slot span (-1 for a non-native object).\n"),
#endif

JS_FN_HELP("getLcovInfo", GetLcovInfo, 1, 0,
"getLcovInfo(global)",
" Generate LCOV tracefile for the given compartment. If no global are provided then\n"
Expand Down
5 changes: 5 additions & 0 deletions js/src/jit/CacheIR.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5609,6 +5609,11 @@ AttachDecision SetPropIRGenerator::tryAttachAddSlotStub(
return AttachDecision::NoAction;
}

// Add stubs store only the new property's slot, which is the old span.
if (holder->shape()->hasPermutedSlots()) {
return AttachDecision::NoAction;
}

SharedShape* oldSharedShape = &oldShape->asShared();

// The property must be the last added property of the object.
Expand Down
3 changes: 3 additions & 0 deletions js/src/jit/VMFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2345,7 +2345,10 @@ static bool TryAddOrSetPlainObjectProperty(JSContext* cx,
bool res = AddDataPropertyToPlainObject(cx, obj, keyRoot, value, &resultSlot);

if constexpr (UseCache) {
// A transition to permuted slots may skip slots (initialized by the add)
// that a cached replay would leave alone.
if (res && obj->shape()->isShared() &&
!obj->shape()->hasPermutedSlots() &&
resultSlot < SharedPropMap::MaxPropsForNonDictionary &&
!Watchtower::watchesPropertyAdd(obj)) {
TaggedSlotOffset offset = obj->getTaggedSlotOffset(resultSlot);
Expand Down
36 changes: 36 additions & 0 deletions js/src/vm/ExternalCompilerHooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "vm/JSObject.h"
#include "vm/NativeObject.h"
#include "vm/Runtime.h"
#include "vm/Shape.h"

using namespace js;

Expand Down Expand Up @@ -90,3 +91,38 @@ JS_PUBLIC_API void js::ExternalPropertyAdded(JSContext* cx, NativeObject* obj,
hooks->propertyAdded(cx, obj, id, slot, obj->numFixedSlots());
}
}

JS_PUBLIC_API bool js::ExternalShapeForAdd(JSContext* cx,
JS::Handle<NativeObject*> obj,
JS::HandleId id, uint8_t flags,
SharedShape** result) {
*result = nullptr;
JS::ExternalCompilerHooks* hooks = cx->externalCompilerHooks();
if (!hooks || !hooks->shapeForAdd) {
return true;
}
if (!hooks->shapeForAdd(cx, obj, id, flags, result)) {
return false;
}
if (SharedShape* shape = *result) {
// Cheap validation of the tier's answer; addPropertyWithShape checks
// that the shape extends the object's by one property.
PropertyInfoWithKey prop = shape->lastProperty();
MOZ_RELEASE_ASSERT(prop.key() == id.get());
MOZ_RELEASE_ASSERT(prop.flags().toRaw() == flags);
}
return true;
}

JS_PUBLIC_API bool js::ExternalShapeWithPropertyAtSlot(
JSContext* cx, JS::Handle<SharedShape*> shape, JS::HandleId id,
uint8_t flags, uint32_t slot, SharedShape** result) {
return SharedShape::getShapeWithPropertyAtSlot(
cx, shape, id, PropertyFlags::fromRaw(flags), slot, result);
}

JS_PUBLIC_API bool js::ExternalAddPropertyWithShape(
JSContext* cx, JS::Handle<NativeObject*> obj, SharedShape* newShape,
uint32_t* slot) {
return NativeObject::addPropertyWithShape(cx, obj, newShape, slot);
}
5 changes: 5 additions & 0 deletions js/src/vm/JSContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
return externalCompilerHooks_;
}
void* getExternalCompilerState() const { return externalCompilerState_; }
// For a tier's compiled code, which reaches its per-context state from
// the context it is passed.
static constexpr size_t offsetOfExternalCompilerState() {
return offsetof(JSContext, externalCompilerState_);
}
void setExternalCompilerState(void* state) {
externalCompilerState_ = state;
}
Expand Down
9 changes: 9 additions & 0 deletions js/src/vm/NativeObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,15 @@ class NativeObject : public JSObject {
return addProperty(cx, obj, id, flags, slotOut);
}

#ifdef JS_EXTERNAL_COMPILER_HOOKS
// Add the property that `newShape` (from
// SharedShape::getShapeWithPropertyAtSlot on the object's shape) adds, in
// that shape's slot, which is returned in *slot holding undefined. Slots
// the new span skips are initialized to undefined.
static bool addPropertyWithShape(JSContext* cx, Handle<NativeObject*> obj,
SharedShape* newShape, uint32_t* slot);
#endif

static bool addPropertyInReservedSlot(JSContext* cx,
Handle<NativeObject*> obj, HandleId id,
uint32_t slot, PropertyFlags flags);
Expand Down
10 changes: 10 additions & 0 deletions js/src/vm/ObjectFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ enum class ObjectFlag : uint32_t {
// If set, the object may have an accessor property where the getter or setter
// is a non-JSFunction callable object.
HasNonFunctionAccessor = 1 << 17,

// If set, the shape's slot numbers may not follow property insertion order:
// a property was placed in a slot of the embedder's choosing (see
// SharedShape::getShapeWithPropertyAtSlot), so the slot span is the highest
// slot plus one, not the last property's slot plus one, and slots below the
// span may be holes (undefined, owned by no property). Inherited by every
// shape derived from one that has it. Code that assumes slot i holds the
// i-th property, or that the last property holds the top slot, must check
// it. Only set with JS_EXTERNAL_COMPILER_HOOKS.
PermutedSlots = 1 << 18,
};

using ObjectFlags = EnumFlags<ObjectFlag>;
Expand Down
13 changes: 13 additions & 0 deletions js/src/vm/ObjectFuse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include "gc/Barrier.h"
#include "gc/StableCellHasher.h"
#include "jit/InvalidationScriptSet.h"
#include "js/ExternalCompilerHooks.h"
#include "js/SweepingAPI.h"
#include "vm/JSContext.h"
#include "vm/JSScript.h"
#include "vm/NativeObject.h"
#include "vm/PropertyInfo.h"
Expand Down Expand Up @@ -50,6 +52,15 @@ bool ObjectFuse::ensurePropertyStateLength(uint32_t length) {
return true;
}

void ObjectFuse::notifyExternalTier(JSContext* cx, uint32_t propSlot) {
#ifdef JS_EXTERNAL_COMPILER_HOOKS
if (JS::ExternalCompilerHooks* hooks = cx->externalCompilerHooks();
hooks && hooks->objectFuseInvalidated) {
hooks->objectFuseInvalidated(cx, this, propSlot);
}
#endif
}

bool ObjectFuse::addDependency(uint32_t propSlot,
const jit::IonScriptKey& ionScript) {
MOZ_ASSERT(getPropertyState(propSlot) == PropertyState::Constant);
Expand All @@ -66,6 +77,7 @@ bool ObjectFuse::addDependency(uint32_t propSlot,
void ObjectFuse::invalidateDependentIonScriptsForProperty(JSContext* cx,
PropertyInfo prop,
const char* reason) {
notifyExternalTier(cx, prop.slot());
if (auto p = dependencies_.lookup(prop.slot())) {
p->value().invalidateAndClear(cx, reason);
dependencies_.remove(p);
Expand All @@ -74,6 +86,7 @@ void ObjectFuse::invalidateDependentIonScriptsForProperty(JSContext* cx,

void ObjectFuse::invalidateAllDependentIonScripts(JSContext* cx,
const char* reason) {
notifyExternalTier(cx, UINT32_MAX);
for (auto r = dependencies_.all(); !r.empty(); r.popFront()) {
r.front().value().invalidateAndClear(cx, reason);
}
Expand Down
4 changes: 4 additions & 0 deletions js/src/vm/ObjectFuse.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class ObjectFuse {
PropertyInfo prop,
const char* reason);
void invalidateAllDependentIonScripts(JSContext* cx, const char* reason);
// Tell an external tier (JS::ExternalCompilerHooks::objectFuseInvalidated)
// what Ion's dependencies are told: the property in |propSlot|, or every
// property (UINT32_MAX), may no longer be assumed constant.
void notifyExternalTier(JSContext* cx, uint32_t propSlot);

static constexpr uint32_t propertyStateShift(uint32_t propSlot) {
return (propSlot % NumPropsPerWord) * NumBitsPerProp;
Expand Down
4 changes: 3 additions & 1 deletion js/src/vm/PlainObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ static PlainObject* NewPlainObjectWithProperties(
}
}

if (canCache && !obj->inDictionaryMode()) {
// The cache's users store property i in slot i.
if (canCache && !obj->inDictionaryMode() &&
!obj->shape()->hasPermutedSlots()) {
MOZ_ASSERT(obj->getDenseInitializedLength() == 0);
MOZ_ASSERT(obj->slotSpan() == properties.length());
cache.add(obj->sharedShape());
Expand Down
Loading
Loading