summaryrefslogtreecommitdiff
path: root/chromium/headless/public
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/headless/public')
-rw-r--r--chromium/headless/public/headless_browser.cc26
-rw-r--r--chromium/headless/public/headless_browser.h46
-rw-r--r--chromium/headless/public/headless_browser_context.h4
-rw-r--r--chromium/headless/public/headless_devtools_client.h8
-rw-r--r--chromium/headless/public/internal/headless_devtools_client_impl.h8
-rw-r--r--chromium/headless/public/internal/value_conversions.h33
-rw-r--r--chromium/headless/public/util/DEPS6
-rw-r--r--chromium/headless/public/util/black_hole_protocol_handler.cc6
-rw-r--r--chromium/headless/public/util/compositor_controller.cc429
-rw-r--r--chromium/headless/public/util/compositor_controller.h138
-rw-r--r--chromium/headless/public/util/compositor_controller_browsertest.cc110
-rw-r--r--chromium/headless/public/util/compositor_controller_unittest.cc511
-rw-r--r--chromium/headless/public/util/deterministic_dispatcher.cc4
-rw-r--r--chromium/headless/public/util/deterministic_dispatcher_test.cc6
-rw-r--r--chromium/headless/public/util/deterministic_http_protocol_handler.cc4
-rw-r--r--chromium/headless/public/util/error_reporter.cc9
-rw-r--r--chromium/headless/public/util/error_reporter.h18
-rw-r--r--chromium/headless/public/util/expedited_dispatcher.cc2
-rw-r--r--chromium/headless/public/util/expedited_dispatcher_test.cc6
-rw-r--r--chromium/headless/public/util/generic_url_request_job.cc150
-rw-r--r--chromium/headless/public/util/generic_url_request_job.h8
-rw-r--r--chromium/headless/public/util/generic_url_request_job_test.cc158
-rw-r--r--chromium/headless/public/util/http_url_fetcher.cc4
-rw-r--r--chromium/headless/public/util/in_memory_protocol_handler.cc4
-rw-r--r--chromium/headless/public/util/in_memory_request_job.cc2
-rw-r--r--chromium/headless/public/util/moveable_auto_lock.h64
-rw-r--r--chromium/headless/public/util/testing/generic_url_request_mocks.cc29
-rw-r--r--chromium/headless/public/util/testing/generic_url_request_mocks.h16
-rw-r--r--chromium/headless/public/util/testing/mock_devtools_agent_host.cc4
-rw-r--r--chromium/headless/public/util/testing/test_in_memory_protocol_handler.cc34
-rw-r--r--chromium/headless/public/util/testing/test_in_memory_protocol_handler.h19
-rw-r--r--chromium/headless/public/util/throttled_dispatcher.cc2
-rw-r--r--chromium/headless/public/util/throttled_dispatcher_test.cc4
-rw-r--r--chromium/headless/public/util/virtual_time_controller.cc107
-rw-r--r--chromium/headless/public/util/virtual_time_controller.h66
-rw-r--r--chromium/headless/public/util/virtual_time_controller_test.cc265
36 files changed, 2019 insertions, 291 deletions
diff --git a/chromium/headless/public/headless_browser.cc b/chromium/headless/public/headless_browser.cc
index 1f01f5a3f87..7e124fc32b4 100644
--- a/chromium/headless/public/headless_browser.cc
+++ b/chromium/headless/public/headless_browser.cc
@@ -31,16 +31,6 @@ std::string GetProductNameAndVersion() {
Options::Options(int argc, const char** argv)
: argc(argc),
argv(argv),
-#if defined(OS_WIN)
- instance(0),
- sandbox_info(nullptr),
-#endif
- devtools_endpoint(),
- devtools_socket_fd(0),
- message_pump(nullptr),
- single_process_mode(false),
- disable_sandbox(false),
- enable_resource_scheduler(true),
#if defined(USE_OZONE)
// TODO(skyostil): Implement SwiftShader backend for headless ozone.
gl_implementation("osmesa"),
@@ -54,14 +44,12 @@ Options::Options(int argc, const char** argv)
#endif
product_name_and_version(GetProductNameAndVersion()),
user_agent(content::BuildUserAgentFromProduct(product_name_and_version)),
- window_size(kDefaultWindowSize),
- incognito_mode(true),
- enable_crash_reporter(false) {
+ window_size(kDefaultWindowSize) {
}
Options::Options(Options&& options) = default;
-Options::~Options() {}
+Options::~Options() = default;
Options& Options::operator=(Options&& options) = default;
@@ -73,7 +61,7 @@ Builder::Builder(int argc, const char** argv) : options_(argc, argv) {}
Builder::Builder() : options_(0, nullptr) {}
-Builder::~Builder() {}
+Builder::~Builder() = default;
Builder& Builder::SetProductNameAndVersion(
const std::string& product_name_and_version) {
@@ -142,6 +130,12 @@ Builder& Builder::AddMojoServiceName(const std::string& mojo_service_name) {
return *this;
}
+Builder& Builder::SetAppendCommandLineFlagsCallback(
+ const Options::AppendCommandLineFlagsCallback& callback) {
+ options_.append_command_line_flags_callback = callback;
+ return *this;
+}
+
#if defined(OS_WIN)
Builder& Builder::SetInstance(HINSTANCE instance) {
options_.instance = instance;
@@ -170,7 +164,7 @@ Builder& Builder::SetIncognitoMode(bool incognito_mode) {
}
Builder& Builder::SetOverrideWebPreferencesCallback(
- base::Callback<void(WebPreferences*)> callback) {
+ const base::Callback<void(WebPreferences*)>& callback) {
options_.override_web_preferences_callback = callback;
return *this;
}
diff --git a/chromium/headless/public/headless_browser.h b/chromium/headless/public/headless_browser.h
index d3ef26dcd76..3ce4f82a72c 100644
--- a/chromium/headless/public/headless_browser.h
+++ b/chromium/headless/public/headless_browser.h
@@ -12,6 +12,7 @@
#include <vector>
#include "base/callback.h"
+#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
@@ -102,16 +103,16 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options {
Options& operator=(Options&& options);
- // Command line options to be passed to browser.
+ // Command line options to be passed to browser. Initialized in constructor.
int argc;
const char** argv;
#if defined(OS_WIN)
// Set hardware instance if available, otherwise it defaults to 0.
- HINSTANCE instance;
+ HINSTANCE instance = 0;
// Set with sandbox information. This has to be already initialized.
- sandbox::SandboxInterfaceInfo* sandbox_info;
+ sandbox::SandboxInterfaceInfo* sandbox_info = nullptr;
#endif
// Address at which DevTools should listen for connections. Disabled by
@@ -120,25 +121,25 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options {
// The fd of an already-open socket inherited from a parent process. Disabled
// by default. Mutually exclusive with devtools_endpoint.
- size_t devtools_socket_fd;
+ size_t devtools_socket_fd = 0;
// A single way to test whether the devtools server has been requested.
bool DevtoolsServerEnabled();
// Optional message pump that overrides the default. Must outlive the browser.
- base::MessagePump* message_pump;
+ base::MessagePump* message_pump = nullptr;
// Run the browser in single process mode instead of using separate renderer
// processes as per default. Note that this also disables any sandboxing of
// web content, which can be a security risk.
- bool single_process_mode;
+ bool single_process_mode = false;
// Run the browser without renderer sandbox. This option can be
// a security risk and should be used with caution.
- bool disable_sandbox;
+ bool disable_sandbox = false;
// Whether or not to enable content::ResourceScheduler. Enabled by default.
- bool enable_resource_scheduler;
+ bool enable_resource_scheduler = true;
// Choose the GL implementation to use for rendering. A suitable
// implementantion is selected by default. Setting this to an empty
@@ -156,7 +157,7 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options {
std::string user_agent;
// The ProxyConfig to use. The system proxy settings are used by default.
- std::unique_ptr<net::ProxyConfig> proxy_config;
+ std::unique_ptr<net::ProxyConfig> proxy_config = nullptr;
// Comma-separated list of rules that control how hostnames are mapped. See
// chrome::switches::kHostRules for a description for the format.
@@ -171,7 +172,10 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options {
base::FilePath user_data_dir;
// Run a browser context in an incognito mode. Enabled by default.
- bool incognito_mode;
+ bool incognito_mode = true;
+
+ // Whether cookies are allowed. Enabled by default.
+ bool allow_cookies = true;
// Set a callback that is invoked to override WebPreferences for RenderViews
// created within the HeadlessBrowser. Called whenever the WebPreferences of a
@@ -181,10 +185,25 @@ struct HEADLESS_EXPORT HeadlessBrowser::Options {
// exposed WebPreferences API, so use with care.
base::Callback<void(WebPreferences*)> override_web_preferences_callback;
+ // Set a callback that is invoked when a new child process is spawned or
+ // forked and allows adding additional command line flags to the child
+ // process's command line. Executed on the browser main thread.
+ // |child_browser_context| points to the BrowserContext of the child
+ // process, but will only be set if the child process is a renderer process.
+ //
+ // NOTE: This callback may be called on the UI or IO thread even after the
+ // HeadlessBrowser has been destroyed.
+ using AppendCommandLineFlagsCallback =
+ base::Callback<void(base::CommandLine* command_line,
+ HeadlessBrowserContext* child_browser_context,
+ const std::string& child_process_type,
+ int child_process_id)>;
+ AppendCommandLineFlagsCallback append_command_line_flags_callback;
+
// Minidump crash reporter settings. Crash reporting is disabled by default.
// By default crash dumps are written to the directory containing the
// executable.
- bool enable_crash_reporter;
+ bool enable_crash_reporter = false;
base::FilePath crash_dumps_dir;
// Reminder: when adding a new field here, do not forget to add it to
@@ -211,6 +230,8 @@ class HEADLESS_EXPORT HeadlessBrowser::Options::Builder {
Builder& SetEnableResourceScheduler(bool enable_resource_scheduler);
Builder& SetGLImplementation(const std::string& gl_implementation);
Builder& AddMojoServiceName(const std::string& mojo_service_name);
+ Builder& SetAppendCommandLineFlagsCallback(
+ const Options::AppendCommandLineFlagsCallback& callback);
#if defined(OS_WIN)
Builder& SetInstance(HINSTANCE instance);
Builder& SetSandboxInfo(sandbox::SandboxInterfaceInfo* sandbox_info);
@@ -227,8 +248,9 @@ class HEADLESS_EXPORT HeadlessBrowser::Options::Builder {
Builder& SetWindowSize(const gfx::Size& window_size);
Builder& SetUserDataDir(const base::FilePath& user_data_dir);
Builder& SetIncognitoMode(bool incognito_mode);
+ Builder& SetAllowCookies(bool allow_cookies);
Builder& SetOverrideWebPreferencesCallback(
- base::Callback<void(WebPreferences*)> callback);
+ const base::Callback<void(WebPreferences*)>& callback);
Builder& SetCrashReporterEnabled(bool enabled);
Builder& SetCrashDumpsDir(const base::FilePath& dir);
diff --git a/chromium/headless/public/headless_browser_context.h b/chromium/headless/public/headless_browser_context.h
index 83b1207ea52..1d117decfa6 100644
--- a/chromium/headless/public/headless_browser_context.h
+++ b/chromium/headless/public/headless_browser_context.h
@@ -14,6 +14,7 @@
#include "base/callback.h"
#include "base/optional.h"
#include "content/public/common/web_preferences.h"
+#include "headless/lib/browser/headless_network_conditions.h"
#include "headless/public/headless_export.h"
#include "headless/public/headless_web_contents.h"
#include "net/proxy/proxy_service.h"
@@ -69,6 +70,8 @@ class HEADLESS_EXPORT HeadlessBrowserContext {
virtual void AddObserver(Observer* observer) = 0;
virtual void RemoveObserver(Observer* observer) = 0;
+ virtual HeadlessNetworkConditions GetNetworkConditions() = 0;
+
// TODO(skyostil): Allow saving and restoring contexts (crbug.com/617931).
protected:
@@ -132,6 +135,7 @@ class HEADLESS_EXPORT HeadlessBrowserContext::Builder {
Builder& SetWindowSize(const gfx::Size& window_size);
Builder& SetUserDataDir(const base::FilePath& user_data_dir);
Builder& SetIncognitoMode(bool incognito_mode);
+ Builder& SetAllowCookies(bool incognito_mode);
Builder& SetOverrideWebPreferencesCallback(
base::Callback<void(WebPreferences*)> callback);
diff --git a/chromium/headless/public/headless_devtools_client.h b/chromium/headless/public/headless_devtools_client.h
index 88b11826ee3..ee4a336f25f 100644
--- a/chromium/headless/public/headless_devtools_client.h
+++ b/chromium/headless/public/headless_devtools_client.h
@@ -10,6 +10,10 @@
#include "base/macros.h"
#include "headless/public/headless_export.h"
+namespace base {
+class DictionaryValue;
+} // namespace base
+
namespace headless {
namespace accessibility {
@@ -90,6 +94,9 @@ class Domain;
namespace page {
class Domain;
}
+namespace performance {
+class Domain;
+}
namespace profiler {
class Domain;
}
@@ -147,6 +154,7 @@ class HEADLESS_EXPORT HeadlessDevToolsClient {
virtual memory::Domain* GetMemory() = 0;
virtual network::Domain* GetNetwork() = 0;
virtual page::Domain* GetPage() = 0;
+ virtual performance::Domain* GetPerformance() = 0;
virtual profiler::Domain* GetProfiler() = 0;
virtual runtime::Domain* GetRuntime() = 0;
virtual security::Domain* GetSecurity() = 0;
diff --git a/chromium/headless/public/internal/headless_devtools_client_impl.h b/chromium/headless/public/internal/headless_devtools_client_impl.h
index ed1eb70efcd..d9b89eae511 100644
--- a/chromium/headless/public/internal/headless_devtools_client_impl.h
+++ b/chromium/headless/public/internal/headless_devtools_client_impl.h
@@ -36,6 +36,7 @@
#include "headless/public/devtools/domains/memory.h"
#include "headless/public/devtools/domains/network.h"
#include "headless/public/devtools/domains/page.h"
+#include "headless/public/devtools/domains/performance.h"
#include "headless/public/devtools/domains/profiler.h"
#include "headless/public/devtools/domains/runtime.h"
#include "headless/public/devtools/domains/security.h"
@@ -93,6 +94,7 @@ class HEADLESS_EXPORT HeadlessDevToolsClientImpl
memory::Domain* GetMemory() override;
network::Domain* GetNetwork() override;
page::Domain* GetPage() override;
+ performance::Domain* GetPerformance() override;
profiler::Domain* GetProfiler() override;
runtime::Domain* GetRuntime() override;
security::Domain* GetSecurity() override;
@@ -105,11 +107,10 @@ class HEADLESS_EXPORT HeadlessDevToolsClientImpl
void SendRawDevToolsMessage(const std::string& json_message) override;
void SendRawDevToolsMessage(const base::DictionaryValue& message) override;
- // content::DevToolstAgentHostClient implementation:
+ // content::DevToolsAgentHostClient implementation:
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
const std::string& json_message) override;
- void AgentHostClosed(content::DevToolsAgentHost* agent_host,
- bool replaced_with_another_client) override;
+ void AgentHostClosed(content::DevToolsAgentHost* agent_host) override;
// internal::MessageDispatcher implementation:
void SendMessage(const char* method,
@@ -203,6 +204,7 @@ class HEADLESS_EXPORT HeadlessDevToolsClientImpl
memory::ExperimentalDomain memory_domain_;
network::ExperimentalDomain network_domain_;
page::ExperimentalDomain page_domain_;
+ performance::ExperimentalDomain performance_domain_;
profiler::ExperimentalDomain profiler_domain_;
runtime::ExperimentalDomain runtime_domain_;
security::ExperimentalDomain security_domain_;
diff --git a/chromium/headless/public/internal/value_conversions.h b/chromium/headless/public/internal/value_conversions.h
index f2ded264939..7f218b42da9 100644
--- a/chromium/headless/public/internal/value_conversions.h
+++ b/chromium/headless/public/internal/value_conversions.h
@@ -74,40 +74,44 @@ std::unique_ptr<base::Value> ToValueImpl(const std::unique_ptr<T>& value,
template <>
struct FromValue<bool> {
static bool Parse(const base::Value& value, ErrorReporter* errors) {
- bool result = false;
- if (!value.GetAsBoolean(&result))
+ if (!value.is_bool()) {
errors->AddError("boolean value expected");
- return result;
+ return false;
+ }
+ return value.GetBool();
}
};
template <>
struct FromValue<int> {
static int Parse(const base::Value& value, ErrorReporter* errors) {
- int result = 0;
- if (!value.GetAsInteger(&result))
+ if (!value.is_int()) {
errors->AddError("integer value expected");
- return result;
+ return 0;
+ }
+ return value.GetInt();
}
};
template <>
struct FromValue<double> {
static double Parse(const base::Value& value, ErrorReporter* errors) {
- double result = 0;
- if (!value.GetAsDouble(&result))
+ if (!value.is_double() && !value.is_int()) {
errors->AddError("double value expected");
- return result;
+ return 0;
+ }
+ return value.GetDouble();
}
};
template <>
struct FromValue<std::string> {
static std::string Parse(const base::Value& value, ErrorReporter* errors) {
- std::string result;
- if (!value.GetAsString(&result))
+ if (!value.is_string()) {
errors->AddError("string value expected");
- return result;
+ return "";
+ }
+ return value.GetString();
}
};
@@ -144,13 +148,12 @@ template <typename T>
struct FromValue<std::vector<T>> {
static std::vector<T> Parse(const base::Value& value, ErrorReporter* errors) {
std::vector<T> result;
- const base::ListValue* list;
- if (!value.GetAsList(&list)) {
+ if (!value.is_list()) {
errors->AddError("list value expected");
return result;
}
errors->Push();
- for (const auto& item : *list)
+ for (const auto& item : value.GetList())
result.push_back(FromValue<T>::Parse(item, errors));
errors->Pop();
return result;
diff --git a/chromium/headless/public/util/DEPS b/chromium/headless/public/util/DEPS
new file mode 100644
index 00000000000..5fc6a7b9a6a
--- /dev/null
+++ b/chromium/headless/public/util/DEPS
@@ -0,0 +1,6 @@
+specific_include_rules = {
+ "compositor_controller_browsertest.cc": [
+ "+cc/base/switches.h",
+ ]
+}
+
diff --git a/chromium/headless/public/util/black_hole_protocol_handler.cc b/chromium/headless/public/util/black_hole_protocol_handler.cc
index 3f036c59706..31a269b3328 100644
--- a/chromium/headless/public/util/black_hole_protocol_handler.cc
+++ b/chromium/headless/public/util/black_hole_protocol_handler.cc
@@ -34,7 +34,7 @@ BlackHoleRequestJob::BlackHoleRequestJob(net::URLRequest* request,
net::NetworkDelegate* network_delegate)
: net::URLRequestJob(request, network_delegate), weak_factory_(this) {}
-BlackHoleRequestJob::~BlackHoleRequestJob() {}
+BlackHoleRequestJob::~BlackHoleRequestJob() = default;
void BlackHoleRequestJob::Start() {
base::ThreadTaskRunnerHandle::Get()->PostTask(
@@ -54,8 +54,8 @@ void BlackHoleRequestJob::Kill() {
}
} // namespace
-BlackHoleProtocolHandler::BlackHoleProtocolHandler() {}
-BlackHoleProtocolHandler::~BlackHoleProtocolHandler() {}
+BlackHoleProtocolHandler::BlackHoleProtocolHandler() = default;
+BlackHoleProtocolHandler::~BlackHoleProtocolHandler() = default;
net::URLRequestJob* BlackHoleProtocolHandler::MaybeCreateJob(
net::URLRequest* request,
diff --git a/chromium/headless/public/util/compositor_controller.cc b/chromium/headless/public/util/compositor_controller.cc
new file mode 100644
index 00000000000..2bc803c8532
--- /dev/null
+++ b/chromium/headless/public/util/compositor_controller.cc
@@ -0,0 +1,429 @@
+// Copyright 2017 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 "headless/public/util/compositor_controller.h"
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/cancelable_callback.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/single_thread_task_runner.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/trace_event/trace_event.h"
+#include "headless/public/util/virtual_time_controller.h"
+
+namespace headless {
+
+// Sends BeginFrames to advance animations while virtual time advances in
+// intervals.
+class CompositorController::AnimationBeginFrameTask
+ : public VirtualTimeController::RepeatingTask {
+ public:
+ explicit AnimationBeginFrameTask(CompositorController* compositor_controller)
+ : compositor_controller_(compositor_controller),
+ weak_ptr_factory_(this) {}
+
+ // VirtualTimeController::RepeatingTask implementation:
+ void IntervalElapsed(base::TimeDelta virtual_time_offset,
+ const base::Closure& continue_callback) override {
+ continue_callback_ = continue_callback;
+
+ // Post a cancellable task that will issue a BeginFrame. This way, we can
+ // cancel sending an animation-only BeginFrame if another virtual time task
+ // sends a screenshotting BeginFrame first, or if the budget was exhausted.
+ // TODO(eseckler): This won't capture screenshot requests sent
+ // asynchronously.
+ begin_frame_task_.Reset(
+ base::Bind(&AnimationBeginFrameTask::IssueAnimationBeginFrame,
+ weak_ptr_factory_.GetWeakPtr()));
+ compositor_controller_->task_runner_->PostTask(
+ FROM_HERE, begin_frame_task_.callback());
+ }
+
+ void BudgetRequested(base::TimeDelta virtual_time_offset,
+ base::TimeDelta requested_budget,
+ const base::Closure& continue_callback) override {
+ // Run a BeginFrame if we cancelled it because the budged expired previously
+ // and no other BeginFrame was sent while virtual time was paused.
+ if (needs_begin_frame_on_virtual_time_resume_) {
+ continue_callback_ = continue_callback;
+ IssueAnimationBeginFrame();
+ return;
+ }
+ continue_callback.Run();
+ }
+
+ void BudgetExpired(base::TimeDelta virtual_time_offset) override {
+ // Wait until a new budget was requested before sending another animation
+ // BeginFrame, as it's likely that we will send a screenshotting BeginFrame.
+ if (!begin_frame_task_.IsCancelled()) {
+ begin_frame_task_.Cancel();
+ needs_begin_frame_on_virtual_time_resume_ = true;
+ BeginFrameComplete(nullptr);
+ }
+ }
+
+ void CompositorControllerIssuingScreenshotBeginFrame() {
+ // The screenshotting BeginFrame will replace our animation-only BeginFrame.
+ // We cancel any pending animation BeginFrame to avoid sending two
+ // BeginFrames within the same virtual time pause.
+ needs_begin_frame_on_virtual_time_resume_ = false;
+ if (!begin_frame_task_.IsCancelled()) {
+ begin_frame_task_.Cancel();
+ BeginFrameComplete(nullptr);
+ }
+ }
+
+ private:
+ void IssueAnimationBeginFrame() {
+ TRACE_EVENT0("headless",
+ "CompositorController::AnimationBeginFrameTask::"
+ "IssueAnimationBeginFrame");
+ begin_frame_task_.Cancel();
+ needs_begin_frame_on_virtual_time_resume_ = false;
+ // No need for PostBeginFrame, since the begin_frame_task_ has already been
+ // posted above.
+ compositor_controller_->BeginFrame(
+ base::Bind(&AnimationBeginFrameTask::BeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ void BeginFrameComplete(std::unique_ptr<BeginFrameResult>) {
+ TRACE_EVENT0(
+ "headless",
+ "CompositorController::AnimationBeginFrameTask::BeginFrameComplete");
+ DCHECK(continue_callback_);
+ auto callback = continue_callback_;
+ continue_callback_.Reset();
+ callback.Run();
+ }
+
+ CompositorController* compositor_controller_; // NOT OWNED
+ bool needs_begin_frame_on_virtual_time_resume_ = false;
+ base::CancelableClosure begin_frame_task_;
+ base::Closure continue_callback_;
+ base::WeakPtrFactory<AnimationBeginFrameTask> weak_ptr_factory_;
+};
+
+CompositorController::CompositorController(
+ scoped_refptr<base::SequencedTaskRunner> task_runner,
+ HeadlessDevToolsClient* devtools_client,
+ VirtualTimeController* virtual_time_controller,
+ base::TimeDelta animation_begin_frame_interval,
+ base::TimeDelta wait_for_compositor_ready_begin_frame_delay)
+ : task_runner_(std::move(task_runner)),
+ devtools_client_(devtools_client),
+ virtual_time_controller_(virtual_time_controller),
+ animation_task_(base::MakeUnique<AnimationBeginFrameTask>(this)),
+ animation_begin_frame_interval_(animation_begin_frame_interval),
+ wait_for_compositor_ready_begin_frame_delay_(
+ wait_for_compositor_ready_begin_frame_delay),
+ weak_ptr_factory_(this) {
+ devtools_client_->GetHeadlessExperimental()->GetExperimental()->AddObserver(
+ this);
+ // No need to wait for completion of this, since we are waiting for the
+ // setNeedsBeginFramesChanged event instead, which will be sent at some point
+ // after enabling the domain.
+ devtools_client_->GetHeadlessExperimental()->GetExperimental()->Enable(
+ headless_experimental::EnableParams::Builder().Build());
+ virtual_time_controller_->ScheduleRepeatingTask(
+ animation_task_.get(), animation_begin_frame_interval_);
+}
+
+CompositorController::~CompositorController() {
+ virtual_time_controller_->CancelRepeatingTask(animation_task_.get());
+ devtools_client_->GetHeadlessExperimental()
+ ->GetExperimental()
+ ->RemoveObserver(this);
+}
+
+void CompositorController::PostBeginFrame(
+ const base::Callback<void(std::unique_ptr<BeginFrameResult>)>&
+ begin_frame_complete_callback,
+ std::unique_ptr<ScreenshotParams> screenshot) {
+ // In certain nesting situations, we should not issue a BeginFrame immediately
+ // - for example, issuing a new BeginFrame within a BeginFrameCompleted or
+ // NeedsBeginFramesChanged event can upset the compositor. We avoid these
+ // situations by issuing our BeginFrames from a separately posted task.
+ task_runner_->PostTask(
+ FROM_HERE,
+ base::Bind(&CompositorController::BeginFrame,
+ weak_ptr_factory_.GetWeakPtr(), begin_frame_complete_callback,
+ base::Passed(&screenshot)));
+}
+
+void CompositorController::BeginFrame(
+ const base::Callback<void(std::unique_ptr<BeginFrameResult>)>&
+ begin_frame_complete_callback,
+ std::unique_ptr<ScreenshotParams> screenshot) {
+ DCHECK(!begin_frame_complete_callback_);
+ begin_frame_complete_callback_ = begin_frame_complete_callback;
+ if (needs_begin_frames_ || screenshot) {
+ auto params_builder = headless_experimental::BeginFrameParams::Builder();
+
+ // Use virtual time for frame time, so that rendering of animations etc. is
+ // aligned with virtual time progression.
+ base::Time frame_time = virtual_time_controller_->GetCurrentVirtualTime();
+ if (frame_time <= last_begin_frame_time_) {
+ // Frame time cannot go backwards or stop, so we issue another BeginFrame
+ // with a small time offset from the last BeginFrame's time instead.
+ frame_time =
+ last_begin_frame_time_ + base::TimeDelta::FromMicroseconds(1);
+ }
+ params_builder.SetFrameTime(frame_time.ToJsTime());
+ DCHECK_GT(frame_time, last_begin_frame_time_);
+ DCHECK_GT(frame_time.ToJsTime(), last_begin_frame_time_.ToJsTime());
+ last_begin_frame_time_ = frame_time;
+
+ params_builder.SetInterval(
+ animation_begin_frame_interval_.InMillisecondsF());
+
+ // TODO(eseckler): Set time fields. This requires obtaining the absolute
+ // virtual time stamp.
+ if (screenshot)
+ params_builder.SetScreenshot(std::move(screenshot));
+
+ devtools_client_->GetHeadlessExperimental()->GetExperimental()->BeginFrame(
+ params_builder.Build(),
+ base::Bind(&CompositorController::BeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()));
+ } else {
+ BeginFrameComplete(nullptr);
+ }
+}
+
+void CompositorController::BeginFrameComplete(
+ std::unique_ptr<BeginFrameResult> result) {
+ DCHECK(begin_frame_complete_callback_);
+ auto callback = begin_frame_complete_callback_;
+ begin_frame_complete_callback_.Reset();
+ callback.Run(std::move(result));
+ if (idle_callback_) {
+ auto idle_callback = idle_callback_;
+ idle_callback_.Reset();
+ idle_callback.Run();
+ }
+}
+
+void CompositorController::OnNeedsBeginFramesChanged(
+ const NeedsBeginFramesChangedParams& params) {
+ needs_begin_frames_ = params.GetNeedsBeginFrames();
+
+ // If needs_begin_frames_ became true again and we're waiting for the
+ // compositor or a main frame update, continue posting BeginFrames - provided
+ // there's none outstanding.
+ if (compositor_ready_callback_ && needs_begin_frames_ &&
+ !begin_frame_complete_callback_ &&
+ wait_for_compositor_ready_begin_frame_task_.IsCancelled()) {
+ PostBeginFrame(base::Bind(
+ &CompositorController::WaitForCompositorReadyBeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()));
+ } else if (main_frame_content_updated_callback_ && needs_begin_frames_ &&
+ !begin_frame_complete_callback_) {
+ PostWaitForMainFrameContentUpdateBeginFrame();
+ }
+}
+
+void CompositorController::OnMainFrameReadyForScreenshots(
+ const MainFrameReadyForScreenshotsParams& params) {
+ main_frame_ready_ = true;
+
+ // If a WaitForCompositorReadyBeginFrame is still scheduled, skip it.
+ if (!wait_for_compositor_ready_begin_frame_task_.IsCancelled()) {
+ wait_for_compositor_ready_begin_frame_task_.Cancel();
+ auto callback = compositor_ready_callback_;
+ compositor_ready_callback_.Reset();
+ callback.Run();
+ }
+}
+
+void CompositorController::WaitForCompositorReady(
+ const base::Closure& compositor_ready_callback) {
+ // We need to wait for the mainFrameReadyForScreenshots event, which will be
+ // issued once the renderer has submitted its first CompositorFrame in
+ // response to a BeginFrame. At that point, we know that the renderer
+ // compositor has initialized. We do this by issuing BeginFrames until we
+ // receive the event. To avoid bogging down the system with a flood of
+ // BeginFrames, we add a short delay between them.
+ // TODO(eseckler): Investigate if we can remove the need for these initial
+ // BeginFrames and the mainFrameReadyForScreenshots event, by making the
+ // compositor wait for the renderer in the very first BeginFrame, even if it
+ // isn't yet present in the surface hierarchy. Maybe surface synchronization
+ // can help here?
+ DCHECK(!begin_frame_complete_callback_);
+ DCHECK(!compositor_ready_callback_);
+
+ if (main_frame_ready_) {
+ compositor_ready_callback.Run();
+ return;
+ }
+
+ compositor_ready_callback_ = compositor_ready_callback;
+ if (needs_begin_frames_) {
+ // Post BeginFrames with a delay until the main frame becomes ready.
+ PostWaitForCompositorReadyBeginFrameTask();
+ }
+}
+
+void CompositorController::PostWaitForCompositorReadyBeginFrameTask() {
+ TRACE_EVENT0(
+ "headless",
+ "CompositorController::PostWaitForCompositorReadyBeginFrameTask");
+ wait_for_compositor_ready_begin_frame_task_.Reset(
+ base::Bind(&CompositorController::IssueWaitForCompositorReadyBeginFrame,
+ weak_ptr_factory_.GetWeakPtr()));
+
+ // We may receive the mainFrameReadyForScreenshots event before this task
+ // is run. In that case, we cancel it in OnMainFrameReadyForScreenshots to
+ // avoid another unnecessary BeginFrame.
+ task_runner_->PostDelayedTask(
+ FROM_HERE, wait_for_compositor_ready_begin_frame_task_.callback(),
+ wait_for_compositor_ready_begin_frame_delay_);
+}
+
+void CompositorController::IssueWaitForCompositorReadyBeginFrame() {
+ TRACE_EVENT0("headless",
+ "CompositorController::IssueWaitForCompositorReadyBeginFrame");
+ // No need for PostBeginFrame, since
+ // wait_for_compositor_ready_begin_frame_task_ has already been posted.
+ wait_for_compositor_ready_begin_frame_task_.Cancel();
+ BeginFrame(base::Bind(
+ &CompositorController::WaitForCompositorReadyBeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void CompositorController::WaitForCompositorReadyBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult>) {
+ TRACE_EVENT0(
+ "headless",
+ "CompositorController::WaitForCompositorReadyBeginFrameComplete");
+ DCHECK(compositor_ready_callback_);
+
+ if (main_frame_ready_) {
+ auto callback = compositor_ready_callback_;
+ compositor_ready_callback_.Reset();
+ callback.Run();
+ return;
+ }
+
+ // Continue posting more BeginFrames with a delay until the main frame
+ // becomes ready. If needs_begin_frames_ is false, it will eventually turn
+ // true again once the renderer's compositor has started up.
+ if (needs_begin_frames_)
+ PostWaitForCompositorReadyBeginFrameTask();
+}
+
+void CompositorController::WaitUntilIdle(const base::Closure& idle_callback) {
+ TRACE_EVENT_INSTANT1("headless", "CompositorController::WaitUntilIdle",
+ TRACE_EVENT_SCOPE_THREAD, "begin_frame_in_flight",
+ !!begin_frame_complete_callback_);
+ DCHECK(!idle_callback_);
+
+ if (!begin_frame_complete_callback_) {
+ idle_callback.Run();
+ return;
+ }
+
+ idle_callback_ = idle_callback;
+}
+
+void CompositorController::WaitForMainFrameContentUpdate(
+ const base::Closure& main_frame_content_updated_callback) {
+ TRACE_EVENT0("headless",
+ "CompositorController::WaitForMainFrameContentUpdate");
+ DCHECK(!begin_frame_complete_callback_);
+ DCHECK(!main_frame_content_updated_callback_);
+ main_frame_content_updated_callback_ = main_frame_content_updated_callback;
+
+ // Post BeginFrames until we see a main frame update.
+ if (needs_begin_frames_)
+ PostWaitForMainFrameContentUpdateBeginFrame();
+}
+
+void CompositorController::PostWaitForMainFrameContentUpdateBeginFrame() {
+ TRACE_EVENT0(
+ "headless",
+ "CompositorController::PostWaitForMainFrameContentUpdateBeginFrame");
+ DCHECK(main_frame_content_updated_callback_);
+ PostBeginFrame(base::Bind(
+ &CompositorController::WaitForMainFrameContentUpdateBeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void CompositorController::WaitForMainFrameContentUpdateBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult> result) {
+ if (!result)
+ return;
+
+ TRACE_EVENT1(
+ "headless",
+ "CompositorController::WaitForMainFrameContentUpdateBeginFrameComplete",
+ "main_frame_content_updated", result->GetMainFrameContentUpdated());
+ DCHECK(!begin_frame_complete_callback_);
+ DCHECK(main_frame_content_updated_callback_);
+
+ if (result->GetMainFrameContentUpdated()) {
+ auto callback = main_frame_content_updated_callback_;
+ main_frame_content_updated_callback_.Reset();
+ callback.Run();
+ return;
+ }
+
+ // Continue posting BeginFrames until we see a main frame update.
+ if (needs_begin_frames_)
+ PostWaitForMainFrameContentUpdateBeginFrame();
+}
+
+void CompositorController::CaptureScreenshot(
+ ScreenshotParamsFormat format,
+ int quality,
+ const base::Callback<void(const std::string&)>&
+ screenshot_captured_callback) {
+ TRACE_EVENT0("headless", "CompositorController::CaptureScreenshot");
+ DCHECK(!begin_frame_complete_callback_);
+ DCHECK(!screenshot_captured_callback_);
+ DCHECK(main_frame_ready_);
+
+ screenshot_captured_callback_ = screenshot_captured_callback;
+
+ // Let AnimationBeginFrameTask know that it doesn't need to issue an
+ // animation BeginFrame for the current virtual time pause.
+ animation_task_->CompositorControllerIssuingScreenshotBeginFrame();
+
+ PostBeginFrame(
+ base::Bind(&CompositorController::CaptureScreenshotBeginFrameComplete,
+ weak_ptr_factory_.GetWeakPtr()),
+ ScreenshotParams::Builder()
+ .SetFormat(format)
+ .SetQuality(quality)
+ .Build());
+}
+
+void CompositorController::CaptureScreenshotBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult> result) {
+ TRACE_EVENT1(
+ "headless", "CompositorController::CaptureScreenshotBeginFrameComplete",
+ "hasScreenshotData",
+ result ? std::to_string(result->HasScreenshotData()) : "invalid");
+ DCHECK(screenshot_captured_callback_);
+ if (result && result->HasScreenshotData()) {
+ // TODO(eseckler): Look into returning binary screenshot data via DevTools.
+ std::string decoded_data;
+ base::Base64Decode(result->GetScreenshotData(), &decoded_data);
+ auto callback = screenshot_captured_callback_;
+ screenshot_captured_callback_.Reset();
+ callback.Run(decoded_data);
+ } else {
+ LOG(ERROR) << "Screenshotting failed, BeginFrameResult has no data and "
+ "hasDamage is "
+ << (result ? std::to_string(result->HasScreenshotData())
+ : "invalid");
+ auto callback = screenshot_captured_callback_;
+ screenshot_captured_callback_.Reset();
+ callback.Run(std::string());
+ }
+}
+
+} // namespace headless
diff --git a/chromium/headless/public/util/compositor_controller.h b/chromium/headless/public/util/compositor_controller.h
new file mode 100644
index 00000000000..09012a4b377
--- /dev/null
+++ b/chromium/headless/public/util/compositor_controller.h
@@ -0,0 +1,138 @@
+// Copyright 2017 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.
+
+#ifndef HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
+#define HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
+
+#include "base/callback.h"
+#include "base/cancelable_callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequenced_task_runner.h"
+#include "base/time/time.h"
+#include "headless/public/devtools/domains/headless_experimental.h"
+#include "headless/public/headless_devtools_client.h"
+
+namespace headless {
+
+class VirtualTimeController;
+
+// Issues BeginFrames (Chromium's vsync signal) while virtual time advances and
+// and takes screenshots.
+class HEADLESS_EXPORT CompositorController
+ : public headless_experimental::ExperimentalObserver {
+ public:
+ using BeginFrameResult = headless_experimental::BeginFrameResult;
+ using MainFrameReadyForScreenshotsParams =
+ headless_experimental::MainFrameReadyForScreenshotsParams;
+ using NeedsBeginFramesChangedParams =
+ headless_experimental::NeedsBeginFramesChangedParams;
+ using ScreenshotParams = headless_experimental::ScreenshotParams;
+ using ScreenshotParamsFormat = headless_experimental::ScreenshotParamsFormat;
+
+ // |animation_begin_frame_interval| specifies the virtual time between
+ // individual BeginFrames while virtual time advances.
+ // |wait_for_compositor_ready_begin_frame_delay| is the real time delay
+ // between BeginFrames that are sent while waiting for the main frame
+ // compositor to become ready (real time).
+ CompositorController(
+ scoped_refptr<base::SequencedTaskRunner> task_runner,
+ HeadlessDevToolsClient* devtools_client,
+ VirtualTimeController* virtual_time_controller,
+ base::TimeDelta animation_begin_frame_interval,
+ base::TimeDelta wait_for_compositor_ready_begin_frame_delay);
+ ~CompositorController() override;
+
+ // Issues BeginFrames until the main frame's compositor has completed
+ // initialization. Should not be called again until
+ // |compositor_ready_callback| was run. Should only be called while no other
+ // BeginFrame is in flight.
+ void WaitForCompositorReady(const base::Closure& compositor_ready_callback);
+
+ // Executes |idle_callback| when no BeginFrames are in flight.
+ void WaitUntilIdle(const base::Closure& idle_callback);
+
+ // Issues BeginFrames until a new main frame update was committed. Should not
+ // be called again until |main_frame_content_updated_callback| was run. Should
+ // only be called while no other BeginFrame is in flight.
+ //
+ // This can be used in situations where e.g. the main frame size changes and
+ // we need to wait for the update to propagate down into a new surface before
+ // taking a screenshot.
+ // TODO(eseckler): Investigate whether we can replace this with surface
+ // synchronization or some other mechanism that avoids the need for additional
+ // BeginFrames.
+ void WaitForMainFrameContentUpdate(
+ const base::Closure& main_frame_content_updated_callback);
+
+ // Captures a screenshot by issuing a BeginFrame. |quality| is only valid for
+ // jpeg format screenshots, in range 0..100. Should not be called again until
+ // |screenshot_captured_callback| was run. Should only be called while no
+ // other BeginFrame is in flight and after the compositor is ready.
+ void CaptureScreenshot(ScreenshotParamsFormat format,
+ int quality,
+ const base::Callback<void(const std::string&)>&
+ screenshot_captured_callback);
+
+ private:
+ class AnimationBeginFrameTask;
+
+ // headless_experimental_::Observer implementation:
+ void OnNeedsBeginFramesChanged(
+ const NeedsBeginFramesChangedParams& params) override;
+ void OnMainFrameReadyForScreenshots(
+ const MainFrameReadyForScreenshotsParams& params) override;
+
+ // Posts a BeginFrame as a new task to avoid nesting it inside the current
+ // callstack, which can upset the compositor.
+ void PostBeginFrame(
+ const base::Callback<void(std::unique_ptr<BeginFrameResult>)>&
+ begin_frame_complete_callback,
+ std::unique_ptr<ScreenshotParams> screenshot = nullptr);
+ // Issues a BeginFrame synchronously and runs |begin_frame_complete_callback|
+ // when done. Should not be called again until |begin_frame_complete_callback|
+ // was run.
+ void BeginFrame(const base::Callback<void(std::unique_ptr<BeginFrameResult>)>&
+ begin_frame_complete_callback,
+ std::unique_ptr<ScreenshotParams> screenshot = nullptr);
+ // Runs the |begin_frame_complete_callback_| and the |idle_callback_| if set.
+ void BeginFrameComplete(std::unique_ptr<BeginFrameResult>);
+
+ // Posts a task to issue a BeginFrame while waiting for the
+ // mainFrameReadyForScreenshots event. The taks may be cancelled by the event.
+ void PostWaitForCompositorReadyBeginFrameTask();
+ void IssueWaitForCompositorReadyBeginFrame();
+ void WaitForCompositorReadyBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult>);
+
+ // Posts a BeginFrame while waiting for a main frame content update.
+ void PostWaitForMainFrameContentUpdateBeginFrame();
+ void WaitForMainFrameContentUpdateBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult> result);
+
+ void CaptureScreenshotBeginFrameComplete(
+ std::unique_ptr<BeginFrameResult> result);
+
+ scoped_refptr<base::SequencedTaskRunner> task_runner_;
+ HeadlessDevToolsClient* devtools_client_; // NOT OWNED
+ VirtualTimeController* virtual_time_controller_; // NOT OWNED
+ std::unique_ptr<AnimationBeginFrameTask> animation_task_;
+ base::Closure compositor_ready_callback_;
+ base::Closure idle_callback_;
+ base::Closure main_frame_content_updated_callback_;
+ base::Callback<void(const std::string&)> screenshot_captured_callback_;
+ base::Callback<void(std::unique_ptr<BeginFrameResult>)>
+ begin_frame_complete_callback_;
+ base::CancelableClosure wait_for_compositor_ready_begin_frame_task_;
+ base::TimeDelta animation_begin_frame_interval_;
+ base::TimeDelta wait_for_compositor_ready_begin_frame_delay_;
+ bool needs_begin_frames_ = false;
+ bool main_frame_ready_ = false;
+ base::Time last_begin_frame_time_ = base::Time::UnixEpoch();
+ base::WeakPtrFactory<CompositorController> weak_ptr_factory_;
+};
+
+} // namespace headless
+
+#endif // HEADLESS_PUBLIC_UTIL_COMPOSITOR_CONTROLLER_H_
diff --git a/chromium/headless/public/util/compositor_controller_browsertest.cc b/chromium/headless/public/util/compositor_controller_browsertest.cc
new file mode 100644
index 00000000000..5169490626e
--- /dev/null
+++ b/chromium/headless/public/util/compositor_controller_browsertest.cc
@@ -0,0 +1,110 @@
+// Copyright 2017 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 "headless/public/util/compositor_controller.h"
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "build/build_config.h"
+#include "cc/base/switches.h"
+#include "content/public/common/content_switches.h"
+#include "content/public/test/browser_test.h"
+#include "headless/public/devtools/domains/runtime.h"
+#include "headless/public/headless_browser.h"
+#include "headless/public/headless_devtools_client.h"
+#include "headless/public/util/virtual_time_controller.h"
+#include "headless/test/headless_browser_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace headless {
+
+// BeginFrameControl is not supported on Mac.
+#if !defined(OS_MACOSX)
+
+namespace {
+static constexpr base::TimeDelta kAnimationFrameInterval =
+ base::TimeDelta::FromMilliseconds(16);
+static constexpr base::TimeDelta kWaitForCompositorReadyFrameDelay =
+ base::TimeDelta::FromMilliseconds(20);
+} // namespace
+
+class CompositorControllerBrowserTest
+ : public HeadlessAsyncDevTooledBrowserTest {
+ public:
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ HeadlessAsyncDevTooledBrowserTest::SetUpCommandLine(command_line);
+ command_line->AppendSwitch(cc::switches::kRunAllCompositorStagesBeforeDraw);
+ command_line->AppendSwitch(switches::kDisableNewContentRenderingTimeout);
+ }
+
+ bool GetEnableBeginFrameControl() override { return true; }
+
+ void RunDevTooledTest() override {
+ virtual_time_controller_ =
+ base::MakeUnique<VirtualTimeController>(devtools_client_.get());
+ compositor_controller_ = base::MakeUnique<CompositorController>(
+ browser()->BrowserMainThread(), devtools_client_.get(),
+ virtual_time_controller_.get(), kAnimationFrameInterval,
+ kWaitForCompositorReadyFrameDelay);
+
+ compositor_controller_->WaitForCompositorReady(
+ base::Bind(&CompositorControllerBrowserTest::OnCompositorReady,
+ base::Unretained(this)));
+ }
+
+ void OnCompositorReady() {
+ // Request animation frames in the main frame.
+ devtools_client_->GetRuntime()->Evaluate(
+ "window.rafCount = 0;"
+ "function onRaf(timestamp) {"
+ " window.rafCount++;"
+ " window.requestAnimationFrame(onRaf);"
+ "};"
+ "window.requestAnimationFrame(onRaf);",
+ base::Bind(&CompositorControllerBrowserTest::OnRafReady,
+ base::Unretained(this)));
+ }
+
+ void OnRafReady(std::unique_ptr<runtime::EvaluateResult> result) {
+ EXPECT_NE(nullptr, result);
+ EXPECT_FALSE(result->HasExceptionDetails());
+
+ virtual_time_controller_->GrantVirtualTimeBudget(
+ emulation::VirtualTimePolicy::ADVANCE,
+ kNumFrames * kAnimationFrameInterval, base::Bind([]() {}),
+ base::Bind(&CompositorControllerBrowserTest::OnVirtualTimeBudgetExpired,
+ base::Unretained(this)));
+ }
+
+ void OnVirtualTimeBudgetExpired() {
+ // Get animation frame count.
+ devtools_client_->GetRuntime()->Evaluate(
+ "window.rafCount",
+ base::Bind(&CompositorControllerBrowserTest::OnGetRafCount,
+ base::Unretained(this)));
+ }
+
+ void OnGetRafCount(std::unique_ptr<runtime::EvaluateResult> result) {
+ EXPECT_NE(nullptr, result);
+ EXPECT_FALSE(result->HasExceptionDetails());
+
+ EXPECT_EQ(kNumFrames, result->GetResult()->GetValue()->GetInt());
+ FinishAsynchronousTest();
+ }
+
+ private:
+ static constexpr int kNumFrames = 3;
+
+ std::unique_ptr<VirtualTimeController> virtual_time_controller_;
+ std::unique_ptr<CompositorController> compositor_controller_;
+};
+
+/* static */
+constexpr int CompositorControllerBrowserTest::kNumFrames;
+
+HEADLESS_ASYNC_DEVTOOLED_TEST_F(CompositorControllerBrowserTest);
+
+#endif // !defined(OS_MACOSX)
+
+} // namespace headless
diff --git a/chromium/headless/public/util/compositor_controller_unittest.cc b/chromium/headless/public/util/compositor_controller_unittest.cc
new file mode 100644
index 00000000000..33aa032de0c
--- /dev/null
+++ b/chromium/headless/public/util/compositor_controller_unittest.cc
@@ -0,0 +1,511 @@
+// Copyright 2017 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 "headless/public/util/compositor_controller.h"
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/json/json_writer.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/test_simple_task_runner.h"
+#include "headless/public/internal/headless_devtools_client_impl.h"
+#include "headless/public/util/testing/mock_devtools_agent_host.h"
+#include "headless/public/util/virtual_time_controller.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace headless {
+
+namespace {
+static constexpr base::TimeDelta kAnimationFrameInterval =
+ base::TimeDelta::FromMilliseconds(16);
+static constexpr base::TimeDelta kWaitForCompositorReadyFrameDelay =
+ base::TimeDelta::FromMilliseconds(20);
+} // namespace
+
+using testing::Return;
+using testing::_;
+
+class TestVirtualTimeController : public VirtualTimeController {
+ public:
+ TestVirtualTimeController(HeadlessDevToolsClient* devtools_client)
+ : VirtualTimeController(devtools_client) {}
+ ~TestVirtualTimeController() override = default;
+
+ MOCK_METHOD4(GrantVirtualTimeBudget,
+ void(emulation::VirtualTimePolicy policy,
+ base::TimeDelta budget,
+ const base::Callback<void()>& set_up_complete_callback,
+ const base::Callback<void()>& budget_expired_callback));
+ MOCK_METHOD2(ScheduleRepeatingTask,
+ void(RepeatingTask* task, base::TimeDelta interval));
+ MOCK_METHOD1(CancelRepeatingTask, void(RepeatingTask* task));
+
+ MOCK_CONST_METHOD0(GetVirtualTimeBase, base::Time());
+ MOCK_CONST_METHOD0(GetCurrentVirtualTimeOffset, base::TimeDelta());
+};
+
+class CompositorControllerTest : public ::testing::Test {
+ protected:
+ CompositorControllerTest() {
+ task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
+ client_.SetTaskRunnerForTests(task_runner_);
+ mock_host_ = base::MakeRefCounted<MockDevToolsAgentHost>();
+
+ EXPECT_CALL(*mock_host_, IsAttached()).WillOnce(Return(false));
+ EXPECT_CALL(*mock_host_, AttachClient(&client_));
+ client_.AttachToHost(mock_host_.get());
+ virtual_time_controller_ =
+ base::MakeUnique<TestVirtualTimeController>(&client_);
+ EXPECT_CALL(*virtual_time_controller_,
+ ScheduleRepeatingTask(_, kAnimationFrameInterval))
+ .WillOnce(testing::SaveArg<0>(&task_));
+ ExpectHeadlessExperimentalEnable();
+ controller_ = base::MakeUnique<CompositorController>(
+ task_runner_, &client_, virtual_time_controller_.get(),
+ kAnimationFrameInterval, kWaitForCompositorReadyFrameDelay);
+ EXPECT_NE(nullptr, task_);
+ }
+
+ ~CompositorControllerTest() override {
+ EXPECT_CALL(*virtual_time_controller_, CancelRepeatingTask(_));
+ }
+
+ void ExpectHeadlessExperimentalEnable() {
+ last_command_id_ += 2;
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ base::StringPrintf(
+ "{\"id\":%d,\"method\":\"HeadlessExperimental.enable\","
+ "\"params\":{}}",
+ last_command_id_)))
+ .WillOnce(Return(true));
+ }
+
+ void ExpectVirtualTime(double base, double offset) {
+ auto base_time = base::Time::FromJsTime(base);
+ auto offset_delta = base::TimeDelta::FromMilliseconds(offset);
+ EXPECT_CALL(*virtual_time_controller_, GetVirtualTimeBase())
+ .WillOnce(Return(base_time));
+ EXPECT_CALL(*virtual_time_controller_, GetCurrentVirtualTimeOffset())
+ .WillOnce(Return(offset_delta));
+
+ // Next BeginFrame's time should be the virtual time provided it has
+ // progressed.
+ base::Time virtual_time = base_time + offset_delta;
+ if (virtual_time > next_begin_frame_time_)
+ next_begin_frame_time_ = virtual_time;
+ }
+
+ void ExpectBeginFrame(std::unique_ptr<headless_experimental::ScreenshotParams>
+ screenshot_params = nullptr) {
+ last_command_id_ += 2;
+ base::DictionaryValue params;
+ auto builder =
+ std::move(headless_experimental::BeginFrameParams::Builder()
+ .SetFrameTime(next_begin_frame_time_.ToJsTime())
+ .SetInterval(kAnimationFrameInterval.InMillisecondsF()));
+ if (screenshot_params)
+ builder.SetScreenshot(std::move(screenshot_params));
+ // Subsequent BeginFrames should have a later timestamp.
+ next_begin_frame_time_ += base::TimeDelta::FromMicroseconds(1);
+
+ std::string params_json;
+ auto params_value = builder.Build()->Serialize();
+ base::JSONWriter::Write(*params_value, &params_json);
+
+ EXPECT_CALL(
+ *mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ base::StringPrintf(
+ "{\"id\":%d,\"method\":\"HeadlessExperimental.beginFrame\","
+ "\"params\":%s}",
+ last_command_id_, params_json.c_str())))
+ .WillOnce(Return(true));
+ }
+
+ void SendBeginFrameReply(bool has_damage,
+ bool main_frame_content_updated,
+ const std::string& screenshot_data) {
+ auto result = headless_experimental::BeginFrameResult::Builder()
+ .SetHasDamage(has_damage)
+ .SetMainFrameContentUpdated(main_frame_content_updated)
+ .Build();
+ if (screenshot_data.length())
+ result->SetScreenshotData(screenshot_data);
+ std::string result_json;
+ auto result_value = result->Serialize();
+ base::JSONWriter::Write(*result_value, &result_json);
+
+ client_.DispatchProtocolMessage(
+ mock_host_.get(),
+ base::StringPrintf("{\"id\":%d,\"result\":%s}", last_command_id_,
+ result_json.c_str()));
+ }
+
+ void SendNeedsBeginFramesEvent(bool needs_begin_frames) {
+ client_.DispatchProtocolMessage(
+ mock_host_.get(),
+ base::StringPrintf("{\"method\":\"HeadlessExperimental."
+ "needsBeginFramesChanged\",\"params\":{"
+ "\"needsBeginFrames\":%s}}",
+ needs_begin_frames ? "true" : "false"));
+ // Events are dispatched asynchronously.
+ task_runner_->RunPendingTasks();
+ }
+
+ void SendMainFrameReadyForScreenshotsEvent() {
+ client_.DispatchProtocolMessage(mock_host_.get(),
+ "{\"method\":\"HeadlessExperimental."
+ "mainFrameReadyForScreenshots\",\"params\":"
+ "{}}");
+ // Events are dispatched asynchronously.
+ task_runner_->RunPendingTasks();
+ }
+
+ scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
+ scoped_refptr<MockDevToolsAgentHost> mock_host_;
+ HeadlessDevToolsClientImpl client_;
+ std::unique_ptr<TestVirtualTimeController> virtual_time_controller_;
+ std::unique_ptr<CompositorController> controller_;
+ int last_command_id_ = -2;
+ TestVirtualTimeController::RepeatingTask* task_ = nullptr;
+ base::Time next_begin_frame_time_ =
+ base::Time::UnixEpoch() + base::TimeDelta::FromMicroseconds(1);
+};
+
+TEST_F(CompositorControllerTest, WaitForCompositorReady) {
+ // Shouldn't send any commands yet as no needsBeginFrames event was sent yet.
+ bool ready = false;
+ controller_->WaitForCompositorReady(
+ base::Bind([](bool* ready) { *ready = true; }, &ready));
+ EXPECT_FALSE(ready);
+
+ // Sends BeginFrames with delay while they are needed.
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ SendBeginFrameReply(true, false, std::string());
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ EXPECT_EQ(kWaitForCompositorReadyFrameDelay,
+ task_runner_->NextPendingTaskDelay());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ SendBeginFrameReply(false, false, std::string());
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ EXPECT_EQ(kWaitForCompositorReadyFrameDelay,
+ task_runner_->NextPendingTaskDelay());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // No new BeginFrames are scheduled when BeginFrames are not needed.
+ SendNeedsBeginFramesEvent(false);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Restarts sending BeginFrames when they are needed again.
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // Stops sending BeginFrames when main frame becomes ready.
+ SendMainFrameReadyForScreenshotsEvent();
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ EXPECT_FALSE(ready);
+ SendBeginFrameReply(true, true, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(ready);
+}
+
+TEST_F(CompositorControllerTest, CaptureScreenshot) {
+ SendMainFrameReadyForScreenshotsEvent();
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ bool done = false;
+ controller_->CaptureScreenshot(
+ headless_experimental::ScreenshotParamsFormat::PNG, 100,
+ base::Bind(
+ [](bool* done, const std::string& screenshot_data) {
+ *done = true;
+ EXPECT_EQ("test", screenshot_data);
+ },
+ &done));
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame(
+ headless_experimental::ScreenshotParams::Builder()
+ .SetFormat(headless_experimental::ScreenshotParamsFormat::PNG)
+ .SetQuality(100)
+ .Build());
+ task_runner_->RunPendingTasks();
+
+ std::string base64;
+ base::Base64Encode("test", &base64);
+ SendBeginFrameReply(true, true, base64);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(done);
+}
+
+TEST_F(CompositorControllerTest, WaitForMainFrameContentUpdate) {
+ bool updated = false;
+ controller_->WaitForMainFrameContentUpdate(
+ base::Bind([](bool* updated) { *updated = true; }, &updated));
+ EXPECT_FALSE(updated);
+
+ // Sends BeginFrames while they are needed.
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ SendBeginFrameReply(true, false, std::string());
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ EXPECT_EQ(base::TimeDelta(), task_runner_->NextPendingTaskDelay());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ SendBeginFrameReply(false, false, std::string());
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ EXPECT_EQ(base::TimeDelta(), task_runner_->NextPendingTaskDelay());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // No new BeginFrames are scheduled when BeginFrames are not needed.
+ SendNeedsBeginFramesEvent(false);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Restarts sending BeginFrames when they are needed again.
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // Stops sending BeginFrames when an main frame update is included.
+ SendMainFrameReadyForScreenshotsEvent();
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ EXPECT_FALSE(updated);
+ SendBeginFrameReply(true, true, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(updated);
+}
+
+TEST_F(CompositorControllerTest, SendsAnimationFrames) {
+ bool can_continue = false;
+ auto continue_callback = base::Bind(
+ [](bool* can_continue) { *can_continue = true; }, &can_continue);
+
+ // Task doesn't block virtual time request.
+ task_->BudgetRequested(base::TimeDelta(),
+ base::TimeDelta::FromMilliseconds(100),
+ continue_callback);
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+
+ // Doesn't send BeginFrames before virtual time started.
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Sends a BeginFrame after interval elapsed.
+ task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF());
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // Lets virtual time continue after BeginFrame was completed.
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+
+ // Sends another BeginFrame after next interval elapsed.
+ task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(1000, kAnimationFrameInterval.InMillisecondsF() * 2);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // Lets virtual time continue after BeginFrame was completed.
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+}
+
+TEST_F(CompositorControllerTest, SkipsAnimationFrameForScreenshots) {
+ bool can_continue = false;
+ auto continue_callback = base::Bind(
+ [](bool* can_continue) { *can_continue = true; }, &can_continue);
+
+ SendMainFrameReadyForScreenshotsEvent();
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Doesn't send a BeginFrame after interval elapsed if a screenshot is taken
+ // instead.
+ task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ controller_->CaptureScreenshot(
+ headless_experimental::ScreenshotParamsFormat::PNG, 100,
+ base::Bind([](const std::string&) {}));
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame(
+ headless_experimental::ScreenshotParams::Builder()
+ .SetFormat(headless_experimental::ScreenshotParamsFormat::PNG)
+ .SetQuality(100)
+ .Build());
+ task_runner_->RunPendingTasks();
+
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+}
+
+TEST_F(CompositorControllerTest,
+ PostponesAnimationFrameWhenBudgetExpired) {
+ bool can_continue = false;
+ auto continue_callback = base::Bind(
+ [](bool* can_continue) { *can_continue = true; }, &can_continue);
+
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Doesn't send a BeginFrame after interval elapsed if the budget also
+ // expired.
+ task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ task_->BudgetExpired(kAnimationFrameInterval);
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+ // Flush cancelled task.
+ task_runner_->RunPendingTasks();
+
+ // Sends a BeginFrame when more virtual time budget is requested.
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_->BudgetRequested(kAnimationFrameInterval,
+ base::TimeDelta::FromMilliseconds(100),
+ continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+}
+
+TEST_F(CompositorControllerTest,
+ SkipsAnimationFrameWhenBudgetExpiredAndScreenshotWasTaken) {
+ bool can_continue = false;
+ auto continue_callback = base::Bind(
+ [](bool* can_continue) { *can_continue = true; }, &can_continue);
+
+ SendMainFrameReadyForScreenshotsEvent();
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // Doesn't send a BeginFrame after interval elapsed if the budget also
+ // expired.
+ task_->IntervalElapsed(kAnimationFrameInterval, continue_callback);
+ EXPECT_FALSE(can_continue);
+
+ task_->BudgetExpired(kAnimationFrameInterval);
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+ // Flush cancelled task.
+ task_runner_->RunPendingTasks();
+
+ controller_->CaptureScreenshot(
+ headless_experimental::ScreenshotParamsFormat::PNG, 100,
+ base::Bind([](const std::string&) {}));
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame(
+ headless_experimental::ScreenshotParams::Builder()
+ .SetFormat(headless_experimental::ScreenshotParamsFormat::PNG)
+ .SetQuality(100)
+ .Build());
+ task_runner_->RunPendingTasks();
+
+ // Sends a BeginFrame when more virtual time budget is requested.
+ task_->BudgetRequested(kAnimationFrameInterval,
+ base::TimeDelta::FromMilliseconds(100),
+ continue_callback);
+ EXPECT_TRUE(can_continue);
+ can_continue = false;
+}
+
+TEST_F(CompositorControllerTest, WaitUntilIdle) {
+ bool idle = false;
+ auto idle_callback = base::Bind([](bool* idle) { *idle = true; }, &idle);
+
+ SendNeedsBeginFramesEvent(true);
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+
+ // WaitUntilIdle executes callback immediately if no BeginFrame is active.
+ controller_->WaitUntilIdle(idle_callback);
+ EXPECT_TRUE(idle);
+ idle = false;
+
+ // Send a BeginFrame.
+ task_->IntervalElapsed(kAnimationFrameInterval, base::Bind([]() {}));
+
+ EXPECT_TRUE(task_runner_->HasPendingTask());
+ ExpectVirtualTime(0, 0);
+ ExpectBeginFrame();
+ task_runner_->RunPendingTasks();
+
+ // WaitUntilIdle only executes callback after BeginFrame was completed.
+ controller_->WaitUntilIdle(idle_callback);
+ EXPECT_FALSE(idle);
+
+ SendBeginFrameReply(false, false, std::string());
+ EXPECT_FALSE(task_runner_->HasPendingTask());
+ EXPECT_TRUE(idle);
+ idle = false;
+}
+
+} // namespace headless
diff --git a/chromium/headless/public/util/deterministic_dispatcher.cc b/chromium/headless/public/util/deterministic_dispatcher.cc
index bfa75e5b714..e9507c53722 100644
--- a/chromium/headless/public/util/deterministic_dispatcher.cc
+++ b/chromium/headless/public/util/deterministic_dispatcher.cc
@@ -20,7 +20,7 @@ DeterministicDispatcher::DeterministicDispatcher(
navigation_in_progress_(false),
weak_ptr_factory_(this) {}
-DeterministicDispatcher::~DeterministicDispatcher() {}
+DeterministicDispatcher::~DeterministicDispatcher() = default;
void DeterministicDispatcher::JobCreated(ManagedDispatchURLRequestJob* job) {
base::AutoLock lock(lock_);
@@ -144,7 +144,7 @@ void DeterministicDispatcher::NavigationDoneTask() {
DeterministicDispatcher::Request::Request() = default;
DeterministicDispatcher::Request::Request(Request&&) = default;
-DeterministicDispatcher::Request::~Request() {}
+DeterministicDispatcher::Request::~Request() = default;
DeterministicDispatcher::Request::Request(
ManagedDispatchURLRequestJob* url_request)
diff --git a/chromium/headless/public/util/deterministic_dispatcher_test.cc b/chromium/headless/public/util/deterministic_dispatcher_test.cc
index 48b30e298c5..5b769800ec8 100644
--- a/chromium/headless/public/util/deterministic_dispatcher_test.cc
+++ b/chromium/headless/public/util/deterministic_dispatcher_test.cc
@@ -23,8 +23,8 @@ namespace headless {
class DeterministicDispatcherTest : public ::testing::Test {
protected:
- DeterministicDispatcherTest() {}
- ~DeterministicDispatcherTest() override {}
+ DeterministicDispatcherTest() = default;
+ ~DeterministicDispatcherTest() override = default;
void SetUp() override {
deterministic_dispatcher_.reset(
@@ -135,7 +135,7 @@ class NavigationRequestForTest : public NavigationRequest {
explicit NavigationRequestForTest(base::Closure* done_closure)
: done_closure_(done_closure) {}
- ~NavigationRequestForTest() override {}
+ ~NavigationRequestForTest() override = default;
// NavigationRequest implementation:
void StartProcessing(base::Closure done_callback) override {
diff --git a/chromium/headless/public/util/deterministic_http_protocol_handler.cc b/chromium/headless/public/util/deterministic_http_protocol_handler.cc
index 8ff8e37df81..84cc84875be 100644
--- a/chromium/headless/public/util/deterministic_http_protocol_handler.cc
+++ b/chromium/headless/public/util/deterministic_http_protocol_handler.cc
@@ -19,8 +19,8 @@ namespace headless {
class DeterministicHttpProtocolHandler::NopGenericURLRequestJobDelegate
: public GenericURLRequestJob::Delegate {
public:
- NopGenericURLRequestJobDelegate() {}
- ~NopGenericURLRequestJobDelegate() override {}
+ NopGenericURLRequestJobDelegate() = default;
+ ~NopGenericURLRequestJobDelegate() override = default;
void OnResourceLoadFailed(const Request* request, net::Error error) override {
}
diff --git a/chromium/headless/public/util/error_reporter.cc b/chromium/headless/public/util/error_reporter.cc
index 9daa0198670..7f4df916177 100644
--- a/chromium/headless/public/util/error_reporter.cc
+++ b/chromium/headless/public/util/error_reporter.cc
@@ -7,12 +7,13 @@
#include <sstream>
#include "base/logging.h"
+#include "base/strings/string_util.h"
namespace headless {
-ErrorReporter::ErrorReporter() {}
+ErrorReporter::ErrorReporter() = default;
-ErrorReporter::~ErrorReporter() {}
+ErrorReporter::~ErrorReporter() = default;
#if DCHECK_IS_ON()
void ErrorReporter::Push() {
@@ -48,6 +49,10 @@ void ErrorReporter::AddError(base::StringPiece description) {
bool ErrorReporter::HasErrors() const {
return !errors_.empty();
}
+
+std::string ErrorReporter::ToString() const {
+ return base::JoinString(errors_, ", ");
+}
#endif // DCHECK_IS_ON()
} // namespace headless
diff --git a/chromium/headless/public/util/error_reporter.h b/chromium/headless/public/util/error_reporter.h
index 2fd432f3f2d..c38c60c8940 100644
--- a/chromium/headless/public/util/error_reporter.h
+++ b/chromium/headless/public/util/error_reporter.h
@@ -39,13 +39,17 @@ class HEADLESS_EXPORT ErrorReporter {
// Returns a list of reported errors.
const std::vector<std::string>& errors() const { return errors_; }
-#else // DCHECK_IS_ON()
- inline void Push() {}
- inline void Pop() {}
- inline void SetName(const char* name) {}
- inline void AddError(base::StringPiece description) {}
- inline bool HasErrors() const { return false; }
- std::vector<std::string> errors() const { return std::vector<std::string>(); }
+
+ // Returns a string containing all the errors concatenated together.
+ std::string ToString() const;
+#else // DCHECK_IS_ON()
+ void Push() {}
+ void Pop() {}
+ void SetName(const char* name) {}
+ void AddError(base::StringPiece description) {}
+ bool HasErrors() const { return false; }
+ std::vector<std::string> errors() const { return {}; }
+ std::string ToString() const { return ""; }
#endif // DCHECK_IS_ON()
private:
diff --git a/chromium/headless/public/util/expedited_dispatcher.cc b/chromium/headless/public/util/expedited_dispatcher.cc
index 649321a7ad0..1fe445cca88 100644
--- a/chromium/headless/public/util/expedited_dispatcher.cc
+++ b/chromium/headless/public/util/expedited_dispatcher.cc
@@ -16,7 +16,7 @@ ExpeditedDispatcher::ExpeditedDispatcher(
scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner)
: io_thread_task_runner_(std::move(io_thread_task_runner)) {}
-ExpeditedDispatcher::~ExpeditedDispatcher() {}
+ExpeditedDispatcher::~ExpeditedDispatcher() = default;
void ExpeditedDispatcher::JobCreated(ManagedDispatchURLRequestJob*) {}
diff --git a/chromium/headless/public/util/expedited_dispatcher_test.cc b/chromium/headless/public/util/expedited_dispatcher_test.cc
index dacc82deab0..05571f6df02 100644
--- a/chromium/headless/public/util/expedited_dispatcher_test.cc
+++ b/chromium/headless/public/util/expedited_dispatcher_test.cc
@@ -23,8 +23,8 @@ namespace headless {
class ExpeditedDispatcherTest : public ::testing::Test {
protected:
- ExpeditedDispatcherTest() {}
- ~ExpeditedDispatcherTest() override {}
+ ExpeditedDispatcherTest() = default;
+ ~ExpeditedDispatcherTest() override = default;
void SetUp() override {
expedited_dispatcher_.reset(new ExpeditedDispatcher(loop_.task_runner()));
@@ -92,7 +92,7 @@ class NavigationRequestForTest : public NavigationRequest {
explicit NavigationRequestForTest(base::Closure* done_closure)
: done_closure_(done_closure) {}
- ~NavigationRequestForTest() override {}
+ ~NavigationRequestForTest() override = default;
// NavigationRequest implementation:
void StartProcessing(base::Closure done_callback) override {
diff --git a/chromium/headless/public/util/generic_url_request_job.cc b/chromium/headless/public/util/generic_url_request_job.cc
index ff2dfe86d38..e865fae2f50 100644
--- a/chromium/headless/public/util/generic_url_request_job.cc
+++ b/chromium/headless/public/util/generic_url_request_job.cc
@@ -8,7 +8,6 @@
#include <algorithm>
#include "base/logging.h"
-#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/render_frame_host.h"
@@ -21,6 +20,7 @@
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/upload_bytes_element_reader.h"
+#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_store.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
@@ -78,7 +78,7 @@ void GenericURLRequestJob::SetExtraRequestHeaders(
void GenericURLRequestJob::Start() {
PrepareCookies(request_->url(), request_->method(),
- url::Origin(request_->site_for_cookies()));
+ url::Origin::Create(request_->site_for_cookies()));
}
void GenericURLRequestJob::PrepareCookies(const GURL& rewritten_url,
@@ -90,7 +90,7 @@ void GenericURLRequestJob::PrepareCookies(const GURL& rewritten_url,
options.set_include_httponly();
// See net::URLRequestHttpJob::AddCookieHeaderAndStart().
- url::Origin requested_origin(rewritten_url);
+ url::Origin requested_origin = url::Origin::Create(rewritten_url);
if (net::registry_controlled_domains::SameDomainOrHost(
requested_origin, site_for_cookies,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
@@ -118,7 +118,7 @@ void GenericURLRequestJob::OnCookiesAvailable(
DCHECK(origin_task_runner_->RunsTasksInCurrentSequence());
// TODO(alexclarke): Set user agent.
// Pass cookies, the referrer and any extra headers into the fetch request.
- std::string cookie = net::CookieStore::BuildCookieLine(cookie_list);
+ std::string cookie = net::CanonicalCookie::BuildCookieLine(cookie_list);
if (!cookie.empty())
extra_request_headers_.SetHeader(net::HttpRequestHeaders::kCookie, cookie);
@@ -299,99 +299,133 @@ bool GenericURLRequestJob::IsAsync() const {
return true;
}
+bool GenericURLRequestJob::IsBrowserSideFetch() const {
+ if (!request_resource_info_)
+ return false;
+ return request_resource_info_->GetFrameTreeNodeId() != -1;
+}
+
namespace {
void CompletionCallback(int* dest, base::Closure* quit_closure, int value) {
*dest = value;
quit_closure->Run();
}
+
+bool ReadAndAppendBytes(const std::unique_ptr<net::UploadElementReader>& reader,
+ std::string& post_data) {
+ // TODO(alexclarke): Consider changing the interface of
+ // GenericURLRequestJob::GetPostData to use a callback which would let us
+ // avoid the nested run loops below.
+
+ // Read the POST bytes.
+ uint64_t size_to_stop_at = post_data.size() + reader->GetContentLength();
+ const size_t block_size = 1024;
+ scoped_refptr<net::IOBuffer> read_buffer(new net::IOBuffer(block_size));
+ while (post_data.size() < size_to_stop_at) {
+ base::Closure quit_closure;
+ int bytes_read = reader->Read(
+ read_buffer.get(), block_size,
+ base::Bind(&CompletionCallback, &bytes_read, &quit_closure));
+
+ if (bytes_read == net::ERR_IO_PENDING) {
+ base::RunLoop nested_run_loop(base::RunLoop::Type::kNestableTasksAllowed);
+ quit_closure = nested_run_loop.QuitClosure();
+ nested_run_loop.Run();
+ }
+
+ // Bail out if an error occured.
+ if (bytes_read < 0)
+ return false;
+
+ post_data.append(read_buffer->data(), bytes_read);
+ }
+ return true;
+}
} // namespace
-std::string GenericURLRequestJob::GetPostData() const {
+const std::vector<std::unique_ptr<net::UploadElementReader>>*
+GenericURLRequestJob::GetInitializedReaders() const {
if (!request_->has_upload())
- return "";
+ return nullptr;
const net::UploadDataStream* stream = request_->get_upload();
if (!stream->GetElementReaders())
- return "";
+ return nullptr;
if (stream->GetElementReaders()->size() == 0)
- return "";
+ return nullptr;
- DCHECK_EQ(1u, stream->GetElementReaders()->size());
- const std::unique_ptr<net::UploadElementReader>& reader =
- (*stream->GetElementReaders())[0];
- // If |reader| is actually an UploadBytesElementReader we can get the data
- // directly (should be faster than the horrible stuff below).
- const net::UploadBytesElementReader* bytes_reader = reader->AsBytesReader();
- if (bytes_reader)
- return std::string(bytes_reader->bytes(), bytes_reader->length());
+ const std::vector<std::unique_ptr<net::UploadElementReader>>* readers =
+ stream->GetElementReaders();
- // TODO(alexclarke): Consider changing the interface of
- // GenericURLRequestJob::GetPostData to use a callback which would let us
- // avoid the nested run loops below.
+ // Initialize the readers, this necessary because some of them will segfault
+ // if you try to call any method without initializing first.
+ for (size_t i = 0; i < readers->size(); ++i) {
+ const std::unique_ptr<net::UploadElementReader>& reader = (*readers)[i];
+ if (!reader)
+ continue;
- // Initialize the reader.
- {
base::Closure quit_closure;
int init_result = reader->Init(
base::Bind(&CompletionCallback, &init_result, &quit_closure));
if (init_result == net::ERR_IO_PENDING) {
- base::RunLoop nested_run_loop;
- base::MessageLoop::ScopedNestableTaskAllower allow_nested(
- base::MessageLoop::current());
+ base::RunLoop nested_run_loop(base::RunLoop::Type::kNestableTasksAllowed);
quit_closure = nested_run_loop.QuitClosure();
nested_run_loop.Run();
}
if (init_result != net::OK)
- return "";
+ return nullptr;
}
- // Read the POST bytes.
- uint64_t content_length = reader->GetContentLength();
- std::string post_data;
- post_data.reserve(content_length);
- const size_t block_size = 1024;
- scoped_refptr<net::IOBuffer> read_buffer(new net::IOBuffer(block_size));
- while (post_data.size() < content_length) {
- base::Closure quit_closure;
- int bytes_read = reader->Read(
- read_buffer.get(), block_size,
- base::Bind(&CompletionCallback, &bytes_read, &quit_closure));
+ return readers;
+}
- if (bytes_read == net::ERR_IO_PENDING) {
- base::MessageLoop::ScopedNestableTaskAllower allow_nested(
- base::MessageLoop::current());
- base::RunLoop nested_run_loop;
- quit_closure = nested_run_loop.QuitClosure();
- nested_run_loop.Run();
- }
+std::string GenericURLRequestJob::GetPostData() const {
+ const std::vector<std::unique_ptr<net::UploadElementReader>>* readers =
+ GetInitializedReaders();
- // Bail out if an error occured.
- if (bytes_read < 0)
- return "";
+ if (!readers)
+ return "";
- post_data.append(read_buffer->data(), bytes_read);
+ uint64_t total_content_length = 0;
+ for (size_t i = 0; i < readers->size(); ++i) {
+ const std::unique_ptr<net::UploadElementReader>& reader = (*readers)[i];
+ total_content_length += reader->GetContentLength();
+ }
+ std::string post_data;
+ post_data.reserve(total_content_length);
+
+ for (size_t i = 0; i < readers->size(); ++i) {
+ const std::unique_ptr<net::UploadElementReader>& reader = (*readers)[i];
+ // If |reader| is actually an UploadBytesElementReader we can get the data
+ // directly.
+ const net::UploadBytesElementReader* bytes_reader = reader->AsBytesReader();
+ if (bytes_reader) {
+ post_data.append(bytes_reader->bytes(), bytes_reader->length());
+ } else {
+ if (!ReadAndAppendBytes(reader, post_data))
+ break; // Bail out if something went wrong.
+ }
}
return post_data;
}
uint64_t GenericURLRequestJob::GetPostDataSize() const {
- if (!request_->has_upload())
- return 0;
-
- const net::UploadDataStream* stream = request_->get_upload();
- if (!stream->GetElementReaders())
- return 0;
+ const std::vector<std::unique_ptr<net::UploadElementReader>>* readers =
+ GetInitializedReaders();
- if (stream->GetElementReaders()->size() == 0)
+ if (!readers)
return 0;
- DCHECK_EQ(1u, stream->GetElementReaders()->size());
- const std::unique_ptr<net::UploadElementReader>& reader =
- (*stream->GetElementReaders())[0];
- return reader->GetContentLength();
+ uint64_t total_content_length = 0;
+ for (size_t i = 0; i < readers->size(); ++i) {
+ const std::unique_ptr<net::UploadElementReader>& reader = (*readers)[i];
+ if (reader)
+ total_content_length += reader->GetContentLength();
+ }
+ return total_content_length;
}
} // namespace headless
diff --git a/chromium/headless/public/util/generic_url_request_job.h b/chromium/headless/public/util/generic_url_request_job.h
index 192e7545104..06c90f7296e 100644
--- a/chromium/headless/public/util/generic_url_request_job.h
+++ b/chromium/headless/public/util/generic_url_request_job.h
@@ -24,6 +24,7 @@
namespace net {
class IOBuffer;
+class UploadElementReader;
} // namespace net
namespace content {
@@ -56,6 +57,9 @@ class HEADLESS_EXPORT Request {
// Returns the size of the POST data, if any, from the net::URLRequest.
virtual uint64_t GetPostDataSize() const = 0;
+ // Returns true if the fetch was issues by the browser.
+ virtual bool IsBrowserSideFetch() const = 0;
+
enum class ResourceType {
MAIN_FRAME = 0,
SUB_FRAME = 1,
@@ -159,6 +163,7 @@ class HEADLESS_EXPORT GenericURLRequestJob
uint64_t GetPostDataSize() const override;
ResourceType GetResourceType() const override;
bool IsAsync() const override;
+ bool IsBrowserSideFetch() const override;
private:
void PrepareCookies(const GURL& rewritten_url,
@@ -168,6 +173,9 @@ class HEADLESS_EXPORT GenericURLRequestJob
const std::string& method,
const net::CookieList& cookie_list);
+ const std::vector<std::unique_ptr<net::UploadElementReader>>*
+ GetInitializedReaders() const;
+
std::unique_ptr<URLFetcher> url_fetcher_;
net::HttpRequestHeaders extra_request_headers_;
scoped_refptr<net::HttpResponseHeaders> response_headers_;
diff --git a/chromium/headless/public/util/generic_url_request_job_test.cc b/chromium/headless/public/util/generic_url_request_job_test.cc
index e278720295b..777837d4b03 100644
--- a/chromium/headless/public/util/generic_url_request_job_test.cc
+++ b/chromium/headless/public/util/generic_url_request_job_test.cc
@@ -28,6 +28,7 @@
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
+using testing::NotNull;
using testing::_;
std::ostream& operator<<(std::ostream& os, const base::DictionaryValue& value) {
@@ -57,13 +58,15 @@ class MockDelegate : public MockGenericURLRequestJobDelegate {
class MockFetcher : public URLFetcher {
public:
MockFetcher(base::DictionaryValue* fetch_request,
+ std::string* received_post_data,
std::map<std::string, std::string>* json_fetch_reply_map,
base::Callback<void(const Request*)>* on_request_callback)
: json_fetch_reply_map_(json_fetch_reply_map),
fetch_request_(fetch_request),
+ received_post_data_(received_post_data),
on_request_callback_(on_request_callback) {}
- ~MockFetcher() override {}
+ ~MockFetcher() override = default;
void StartFetch(const Request* request,
ResultListener* result_listener) override {
@@ -80,9 +83,9 @@ class MockFetcher : public URLFetcher {
headers->SetString(it.name(), it.value());
}
fetch_request_->Set("headers", std::move(headers));
- std::string post_data = request->GetPostData();
- if (!post_data.empty())
- fetch_request_->SetString("post_data", std::move(post_data));
+ *received_post_data_ = request->GetPostData();
+ if (!received_post_data_->empty() && received_post_data_->size() < 1024)
+ fetch_request_->SetString("post_data", *received_post_data_);
const auto find_it = json_fetch_reply_map_->find(url);
if (find_it == json_fetch_reply_map_->end()) {
@@ -97,9 +100,6 @@ class MockFetcher : public URLFetcher {
base::DictionaryValue* reply_dictionary;
ASSERT_TRUE(fetch_reply->GetAsDictionary(&reply_dictionary));
- std::string final_url;
- ASSERT_TRUE(reply_dictionary->GetString("url", &final_url));
- ASSERT_TRUE(reply_dictionary->GetString("data", &response_data_));
base::DictionaryValue* reply_headers_dictionary;
ASSERT_TRUE(
reply_dictionary->GetDictionary("headers", &reply_headers_dictionary));
@@ -107,10 +107,8 @@ class MockFetcher : public URLFetcher {
new net::HttpResponseHeaders(""));
for (base::DictionaryValue::Iterator it(*reply_headers_dictionary);
!it.IsAtEnd(); it.Advance()) {
- std::string value;
- ASSERT_TRUE(it.value().GetAsString(&value));
- response_headers->AddHeader(
- base::StringPrintf("%s: %s", it.key().c_str(), value.c_str()));
+ response_headers->AddHeader(base::StringPrintf(
+ "%s: %s", it.key().c_str(), it.value().GetString().c_str()));
}
// Set the fields needed for tracing, so that we can check
@@ -118,14 +116,20 @@ class MockFetcher : public URLFetcher {
net::LoadTimingInfo load_timing_info;
load_timing_info.send_start = base::TimeTicks::Max();
load_timing_info.receive_headers_end = base::TimeTicks::Max();
+ const base::Value* final_url_value = reply_dictionary->FindKey("url");
+ ASSERT_THAT(final_url_value, NotNull());
+ const base::Value* response_data_value = reply_dictionary->FindKey("data");
+ ASSERT_THAT(response_data_value, NotNull());
+ response_data_ = response_data_value->GetString();
result_listener->OnFetchComplete(
- GURL(final_url), std::move(response_headers), response_data_.c_str(),
- response_data_.size(), load_timing_info);
+ GURL(final_url_value->GetString()), std::move(response_headers),
+ response_data_.c_str(), response_data_.size(), load_timing_info);
}
private:
- std::map<std::string, std::string>* json_fetch_reply_map_; // NOT OWNED
- base::DictionaryValue* fetch_request_; // NOT OWNED
+ std::map<std::string, std::string>* json_fetch_reply_map_; // NOT OWNED
+ base::DictionaryValue* fetch_request_; // NOT OWNED
+ std::string* received_post_data_; // NOT OWNED
base::Callback<void(const Request*)>* on_request_callback_; // NOT OWNED
std::string response_data_; // Here to ensure the required lifetime.
};
@@ -135,11 +139,13 @@ class MockProtocolHandler : public net::URLRequestJobFactory::ProtocolHandler {
// Details of the fetch will be stored in |fetch_request|.
// The fetch response will be created from parsing |json_fetch_reply_map|.
MockProtocolHandler(base::DictionaryValue* fetch_request,
+ std::string* received_post_data,
std::map<std::string, std::string>* json_fetch_reply_map,
URLRequestDispatcher* dispatcher,
GenericURLRequestJob::Delegate* job_delegate,
base::Callback<void(const Request*)>* on_request_callback)
: fetch_request_(fetch_request),
+ received_post_data_(received_post_data),
json_fetch_reply_map_(json_fetch_reply_map),
job_delegate_(job_delegate),
dispatcher_(dispatcher),
@@ -151,13 +157,15 @@ class MockProtocolHandler : public net::URLRequestJobFactory::ProtocolHandler {
net::NetworkDelegate* network_delegate) const override {
return new GenericURLRequestJob(
request, network_delegate, dispatcher_,
- base::MakeUnique<MockFetcher>(fetch_request_, json_fetch_reply_map_,
+ base::MakeUnique<MockFetcher>(fetch_request_, received_post_data_,
+ json_fetch_reply_map_,
on_request_callback_),
job_delegate_, nullptr);
}
private:
base::DictionaryValue* fetch_request_; // NOT OWNED
+ std::string* received_post_data_; // NOT OWNED
std::map<std::string, std::string>* json_fetch_reply_map_; // NOT OWNED
GenericURLRequestJob::Delegate* job_delegate_; // NOT OWNED
URLRequestDispatcher* dispatcher_; // NOT OWNED
@@ -170,9 +178,10 @@ class GenericURLRequestJobTest : public testing::Test {
public:
GenericURLRequestJobTest() : dispatcher_(message_loop_.task_runner()) {
url_request_job_factory_.SetProtocolHandler(
- "https", base::WrapUnique(new MockProtocolHandler(
- &fetch_request_, &json_fetch_reply_map_, &dispatcher_,
- &job_delegate_, &on_request_callback_)));
+ "https",
+ base::WrapUnique(new MockProtocolHandler(
+ &fetch_request_, &received_post_data_, &json_fetch_reply_map_,
+ &dispatcher_, &job_delegate_, &on_request_callback_)));
url_request_context_.set_job_factory(&url_request_job_factory_);
url_request_context_.set_cookie_store(&cookie_store_);
}
@@ -219,6 +228,7 @@ class GenericURLRequestJobTest : public testing::Test {
MockURLRequestDelegate request_delegate_;
base::DictionaryValue fetch_request_; // The request sent to MockFetcher.
+ std::string received_post_data_; // The POST data (useful if large).
std::map<std::string, std::string>
json_fetch_reply_map_; // Replies to be sent by MockFetcher.
MockDelegate job_delegate_;
@@ -304,6 +314,44 @@ TEST_F(GenericURLRequestJobTest, BasicPostRequestParams) {
EXPECT_THAT(fetch_request_, MatchesJson(expected_request_json));
}
+TEST_F(GenericURLRequestJobTest, LargePostData) {
+ json_fetch_reply_map_["https://example.com/"] = R"(
+ {
+ "url": "https://example.com",
+ "data": "Reply",
+ "headers": {
+ "Content-Type": "text/html; charset=UTF-8"
+ }
+ })";
+
+ std::unique_ptr<net::URLRequest> request(url_request_context_.CreateRequest(
+ GURL("https://example.com"), net::DEFAULT_PRIORITY, &request_delegate_,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ request->SetReferrer("https://referrer.example.com");
+ request->SetExtraRequestHeaderByName("Extra-Header", "Value", true);
+ request->SetExtraRequestHeaderByName("User-Agent", "TestBrowser", true);
+ request->SetExtraRequestHeaderByName("Accept", "text/plain", true);
+ request->set_method("POST");
+
+ std::vector<char> post_data(4000000);
+ for (size_t i = 0; i < post_data.size(); i++)
+ post_data[i] = i & 127;
+
+ request->set_upload(net::ElementsUploadDataStream::CreateWithReader(
+ base::MakeUnique<net::UploadBytesElementReader>(&post_data[0],
+ post_data.size()),
+ 0));
+ request->Start();
+ base::RunLoop().RunUntilIdle();
+
+ // Make sure we captured the expected post.
+ for (size_t i = 0; i < received_post_data_.size(); i++) {
+ EXPECT_EQ(static_cast<char>(i & 127), post_data[i]);
+ }
+
+ EXPECT_EQ(post_data.size(), received_post_data_.size());
+}
+
TEST_F(GenericURLRequestJobTest, BasicRequestProperties) {
std::string reply = R"(
{
@@ -622,4 +670,76 @@ TEST_F(GenericURLRequestJobTest, GetPostDataAsync) {
EXPECT_EQ(post_data_size, post_data.size());
}
+TEST_F(GenericURLRequestJobTest, LargePostDataNotByteReader) {
+ json_fetch_reply_map_["https://example.com/"] = R"(
+ {
+ "url": "https://example.com",
+ "data": "Reply",
+ "headers": {
+ "Content-Type": "text/html; charset=UTF-8"
+ }
+ })";
+
+ std::unique_ptr<net::URLRequest> request(url_request_context_.CreateRequest(
+ GURL("https://example.com"), net::DEFAULT_PRIORITY, &request_delegate_,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ request->SetReferrer("https://referrer.example.com");
+ request->SetExtraRequestHeaderByName("Extra-Header", "Value", true);
+ request->SetExtraRequestHeaderByName("User-Agent", "TestBrowser", true);
+ request->SetExtraRequestHeaderByName("Accept", "text/plain", true);
+ request->set_method("POST");
+
+ std::string post_data;
+ post_data.reserve(4000000);
+ for (size_t i = 0; i < post_data.size(); i++)
+ post_data.at(i) = i & 127;
+
+ request->set_upload(net::ElementsUploadDataStream::CreateWithReader(
+ base::MakeUnique<ByteAtATimeUploadElementReader>(post_data), 0));
+ request->Start();
+ base::RunLoop().RunUntilIdle();
+
+ // Make sure we captured the expected post.
+ for (size_t i = 0; i < received_post_data_.size(); i++) {
+ EXPECT_EQ(static_cast<char>(i & 127), post_data[i]);
+ }
+
+ EXPECT_EQ(post_data.size(), received_post_data_.size());
+}
+
+TEST_F(GenericURLRequestJobTest, PostWithMultipleElements) {
+ json_fetch_reply_map_["https://example.com/"] = R"(
+ {
+ "url": "https://example.com",
+ "data": "Reply",
+ "headers": {
+ "Content-Type": "text/html; charset=UTF-8"
+ }
+ })";
+
+ std::unique_ptr<net::URLRequest> request(url_request_context_.CreateRequest(
+ GURL("https://example.com"), net::DEFAULT_PRIORITY, &request_delegate_,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ request->SetReferrer("https://referrer.example.com");
+ request->SetExtraRequestHeaderByName("Extra-Header", "Value", true);
+ request->SetExtraRequestHeaderByName("User-Agent", "TestBrowser", true);
+ request->SetExtraRequestHeaderByName("Accept", "text/plain", true);
+ request->set_method("POST");
+
+ std::vector<std::unique_ptr<net::UploadElementReader>> element_readers;
+ element_readers.push_back(
+ base::MakeUnique<ByteAtATimeUploadElementReader>("Does "));
+ element_readers.push_back(
+ base::MakeUnique<ByteAtATimeUploadElementReader>("this "));
+ element_readers.push_back(
+ base::MakeUnique<ByteAtATimeUploadElementReader>("work?"));
+
+ request->set_upload(base::MakeUnique<net::ElementsUploadDataStream>(
+ std::move(element_readers), 0));
+ request->Start();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ("Does this work?", received_post_data_);
+}
+
} // namespace headless
diff --git a/chromium/headless/public/util/http_url_fetcher.cc b/chromium/headless/public/util/http_url_fetcher.cc
index 20e44ea93de..0e83ebb970f 100644
--- a/chromium/headless/public/util/http_url_fetcher.cc
+++ b/chromium/headless/public/util/http_url_fetcher.cc
@@ -116,7 +116,7 @@ HttpURLFetcher::Delegate::Delegate(
request_->Start();
}
-HttpURLFetcher::Delegate::~Delegate() {}
+HttpURLFetcher::Delegate::~Delegate() = default;
void HttpURLFetcher::Delegate::OnAuthRequired(
net::URLRequest* request,
@@ -228,7 +228,7 @@ HttpURLFetcher::HttpURLFetcher(
const net::URLRequestContext* url_request_context)
: url_request_context_(url_request_context) {}
-HttpURLFetcher::~HttpURLFetcher() {}
+HttpURLFetcher::~HttpURLFetcher() = default;
void HttpURLFetcher::StartFetch(const Request* request,
ResultListener* result_listener) {
diff --git a/chromium/headless/public/util/in_memory_protocol_handler.cc b/chromium/headless/public/util/in_memory_protocol_handler.cc
index 27c076551db..49dd8c01657 100644
--- a/chromium/headless/public/util/in_memory_protocol_handler.cc
+++ b/chromium/headless/public/util/in_memory_protocol_handler.cc
@@ -8,8 +8,8 @@
namespace headless {
-InMemoryProtocolHandler::InMemoryProtocolHandler() {}
-InMemoryProtocolHandler::~InMemoryProtocolHandler() {}
+InMemoryProtocolHandler::InMemoryProtocolHandler() = default;
+InMemoryProtocolHandler::~InMemoryProtocolHandler() = default;
net::URLRequestJob* InMemoryProtocolHandler::MaybeCreateJob(
net::URLRequest* request,
diff --git a/chromium/headless/public/util/in_memory_request_job.cc b/chromium/headless/public/util/in_memory_request_job.cc
index 5fa84151980..4e9451869c4 100644
--- a/chromium/headless/public/util/in_memory_request_job.cc
+++ b/chromium/headless/public/util/in_memory_request_job.cc
@@ -21,7 +21,7 @@ InMemoryRequestJob::InMemoryRequestJob(
data_offset_(0),
weak_factory_(this) {}
-InMemoryRequestJob::~InMemoryRequestJob() {}
+InMemoryRequestJob::~InMemoryRequestJob() = default;
void InMemoryRequestJob::Start() {
// Start reading asynchronously so that all error reporting and data
diff --git a/chromium/headless/public/util/moveable_auto_lock.h b/chromium/headless/public/util/moveable_auto_lock.h
new file mode 100644
index 00000000000..a13cf78b110
--- /dev/null
+++ b/chromium/headless/public/util/moveable_auto_lock.h
@@ -0,0 +1,64 @@
+// Copyright 2017 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.
+
+#ifndef HEADLESS_PUBLIC_UTIL_MOVEABLE_AUTO_LOCK_H_
+#define HEADLESS_PUBLIC_UTIL_MOVEABLE_AUTO_LOCK_H_
+
+#include "base/synchronization/lock.h"
+
+namespace headless {
+
+class MoveableAutoLock {
+ public:
+ explicit MoveableAutoLock(base::Lock& lock) : lock_(lock), moved_(false) {
+ lock_.Acquire();
+ }
+
+ MoveableAutoLock(MoveableAutoLock&& other)
+ : lock_(other.lock_), moved_(other.moved_) {
+ lock_.AssertAcquired();
+ other.moved_ = true;
+ }
+
+ ~MoveableAutoLock() {
+ if (moved_)
+ return;
+ lock_.AssertAcquired();
+ lock_.Release();
+ }
+
+ private:
+ base::Lock& lock_;
+ bool moved_;
+ DISALLOW_COPY_AND_ASSIGN(MoveableAutoLock);
+};
+
+// RAII helper to allow threadsafe access to an object guarded by a lock.
+template <class T>
+class LockedPtr {
+ public:
+ LockedPtr(MoveableAutoLock&& lock, T* object)
+ : lock_(std::move(lock)), object_(object) {}
+
+ LockedPtr(LockedPtr&& other)
+ : lock_(std::move(other.lock_)), object_(other.object_) {}
+
+ T* operator->() { return object_; }
+
+ T& operator*() { return *object_; }
+
+ T* Get() { return object_; }
+
+ explicit operator bool() const { return object_; }
+
+ private:
+ MoveableAutoLock lock_;
+ T* object_;
+
+ DISALLOW_COPY_AND_ASSIGN(LockedPtr);
+};
+
+} // namespace headless
+
+#endif // HEADLESS_PUBLIC_UTIL_MOVEABLE_AUTO_LOCK_H_
diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.cc b/chromium/headless/public/util/testing/generic_url_request_mocks.cc
index c811651ec15..9c20668d5d9 100644
--- a/chromium/headless/public/util/testing/generic_url_request_mocks.cc
+++ b/chromium/headless/public/util/testing/generic_url_request_mocks.cc
@@ -20,7 +20,7 @@ namespace headless {
MockGenericURLRequestJobDelegate::MockGenericURLRequestJobDelegate()
: main_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()) {}
-MockGenericURLRequestJobDelegate::~MockGenericURLRequestJobDelegate() {}
+MockGenericURLRequestJobDelegate::~MockGenericURLRequestJobDelegate() = default;
void MockGenericURLRequestJobDelegate::OnResourceLoadFailed(
const Request* request,
@@ -34,8 +34,8 @@ void MockGenericURLRequestJobDelegate::OnResourceLoadComplete(
size_t body_size) {}
// MockCookieStore
-MockCookieStore::MockCookieStore() {}
-MockCookieStore::~MockCookieStore() {}
+MockCookieStore::MockCookieStore() = default;
+MockCookieStore::~MockCookieStore() = default;
void MockCookieStore::SetCookieWithOptionsAsync(
const GURL& url,
@@ -45,22 +45,6 @@ void MockCookieStore::SetCookieWithOptionsAsync(
CHECK(false);
}
-void MockCookieStore::SetCookieWithDetailsAsync(const GURL& url,
- const std::string& name,
- const std::string& value,
- const std::string& domain,
- const std::string& path,
- base::Time creation_time,
- base::Time expiration_time,
- base::Time last_access_time,
- bool secure,
- bool http_only,
- net::CookieSameSite same_site,
- net::CookiePriority priority,
- SetCookiesCallback callback) {
- CHECK(false);
-}
-
void MockCookieStore::SetCanonicalCookieAsync(
std::unique_ptr<net::CanonicalCookie> cookie,
bool secure_source,
@@ -161,10 +145,11 @@ void MockCookieStore::SendCookies(const GURL& url,
}
// MockURLRequestDelegate
-MockURLRequestDelegate::MockURLRequestDelegate() {}
-MockURLRequestDelegate::~MockURLRequestDelegate() {}
+MockURLRequestDelegate::MockURLRequestDelegate() = default;
+MockURLRequestDelegate::~MockURLRequestDelegate() = default;
-void MockURLRequestDelegate::OnResponseStarted(net::URLRequest* request) {}
+void MockURLRequestDelegate::OnResponseStarted(net::URLRequest* request,
+ int net_error) {}
void MockURLRequestDelegate::OnReadCompleted(net::URLRequest* request,
int bytes_read) {}
const std::string& MockURLRequestDelegate::response_data() const {
diff --git a/chromium/headless/public/util/testing/generic_url_request_mocks.h b/chromium/headless/public/util/testing/generic_url_request_mocks.h
index 36b049114ea..e1ff2ab014f 100644
--- a/chromium/headless/public/util/testing/generic_url_request_mocks.h
+++ b/chromium/headless/public/util/testing/generic_url_request_mocks.h
@@ -50,20 +50,6 @@ class MockCookieStore : public net::CookieStore {
const net::CookieOptions& options,
SetCookiesCallback callback) override;
- void SetCookieWithDetailsAsync(const GURL& url,
- const std::string& name,
- const std::string& value,
- const std::string& domain,
- const std::string& path,
- base::Time creation_time,
- base::Time expiration_time,
- base::Time last_access_time,
- bool secure,
- bool http_only,
- net::CookieSameSite same_site,
- net::CookiePriority priority,
- SetCookiesCallback callback) override;
-
void SetCanonicalCookieAsync(std::unique_ptr<net::CanonicalCookie> cookie,
bool secure_source,
bool modify_http_only,
@@ -129,7 +115,7 @@ class MockURLRequestDelegate : public net::URLRequest::Delegate {
MockURLRequestDelegate();
~MockURLRequestDelegate() override;
- void OnResponseStarted(net::URLRequest* request) override;
+ void OnResponseStarted(net::URLRequest* request, int net_error) override;
void OnReadCompleted(net::URLRequest* request, int bytes_read) override;
const std::string& response_data() const;
const net::IOBufferWithSize* metadata() const;
diff --git a/chromium/headless/public/util/testing/mock_devtools_agent_host.cc b/chromium/headless/public/util/testing/mock_devtools_agent_host.cc
index 78959226a1d..683b4f99d33 100644
--- a/chromium/headless/public/util/testing/mock_devtools_agent_host.cc
+++ b/chromium/headless/public/util/testing/mock_devtools_agent_host.cc
@@ -6,8 +6,8 @@
namespace headless {
-MockDevToolsAgentHost::MockDevToolsAgentHost() {}
+MockDevToolsAgentHost::MockDevToolsAgentHost() = default;
-MockDevToolsAgentHost::~MockDevToolsAgentHost() {}
+MockDevToolsAgentHost::~MockDevToolsAgentHost() = default;
} // namespace headless
diff --git a/chromium/headless/public/util/testing/test_in_memory_protocol_handler.cc b/chromium/headless/public/util/testing/test_in_memory_protocol_handler.cc
index 8864b0b7b44..8883fb1d107 100644
--- a/chromium/headless/public/util/testing/test_in_memory_protocol_handler.cc
+++ b/chromium/headless/public/util/testing/test_in_memory_protocol_handler.cc
@@ -13,12 +13,9 @@ namespace headless {
class TestInMemoryProtocolHandler::MockURLFetcher : public URLFetcher {
public:
- MockURLFetcher(
- TestInMemoryProtocolHandler* protocol_handler,
- scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner)
- : protocol_handler_(protocol_handler),
- io_thread_task_runner_(io_thread_task_runner) {}
- ~MockURLFetcher() override {}
+ explicit MockURLFetcher(TestInMemoryProtocolHandler* protocol_handler)
+ : protocol_handler_(protocol_handler) {}
+ ~MockURLFetcher() override = default;
// URLFetcher implementation:
void StartFetch(const Request* request,
@@ -30,12 +27,10 @@ class TestInMemoryProtocolHandler::MockURLFetcher : public URLFetcher {
DCHECK_NE(devtools_frame_id, "") << " For url " << url;
protocol_handler_->RegisterUrl(url.spec(), devtools_frame_id);
- if (protocol_handler_->simulate_slow_fetch()) {
- io_thread_task_runner_->PostDelayedTask(
- FROM_HERE,
- base::Bind(&MockURLFetcher::FinishFetch, base::Unretained(this),
- result_listener, url),
- base::TimeDelta::FromMilliseconds(100));
+ if (protocol_handler_->request_deferrer()) {
+ protocol_handler_->request_deferrer()->OnRequest(
+ url, base::Bind(&MockURLFetcher::FinishFetch, base::Unretained(this),
+ result_listener, url));
} else {
FinishFetch(result_listener, url);
}
@@ -46,6 +41,7 @@ class TestInMemoryProtocolHandler::MockURLFetcher : public URLFetcher {
protocol_handler_->GetResponse(url.spec());
if (response) {
net::LoadTimingInfo load_timing_info;
+ load_timing_info.receive_headers_end = base::TimeTicks::Now();
result_listener->OnFetchCompleteExtractHeaders(
url, response->data.c_str(), response->data.size(), load_timing_info);
} else {
@@ -55,7 +51,6 @@ class TestInMemoryProtocolHandler::MockURLFetcher : public URLFetcher {
private:
TestInMemoryProtocolHandler* protocol_handler_; // NOT OWNED.
- scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_;
DISALLOW_COPY_AND_ASSIGN(MockURLFetcher);
};
@@ -63,8 +58,8 @@ class TestInMemoryProtocolHandler::MockURLFetcher : public URLFetcher {
class TestInMemoryProtocolHandler::TestDelegate
: public GenericURLRequestJob::Delegate {
public:
- TestDelegate() {}
- ~TestDelegate() override {}
+ TestDelegate() = default;
+ ~TestDelegate() override = default;
// GenericURLRequestJob::Delegate implementation:
void OnResourceLoadFailed(const Request* request, net::Error error) override {
@@ -85,14 +80,14 @@ class TestInMemoryProtocolHandler::TestDelegate
TestInMemoryProtocolHandler::TestInMemoryProtocolHandler(
scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner,
- bool simulate_slow_fetch)
+ RequestDeferrer* request_deferrer)
: test_delegate_(new TestDelegate()),
dispatcher_(new ExpeditedDispatcher(io_thread_task_runner)),
headless_browser_context_(nullptr),
- simulate_slow_fetch_(simulate_slow_fetch),
+ request_deferrer_(request_deferrer),
io_thread_task_runner_(io_thread_task_runner) {}
-TestInMemoryProtocolHandler::~TestInMemoryProtocolHandler() {}
+TestInMemoryProtocolHandler::~TestInMemoryProtocolHandler() = default;
void TestInMemoryProtocolHandler::SetHeadlessBrowserContext(
HeadlessBrowserContext* headless_browser_context) {
@@ -119,8 +114,7 @@ net::URLRequestJob* TestInMemoryProtocolHandler::MaybeCreateJob(
return new GenericURLRequestJob(
request, network_delegate, dispatcher_.get(),
base::MakeUnique<MockURLFetcher>(
- const_cast<TestInMemoryProtocolHandler*>(this),
- io_thread_task_runner_),
+ const_cast<TestInMemoryProtocolHandler*>(this)),
test_delegate_.get(), headless_browser_context_);
}
diff --git a/chromium/headless/public/util/testing/test_in_memory_protocol_handler.h b/chromium/headless/public/util/testing/test_in_memory_protocol_handler.h
index c2d6d1f48f0..b3265725097 100644
--- a/chromium/headless/public/util/testing/test_in_memory_protocol_handler.h
+++ b/chromium/headless/public/util/testing/test_in_memory_protocol_handler.h
@@ -19,9 +19,21 @@ class HeadlessBrowserContext;
class TestInMemoryProtocolHandler
: public net::URLRequestJobFactory::ProtocolHandler {
public:
+ class RequestDeferrer {
+ public:
+ virtual ~RequestDeferrer() {}
+
+ // Notifies that the target page has made a request for the |url|. The
+ // request will not be completed until |complete_request| is run.
+ // NOTE this will be called on the IO thread.
+ virtual void OnRequest(const GURL& url, base::Closure complete_request) = 0;
+ };
+
+ // Note |request_deferrer| is optional. If the test doesn't need to control
+ // when resources are fulfilled then pass in nullptr.
TestInMemoryProtocolHandler(
scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner,
- bool simulate_slow_fetch);
+ RequestDeferrer* request_deferrer);
~TestInMemoryProtocolHandler() override;
@@ -30,6 +42,7 @@ class TestInMemoryProtocolHandler
struct Response {
Response() {}
+ Response(const std::string& data) : data(data) {}
Response(const std::string& body, const std::string& mime_type)
: data("HTTP/1.1 200 OK\r\nContent-Type: " + mime_type + "\r\n\r\n" +
body) {}
@@ -60,7 +73,7 @@ class TestInMemoryProtocolHandler
url_to_devtools_frame_id_[url] = devtools_frame_id;
}
- bool simulate_slow_fetch() const { return simulate_slow_fetch_; }
+ RequestDeferrer* request_deferrer() const { return request_deferrer_; }
class TestDelegate;
class MockURLFetcher;
@@ -73,7 +86,7 @@ class TestInMemoryProtocolHandler
HeadlessBrowserContext* headless_browser_context_;
std::map<std::string, std::string> url_to_devtools_frame_id_;
std::vector<std::string> urls_requested_;
- bool simulate_slow_fetch_;
+ RequestDeferrer* request_deferrer_; // NOT OWNED.
scoped_refptr<base::SingleThreadTaskRunner> io_thread_task_runner_;
DISALLOW_COPY_AND_ASSIGN(TestInMemoryProtocolHandler);
diff --git a/chromium/headless/public/util/throttled_dispatcher.cc b/chromium/headless/public/util/throttled_dispatcher.cc
index 48efca74255..0e767605568 100644
--- a/chromium/headless/public/util/throttled_dispatcher.cc
+++ b/chromium/headless/public/util/throttled_dispatcher.cc
@@ -17,7 +17,7 @@ ThrottledDispatcher::ThrottledDispatcher(
: ExpeditedDispatcher(std::move(io_thread_task_runner)),
requests_paused_(false) {}
-ThrottledDispatcher::~ThrottledDispatcher() {}
+ThrottledDispatcher::~ThrottledDispatcher() = default;
void ThrottledDispatcher::PauseRequests() {
base::AutoLock lock(lock_);
diff --git a/chromium/headless/public/util/throttled_dispatcher_test.cc b/chromium/headless/public/util/throttled_dispatcher_test.cc
index 5e3ba3f382b..d37c9fb1b80 100644
--- a/chromium/headless/public/util/throttled_dispatcher_test.cc
+++ b/chromium/headless/public/util/throttled_dispatcher_test.cc
@@ -23,8 +23,8 @@ namespace headless {
class ThrottledDispatcherTest : public ::testing::Test {
protected:
- ThrottledDispatcherTest() {}
- ~ThrottledDispatcherTest() override {}
+ ThrottledDispatcherTest() = default;
+ ~ThrottledDispatcherTest() override = default;
void SetUp() override {
throttled_dispatcher_.reset(new ThrottledDispatcher(loop_.task_runner()));
diff --git a/chromium/headless/public/util/virtual_time_controller.cc b/chromium/headless/public/util/virtual_time_controller.cc
index 5505bcb3d8f..3dd107dc628 100644
--- a/chromium/headless/public/util/virtual_time_controller.cc
+++ b/chromium/headless/public/util/virtual_time_controller.cc
@@ -13,8 +13,10 @@ namespace headless {
using base::TimeDelta;
VirtualTimeController::VirtualTimeController(
- HeadlessDevToolsClient* devtools_client)
- : devtools_client_(devtools_client) {
+ HeadlessDevToolsClient* devtools_client,
+ int max_task_starvation_count)
+ : devtools_client_(devtools_client),
+ max_task_starvation_count_(max_task_starvation_count) {
devtools_client_->GetEmulation()->GetExperimental()->AddObserver(this);
}
@@ -24,7 +26,7 @@ VirtualTimeController::~VirtualTimeController() {
void VirtualTimeController::GrantVirtualTimeBudget(
emulation::VirtualTimePolicy policy,
- int budget_ms,
+ base::TimeDelta budget,
const base::Callback<void()>& set_up_complete_callback,
const base::Callback<void()>& budget_expired_callback) {
DCHECK(!set_up_complete_callback_);
@@ -32,13 +34,19 @@ void VirtualTimeController::GrantVirtualTimeBudget(
set_up_complete_callback_ = set_up_complete_callback;
budget_expired_callback_ = budget_expired_callback;
- requested_budget_ = TimeDelta::FromMilliseconds(budget_ms);
- accumulated_time_ = TimeDelta();
+ requested_budget_ = budget;
+ accumulated_budget_portion_ = TimeDelta();
virtual_time_policy_ = policy;
// Notify tasks of new budget request.
for (TaskEntry& entry : tasks_)
- NotifyTaskBudgetRequested(&entry, budget_ms);
+ entry.ready_to_advance = false;
+
+ iterating_over_tasks_ = true;
+ for (TaskEntry& entry : tasks_)
+ NotifyTaskBudgetRequested(&entry, requested_budget_);
+ iterating_over_tasks_ = false;
+ DeleteTasksIfRequested();
// If there tasks, NotifyTasksAndAdvance is called from TaskReadyToAdvance.
if (tasks_.empty())
@@ -56,22 +64,39 @@ void VirtualTimeController::NotifyTasksAndAdvance() {
// Give at most as much virtual time as available until the next callback.
bool ready_to_advance = true;
- TimeDelta next_budget = requested_budget_ - accumulated_time_;
+ iterating_over_tasks_ = true;
+ TimeDelta next_budget = requested_budget_ - accumulated_budget_portion_;
+ // TODO(alexclarke): This is a little ugly, find a nicer way.
+ for (TaskEntry& entry : tasks_) {
+ if (entry.next_execution_time <= total_elapsed_time_offset_)
+ entry.ready_to_advance = false;
+ }
+
for (TaskEntry& entry : tasks_) {
- if (entry.next_execution_time <= total_elapsed_time_)
+ if (entry.next_execution_time <= total_elapsed_time_offset_)
NotifyTaskIntervalElapsed(&entry);
- ready_to_advance &= entry.ready_to_advance;
- next_budget =
- std::min(next_budget, entry.next_execution_time - total_elapsed_time_);
+ if (tasks_to_delete_.find(entry.task) == tasks_to_delete_.end()) {
+ ready_to_advance &= entry.ready_to_advance;
+ next_budget = std::min(
+ next_budget, entry.next_execution_time - total_elapsed_time_offset_);
+ }
}
+ iterating_over_tasks_ = false;
+ DeleteTasksIfRequested();
if (!ready_to_advance)
return;
- if (accumulated_time_ >= requested_budget_) {
+ if (accumulated_budget_portion_ >= requested_budget_) {
+ for (TaskEntry& entry : tasks_)
+ entry.ready_to_advance = false;
+
+ iterating_over_tasks_ = true;
for (const TaskEntry& entry : tasks_)
- entry.task->BudgetExpired(total_elapsed_time_);
+ entry.task->BudgetExpired(total_elapsed_time_offset_);
+ iterating_over_tasks_ = false;
+ DeleteTasksIfRequested();
auto callback = budget_expired_callback_;
budget_expired_callback_.Reset();
@@ -82,21 +107,31 @@ void VirtualTimeController::NotifyTasksAndAdvance() {
SetVirtualTimePolicy(next_budget);
}
+void VirtualTimeController::DeleteTasksIfRequested() {
+ DCHECK(!iterating_over_tasks_);
+
+ // It's not safe to delete tasks while iterating over |tasks_| so do it now.
+ while (!tasks_to_delete_.empty()) {
+ CancelRepeatingTask(*tasks_to_delete_.begin());
+ tasks_to_delete_.erase(tasks_to_delete_.begin());
+ }
+}
+
void VirtualTimeController::NotifyTaskIntervalElapsed(TaskEntry* entry) {
- entry->ready_to_advance = false;
- entry->next_execution_time = total_elapsed_time_ + entry->interval;
+ DCHECK(!entry->ready_to_advance);
+ entry->next_execution_time = total_elapsed_time_offset_ + entry->interval;
entry->task->IntervalElapsed(
- total_elapsed_time_,
+ total_elapsed_time_offset_,
base::Bind(&VirtualTimeController::TaskReadyToAdvance,
base::Unretained(this), base::Unretained(entry)));
}
void VirtualTimeController::NotifyTaskBudgetRequested(TaskEntry* entry,
- int budget_ms) {
- entry->ready_to_advance = false;
+ base::TimeDelta budget) {
+ DCHECK(!entry->ready_to_advance);
entry->task->BudgetRequested(
- total_elapsed_time_, budget_ms,
+ total_elapsed_time_offset_, budget,
base::Bind(&VirtualTimeController::TaskReadyToAdvance,
base::Unretained(this), base::Unretained(entry)));
}
@@ -106,20 +141,26 @@ void VirtualTimeController::TaskReadyToAdvance(TaskEntry* entry) {
NotifyTasksAndAdvance();
}
-void VirtualTimeController::SetVirtualTimePolicy(const TimeDelta& next_budget) {
+void VirtualTimeController::SetVirtualTimePolicy(base::TimeDelta next_budget) {
last_used_budget_ = next_budget;
virtual_time_active_ = true;
devtools_client_->GetEmulation()->GetExperimental()->SetVirtualTimePolicy(
emulation::SetVirtualTimePolicyParams::Builder()
.SetPolicy(virtual_time_policy_)
- .SetBudget(next_budget.InMilliseconds())
+ .SetBudget(next_budget.InMillisecondsF())
+ .SetMaxVirtualTimeTaskStarvationCount(max_task_starvation_count_)
.Build(),
base::Bind(&VirtualTimeController::SetVirtualTimePolicyDone,
base::Unretained(this)));
}
void VirtualTimeController::SetVirtualTimePolicyDone(
- std::unique_ptr<emulation::SetVirtualTimePolicyResult>) {
+ std::unique_ptr<emulation::SetVirtualTimePolicyResult> result) {
+ if (result) {
+ virtual_time_base_ = base::Time::FromJsTime(result->GetVirtualTimeBase());
+ } else {
+ LOG(WARNING) << "SetVirtualTimePolicy did not succeed";
+ }
if (set_up_complete_callback_) {
set_up_complete_callback_.Run();
set_up_complete_callback_.Reset();
@@ -133,14 +174,14 @@ void VirtualTimeController::OnVirtualTimeBudgetExpired(
if (!budget_expired_callback_)
return;
- accumulated_time_ += last_used_budget_;
- total_elapsed_time_ += last_used_budget_;
+ accumulated_budget_portion_ += last_used_budget_;
+ total_elapsed_time_offset_ += last_used_budget_;
virtual_time_active_ = false;
NotifyTasksAndAdvance();
}
void VirtualTimeController::ScheduleRepeatingTask(RepeatingTask* task,
- int interval_ms) {
+ base::TimeDelta interval) {
if (virtual_time_active_) {
// We cannot accurately modify any previously granted virtual time budget.
LOG(WARNING) << "VirtualTimeController tasks should be added while "
@@ -149,14 +190,26 @@ void VirtualTimeController::ScheduleRepeatingTask(RepeatingTask* task,
TaskEntry entry;
entry.task = task;
- entry.interval = TimeDelta::FromMilliseconds(interval_ms);
- entry.next_execution_time = total_elapsed_time_ + entry.interval;
+ entry.interval = interval;
+ entry.next_execution_time = total_elapsed_time_offset_ + entry.interval;
tasks_.push_back(entry);
}
void VirtualTimeController::CancelRepeatingTask(RepeatingTask* task) {
+ if (iterating_over_tasks_) {
+ tasks_to_delete_.insert(task);
+ return;
+ }
tasks_.remove_if(
[task](const TaskEntry& entry) { return entry.task == task; });
}
+base::Time VirtualTimeController::GetVirtualTimeBase() const {
+ return virtual_time_base_;
+}
+
+base::TimeDelta VirtualTimeController::GetCurrentVirtualTimeOffset() const {
+ return total_elapsed_time_offset_;
+}
+
} // namespace headless
diff --git a/chromium/headless/public/util/virtual_time_controller.h b/chromium/headless/public/util/virtual_time_controller.h
index ffeee16f36b..05503aa11eb 100644
--- a/chromium/headless/public/util/virtual_time_controller.h
+++ b/chromium/headless/public/util/virtual_time_controller.h
@@ -19,11 +19,11 @@ namespace headless {
class HEADLESS_EXPORT VirtualTimeController
: public emulation::ExperimentalObserver {
public:
- explicit VirtualTimeController(HeadlessDevToolsClient* devtools_client);
+ VirtualTimeController(HeadlessDevToolsClient* devtools_client,
+ int max_task_starvation_count = 0);
~VirtualTimeController() override;
- // Grants |budget_ms| milliseconds of virtual time by applying the provided
- // |policy|.
+ // Grants a |budget| of virtual time by applying the provided |policy|.
//
// |set_up_complete_callback|, if set, is run after the (initial) policy was
// applied via DevTools. |budget_expired_callback| will be called when the
@@ -31,9 +31,9 @@ class HEADLESS_EXPORT VirtualTimeController
//
// Should not be called again until the previous invocation's
// budget_expired_callback was executed.
- void GrantVirtualTimeBudget(
+ virtual void GrantVirtualTimeBudget(
emulation::VirtualTimePolicy policy,
- int budget_ms,
+ base::TimeDelta budget,
const base::Callback<void()>& set_up_complete_callback,
const base::Callback<void()>& budget_expired_callback);
@@ -42,35 +42,48 @@ class HEADLESS_EXPORT VirtualTimeController
virtual ~RepeatingTask() {}
// Called when the tasks's requested virtual time interval has elapsed.
- // |virtual_time| is the virtual time duration that has advanced since the
- // page started loading (millisecond granularity). The task should call
- // |continue_callback| when it is ready for virtual time to continue
+ // |virtual_time_offset| is the virtual time duration that has advanced
+ // since the page started loading (millisecond granularity). The task should
+ // call |continue_callback| when it is ready for virtual time to continue
// advancing.
virtual void IntervalElapsed(
- const base::TimeDelta& virtual_time,
+ base::TimeDelta virtual_time_offset,
const base::Callback<void()>& continue_callback) = 0;
// Called when a new virtual time budget grant was requested. The task
// should call |continue_callback| when it is ready for virtual time to
// continue advancing.
virtual void BudgetRequested(
- const base::TimeDelta& virtual_time,
- int requested_budget_ms,
+ base::TimeDelta virtual_time_offset,
+ base::TimeDelta requested_budget,
const base::Callback<void()>& continue_callback) = 0;
// Called when the latest virtual time budget has been used up.
- virtual void BudgetExpired(const base::TimeDelta& virtual_time) = 0;
+ virtual void BudgetExpired(base::TimeDelta virtual_time_offset) = 0;
};
- // Interleaves execution of the provided |task| with advancing of virtual
- // time. The task will be notified whenever another |interval_ms| milliseconds
- // of virtual time have elapsed, as well as when the last granted budget has
- // been used up.
+ // Interleaves execution of the provided |task| with progression of virtual
+ // time. The task will be notified whenever another |interval| of virtual time
+ // have elapsed, as well as when the last granted budget has been used up.
//
// To ensure that the task is notified of elapsed intervals accurately, it
// should be added while virtual time is paused.
- void ScheduleRepeatingTask(RepeatingTask* task, int interval_ms);
- void CancelRepeatingTask(RepeatingTask* task);
+ virtual void ScheduleRepeatingTask(RepeatingTask* task,
+ base::TimeDelta interval);
+ virtual void CancelRepeatingTask(RepeatingTask* task);
+
+ // Returns the time that virtual time offsets are relative to.
+ virtual base::Time GetVirtualTimeBase() const;
+
+ // Returns the current virtual time offset. Only accurate while virtual time
+ // is paused.
+ virtual base::TimeDelta GetCurrentVirtualTimeOffset() const;
+
+ // Returns the current virtual time stamp. Only accurate while virtual time
+ // is paused.
+ base::Time GetCurrentVirtualTime() const {
+ return GetVirtualTimeBase() + GetCurrentVirtualTimeOffset();
+ }
private:
struct TaskEntry {
@@ -86,14 +99,17 @@ class HEADLESS_EXPORT VirtualTimeController
void NotifyTasksAndAdvance();
void NotifyTaskIntervalElapsed(TaskEntry* entry);
- void NotifyTaskBudgetRequested(TaskEntry* entry, int budget_ms);
+ void NotifyTaskBudgetRequested(TaskEntry* entry, base::TimeDelta budget);
void TaskReadyToAdvance(TaskEntry* entry);
- void SetVirtualTimePolicy(const base::TimeDelta& next_budget);
+ void DeleteTasksIfRequested();
+
+ void SetVirtualTimePolicy(base::TimeDelta next_budget);
void SetVirtualTimePolicyDone(
std::unique_ptr<emulation::SetVirtualTimePolicyResult>);
- HeadlessDevToolsClient* devtools_client_; // NOT OWNED
+ HeadlessDevToolsClient* const devtools_client_; // NOT OWNED
+ const int max_task_starvation_count_;
emulation::VirtualTimePolicy virtual_time_policy_ =
emulation::VirtualTimePolicy::ADVANCE;
@@ -101,13 +117,17 @@ class HEADLESS_EXPORT VirtualTimeController
base::Callback<void()> budget_expired_callback_;
bool virtual_time_active_ = false;
- base::TimeDelta total_elapsed_time_;
+ base::TimeDelta total_elapsed_time_offset_;
base::TimeDelta requested_budget_;
base::TimeDelta last_used_budget_;
- base::TimeDelta accumulated_time_;
+ base::TimeDelta accumulated_budget_portion_;
+ // Initial virtual time that virtual time offsets are relative to.
+ base::Time virtual_time_base_;
std::list<TaskEntry> tasks_;
+ std::set<RepeatingTask*> tasks_to_delete_;
bool in_notify_tasks_and_advance_ = false;
+ bool iterating_over_tasks_ = false;
};
} // namespace headless
diff --git a/chromium/headless/public/util/virtual_time_controller_test.cc b/chromium/headless/public/util/virtual_time_controller_test.cc
index 469d6104bb5..483a99bc21a 100644
--- a/chromium/headless/public/util/virtual_time_controller_test.cc
+++ b/chromium/headless/public/util/virtual_time_controller_test.cc
@@ -16,6 +16,8 @@
namespace headless {
+using testing::ElementsAre;
+using testing::Mock;
using testing::Return;
using testing::_;
@@ -29,17 +31,18 @@ class VirtualTimeControllerTest : public ::testing::Test {
EXPECT_CALL(*mock_host_, IsAttached()).WillOnce(Return(false));
EXPECT_CALL(*mock_host_, AttachClient(&client_));
client_.AttachToHost(mock_host_.get());
- controller_ = base::MakeUnique<VirtualTimeController>(&client_);
+ controller_ = base::MakeUnique<VirtualTimeController>(&client_, 0);
}
- ~VirtualTimeControllerTest() override {}
+ ~VirtualTimeControllerTest() override = default;
void GrantVirtualTimeBudget(int budget_ms) {
ASSERT_FALSE(set_up_complete_);
ASSERT_FALSE(budget_expired_);
controller_->GrantVirtualTimeBudget(
- emulation::VirtualTimePolicy::ADVANCE, budget_ms,
+ emulation::VirtualTimePolicy::ADVANCE,
+ base::TimeDelta::FromMilliseconds(budget_ms),
base::Bind(
[](bool* set_up_complete) {
EXPECT_FALSE(*set_up_complete);
@@ -75,16 +78,34 @@ class VirtualTimeControllerTest : public ::testing::Test {
};
TEST_F(VirtualTimeControllerTest, AdvancesTimeWithoutTasks) {
+ controller_ = base::MakeUnique<VirtualTimeController>(&client_, 1000);
+
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ "{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
+ "\"params\":{\"budget\":5000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":1000,"
+ "\"policy\":\"advance\"}}"))
+ .WillOnce(Return(true));
+
+ GrantVirtualTimeBudget(5000);
+}
+
+TEST_F(VirtualTimeControllerTest, MaxVirtualTimeTaskStarvationCount) {
EXPECT_CALL(*mock_host_,
DispatchProtocolMessage(
&client_,
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":5000,\"policy\":\"advance\"}}"))
+ "\"params\":{\"budget\":5000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
.WillOnce(Return(true));
GrantVirtualTimeBudget(5000);
- client_.DispatchProtocolMessage(mock_host_.get(), "{\"id\":0,\"result\":{}}");
+ client_.DispatchProtocolMessage(
+ mock_host_.get(), "{\"id\":0,\"result\":{\"virtualTimeBase\":1.0}}");
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -98,13 +119,13 @@ namespace {
class MockTask : public VirtualTimeController::RepeatingTask {
public:
MOCK_METHOD2(IntervalElapsed,
- void(const base::TimeDelta& virtual_time,
+ void(base::TimeDelta virtual_time_offset,
const base::Callback<void()>& continue_callback));
MOCK_METHOD3(BudgetRequested,
- void(const base::TimeDelta& virtual_time,
- int requested_budget_ms,
+ void(base::TimeDelta virtual_time_offset,
+ base::TimeDelta requested_budget,
const base::Callback<void()>& continue_callback));
- MOCK_METHOD1(BudgetExpired, void(const base::TimeDelta& virtual_time));
+ MOCK_METHOD1(BudgetExpired, void(base::TimeDelta virtual_time_offset));
};
ACTION_TEMPLATE(RunClosure,
@@ -120,16 +141,19 @@ ACTION_P(RunClosure, closure) {
TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
MockTask task;
- controller_->ScheduleRepeatingTask(&task, 1000);
+ controller_->ScheduleRepeatingTask(&task,
+ base::TimeDelta::FromMilliseconds(1000));
- EXPECT_CALL(task,
- BudgetRequested(base::TimeDelta::FromMilliseconds(0), 3000, _))
+ EXPECT_CALL(task, BudgetRequested(base::TimeDelta::FromMilliseconds(0),
+ base::TimeDelta::FromMilliseconds(3000), _))
.WillOnce(RunClosure<2>());
EXPECT_CALL(*mock_host_,
DispatchProtocolMessage(
&client_,
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":1000,\"policy\":\"advance\"}}"))
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
.WillOnce(Return(true));
GrantVirtualTimeBudget(3000);
@@ -137,7 +161,8 @@ TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
- client_.DispatchProtocolMessage(mock_host_.get(), "{\"id\":0,\"result\":{}}");
+ client_.DispatchProtocolMessage(
+ mock_host_.get(), "{\"id\":0,\"result\":{\"virtualTimeBase\":1.0}}");
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -155,7 +180,9 @@ TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
&client_,
base::StringPrintf(
"{\"id\":%d,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":1000,\"policy\":\"advance\"}}",
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}",
i * 2)))
.WillOnce(Return(true));
@@ -166,7 +193,8 @@ TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
client_.DispatchProtocolMessage(
mock_host_.get(),
- base::StringPrintf("{\"id\":%d,\"result\":{}}", i * 2));
+ base::StringPrintf("{\"id\":%d,\"result\":{\"virtualTimeBase\":1.0}}",
+ i * 2));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -183,16 +211,19 @@ TEST_F(VirtualTimeControllerTest, InterleavesTasksWithVirtualTime) {
TEST_F(VirtualTimeControllerTest, CanceledTask) {
MockTask task;
- controller_->ScheduleRepeatingTask(&task, 1000);
+ controller_->ScheduleRepeatingTask(&task,
+ base::TimeDelta::FromMilliseconds(1000));
- EXPECT_CALL(task,
- BudgetRequested(base::TimeDelta::FromMilliseconds(0), 5000, _))
+ EXPECT_CALL(task, BudgetRequested(base::TimeDelta::FromMilliseconds(0),
+ base::TimeDelta::FromMilliseconds(5000), _))
.WillOnce(RunClosure<2>());
EXPECT_CALL(*mock_host_,
DispatchProtocolMessage(
&client_,
"{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":1000,\"policy\":\"advance\"}}"))
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
.WillOnce(Return(true));
GrantVirtualTimeBudget(5000);
@@ -200,7 +231,8 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
- client_.DispatchProtocolMessage(mock_host_.get(), "{\"id\":0,\"result\":{}}");
+ client_.DispatchProtocolMessage(
+ mock_host_.get(), "{\"id\":0,\"result\":{\"virtualTimeBase\":1.0}}");
EXPECT_TRUE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -214,7 +246,9 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
DispatchProtocolMessage(
&client_,
"{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":1000,\"policy\":\"advance\"}}"))
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
.WillOnce(Return(true));
SendVirtualTimeBudgetExpiredEvent();
@@ -223,7 +257,8 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
EXPECT_FALSE(budget_expired_);
client_.DispatchProtocolMessage(
- mock_host_.get(), base::StringPrintf("{\"id\":2,\"result\":{}}"));
+ mock_host_.get(),
+ base::StringPrintf("{\"id\":2,\"result\":{\"virtualTimeBase\":1.0}}"));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -234,7 +269,9 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
DispatchProtocolMessage(
&client_,
"{\"id\":4,\"method\":\"Emulation.setVirtualTimePolicy\","
- "\"params\":{\"budget\":3000,\"policy\":\"advance\"}}"))
+ "\"params\":{\"budget\":3000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
.WillOnce(Return(true));
SendVirtualTimeBudgetExpiredEvent();
@@ -243,7 +280,8 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
EXPECT_FALSE(budget_expired_);
client_.DispatchProtocolMessage(
- mock_host_.get(), base::StringPrintf("{\"id\":4,\"result\":{}}"));
+ mock_host_.get(),
+ base::StringPrintf("{\"id\":4,\"result\":{\"virtualTimeBase\":1.0}}"));
EXPECT_FALSE(set_up_complete_);
EXPECT_FALSE(budget_expired_);
@@ -254,4 +292,181 @@ TEST_F(VirtualTimeControllerTest, CanceledTask) {
EXPECT_TRUE(budget_expired_);
}
+TEST_F(VirtualTimeControllerTest, MultipleTasks) {
+ MockTask task1;
+ MockTask task2;
+ controller_->ScheduleRepeatingTask(&task1,
+ base::TimeDelta::FromMilliseconds(1000));
+ controller_->ScheduleRepeatingTask(&task2,
+ base::TimeDelta::FromMilliseconds(1000));
+
+ EXPECT_CALL(task1,
+ BudgetRequested(base::TimeDelta::FromMilliseconds(0),
+ base::TimeDelta::FromMilliseconds(2000), _))
+ .WillOnce(RunClosure<2>());
+ EXPECT_CALL(task2,
+ BudgetRequested(base::TimeDelta::FromMilliseconds(0),
+ base::TimeDelta::FromMilliseconds(2000), _))
+ .WillOnce(RunClosure<2>());
+ // We should only get one call to Emulation.setVirtualTimePolicy despite
+ // having two tasks.
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ "{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
+ .WillOnce(Return(true));
+
+ GrantVirtualTimeBudget(2000);
+ EXPECT_FALSE(set_up_complete_);
+ EXPECT_FALSE(budget_expired_);
+
+ client_.DispatchProtocolMessage(
+ mock_host_.get(),
+ base::StringPrintf("{\"id\":0,\"result\":{\"virtualTimeBase\":1.0}}"));
+
+ EXPECT_TRUE(set_up_complete_);
+ EXPECT_FALSE(budget_expired_);
+}
+
+class VirtualTimeTask : public VirtualTimeController::RepeatingTask {
+ public:
+ using Task = base::Callback<void(base::TimeDelta virtual_time)>;
+
+ VirtualTimeTask(VirtualTimeController* controller,
+ Task budget_requested_task,
+ Task interval_elapsed_task,
+ Task budget_expired_task)
+ : controller_(controller),
+ budget_requested_task_(budget_requested_task),
+ interval_elapsed_task_(interval_elapsed_task),
+ budget_expired_task_(budget_expired_task) {}
+
+ void IntervalElapsed(
+ base::TimeDelta virtual_time,
+ const base::Callback<void()>& continue_callback) override {
+ interval_elapsed_task_.Run(virtual_time);
+ continue_callback.Run();
+ }
+
+ void BudgetRequested(
+ base::TimeDelta virtual_time,
+ base::TimeDelta requested_budget_ms,
+ const base::Callback<void()>& continue_callback) override {
+ budget_requested_task_.Run(virtual_time);
+ continue_callback.Run();
+ }
+
+ void BudgetExpired(base::TimeDelta virtual_time) override {
+ budget_expired_task_.Run(virtual_time);
+ };
+
+ VirtualTimeController* controller_; // NOT OWNED
+ Task budget_requested_task_;
+ Task interval_elapsed_task_;
+ Task budget_expired_task_;
+};
+
+TEST_F(VirtualTimeControllerTest, ReentrantTask) {
+#if defined(__clang__)
+ std::vector<std::string> log;
+ VirtualTimeTask task_b(
+ controller_.get(),
+ base::Bind(
+ [](std::vector<std::string>* log, base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "B: budget requested @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ },
+ &log),
+ base::Bind(
+ [](std::vector<std::string>* log, VirtualTimeController* controller,
+ VirtualTimeTask* task_b, base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "B: interval elapsed @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ controller->CancelRepeatingTask(task_b);
+ },
+ &log, controller_.get(), &task_b),
+ base::Bind(
+ [](std::vector<std::string>* log, base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "B: budget expired @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ },
+ &log));
+
+ VirtualTimeTask task_a(
+ controller_.get(),
+ base::Bind(
+ [](std::vector<std::string>* log, base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "A: budget requested @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ },
+ &log),
+ base::Bind(
+ [](std::vector<std::string>* log, VirtualTimeController* controller,
+ VirtualTimeTask* task_a, VirtualTimeTask* task_b,
+ base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "A: interval elapsed @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ controller->CancelRepeatingTask(task_a);
+ controller->ScheduleRepeatingTask(
+ task_b, base::TimeDelta::FromMilliseconds(1500));
+ },
+ &log, controller_.get(), &task_a, &task_b),
+ base::Bind(
+ [](std::vector<std::string>* log, base::TimeDelta virtual_time) {
+ log->push_back(base::StringPrintf(
+ "A: budget expired @ %d",
+ static_cast<int>(virtual_time.InMilliseconds())));
+ },
+ &log));
+
+ controller_->ScheduleRepeatingTask(&task_a,
+ base::TimeDelta::FromMilliseconds(1000));
+
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ "{\"id\":0,\"method\":\"Emulation.setVirtualTimePolicy\","
+ "\"params\":{\"budget\":1000.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
+ .WillOnce(Return(true));
+
+ GrantVirtualTimeBudget(6000);
+ Mock::VerifyAndClearExpectations(&mock_host_);
+
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ "{\"id\":2,\"method\":\"Emulation.setVirtualTimePolicy\","
+ "\"params\":{\"budget\":1500.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
+ .WillOnce(Return(true));
+ SendVirtualTimeBudgetExpiredEvent();
+ Mock::VerifyAndClearExpectations(&mock_host_);
+
+ EXPECT_CALL(*mock_host_,
+ DispatchProtocolMessage(
+ &client_,
+ "{\"id\":4,\"method\":\"Emulation.setVirtualTimePolicy\","
+ "\"params\":{\"budget\":3500.0,"
+ "\"maxVirtualTimeTaskStarvationCount\":0,"
+ "\"policy\":\"advance\"}}"))
+ .WillOnce(Return(true));
+ SendVirtualTimeBudgetExpiredEvent();
+
+ EXPECT_THAT(
+ log, ElementsAre("A: budget requested @ 0", "A: interval elapsed @ 1000",
+ "B: interval elapsed @ 2500"));
+#endif
+}
+
} // namespace headless