diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp index a362bc50e6..8b1a5f03de 100644 --- a/image/imgLoader.cpp +++ b/image/imgLoader.cpp @@ -928,12 +928,38 @@ using namespace std; void imgCacheQueue::Remove(imgCacheEntry* entry) { - queueContainer::iterator it = find(mQueue.begin(), mQueue.end(), entry); - if (it != mQueue.end()) { - mSize -= (*it)->GetDataSize(); - mQueue.erase(it); - MarkDirty(); + uint64_t index = mQueue.IndexOf(entry); + if (index == queueContainer::NoIndex) { + return; } + + mSize -= mQueue[index]->GetDataSize(); + + // If the queue is clean and this is the first entry, + // then we can efficiently remove the entry without + // dirtying the sort order. + if (!IsDirty() && index == 0) { + std::pop_heap(mQueue.begin(), mQueue.end(), + imgLoader::CompareCacheEntries); + mQueue.RemoveElementAt(mQueue.Length() - 1); + return; + } + + // Remove from the middle of the list. This potentially + // breaks the binary heap sort order. + mQueue.RemoveElementAt(index); + + // If we only have one entry or the queue is empty, though, + // then the sort order is still effectively good. + // Simply refresh the list to clear the dirty flag. + if (mQueue.Length() <= 1) { + Refresh(); + return; + } + + // Otherwise we must mark the queue dirty and potentially + // trigger an expensive sort later. + MarkDirty(); } void @@ -942,23 +968,26 @@ imgCacheQueue::Push(imgCacheEntry* entry) mSize += entry->GetDataSize(); RefPtr refptr(entry); - mQueue.push_back(refptr); - MarkDirty(); + mQueue.AppendElement(Move(refptr)); + // If we're not dirty already, then we can efficiently add this to the binary heap immediately. + if (!IsDirty()) { + std::push_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); + } } already_AddRefed imgCacheQueue::Pop() { - if (mQueue.empty()) { + if (mQueue.IsEmpty()) { return nullptr; } if (IsDirty()) { Refresh(); } - RefPtr entry = mQueue[0]; std::pop_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); - mQueue.pop_back(); + RefPtr entry = Move(mQueue.LastElement()); + mQueue.RemoveElementAt(mQueue.Length() - 1); mSize -= entry->GetDataSize(); return entry.forget(); @@ -967,6 +996,7 @@ imgCacheQueue::Pop() void imgCacheQueue::Refresh() { + // Re-heap the list. This is an O(3 * n) operation and best avoided if possible. std::make_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries); mDirty = false; } @@ -986,7 +1016,13 @@ imgCacheQueue::IsDirty() uint32_t imgCacheQueue::GetNumElements() const { - return mQueue.size(); + return mQueue.Length(); +} + +bool +imgCacheQueue::Contains(imgCacheEntry* aEntry) const +{ + return mQueue.Contains(aEntry); } imgCacheQueue::iterator @@ -1540,7 +1576,11 @@ void imgLoader::CacheEntriesChanged(bool aForChrome, int32_t aSizeDiff /* = 0 */) { imgCacheQueue& queue = GetCacheQueue(aForChrome); - queue.MarkDirty(); + // We only need to dirty the queue if there is any sorting taking place. + // Empty or single-entry lists can't become dirty. + if (queue.GetNumElements() > 1) { + queue.MarkDirty(); + } queue.UpdateSize(aSizeDiff); } @@ -1569,7 +1609,9 @@ imgLoader::CheckCacheLimits(imgCacheTable& cache, imgCacheQueue& queue) } if (entry) { - RemoveFromCache(entry); + // We just popped this entry from the queue, so pass AlreadyRemoved + // to avoid searching the queue again in RemoveFromCache. + RemoveFromCache(entry, QueueState::AlreadyRemoved); } } } @@ -1870,7 +1912,7 @@ imgLoader::RemoveFromCache(const ImageCacheKey& aKey) } bool -imgLoader::RemoveFromCache(imgCacheEntry* entry) +imgLoader::RemoveFromCache(imgCacheEntry* entry, QueueState aQueueState) { LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache entry"); @@ -1886,13 +1928,24 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry) cache.Remove(key); + if (queue.IsDirty()) { + queue.Refresh(); + } + if (entry->HasNoProxies()) { LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache removing from tracker"); if (mCacheTracker) { mCacheTracker->RemoveObject(entry); } - queue.Remove(entry); + // Only search the queue to remove the entry if its possible it might + // be in the queue. If we know its not in the queue this would be + // wasted work. + MOZ_ASSERT_IF(aQueueState == QueueState::AlreadyRemoved, + !queue.Contains(entry)); + if (aQueueState == QueueState::MaybeExists) { + queue.Remove(entry); + } } entry->SetEvicted(true); @@ -1937,13 +1990,13 @@ imgLoader::EvictEntries(imgCacheQueue& aQueueToClear) // We have to make a temporary, since RemoveFromCache removes the element // from the queue, invalidating iterators. nsTArray > entries(aQueueToClear.GetNumElements()); - for (imgCacheQueue::const_iterator i = aQueueToClear.begin(); - i != aQueueToClear.end(); ++i) { + for (auto i = aQueueToClear.begin(); i != aQueueToClear.end(); ++i) { entries.AppendElement(*i); } - for (uint32_t i = 0; i < entries.Length(); ++i) { - if (!RemoveFromCache(entries[i])) { + // Iterate in reverse order to minimize array copying. + for (auto& entry : entries) { + if (!RemoveFromCache(entry)) { return NS_ERROR_FAILURE; } } @@ -2732,6 +2785,12 @@ imgCacheValidator::AddProxy(imgRequestProxy* aProxy) mProxies.AppendObject(aProxy); } +void +imgCacheValidator::RemoveProxy(imgRequestProxy* aProxy) +{ + mProxies.RemoveObject(aProxy); +} + /** nsIRequestObserver methods **/ NS_IMETHODIMP diff --git a/image/imgLoader.h b/image/imgLoader.h index 84272c4832..3b525173b1 100644 --- a/image/imgLoader.h +++ b/image/imgLoader.h @@ -177,7 +177,8 @@ public: uint32_t GetSize() const; void UpdateSize(int32_t diff); uint32_t GetNumElements() const; - typedef std::vector > queueContainer; + bool Contains(imgCacheEntry* aEntry) const; + typedef nsTArray > queueContainer; typedef queueContainer::iterator iterator; typedef queueContainer::const_iterator const_iterator; @@ -315,7 +316,16 @@ public: nsresult InitCache(); bool RemoveFromCache(const ImageCacheKey& aKey); - bool RemoveFromCache(imgCacheEntry* entry); + + // Enumeration describing if a given entry is in the cache queue or not. + // There are some cases we know the entry is definitely not in the queue. + enum class QueueState { + MaybeExists, + AlreadyRemoved + }; + + bool RemoveFromCache(imgCacheEntry* entry, + QueueState aQueueState = QueueState::MaybeExists); bool PutIntoCache(const ImageCacheKey& aKey, imgCacheEntry* aEntry); @@ -515,6 +525,7 @@ public: bool forcePrincipalCheckForCacheEntry); void AddProxy(imgRequestProxy* aProxy); + void RemoveProxy(imgRequestProxy* aProxy); NS_DECL_ISUPPORTS NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER diff --git a/image/imgRequestProxy.cpp b/image/imgRequestProxy.cpp index 4ccf1ff1d2..08e8c61903 100644 --- a/image/imgRequestProxy.cpp +++ b/image/imgRequestProxy.cpp @@ -342,8 +342,14 @@ imgRequestProxy::CancelAndForgetObserver(nsresult aStatus) bool oldIsInLoadGroup = mIsInLoadGroup; mIsInLoadGroup = false; - if (GetOwner()) { - GetOwner()->RemoveProxy(this, aStatus); + imgRequest* owner = GetOwner(); + if (owner) { + imgCacheValidator* validator = owner->GetValidator(); + if (validator) { + validator->RemoveProxy(this); + } + + owner->RemoveProxy(this, aStatus); } mIsInLoadGroup = oldIsInLoadGroup; diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 54446d2578..9a6022f340 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -253,6 +253,67 @@ function ArraySort(comparefn) { return MergeSort(O, len, comparefn); } +// ES2023 22.1.3.30 Array.prototype.toSorted ( comparefn ) +function ArrayToSorted(comparefn) { + if (comparefn !== undefined) { + if (!IsCallable(comparefn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); + } + } + + // Step 1: Let O be ? ToObject(this). Let len be ? ToLength(O.length). + var O = ToObject(this); + var len = ToLength(O.length); + + // Step 2: Snapshot values in ascending index order into a List. + var items = new List(); + var itemsLen = len; + for (var k = 0; k < len; k++) { + items[k] = O[k]; + } + + // Step 3: Create SortCompare per spec. + var wrappedCompareFn = comparefn; + var sortCompare; + if (wrappedCompareFn === undefined) { + sortCompare = function(x, y) { + if (x === undefined) + return y === undefined ? 0 : 1; + if (y === undefined) + return -1; + + var xString = ToString(x); + var yString = ToString(y); + if (xString < yString) + return -1; + if (xString > yString) + return 1; + return 0; + }; + } else { + sortCompare = function(x, y) { + if (x === undefined) + return y === undefined ? 0 : 1; + if (y === undefined) + return -1; + + var v = ToNumber(wrappedCompareFn(x, y)); + return v !== v ? 0 : v; + }; + } + + // Step 4: Sort the snapshot List using SortCompare. + if (itemsLen > 1) + MergeSort(items, itemsLen, sortCompare); + + // Step 5: Create result array and write sorted values. + var A = ArraySpeciesCreate(O, len); + for (var j = 0; j < itemsLen; j++) + _DefineDataProperty(A, j, items[j]); + + return A; +} + /* ES5 15.4.4.18. */ function ArrayForEach(callbackfn/*, thisArg*/) { /* Step 1. */ diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp index 4f5c717be5..6cd57f19a2 100644 --- a/js/src/jsarray.cpp +++ b/js/src/jsarray.cpp @@ -3208,6 +3208,7 @@ static const JSFunctionSpec array_methods[] = { /* ES2023 proposals */ JS_SELF_HOSTED_FN("findLast", "ArrayFindLast", 1,0), JS_SELF_HOSTED_FN("findLastIndex", "ArrayFindLastIndex", 1,0), + JS_SELF_HOSTED_FN("toSorted", "ArrayToSorted", 1,0), JS_FS_END }; @@ -3379,7 +3380,8 @@ array_proto_finish(JSContext* cx, JS::HandleObject ctor, JS::HandleObject proto) !DefineProperty(cx, unscopables, cx->names().flatMap, value) || !DefineProperty(cx, unscopables, cx->names().includes, value) || !DefineProperty(cx, unscopables, cx->names().keys, value) || - !DefineProperty(cx, unscopables, cx->names().values, value)) + !DefineProperty(cx, unscopables, cx->names().values, value) || + !DefineProperty(cx, unscopables, cx->names().toSorted, value)) { return false; } diff --git a/js/src/tests/ecma_6/Array/toSorted.html b/js/src/tests/ecma_6/Array/toSorted.html new file mode 100644 index 0000000000..3738b05c48 --- /dev/null +++ b/js/src/tests/ecma_6/Array/toSorted.html @@ -0,0 +1,59 @@ + + +Array.prototype.toSorted test + +

+
+
diff --git a/js/src/tests/ecma_6/Array/toSorted.js b/js/src/tests/ecma_6/Array/toSorted.js
new file mode 100644
index 0000000000..fbd2540deb
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/toSorted.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/ */
+
+assertEq(typeof Array.prototype.toSorted, "function");
+
+// Non-mutating behavior.
+let original = [3, 1, 2];
+let sorted = original.toSorted();
+assertEq(original !== sorted, true);
+assertEq(original.join(","), "3,1,2");
+assertEq(sorted.join(","), "1,2,3");
+
+// Compare function.
+let nums = [10, 1, 5];
+let desc = nums.toSorted((a, b) => b - a);
+assertEq(desc.join(","), "10,5,1");
+
+// Stable sort.
+let stableInput = [
+    {v: 1, id: "a"},
+    {v: 1, id: "b"},
+    {v: 1, id: "c"}
+];
+let stableSorted = stableInput.toSorted((x, y) => x.v - y.v);
+assertEq(stableSorted.map(o => o.id).join(""), "abc");
+
+// Holes are treated as undefined (properties are created).
+let sparse = [3, , 1];
+let sparseSorted = sparse.toSorted();
+assertEq(sparseSorted.length, 3);
+assertEq(sparseSorted[0], 1);
+assertEq(sparseSorted[1], 3);
+assertEq(2 in sparseSorted, true);
+assertEq(sparseSorted[2], undefined);
+
+// Array-like input.
+let arrayLike = {0: 2, 1: 1, length: 2};
+let arrayLikeSorted = Array.prototype.toSorted.call(arrayLike);
+assertEq(Array.isArray(arrayLikeSorted), true);
+assertEq(arrayLikeSorted.join(","), "1,2");
+
+// Getter access order (ascending indices).
+let accessLog = [];
+let getterArr = {
+    length: 3,
+    get 0() { accessLog.push(0); return 3; },
+    get 1() { accessLog.push(1); return 1; },
+    get 2() { accessLog.push(2); return 2; }
+};
+Array.prototype.toSorted.call(getterArr);
+assertEq(accessLog.join(","), "0,1,2");
+
+// Comparator errors propagate.
+assertThrowsInstanceOf(() => [1, 2].toSorted(1), TypeError);
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0);
diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h
index 8489648d6b..166bce158a 100644
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -428,6 +428,7 @@
     macro(toJSON, toJSON, "toJSON") \
     macro(toLocaleString, toLocaleString, "toLocaleString") \
     macro(toSource, toSource, "toSource") \
+    macro(toSorted, toSorted, "toSorted") \
     macro(toString, toString, "toString") \
     macro(toUTCString, toUTCString, "toUTCString") \
     macro(true, true_, "true") \
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index ca6d7fd795..3f66354876 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -206,11 +206,7 @@ pref("dom.permissions.revoke.enable", false);
 pref("dom.performance.time_to_non_blank_paint.enabled", false);
 
 // Enable Performance Observer API
-#ifdef NIGHTLY_BUILD
 pref("dom.enable_performance_observer", true);
-#else
-pref("dom.enable_performance_observer", false);
-#endif
 
 // Enable requestIdleCallback API
 pref("dom.requestIdleCallback.enabled", true);
diff --git a/toolkit/fonts/moz.build b/toolkit/fonts/moz.build
index bfdff9e075..129a7737c6 100644
--- a/toolkit/fonts/moz.build
+++ b/toolkit/fonts/moz.build
@@ -3,5 +3,4 @@
 # 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/.
 
-if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
-    FINAL_TARGET_FILES.fonts += ['TwemojiMozilla.ttf']
+FINAL_TARGET_FILES.fonts += ['TwemojiMozilla.ttf']
diff --git a/xpcom/ds/nsExpirationTracker.h b/xpcom/ds/nsExpirationTracker.h
index 672256d1dc..53b4ceafdc 100644
--- a/xpcom/ds/nsExpirationTracker.h
+++ b/xpcom/ds/nsExpirationTracker.h
@@ -127,9 +127,15 @@ public:
    */
   nsresult AddObjectLocked(T* aObj, const AutoLock& aAutoLock)
   {
+    if (NS_WARN_IF(!aObj)) {
+      // Invalid object to add
+      return NS_ERROR_UNEXPECTED;
+    }
     nsExpirationState* state = aObj->GetExpirationState();
-    NS_ASSERTION(!state->IsTracked(),
-                 "Tried to add an object that's already tracked");
+    if (NS_WARN_IF(state->IsTracked())) {
+      // Tried to add an object that's already tracked.
+      return NS_ERROR_UNEXPECTED;
+    }
     nsTArray& generation = mGenerations[mNewestGeneration];
     uint32_t index = generation.Length();
     if (index > nsExpirationState::MAX_INDEX_IN_GENERATION) {
@@ -156,8 +162,15 @@ public:
    */
   void RemoveObjectLocked(T* aObj, const AutoLock& aAutoLock)
   {
+    if (NS_WARN_IF(!aObj)) {
+      // Invalid object to remove
+      return;
+    }
     nsExpirationState* state = aObj->GetExpirationState();
-    NS_ASSERTION(state->IsTracked(), "Tried to remove an object that's not tracked");
+    if (NS_WARN_IF(!state->IsTracked())) {
+      // Tried to remove an object that's not tracked
+      return;
+    }
     nsTArray& generation = mGenerations[state->mGeneration];
     uint32_t index = state->mIndexInGeneration;
     NS_ASSERTION(generation.Length() > index &&
@@ -458,11 +471,13 @@ class nsExpirationTracker : protected ::detail::SingleThreadedExpirationTracker<
   Lock mLock;
 
   AutoLock FakeLock() {
+    MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
     return AutoLock(mLock);
   }
 
   Lock& GetMutex() override
   {
+    MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
     return mLock;
   }