LibWeb: Collect layout inspection data only when DevTools is connected

Grid and flex inspector payloads are only consumed by DevTools, but
layout currently builds them for every page. Gate that collection on an
active DevTools client so ordinary browsing avoids the extra CPU work
and retained memory.

DevTools may connect after the page has already completed layout, so
force one catch-up layout pass when the first DevTools client attaches.
After that, normal relayouts keep the data fresh until DevTools
disconnects. Inspection requests only flush dirty layout instead of
forcing repeated collection passes.

When DevTools detaches, clear the stored inspection data and overlays.
This commit is contained in:
Sam Atkins 2026-06-01 12:27:46 +01:00
parent 8c60d20dfc
commit 26fdc5c09d
9 changed files with 75 additions and 12 deletions

View file

@ -1779,7 +1779,11 @@ void Document::update_layout(UpdateLayoutReason reason)
for (size_t layout_pass = 0; layout_pass < max_container_query_layout_passes; ++layout_pass) {
update_style();
if (layout_is_up_to_date())
auto const should_collect_devtools_layout_data = page().client().has_active_devtools_client();
auto const force_devtools_layout_data_collection = should_collect_devtools_layout_data
&& reason == UpdateLayoutReason::InspectDevToolsLayoutData;
if (layout_is_up_to_date() && !force_devtools_layout_data_collection)
return;
auto svg_roots_to_relayout = move(m_svg_roots_needing_relayout);
@ -1866,6 +1870,7 @@ void Document::update_layout(UpdateLayoutReason reason)
Layout::LayoutState layout_state;
layout_state.ensure_capacity(layout_index_counter);
layout_state.set_should_collect_devtools_layout_data(should_collect_devtools_layout_data);
{
auto& viewport = static_cast<Layout::Viewport&>(*m_layout_root);
@ -1963,6 +1968,22 @@ void Document::update_layout(UpdateLayoutReason reason)
VERIFY(layout_is_up_to_date());
}
void Document::clear_devtools_layout_inspection_data()
{
clear_grid_highlighted_node(nullptr);
clear_flexbox_highlighted_node(nullptr);
auto paintable = this->paintable();
if (!paintable)
return;
paintable->for_each_in_subtree_of_type<Painting::PaintableBox>([](auto& paintable_box) {
paintable_box.set_grid_layout_data(nullptr);
paintable_box.set_flex_layout_data(nullptr);
return TraversalDecision::Continue;
});
}
bool Document::layout_is_up_to_date() const
{
if (!navigable() || navigable()->active_document() != this)

View file

@ -126,8 +126,7 @@ enum class InvalidateLayoutTreeReason {
X(HostedDocumentBeforePaint) \
X(InspectAccessibilityTree) \
X(InspectDOMTree) \
X(InspectFlexboxLayout) \
X(InspectGridLayout) \
X(InspectDevToolsLayoutData) \
X(InternalsHitTest) \
X(MediaQueryListMatches) \
X(NavigableSelectedText) \
@ -400,6 +399,7 @@ public:
void update_layout(UpdateLayoutReason);
void update_layout_if_needed_for_node(Node const&, UpdateLayoutReason);
[[nodiscard]] bool layout_is_up_to_date() const;
void clear_devtools_layout_inspection_data();
void update_paint_and_hit_testing_properties_if_needed();
void update_animated_style_if_needed();

View file

@ -247,7 +247,8 @@ void FlexFormattingContext::run(AvailableSpace const& available_space)
resolve_baseline_aligned_items();
}
save_flex_layout_data();
if (m_state.should_collect_devtools_layout_data())
save_flex_layout_data();
}
void FlexFormattingContext::parent_context_did_dimension_child_root_box()

View file

@ -2420,6 +2420,9 @@ void GridFormattingContext::resolve_track_spacing(GridDimension dimension)
void GridFormattingContext::save_grid_layout_data()
{
if (!m_state.should_collect_devtools_layout_data())
return;
auto data = make<GridLayoutData>();
data->direction = grid_container().computed_values().direction();
data->is_subgrid = is_subgridded_axis(GridDimension::Column) || is_subgridded_axis(GridDimension::Row);

View file

@ -392,6 +392,9 @@ struct LayoutState {
void ensure_capacity(u32 node_count);
void set_should_collect_devtools_layout_data(bool should_collect) { m_should_collect_devtools_layout_data = should_collect; }
bool should_collect_devtools_layout_data() const { return m_should_collect_devtools_layout_data; }
UsedValues& get_mutable(NodeWithStyle const&);
UsedValues const& get(NodeWithStyle const&) const;
@ -408,6 +411,7 @@ private:
PagedStore<UsedValues> m_used_values_store;
GC::Ptr<Layout::NodeWithStyle const> m_subtree_root;
bool m_should_collect_devtools_layout_data { false };
};
inline CSSPixels clamp_to_max_dimension_value(CSSPixels value)

View file

@ -414,6 +414,7 @@ public:
virtual Page& page() = 0;
virtual Page const& page() const = 0;
virtual bool is_connection_open() const = 0;
virtual bool has_active_devtools_client() const { return false; }
virtual bool is_url_suitable_for_same_process_navigation([[maybe_unused]] URL::URL const& current_url, [[maybe_unused]] URL::URL const& target_url) const { return true; }
virtual void request_new_process_for_navigation(URL::URL const&) { }
virtual Gfx::Palette palette() const = 0;

View file

@ -902,7 +902,7 @@ static void append_grid_layouts_for_node_and_frame_descendants(Web::DOM::Node& r
if (!content_document->origin().is_same_origin_domain(navigable_container->document().origin()))
return Web::TraversalDecision::Continue;
content_document->update_layout(Web::DOM::UpdateLayoutReason::InspectGridLayout);
content_document->update_layout(Web::DOM::UpdateLayoutReason::Debugging);
append_grid_layouts_for_node_and_frame_descendants(*content_document, grid_layouts);
return Web::TraversalDecision::Continue;
});
@ -920,7 +920,7 @@ void ConnectionFromClient::inspect_grid_layouts(u64 page_id, Web::UniqueNodeID r
return;
}
root_node->document().update_layout(Web::DOM::UpdateLayoutReason::InspectGridLayout);
root_node->document().update_layout(Web::DOM::UpdateLayoutReason::Debugging);
JsonArray grid_layouts;
append_grid_layouts_for_node_and_frame_descendants(*root_node, grid_layouts);
@ -940,7 +940,7 @@ void ConnectionFromClient::inspect_current_grid(u64 page_id, Web::UniqueNodeID n
return;
}
node->document().update_layout(Web::DOM::UpdateLayoutReason::InspectGridLayout);
node->document().update_layout(Web::DOM::UpdateLayoutReason::Debugging);
for (auto const* current = node; current; current = current->parent_or_shadow_host_node()) {
if (auto grid_layout = grid_layout_for_node(*current); grid_layout.has_value()) {
@ -964,7 +964,7 @@ void ConnectionFromClient::inspect_current_flexbox(u64 page_id, Web::UniqueNodeI
return;
}
node->document().update_layout(Web::DOM::UpdateLayoutReason::InspectFlexboxLayout);
node->document().update_layout(Web::DOM::UpdateLayoutReason::Debugging);
for (auto const* current = only_look_at_parents ? node->parent_or_shadow_host_node() : node; current; current = current->parent_or_shadow_host_node()) {
if (auto flex_layout = flex_layout_for_node(*current); flex_layout.has_value()) {
@ -1062,10 +1062,15 @@ void ConnectionFromClient::highlight_flexbox(u64 page_id, Web::UniqueNodeID node
return;
auto* node = Web::DOM::Node::from_unique_id(node_id);
if (!node || !node->layout_node())
if (!node)
return;
node->document().set_flexbox_highlighted_node(node, flexbox_inspector_overlay_options_from_json(options));
auto& document = node->document();
document.update_layout(Web::DOM::UpdateLayoutReason::Debugging);
if (!node->layout_node())
return;
document.set_flexbox_highlighted_node(node, flexbox_inspector_overlay_options_from_json(options));
}
void ConnectionFromClient::clear_flexbox_highlight(u64 page_id, Web::UniqueNodeID node_id)
@ -1094,10 +1099,15 @@ void ConnectionFromClient::highlight_grid(u64 page_id, Web::UniqueNodeID node_id
return;
auto* node = Web::DOM::Node::from_unique_id(node_id);
if (!node || !node->layout_node())
if (!node)
return;
node->document().set_grid_highlighted_node(node, grid_inspector_overlay_options_from_json(options));
auto& document = node->document();
document.update_layout(Web::DOM::UpdateLayoutReason::Debugging);
if (!node->layout_node())
return;
document.set_grid_highlighted_node(node, grid_inspector_overlay_options_from_json(options));
}
void ConnectionFromClient::clear_grid_highlight(u64 page_id, Web::UniqueNodeID node_id)

View file

@ -28,6 +28,7 @@
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
@ -935,13 +936,34 @@ void PageClient::page_did_receive_network_response_body(u64 request_id, Readonly
void PageClient::did_connect_devtools_client()
{
auto was_first_devtools_client = !has_devtools_client();
++m_devtools_client_count;
if (!was_first_devtools_client)
return;
for (auto& navigable : Web::HTML::all_navigables()) {
if (&navigable->page() != &page())
continue;
if (auto active_document = navigable->active_document())
active_document->update_layout(Web::DOM::UpdateLayoutReason::InspectDevToolsLayoutData);
}
}
void PageClient::did_disconnect_devtools_client()
{
VERIFY(m_devtools_client_count > 0);
--m_devtools_client_count;
if (has_devtools_client())
return;
for (auto& navigable : Web::HTML::all_navigables()) {
if (&navigable->page() != &page())
continue;
if (auto active_document = navigable->active_document())
active_document->clear_devtools_layout_inspection_data();
}
}
void PageClient::page_did_finish_network_request(u64 request_id, u64 body_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error)

View file

@ -87,6 +87,7 @@ public:
void did_connect_devtools_client();
void did_disconnect_devtools_client();
bool has_devtools_client() const { return m_devtools_client_count > 0; }
virtual bool has_active_devtools_client() const override { return has_devtools_client(); }
void initialize_js_console(Web::DOM::Document& document);
void js_console_input(StringView js_source);