gecko-dev/uriloader/preload/PreloadService.cpp
Frédéric Wang b005b82248 Bug 1872657 - Add fetchpriority support for <link rel=preload as=image>. r=valentin,smaug,manuel
This patch adds fetchpriority support for `<link rel=preload as=image>`
and equivalent HTTP Link header. The fetchpriority value is passed from
where the link is parsed down to `NewImageChannel` where the priority
is initially set. Currently, the default equals PRIORITY_LOW, but is
decreased a bit if LOAD_BACKGROUND flag is set (this is always the case
for link preload images, see `imgLoader::LoadImage`). Later, the
priority can be increased again depending on the category (see
`imgRequest::BoostPriority`).

In order to minimize the changes, the new calculation is to keep the
initial setting to PRIORITY_LOW, adjust it using a new
`network.fetchpriority.adjustments.*` preference depending on the
fetchpriority attributes, and then preserve further adjustments for
LOAD_BACKGROUND and `BoostPriority`.

For the default value `fetchpriority=auto`, there is no adjustment
i.e. we continue to start with PRIORITY_LOW. `fetchpriority=low/high`
are respectively mapped to PRIORITY_LOW/PRIORITY_HIGH which is simple
and consistent with the "Image" cases from Google's web.dev article
https://web.dev/articles/fetch-priority. These values could of course
be revised in the future after more experiments.

This change is covered by the following tests below. The expectations
is modified to match what is described above (i.e. map to PRIORITY_LOW
or PRIORITY_HIGH with adjustment due to LOAD_BACKGROUND):
- `link-initial-preload-image.h2.html`
- `link-dynamic-preload-image.h2.html`
- `kPipeHeaderPreloadImageLinks`

Based on a patch by Mirko Brodesser (mbrodesser@igalia.com)

Differential Revision: https://phabricator.services.mozilla.com/D197493
2024-02-27 06:33:48 +00:00

356 lines
13 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "PreloadService.h"
#include "FetchPreloader.h"
#include "PreloaderBase.h"
#include "mozilla/Assertions.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/Maybe.h"
#include "mozilla/dom/FetchPriority.h"
#include "mozilla/dom/HTMLLinkElement.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/Encoding.h"
#include "mozilla/FontPreloader.h"
#include "mozilla/StaticPrefs_network.h"
#include "nsGenericHTMLElement.h"
#include "nsNetUtil.h"
namespace mozilla {
using namespace dom;
static LazyLogModule sPreloadServiceLog{"PreloadService"};
PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {}
PreloadService::~PreloadService() = default;
bool PreloadService::RegisterPreload(const PreloadHashKey& aKey,
PreloaderBase* aPreload) {
return mPreloads.WithEntryHandle(aKey, [&](auto&& lookup) {
if (lookup) {
lookup.Data() = aPreload;
return true;
}
lookup.Insert(aPreload);
return false;
});
}
void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) {
mPreloads.Remove(aKey);
}
void PreloadService::ClearAllPreloads() { mPreloads.Clear(); }
bool PreloadService::PreloadExists(const PreloadHashKey& aKey) {
return mPreloads.Contains(aKey);
}
already_AddRefed<PreloaderBase> PreloadService::LookupPreload(
const PreloadHashKey& aKey) const {
return mPreloads.Get(aKey);
}
already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) {
nsIURI* base = BaseURIForPreload();
auto encoding = mDocument->GetDocumentCharacterSet();
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base);
if (NS_FAILED(rv)) {
return nullptr;
}
return uri.forget();
}
already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) {
if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
MOZ_ASSERT_UNREACHABLE("Caller should check");
return nullptr;
}
nsAutoString as, charset, crossOrigin, integrity, referrerPolicy,
fetchPriority, rel, srcset, sizes, type, url;
nsCOMPtr<nsIURI> uri = aLinkElement->GetURI();
aLinkElement->GetCharset(charset);
aLinkElement->GetImageSrcset(srcset);
aLinkElement->GetImageSizes(sizes);
aLinkElement->GetHref(url);
aLinkElement->GetCrossOrigin(crossOrigin);
aLinkElement->GetIntegrity(integrity);
aLinkElement->GetReferrerPolicy(referrerPolicy);
aLinkElement->GetFetchPriority(fetchPriority);
aLinkElement->GetRel(rel);
nsAutoString nonce;
if (nsString* cspNonce =
static_cast<nsString*>(aLinkElement->GetProperty(nsGkAtoms::nonce))) {
nonce = *cspNonce;
}
if (rel.LowerCaseEqualsASCII("modulepreload")) {
as = u"script"_ns;
type = u"module"_ns;
} else {
aLinkElement->GetAs(as);
aLinkElement->GetType(type);
}
auto result = PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset,
srcset, sizes, nonce, integrity, crossOrigin,
referrerPolicy, fetchPriority,
/* aFromHeader = */ false, 0);
if (!result.mPreloader) {
NotifyNodeEvent(aLinkElement, result.mAlreadyComplete);
return nullptr;
}
result.mPreloader->AddLinkPreloadNode(aLinkElement);
return result.mPreloader.forget();
}
void PreloadService::PreloadLinkHeader(
nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
const nsAString& aAs, const nsAString& aType, const nsAString& aNonce,
const nsAString& aIntegrity, const nsAString& aSrcset,
const nsAString& aSizes, const nsAString& aCORS,
const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId,
const nsAString& aFetchPriority) {
if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
MOZ_ASSERT_UNREACHABLE("Caller should check");
return;
}
PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset,
aSizes, aNonce, aIntegrity, aCORS, aReferrerPolicy,
aFetchPriority,
/* aFromHeader = */ true, aEarlyHintPreloaderId);
}
// The mapping is specified as implementation-defined, see step 15 of
// <https://fetch.spec.whatwg.org/#concept-fetch>.
// See corresponding preferences in StaticPrefList.yaml for more context.
class SupportsPriorityValueFor {
public:
static int32_t LinkRelPreloadFont(const FetchPriority aFetchPriority) {
int32_t priorityValue = nsISupportsPriority::PRIORITY_HIGH;
if (!StaticPrefs::network_fetchpriority_enabled()) {
return priorityValue;
}
return priorityValue +
FETCH_PRIORITY_ADJUSTMENT_FOR(link_preload_font, aFetchPriority);
}
static int32_t LinkRelPreloadFetch(const FetchPriority aFetchPriority) {
int32_t priorityValue = nsISupportsPriority::PRIORITY_NORMAL;
if (!StaticPrefs::network_fetchpriority_enabled()) {
return priorityValue;
}
return priorityValue +
FETCH_PRIORITY_ADJUSTMENT_FOR(link_preload_fetch, aFetchPriority);
}
};
PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce(
nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
const nsAString& aAs, const nsAString& aType, const nsAString& aCharset,
const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aNonce,
const nsAString& aIntegrity, const nsAString& aCORS,
const nsAString& aReferrerPolicy, const nsAString& aFetchPriority,
bool aFromHeader, uint64_t aEarlyHintPreloaderId) {
if (!aURI) {
MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI");
return {nullptr, false};
}
bool isImgSet = false;
PreloadHashKey preloadKey;
nsCOMPtr<nsIURI> uri = aURI;
if (aAs.LowerCaseEqualsASCII("script")) {
preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType);
} else if (aAs.LowerCaseEqualsASCII("style")) {
preloadKey = PreloadHashKey::CreateAsStyle(
uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS),
css::eAuthorSheetFeatures /* see Loader::LoadSheet */);
} else if (aAs.LowerCaseEqualsASCII("image")) {
uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset,
aSizes, &isImgSet);
if (!uri) {
return {nullptr, false};
}
preloadKey = PreloadHashKey::CreateAsImage(
uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS));
} else if (aAs.LowerCaseEqualsASCII("font")) {
preloadKey = PreloadHashKey::CreateAsFont(
uri, dom::Element::StringToCORSMode(aCORS));
} else if (aAs.LowerCaseEqualsASCII("fetch")) {
preloadKey = PreloadHashKey::CreateAsFetch(
uri, dom::Element::StringToCORSMode(aCORS));
} else {
return {nullptr, false};
}
if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) {
return {std::move(preload), false};
}
if (aAs.LowerCaseEqualsASCII("script")) {
PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aNonce,
aFetchPriority, aIntegrity, true /* isInHead - TODO */,
aEarlyHintPreloaderId);
} else if (aAs.LowerCaseEqualsASCII("style")) {
auto status = mDocument->PreloadStyle(
aURI, Encoding::ForLabel(aCharset), aCORS,
PreloadReferrerPolicy(aReferrerPolicy), aNonce, aIntegrity,
aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader
: css::StylePreloadKind::FromLinkRelPreloadElement,
aEarlyHintPreloaderId, aFetchPriority);
switch (status) {
case dom::SheetPreloadStatus::AlreadyComplete:
return {nullptr, /* already_complete = */ true};
case dom::SheetPreloadStatus::Errored:
case dom::SheetPreloadStatus::InProgress:
break;
}
} else if (aAs.LowerCaseEqualsASCII("image")) {
PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet, aEarlyHintPreloaderId,
aFetchPriority);
} else if (aAs.LowerCaseEqualsASCII("font")) {
PreloadFont(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId,
aFetchPriority);
} else if (aAs.LowerCaseEqualsASCII("fetch")) {
PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId,
aFetchPriority);
}
RefPtr<PreloaderBase> preload = LookupPreload(preloadKey);
if (preload && aEarlyHintPreloaderId) {
preload->SetForEarlyHints();
}
return {preload, false};
}
void PreloadService::PreloadScript(
nsIURI* aURI, const nsAString& aType, const nsAString& aCharset,
const nsAString& aCrossOrigin, const nsAString& aReferrerPolicy,
const nsAString& aNonce, const nsAString& aFetchPriority,
const nsAString& aIntegrity, bool aScriptFromHead,
uint64_t aEarlyHintPreloaderId) {
mDocument->ScriptLoader()->PreloadURI(
aURI, aCharset, aType, aCrossOrigin, aNonce, aFetchPriority, aIntegrity,
aScriptFromHead, false, false, true,
PreloadReferrerPolicy(aReferrerPolicy), aEarlyHintPreloaderId);
}
void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aImageReferrerPolicy,
bool aIsImgSet,
uint64_t aEarlyHintPreloaderId,
const nsAString& aFetchPriority) {
mDocument->PreLoadImage(
aURI, aCrossOrigin, PreloadReferrerPolicy(aImageReferrerPolicy),
aIsImgSet, true, aEarlyHintPreloaderId, aFetchPriority);
}
void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy,
uint64_t aEarlyHintPreloaderId,
const nsAString& aFetchPriority) {
CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
auto key = PreloadHashKey::CreateAsFont(aURI, cors);
if (PreloadExists(key)) {
return;
}
const auto fetchPriority =
nsGenericHTMLElement::ToFetchPriority(aFetchPriority);
const auto supportsPriorityValue =
SupportsPriorityValueFor::LinkRelPreloadFont(fetchPriority);
LogPriorityMapping(sPreloadServiceLog, fetchPriority, supportsPriorityValue);
RefPtr<FontPreloader> preloader = new FontPreloader();
dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
aEarlyHintPreloaderId, supportsPriorityValue);
}
void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
const nsAString& aReferrerPolicy,
uint64_t aEarlyHintPreloaderId,
const nsAString& aFetchPriority) {
CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
auto key = PreloadHashKey::CreateAsFetch(aURI, cors);
if (PreloadExists(key)) {
return;
}
RefPtr<FetchPreloader> preloader = new FetchPreloader();
dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
const auto fetchPriority =
nsGenericHTMLElement::ToFetchPriority(aFetchPriority);
const int32_t supportsPriorityValue =
SupportsPriorityValueFor::LinkRelPreloadFetch(fetchPriority);
if (supportsPriorityValue) {
LogPriorityMapping(sPreloadServiceLog, fetchPriority,
supportsPriorityValue);
}
preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
aEarlyHintPreloaderId, supportsPriorityValue);
}
// static
void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) {
if (!aNode->IsInComposedDoc()) {
return;
}
// We don't dispatch synchronously since |node| might be in a DocGroup
// that we're not allowed to touch. (Our network request happens in the
// DocGroup of one of the mSources nodes--not necessarily this one).
RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);
dispatcher->RequireNodeInDocument();
dispatcher->PostDOMEvent();
}
dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy(
const nsAString& aReferrerPolicy) {
dom::ReferrerPolicy referrerPolicy =
dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
if (referrerPolicy == dom::ReferrerPolicy::_empty) {
referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy();
}
return referrerPolicy;
}
nsIURI* PreloadService::BaseURIForPreload() {
nsIURI* documentURI = mDocument->GetDocumentURI();
nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
return (documentURI == documentBaseURI)
? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI)
: documentBaseURI;
}
} // namespace mozilla