LibWebView+WebContent+headless-browser: Make the page info IPCs async

The IPCs to request a page's text, layout tree, etc. are currently all
synchronous. This can result in a deadlock when WebContent also makes
a synchronous IPC call, as both ends will be waiting on each other.

This replaces the page info IPCs with a single, asynchronous IPC. This
new IPC is promise-based, much like our screenshot IPC.
This commit is contained in:
Timothy Flynn 2024-09-19 10:40:00 -04:00 committed by Andreas Kling
parent b688bbf26c
commit 3332230cef
Notes: github-actions[bot] 2024-09-19 16:08:09 +00:00
11 changed files with 174 additions and 90 deletions

View file

@ -71,7 +71,7 @@ static bool is_primitive_type(ByteString const& type)
static bool is_simple_type(ByteString const& type)
{
// Small types that it makes sense just to pass by value.
return type.is_one_of("AK::CaseSensitivity", "Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source", "Web::EventResult", "Web::HTML::AllowMultipleFiles", "Web::HTML::AudioPlayState", "Web::HTML::HistoryHandlingBehavior");
return type.is_one_of("AK::CaseSensitivity", "Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source", "Web::EventResult", "Web::HTML::AllowMultipleFiles", "Web::HTML::AudioPlayState", "Web::HTML::HistoryHandlingBehavior", "WebView::PageInfoType");
}
static bool is_primitive_or_simple_type(ByteString const& type)

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/EnumBits.h>
namespace WebView {
enum class PageInfoType {
Text = 1 << 0,
LayoutTree = 1 << 2,
PaintTree = 1 << 3,
GCGraph = 1 << 4,
};
AK_ENUM_BITWISE_OPERATORS(PageInfoType);
}

View file

@ -583,15 +583,40 @@ void ViewImplementation::did_receive_screenshot(Badge<WebContentClient>, Gfx::Sh
m_pending_screenshot = nullptr;
}
NonnullRefPtr<Core::Promise<String>> ViewImplementation::request_internal_page_info(PageInfoType type)
{
auto promise = Core::Promise<String>::construct();
if (m_pending_info_request) {
// For simplicitly, only allow one info request at a time for now.
promise->reject(Error::from_string_literal("A page info request is already in progress"));
return promise;
}
m_pending_info_request = promise;
client().async_request_internal_page_info(page_id(), type);
return promise;
}
void ViewImplementation::did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, String const& info)
{
VERIFY(m_pending_info_request);
m_pending_info_request->resolve(String { info });
m_pending_info_request = nullptr;
}
ErrorOr<LexicalPath> ViewImplementation::dump_gc_graph()
{
auto gc_graph_json = client().dump_gc_graph(page_id());
auto promise = request_internal_page_info(PageInfoType::GCGraph);
auto gc_graph_json = TRY(promise->await());
LexicalPath path { Core::StandardPaths::tempfile_directory() };
path = path.append(TRY(Core::DateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.json"sv)));
auto screenshot_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(screenshot_file->write_until_depleted(gc_graph_json.bytes()));
auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
TRY(dump_file->write_until_depleted(gc_graph_json.bytes()));
return path;
}

View file

@ -25,6 +25,7 @@
#include <LibWeb/Page/EventResult.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWebView/Forward.h>
#include <LibWebView/PageInfo.h>
#include <LibWebView/WebContentClient.h>
namespace WebView {
@ -140,6 +141,9 @@ public:
NonnullRefPtr<Core::Promise<LexicalPath>> take_dom_node_screenshot(i32);
virtual void did_receive_screenshot(Badge<WebContentClient>, Gfx::ShareableBitmap const&);
NonnullRefPtr<Core::Promise<String>> request_internal_page_info(PageInfoType);
void did_receive_internal_page_info(Badge<WebContentClient>, PageInfoType, String const&);
ErrorOr<LexicalPath> dump_gc_graph();
void set_user_style_sheet(String source);
@ -286,6 +290,7 @@ protected:
RefPtr<Core::Timer> m_repeated_crash_timer;
RefPtr<Core::Promise<LexicalPath>> m_pending_screenshot;
RefPtr<Core::Promise<String>> m_pending_info_request;
Web::HTML::AudioPlayState m_audio_play_state { Web::HTML::AudioPlayState::Paused };
size_t m_number_of_elements_playing_audio { 0 };

View file

@ -342,6 +342,12 @@ void WebContentClient::did_take_screenshot(u64 page_id, Gfx::ShareableBitmap con
view->did_receive_screenshot({}, screenshot);
}
void WebContentClient::did_get_internal_page_info(u64 page_id, WebView::PageInfoType type, String const& info)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_receive_internal_page_info({}, type, info);
}
void WebContentClient::did_output_js_console_message(u64 page_id, i32 message_index)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -78,6 +78,7 @@ private:
virtual void did_finish_editing_dom_node(u64 page_id, Optional<i32> const& node_id) override;
virtual void did_get_dom_node_html(u64 page_id, String const& html) override;
virtual void did_take_screenshot(u64 page_id, Gfx::ShareableBitmap const& screenshot) override;
virtual void did_get_internal_page_info(u64 page_id, PageInfoType, String const&) override;
virtual void did_output_js_console_message(u64 page_id, i32 message_index) override;
virtual void did_get_js_console_messages(u64 page_id, i32 start_index, Vector<ByteString> const& message_types, Vector<ByteString> const& messages) override;
virtual void did_change_favicon(u64 page_id, Gfx::ShareableBitmap const&) override;

View file

@ -840,10 +840,104 @@ void ConnectionFromClient::take_dom_node_screenshot(u64 page_id, i32 node_id)
page->queue_screenshot_task(node_id);
}
Messages::WebContentServer::DumpGcGraphResponse ConnectionFromClient::dump_gc_graph(u64)
static void append_page_text(Web::Page& page, StringBuilder& builder)
{
auto gc_graph_json = Web::Bindings::main_thread_vm().heap().dump_graph();
return MUST(String::from_byte_string(gc_graph_json.to_byte_string()));
auto* document = page.top_level_browsing_context().active_document();
if (!document) {
builder.append("(no DOM tree)"sv);
return;
}
auto* body = document->body();
if (!body) {
builder.append("(no body)"sv);
return;
}
builder.append(body->inner_text());
}
static void append_layout_tree(Web::Page& page, StringBuilder& builder)
{
auto* document = page.top_level_browsing_context().active_document();
if (!document) {
builder.append("(no DOM tree)"sv);
return;
}
document->update_layout();
auto* layout_root = document->layout_node();
if (!layout_root) {
builder.append("(no layout tree)"sv);
return;
}
Web::dump_tree(builder, *layout_root);
}
static void append_paint_tree(Web::Page& page, StringBuilder& builder)
{
auto* document = page.top_level_browsing_context().active_document();
if (!document) {
builder.append("(no DOM tree)"sv);
return;
}
document->update_layout();
auto* layout_root = document->layout_node();
if (!layout_root) {
builder.append("(no layout tree)"sv);
return;
}
if (!layout_root->paintable()) {
builder.append("(no paint tree)"sv);
return;
}
Web::dump_tree(builder, *layout_root->paintable());
}
static void append_gc_graph(StringBuilder& builder)
{
auto gc_graph = Web::Bindings::main_thread_vm().heap().dump_graph();
gc_graph.serialize(builder);
}
void ConnectionFromClient::request_internal_page_info(u64 page_id, WebView::PageInfoType type)
{
auto page = this->page(page_id);
if (!page.has_value()) {
async_did_get_internal_page_info(page_id, type, "(no page)"_string);
return;
}
StringBuilder builder;
if (has_flag(type, WebView::PageInfoType::Text)) {
append_page_text(page->page(), builder);
}
if (has_flag(type, WebView::PageInfoType::LayoutTree)) {
if (!builder.is_empty())
builder.append("\n"sv);
append_layout_tree(page->page(), builder);
}
if (has_flag(type, WebView::PageInfoType::PaintTree)) {
if (!builder.is_empty())
builder.append("\n"sv);
append_paint_tree(page->page(), builder);
}
if (has_flag(type, WebView::PageInfoType::GCGraph)) {
if (!builder.is_empty())
builder.append("\n"sv);
append_gc_graph(builder);
}
async_did_get_internal_page_info(page_id, type, MUST(builder.to_string()));
}
Messages::WebContentServer::GetSelectedTextResponse ConnectionFromClient::get_selected_text(u64 page_id)
@ -895,58 +989,6 @@ void ConnectionFromClient::paste(u64 page_id, String const& text)
page->page().focused_navigable().paste(text);
}
Messages::WebContentServer::DumpLayoutTreeResponse ConnectionFromClient::dump_layout_tree(u64 page_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return ByteString { "(no page)" };
auto* document = page->page().top_level_browsing_context().active_document();
if (!document)
return ByteString { "(no DOM tree)" };
document->update_layout();
auto* layout_root = document->layout_node();
if (!layout_root)
return ByteString { "(no layout tree)" };
StringBuilder builder;
Web::dump_tree(builder, *layout_root);
return builder.to_byte_string();
}
Messages::WebContentServer::DumpPaintTreeResponse ConnectionFromClient::dump_paint_tree(u64 page_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return ByteString { "(no page)" };
auto* document = page->page().top_level_browsing_context().active_document();
if (!document)
return ByteString { "(no DOM tree)" };
document->update_layout();
auto* layout_root = document->layout_node();
if (!layout_root)
return ByteString { "(no layout tree)" };
if (!layout_root->paintable())
return ByteString { "(no paint tree)" };
StringBuilder builder;
Web::dump_tree(builder, *layout_root->paintable());
return builder.to_byte_string();
}
Messages::WebContentServer::DumpTextResponse ConnectionFromClient::dump_text(u64 page_id)
{
auto page = this->page(page_id);
if (!page.has_value())
return ByteString { "(no page)" };
auto* document = page->page().top_level_browsing_context().active_document();
if (!document)
return ByteString { "(no DOM tree)" };
if (!document->body())
return ByteString { "(no body)" };
return document->body()->inner_text();
}
void ConnectionFromClient::set_content_filters(u64, Vector<String> const& filters)
{
Web::ContentFilter::the().set_patterns(filters).release_value_but_fixme_should_propagate_errors();

View file

@ -23,6 +23,7 @@
#include <LibWeb/Page/InputEvent.h>
#include <LibWeb/Platform/Timer.h>
#include <LibWebView/Forward.h>
#include <LibWebView/PageInfo.h>
#include <WebContent/Forward.h>
#include <WebContent/WebContentClientEndpoint.h>
#include <WebContent/WebContentConsoleClient.h>
@ -90,9 +91,6 @@ private:
virtual void remove_dom_node(u64 page_id, i32 node_id) override;
virtual void get_dom_node_html(u64 page_id, i32 node_id) override;
virtual Messages::WebContentServer::DumpLayoutTreeResponse dump_layout_tree(u64 page_id) override;
virtual Messages::WebContentServer::DumpPaintTreeResponse dump_paint_tree(u64 page_id) override;
virtual Messages::WebContentServer::DumpTextResponse dump_text(u64 page_id) override;
virtual void set_content_filters(u64 page_id, Vector<String> const&) override;
virtual void set_autoplay_allowed_on_all_websites(u64 page_id) override;
virtual void set_autoplay_allowlist(u64 page_id, Vector<String> const& allowlist) override;
@ -135,7 +133,7 @@ private:
virtual void take_document_screenshot(u64 page_id) override;
virtual void take_dom_node_screenshot(u64 page_id, i32 node_id) override;
virtual Messages::WebContentServer::DumpGcGraphResponse dump_gc_graph(u64 page_id) override;
virtual void request_internal_page_info(u64 page_id, WebView::PageInfoType) override;
virtual Messages::WebContentServer::GetLocalStorageEntriesResponse get_local_storage_entries(u64 page_id) override;
virtual Messages::WebContentServer::GetSessionStorageEntriesResponse get_session_storage_entries(u64 page_id) override;

View file

@ -16,6 +16,7 @@
#include <LibWeb/Page/Page.h>
#include <LibWebView/Attribute.h>
#include <LibWebView/ProcessHandle.h>
#include <LibWebView/PageInfo.h>
endpoint WebContentClient
{
@ -62,6 +63,8 @@ endpoint WebContentClient
did_take_screenshot(u64 page_id, Gfx::ShareableBitmap screenshot) =|
did_get_internal_page_info(u64 page_id, WebView::PageInfoType type, String info) =|
did_change_favicon(u64 page_id, Gfx::ShareableBitmap favicon) =|
did_request_all_cookies(u64 page_id, URL::URL url) => (Vector<Web::Cookie::Cookie> cookies)
did_request_named_cookie(u64 page_id, URL::URL url, String name) => (Optional<Web::Cookie::Cookie> cookie)

View file

@ -11,6 +11,7 @@
#include <LibWeb/Page/InputEvent.h>
#include <LibWeb/WebDriver/ExecuteScript.h>
#include <LibWebView/Attribute.h>
#include <LibWebView/PageInfo.h>
endpoint WebContentServer
{
@ -62,14 +63,10 @@ endpoint WebContentServer
take_document_screenshot(u64 page_id) =|
take_dom_node_screenshot(u64 page_id, i32 node_id) =|
dump_gc_graph(u64 page_id) => (String json)
request_internal_page_info(u64 page_id, WebView::PageInfoType type) =|
run_javascript(u64 page_id, ByteString js_source) =|
dump_layout_tree(u64 page_id) => (ByteString dump)
dump_paint_tree(u64 page_id) => (ByteString dump)
dump_text(u64 page_id) => (ByteString dump)
get_selected_text(u64 page_id) => (ByteString selection)
select_all(u64 page_id) =|
paste(u64 page_id, String text) =|

View file

@ -120,21 +120,6 @@ public:
m_pending_screenshot->resolve(screenshot.bitmap());
}
ErrorOr<String> dump_layout_tree()
{
return String::from_byte_string(client().dump_layout_tree(0));
}
ErrorOr<String> dump_paint_tree()
{
return String::from_byte_string(client().dump_paint_tree(0));
}
ErrorOr<String> dump_text()
{
return String::from_byte_string(client().dump_text(0));
}
void clear_content_filters()
{
client().async_set_content_filters(0, {});
@ -256,18 +241,15 @@ static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL
// It also causes a lot more code to run, which is good for finding bugs. :^)
(void)view.take_screenshot();
StringBuilder builder;
builder.append(view.dump_layout_tree().release_value_but_fixme_should_propagate_errors());
builder.append("\n"sv);
builder.append(view.dump_paint_tree().release_value_but_fixme_should_propagate_errors());
result = builder.to_string().release_value_but_fixme_should_propagate_errors();
auto promise = view.request_internal_page_info(WebView::PageInfoType::LayoutTree | WebView::PageInfoType::PaintTree);
result = MUST(promise->await());
loop.quit(0);
}
};
view.on_text_test_finish = {};
} else if (mode == TestMode::Text) {
view.on_load_finish = [&](auto const& loaded_url) {
// NOTE: We don't want subframe loads to trigger the test finish.
if (!url.equals(loaded_url, URL::ExcludeFragment::Yes))
@ -276,8 +258,11 @@ static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL
if (did_finish_test)
loop.quit(0);
};
view.on_text_test_finish = [&]() {
result = view.dump_text().release_value_but_fixme_should_propagate_errors();
auto promise = view.request_internal_page_info(WebView::PageInfoType::Text);
result = MUST(promise->await());
did_finish_test = true;
if (did_finish_loading)
loop.quit(0);