-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CSS highlight painting: further optimise offset mapping
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
1 parent
0748fc2
commit 1660cb9
Showing
10 changed files
with
444 additions
and
280 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
third_party/blink/renderer/core/paint/ng/marker_range_mapping_context.cc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
75 changes: 75 additions & 0 deletions
75
third_party/blink/renderer/core/paint/ng/marker_range_mapping_context.h
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_ |
78 changes: 78 additions & 0 deletions
78
third_party/blink/renderer/core/paint/ng/marker_range_mapping_context_test.cc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.