Skip to content

Commit

Permalink
CSS highlight painting: further optimise offset mapping
Browse files Browse the repository at this point in the history
One of the most expensive highlight painting operations is converting
DOM offsets to canonical text offsets.

This CL improves performance by switching to DOM offsets for
comparing the target fragment to the markers, meaning the fragment
is converted once and markers are only converted if they overlap
the fragment in DOM space. The existing MarkerRangeMappingContext
is modified to use DOM offsets and its use is enforced everywhere
where offset mapping is done for markers.

Based on an original proposal by dazabani@,
https://chromium-review.googlesource.com/c/chromium/src/+/4014023,
heavily modified to rebase and optimize further.

Other changes include:
* Clamp the marker offsets to the text fragment in DOM space before
mapping.
* Do not try to compute overflow for empty marker lists.
* Testing for MarkerOffsetMappingContext
* Clean up some use of Member<>

Bug: 1442067
Change-Id: I50740cf88e0dfddf08c693fcb74068b51661d969
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4958993
Reviewed-by: Fredrik Söderquist <fs@opera.com>
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1216251}
  • Loading branch information
schenney-chromium authored and Chromium LUCI CQ committed Oct 27, 2023
1 parent 0748fc2 commit 1660cb9
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 280 deletions.
51 changes: 28 additions & 23 deletions third_party/blink/renderer/core/layout/ng/ng_ink_overflow.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "third_party/blink/renderer/core/layout/geometry/writing_mode_converter.h"
#include "third_party/blink/renderer/core/layout/inline/fragment_item.h"
#include "third_party/blink/renderer/core/layout/ng/ng_text_decoration_offset.h"
#include "third_party/blink/renderer/core/paint/ng/marker_range_mapping_context.h"
#include "third_party/blink/renderer/core/paint/ng/ng_highlight_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_inline_paint_context.h"
#include "third_party/blink/renderer/core/paint/text_decoration_info.h"
Expand Down Expand Up @@ -602,21 +603,25 @@ LogicalRect NGInkOverflow::ComputeDecorationOverflow(
accumulated_bound.Unite(custom_bound);
}
}

if (do_spelling_grammar) {
DocumentMarkerVector spelling_markers = controller.MarkersFor(
*text_node, DocumentMarker::MarkerTypes::Spelling());
LogicalRect spelling_bound = ComputeMarkerOverflow(
spelling_markers, DocumentMarker::kSpelling, fragment_item, text_node,
style, scaled_font, container_offset, ink_overflow, inline_context);
accumulated_bound.Unite(spelling_bound);
if (!spelling_markers.empty()) {
LogicalRect spelling_bound = ComputeMarkerOverflow(
spelling_markers, DocumentMarker::kSpelling, fragment_item,
text_node, style, scaled_font, container_offset, ink_overflow,
inline_context);
accumulated_bound.Unite(spelling_bound);
}

DocumentMarkerVector grammar_markers = controller.MarkersFor(
*text_node, DocumentMarker::MarkerTypes::Grammar());
LogicalRect grammar_bound = ComputeMarkerOverflow(
grammar_markers, DocumentMarker::kGrammar, fragment_item, text_node,
style, scaled_font, container_offset, ink_overflow, inline_context);
accumulated_bound.Unite(grammar_bound);
if (!grammar_markers.empty()) {
LogicalRect grammar_bound = ComputeMarkerOverflow(
grammar_markers, DocumentMarker::kGrammar, fragment_item, text_node,
style, scaled_font, container_offset, ink_overflow, inline_context);
accumulated_bound.Unite(grammar_bound);
}
}
}
return accumulated_bound;
Expand Down Expand Up @@ -681,14 +686,14 @@ LogicalRect NGInkOverflow::ComputeMarkerOverflow(
? nullptr
: HighlightStyleUtils::HighlightPseudoStyle(
text_node, style, NGHighlightPainter::PseudoFor(type));
const TextOffsetRange fragment_dom_offsets =
NGHighlightPainter::GetFragmentDOMOffsets(
*text_node, fragment_item->StartOffset(), fragment_item->EndOffset());
MarkerRangeMappingContext mapping_context(*text_node, fragment_dom_offsets);
for (auto marker : markers) {
const unsigned marker_start_offset =
NGHighlightPainter::GetTextContentOffset(*text_node,
marker->StartOffset());
const unsigned marker_end_offset = NGHighlightPainter::GetTextContentOffset(
*text_node, marker->EndOffset());
if (marker_start_offset > fragment_item->EndOffset() ||
marker_end_offset < fragment_item->StartOffset()) {
std::optional<TextOffsetRange> marker_offsets =
mapping_context.GetTextContentOffsets(*marker);
if (!marker_offsets) {
return LogicalRect();
}
LogicalRect decoration_bound;
Expand Down Expand Up @@ -723,14 +728,14 @@ LogicalRect NGInkOverflow::ComputeCustomHighlightOverflow(
const LogicalRect& ink_overflow,
const NGInlinePaintContext* inline_context) {
LogicalRect accumulated_bound;
const TextOffsetRange fragment_dom_offsets =
NGHighlightPainter::GetFragmentDOMOffsets(
*text_node, fragment_item->StartOffset(), fragment_item->EndOffset());
MarkerRangeMappingContext mapping_context(*text_node, fragment_dom_offsets);
for (auto marker : markers) {
const unsigned marker_start_offset =
NGHighlightPainter::GetTextContentOffset(*text_node,
marker->StartOffset());
const unsigned marker_end_offset = NGHighlightPainter::GetTextContentOffset(
*text_node, marker->EndOffset());
if (marker_start_offset > fragment_item->EndOffset() ||
marker_end_offset < fragment_item->StartOffset()) {
std::optional<TextOffsetRange> marker_offsets =
mapping_context.GetTextContentOffsets(*marker);
if (!marker_offsets) {
return LogicalRect();
}

Expand Down
3 changes: 3 additions & 0 deletions third_party/blink/renderer/core/paint/build.gni
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ blink_core_sources_paint = [
"image_painter.h",
"link_highlight_impl.cc",
"link_highlight_impl.h",
"ng/marker_range_mapping_context.cc",
"ng/marker_range_mapping_context.h",
"ng/ng_box_fragment_painter.cc",
"ng/ng_box_fragment_painter.h",
"ng/ng_decorating_box.h",
Expand Down Expand Up @@ -233,6 +235,7 @@ blink_core_tests_paint = [
"image_painter_test.cc",
"line_relative_rect_test.cc",
"link_highlight_impl_test.cc",
"ng/marker_range_mapping_context_test.cc",
"ng/ng_box_fragment_painter_test.cc",
"ng/ng_highlight_overlay_test.cc",
"ng/ng_highlight_painter_test.cc",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/core/paint/ng/marker_range_mapping_context.h"

#include "third_party/blink/renderer/core/editing/markers/document_marker.h"
#include "third_party/blink/renderer/core/editing/position.h"

namespace blink {

MarkerRangeMappingContext::DOMToTextContentOffsetMapper::
DOMToTextContentOffsetMapper(const Text& text_node) {
units_ = GetMappingUnits(text_node.GetLayoutObject());
units_begin_ = units_.begin();
DCHECK(units_.size());
}

base::span<const OffsetMappingUnit>
MarkerRangeMappingContext::DOMToTextContentOffsetMapper::GetMappingUnits(
const LayoutObject* layout_object) {
const OffsetMapping* const offset_mapping =
OffsetMapping::GetFor(layout_object);
DCHECK(offset_mapping);
return offset_mapping->GetMappingUnitsForLayoutObject(*layout_object);
}

unsigned
MarkerRangeMappingContext::DOMToTextContentOffsetMapper::GetTextContentOffset(
unsigned dom_offset) const {
auto unit = FindUnit(units_begin_, dom_offset);
// Update the cached search starting point.
units_begin_ = unit;
// Since the unit range only covers the fragment, map anything that falls
// outside of that range to the start/end.
if (dom_offset < unit->DOMStart()) {
return unit->TextContentStart();
}
if (dom_offset > unit->DOMEnd()) {
return unit->TextContentEnd();
}
return unit->ConvertDOMOffsetToTextContent(dom_offset);
}

unsigned MarkerRangeMappingContext::DOMToTextContentOffsetMapper::
GetTextContentOffsetNoCache(unsigned dom_offset) const {
auto unit = FindUnit(units_begin_, dom_offset);
// Since the unit range only covers the fragment, map anything that falls
// outside of that range to the start/end.
if (dom_offset < unit->DOMStart()) {
return unit->TextContentStart();
}
if (dom_offset > unit->DOMEnd()) {
return unit->TextContentEnd();
}
return unit->ConvertDOMOffsetToTextContent(dom_offset);
}

// Find the mapping unit for `dom_offset`, starting from `begin`.
base::span<const OffsetMappingUnit>::iterator
MarkerRangeMappingContext::DOMToTextContentOffsetMapper::FindUnit(
base::span<const OffsetMappingUnit>::iterator begin,
unsigned dom_offset) const {
if (dom_offset <= begin->DOMEnd()) {
return begin;
}
return std::prev(
std::upper_bound(begin, units_.end(), dom_offset,
[](unsigned offset, const OffsetMappingUnit& unit) {
return offset < unit.DOMStart();
}));
}

absl::optional<TextOffsetRange>
MarkerRangeMappingContext::GetTextContentOffsets(
const DocumentMarker& marker) const {
if (marker.EndOffset() <= fragment_dom_range_.start ||
marker.StartOffset() >= fragment_dom_range_.end) {
return absl::nullopt;
}

// Clamp the marker to the fragment in DOM space
const unsigned start_dom_offset =
std::max(marker.StartOffset(), fragment_dom_range_.start);
const unsigned end_dom_offset =
std::min(marker.EndOffset(), fragment_dom_range_.end);
const unsigned text_content_start =
mapper_.GetTextContentOffset(start_dom_offset);
const unsigned text_content_end =
mapper_.GetTextContentOffsetNoCache(end_dom_offset);
return TextOffsetRange(text_content_start, text_content_end);
}

} // namespace blink
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_NG_MARKER_RANGE_MAPPING_CONTEXT_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_NG_MARKER_RANGE_MAPPING_CONTEXT_H_

#include "third_party/blink/renderer/core/layout/inline/fragment_item.h"
#include "third_party/blink/renderer/core/layout/inline/offset_mapping.h"
#include "third_party/blink/renderer/core/layout/inline/text_offset_range.h"

namespace blink {

class DocumentMarker;

// Helper for mapping from DOM offset (range) to text content offset.
//
// Exploits the fact that DocumentMarkers are sorted in DOM offset order, to
// maintain a cached starting point within the unit mapping range and thus
// amortize the cost of unit lookup.
class CORE_EXPORT MarkerRangeMappingContext {
STACK_ALLOCATED();

private:
// The internal class that implements the mapping.
class CORE_EXPORT DOMToTextContentOffsetMapper {
STACK_ALLOCATED();

public:
explicit DOMToTextContentOffsetMapper(const Text& text_node);

unsigned GetTextContentOffset(unsigned dom_offset) const;

unsigned GetTextContentOffsetNoCache(unsigned dom_offset) const;

void Reset() const { units_begin_ = units_.begin(); }

private:
base::span<const OffsetMappingUnit> GetMappingUnits(
const LayoutObject* layout_object);

// Find the mapping unit for `dom_offset`, starting from `begin`.
base::span<const OffsetMappingUnit>::iterator FindUnit(
base::span<const OffsetMappingUnit>::iterator begin,
unsigned dom_offset) const;

base::span<const OffsetMappingUnit> units_;
mutable base::span<const OffsetMappingUnit>::iterator units_begin_;
};

public:
MarkerRangeMappingContext() = delete;

explicit MarkerRangeMappingContext(const Text& text_node,
const TextOffsetRange& fragment_dom_range)
: mapper_(DOMToTextContentOffsetMapper(text_node)),
fragment_dom_range_(fragment_dom_range),
text_length_(text_node.length()) {}

// Computes the text fragment offsets for the given marker’s start and end,
// or returns nullopt if the marker is completely outside the fragment.
absl::optional<TextOffsetRange> GetTextContentOffsets(
const DocumentMarker&) const;

void Reset() const { mapper_.Reset(); }

private:
const DOMToTextContentOffsetMapper mapper_;
const TextOffsetRange fragment_dom_range_;
const unsigned text_length_;
};

} // namespace blink

#endif // THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_NG_MARKER_RANGE_MAPPING_CONTEXT_H_
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/core/paint/ng/marker_range_mapping_context.h"

#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/markers/text_fragment_marker.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"

namespace blink {

class MarkerRangeMappingContextTest : public RenderingTest {
public:
MarkerRangeMappingContextTest()
: RenderingTest(MakeGarbageCollected<EmptyLocalFrameClient>()) {}
};

TEST_F(MarkerRangeMappingContextTest, FullNodeOffsetsCorrect) {
// Laid out as
// a b c d e f g h i
// j k l m n o p q r
//
// Two fragments:
// DOM offsets (9,26), (27,44)
// Text offsets (0,17), (0,17)
SetBodyInnerHTML(R"HTML(
<div style="width:100px;">
a b c d e f g h i j k l m n o p q r
</div>
)HTML");

auto* div_node =
To<HTMLDivElement>(GetDocument().QuerySelector(AtomicString("div")));
ASSERT_TRUE(div_node->firstChild()->IsTextNode());
auto* text_node = To<Text>(div_node->firstChild());
ASSERT_TRUE(text_node);

const TextOffsetRange fragment_range = {9, 26};
MarkerRangeMappingContext mapping_context(*text_node, fragment_range);

TextFragmentMarker marker_pre(1, 5); // Before text
auto offsets = mapping_context.GetTextContentOffsets(marker_pre);
ASSERT_FALSE(offsets.has_value());
offsets.reset();

TextFragmentMarker marker_a(7, 10); // Partially before
offsets = mapping_context.GetTextContentOffsets(marker_a);
ASSERT_TRUE(offsets.has_value());
ASSERT_EQ(0u, offsets->start);
ASSERT_EQ(1u, offsets->end);
offsets.reset();

TextFragmentMarker marker_b(11, 12); // 'b'
offsets = mapping_context.GetTextContentOffsets(marker_b);
ASSERT_TRUE(offsets.has_value());
ASSERT_EQ(2u, offsets->start);
ASSERT_EQ(3u, offsets->end);
offsets.reset();

TextFragmentMarker marker_ij(25, 28); // Overlaps 1st and 2nd line
offsets = mapping_context.GetTextContentOffsets(marker_ij);
ASSERT_TRUE(offsets.has_value());
ASSERT_EQ(16u, offsets->start);
ASSERT_EQ(17u, offsets->end);
offsets.reset();

TextFragmentMarker marker_post(30, 35); // After text
offsets = mapping_context.GetTextContentOffsets(marker_post);
ASSERT_FALSE(offsets.has_value());
offsets.reset();
}

} // namespace blink

0 comments on commit 1660cb9

Please sign in to comment.