diff --git a/js/src/jit-test/tests/basic/json-parse-object-edge-cases.js b/js/src/jit-test/tests/basic/json-parse-object-edge-cases.js new file mode 100644 index 000000000000..8353d8a877ae --- /dev/null +++ b/js/src/jit-test/tests/basic/json-parse-object-edge-cases.js @@ -0,0 +1,35 @@ +// Array includes objects with duplicate keys and integer keys. +let json = `[ + {"x1": 1}, + {"x2": 2}, + {"x3": 3}, + {"x1": 1, "y": 0}, + {"x2": 1, "y": 0}, + {"x3": 1, "y": 0}, + {"x1": 1, "x1": 2, "y": 0}, + {"x1": 1, "x1": 2, "y": 0}, + {"x1": 1, "x1": 2, "y": 0}, + {"0": 1, "x1": 1}, + {"0": 1, "0": 2, "x1": 1}, + {"0": 1, "0": 2, "x1": 1}, + {"__proto__": 1}, + {"__proto__": 2} +]`; +for (let i = 0; i < 3; i++) { + let res = JSON.parse(json); + assertEq(JSON.stringify(res), + `[{"x1":1},` + + `{"x2":2},` + + `{"x3":3},` + + `{"x1":1,"y":0},` + + `{"x2":1,"y":0},` + + `{"x3":1,"y":0},` + + `{"x1":2,"y":0},` + + `{"x1":2,"y":0},` + + `{"x1":2,"y":0},` + + `{"0":1,"x1":1},` + + `{"0":2,"x1":1},` + + `{"0":2,"x1":1},` + + `{"__proto__":1},` + + `{"__proto__":2}]`); +} diff --git a/js/src/vm/PlainObject.cpp b/js/src/vm/PlainObject.cpp index 4ba63cc3542d..15d36a523330 100644 --- a/js/src/vm/PlainObject.cpp +++ b/js/src/vm/PlainObject.cpp @@ -196,20 +196,80 @@ PlainObject* js::NewPlainObjectWithProtoAndAllocKind(JSContext* cx, return PlainObject::createWithShape(cx, shape, allocKind, newKind); } +void js::NewPlainObjectWithPropsCache::add(Shape* shape) { + MOZ_ASSERT(shape); + MOZ_ASSERT(shape->slotSpan() > 0); + for (size_t i = NumEntries - 1; i > 0; i--) { + entries_[i] = entries_[i - 1]; + } + entries_[0] = shape; +} + +static bool ShapeMatches(IdValuePair* properties, size_t nproperties, + Shape* shape) { + if (shape->slotSpan() != nproperties) { + return false; + } + ShapePropertyIter iter(shape); + for (size_t i = nproperties; i > 0; i--) { + MOZ_ASSERT(iter->isDataProperty()); + MOZ_ASSERT(iter->flags() == PropertyFlags::defaultDataPropFlags); + if (properties[i - 1].id != iter->key()) { + return false; + } + iter++; + } + MOZ_ASSERT(iter.done()); + return true; +} + +Shape* js::NewPlainObjectWithPropsCache::lookup(IdValuePair* properties, + size_t nproperties) const { + for (size_t i = 0; i < NumEntries; i++) { + Shape* shape = entries_[i]; + if (shape && ShapeMatches(properties, nproperties, shape)) { + return shape; + } + } + return nullptr; +} + enum class KeysKind { UniqueNames, Unknown }; template static PlainObject* NewPlainObjectWithProperties(JSContext* cx, IdValuePair* properties, size_t nproperties) { + auto& cache = cx->realm()->newPlainObjectWithPropsCache; + + // If we recently created an object with these properties, we can use that + // Shape directly. + if (Shape* shape = cache.lookup(properties, nproperties)) { + Rooted shapeRoot(cx, shape); + PlainObject* obj = PlainObject::createWithShape(cx, shapeRoot); + if (!obj) { + return nullptr; + } + MOZ_ASSERT(obj->slotSpan() == nproperties); + for (size_t i = 0; i < nproperties; i++) { + obj->initSlot(i, properties[i].value); + } + return obj; + } + gc::AllocKind allocKind = gc::GetGCObjectKind(nproperties); Rooted obj(cx, NewPlainObjectWithAllocKind(cx, allocKind)); if (!obj) { return nullptr; } + if (nproperties == 0) { + return obj; + } + Rooted key(cx); Rooted value(cx); + bool canCache = true; for (size_t i = 0; i < nproperties; i++) { key = properties[i].id; @@ -219,6 +279,7 @@ static PlainObject* NewPlainObjectWithProperties(JSContext* cx, // just fall back to NativeDefineDataProperty. if constexpr (Kind == KeysKind::Unknown) { if (MOZ_UNLIKELY(key.isInt())) { + canCache = false; if (!NativeDefineDataProperty(cx, obj, key, value, JSPROP_ENUMERATE)) { return nullptr; } @@ -235,6 +296,7 @@ static PlainObject* NewPlainObjectWithProperties(JSContext* cx, } else { mozilla::Maybe prop = obj->lookup(cx, key); if (MOZ_UNLIKELY(prop)) { + canCache = false; MOZ_ASSERT(prop->isDataProperty()); obj->setSlot(prop->slot(), value); continue; @@ -246,6 +308,12 @@ static PlainObject* NewPlainObjectWithProperties(JSContext* cx, } } + if (canCache && !obj->inDictionaryMode()) { + MOZ_ASSERT(!obj->isIndexed()); + MOZ_ASSERT(obj->slotSpan() == nproperties); + cache.add(obj->shape()); + } + return obj; } diff --git a/js/src/vm/Realm.cpp b/js/src/vm/Realm.cpp index e9b1d5c63e42..fbf6aa650d60 100644 --- a/js/src/vm/Realm.cpp +++ b/js/src/vm/Realm.cpp @@ -395,6 +395,7 @@ void Realm::fixupAfterMovingGC(JSTracer* trc) { void Realm::purge() { dtoaCache.purge(); newProxyCache.purge(); + newPlainObjectWithPropsCache.purge(); objects_.iteratorCache.clearAndCompact(); arraySpeciesLookup.purge(); promiseLookup.purge(); diff --git a/js/src/vm/Realm.h b/js/src/vm/Realm.h index 2559abd71f70..50c2e7e3830b 100644 --- a/js/src/vm/Realm.h +++ b/js/src/vm/Realm.h @@ -7,6 +7,7 @@ #ifndef vm_Realm_h #define vm_Realm_h +#include "mozilla/Array.h" #include "mozilla/Atomics.h" #include "mozilla/LinkedList.h" #include "mozilla/Maybe.h" @@ -127,6 +128,25 @@ class NewProxyCache { void purge() { entries_.reset(); } }; +// Cache for NewPlainObjectWithProperties. When the list of properties matches +// a recently created object's shape, we can use this shape directly. +class NewPlainObjectWithPropsCache { + static const size_t NumEntries = 4; + mozilla::Array entries_; + + public: + NewPlainObjectWithPropsCache() { purge(); } + + Shape* lookup(IdValuePair* properties, size_t nproperties) const; + void add(Shape* shape); + + void purge() { + for (size_t i = 0; i < NumEntries; i++) { + entries_[i] = nullptr; + } + } +}; + // [SMDOC] Object MetadataBuilder API // // We must ensure that all newly allocated JSObjects get their metadata @@ -404,6 +424,7 @@ class JS::Realm : public JS::shadow::Realm { js::DtoaCache dtoaCache; js::NewProxyCache newProxyCache; + js::NewPlainObjectWithPropsCache newPlainObjectWithPropsCache; js::ArraySpeciesLookup arraySpeciesLookup; js::PromiseLookup promiseLookup;