diff options
Diffstat (limited to 'chromium/headless/public')
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, ¶ms_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 |