blob: e6126f52ee194724eb2c778ec159d419215dbaa9 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/css/remote_font_face_source.h"
#include "base/metrics/histogram_functions.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_effective_connection_type.h"
#include "third_party/blink/renderer/core/css/css_custom_font_data.h"
#include "third_party/blink/renderer/core/css/css_font_face.h"
#include "third_party/blink/renderer/core/css/font_face_set_document.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/platform/fonts/font_cache.h"
#include "third_party/blink/renderer/platform/fonts/font_custom_platform_data.h"
#include "third_party/blink/renderer/platform/fonts/font_description.h"
#include "third_party/blink/renderer/platform/fonts/font_selector.h"
#include "third_party/blink/renderer/platform/fonts/simple_font_data.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_load_priority.h"
#include "third_party/blink/renderer/platform/network/network_state_notifier.h"
namespace blink {
bool RemoteFontFaceSource::NeedsInterventionToAlignWithLCPGoal() const {
DCHECK_EQ(display_, kFontDisplayAuto);
if (!base::FeatureList::IsEnabled(
features::kAlignFontDisplayAutoTimeoutWithLCPGoal)) {
return false;
}
if (!GetDocument() ||
!FontFaceSetDocument::From(*GetDocument())->HasReachedLCPLimit()) {
return false;
}
// If a 'font-display: auto' font hasn't finished loading by the LCP limit, it
// should enter the swap or failure period immediately, so that it doesn't
// become a source of bad LCP. The only exception is when the font is
// immediately available from the memory cache, in which case it can be used
// right away without any latency.
return !IsLoaded() ||
(!FinishedFromMemoryCache() && !finished_before_lcp_limit_);
}
RemoteFontFaceSource::DisplayPeriod
RemoteFontFaceSource::ComputeFontDisplayAutoPeriod() const {
DCHECK_EQ(display_, kFontDisplayAuto);
if (NeedsInterventionToAlignWithLCPGoal()) {
using Mode = features::AlignFontDisplayAutoTimeoutWithLCPGoalMode;
Mode mode =
features::kAlignFontDisplayAutoTimeoutWithLCPGoalModeParam.Get();
if (mode == Mode::kToSwapPeriod)
return kSwapPeriod;
DCHECK_EQ(Mode::kToFailurePeriod, mode);
if (custom_font_data_ && !custom_font_data_->MayBeIconFont())
return kFailurePeriod;
return kSwapPeriod;
}
if (is_intervention_triggered_)
return kSwapPeriod;
switch (phase_) {
case kNoLimitExceeded:
case kShortLimitExceeded:
return kBlockPeriod;
case kLongLimitExceeded:
return kSwapPeriod;
}
}
RemoteFontFaceSource::DisplayPeriod RemoteFontFaceSource::ComputePeriod()
const {
switch (display_) {
case kFontDisplayAuto:
return ComputeFontDisplayAutoPeriod();
case kFontDisplayBlock:
switch (phase_) {
case kNoLimitExceeded:
case kShortLimitExceeded:
return kBlockPeriod;
case kLongLimitExceeded:
return kSwapPeriod;
}
case kFontDisplaySwap:
return kSwapPeriod;
case kFontDisplayFallback:
switch (phase_) {
case kNoLimitExceeded:
return kBlockPeriod;
case kShortLimitExceeded:
return kSwapPeriod;
case kLongLimitExceeded:
return kFailurePeriod;
}
case kFontDisplayOptional: {
const bool use_phase_value =
!base::FeatureList::IsEnabled(
features::kFontPreloadingDelaysRendering) ||
!GetDocument();
if (use_phase_value) {
switch (phase_) {
case kNoLimitExceeded:
return kBlockPeriod;
case kShortLimitExceeded:
case kLongLimitExceeded:
return kFailurePeriod;
}
}
// We simply skip the block period, as we should never render invisible
// fallback for 'font-display: optional'.
if (GetDocument()->GetFontPreloadManager().RenderingHasBegun()) {
if (FinishedFromMemoryCache() ||
finished_before_document_rendering_begin_ ||
!has_been_requested_while_pending_)
return kSwapPeriod;
return kFailurePeriod;
}
return kSwapPeriod;
}
case kFontDisplayEnumMax:
NOTREACHED();
break;
}
NOTREACHED();
return kSwapPeriod;
}
RemoteFontFaceSource::RemoteFontFaceSource(CSSFontFace* css_font_face,
FontSelector* font_selector,
FontDisplay display)
: face_(css_font_face),
font_selector_(font_selector),
// No need to report the violation here since the font is not loaded yet
display_(
GetFontDisplayWithFeaturePolicyCheck(display,
font_selector,
ReportOptions::kDoNotReport)),
phase_(kNoLimitExceeded),
is_intervention_triggered_(ShouldTriggerWebFontsIntervention()),
finished_before_document_rendering_begin_(false),
has_been_requested_while_pending_(false),
finished_before_lcp_limit_(false) {
DCHECK(face_);
period_ = ComputePeriod();
}
RemoteFontFaceSource::~RemoteFontFaceSource() = default;
Document* RemoteFontFaceSource::GetDocument() const {
auto* window =
DynamicTo<LocalDOMWindow>(font_selector_->GetExecutionContext());
return window ? window->document() : nullptr;
}
void RemoteFontFaceSource::Dispose() {
ClearResource();
PruneTable();
}
bool RemoteFontFaceSource::IsLoading() const {
return GetResource() && GetResource()->IsLoading();
}
bool RemoteFontFaceSource::IsLoaded() const {
return !GetResource();
}
bool RemoteFontFaceSource::IsValid() const {
return GetResource() || custom_font_data_;
}
void RemoteFontFaceSource::NotifyFinished(Resource* resource) {
ExecutionContext* execution_context = font_selector_->GetExecutionContext();
if (!execution_context)
return;
// Prevent promise rejection while shutting down the document.
// See crbug.com/960290
auto* window = DynamicTo<LocalDOMWindow>(execution_context);
if (window && window->document()->IsDetached())
return;
FontResource* font = ToFontResource(resource);
histograms_.RecordRemoteFont(font);
custom_font_data_ = font->GetCustomFontData();
url_ = resource->Url().GetString();
// FIXME: Provide more useful message such as OTS rejection reason.
// See crbug.com/97467
if (font->GetStatus() == ResourceStatus::kDecodeError) {
execution_context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kOther,
mojom::ConsoleMessageLevel::kWarning,
"Failed to decode downloaded font: " + font->Url().ElidedString()));
if (font->OtsParsingMessage().length() > 1) {
execution_context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kOther,
mojom::ConsoleMessageLevel::kWarning,
"OTS parsing error: " + font->OtsParsingMessage()));
}
}
ClearResource();
PruneTable();
if (GetDocument()) {
if (!GetDocument()->GetFontPreloadManager().RenderingHasBegun())
finished_before_document_rendering_begin_ = true;
if (!FontFaceSetDocument::From(*GetDocument())->HasReachedLCPLimit())
finished_before_lcp_limit_ = true;
}
if (FinishedFromMemoryCache())
period_ = kNotApplicablePeriod;
else
UpdatePeriod();
if (face_->FontLoaded(this)) {
font_selector_->FontFaceInvalidated(
FontInvalidationReason::kFontFaceLoaded);
const scoped_refptr<FontCustomPlatformData> customFontData =
font->GetCustomFontData();
if (customFontData) {
probe::FontsUpdated(execution_context, face_->GetFontFace(),
resource->Url().GetString(), customFontData.get());
}
}
}
void RemoteFontFaceSource::FontLoadShortLimitExceeded(FontResource*) {
if (IsLoaded())
return;
phase_ = kShortLimitExceeded;
UpdatePeriod();
}
void RemoteFontFaceSource::FontLoadLongLimitExceeded(FontResource*) {
if (IsLoaded())
return;
phase_ = kLongLimitExceeded;
UpdatePeriod();
histograms_.LongLimitExceeded();
}
void RemoteFontFaceSource::SetDisplay(FontDisplay display) {
// TODO(ksakamoto): If the font is loaded and in the failure period,
// changing it to block or swap period should update the font rendering
// using the loaded font.
if (IsLoaded())
return;
display_ = GetFontDisplayWithFeaturePolicyCheck(
display, font_selector_, ReportOptions::kReportOnFailure);
UpdatePeriod();
}
bool RemoteFontFaceSource::UpdatePeriod() {
DisplayPeriod new_period = ComputePeriod();
bool changed = new_period != period_;
// Fallback font is invisible iff the font is loading and in the block period.
// Invalidate the font if its fallback visibility has changed.
if (IsLoading() && period_ != new_period &&
(period_ == kBlockPeriod || new_period == kBlockPeriod)) {
PruneTable();
if (face_->FallbackVisibilityChanged(this)) {
font_selector_->FontFaceInvalidated(
FontInvalidationReason::kGeneralInvalidation);
}
histograms_.RecordFallbackTime();
}
period_ = new_period;
return changed;
}
FontDisplay RemoteFontFaceSource::GetFontDisplayWithFeaturePolicyCheck(
FontDisplay display,
const FontSelector* font_selector,
ReportOptions report_option) const {
ExecutionContext* context = font_selector->GetExecutionContext();
if (display != kFontDisplayFallback && display != kFontDisplayOptional &&
context && context->IsWindow() &&
!context->IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kFontDisplay, report_option)) {
return kFontDisplayOptional;
}
return display;
}
bool RemoteFontFaceSource::ShouldTriggerWebFontsIntervention() {
if (!IsA<LocalDOMWindow>(font_selector_->GetExecutionContext()))
return false;
WebEffectiveConnectionType connection_type =
GetNetworkStateNotifier().EffectiveType();
bool network_is_slow =
WebEffectiveConnectionType::kTypeOffline <= connection_type &&
connection_type <= WebEffectiveConnectionType::kType3G;
return network_is_slow && display_ == kFontDisplayAuto;
}
bool RemoteFontFaceSource::IsLowPriorityLoadingAllowedForRemoteFont() const {
return is_intervention_triggered_;
}
scoped_refptr<SimpleFontData> RemoteFontFaceSource::CreateFontData(
const FontDescription& font_description,
const FontSelectionCapabilities& font_selection_capabilities) {
if (period_ == kFailurePeriod || !IsValid())
return nullptr;
if (!IsLoaded())
return CreateLoadingFallbackFontData(font_description);
DCHECK(custom_font_data_);
histograms_.RecordFallbackTime();
return SimpleFontData::Create(
custom_font_data_->GetFontPlatformData(
font_description.EffectiveFontSize(),
font_description.IsSyntheticBold(),
font_description.IsSyntheticItalic(),
font_description.GetFontSelectionRequest(),
font_selection_capabilities, font_description.FontOpticalSizing(),
font_description.Orientation(), font_description.VariationSettings()),
CustomFontData::Create());
}
scoped_refptr<SimpleFontData>
RemoteFontFaceSource::CreateLoadingFallbackFontData(
const FontDescription& font_description) {
// This temporary font is not retained and should not be returned.
FontCachePurgePreventer font_cache_purge_preventer;
scoped_refptr<SimpleFontData> temporary_font =
FontCache::GetFontCache()->GetLastResortFallbackFont(font_description,
kDoNotRetain);
if (!temporary_font) {
NOTREACHED();
return nullptr;
}
scoped_refptr<CSSCustomFontData> css_font_data = CSSCustomFontData::Create(
this, period_ == kBlockPeriod ? CSSCustomFontData::kInvisibleFallback
: CSSCustomFontData::kVisibleFallback);
has_been_requested_while_pending_ = true;
return SimpleFontData::Create(temporary_font->PlatformData(), css_font_data);
}
void RemoteFontFaceSource::BeginLoadIfNeeded() {
if (IsLoaded() || !font_selector_->GetExecutionContext())
return;
DCHECK(GetResource());
SetDisplay(face_->GetFontFace()->GetFontDisplay());
FontResource* font = ToFontResource(GetResource());
if (font->StillNeedsLoad()) {
if (font->IsLowPriorityLoadingAllowedForRemoteFont()) {
font_selector_->GetExecutionContext()->AddConsoleMessage(
MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kIntervention,
mojom::ConsoleMessageLevel::kInfo,
"Slow network is detected. See "
"https://www.chromestatus.com/feature/5636954674692096 for more "
"details. Fallback font will be used while loading: " +
font->Url().ElidedString()));
// Set the loading priority to VeryLow only when all other clients agreed
// that this font is not required for painting the text.
font->DidChangePriority(ResourceLoadPriority::kVeryLow, 0);
}
if (font_selector_->GetExecutionContext()->Fetcher()->StartLoad(font))
histograms_.LoadStarted();
}
// Start the timers upon the first load request from RemoteFontFaceSource.
// Note that <link rel=preload> may have initiated loading without kicking
// off the timers.
font->StartLoadLimitTimersIfNecessary(
font_selector_->GetExecutionContext()
->GetTaskRunner(TaskType::kInternalLoading)
.get());
face_->DidBeginLoad();
}
void RemoteFontFaceSource::Trace(Visitor* visitor) const {
visitor->Trace(face_);
visitor->Trace(font_selector_);
CSSFontFaceSource::Trace(visitor);
FontResourceClient::Trace(visitor);
}
void RemoteFontFaceSource::FontLoadHistograms::LoadStarted() {
if (load_start_time_.is_null())
load_start_time_ = base::TimeTicks::Now();
}
void RemoteFontFaceSource::FontLoadHistograms::FallbackFontPainted(
DisplayPeriod period) {
if (period == kBlockPeriod && blank_paint_time_.is_null()) {
blank_paint_time_ = base::TimeTicks::Now();
blank_paint_time_recorded_ = false;
}
}
void RemoteFontFaceSource::FontLoadHistograms::LongLimitExceeded() {
is_long_limit_exceeded_ = true;
MaySetDataSource(kFromNetwork);
}
void RemoteFontFaceSource::FontLoadHistograms::RecordFallbackTime() {
if (blank_paint_time_.is_null() || blank_paint_time_recorded_)
return;
// TODO(https://crbug.com/1049257): This time should be recorded using a more
// appropriate UMA helper, since >1% of samples are in the overflow bucket.
base::TimeDelta duration = base::TimeTicks::Now() - blank_paint_time_;
base::UmaHistogramTimes("WebFont.BlankTextShownTime", duration);
blank_paint_time_recorded_ = true;
}
void RemoteFontFaceSource::FontLoadHistograms::RecordRemoteFont(
const FontResource* font) {
MaySetDataSource(DataSourceForLoadFinish(font));
base::UmaHistogramEnumeration("WebFont.CacheHit", DataSourceMetricsValue());
if (data_source_ == kFromDiskCache || data_source_ == kFromNetwork) {
DCHECK(!load_start_time_.is_null());
RecordLoadTimeHistogram(font, base::TimeTicks::Now() - load_start_time_);
}
}
void RemoteFontFaceSource::FontLoadHistograms::MaySetDataSource(
DataSource data_source) {
if (data_source_ != kFromUnknown)
return;
// Classify as memory cache hit if |load_start_time_| is not set, i.e.
// this RemoteFontFaceSource instance didn't trigger FontResource
// loading.
if (load_start_time_.is_null())
data_source_ = kFromMemoryCache;
else
data_source_ = data_source;
}
void RemoteFontFaceSource::FontLoadHistograms::RecordLoadTimeHistogram(
const FontResource* font,
base::TimeDelta delta) {
CHECK_NE(kFromUnknown, data_source_);
// TODO(https://crbug.com/1049257): These times should be recorded using a
// more appropriate UMA helper, since >1% of samples are in the overflow
// bucket.
if (font->ErrorOccurred()) {
base::UmaHistogramTimes("WebFont.DownloadTime.LoadError", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.LoadError",
delta);
}
return;
}
size_t size = font->EncodedSize();
if (size < 10 * 1024) {
base::UmaHistogramTimes("WebFont.DownloadTime.0.Under10KB", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.0.Under10KB",
delta);
}
return;
}
if (size < 50 * 1024) {
base::UmaHistogramTimes("WebFont.DownloadTime.1.10KBTo50KB", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.1.10KBTo50KB",
delta);
}
return;
}
if (size < 100 * 1024) {
base::UmaHistogramTimes("WebFont.DownloadTime.2.50KBTo100KB", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.2.50KBTo100KB",
delta);
}
return;
}
if (size < 1024 * 1024) {
base::UmaHistogramTimes("WebFont.DownloadTime.3.100KBTo1MB", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.3.100KBTo1MB",
delta);
}
return;
}
base::UmaHistogramTimes("WebFont.DownloadTime.4.Over1MB", delta);
if (data_source_ == kFromNetwork) {
base::UmaHistogramTimes("WebFont.MissedCache.DownloadTime.4.Over1MB",
delta);
}
}
RemoteFontFaceSource::FontLoadHistograms::CacheHitMetrics
RemoteFontFaceSource::FontLoadHistograms::DataSourceMetricsValue() {
switch (data_source_) {
case kFromDataURL:
return CacheHitMetrics::kDataUrl;
case kFromMemoryCache:
return CacheHitMetrics::kMemoryHit;
case kFromDiskCache:
return CacheHitMetrics::kDiskHit;
case kFromNetwork:
return CacheHitMetrics::kMiss;
case kFromUnknown:
return CacheHitMetrics::kMiss;
}
NOTREACHED();
return CacheHitMetrics::kMiss;
}
} // namespace blink