blob: 38ab6beb571f0a347dc9303c45c8218f47c095bf [file] [log] [blame]
// Copyright 2019 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 "components/paint_preview/browser/paint_preview_client.h"
#include <utility>
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/post_task.h"
#include "base/threading/scoped_blocking_call.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
namespace paint_preview {
namespace {
// Creates an old style id of Process ID || Routing ID. This should only be used
// for looking up the main frame's filler GUID in cases where only the
// RenderFrameHost is available (such as in RenderFrameDeleted()).
uint64_t MakeOldStyleId(content::RenderFrameHost* render_frame_host) {
return (static_cast<uint64_t>(render_frame_host->GetProcess()->GetID())
<< 32) |
render_frame_host->GetRoutingID();
}
// Converts gfx::Rect to its RectProto form.
void RectToRectProto(const gfx::Rect& rect, RectProto* proto) {
proto->set_x(rect.x());
proto->set_y(rect.y());
proto->set_width(rect.width());
proto->set_height(rect.height());
}
// Converts |response| into |proto|. Returns a list of the frame GUIDs
// referenced by the response.
std::vector<base::UnguessableToken>
PaintPreviewCaptureResponseToPaintPreviewFrameProto(
mojom::PaintPreviewCaptureResponsePtr response,
base::UnguessableToken frame_guid,
PaintPreviewFrameProto* proto) {
proto->set_embedding_token_high(frame_guid.GetHighForSerialization());
proto->set_embedding_token_low(frame_guid.GetLowForSerialization());
std::vector<base::UnguessableToken> frame_guids;
for (const auto& id_pair : response->content_id_to_embedding_token) {
auto* content_id_embedding_token_pair =
proto->add_content_id_to_embedding_tokens();
content_id_embedding_token_pair->set_content_id(id_pair.first);
content_id_embedding_token_pair->set_embedding_token_low(
id_pair.second.GetLowForSerialization());
content_id_embedding_token_pair->set_embedding_token_high(
id_pair.second.GetHighForSerialization());
frame_guids.push_back(id_pair.second);
}
for (const auto& link : response->links) {
auto* link_proto = proto->add_links();
link_proto->set_url(link->url.spec());
RectToRectProto(link->rect, link_proto->mutable_rect());
}
return frame_guids;
}
} // namespace
PaintPreviewClient::PaintPreviewParams::PaintPreviewParams() = default;
PaintPreviewClient::PaintPreviewParams::~PaintPreviewParams() = default;
PaintPreviewClient::PaintPreviewData::PaintPreviewData() = default;
PaintPreviewClient::PaintPreviewData::~PaintPreviewData() = default;
PaintPreviewClient::PaintPreviewData& PaintPreviewClient::PaintPreviewData::
operator=(PaintPreviewData&& rhs) noexcept = default;
PaintPreviewClient::PaintPreviewData::PaintPreviewData(
PaintPreviewData&& other) noexcept = default;
PaintPreviewClient::CreateResult::CreateResult(base::File file,
base::File::Error error)
: file(std::move(file)), error(error) {}
PaintPreviewClient::CreateResult::~CreateResult() = default;
PaintPreviewClient::CreateResult::CreateResult(CreateResult&& other) = default;
PaintPreviewClient::CreateResult& PaintPreviewClient::CreateResult::operator=(
CreateResult&& other) = default;
PaintPreviewClient::PaintPreviewClient(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
PaintPreviewClient::~PaintPreviewClient() = default;
void PaintPreviewClient::CapturePaintPreview(
const PaintPreviewParams& params,
content::RenderFrameHost* render_frame_host,
PaintPreviewCallback callback) {
if (base::Contains(all_document_data_, params.document_guid)) {
std::move(callback).Run(params.document_guid,
mojom::PaintPreviewStatus::kGuidCollision, nullptr);
return;
}
all_document_data_.insert({params.document_guid, PaintPreviewData()});
auto* document_data = &all_document_data_[params.document_guid];
document_data->root_dir = params.root_dir;
document_data->callback = std::move(callback);
document_data->root_url = render_frame_host->GetLastCommittedURL();
CapturePaintPreviewInternal(params, render_frame_host);
}
void PaintPreviewClient::CaptureSubframePaintPreview(
const base::UnguessableToken& guid,
const gfx::Rect& rect,
content::RenderFrameHost* render_subframe_host) {
PaintPreviewParams params;
params.document_guid = guid;
params.clip_rect = rect;
params.is_main_frame = false;
CapturePaintPreviewInternal(params, render_subframe_host);
}
void PaintPreviewClient::RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) {
// TODO(crbug/1044983): Investigate possible issues with cleanup if just
// a single subframe gets deleted.
auto maybe_token = render_frame_host->GetEmbeddingToken();
bool is_main_frame = false;
if (!maybe_token.has_value()) {
uint64_t old_style_id = MakeOldStyleId(render_frame_host);
auto it = main_frame_guids_.find(old_style_id);
if (it == main_frame_guids_.end())
return;
maybe_token = it->second;
is_main_frame = true;
}
base::UnguessableToken frame_guid = maybe_token.value();
auto it = pending_previews_on_subframe_.find(frame_guid);
if (it == pending_previews_on_subframe_.end())
return;
for (const auto& document_guid : it->second) {
auto data_it = all_document_data_.find(document_guid);
if (data_it == all_document_data_.end())
continue;
data_it->second.awaiting_subframes.erase(frame_guid);
data_it->second.finished_subframes.insert(frame_guid);
data_it->second.had_error = true;
if (data_it->second.awaiting_subframes.empty() || is_main_frame) {
if (is_main_frame) {
for (const auto& subframe_guid : data_it->second.awaiting_subframes) {
auto subframe_docs = pending_previews_on_subframe_[subframe_guid];
subframe_docs.erase(document_guid);
if (subframe_docs.empty())
pending_previews_on_subframe_.erase(subframe_guid);
}
}
interface_ptrs_.erase(frame_guid);
OnFinished(document_guid, &data_it->second);
}
}
pending_previews_on_subframe_.erase(frame_guid);
}
PaintPreviewClient::CreateResult PaintPreviewClient::CreateFileHandle(
const base::FilePath& path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
base::File file(path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
return CreateResult(std::move(file), file.error_details());
}
mojom::PaintPreviewCaptureParamsPtr PaintPreviewClient::CreateMojoParams(
const PaintPreviewParams& params,
base::File file) {
mojom::PaintPreviewCaptureParamsPtr mojo_params =
mojom::PaintPreviewCaptureParams::New();
mojo_params->guid = params.document_guid;
mojo_params->clip_rect = params.clip_rect;
mojo_params->is_main_frame = params.is_main_frame;
mojo_params->file = std::move(file);
return mojo_params;
}
void PaintPreviewClient::CapturePaintPreviewInternal(
const PaintPreviewParams& params,
content::RenderFrameHost* render_frame_host) {
// Use a frame's embedding token as its GUID. Note that we create a GUID for
// the main frame so that we can treat it the same as other frames.
auto token = render_frame_host->GetEmbeddingToken();
if (params.is_main_frame && !token.has_value()) {
token = base::UnguessableToken::Create();
main_frame_guids_.insert(
{MakeOldStyleId(render_frame_host), token.value()});
}
// This should be impossible, but if it happens in a release build just abort.
if (!token.has_value()) {
DVLOG(1) << "Error: Attempted to capture a non-main frame without an "
"embedding token.";
NOTREACHED();
return;
}
auto* document_data = &all_document_data_[params.document_guid];
base::UnguessableToken frame_guid = token.value();
if (params.is_main_frame)
document_data->root_frame_token = frame_guid;
// Deduplicate data if a subframe is required multiple times.
if (base::Contains(document_data->awaiting_subframes, frame_guid) ||
base::Contains(document_data->finished_subframes, frame_guid))
return;
base::FilePath file_path = document_data->root_dir.AppendASCII(
base::StrCat({frame_guid.ToString(), ".skp"}));
base::PostTaskAndReplyWithResult(
FROM_HERE,
{base::ThreadPool(), base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&CreateFileHandle, file_path),
base::BindOnce(&PaintPreviewClient::RequestCaptureOnUIThread,
weak_ptr_factory_.GetWeakPtr(), params, frame_guid,
base::Unretained(render_frame_host), file_path));
}
void PaintPreviewClient::RequestCaptureOnUIThread(
const PaintPreviewParams& params,
const base::UnguessableToken& frame_guid,
content::RenderFrameHost* render_frame_host,
const base::FilePath& file_path,
CreateResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* document_data = &all_document_data_[params.document_guid];
if (result.error != base::File::FILE_OK) {
// Don't block up the UI thread and answer the callback on a different
// thread.
base::PostTask(
FROM_HERE,
base::BindOnce(std::move(document_data->callback), params.document_guid,
mojom::PaintPreviewStatus::kFileCreationError, nullptr));
return;
}
document_data->awaiting_subframes.insert(frame_guid);
auto it = pending_previews_on_subframe_.find(frame_guid);
if (it != pending_previews_on_subframe_.end()) {
it->second.insert(params.document_guid);
} else {
pending_previews_on_subframe_.insert(std::make_pair(
frame_guid,
base::flat_set<base::UnguessableToken>({params.document_guid})));
}
if (!base::Contains(interface_ptrs_, frame_guid)) {
interface_ptrs_.insert(
{frame_guid, mojo::AssociatedRemote<mojom::PaintPreviewRecorder>()});
render_frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&interface_ptrs_[frame_guid]);
}
interface_ptrs_[frame_guid]->CapturePaintPreview(
CreateMojoParams(params, std::move(result.file)),
base::BindOnce(&PaintPreviewClient::OnPaintPreviewCapturedCallback,
weak_ptr_factory_.GetWeakPtr(), params.document_guid,
frame_guid, params.is_main_frame, file_path,
base::Unretained(render_frame_host)));
}
void PaintPreviewClient::OnPaintPreviewCapturedCallback(
const base::UnguessableToken& guid,
const base::UnguessableToken& frame_guid,
bool is_main_frame,
const base::FilePath& filename,
content::RenderFrameHost* render_frame_host,
mojom::PaintPreviewStatus status,
mojom::PaintPreviewCaptureResponsePtr response) {
// There is no retry logic so always treat a frame as processed regardless of
// |status|
MarkFrameAsProcessed(guid, frame_guid);
if (status == mojom::PaintPreviewStatus::kOk)
status = RecordFrame(guid, frame_guid, is_main_frame, filename,
render_frame_host, std::move(response));
auto* document_data = &all_document_data_[guid];
if (status != mojom::PaintPreviewStatus::kOk)
document_data->had_error = true;
if (document_data->awaiting_subframes.empty())
OnFinished(guid, document_data);
}
void PaintPreviewClient::MarkFrameAsProcessed(
base::UnguessableToken guid,
const base::UnguessableToken& frame_guid) {
pending_previews_on_subframe_[frame_guid].erase(guid);
if (pending_previews_on_subframe_[frame_guid].empty())
interface_ptrs_.erase(frame_guid);
all_document_data_[guid].finished_subframes.insert(frame_guid);
all_document_data_[guid].awaiting_subframes.erase(frame_guid);
}
mojom::PaintPreviewStatus PaintPreviewClient::RecordFrame(
const base::UnguessableToken& guid,
const base::UnguessableToken& frame_guid,
bool is_main_frame,
const base::FilePath& filename,
content::RenderFrameHost* render_frame_host,
mojom::PaintPreviewCaptureResponsePtr response) {
auto it = all_document_data_.find(guid);
if (!it->second.proto) {
it->second.proto = std::make_unique<PaintPreviewProto>();
it->second.proto->mutable_metadata()->set_url(
all_document_data_[guid].root_url.spec());
}
PaintPreviewProto* proto_ptr = it->second.proto.get();
PaintPreviewFrameProto* frame_proto;
if (is_main_frame) {
frame_proto = proto_ptr->mutable_root_frame();
frame_proto->set_is_main_frame(true);
uint64_t old_style_id = MakeOldStyleId(render_frame_host);
main_frame_guids_.erase(old_style_id);
} else {
frame_proto = proto_ptr->add_subframes();
frame_proto->set_is_main_frame(false);
}
// Safe since always HEX.skp.
frame_proto->set_file_path(filename.AsUTF8Unsafe());
std::vector<base::UnguessableToken> remote_frame_guids =
PaintPreviewCaptureResponseToPaintPreviewFrameProto(
std::move(response), frame_guid, frame_proto);
for (const auto& remote_frame_guid : remote_frame_guids) {
if (!base::Contains(it->second.finished_subframes, remote_frame_guid))
it->second.awaiting_subframes.insert(remote_frame_guid);
}
return mojom::PaintPreviewStatus::kOk;
}
void PaintPreviewClient::OnFinished(base::UnguessableToken guid,
PaintPreviewData* document_data) {
if (!document_data)
return;
base::UmaHistogramBoolean("Browser.PaintPreview.Capture.Success",
document_data->proto != nullptr);
if (document_data->proto) {
base::UmaHistogramCounts100(
"Browser.PaintPreview.Capture.NumberOfFramesCaptured",
document_data->finished_subframes.size());
// At a minimum one frame was captured successfully, it is up to the
// caller to decide if a partial success is acceptable based on what is
// contained in the proto.
base::PostTask(
FROM_HERE,
base::BindOnce(std::move(document_data->callback), guid,
document_data->had_error
? mojom::PaintPreviewStatus::kPartialSuccess
: mojom::PaintPreviewStatus::kOk,
std::move(document_data->proto)));
} else {
// A proto could not be created indicating all frames failed to capture.
base::PostTask(FROM_HERE,
base::BindOnce(std::move(document_data->callback), guid,
mojom::PaintPreviewStatus::kFailed, nullptr));
}
all_document_data_.erase(guid);
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(PaintPreviewClient)
} // namespace paint_preview