summaryrefslogtreecommitdiffstats
path: root/js/src/vm/Watchtower.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/src/vm/Watchtower.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/vm/Watchtower.cpp')
-rw-r--r--js/src/vm/Watchtower.cpp427
1 files changed, 427 insertions, 0 deletions
diff --git a/js/src/vm/Watchtower.cpp b/js/src/vm/Watchtower.cpp
new file mode 100644
index 0000000000..80023d7e81
--- /dev/null
+++ b/js/src/vm/Watchtower.cpp
@@ -0,0 +1,427 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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/Watchtower.h"
+
+#include "js/CallAndConstruct.h"
+#include "vm/Compartment.h"
+#include "vm/JSContext.h"
+#include "vm/JSObject.h"
+#include "vm/NativeObject.h"
+#include "vm/PlainObject.h"
+#include "vm/Realm.h"
+
+#include "vm/Compartment-inl.h"
+#include "vm/JSObject-inl.h"
+#include "vm/NativeObject-inl.h"
+#include "vm/Realm-inl.h"
+#include "vm/Shape-inl.h"
+
+using namespace js;
+
+static bool AddToWatchtowerLog(JSContext* cx, const char* kind,
+ HandleObject obj, HandleValue extra) {
+ // Add an object storing {kind, object, extra} to the log for testing
+ // purposes.
+
+ MOZ_ASSERT(obj->useWatchtowerTestingLog());
+
+ RootedString kindString(cx, NewStringCopyZ<CanGC>(cx, kind));
+ if (!kindString) {
+ return false;
+ }
+
+ Rooted<PlainObject*> logObj(cx, NewPlainObjectWithProto(cx, nullptr));
+ if (!logObj) {
+ return false;
+ }
+ if (!JS_DefineProperty(cx, logObj, "kind", kindString, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ if (!JS_DefineProperty(cx, logObj, "object", obj, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ if (!JS_DefineProperty(cx, logObj, "extra", extra, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ if (!cx->runtime()->watchtowerTestingLog->append(logObj)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ return true;
+}
+
+static bool ReshapeForShadowedProp(JSContext* cx, Handle<NativeObject*> obj,
+ HandleId id) {
+ // |obj| has been used as the prototype of another object. Check if we're
+ // shadowing a property on its proto chain. In this case we need to reshape
+ // that object for shape teleporting to work correctly.
+ //
+ // See also the 'Shape Teleporting Optimization' comment in jit/CacheIR.cpp.
+
+ MOZ_ASSERT(obj->isUsedAsPrototype());
+
+ // Lookups on integer ids cannot be cached through prototypes.
+ if (id.isInt()) {
+ return true;
+ }
+
+ RootedObject proto(cx, obj->staticPrototype());
+ while (proto) {
+ // Lookups will not be cached through non-native protos.
+ if (!proto->is<NativeObject>()) {
+ break;
+ }
+
+ if (proto->as<NativeObject>().contains(cx, id)) {
+ return JSObject::setInvalidatedTeleporting(cx, proto);
+ }
+
+ proto = proto->staticPrototype();
+ }
+
+ return true;
+}
+
+static void InvalidateMegamorphicCache(JSContext* cx,
+ Handle<NativeObject*> obj) {
+ // The megamorphic cache only checks the receiver object's shape. We need to
+ // invalidate the cache when a prototype object changes its set of properties,
+ // to account for cached properties that are deleted, turned into an accessor
+ // property, or shadowed by another object on the proto chain.
+
+ MOZ_ASSERT(obj->isUsedAsPrototype());
+
+ cx->caches().megamorphicCache.bumpGeneration();
+ cx->caches().megamorphicSetPropCache->bumpGeneration();
+}
+
+void MaybePopReturnFuses(JSContext* cx, Handle<NativeObject*> nobj) {
+ JSObject* objectProto = &cx->global()->getObjectPrototype();
+ if (nobj == objectProto) {
+ nobj->realm()->realmFuses.objectPrototypeHasNoReturnProperty.popFuse(
+ cx, nobj->realm()->realmFuses);
+ return;
+ }
+
+ JSObject* iteratorProto = cx->global()->maybeGetIteratorPrototype();
+ if (nobj == iteratorProto) {
+ nobj->realm()->realmFuses.iteratorPrototypeHasNoReturnProperty.popFuse(
+ cx, nobj->realm()->realmFuses);
+ return;
+ }
+
+ JSObject* arrayIterProto = cx->global()->maybeGetArrayIteratorPrototype();
+ if (nobj == arrayIterProto) {
+ cx->realm()->realmFuses.arrayIteratorPrototypeHasNoReturnProperty.popFuse(
+ cx, nobj->realm()->realmFuses);
+ return;
+ }
+}
+
+// static
+bool Watchtower::watchPropertyAddSlow(JSContext* cx, Handle<NativeObject*> obj,
+ HandleId id) {
+ MOZ_ASSERT(watchesPropertyAdd(obj));
+
+ if (obj->isUsedAsPrototype()) {
+ if (!ReshapeForShadowedProp(cx, obj, id)) {
+ return false;
+ }
+ if (!id.isInt()) {
+ InvalidateMegamorphicCache(cx, obj);
+ }
+
+ if (id == NameToId(cx->names().return_)) {
+ MaybePopReturnFuses(cx, obj);
+ }
+ }
+
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ RootedValue val(cx, IdToValue(id));
+ if (!AddToWatchtowerLog(cx, "add-prop", obj, val)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool ReshapeForProtoMutation(JSContext* cx, HandleObject obj) {
+ // To avoid the JIT guarding on each prototype in the proto chain to detect
+ // prototype mutation, we can instead reshape the rest of the proto chain such
+ // that a guard on any of them is sufficient. To avoid excessive reshaping and
+ // invalidation, we apply heuristics to decide when to apply this and when
+ // to require a guard.
+ //
+ // There are two cases:
+ //
+ // (1) The object is not marked IsUsedAsPrototype. This is the common case.
+ // Because shape implies proto, we rely on the caller changing the
+ // object's shape. The JIT guards on this object's shape or prototype so
+ // there's nothing we have to do here for objects on the proto chain.
+ //
+ // (2) The object is marked IsUsedAsPrototype. This implies the object may be
+ // participating in shape teleporting. To invalidate JIT ICs depending on
+ // the proto chain being unchanged, set the InvalidatedTeleporting shape
+ // flag for this object and objects on its proto chain.
+ //
+ // This flag disables future shape teleporting attempts, so next time this
+ // happens the loop below will be a no-op.
+ //
+ // NOTE: We only handle NativeObjects and don't propagate reshapes through
+ // any non-native objects on the chain.
+ //
+ // See Also:
+ // - GeneratePrototypeGuards
+ // - GeneratePrototypeHoleGuards
+
+ MOZ_ASSERT(obj->isUsedAsPrototype());
+
+ RootedObject pobj(cx, obj);
+
+ while (pobj && pobj->is<NativeObject>()) {
+ if (!pobj->hasInvalidatedTeleporting()) {
+ if (!JSObject::setInvalidatedTeleporting(cx, pobj)) {
+ return false;
+ }
+ }
+ pobj = pobj->staticPrototype();
+ }
+
+ return true;
+}
+
+static bool WatchProtoChangeImpl(JSContext* cx, HandleObject obj) {
+ if (!obj->isUsedAsPrototype()) {
+ return true;
+ }
+ if (!ReshapeForProtoMutation(cx, obj)) {
+ return false;
+ }
+ if (obj->is<NativeObject>()) {
+ InvalidateMegamorphicCache(cx, obj.as<NativeObject>());
+
+ NativeObject* nobj = &obj->as<NativeObject>();
+ if (nobj == cx->global()->maybeGetArrayIteratorPrototype()) {
+ nobj->realm()->realmFuses.arrayIteratorPrototypeHasIteratorProto.popFuse(
+ cx, nobj->realm()->realmFuses);
+ }
+
+ if (nobj == cx->global()->maybeGetIteratorPrototype()) {
+ nobj->realm()->realmFuses.iteratorPrototypeHasObjectProto.popFuse(
+ cx, nobj->realm()->realmFuses);
+ }
+ }
+
+ return true;
+}
+
+// static
+bool Watchtower::watchProtoChangeSlow(JSContext* cx, HandleObject obj) {
+ MOZ_ASSERT(watchesProtoChange(obj));
+
+ if (!WatchProtoChangeImpl(cx, obj)) {
+ return false;
+ }
+
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ if (!AddToWatchtowerLog(cx, "proto-change", obj,
+ JS::UndefinedHandleValue)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void MaybePopArrayIteratorFuse(JSContext* cx, NativeObject* obj,
+ jsid id) {
+ if (!id.isWellKnownSymbol(JS::SymbolCode::iterator)) {
+ return;
+ }
+
+ JSObject* originalArrayPrototype = obj->global().maybeGetArrayPrototype();
+ if (!originalArrayPrototype) {
+ return;
+ }
+
+ if (obj != originalArrayPrototype) {
+ return;
+ }
+
+ obj->realm()->realmFuses.arrayPrototypeIteratorFuse.popFuse(
+ cx, obj->realm()->realmFuses);
+}
+
+static void MaybePopArrayIteratorPrototypeNextFuse(JSContext* cx,
+ NativeObject* obj, jsid id) {
+ JSObject* originalArrayIteratorPrototoype =
+ obj->global().maybeGetArrayIteratorPrototype();
+ if (!originalArrayIteratorPrototoype) {
+ return;
+ }
+
+ if (obj != originalArrayIteratorPrototoype) {
+ return;
+ }
+
+ PropertyKey nextId = NameToId(cx->names().next);
+ if (id != nextId) {
+ return;
+ }
+
+ obj->realm()->realmFuses.arrayPrototypeIteratorNextFuse.popFuse(
+ cx, obj->realm()->realmFuses);
+}
+
+static void MaybePopFuses(JSContext* cx, NativeObject* obj, jsid id) {
+ // Handle a write to Array.prototype[@@iterator]
+ MaybePopArrayIteratorFuse(cx, obj, id);
+ // Handle a write to Array.prototype[@@iterator].next
+ MaybePopArrayIteratorPrototypeNextFuse(cx, obj, id);
+}
+
+// static
+bool Watchtower::watchPropertyRemoveSlow(JSContext* cx,
+ Handle<NativeObject*> obj,
+ HandleId id) {
+ MOZ_ASSERT(watchesPropertyRemove(obj));
+
+ if (obj->isUsedAsPrototype() && !id.isInt()) {
+ InvalidateMegamorphicCache(cx, obj);
+ }
+
+ if (obj->isGenerationCountedGlobal()) {
+ obj->as<GlobalObject>().bumpGenerationCount();
+ }
+
+ if (MOZ_UNLIKELY(obj->hasFuseProperty())) {
+ MaybePopFuses(cx, obj, id);
+ }
+
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ RootedValue val(cx, IdToValue(id));
+ if (!AddToWatchtowerLog(cx, "remove-prop", obj, val)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// static
+bool Watchtower::watchPropertyChangeSlow(JSContext* cx,
+ Handle<NativeObject*> obj, HandleId id,
+ PropertyFlags flags) {
+ MOZ_ASSERT(watchesPropertyChange(obj));
+
+ if (obj->isUsedAsPrototype() && !id.isInt()) {
+ InvalidateMegamorphicCache(cx, obj);
+ }
+
+ if (obj->isGenerationCountedGlobal()) {
+ // The global generation counter only cares whether a property
+ // changes from data property to accessor or vice-versa. Changing
+ // the flags on a property doesn't matter.
+ uint32_t propIndex;
+ Rooted<PropMap*> map(cx, obj->shape()->lookup(cx, id, &propIndex));
+ MOZ_ASSERT(map);
+ PropertyInfo prop = map->getPropertyInfo(propIndex);
+ bool wasAccessor = prop.isAccessorProperty();
+ bool isAccessor = flags.isAccessorProperty();
+ if (wasAccessor != isAccessor) {
+ obj->as<GlobalObject>().bumpGenerationCount();
+ }
+ }
+
+ // Property fuses should also be popped on property changes, as value can
+ // change via this path.
+ if (MOZ_UNLIKELY(obj->hasFuseProperty())) {
+ MaybePopFuses(cx, obj, id);
+ }
+
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ RootedValue val(cx, IdToValue(id));
+ if (!AddToWatchtowerLog(cx, "change-prop", obj, val)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// static
+template <AllowGC allowGC>
+bool Watchtower::watchPropertyModificationSlow(
+ JSContext* cx, typename MaybeRooted<NativeObject*, allowGC>::HandleType obj,
+ typename MaybeRooted<PropertyKey, allowGC>::HandleType id) {
+ MOZ_ASSERT(watchesPropertyModification(obj));
+
+ if (MOZ_UNLIKELY(obj->hasFuseProperty())) {
+ MaybePopFuses(cx, obj, id);
+ }
+
+ // If we cannot GC, we can't manipulate the log, but we need to be able to
+ // call this in places we cannot GC.
+ if constexpr (allowGC == AllowGC::CanGC) {
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ RootedValue val(cx, IdToValue(id));
+ if (!AddToWatchtowerLog(cx, "modify-prop", obj, val)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+template bool Watchtower::watchPropertyModificationSlow<AllowGC::CanGC>(
+ JSContext* cx,
+ typename MaybeRooted<NativeObject*, AllowGC::CanGC>::HandleType obj,
+ typename MaybeRooted<PropertyKey, AllowGC::CanGC>::HandleType id);
+template bool Watchtower::watchPropertyModificationSlow<AllowGC::NoGC>(
+ JSContext* cx,
+ typename MaybeRooted<NativeObject*, AllowGC::NoGC>::HandleType obj,
+ typename MaybeRooted<PropertyKey, AllowGC::NoGC>::HandleType id);
+
+// static
+bool Watchtower::watchFreezeOrSealSlow(JSContext* cx,
+ Handle<NativeObject*> obj) {
+ MOZ_ASSERT(watchesFreezeOrSeal(obj));
+
+ if (MOZ_UNLIKELY(obj->useWatchtowerTestingLog())) {
+ if (!AddToWatchtowerLog(cx, "freeze-or-seal", obj,
+ JS::UndefinedHandleValue)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// static
+bool Watchtower::watchObjectSwapSlow(JSContext* cx, HandleObject a,
+ HandleObject b) {
+ MOZ_ASSERT(watchesObjectSwap(a, b));
+
+ // If we're swapping an object that's used as prototype, we're mutating the
+ // proto chains of other objects. Treat this as a proto change to ensure we
+ // invalidate shape teleporting and megamorphic caches.
+ if (!WatchProtoChangeImpl(cx, a)) {
+ return false;
+ }
+ if (!WatchProtoChangeImpl(cx, b)) {
+ return false;
+ }
+
+ // Note: we don't invoke the testing callback for swap because the objects may
+ // not be safe to expose to JS at this point. See bug 1754699.
+
+ return true;
+}