blob: 524d106ad899a00302744b858d09c469b63823b4 [file] [log] [blame]
// Copyright 2016 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 "chrome/browser/offline_pages/prerendering_loader.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/network_change_notifier.h"
#include "ui/gfx/geometry/size.h"
namespace offline_pages {
// Classifies the appropriate RequestStatus for for the given prerender
// FinalStatus.
Offliner::RequestStatus ClassifyFinalStatus(
prerender::FinalStatus final_status) {
switch (final_status) {
// Identify aborted/canceled operations.
case prerender::FINAL_STATUS_CANCELLED:
// TODO(dougarnett): Reconsider if/when get better granularity (642768)
case prerender::FINAL_STATUS_UNSUPPORTED_SCHEME:
return Offliner::LOADING_CANCELED;
// Identify non-retryable failures. These are a hard type failures
// associated with the page and so are expected to occur again if retried.
case prerender::FINAL_STATUS_SAFE_BROWSING:
case prerender::FINAL_STATUS_CREATING_AUDIO_STREAM:
case prerender::FINAL_STATUS_JAVASCRIPT_ALERT:
case prerender::FINAL_STATUS_CREATE_NEW_WINDOW:
case prerender::FINAL_STATUS_INVALID_HTTP_METHOD:
case prerender::FINAL_STATUS_OPEN_URL:
return Offliner::RequestStatus::LOADING_FAILED_NO_RETRY;
// Identify failures that indicate we should stop further processing
// for now. These may be current resource issues or app closing.
case prerender::FINAL_STATUS_MEMORY_LIMIT_EXCEEDED:
case prerender::FINAL_STATUS_RATE_LIMIT_EXCEEDED:
case prerender::FINAL_STATUS_RENDERER_CRASHED:
case prerender::FINAL_STATUS_TOO_MANY_PROCESSES:
case prerender::FINAL_STATUS_TIMED_OUT:
case prerender::FINAL_STATUS_APP_TERMINATING:
case prerender::FINAL_STATUS_PROFILE_DESTROYED:
return Offliner::RequestStatus::LOADING_FAILED_NO_NEXT;
// Otherwise, assume retryable failure.
case prerender::FINAL_STATUS_NEW_NAVIGATION_ENTRY:
case prerender::FINAL_STATUS_CACHE_OR_HISTORY_CLEARED:
case prerender::FINAL_STATUS_SSL_ERROR:
case prerender::FINAL_STATUS_SSL_CLIENT_CERTIFICATE_REQUESTED:
case prerender::FINAL_STATUS_WINDOW_PRINT:
default:
return Offliner::RequestStatus::LOADING_FAILED;
}
}
PrerenderingLoader::PrerenderingLoader(content::BrowserContext* browser_context)
: state_(State::IDLE),
snapshot_controller_(nullptr),
browser_context_(browser_context),
is_lowbar_met_(false) {
adapter_.reset(new PrerenderAdapter(this));
}
PrerenderingLoader::~PrerenderingLoader() {
CancelPrerender();
}
void PrerenderingLoader::MarkLoadStartTime() {
load_start_time_ = base::TimeTicks::Now();
}
void PrerenderingLoader::AddLoadingSignal(const char* signal_name) {
base::TimeTicks current_time = base::TimeTicks::Now();
base::TimeDelta delay_so_far = current_time - load_start_time_;
double delay = delay_so_far.InMilliseconds();
signal_data_.SetDouble(signal_name, delay);
}
bool PrerenderingLoader::LoadPage(const GURL& url,
const LoadPageCallback& load_done_callback,
const ProgressCallback& progress_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!IsIdle()) {
DVLOG(1)
<< "WARNING: Existing request in progress or waiting for StopLoading()";
return false;
}
// Add this signal to signal_data_.
MarkLoadStartTime();
// Create a WebContents instance to define and hold a SessionStorageNamespace
// for this load request.
DCHECK(!session_contents_.get());
std::unique_ptr<content::WebContents> new_web_contents(
content::WebContents::Create(
content::WebContents::CreateParams(browser_context_)));
content::SessionStorageNamespace* sessionStorageNamespace =
new_web_contents->GetController().GetDefaultSessionStorageNamespace();
gfx::Size renderWindowSize = new_web_contents->GetContainerBounds().size();
bool accepted = adapter_->StartPrerender(
browser_context_, url, sessionStorageNamespace, renderWindowSize);
if (!accepted)
return false;
DCHECK(adapter_->IsActive());
snapshot_controller_ = SnapshotController::CreateForBackgroundOfflining(
base::ThreadTaskRunnerHandle::Get(), this, false);
load_done_callback_ = load_done_callback;
progress_callback_ = progress_callback;
session_contents_.swap(new_web_contents);
state_ = State::LOADING;
return true;
}
void PrerenderingLoader::StopLoading() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CancelPrerender();
}
bool PrerenderingLoader::IsIdle() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return state_ == State::IDLE;
}
bool PrerenderingLoader::IsLoaded() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return state_ == State::LOADED;
}
void PrerenderingLoader::SetAdapterForTesting(
std::unique_ptr<PrerenderAdapter> prerender_adapter) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
adapter_ = std::move(prerender_adapter);
}
void PrerenderingLoader::OnPrerenderStopLoading() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!IsIdle());
DCHECK(adapter_->GetWebContents());
// Inform SnapshotController of OnLoad event so it can determine
// when to consider it really LOADED.
snapshot_controller_->DocumentOnLoadCompletedInMainFrame();
// Add this signal to signal_data_.
AddLoadingSignal("OnLoad");
}
void PrerenderingLoader::OnPrerenderDomContentLoaded() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!IsIdle());
if (!adapter_->GetWebContents()) {
// Without a WebContents object at this point, we are done.
HandleLoadingStopped();
} else {
is_lowbar_met_ = true;
// Inform SnapshotController of DomContentLoaded event so it can
// determine when to consider it really LOADED (e.g., some multiple
// second delay from this event).
snapshot_controller_->DocumentAvailableInMainFrame();
// Add this signal to signal_data_.
AddLoadingSignal("OnDomContentLoaded");
}
}
void PrerenderingLoader::OnPrerenderStop() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
HandleLoadingStopped();
}
void PrerenderingLoader::OnPrerenderNetworkBytesChanged(int64_t bytes) {
if (state_ == State::LOADING)
progress_callback_.Run(bytes);
}
void PrerenderingLoader::StartSnapshot() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Add this signal to signal_data_.
AddLoadingSignal("Snapshotting");
HandleLoadEvent();
}
void PrerenderingLoader::RunRenovations() {
snapshot_controller_->RenovationsCompleted();
}
bool PrerenderingLoader::IsLowbarMet() {
return is_lowbar_met_;
}
void PrerenderingLoader::HandleLoadEvent() {
// If still loading, check if the load succeeded or not, then update
// the internal state (LOADED for success or IDLE for failure) and post
// callback.
// Note: it is possible to receive a load event (e.g., if timeout-based)
// after the request has completed via another path (e.g., canceled) so
// the Loader may be idle at this point.
if (IsIdle() || IsLoaded())
return;
content::WebContents* web_contents = adapter_->GetWebContents();
if (web_contents) {
state_ = State::LOADED;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(load_done_callback_,
Offliner::RequestStatus::LOADED, web_contents));
} else {
// No WebContents means that the load failed (and it stopped).
HandleLoadingStopped();
}
}
void PrerenderingLoader::HandleLoadingStopped() {
// Loading has stopped so unless the Loader has already transitioned to the
// idle state, clean up the previous request state, transition to the idle
// state, and post callback.
// Note: it is possible to receive some asynchronous stopped indication after
// the request has completed/stopped via another path so the Loader may be
// idle at this point.
if (IsIdle())
return;
Offliner::RequestStatus request_status;
if (adapter_->IsActive()) {
if (IsLoaded()) {
// If page already loaded, then prerender is telling us that it is
// canceling (and we should stop using the loaded WebContents).
request_status = Offliner::RequestStatus::LOADING_CANCELED;
} else {
// Otherwise, get the available FinalStatus to classify the outcome.
prerender::FinalStatus final_status = adapter_->GetFinalStatus();
DVLOG(1) << "Load failed: " << final_status;
request_status = ClassifyFinalStatus(final_status);
// Loss of network connection can show up as unsupported scheme per
// a redirect to a special data URL is used to navigate to error page.
// Capture the current connectivity here in case we can leverage that
// to differentiate how to treat it.
if (final_status == prerender::FINAL_STATUS_UNSUPPORTED_SCHEME) {
UMA_HISTOGRAM_ENUMERATION(
"OfflinePages.Background.UnsupportedScheme.ConnectionType",
net::NetworkChangeNotifier::GetConnectionType(),
net::NetworkChangeNotifier::ConnectionType::CONNECTION_LAST + 1);
}
}
// Now clean up the active prerendering operation detail.
adapter_->DestroyActive();
} else {
// No access to FinalStatus so classify as retryable failure.
request_status = Offliner::RequestStatus::LOADING_FAILED;
}
snapshot_controller_.reset(nullptr);
session_contents_.reset(nullptr);
state_ = State::IDLE;
is_lowbar_met_ = false;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(load_done_callback_, request_status, nullptr));
}
void PrerenderingLoader::CancelPrerender() {
if (adapter_->IsActive()) {
adapter_->DestroyActive();
}
snapshot_controller_.reset(nullptr);
session_contents_.reset(nullptr);
state_ = State::IDLE;
is_lowbar_met_ = false;
}
} // namespace offline_pages