blob: 8625772dbe07b809da7dad040929a4249ba484f1 [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/files/file_util.h"
#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/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "components/ukm/content/source_url_recorder.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 "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
namespace paint_preview {
namespace {
// 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());
proto->set_scroll_offset_x(response->scroll_offsets.width());
proto->set_scroll_offset_y(response->scroll_offsets.height());
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;
}
// Records UKM data for the capture.
// TODO(crbug/1038390): Add more metrics;
// - Peak memory during capture (bucketized).
// - Compressed on disk size (bucketized).
void RecordUkmCaptureData(ukm::SourceId source_id,
base::TimeDelta blink_recording_time) {
if (source_id == ukm::kInvalidSourceId)
return;
ukm::builders::PaintPreviewCapture(source_id)
.SetBlinkCaptureTime(blink_recording_time.InMilliseconds())
.Record(ukm::UkmRecorder::Get());
}
base::flat_set<base::UnguessableToken> CreateAcceptedTokenList(
content::RenderFrameHost* render_frame_host) {
auto rfhs = render_frame_host->GetFramesInSubtree();
std::vector<base::UnguessableToken> tokens;
tokens.reserve(rfhs.size());
for (content::RenderFrameHost* rfh : rfhs) {
auto maybe_token = rfh->GetEmbeddingToken();
if (maybe_token.has_value())
tokens.push_back(maybe_token.value());
}
return base::flat_set<base::UnguessableToken>(std::move(tokens));
}
} // namespace
PaintPreviewClient::PaintPreviewParams::PaintPreviewParams()
: is_main_frame(false), max_per_capture_size(0) {}
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;
}
PaintPreviewData document_data;
document_data.root_dir = params.root_dir;
document_data.callback = std::move(callback);
document_data.root_url = render_frame_host->GetLastCommittedURL();
document_data.source_id =
ukm::GetSourceIdForWebContentsDocument(web_contents());
document_data.accepted_tokens = CreateAcceptedTokenList(render_frame_host);
all_document_data_.insert({params.document_guid, std::move(document_data)});
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0(
"paint_preview", "PaintPreviewClient::CapturePaintPreview",
TRACE_ID_LOCAL(&all_document_data_[params.document_guid]));
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();
if (!maybe_token.has_value())
return;
bool is_main_frame = render_frame_host->GetParent() == nullptr;
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;
auto* document_data = &data_it->second;
document_data->awaiting_subframes.erase(frame_guid);
document_data->finished_subframes.insert(frame_guid);
document_data->had_error = true;
if (document_data->awaiting_subframes.empty() || is_main_frame) {
if (is_main_frame) {
for (const auto& subframe_guid : document_data->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, document_data);
}
}
pending_previews_on_subframe_.erase(frame_guid);
}
PaintPreviewClient::CreateResult PaintPreviewClient::CreateFileHandle(
const base::FilePath& path) {
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;
// For now treat all clip rects as hints only. This API should be exposed
// when clip_rects are used intentionally to limit capture time.
mojo_params->clip_rect_is_hint = true;
mojo_params->is_main_frame = params.is_main_frame;
mojo_params->file = std::move(file);
mojo_params->max_capture_size = params.max_per_capture_size;
return mojo_params;
}
void PaintPreviewClient::CapturePaintPreviewInternal(
const PaintPreviewParams& params,
content::RenderFrameHost* render_frame_host) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Use a frame's embedding token as its GUID.
auto token = render_frame_host->GetEmbeddingToken();
// 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 frame without an "
"embedding token.";
NOTREACHED();
return;
}
auto it = all_document_data_.find(params.document_guid);
if (it == all_document_data_.end())
return;
auto* document_data = &it->second;
// The embedding token should be in the list of tokens in the tree when
// capture was started. If this is not the case then the frame may have
// navigated. This is unsafe to capture.
base::UnguessableToken frame_guid = token.value();
if (!base::Contains(document_data->accepted_tokens, frame_guid))
return;
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::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&CreateFileHandle, file_path),
base::BindOnce(&PaintPreviewClient::RequestCaptureOnUIThread,
weak_ptr_factory_.GetWeakPtr(), params, frame_guid,
content::GlobalFrameRoutingId(
render_frame_host->GetProcess()->GetID(),
render_frame_host->GetRoutingID()),
file_path));
}
void PaintPreviewClient::RequestCaptureOnUIThread(
const PaintPreviewParams& params,
const base::UnguessableToken& frame_guid,
const content::GlobalFrameRoutingId& render_frame_id,
const base::FilePath& file_path,
CreateResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto it = all_document_data_.find(params.document_guid);
if (it == all_document_data_.end())
return;
auto* document_data = &it->second;
if (!document_data->callback)
return;
if (result.error != base::File::FILE_OK) {
std::move(document_data->callback)
.Run(params.document_guid,
mojom::PaintPreviewStatus::kFileCreationError, nullptr);
return;
}
// If the render frame host navigated or is no longer around treat this as a
// failure as a navigation occurring during capture is bad.
auto* render_frame_host = content::RenderFrameHost::FromID(render_frame_id);
if (!render_frame_host || render_frame_host->GetEmbeddingToken().value_or(
base::UnguessableToken::Null()) != frame_guid) {
std::move(document_data->callback)
.Run(params.document_guid, mojom::PaintPreviewStatus::kCaptureFailed,
nullptr);
return;
}
document_data->awaiting_subframes.insert(frame_guid);
auto subframe_it = pending_previews_on_subframe_.find(frame_guid);
if (subframe_it != pending_previews_on_subframe_.end()) {
subframe_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,
render_frame_id));
}
void PaintPreviewClient::OnPaintPreviewCapturedCallback(
const base::UnguessableToken& guid,
const base::UnguessableToken& frame_guid,
bool is_main_frame,
const base::FilePath& filename,
const content::GlobalFrameRoutingId& render_frame_id,
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_id, std::move(response));
}
if (status != mojom::PaintPreviewStatus::kOk) {
// If the capture failed then cleanup the file.
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(base::GetDeleteFileCallback(), filename));
// If this is the main frame we should just abort the capture.
if (is_main_frame) {
auto it = all_document_data_.find(guid);
if (it != all_document_data_.end())
OnFinished(guid, &it->second);
}
}
auto it = all_document_data_.find(guid);
if (it == all_document_data_.end())
return;
auto* document_data = &it->second;
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);
auto it = all_document_data_.find(guid);
if (it == all_document_data_.end())
return;
auto* document_data = &it->second;
document_data->finished_subframes.insert(frame_guid);
document_data->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,
const content::GlobalFrameRoutingId& render_frame_id,
mojom::PaintPreviewCaptureResponsePtr response) {
// If the render frame host navigated or is no longer around treat this as a
// failure as a navigation occurring during capture is bad.
auto* render_frame_host = content::RenderFrameHost::FromID(render_frame_id);
if (!render_frame_host || render_frame_host->GetEmbeddingToken().value_or(
base::UnguessableToken::Null()) != frame_guid) {
return mojom::PaintPreviewStatus::kCaptureFailed;
}
auto it = all_document_data_.find(guid);
if (it == all_document_data_.end())
return mojom::PaintPreviewStatus::kCaptureFailed;
auto* document_data = &it->second;
if (!document_data->proto) {
document_data->proto = std::make_unique<PaintPreviewProto>();
document_data->proto->mutable_metadata()->set_url(
document_data->root_url.spec());
}
PaintPreviewProto* proto_ptr = document_data->proto.get();
PaintPreviewFrameProto* frame_proto;
if (is_main_frame) {
document_data->main_frame_blink_recording_time =
response->blink_recording_time;
frame_proto = proto_ptr->mutable_root_frame();
frame_proto->set_is_main_frame(true);
} 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) {
// Don't wait again for a frame that was already captured. Also don't wait
// on frames that navigated during capture and have new embedding tokens.
if (!base::Contains(document_data->finished_subframes, remote_frame_guid) &&
base::Contains(document_data->accepted_tokens, remote_frame_guid)) {
document_data->awaiting_subframes.insert(remote_frame_guid);
}
}
return mojom::PaintPreviewStatus::kOk;
}
void PaintPreviewClient::OnFinished(base::UnguessableToken guid,
PaintPreviewData* document_data) {
if (!document_data || !document_data->callback)
return;
TRACE_EVENT_NESTABLE_ASYNC_END2(
"paint_preview", "PaintPreviewClient::CapturePaintPreview",
TRACE_ID_LOCAL(document_data), "success", document_data->proto != nullptr,
"subframes", document_data->finished_subframes.size());
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());
RecordUkmCaptureData(document_data->source_id,
document_data->main_frame_blink_recording_time);
// 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.
std::move(document_data->callback)
.Run(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.
std::move(document_data->callback)
.Run(guid, mojom::PaintPreviewStatus::kFailed, nullptr);
}
all_document_data_.erase(guid);
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(PaintPreviewClient)
} // namespace paint_preview