Skip to content

Commit

Permalink
LibWeb: Cache text blocks used by find in page
Browse files Browse the repository at this point in the history
The first step of the find in page algorithm is to walk the layout tree
of each document on the page and construct a list of strings against
which to search for matches.

Previously, this was being done for each new query, even when the
page content hadn't been updated. The output of this process is now
cached in the viewport node of the associated document. This ensures
that this process is no longer repeated unnceessarily.
  • Loading branch information
tcl3 committed Jun 29, 2024
1 parent d9470d6 commit 6bef44b
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 54 deletions.
56 changes: 2 additions & 54 deletions Userland/Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5111,65 +5111,13 @@ void Document::set_needs_to_refresh_scroll_state(bool b)

Vector<JS::Handle<DOM::Range>> Document::find_matching_text(String const& query, CaseSensitivity case_sensitivity)
{
if (!document_element() || !document_element()->layout_node())
if (!layout_node())
return {};

struct TextPosition {
DOM::Text& dom_node;
size_t start_offset { 0 };
};

struct TextBlock {
String text;
Vector<TextPosition> positions;
};

auto gather_text_blocks = [&]() -> Vector<TextBlock> {
StringBuilder builder;
size_t current_start_position = 0;
Vector<TextPosition> text_positions;
Vector<TextBlock> text_blocks;
document_element()->layout_node()->for_each_in_inclusive_subtree([&](auto const& layout_node) {
if (layout_node.display().is_none() || !layout_node.paintable() || !layout_node.paintable()->is_visible())
return TraversalDecision::Continue;

if (layout_node.is_block_container()) {
if (!builder.is_empty()) {
text_blocks.append({ builder.to_string_without_validation(), text_positions });
current_start_position = 0;
text_positions.clear_with_capacity();
builder.clear();
}
return TraversalDecision::Continue;
}

if (layout_node.is_text_node()) {
auto const& text_node = verify_cast<Layout::TextNode>(layout_node);
auto& dom_node = const_cast<DOM::Text&>(text_node.dom_node());
if (text_positions.is_empty()) {
text_positions.empend(dom_node);
} else {
text_positions.empend(dom_node, current_start_position);
}

auto const& current_node_text = text_node.text_for_rendering();
current_start_position += current_node_text.bytes_as_string_view().length();
builder.append(move(current_node_text));
}

return TraversalDecision::Continue;
});

if (!builder.is_empty())
text_blocks.append({ builder.to_string_without_validation(), text_positions });

return text_blocks;
};

// Ensure the layout tree exists before searching for text matches.
update_layout();

auto text_blocks = gather_text_blocks();
auto const& text_blocks = layout_node()->text_blocks();
if (text_blocks.is_empty())
return {};

Expand Down
63 changes: 63 additions & 0 deletions Userland/Libraries/LibWeb/Layout/Viewport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,67 @@ JS::GCPtr<Painting::Paintable> Viewport::create_paintable() const
return Painting::ViewportPaintable::create(*this);
}

void Viewport::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
if (!m_text_blocks.has_value())
return;

for (auto& text_block : *m_text_blocks) {
for (auto& text_position : text_block.positions)
visitor.visit(text_position.dom_node);
}
}

Vector<Viewport::TextBlock> const& Viewport::text_blocks()
{
if (!m_text_blocks.has_value())
update_text_blocks();

return *m_text_blocks;
}

void Viewport::update_text_blocks()
{
StringBuilder builder;
size_t current_start_position = 0;
Vector<TextPosition> text_positions;
Vector<TextBlock> text_blocks;
for_each_in_inclusive_subtree([&](auto const& layout_node) {
if (layout_node.display().is_none() || !layout_node.paintable() || !layout_node.paintable()->is_visible())
return TraversalDecision::Continue;

if (layout_node.is_box()) {
if (!builder.is_empty()) {
text_blocks.append({ builder.to_string_without_validation(), text_positions });
current_start_position = 0;
text_positions.clear_with_capacity();
builder.clear();
}
return TraversalDecision::Continue;
}

if (layout_node.is_text_node()) {
auto const& text_node = verify_cast<Layout::TextNode>(layout_node);
auto& dom_node = const_cast<DOM::Text&>(text_node.dom_node());
if (text_positions.is_empty()) {
text_positions.empend(dom_node);
} else {
text_positions.empend(dom_node, current_start_position);
}

auto const& current_node_text = text_node.text_for_rendering();
current_start_position += current_node_text.bytes_as_string_view().length();
builder.append(move(current_node_text));
}

return TraversalDecision::Continue;
});

if (!builder.is_empty())
text_blocks.append({ builder.to_string_without_validation(), text_positions });

m_text_blocks = move(text_blocks);
}

}
16 changes: 16 additions & 0 deletions Userland/Libraries/LibWeb/Layout/Viewport.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,28 @@ class Viewport final : public BlockContainer {
explicit Viewport(DOM::Document&, NonnullRefPtr<CSS::StyleProperties>);
virtual ~Viewport() override;

struct TextPosition {
JS::NonnullGCPtr<DOM::Text> dom_node;
size_t start_offset { 0 };
};
struct TextBlock {
String text;
Vector<TextPosition> positions;
};
Vector<TextBlock> const& text_blocks();

const DOM::Document& dom_node() const { return static_cast<const DOM::Document&>(*Node::dom_node()); }

virtual void visit_edges(Visitor&) override;

private:
virtual JS::GCPtr<Painting::Paintable> create_paintable() const override;

void update_text_blocks();

virtual bool is_viewport() const override { return true; }

Optional<Vector<TextBlock>> m_text_blocks;
};

template<>
Expand Down

0 comments on commit 6bef44b

Please sign in to comment.