Skip to content

Commit

Permalink
WebDriver: Handle script execution results without spinning event loops
Browse files Browse the repository at this point in the history
We currently spin the platform event loop while awaiting scripts to
complete. This causes WebContent to hang if another component is also
spinning the event loop. The particular example that instigated this
patch was the navigable's navigation loop (which spins until the fetch
process is complete), triggered by a form submission to an iframe.

So instead of spinning, we now return immediately from the script
executors, after setting up listeners for either the script's promise to
be resolved or for a timeout. The HTTP request to WebDriver must finish
synchronously though, so now the WebDriver process spins its event loop
until WebContent signals that the script completed. This should be ok -
the WebDriver process isn't expected to be doing anything else in the
meantime.

Also, as a consequence of these changes, we now actually handle time
outs. We were previously creating the timeout timer, but not starting
it.

(cherry picked from commit c2cf65adac78912883996153fb608dafe389b6e0)
  • Loading branch information
trflynn89 authored and nico committed Nov 14, 2024
1 parent 05d86c3 commit 3802508
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 120 deletions.
196 changes: 123 additions & 73 deletions Userland/Libraries/LibWeb/WebDriver/ExecuteScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,36 +283,89 @@ static JS::ThrowCompletionOr<JS::Value> execute_a_function_body(Web::Page& page,
return completion;
}

ExecuteScriptResultSerialized execute_script(Web::Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms)
class HeapTimer : public JS::Cell {
JS_CELL(HeapTimer, JS::Cell);
JS_DECLARE_ALLOCATOR(HeapTimer);

public:
explicit HeapTimer()
: m_timer(Core::Timer::create())
{
}

virtual ~HeapTimer() override = default;

void start(u64 timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_timeout)
{
m_on_timeout = on_timeout;

m_timer->on_timeout = [this]() {
m_timed_out = true;

if (m_on_timeout) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Script Timeout");

m_on_timeout->function()({ ExecuteScriptResultType::Timeout, move(error_object) });
m_on_timeout = nullptr;
}
};

m_timer->set_interval(static_cast<int>(timeout_ms));
m_timer->set_single_shot(true);
m_timer->start();
}

void stop()
{
m_on_timeout = nullptr;
m_timer->stop();
}

bool is_timed_out() const { return m_timed_out; }

private:
virtual void visit_edges(JS::Cell::Visitor& visitor) override
{
Base::visit_edges(visitor);
visitor.visit(m_on_timeout);
}

NonnullRefPtr<Core::Timer> m_timer;
JS::GCPtr<OnScriptComplete> m_on_timeout;
bool m_timed_out { false };
};

JS_DEFINE_ALLOCATOR(HeapTimer);

void execute_script(Web::Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
{
auto* document = page.top_level_browsing_context().active_document();
auto* window = page.top_level_browsing_context().active_window();
auto& realm = window->realm();
auto& vm = window->vm();

// 5. Let timer be a new timer.
auto timeout_flag = false;
auto timer = Core::Timer::create();
auto timer = vm.heap().allocate<HeapTimer>(realm);

// 6. If timeout is not null:
if (timeout_ms.has_value()) {
// 1. Start the timer with timer and timeout.
timer->on_timeout = [&]() {
timeout_flag = true;
};
timer->set_interval(timeout_ms.value());
timer->set_single_shot(true);
timer->start(timeout_ms.value(), on_complete);
}

// AD-HOC: An execution context is required for Promise creation hooks.
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };

// 7. Let promise be a new Promise.
auto promise_capability = WebIDL::create_promise(realm);
JS::NonnullGCPtr promise { verify_cast<JS::Promise>(*promise_capability->promise()) };

// 8. Run the following substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke([&realm, &page, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
Platform::EventLoopPlugin::the().deferred_invoke([&realm, &page, promise_capability, document, promise, body = move(body), arguments = move(arguments)]() mutable {
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };

// 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments.
auto script_result = execute_a_function_body(page, body, move(arguments));

Expand All @@ -328,69 +381,66 @@ ExecuteScriptResultSerialized execute_script(Web::Page& page, ByteString const&
});

// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
vm.custom_data()->spin_event_loop_until([&] {
return timeout_flag || promise->state() != JS::Promise::State::Pending;
});
auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
if (timer->is_timed_out())
return JS::js_undefined();
timer->stop();

auto json_value_or_error = json_clone(realm, promise->result());
if (json_value_or_error.is_error()) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Could not clone result value");

on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) });
}

// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
if (timeout_flag && promise->state() == JS::Promise::State::Pending) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Script Timeout");
return { ExecuteScriptResultType::Timeout, move(error_object) };
}
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
// NOTE: This is handled by the HeapTimer.

auto json_value_or_error = json_clone(realm, promise->result());
if (json_value_or_error.is_error()) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Could not clone result value");
return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
}
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
else if (promise->state() == JS::Promise::State::Fulfilled) {
on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() });
}

// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
if (promise->state() == JS::Promise::State::Fulfilled) {
return { ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() };
}
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
else if (promise->state() == JS::Promise::State::Rejected) {
on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() });
}

// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
if (promise->state() == JS::Promise::State::Rejected) {
return { ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() };
}
return JS::js_undefined();
});

VERIFY_NOT_REACHED();
WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps);
}

ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms)
void execute_async_script(Web::Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete)
{
auto* document = page.top_level_browsing_context().active_document();
auto* window = page.top_level_browsing_context().active_window();
auto& realm = window->realm();
auto& vm = window->vm();

// 5. Let timer be a new timer.
IGNORE_USE_IN_ESCAPING_LAMBDA auto timeout_flag = false;
auto timer = Core::Timer::create();
auto timer = vm.heap().allocate<HeapTimer>(realm);

// 6. If timeout is not null:
if (timeout_ms.has_value()) {
// 1. Start the timer with timer and timeout.
timer->on_timeout = [&]() {
timeout_flag = true;
};
timer->set_interval(timeout_ms.value());
timer->set_single_shot(true);
timer->start(timeout_ms.value(), on_complete);
}

// AD-HOC: An execution context is required for Promise creation hooks.
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };

// 7. Let promise be a new Promise.
auto promise_capability = WebIDL::create_promise(realm);
JS::NonnullGCPtr promise { verify_cast<JS::Promise>(*promise_capability->promise()) };

// 8. Run the following substeps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke([&vm, &realm, &page, &timeout_flag, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
Platform::EventLoopPlugin::the().deferred_invoke([&vm, &realm, &page, timer, document, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable {
HTML::TemporaryExecutionContext execution_context { document->relevant_settings_object() };

// 1. Let resolvingFunctions be CreateResolvingFunctions(promise).
auto resolving_functions = promise->create_resolving_functions();

Expand Down Expand Up @@ -434,7 +484,7 @@ ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString c
auto& script_promise = static_cast<JS::Promise&>(*script_promise_or_error.value());

vm.custom_data()->spin_event_loop_until([&] {
return timeout_flag || script_promise.state() != JS::Promise::State::Pending;
return timer->is_timed_out() || script_promise.state() != JS::Promise::State::Pending;
});

// 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v.
Expand All @@ -447,37 +497,37 @@ ExecuteScriptResultSerialized execute_async_script(Web::Page& page, ByteString c
});

// 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first.
vm.custom_data()->spin_event_loop_until([&] {
return timeout_flag || promise->state() != JS::Promise::State::Pending;
});
auto reaction_steps = JS::create_heap_function(vm.heap(), [&realm, promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr<JS::Value> {
if (timer->is_timed_out())
return JS::js_undefined();
timer->stop();

auto json_value_or_error = json_clone(realm, promise->result());
if (json_value_or_error.is_error()) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Could not clone result value");

on_complete->function()({ ExecuteScriptResultType::JavaScriptError, move(error_object) });
}

// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script
if (timeout_flag && promise->state() == JS::Promise::State::Pending) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Script Timeout");
return { ExecuteScriptResultType::Timeout, move(error_object) };
}
// 10. If promise is still pending and timer's timeout fired flag is set, return error with error code script timeout.
// NOTE: This is handled by the HeapTimer.

auto json_value_or_error = json_clone(realm, promise->result());
if (json_value_or_error.is_error()) {
auto error_object = JsonObject {};
error_object.set("name", "Error");
error_object.set("message", "Could not clone result value");
return { ExecuteScriptResultType::JavaScriptError, move(error_object) };
}
// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
else if (promise->state() == JS::Promise::State::Fulfilled) {
on_complete->function()({ ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() });
}

// 11. If promise is fulfilled with value v, let result be JSON clone with session and v, and return success with data result.
if (promise->state() == JS::Promise::State::Fulfilled) {
return { ExecuteScriptResultType::PromiseResolved, json_value_or_error.release_value() };
}
// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
else if (promise->state() == JS::Promise::State::Rejected) {
on_complete->function()({ ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() });
}

// 12. If promise is rejected with reason r, let result be JSON clone with session and r, and return error with error code javascript error and data result.
if (promise->state() == JS::Promise::State::Rejected) {
return { ExecuteScriptResultType::PromiseRejected, json_value_or_error.release_value() };
}
return JS::js_undefined();
});

VERIFY_NOT_REACHED();
WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps);
}

}
7 changes: 5 additions & 2 deletions Userland/Libraries/LibWeb/WebDriver/ExecuteScript.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <AK/Forward.h>
#include <AK/JsonValue.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/HeapFunction.h>
#include <LibJS/Runtime/Value.h>
#include <LibWeb/Forward.h>

Expand All @@ -32,7 +33,9 @@ struct ExecuteScriptResultSerialized {
JsonValue value;
};

ExecuteScriptResultSerialized execute_script(Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms);
ExecuteScriptResultSerialized execute_async_script(Page& page, ByteString const& body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms);
using OnScriptComplete = JS::HeapFunction<void(ExecuteScriptResultSerialized)>;

void execute_script(Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);
void execute_async_script(Page& page, ByteString body, JS::MarkedVector<JS::Value> arguments, Optional<u64> const& timeout_ms, JS::NonnullGCPtr<OnScriptComplete> on_complete);

}
Loading

0 comments on commit 3802508

Please sign in to comment.