Bug 2061013 - Part 3: Fire prefetches for moderate eagerness speculation rules on hover. r=jjaschke

Differential Revision: https://phabricator.services.mozilla.com/D321440
This commit is contained in:
Adam Vandolder
2026-09-11 22:30:53 +00:00
committed by avandolder@mozilla.com
parent 961ce625e0
commit 5ef9925313
5 changed files with 121 additions and 1 deletions
+75
View File
@@ -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<SpeculationRuleSet> aRuleSet) {
@@ -260,4 +269,70 @@ void SpeculationRules::FindMatchingLinks(nsTArray<const Element*>& 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<Element> 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<SpeculationRules*>(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<Element> link = speculationRules->mHoverLink;
if (!link || !link->IsInComposedDoc()) {
return;
}
nsCOMPtr<nsIURI> 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
+18 -1
View File
@@ -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<const Element*>& 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<Document> 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<PrefetchCandidate> mCandidateGroups;
// The link the hover timer is waiting on. Owning, as nothing else keeps the
// element alive for the duration of the timer.
RefPtr<Element> mHoverLink;
nsCOMPtr<nsITimer> mHoverTimer;
};
} // namespace mozilla::dom
+16
View File
@@ -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) {
+5
View File
@@ -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;
+7
View File
@@ -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