Bug 1828455 - Part 1: Use dynamic slots header to store unique IDs for native objects r=jandem

The patch adds a field to the dynamic slots header and uses it to store the
unique ID if present.  This allocates object slots with zero capacity when
setting a unqiue ID on an object that didn't previously have dynamic slots.

One issue is that we previously used the dynamic slot capacity to detect
whether dynamic slots were present (rather than using the shared empty slots).
The patch addresses this using a special value for the optional unique ID for
shared empty slots.

Tests pass and this reduces time on the weakmap benchmark from 66.1 ns to 50.6
ns, a reduction of 23.5%.

Differential Revision: https://phabricator.services.mozilla.com/D176066
This commit is contained in:
Jon Coppeard 2023-05-09 15:25:17 +00:00
parent e220a7d75e
commit ef90924587
14 changed files with 371 additions and 86 deletions

View file

@ -4,21 +4,32 @@
// Check byte counts produced by takeCensus.
//
// Note that tracking allocation sites adds unique IDs to objects which
// increases their size.
//
// Ported from js/src/jit-test/tests/debug/Memory-take Census-10.js
function run_test() {
const g = newGlobal();
const dbg = new Debugger(g);
const sizeOfAM = byteSize(allocationMarker());
// Allocate a single allocation marker, and check that we can find it.
dbg.memory.trackingAllocationSites = true;
g.eval("var hold = allocationMarker();");
let census = saveHeapSnapshotAndTakeCensus(dbg, {
breakdown: { by: "objectClass" },
dbg.memory.trackingAllocationSites = false;
let census = dbg.memory.takeCensus({
breakdown: { by: "objectClass", then: { by: "allocationStack" } },
});
equal(census.AllocationMarker.count, 1);
equal(census.AllocationMarker.bytes, sizeOfAM);
let markers = 0;
let count;
let sizeOfAM;
census.AllocationMarker.forEach((v, k) => {
count = v.count;
sizeOfAM = v.bytes;
markers++;
});
equal(markers, 1);
equal(count, 1);
g.hold = null;
g.eval(` // 1
@ -51,7 +62,7 @@ function run_test() {
case 6:
equal(v.count, 10);
equal(v.bytes, 10 * sizeOfAM);
equal(v.bytes >= 10 * sizeOfAM, true);
seen++;
break;

View file

@ -24,12 +24,25 @@ function doLiveAndOfflineCensus(g, dbg, opts) {
};
}
function getMarkerSize(g, dbg) {
dbg.memory.allocationSamplingProbability = 1;
dbg.memory.trackingAllocationSites = true;
g.eval("var hold = allocationMarker();");
dbg.memory.trackingAllocationSites = false;
const live = dbg.memory.takeCensus({
breakdown: { by: "objectClass", then: { by: "count" } },
});
g.hold = null;
equal(live.AllocationMarker.count, 1);
return live.AllocationMarker.bytes;
}
function run_test() {
const g = newGlobal();
const dbg = new Debugger(g);
g.eval("this.markers = []");
const markerSize = byteSize(allocationMarker());
const markerSize = getMarkerSize(g, dbg);
// First, test that we get the same counts and sizes as we allocate and retain
// more things.

View file

@ -600,11 +600,15 @@ size_t js::TenuringTracer::moveSlotsToTenured(NativeObject* dst,
Zone* zone = src->nurseryZone();
size_t count = src->numDynamicSlots();
uint64_t uid = src->maybeUniqueId();
size_t allocSize = ObjectSlots::allocSize(count);
if (!nursery().isInside(src->slots_)) {
ObjectSlots* srcHeader = src->getSlotsHeader();
if (!nursery().isInside(srcHeader)) {
AddCellMemory(dst, allocSize, MemoryUse::ObjectSlots);
nursery().removeMallocedBufferDuringMinorGC(src->getSlotsHeader());
nursery().removeMallocedBufferDuringMinorGC(srcHeader);
return 0;
}
@ -617,14 +621,16 @@ size_t js::TenuringTracer::moveSlotsToTenured(NativeObject* dst,
}
ObjectSlots* slotsHeader = new (allocation)
ObjectSlots(count, src->getSlotsHeader()->dictionarySlotSpan());
ObjectSlots(count, srcHeader->dictionarySlotSpan(), uid);
dst->slots_ = slotsHeader->slots();
}
AddCellMemory(dst, allocSize, MemoryUse::ObjectSlots);
PodCopy(dst->slots_, src->slots_, count);
nursery().setSlotsForwardingPointer(src->slots_, dst->slots_, count);
if (count) {
nursery().setSlotsForwardingPointer(src->slots_, dst->slots_, count);
}
return allocSize;
}

View file

@ -31,13 +31,28 @@ inline bool JS::Zone::maybeGetUniqueId(js::gc::Cell* cell, uint64_t* uidp) {
MOZ_ASSERT(js::CurrentThreadCanAccessZone(this) ||
js::CurrentThreadIsPerformingGC());
// Get an existing uid, if one has been set.
auto p = uniqueIds().readonlyThreadsafeLookup(cell);
if (p) {
*uidp = p->value();
if (cell->is<JSObject>()) {
JSObject* obj = cell->as<JSObject>();
if (obj->is<js::NativeObject>()) {
auto* nobj = &obj->as<js::NativeObject>();
if (!nobj->hasUniqueId()) {
return false;
}
*uidp = nobj->uniqueId();
return true;
}
}
return p.found();
// Get an existing uid, if one has been set.
auto p = uniqueIds().readonlyThreadsafeLookup(cell);
if (!p) {
return false;
}
*uidp = p->value();
return true;
}
inline bool JS::Zone::getOrCreateHashCode(js::gc::Cell* cell,
@ -56,6 +71,21 @@ inline bool JS::Zone::getOrCreateUniqueId(js::gc::Cell* cell, uint64_t* uidp) {
MOZ_ASSERT(js::CurrentThreadCanAccessZone(this) ||
js::CurrentThreadIsPerformingGC());
if (cell->is<JSObject>()) {
JSObject* obj = cell->as<JSObject>();
if (obj->is<js::NativeObject>()) {
auto* nobj = &obj->as<js::NativeObject>();
if (nobj->hasUniqueId()) {
*uidp = nobj->uniqueId();
return true;
}
*uidp = js::gc::NextCellUniqueId(runtimeFromAnyThread());
JSContext* cx = runtimeFromMainThread()->mainContextFromOwnThread();
return nobj->setUniqueId(cx, *uidp);
}
}
// Get an existing uid, if one has been set.
auto p = uniqueIds().lookupForAdd(cell);
if (p) {
@ -83,6 +113,29 @@ inline bool JS::Zone::getOrCreateUniqueId(js::gc::Cell* cell, uint64_t* uidp) {
return true;
}
inline bool JS::Zone::setOrUpdateUniqueId(JSContext* cx, js::gc::Cell* cell,
uint64_t uid) {
MOZ_ASSERT(js::CurrentThreadCanAccessZone(this));
if (cell->is<JSObject>()) {
JSObject* obj = cell->as<JSObject>();
if (obj->is<js::NativeObject>()) {
auto* nobj = &obj->as<js::NativeObject>();
return nobj->setOrUpdateUniqueId(cx, uid);
}
}
// If the cell was in the nursery, hopefully unlikely, then we need to
// tell the nursery about it so that it can sweep the uid if the thing
// does not get tenured.
if (IsInsideNursery(cell) &&
!runtimeFromMainThread()->gc.nursery().addedUniqueIdToCell(cell)) {
return false;
}
return uniqueIds().put(cell, uid);
}
inline js::HashNumber JS::Zone::getHashCodeInfallible(js::gc::Cell* cell) {
return UniqueIdToHash(getUniqueIdInfallible(cell));
}
@ -99,6 +152,14 @@ inline uint64_t JS::Zone::getUniqueIdInfallible(js::gc::Cell* cell) {
inline bool JS::Zone::hasUniqueId(js::gc::Cell* cell) {
MOZ_ASSERT(js::CurrentThreadCanAccessZone(this) ||
js::CurrentThreadIsPerformingGC());
if (cell->is<JSObject>()) {
JSObject* obj = cell->as<JSObject>();
if (obj->is<js::NativeObject>()) {
return obj->as<js::NativeObject>().hasUniqueId();
}
}
return uniqueIds().has(cell);
}
@ -113,6 +174,8 @@ inline void JS::Zone::transferUniqueId(js::gc::Cell* tgt, js::gc::Cell* src) {
inline void JS::Zone::removeUniqueId(js::gc::Cell* cell) {
MOZ_ASSERT(js::CurrentThreadCanAccessZone(this));
// The cell may no longer be in the hash table if it was swapped with a
// NativeObject.
uniqueIds().remove(cell);
}

View file

@ -571,6 +571,9 @@ class Zone : public js::ZoneAllocator, public js::gc::GraphNodeBase<JS::Zone> {
// Remove any unique id associated with this Cell.
void removeUniqueId(js::gc::Cell* cell);
// Used to restore unique ID after JSObject::swap.
bool setOrUpdateUniqueId(JSContext* cx, js::gc::Cell* cell, uint64_t uid);
bool keepPropMapTables() const { return keepPropMapTables_; }
void setKeepPropMapTables(bool b) { keepPropMapTables_ = b; }

View file

@ -1,15 +1,31 @@
// Check byte counts produced by takeCensus.
//
// Note that tracking allocation sites adds unique IDs to objects which
// increases their size.
let g = newGlobal({newCompartment: true});
let dbg = new Debugger(g);
let sizeOfAM = byteSize(allocationMarker());
dbg.memory.allocationSamplingProbability = 1;
dbg.memory.trackingAllocationSites = true;
// Allocate a single allocation marker, and check that we can find it.
g.eval('var hold = allocationMarker();');
let census = dbg.memory.takeCensus({ breakdown: { by: 'objectClass' } });
assertEq(census.AllocationMarker.count, 1);
assertEq(census.AllocationMarker.bytes, sizeOfAM);
let census = dbg.memory.takeCensus({
breakdown: { by: 'objectClass',
then: { by: 'allocationStack' }
}
});
let markers = 0;
let count;
let sizeOfAM;
census.AllocationMarker.forEach((v, k) => {
count = v.count;
sizeOfAM = v.bytes;
markers++;
});
assertEq(markers, 1);
assertEq(count, 1);
g.evaluate(`
var objs = [];
@ -21,9 +37,6 @@ g.evaluate(`
`,
{ fileName: 'J. Edgar Hoover', lineNumber: 2000 });
dbg.memory.allocationSamplingProbability = 1;
dbg.memory.trackingAllocationSites = true;
g.hold = null;
g.fnerd();
@ -46,7 +59,7 @@ census.AllocationMarker.forEach((v, k) => {
case 2005:
assertEq(v.count, 10);
assertEq(v.bytes, 10 * sizeOfAM);
assertEq(v.bytes >= 10 * sizeOfAM, true);
seen++;
break;

View file

@ -2519,8 +2519,8 @@ JitCode* JitRealm::generateRegExpMatcherStub(JSContext* cx) {
masm.bind(&matchResultJoin);
MOZ_ASSERT(nativeTemplateObj.numFixedSlots() == 0);
// Dynamic slot count is always one less than a power of 2.
MOZ_ASSERT(nativeTemplateObj.numDynamicSlots() == 3);
// Dynamic slot count is always two less than a power of 2.
MOZ_ASSERT(nativeTemplateObj.numDynamicSlots() == 6);
static_assert(RegExpRealm::MatchResultObjectIndexSlot == 0,
"First slot holds the 'index' property");
static_assert(RegExpRealm::MatchResultObjectInputSlot == 1,

View file

@ -341,6 +341,8 @@ void MacroAssembler::nurseryAllocateObject(Register result, Register temp,
store32(
Imm32(0),
Address(result, thingSize + ObjectSlots::offsetOfDictionarySlotSpan()));
store64(Imm64(ObjectSlots::NoUniqueIdInDynamicSlots),
Address(result, thingSize + ObjectSlots::offsetOfMaybeUniqueId()));
computeEffectiveAddress(
Address(result, thingSize + ObjectSlots::offsetOfSlots()), temp);
storePtr(temp, Address(result, NativeObject::offsetOfSlots()));

View file

@ -20,6 +20,7 @@
#include "jsapi-tests/tests.h"
#include "vm/PlainObject.h"
#include "gc/Zone-inl.h"
#include "vm/JSObject-inl.h"
using namespace js;
@ -38,6 +39,7 @@ struct ObjectConfig {
const JSClass* clasp;
bool isNative;
bool nurseryAllocated;
bool hasUniqueId;
union {
NativeConfig native;
ProxyConfig proxy;
@ -106,6 +108,15 @@ BEGIN_TEST(testObjectSwap) {
GetLocation(obj1), obj2.get(), GetLocation(obj2));
}
uint64_t uid1 = 0;
if (config1.hasUniqueId) {
uid1 = cx->zone()->getUniqueIdInfallible(obj1);
}
uint64_t uid2 = 0;
if (config2.hasUniqueId) {
uid2 = cx->zone()->getUniqueIdInfallible(obj2);
}
{
AutoEnterOOMUnsafeRegion oomUnsafe;
JSObject::swap(cx, obj1, obj2, oomUnsafe);
@ -113,6 +124,9 @@ BEGIN_TEST(testObjectSwap) {
CHECK(CheckObject(obj1, config2, id2));
CHECK(CheckObject(obj2, config1, id1));
CHECK(CheckUniqueIds(obj1, config1.hasUniqueId, uid1, obj2,
config2.hasUniqueId, uid2));
}
if (Verbose) {
@ -131,46 +145,47 @@ ObjectConfigVector CreateObjectConfigs() {
ObjectConfigVector configs;
ObjectConfig config;
config.isNative = true;
config.native = NativeConfig{0, false};
for (const JSClass& jsClass : TestDOMClasses) {
config.clasp = &jsClass;
for (bool nurseryAllocated : {false, true}) {
config.nurseryAllocated = nurseryAllocated;
for (uint32_t propCount : TestPropertyCounts) {
config.native.propCount = propCount;
for (bool hasUniqueId : {false, true}) {
config.hasUniqueId = hasUniqueId;
for (uint32_t elementCount : TestElementCounts) {
config.native.elementCount = elementCount;
config.isNative = true;
config.native = NativeConfig{0, false};
for (bool nurseryAllocated : {false, true}) {
config.nurseryAllocated = nurseryAllocated;
for (const JSClass& jsClass : TestDOMClasses) {
config.clasp = &jsClass;
for (bool inDictionaryMode : {false, true}) {
if (inDictionaryMode && propCount == 0) {
continue;
for (uint32_t propCount : TestPropertyCounts) {
config.native.propCount = propCount;
for (uint32_t elementCount : TestElementCounts) {
config.native.elementCount = elementCount;
for (bool inDictionaryMode : {false, true}) {
if (inDictionaryMode && propCount == 0) {
continue;
}
config.native.inDictionaryMode = inDictionaryMode;
MOZ_RELEASE_ASSERT(configs.append(config));
}
config.native.inDictionaryMode = inDictionaryMode;
MOZ_RELEASE_ASSERT(configs.append(config));
}
}
}
}
}
config.isNative = false;
config.proxy = ProxyConfig{false};
config.isNative = false;
config.proxy = ProxyConfig{false};
for (const JSClass& jsClass : TestProxyClasses) {
config.clasp = &jsClass;
for (const JSClass& jsClass : TestProxyClasses) {
config.clasp = &jsClass;
for (bool nurseryAllocated : {false, true}) {
config.nurseryAllocated = nurseryAllocated;
for (bool inlineValues : {true, false}) {
config.proxy.inlineValues = inlineValues;
MOZ_RELEASE_ASSERT(configs.append(config));
for (bool inlineValues : {true, false}) {
config.proxy.inlineValues = inlineValues;
MOZ_RELEASE_ASSERT(configs.append(config));
}
}
}
}
@ -187,7 +202,17 @@ uint32_t nextId = 0;
JSObject* CreateObject(const ObjectConfig& config, uint32_t* idOut) {
*idOut = nextId;
return config.isNative ? CreateNativeObject(config) : CreateProxy(config);
JSObject* obj =
config.isNative ? CreateNativeObject(config) : CreateProxy(config);
if (config.hasUniqueId) {
uint64_t unused;
if (!obj->zone()->getOrCreateUniqueId(obj, &unused)) {
return nullptr;
}
}
return obj;
}
JSObject* CreateNativeObject(const ObjectConfig& config) {
@ -364,6 +389,41 @@ bool CheckObject(HandleObject obj, const ObjectConfig& config, uint32_t id) {
return true;
}
bool CheckUniqueIds(HandleObject obj1, bool hasUniqueId1, uint64_t uid1,
HandleObject obj2, bool hasUniqueId2, uint64_t uid2) {
if (uid1 && uid2) {
MOZ_RELEASE_ASSERT(uid1 != uid2);
}
// Check unique IDs are NOT swapped.
CHECK(CheckUniqueId(obj1, hasUniqueId1, uid1));
CHECK(CheckUniqueId(obj2, hasUniqueId2, uid2));
// Check unique IDs are different if present.
Zone* zone = cx->zone();
if (zone->hasUniqueId(obj1) && zone->hasUniqueId(obj2)) {
CHECK(zone->getUniqueIdInfallible(obj1) !=
zone->getUniqueIdInfallible(obj2));
}
return true;
}
bool CheckUniqueId(HandleObject obj, bool hasUniqueId, uint64_t uid) {
Zone* zone = obj->zone();
if (hasUniqueId) {
CHECK(zone->hasUniqueId(obj));
CHECK(zone->getUniqueIdInfallible(obj) == uid);
} else {
// Swap may add a unique ID to an object.
}
if (obj->is<NativeObject>()) {
CHECK(!zone->uniqueIds().has(obj));
}
return true;
}
uint32_t GetPropertyCount(NativeObject* obj) {
uint32_t count = 0;
for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) {

View file

@ -43,7 +43,7 @@ static inline gc::AllocKind NewObjectGCKind() { return gc::AllocKind::OBJECT4; }
MOZ_ALWAYS_INLINE uint32_t js::NativeObject::numDynamicSlots() const {
uint32_t slots = getSlotsHeader()->capacity();
MOZ_ASSERT(slots == calculateDynamicSlots());
MOZ_ASSERT_IF(hasDynamicSlots(), slots != 0);
MOZ_ASSERT_IF(hasDynamicSlots() && !hasUniqueId(), slots != 0);
return slots;
}

View file

@ -67,6 +67,7 @@
#endif
#include "wasm/WasmGcObject.h"
#include "gc/Zone-inl.h"
#include "vm/BooleanObject-inl.h"
#include "vm/EnvironmentObject-inl.h"
#include "vm/Interpreter-inl.h"
@ -1080,6 +1081,7 @@ bool NativeObject::fixupAfterSwap(JSContext* cx, Handle<NativeObject*> obj,
uint32_t oldDictionarySlotSpan =
obj->inDictionaryMode() ? slotValues.length() : 0;
MOZ_ASSERT(!obj->hasUniqueId());
size_t ndynamic =
calculateDynamicSlots(nfixed, slotValues.length(), obj->getClass());
size_t currentSlots = obj->getSlotsHeader()->capacity();
@ -1254,10 +1256,10 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b,
unsigned r = NotifyGCPreSwap(a, b);
bool aIsProxyWithInlineValues =
a->is<ProxyObject>() && a->as<ProxyObject>().usingInlineValueArray();
bool bIsProxyWithInlineValues =
b->is<ProxyObject>() && b->as<ProxyObject>().usingInlineValueArray();
ProxyObject* pa = a->is<ProxyObject>() ? &a->as<ProxyObject>() : nullptr;
ProxyObject* pb = b->is<ProxyObject>() ? &b->as<ProxyObject>() : nullptr;
bool aIsProxyWithInlineValues = pa && pa->usingInlineValueArray();
bool bIsProxyWithInlineValues = pb && pb->usingInlineValueArray();
bool aIsUsedAsPrototype = a->isUsedAsPrototype();
bool bIsUsedAsPrototype = b->isUsedAsPrototype();
@ -1265,6 +1267,35 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b,
// Swap element associations.
Zone* zone = a->zone();
// Record any associated unique IDs and prepare for swap.
//
// Note that unique IDs are NOT swapped but remain associated with the
// original address.
uint64_t aid = 0;
uint64_t bid = 0;
(void)zone->maybeGetUniqueId(a, &aid);
(void)zone->maybeGetUniqueId(b, &bid);
NativeObject* na = a->is<NativeObject>() ? &a->as<NativeObject>() : nullptr;
NativeObject* nb = b->is<NativeObject>() ? &b->as<NativeObject>() : nullptr;
if ((aid || bid) && (na || nb)) {
// We can't remove unique IDs from native objects when they are swapped with
// objects without an ID. Instead ensure they both have IDs so we always
// have something to overwrite the old ID with.
if (!zone->getOrCreateUniqueId(a, &aid) ||
!zone->getOrCreateUniqueId(b, &bid)) {
oomUnsafe.crash("Failed to create unique ID during swap");
}
// IDs stored in NativeObjects could shadow those stored in the zone
// table. Remove any zone table IDs first.
if (pa && aid) {
zone->removeUniqueId(a);
}
if (pb && bid) {
zone->removeUniqueId(b);
}
}
gc::AllocKind ka = SwappableObjectAllocKind(a);
gc::AllocKind kb = SwappableObjectAllocKind(b);
@ -1305,8 +1336,6 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b,
// objects.
RootedValueVector avals(cx);
RootedValueVector bvals(cx);
NativeObject* na = a->is<NativeObject>() ? &a->as<NativeObject>() : nullptr;
NativeObject* nb = b->is<NativeObject>() ? &b->as<NativeObject>() : nullptr;
if (na && !na->prepareForSwap(cx, &avals)) {
oomUnsafe.crash("NativeObject::prepareForSwap");
}
@ -1315,8 +1344,6 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b,
}
// Do the same for proxy value arrays.
ProxyObject* pa = a->is<ProxyObject>() ? &a->as<ProxyObject>() : nullptr;
ProxyObject* pb = b->is<ProxyObject>() ? &b->as<ProxyObject>() : nullptr;
if (pa && !pa->prepareForSwap(cx, &avals)) {
oomUnsafe.crash("ProxyObject::prepareForSwap");
}
@ -1360,6 +1387,16 @@ void JSObject::swap(JSContext* cx, HandleObject a, HandleObject b,
}
}
// Restore original unique IDs.
if ((aid || bid) && (na || nb)) {
if ((aid && !zone->setOrUpdateUniqueId(cx, a, aid)) ||
(bid && !zone->setOrUpdateUniqueId(cx, b, bid))) {
oomUnsafe.crash("Failed to set unique ID after swap");
}
}
MOZ_ASSERT_IF(aid, zone->getUniqueIdInfallible(a) == aid);
MOZ_ASSERT_IF(bid, zone->getUniqueIdInfallible(b) == bid);
/*
* We need a write barrier here. If |a| was marked and |b| was not, then
* after the swap, |b|'s guts would never be marked. The write barrier

View file

@ -40,6 +40,16 @@ extern bool js::IsExtendedPrimitive(const JSObject& obj);
namespace js {
constexpr ObjectSlots::ObjectSlots(uint32_t capacity,
uint32_t dictionarySlotSpan,
uint64_t maybeUniqueId)
: capacity_(capacity),
dictionarySlotSpan_(dictionarySlotSpan),
maybeUniqueId_(maybeUniqueId) {
MOZ_ASSERT(this->capacity() == capacity);
MOZ_ASSERT(this->dictionarySlotSpan() == dictionarySlotSpan);
}
inline uint32_t NativeObject::numFixedSlotsMaybeForwarded() const {
return gc::MaybeForwarded(JSObject::shape())->asNative().numFixedSlots();
}
@ -515,9 +525,13 @@ MOZ_ALWAYS_INLINE void NativeObject::setEmptyDynamicSlots(
uint32_t dictionarySlotSpan) {
MOZ_ASSERT_IF(!inDictionaryMode(), dictionarySlotSpan == 0);
MOZ_ASSERT(dictionarySlotSpan <= MAX_FIXED_SLOTS);
slots_ = emptyObjectSlotsForDictionaryObject[dictionarySlotSpan];
MOZ_ASSERT(getSlotsHeader()->capacity() == 0);
MOZ_ASSERT(getSlotsHeader()->dictionarySlotSpan() == dictionarySlotSpan);
MOZ_ASSERT(!hasDynamicSlots());
MOZ_ASSERT(!hasUniqueId());
}
MOZ_ALWAYS_INLINE bool NativeObject::setShapeAndAddNewSlots(

View file

@ -69,7 +69,7 @@ HeapSlot* const js::emptyObjectElementsShared = reinterpret_cast<HeapSlot*>(
struct EmptyObjectSlots : public ObjectSlots {
explicit constexpr EmptyObjectSlots(size_t dictionarySlotSpan)
: ObjectSlots(0, dictionarySlotSpan) {}
: ObjectSlots(0, dictionarySlotSpan, NoUniqueIdInSharedEmptySlots) {}
};
static constexpr EmptyObjectSlots emptyObjectSlotsHeaders[17] = {
@ -229,10 +229,26 @@ mozilla::Maybe<PropertyInfo> js::NativeObject::lookupPure(jsid id) {
return mozilla::Nothing();
}
bool NativeObject::setUniqueId(JSContext* cx, uint64_t uid) {
MOZ_ASSERT(!hasUniqueId());
MOZ_ASSERT(!zone()->hasUniqueId(this));
return setOrUpdateUniqueId(cx, uid);
}
bool NativeObject::setOrUpdateUniqueId(JSContext* cx, uint64_t uid) {
if (!hasDynamicSlots() && !allocateSlots(cx, 0)) {
return false;
}
getSlotsHeader()->setUniqueId(uid);
return true;
}
bool NativeObject::growSlots(JSContext* cx, uint32_t oldCapacity,
uint32_t newCapacity) {
MOZ_ASSERT(newCapacity > oldCapacity);
MOZ_ASSERT_IF(!is<ArrayObject>(), newCapacity >= SLOT_CAPACITY_MIN);
/*
* Slot capacities are determined by the span of allocated objects. Due to
@ -246,6 +262,8 @@ bool NativeObject::growSlots(JSContext* cx, uint32_t oldCapacity,
return allocateSlots(cx, newCapacity);
}
uint64_t uid = maybeUniqueId();
uint32_t newAllocated = ObjectSlots::allocCount(newCapacity);
uint32_t dictionarySpan = getSlotsHeader()->dictionarySlotSpan();
@ -263,7 +281,7 @@ bool NativeObject::growSlots(JSContext* cx, uint32_t oldCapacity,
}
auto* newHeaderSlots =
new (allocation) ObjectSlots(newCapacity, dictionarySpan);
new (allocation) ObjectSlots(newCapacity, dictionarySpan, uid);
slots_ = newHeaderSlots->slots();
Debug_SetSlotRangeToCrashOnTouch(slots_ + oldCapacity,
@ -296,14 +314,15 @@ bool NativeObject::allocateInitialSlots(JSContext* cx, uint32_t capacity) {
uint32_t count = ObjectSlots::allocCount(capacity);
HeapSlot* allocation = AllocateObjectBuffer<HeapSlot>(cx, this, count);
if (!allocation) {
// The new object will be unreachable, but we still have to make it safe for
// finalization. Also we must check for it during GC compartment checks (see
// IsPartiallyInitializedObject).
// The new object will be unreachable, but we still have to make it safe
// for finalization. Also we must check for it during GC compartment
// checks (see IsPartiallyInitializedObject).
initEmptyDynamicSlots();
return false;
}
auto* headerSlots = new (allocation) ObjectSlots(capacity, 0);
auto* headerSlots = new (allocation)
ObjectSlots(capacity, 0, ObjectSlots::NoUniqueIdInDynamicSlots);
slots_ = headerSlots->slots();
Debug_SetSlotRangeToCrashOnTouch(slots_, capacity);
@ -318,6 +337,7 @@ bool NativeObject::allocateInitialSlots(JSContext* cx, uint32_t capacity) {
}
bool NativeObject::allocateSlots(JSContext* cx, uint32_t newCapacity) {
MOZ_ASSERT(!hasUniqueId());
MOZ_ASSERT(!hasDynamicSlots());
uint32_t newAllocated = ObjectSlots::allocCount(newCapacity);
@ -329,8 +349,8 @@ bool NativeObject::allocateSlots(JSContext* cx, uint32_t newCapacity) {
return false;
}
auto* newHeaderSlots =
new (allocation) ObjectSlots(newCapacity, dictionarySpan);
auto* newHeaderSlots = new (allocation) ObjectSlots(
newCapacity, dictionarySpan, ObjectSlots::NoUniqueIdInDynamicSlots);
slots_ = newHeaderSlots->slots();
Debug_SetSlotRangeToCrashOnTouch(slots_, newCapacity);
@ -396,15 +416,18 @@ static inline void FreeSlots(JSContext* cx, NativeObject* obj,
void NativeObject::shrinkSlots(JSContext* cx, uint32_t oldCapacity,
uint32_t newCapacity) {
MOZ_ASSERT(hasDynamicSlots());
MOZ_ASSERT(newCapacity < oldCapacity);
MOZ_ASSERT(oldCapacity == getSlotsHeader()->capacity());
ObjectSlots* oldHeaderSlots = ObjectSlots::fromSlots(slots_);
MOZ_ASSERT(oldHeaderSlots->capacity() == oldCapacity);
uint64_t uid = maybeUniqueId();
uint32_t oldAllocated = ObjectSlots::allocCount(oldCapacity);
if (newCapacity == 0) {
if (newCapacity == 0 && uid == 0) {
size_t nbytes = ObjectSlots::allocSize(oldCapacity);
RemoveCellMemory(this, nbytes, MemoryUse::ObjectSlots);
FreeSlots(cx, this, oldHeaderSlots, nbytes);
@ -413,7 +436,8 @@ void NativeObject::shrinkSlots(JSContext* cx, uint32_t oldCapacity,
return;
}
MOZ_ASSERT_IF(!is<ArrayObject>(), newCapacity >= SLOT_CAPACITY_MIN);
MOZ_ASSERT_IF(!is<ArrayObject>() && !hasUniqueId(),
newCapacity >= SLOT_CAPACITY_MIN);
uint32_t dictionarySpan = getSlotsHeader()->dictionarySlotSpan();
@ -437,7 +461,7 @@ void NativeObject::shrinkSlots(JSContext* cx, uint32_t oldCapacity,
MemoryUse::ObjectSlots);
auto* newHeaderSlots =
new (allocation) ObjectSlots(newCapacity, dictionarySpan);
new (allocation) ObjectSlots(newCapacity, dictionarySpan, uid);
slots_ = newHeaderSlots->slots();
}

View file

@ -445,9 +445,15 @@ static_assert(ObjectElements::VALUES_PER_HEADER * sizeof(HeapSlot) ==
class alignas(HeapSlot) ObjectSlots {
uint32_t capacity_;
uint32_t dictionarySlotSpan_;
uint64_t maybeUniqueId_;
public:
static constexpr size_t VALUES_PER_HEADER = 1;
// Special values for maybeUniqueId_ to indicate no unique ID is present.
static constexpr uint64_t NoUniqueIdInDynamicSlots = 0;
static constexpr uint64_t NoUniqueIdInSharedEmptySlots = 1;
static constexpr uint64_t LastNoUniqueIdValue = NoUniqueIdInSharedEmptySlots;
static constexpr size_t VALUES_PER_HEADER = 2;
static inline size_t allocCount(size_t slotCount) {
static_assert(sizeof(ObjectSlots) ==
@ -471,16 +477,35 @@ class alignas(HeapSlot) ObjectSlots {
static constexpr size_t offsetOfDictionarySlotSpan() {
return offsetof(ObjectSlots, dictionarySlotSpan_);
}
static constexpr size_t offsetOfMaybeUniqueId() {
return offsetof(ObjectSlots, maybeUniqueId_);
}
static constexpr size_t offsetOfSlots() { return sizeof(ObjectSlots); }
static constexpr int32_t offsetOfDictionarySlotSpanFromSlots() {
return int32_t(offsetOfDictionarySlotSpan()) - int32_t(offsetOfSlots());
constexpr explicit ObjectSlots(uint32_t capacity, uint32_t dictionarySlotSpan,
uint64_t maybeUniqueId);
constexpr uint32_t capacity() const { return capacity_; }
constexpr uint32_t dictionarySlotSpan() const { return dictionarySlotSpan_; }
bool isSharedEmptySlots() const {
return maybeUniqueId_ == NoUniqueIdInSharedEmptySlots;
}
constexpr explicit ObjectSlots(uint32_t capacity, uint32_t dictionarySlotSpan)
: capacity_(capacity), dictionarySlotSpan_(dictionarySlotSpan) {}
uint32_t capacity() const { return capacity_; }
uint32_t dictionarySlotSpan() const { return dictionarySlotSpan_; }
constexpr bool hasUniqueId() const {
return maybeUniqueId_ > LastNoUniqueIdValue;
}
uint64_t uniqueId() const {
MOZ_ASSERT(hasUniqueId());
return maybeUniqueId_;
}
uintptr_t maybeUniqueId() const { return hasUniqueId() ? maybeUniqueId_ : 0; }
void setUniqueId(uint64_t uid) {
MOZ_ASSERT(uid > LastNoUniqueIdValue);
MOZ_ASSERT(!isSharedEmptySlots());
maybeUniqueId_ = uid;
}
void setDictionarySlotSpan(uint32_t span) { dictionarySlotSpan_ = span; }
@ -942,7 +967,13 @@ class NativeObject : public JSObject {
*/
static bool addDenseElementPure(JSContext* cx, NativeObject* obj);
bool hasDynamicSlots() const { return getSlotsHeader()->capacity(); }
/*
* Indicates whether this object has an ObjectSlots allocation attached. The
* capacity of this can be zero if it is only used to hold a unique ID.
*/
bool hasDynamicSlots() const {
return !getSlotsHeader()->isSharedEmptySlots();
}
/* Compute the number of dynamic slots required for this object. */
MOZ_ALWAYS_INLINE uint32_t calculateDynamicSlots() const;
@ -1188,6 +1219,14 @@ class NativeObject : public JSObject {
return UndefinedValue();
}
[[nodiscard]] bool setUniqueId(JSContext* cx, uint64_t uid);
inline bool hasUniqueId() const { return getSlotsHeader()->hasUniqueId(); }
inline uint64_t uniqueId() const { return getSlotsHeader()->uniqueId(); }
inline uint64_t maybeUniqueId() const {
return getSlotsHeader()->maybeUniqueId();
}
bool setOrUpdateUniqueId(JSContext* cx, uint64_t uid);
// MAX_FIXED_SLOTS is the biggest number of fixed slots our GC
// size classes will give an object.
static constexpr uint32_t MAX_FIXED_SLOTS =