blob: 5c9dd3d0f4f3b9b473bf2cd96acb504aed1d8bb1 [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 "third_party/blink/renderer/core/content_capture/content_capture_task.h"
#include <cmath>
#include "base/auto_reset.h"
#include "base/feature_list.h"
#include "cc/trees/layer_tree_host.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/web/web_content_capture_client.h"
#include "third_party/blink/public/web/web_content_holder.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
namespace blink {
ContentCaptureTask::TaskDelay::TaskDelay(
const base::TimeDelta& task_short_delay,
const base::TimeDelta& task_long_delay)
: task_short_delay_(task_short_delay), task_long_delay_(task_long_delay) {}
base::TimeDelta ContentCaptureTask::TaskDelay::ResetAndGetInitialDelay() {
delay_exponent_ = 0;
return task_short_delay_;
}
base::TimeDelta ContentCaptureTask::TaskDelay::GetNextTaskDelay() const {
return base::TimeDelta::FromMilliseconds(task_short_delay_.InMilliseconds() *
(1 << delay_exponent_));
}
void ContentCaptureTask::TaskDelay::IncreaseDelayExponent() {
// Increases the delay up to 128s.
if (delay_exponent_ < 8)
++delay_exponent_;
}
ContentCaptureTask::ContentCaptureTask(LocalFrame& local_frame_root,
TaskSession& task_session)
: local_frame_root_(&local_frame_root),
task_session_(&task_session),
delay_task_(
local_frame_root_->GetTaskRunner(TaskType::kInternalContentCapture),
this,
&ContentCaptureTask::Run) {
base::TimeDelta task_short_delay;
base::TimeDelta task_long_delay;
local_frame_root.Client()
->GetWebContentCaptureClient()
->GetTaskTimingParameters(task_short_delay, task_long_delay);
task_delay_ = std::make_unique<TaskDelay>(task_short_delay, task_long_delay);
// The histogram is all about time, just disable it if high resolution isn't
// supported.
if (base::TimeTicks::IsHighResolution()) {
histogram_reporter_ =
base::MakeRefCounted<ContentCaptureTaskHistogramReporter>();
task_session_->SetSentNodeCountCallback(
WTF::BindRepeating(&ContentCaptureTaskHistogramReporter::
RecordsSentContentCountPerDocument,
histogram_reporter_));
}
}
ContentCaptureTask::~ContentCaptureTask() = default;
void ContentCaptureTask::Shutdown() {
DCHECK(local_frame_root_);
local_frame_root_ = nullptr;
CancelTask();
}
bool ContentCaptureTask::CaptureContent(Vector<cc::NodeInfo>& data) {
if (captured_content_for_testing_) {
data = captured_content_for_testing_.value();
return true;
}
// Because this is called from a different task, the frame may be in any
// lifecycle step so we need to early-out in many cases.
if (const auto* root_frame_view = local_frame_root_->View()) {
if (const auto* cc_layer = root_frame_view->RootCcLayer()) {
if (auto* layer_tree_host = cc_layer->layer_tree_host()) {
std::vector<cc::NodeInfo> content;
if (layer_tree_host->CaptureContent(&content)) {
for (auto c : content)
data.push_back(std::move(c));
return true;
}
return false;
}
}
}
return false;
}
bool ContentCaptureTask::CaptureContent() {
DCHECK(task_session_);
Vector<cc::NodeInfo> buffer;
if (histogram_reporter_)
histogram_reporter_->OnCaptureContentStarted();
bool result = CaptureContent(buffer);
if (!buffer.IsEmpty())
task_session_->SetCapturedContent(buffer);
if (histogram_reporter_)
histogram_reporter_->OnCaptureContentEnded(buffer.size());
return result;
}
void ContentCaptureTask::SendContent(
TaskSession::DocumentSession& doc_session) {
auto* document = doc_session.GetDocument();
DCHECK(document);
auto* client = GetWebContentCaptureClient(*document);
DCHECK(client);
if (histogram_reporter_)
histogram_reporter_->OnSendContentStarted();
WebVector<WebContentHolder> content_batch;
content_batch.reserve(kBatchSize);
// Only send changed content after the new content was sent.
bool sending_changed_content = !doc_session.HasUnsentCapturedContent();
while (content_batch.size() < kBatchSize) {
ContentHolder* holder;
if (sending_changed_content)
holder = doc_session.GetNextChangedNode();
else
holder = doc_session.GetNextUnsentNode();
if (!holder)
break;
content_batch.emplace_back(WebContentHolder(*holder));
}
if (!content_batch.empty()) {
if (sending_changed_content) {
client->DidUpdateContent(content_batch);
} else {
client->DidCaptureContent(content_batch, !doc_session.FirstDataHasSent());
doc_session.SetFirstDataHasSent();
}
}
if (histogram_reporter_)
histogram_reporter_->OnSendContentEnded(content_batch.size());
}
WebContentCaptureClient* ContentCaptureTask::GetWebContentCaptureClient(
const Document& document) {
if (auto* frame = document.GetFrame())
return frame->Client()->GetWebContentCaptureClient();
return nullptr;
}
bool ContentCaptureTask::ProcessSession() {
DCHECK(task_session_);
while (auto* document_session =
task_session_->GetNextUnsentDocumentSession()) {
if (!ProcessDocumentSession(*document_session))
return false;
if (ShouldPause())
return !task_session_->HasUnsentData();
}
return true;
}
bool ContentCaptureTask::ProcessDocumentSession(
TaskSession::DocumentSession& doc_session) {
// If no client, we don't need to send it at all.
auto* content_capture_client =
GetWebContentCaptureClient(*doc_session.GetDocument());
if (!content_capture_client) {
doc_session.Reset();
return true;
}
while (doc_session.HasUnsentCapturedContent() ||
doc_session.HasUnsentChangedContent()) {
SendContent(doc_session);
if (ShouldPause()) {
return !doc_session.HasUnsentData();
}
}
// Sent the detached nodes.
if (doc_session.HasUnsentDetachedNodes())
content_capture_client->DidRemoveContent(doc_session.MoveDetachedNodes());
DCHECK(!doc_session.HasUnsentData());
return true;
}
bool ContentCaptureTask::RunInternal() {
base::AutoReset<TaskState> state(&task_state_, TaskState::kProcessRetryTask);
// Already shutdown.
if (!local_frame_root_)
return true;
do {
switch (task_state_) {
case TaskState::kProcessRetryTask:
if (task_session_->HasUnsentData()) {
if (!ProcessSession())
return false;
}
task_state_ = TaskState::kCaptureContent;
break;
case TaskState::kCaptureContent:
if (!has_content_change_)
return true;
if (!CaptureContent()) {
// Don't schedule task again in this case.
return true;
}
has_content_change_ = false;
if (!task_session_->HasUnsentData())
return true;
task_state_ = TaskState::kProcessCurrentSession;
break;
case TaskState::kProcessCurrentSession:
return ProcessSession();
default:
return true;
}
} while (!ShouldPause());
return false;
}
void ContentCaptureTask::Run(TimerBase*) {
TRACE_EVENT0("content_capture", "RunTask");
task_delay_->IncreaseDelayExponent();
if (histogram_reporter_)
histogram_reporter_->OnTaskRun();
bool completed = RunInternal();
if (!completed) {
ScheduleInternal(ScheduleReason::kRetryTask);
}
if (histogram_reporter_ &&
(completed || task_state_ == TaskState::kCaptureContent)) {
// The current capture session ends if the task indicates it completed or
// is about to capture the new changes.
histogram_reporter_->OnAllCapturedContentSent();
}
}
base::TimeDelta ContentCaptureTask::GetAndAdjustDelay(ScheduleReason reason) {
bool user_activated_delay_enabled =
base::FeatureList::IsEnabled(features::kContentCaptureUserActivatedDelay);
switch (reason) {
case ScheduleReason::kFirstContentChange:
case ScheduleReason::kScrolling:
case ScheduleReason::kRetryTask:
return user_activated_delay_enabled
? task_delay_->ResetAndGetInitialDelay()
: task_delay_->task_short_delay();
case ScheduleReason::kUserActivatedContentChange:
return user_activated_delay_enabled
? task_delay_->ResetAndGetInitialDelay()
: task_delay_->task_long_delay();
case ScheduleReason::kNonUserActivatedContentChange:
return user_activated_delay_enabled ? task_delay_->GetNextTaskDelay()
: task_delay_->task_long_delay();
}
}
void ContentCaptureTask::ScheduleInternal(ScheduleReason reason) {
DCHECK(local_frame_root_);
base::TimeDelta delay = GetAndAdjustDelay(reason);
// Return if the current task is about to run soon.
if (delay_task_.IsActive() && delay_task_.NextFireInterval() < delay) {
return;
}
if (delay_task_.IsActive())
delay_task_.Stop();
delay_task_.StartOneShot(delay, FROM_HERE);
TRACE_EVENT_INSTANT1("content_capture", "ScheduleTask",
TRACE_EVENT_SCOPE_THREAD, "reason", reason);
if (histogram_reporter_) {
histogram_reporter_->OnTaskScheduled(/* record_task_delay = */ reason !=
ScheduleReason::kRetryTask);
}
}
void ContentCaptureTask::Schedule(ScheduleReason reason) {
DCHECK(local_frame_root_);
has_content_change_ = true;
if (histogram_reporter_)
histogram_reporter_->OnContentChanged();
ScheduleInternal(reason);
}
bool ContentCaptureTask::ShouldPause() {
if (task_stop_for_testing_) {
return task_state_ == task_stop_for_testing_.value();
}
return ThreadScheduler::Current()->ShouldYieldForHighPriorityWork();
}
void ContentCaptureTask::CancelTask() {
if (delay_task_.IsActive())
delay_task_.Stop();
}
void ContentCaptureTask::ClearDocumentSessionsForTesting() {
task_session_->ClearDocumentSessionsForTesting();
}
base::TimeDelta ContentCaptureTask::GetTaskNextFireIntervalForTesting() const {
return delay_task_.IsActive() ? delay_task_.NextFireInterval()
: base::TimeDelta();
}
void ContentCaptureTask::CancelTaskForTesting() {
CancelTask();
}
void ContentCaptureTask::Trace(Visitor* visitor) const {
visitor->Trace(local_frame_root_);
visitor->Trace(task_session_);
visitor->Trace(delay_task_);
}
} // namespace blink