Bug 1448838: Add native version for CopyDataProperties. r=jandem
authorAndré Bargull <andre.bargull@gmail.com>
Tue, 27 Mar 2018 13:56:20 -0700
changeset 410472 23c2abe70e6c5da9d8879193ee29c61161ee6fc3
parent 410471 d1fc08acf933155183545f1b2bb77271563c7ff9
child 410473 5d0273aecd2995e0033227b660fc87937bd77b34
push id33729
push userrgurzau@mozilla.com
push dateWed, 28 Mar 2018 21:55:49 +0000
treeherdermozilla-central@6aa3b57955fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem
bugs1448838
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1448838: Add native version for CopyDataProperties. r=jandem
js/src/builtin/Utilities.js
js/src/jit-test/tests/basic/object-assign-unboxed.js
js/src/jit-test/tests/basic/object-rest-unboxed.js
js/src/jit-test/tests/basic/object-rest.js
js/src/jit-test/tests/basic/object-spread-unboxed.js
js/src/jit-test/tests/basic/object-spread.js
js/src/vm/NativeObject.cpp
js/src/vm/NativeObject.h
js/src/vm/SelfHosting.cpp
--- a/js/src/builtin/Utilities.js
+++ b/js/src/builtin/Utilities.js
@@ -214,77 +214,88 @@ function GetInternalError(msg) {
         return e;
     }
     assert(false, "the catch block should've returned from this function.");
 }
 
 // To be used when a function is required but calling it shouldn't do anything.
 function NullFunction() {}
 
-// Object Rest/Spread Properties proposal
-// Abstract operation: CopyDataProperties (target, source, excluded)
-function CopyDataProperties(target, source, excluded) {
+// ES2019 draft rev 4c2df13f4194057f09b920ee88712e5a70b1a556
+// 7.3.23 CopyDataProperties (target, source, excludedItems)
+function CopyDataProperties(target, source, excludedItems) {
     // Step 1.
     assert(IsObject(target), "target is an object");
 
     // Step 2.
-    assert(IsObject(excluded), "excluded is an object");
+    assert(IsObject(excludedItems), "excludedItems is an object");
 
-    // Steps 3, 6.
+    // Steps 3 and 7.
     if (source === undefined || source === null)
         return;
 
-    // Step 4.a.
-    source = ToObject(source);
-
-    // Step 4.b.
-    var keys = OwnPropertyKeys(source);
+    // Step 4.
+    var from = ToObject(source);
 
     // Step 5.
+    var keys = CopyDataPropertiesOrGetOwnKeys(target, from, excludedItems);
+
+    // Return if we copied all properties in native code.
+    if (keys === null)
+        return;
+
+    // Step 6.
     for (var index = 0; index < keys.length; index++) {
         var key = keys[index];
 
         // We abbreviate this by calling propertyIsEnumerable which is faster
         // and returns false for not defined properties.
-        if (!hasOwn(key, excluded) && callFunction(std_Object_propertyIsEnumerable, source, key))
-            _DefineDataProperty(target, key, source[key]);
+        if (!hasOwn(key, excludedItems) &&
+            callFunction(std_Object_propertyIsEnumerable, from, key))
+        {
+            _DefineDataProperty(target, key, from[key]);
+        }
     }
 
-    // Step 6 (Return).
+    // Step 7 (Return).
 }
 
-// Object Rest/Spread Properties proposal
-// Abstract operation: CopyDataProperties (target, source, excluded)
+// ES2019 draft rev 4c2df13f4194057f09b920ee88712e5a70b1a556
+// 7.3.23 CopyDataProperties (target, source, excludedItems)
 function CopyDataPropertiesUnfiltered(target, source) {
     // Step 1.
     assert(IsObject(target), "target is an object");
 
     // Step 2 (Not applicable).
 
-    // Steps 3, 6.
+    // Steps 3 and 7.
     if (source === undefined || source === null)
         return;
 
-    // Step 4.a.
-    source = ToObject(source);
-
-    // Step 4.b.
-    var keys = OwnPropertyKeys(source);
+    // Step 4.
+    var from = ToObject(source);
 
     // Step 5.
+    var keys = CopyDataPropertiesOrGetOwnKeys(target, from, null);
+
+    // Return if we copied all properties in native code.
+    if (keys === null)
+        return;
+
+    // Step 6.
     for (var index = 0; index < keys.length; index++) {
         var key = keys[index];
 
         // We abbreviate this by calling propertyIsEnumerable which is faster
         // and returns false for not defined properties.
-        if (callFunction(std_Object_propertyIsEnumerable, source, key))
-            _DefineDataProperty(target, key, source[key]);
+        if (callFunction(std_Object_propertyIsEnumerable, from, key))
+            _DefineDataProperty(target, key, from[key]);
     }
 
-    // Step 6 (Return).
+    // Step 7 (Return).
 }
 
 /*************************************** Testing functions ***************************************/
 function outer() {
     return function inner() {
         return "foo";
     };
 }
--- a/js/src/jit-test/tests/basic/object-assign-unboxed.js
+++ b/js/src/jit-test/tests/basic/object-assign-unboxed.js
@@ -5,17 +5,18 @@ function Unboxed() {
     this.b = true;
 }
 
 function tryCreateUnboxedObject() {
     var obj;
     for (var i = 0; i < 1000; ++i) {
         obj = new Unboxed();
     }
-
+    if (unboxedObjectsEnabled())
+        assertEq(isUnboxedObject(obj), true);
     return obj;
 }
 
 function basic() {
     var unboxed = tryCreateUnboxedObject();
 
     var target = {};
     Object.assign(target, unboxed);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/object-rest-unboxed.js
@@ -0,0 +1,44 @@
+load(libdir + "asserts.js");
+
+function Unboxed() {
+    this.a = 0;
+    this.b = true;
+}
+
+function tryCreateUnboxedObject() {
+    var obj;
+    for (var i = 0; i < 1000; ++i) {
+        obj = new Unboxed();
+    }
+    if (unboxedObjectsEnabled())
+        assertEq(isUnboxedObject(obj), true);
+    return obj;
+}
+
+function basic() {
+    var unboxed = tryCreateUnboxedObject();
+
+    var {...target} = unboxed;
+    assertDeepEq(target, {a: 0, b: true});
+
+    var {a, c, ...target} = unboxed;
+    assertDeepEq(a, 0);
+    assertDeepEq(c, undefined);
+    assertDeepEq(target, {b: true});
+}
+
+function expando() {
+    var unboxed = tryCreateUnboxedObject();
+    unboxed.c = 3.5;
+
+    var {...target} = unboxed;
+    assertDeepEq(target, {a: 0, b: true, c: 3.5});
+
+    var {a, d, ...target} = unboxed;
+    assertDeepEq(a, 0);
+    assertDeepEq(d, undefined);
+    assertDeepEq(target, {b: true, c: 3.5});
+}
+
+basic();
+expando();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/object-rest.js
@@ -0,0 +1,90 @@
+function test() {
+    var from, to;
+
+    // From values.
+    from = {x: 1, y: 2};
+    ({...to} = from);
+    assertEq(to.y, 2);
+
+    var z;
+    from = {x: 1, y: 2};
+    ({x: z, ...to} = from);
+    assertEq(z, 1);
+    assertEq(to.y, 2);
+
+    // From getter.
+    var c = 7;
+    from = {x: 1, get y() { return ++c; }};
+    ({...to} = from);
+    assertEq(c, 8);
+    assertEq(to.y, 8);
+
+    from = {x: 1, get y() { return ++c; }};
+    ({y: z, ...to} = from);
+    assertEq(c, 9);
+    assertEq(z, 9);
+    assertEq(to.y, undefined);
+
+    // Array with dense elements.
+    from = [1, 2, 3];
+    ({...to} = from);
+    assertEq(to[2], 3);
+    assertEq("length" in to, false);
+
+    from = [1, 2, 3];
+    ({2: z, ...to} = from);
+    assertEq(z, 3);
+    assertEq(to[2], undefined);
+    assertEq(to[0], 1);
+    assertEq("length" in to, false);
+
+    // Object with sparse elements and symbols.
+    from = {x: 1, 1234567: 2, 1234560: 3, [Symbol.iterator]: 5, z: 3};
+    ({...to} = from);
+    assertEq(to[1234567], 2);
+    assertEq(Object.keys(to).toString(), "1234560,1234567,x,z");
+    assertEq(to[Symbol.iterator], 5);
+
+    from = {x: 1, 1234567: 2, 1234560: 3, [Symbol.iterator]: 5, z: 3};
+    ({[Symbol.iterator]: z, ...to} = from);
+    assertEq(to[1234567], 2);
+    assertEq(Object.keys(to).toString(), "1234560,1234567,x,z");
+    assertEq(to[Symbol.iterator], undefined);
+    assertEq(z, 5);
+
+    // Typed array.
+    from = new Int32Array([1, 2, 3]);
+    ({...to} = from);
+    assertEq(to[1], 2);
+
+    from = new Int32Array([1, 2, 3]);
+    ({1: z, ...to} = from);
+    assertEq(z, 2);
+    assertEq(to[1], undefined);
+    assertEq(to[2], 3);
+
+    // Primitive string.
+    from = "foo";
+    ({...to} = from);
+    assertEq(to[0], "f");
+
+    from = "foo";
+    ({0: z, ...to} = from);
+    assertEq(z, "f");
+    assertEq(to[0], undefined);
+    assertEq(to[1], "o");
+
+    // String object.
+    from = new String("bar");
+    ({...to} = from);
+    assertEq(to[2], "r");
+
+    from = new String("bar");
+    ({1: z, ...to} = from);
+    assertEq(z, "a");
+    assertEq(to[1], undefined);
+    assertEq(to[2], "r");
+}
+test();
+test();
+test();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/object-spread-unboxed.js
@@ -0,0 +1,40 @@
+load(libdir + "asserts.js");
+
+function Unboxed() {
+    this.a = 0;
+    this.b = true;
+}
+
+function tryCreateUnboxedObject() {
+    var obj;
+    for (var i = 0; i < 1000; ++i) {
+        obj = new Unboxed();
+    }
+    if (unboxedObjectsEnabled())
+        assertEq(isUnboxedObject(obj), true);
+    return obj;
+}
+
+function basic() {
+    var unboxed = tryCreateUnboxedObject();
+
+    var target = {...unboxed};
+    assertDeepEq(target, {a: 0, b: true});
+
+    target = {a: 1, c: 3, ...unboxed};
+    assertDeepEq(target, {a: 0, c: 3, b: true});
+}
+
+function expando() {
+    var unboxed = tryCreateUnboxedObject();
+    unboxed.c = 3.5;
+
+    var target = {...unboxed};
+    assertDeepEq(target, {a: 0, b: true, c: 3.5});
+
+    target = {a: 1, d: 3, ...unboxed};
+    assertDeepEq(target, {a: 0, d: 3, b: true, c: 3.5});
+}
+
+basic();
+expando();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/object-spread.js
@@ -0,0 +1,49 @@
+function test() {
+    var from, to;
+
+    // From values.
+    from = {x: 1, y: 2};
+    to = {...from};
+    assertEq(to.y, 2);
+    to = {...from, ...from};
+    assertEq(to.y, 2);
+
+    // From getter.
+    var c = 7;
+    from = {x: 1, get y() { return ++c; }};
+    to = {...from};
+    assertEq(to.y, 8);
+    to = {...from, ...from};
+    assertEq(to.y, 10);
+
+    // Array with dense elements.
+    from = [1, 2, 3];
+    to = {...from};
+    assertEq(to[2], 3);
+    assertEq("length" in to, false);
+
+    // Object with sparse elements and symbols.
+    from = {x: 1, 1234567: 2, 1234560: 3, [Symbol.iterator]: 5, z: 3};
+    to = {...from};
+    assertEq(to[1234567], 2);
+    assertEq(Object.keys(to).toString(), "1234560,1234567,x,z");
+    assertEq(to[Symbol.iterator], 5);
+
+    // Typed array.
+    from = new Int32Array([1, 2, 3]);
+    to = {...from};
+    assertEq(to[1], 2);
+
+    // Primitive string.
+    from = "foo";
+    to = {...from};
+    assertEq(to[0], "f");
+
+    // String object.
+    from = new String("bar");
+    to = {...from};
+    assertEq(to[2], "r");
+}
+test();
+test();
+test();
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -4,27 +4,30 @@
  * 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/NativeObject-inl.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Casting.h"
 #include "mozilla/CheckedInt.h"
+#include "mozilla/DebugOnly.h"
 
 #include "gc/Marking.h"
 #include "js/Value.h"
 #include "vm/Debugger.h"
 #include "vm/TypedArrayObject.h"
+#include "vm/UnboxedObject.h"
 
 #include "gc/Nursery-inl.h"
 #include "vm/ArrayObject-inl.h"
 #include "vm/EnvironmentObject-inl.h"
 #include "vm/JSObject-inl.h"
 #include "vm/Shape-inl.h"
+#include "vm/UnboxedObject-inl.h"
 
 using namespace js;
 
 using JS::AutoCheckCannotGC;
 using mozilla::ArrayLength;
 using mozilla::CheckedInt;
 using mozilla::DebugOnly;
 using mozilla::PodCopy;
@@ -1493,18 +1496,18 @@ AddOrChangeProperty(JSContext* cx, Handl
             MOZ_ASSERT(!desc.setter());
             return CallAddPropertyHookDense(cx, obj, index, desc.value());
         }
     }
 
     return CallAddPropertyHook(cx, obj, id, desc.value());
 }
 
-// Version of AddOrChangeProperty optimized for adding a plain data property.
-// This function doesn't handle integer ids as we may have to store them in
+// Versions of AddOrChangeProperty optimized for adding a plain data property.
+// These function doesn't handle integer ids as we may have to store them in
 // dense elements.
 static MOZ_ALWAYS_INLINE bool
 AddDataProperty(JSContext* cx, HandleNativeObject obj, HandleId id, HandleValue v)
 {
     MOZ_ASSERT(!JSID_IS_INT(id));
 
     if (!ReshapeForShadowedProp(cx, obj, id))
         return false;
@@ -1513,16 +1516,34 @@ AddDataProperty(JSContext* cx, HandleNat
     if (!shape)
         return false;
 
     UpdateShapeTypeAndValueForWritableDataProp(cx, obj, shape, id, v);
 
     return CallAddPropertyHook(cx, obj, id, v);
 }
 
+static MOZ_ALWAYS_INLINE bool
+AddDataPropertyNonDelegate(JSContext* cx, HandlePlainObject obj, HandleId id, HandleValue v)
+{
+    MOZ_ASSERT(!JSID_IS_INT(id));
+    MOZ_ASSERT(!obj->isDelegate());
+
+    // If we know this is a new property we can call addProperty instead of
+    // the slower putProperty.
+    Shape* shape = NativeObject::addEnumerableDataProperty(cx, obj, id);
+    if (!shape)
+        return false;
+
+    UpdateShapeTypeAndValueForWritableDataProp(cx, obj, shape, id, v);
+
+    MOZ_ASSERT(!obj->getClass()->getAddProperty());
+    return true;
+}
+
 static bool IsConfigurable(unsigned attrs) { return (attrs & JSPROP_PERMANENT) == 0; }
 static bool IsEnumerable(unsigned attrs) { return (attrs & JSPROP_ENUMERATE) != 0; }
 static bool IsWritable(unsigned attrs) { return (attrs & JSPROP_READONLY) == 0; }
 
 static bool IsAccessorDescriptor(unsigned attrs) {
     return (attrs & (JSPROP_GETTER | JSPROP_SETTER)) != 0;
 }
 
@@ -2890,8 +2911,148 @@ js::NativeDeleteProperty(JSContext* cx, 
         obj->setDenseElementHole(cx, JSID_TO_INT(id));
     } else {
         if (!NativeObject::removeProperty(cx, obj, id))
             return false;
     }
 
     return SuppressDeletedProperty(cx, obj, id);
 }
+
+bool
+js::CopyDataPropertiesNative(JSContext* cx, HandlePlainObject target, HandleNativeObject from,
+                             HandlePlainObject excludedItems, bool* optimized)
+{
+    MOZ_ASSERT(!target->isDelegate(),
+               "CopyDataPropertiesNative should only be called during object literal construction"
+               "which precludes that |target| is the prototype of any other object");
+
+    *optimized = false;
+
+    // Don't use the fast path if |from| may have extra indexed or lazy
+    // properties.
+    if (from->getDenseInitializedLength() > 0 ||
+        from->isIndexed() ||
+        from->is<TypedArrayObject>() ||
+        from->getClass()->getNewEnumerate() ||
+        from->getClass()->getEnumerate())
+    {
+        return true;
+    }
+
+    // Collect all enumerable data properties.
+    using ShapeVector = GCVector<Shape*, 8>;
+    Rooted<ShapeVector> shapes(cx, ShapeVector(cx));
+
+    RootedShape fromShape(cx, from->lastProperty());
+    for (Shape::Range<NoGC> r(fromShape); !r.empty(); r.popFront()) {
+        Shape* shape = &r.front();
+        jsid id = shape->propid();
+        MOZ_ASSERT(!JSID_IS_INT(id));
+
+        if (!shape->enumerable())
+            continue;
+        if (excludedItems && excludedItems->contains(cx, id))
+            continue;
+
+        // Don't use the fast path if |from| contains non-data properties.
+        //
+        // This enables two optimizations:
+        // 1. We don't need to handle the case when accessors modify |from|.
+        // 2. String and symbol properties can be added in one go.
+        if (!shape->isDataProperty())
+            return true;
+
+        if (!shapes.append(shape))
+            return false;
+    }
+
+    *optimized = true;
+
+    // If |target| contains no own properties, we can directly call
+    // addProperty instead of the slower putProperty.
+    const bool targetHadNoOwnProperties = target->lastProperty()->isEmptyShape();
+
+    RootedId key(cx);
+    RootedValue value(cx);
+    for (size_t i = shapes.length(); i > 0; i--) {
+        Shape* shape = shapes[i - 1];
+        MOZ_ASSERT(shape->isDataProperty());
+        MOZ_ASSERT(shape->enumerable());
+
+        key = shape->propid();
+        MOZ_ASSERT(!JSID_IS_INT(key));
+
+        MOZ_ASSERT(from->isNative());
+        MOZ_ASSERT(from->lastProperty() == fromShape);
+
+        value = from->getSlot(shape->slot());
+        if (targetHadNoOwnProperties) {
+            MOZ_ASSERT(!target->contains(cx, key),
+                       "didn't expect to find an existing property");
+
+            if (!AddDataPropertyNonDelegate(cx, target, key, value))
+                return false;
+        } else {
+            if (!NativeDefineDataProperty(cx, target, key, value, JSPROP_ENUMERATE))
+                return false;
+        }
+    }
+
+    return true;
+}
+
+bool
+js::CopyDataPropertiesNative(JSContext* cx, HandlePlainObject target,
+                             Handle<UnboxedPlainObject*> from, HandlePlainObject excludedItems,
+                             bool* optimized)
+{
+    MOZ_ASSERT(!target->isDelegate(),
+               "CopyDataPropertiesNative should only be called during object literal construction"
+               "which precludes that |target| is the prototype of any other object");
+
+    *optimized = false;
+
+    // Don't use the fast path for unboxed objects with expandos.
+    if (from->maybeExpando())
+        return true;
+
+    *optimized = true;
+
+    // If |target| contains no own properties, we can directly call
+    // addProperty instead of the slower putProperty.
+    const bool targetHadNoOwnProperties = target->lastProperty()->isEmptyShape();
+
+#ifdef DEBUG
+    RootedObjectGroup fromGroup(cx, from->group());
+#endif
+
+    RootedId key(cx);
+    RootedValue value(cx);
+    const UnboxedLayout& layout = from->layout();
+    for (size_t i = 0; i < layout.properties().length(); i++) {
+        const UnboxedLayout::Property& property = layout.properties()[i];
+        key = NameToId(property.name);
+        MOZ_ASSERT(!JSID_IS_INT(key));
+
+        if (excludedItems && excludedItems->contains(cx, key))
+            continue;
+
+        // Ensure the object stays unboxed.
+        MOZ_ASSERT(from->group() == fromGroup);
+
+        // All unboxed properties are enumerable.
+        value = from->getValue(property);
+
+        if (targetHadNoOwnProperties) {
+            MOZ_ASSERT(!target->contains(cx, key),
+                       "didn't expect to find an existing property");
+
+            if (!AddDataPropertyNonDelegate(cx, target, key, value))
+                return false;
+        } else {
+            if (!NativeDefineDataProperty(cx, target, key, value, JSPROP_ENUMERATE))
+                return false;
+        }
+    }
+
+    return true;
+}
--- a/js/src/vm/NativeObject.h
+++ b/js/src/vm/NativeObject.h
@@ -24,16 +24,17 @@
 #include "vm/ShapedObject.h"
 #include "vm/StringType.h"
 #include "vm/TypeInference.h"
 
 namespace js {
 
 class Shape;
 class TenuringTracer;
+class UnboxedPlainObject;
 
 /*
  * To really poison a set of values, using 'magic' or 'undefined' isn't good
  * enough since often these will just be ignored by buggy code (see bug 629974)
  * in debug builds and crash in release builds. Instead, we use a safe-for-crash
  * pointer.
  */
 static MOZ_ALWAYS_INLINE void
@@ -1602,16 +1603,26 @@ MaybeNativeObject(JSObject* obj)
 }
 
 // Defined in NativeObject-inl.h.
 bool IsPackedArray(JSObject* obj);
 
 extern void
 AddPropertyTypesAfterProtoChange(JSContext* cx, NativeObject* obj, ObjectGroup* oldGroup);
 
+// Specializations of 7.3.23 CopyDataProperties(...) for NativeObjects.
+extern bool
+CopyDataPropertiesNative(JSContext* cx, HandlePlainObject target, HandleNativeObject from,
+                         HandlePlainObject excludedItems, bool* optimized);
+
+extern bool
+CopyDataPropertiesNative(JSContext* cx, HandlePlainObject target,
+                         Handle<UnboxedPlainObject*> from, HandlePlainObject excludedItems,
+                         bool* optimized);
+
 } // namespace js
 
 
 /*** Inline functions declared in JSObject.h that use the native declarations above **************/
 
 inline bool
 js::HasProperty(JSContext* cx, HandleObject obj, HandleId id, bool* foundp)
 {
--- a/js/src/vm/SelfHosting.cpp
+++ b/js/src/vm/SelfHosting.cpp
@@ -2154,16 +2154,69 @@ intrinsic_PromiseResolve(JSContext* cx, 
     JSObject* promise = js::PromiseResolve(cx, constructor, args[1]);
     if (!promise)
         return false;
 
     args.rval().setObject(*promise);
     return true;
 }
 
+static bool
+intrinsic_CopyDataPropertiesOrGetOwnKeys(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    MOZ_ASSERT(args.length() == 3);
+    MOZ_ASSERT(args[0].isObject());
+    MOZ_ASSERT(args[1].isObject());
+    MOZ_ASSERT(args[2].isObjectOrNull());
+
+    RootedObject target(cx, &args[0].toObject());
+    RootedObject from(cx, &args[1].toObject());
+    RootedObject excludedItems(cx, args[2].toObjectOrNull());
+
+    if (from->isNative() &&
+        target->is<PlainObject>() &&
+        (!excludedItems || excludedItems->is<PlainObject>()))
+    {
+        bool optimized;
+        if (!CopyDataPropertiesNative(cx, target.as<PlainObject>(), from.as<NativeObject>(),
+                                      (excludedItems ? excludedItems.as<PlainObject>() : nullptr),
+                                      &optimized))
+        {
+            return false;
+        }
+
+        if (optimized) {
+            args.rval().setNull();
+            return true;
+        }
+    }
+
+    if (from->is<UnboxedPlainObject>() &&
+        target->is<PlainObject>() &&
+        (!excludedItems || excludedItems->is<PlainObject>()))
+    {
+        bool optimized;
+        if (!CopyDataPropertiesNative(cx, target.as<PlainObject>(), from.as<UnboxedPlainObject>(),
+                                      (excludedItems ? excludedItems.as<PlainObject>() : nullptr),
+                                      &optimized))
+        {
+            return false;
+        }
+
+        if (optimized) {
+            args.rval().setNull();
+            return true;
+        }
+    }
+
+    return GetOwnPropertyKeys(cx, from, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS,
+                              args.rval());
+}
+
 // The self-hosting global isn't initialized with the normal set of builtins.
 // Instead, individual C++-implemented functions that're required by
 // self-hosted code are defined as global functions. Accessing these
 // functions via a content compartment's builtins would be unsafe, because
 // content script might have changed the builtins' prototypes' members.
 // Installing the whole set of builtins in the self-hosting compartment, OTOH,
 // would be wasteful: it increases memory usage and initialization time for
 // self-hosting compartment.
@@ -2279,16 +2332,17 @@ static const JSFunctionSpec intrinsic_fu
     JS_FN("DecompileArg",            intrinsic_DecompileArg,            2,0),
     JS_INLINABLE_FN("_FinishBoundFunctionInit", intrinsic_FinishBoundFunctionInit, 3,0,
                     IntrinsicFinishBoundFunctionInit),
     JS_FN("RuntimeDefaultLocale",    intrinsic_RuntimeDefaultLocale,    0,0),
     JS_FN("IsRuntimeDefaultLocale",  intrinsic_IsRuntimeDefaultLocale,  1,0),
     JS_FN("AddContentTelemetry",     intrinsic_AddContentTelemetry,     2,0),
     JS_FN("_DefineDataProperty",     intrinsic_DefineDataProperty,      4,0),
     JS_FN("_DefineProperty",         intrinsic_DefineProperty,          6,0),
+    JS_FN("CopyDataPropertiesOrGetOwnKeys", intrinsic_CopyDataPropertiesOrGetOwnKeys, 3,0),
 
     JS_INLINABLE_FN("_IsConstructing", intrinsic_IsConstructing,        0,0,
                     IntrinsicIsConstructing),
     JS_INLINABLE_FN("SubstringKernel", intrinsic_SubstringKernel,       3,0,
                     IntrinsicSubstringKernel),
     JS_INLINABLE_FN("ObjectHasPrototype",               intrinsic_ObjectHasPrototype,      2,0,
                     IntrinsicObjectHasPrototype),
     JS_INLINABLE_FN("UnsafeSetReservedSlot",            intrinsic_UnsafeSetReservedSlot,   3,0,