From 5ef99253135698a8bf2d5642cfa0e8a4cb34fb7e Mon Sep 17 00:00:00 2001 From: Adam Vandolder Date: Fri, 11 Sep 2026 22:28:13 +0000 Subject: [PATCH] Bug 2061013 - Part 3: Fire prefetches for moderate eagerness speculation rules on hover. r=jjaschke Differential Revision: https://phabricator.services.mozilla.com/D321440 --- dom/base/SpeculationRules.cpp | 75 ++++++++++++++++++++++++ dom/base/SpeculationRules.h | 19 +++++- dom/events/EventStateManager.cpp | 16 +++++ dom/events/EventStateManager.h | 5 ++ modules/libpref/init/StaticPrefList.yaml | 7 +++ 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/dom/base/SpeculationRules.cpp b/dom/base/SpeculationRules.cpp index e4cf3bd10a0c..2358abef2b58 100644 --- a/dom/base/SpeculationRules.cpp +++ b/dom/base/SpeculationRules.cpp @@ -5,7 +5,9 @@ #include "mozilla/dom/SpeculationRules.h" #include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" #include "mozilla/dom/PrefetchCandidates.h" #include "mozilla/dom/PrefetchLog.h" #include "mozilla/dom/ReferrerPolicyBinding.h" @@ -14,8 +16,10 @@ #include "mozilla/dom/speculationrules_ffi_generated.h" #include "nsContentUtils.h" #include "nsCycleCollectionParticipant.h" +#include "nsIContentInlines.h" #include "nsIFrame.h" #include "nsIScriptElement.h" +#include "nsITimer.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsTArray.h" @@ -72,6 +76,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(SpeculationRules) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SpeculationRules) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHoverLink) for (const auto& entry : tmp->mRuleSetsFromScript) { NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mRuleSetsFromScript key"); cb.NoteXPCOMChild(entry.GetKey()); @@ -79,13 +84,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SpeculationRules) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SpeculationRules) + tmp->CancelHoverTimer(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) NS_IMPL_CYCLE_COLLECTION_UNLINK(mRuleSetsFromScript) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHoverLink) NS_IMPL_CYCLE_COLLECTION_UNLINK_END SpeculationRules::SpeculationRules(Document* aDocument) : mDocument(aDocument) {} +SpeculationRules::~SpeculationRules() { CancelHoverTimer(); } + // https://html.spec.whatwg.org/#register-speculation-rules void SpeculationRules::RegisterFromScript( nsIScriptElement* aScriptElement, UniquePtr aRuleSet) { @@ -260,4 +269,70 @@ void SpeculationRules::FindMatchingLinks(nsTArray& aLinks) { // 3. Return links. } +Element* SpeculationRules::FindInterestedLink(nsIContent* aContent) const { + for (nsIContent* content = aContent; content; + content = content->GetFlattenedTreeParent()) { + if (content->IsElement() && mLinks.Contains(content->AsElement())) { + return content->AsElement(); + } + } + return nullptr; +} + +void SpeculationRules::HoverContentChanged(nsIContent* aContent) { + if (mCandidateGroups.IsEmpty()) { + return; + } + + RefPtr link = FindInterestedLink(aContent); + if (link == mHoverLink) { + // The cursor moved within the same link, so it has been hovered + // continuously: let the timer keep running, or stay expired if the link has + // already been enacted. + return; + } + + CancelHoverTimer(); + mHoverLink = link; + if (!mHoverLink) { + return; + } + + // The timer holds no reference to us, so it must not outlive us; both the + // destructor and the cycle collector cancel it. + NS_NewTimerWithFuncCallback( + getter_AddRefs(mHoverTimer), HoverTimerFired, this, + StaticPrefs::dom_speculation_rules_moderate_hover_delay_ms(), + nsITimer::TYPE_ONE_SHOT, "SpeculationRules::HoverTimerFired"_ns); +} + +void SpeculationRules::CancelHoverTimer() { + if (mHoverTimer) { + mHoverTimer->Cancel(); + mHoverTimer = nullptr; + } + mHoverLink = nullptr; +} + +/* static */ +void SpeculationRules::HoverTimerFired(nsITimer* aTimer, void* aClosure) { + RefPtr speculationRules = static_cast(aClosure); + speculationRules->mHoverTimer = nullptr; + + // mHoverLink is deliberately left set, so that moving the cursor around + // within the link it names doesn't arm the timer all over again. It is + // cleared once the cursor moves on to a different link, or off of links + // entirely. + RefPtr link = speculationRules->mHoverLink; + if (!link || !link->IsInComposedDoc()) { + return; + } + nsCOMPtr uri = link->GetHrefURI(); + if (uri) { + // TODO(avandolder): Currently, this is also how Eager eagerness rules will + // be fired. We will eventually move them to a shorter timer. + speculationRules->EnactCandidates(uri, Eagerness::Moderate); + } +} + } // namespace mozilla::dom diff --git a/dom/base/SpeculationRules.h b/dom/base/SpeculationRules.h index 674983fb4b63..53248f87805d 100644 --- a/dom/base/SpeculationRules.h +++ b/dom/base/SpeculationRules.h @@ -7,13 +7,16 @@ #include "mozilla/UniquePtr.h" #include "mozilla/dom/speculationrules_ffi_generated.h" +#include "nsCOMPtr.h" #include "nsClassHashtable.h" #include "nsCycleCollectionParticipant.h" #include "nsHashKeys.h" #include "nsTArrayForwardDeclare.h" #include "nsTHashSet.h" +class nsIContent; class nsIScriptElement; +class nsITimer; class nsIURI; namespace mozilla::dom { @@ -41,8 +44,14 @@ class SpeculationRules final { void FindMatchingLinks(nsTArray& aLinks); + void HoverContentChanged(nsIContent* aContent); + private: - virtual ~SpeculationRules() = default; + virtual ~SpeculationRules(); + + // The innermost inclusive flat tree ancestor of aContent that is a link this + // document is tracking, or nullptr if there is none. + Element* FindInterestedLink(nsIContent* aContent) const; // https://html.spec.whatwg.org/#inner-consider-speculative-loads-steps // Step 7, for those candidate groups that the user's behaviour has shown to @@ -52,6 +61,9 @@ class SpeculationRules final { // collected from every candidate the user's behaviour justifies. void EnactCandidates(nsIURI* aURL, Eagerness aTriggerLevel); + void CancelHoverTimer(); + static void HoverTimerFired(nsITimer* aTimer, void* aClosure); + RefPtr mDocument; // https://html.spec.whatwg.org/#document-sr-sets @@ -73,6 +85,11 @@ class SpeculationRules final { // InnerConsiderLoads. Immediate groups have already been enacted; the rest // are kept so a later signal of user interest can enact them. nsTArray mCandidateGroups; + + // The link the hover timer is waiting on. Owning, as nothing else keeps the + // element alive for the duration of the timer. + RefPtr mHoverLink; + nsCOMPtr mHoverTimer; }; } // namespace mozilla::dom diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp index da9716667942..09e584825242 100644 --- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp @@ -72,6 +72,7 @@ #include "mozilla/dom/PopoverData.h" #include "mozilla/dom/Record.h" #include "mozilla/dom/Selection.h" +#include "mozilla/dom/SpeculationRules.h" #include "mozilla/dom/UIEvent.h" #include "mozilla/dom/UIEventBinding.h" #include "mozilla/dom/UserActivation.h" @@ -6920,6 +6921,7 @@ bool EventStateManager::SetContentState(nsIContent* aContent, if (newHover != mHoverContent) { notifyContent1 = newHover; notifyContent2 = mHoverContent; + NotifySpeculationRulesOfHover(newHover); mHoverContent = newHover; } } @@ -6984,6 +6986,20 @@ bool EventStateManager::SetContentState(nsIContent* aContent, return true; } +// Hovering a link is a signal of user interest that can enact a speculation +// rules prefetch candidate. This is notified on hover chain changes rather than +// from mouseover/mouseout so that moving the cursor between the children of a +// link doesn't read as leaving and re-entering the link itself. +void EventStateManager::NotifySpeculationRulesOfHover(nsIContent* aNewHover) { + nsIContent* content = aNewHover ? aNewHover : mHoverContent.get(); + if (!content) { + return; + } + if (auto* speculationRules = content->OwnerDoc()->GetSpeculationRules()) { + speculationRules->HoverContentChanged(aNewHover); + } +} + void EventStateManager::RemoveNodeFromChainIfNeeded(ElementState aState, nsIContent* aContentRemoved, bool aNotify) { diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h index 3aab86bc7153..92b8aa74d679 100644 --- a/dom/events/EventStateManager.h +++ b/dom/events/EventStateManager.h @@ -1337,6 +1337,11 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver { void RemoveNodeFromChainIfNeeded(ElementState aState, nsIContent* aContentRemoved, bool aNotify); + // Tells the hovered document's speculation rules that the deepest hovered + // node is about to become aNewHover, so that it can start or cancel the + // hover delay for a moderate eagerness prefetch. + void NotifySpeculationRulesOfHover(nsIContent* aNewHover); + [[nodiscard]] bool IsEventOutsideDragThreshold( const WidgetInputEvent& aEvent) const; diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 377e8342cfab..c83b1cbeca31 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -4934,6 +4934,13 @@ value: true mirror: always +# How long (ms) the cursor has to rest on a link before a "moderate" eagerness +# prefetch candidate matching it is enacted. +- name: dom.speculation_rules.moderate_hover_delay_ms + type: RelaxedAtomicUint32 + value: 200 + mirror: always + - name: dom.securecontext.allowlist_onions type: bool value: false