0

[LayoutNG] Improve LayoutText::SetTextWithOffset() by reducing text shaping

This patch changes |LayoutText::SetTextWithOffset()| to reduce text shaping by
reusing |ShapeResult| of before and after replaced range for improving
performance, 10% to 15% faster but 3 times slower than legacy layout.
(Profile data: http://bit.ly/33B9rfg)

From this patch, |SetTextWithOffset()| does:
 1. Get offset mapping to map DOM offsets to text content offsets for replaced
  range.
 2. Collect inlines into |text_content| and |NGInlineItem| to get newly inserted
  text.
 3. Copy before and after |ShapeResult| from current data to |NGInlineItem| list
  in step 2.
 4. Call |NGInlineNode::ShapeText()| with |NGInlineItem| list to reuse
  |ShapeResult|.

This patch gets rid of NGInlineNodeTest.InvalidateSetTextWithOffset because
new test case replaces this test and this test call |SetTextWithOffset()| with
wrong parameter. It shoulbe |SetTextWithOffset("baftere", 1, 4)| as result of
|"before".replaceData(1, 4, "after")|.

Note: This is reland of http://crrev.com/c/1729330

TBR=yosin@chromium.org

Bug: 707656, 998507,998683, 999005
Change-Id: I016180de37b7afcb71998658ad66c001482029fd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1775647
Reviewed-by: Yoshifumi Inoue <yosin@chromium.org>
Commit-Queue: Yoshifumi Inoue <yosin@chromium.org>
Auto-Submit: Yoshifumi Inoue <yosin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#691893}
This commit is contained in:
Yoshifumi Inoue
2019-08-30 01:17:21 +00:00
committed by Commit Bot
parent 31fc62a978
commit e388dd67d6
14 changed files with 902 additions and 51 deletions

@ -0,0 +1,40 @@
<!DOCTYPE html>
<body>
<script src="../resources/runner.js"></script>
<script src="resources/line-layout-perf-test.js"></script>
<style>
#container {
width: 120ch;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
<pre id="container">A</pre>
<script>
const kNumberOfWords = 100;
const kSampleText = (() => {
const words = [];
for (let i = 1; i <= kNumberOfWords; ++i) {
words.push(TextGenerator.createWord(i % 12 + 3));
if (i % 10 === 0)
words.push('\n');
}
return words.join(' ');
})().repeat(1);
const kCount = 10;
const container = document.getElementById('container');
const editable = container.firstChild;
PerfTestRunner.measureRunsPerSecond({
run: function() {
for (let i = 0; i < kCount; ++i) {
editable.nodeValue = kSampleText;
PerfTestRunner.forceLayout();
editable.appendData('xyz');
PerfTestRunner.forceLayout();
}
},
});
</script>

@ -0,0 +1,38 @@
<!DOCTYPE html>
<body>
<script src="../resources/runner.js"></script>
<script src="resources/line-layout-perf-test.js"></script>
<style>
#container {
width: 120ch;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
<pre id="container">A</pre>
<script>
const kNumberOfWords = 10;
const kSampleText = (() => {
const words = [];
for (let i = 1; i <= kNumberOfWords; ++i) {
words.push(TextGenerator.createWord(i % 12 + 3));
}
return words.join(' ');
})().repeat(1);
const kCount = 10;
const container = document.getElementById('container');
const editable = container.firstChild;
PerfTestRunner.measureRunsPerSecond({
run: function() {
for (let i = 0; i < kCount; ++i) {
editable.nodeValue = kSampleText;
PerfTestRunner.forceLayout();
editable.appendData('XYZ');
PerfTestRunner.forceLayout();
}
},
});
</script>

@ -0,0 +1,40 @@
<!DOCTYPE html>
<body>
<script src="../resources/runner.js"></script>
<script src="resources/line-layout-perf-test.js"></script>
<style>
#container {
width: 120ch;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
<pre id="container">A</pre>
<script>
const kNumberOfWords = 100;
const kSampleText = (() => {
const words = [];
for (let i = 1; i <= kNumberOfWords; ++i) {
words.push(TextGenerator.createWord(i % 12 + 3));
if (i % 10 === 0)
words.push('\n');
}
return words.join(' ');
})().repeat(1);
const kCount = 10;
const container = document.getElementById('container');
const editable = container.firstChild;
PerfTestRunner.measureRunsPerSecond({
run: function() {
for (let i = 0; i < kCount; ++i) {
editable.nodeValue = kSampleText;
PerfTestRunner.forceLayout();
editable.deleteData(10, 20);
PerfTestRunner.forceLayout();
}
},
});
</script>

@ -0,0 +1,40 @@
<!DOCTYPE html>
<body>
<script src="../resources/runner.js"></script>
<script src="resources/line-layout-perf-test.js"></script>
<style>
#container {
width: 120ch;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
<pre id="container">A</pre>
<script>
const kNumberOfWords = 100;
const kSampleText = (() => {
const words = [];
for (let i = 1; i <= kNumberOfWords; ++i) {
words.push(TextGenerator.createWord(i % 12 + 3));
if (i % 10 === 0)
words.push('\n');
}
return words.join(' ');
})().repeat(1);
const kCount = 10;
const container = document.getElementById('container');
const editable = container.firstChild;
PerfTestRunner.measureRunsPerSecond({
run: function() {
for (let i = 0; i < kCount; ++i) {
editable.nodeValue = kSampleText;
PerfTestRunner.forceLayout();
editable.insertData(10, 'xyz');
PerfTestRunner.forceLayout();
}
},
});
</script>

@ -0,0 +1,40 @@
<!DOCTYPE html>
<body>
<script src="../resources/runner.js"></script>
<script src="resources/line-layout-perf-test.js"></script>
<style>
#container {
width: 120ch;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
<pre id="container">A</pre>
<script>
const kNumberOfWords = 100;
const kSampleText = (() => {
const words = [];
for (let i = 1; i <= kNumberOfWords; ++i) {
words.push(TextGenerator.createWord(i % 12 + 3));
if (i % 10 === 0)
words.push('\n');
}
return words.join(' ');
})().repeat(1);
const kCount = 10;
const container = document.getElementById('container');
const editable = container.firstChild;
PerfTestRunner.measureRunsPerSecond({
run: function() {
for (let i = 0; i < kCount; ++i) {
editable.nodeValue = kSampleText;
PerfTestRunner.forceLayout();
editable.insertData(0, 'xyz');
PerfTestRunner.forceLayout();
}
},
});
</script>

@ -1359,6 +1359,7 @@ jumbo_source_set("unit_tests") {
"layout/ng/exclusions/ng_exclusion_space_test.cc",
"layout/ng/geometry/ng_box_strut_test.cc",
"layout/ng/geometry/ng_static_position_test.cc",
"layout/ng/inline/layout_ng_text_test.cc",
"layout/ng/inline/ng_baseline_test.cc",
"layout/ng/inline/ng_caret_position_test.cc",
"layout/ng/inline/ng_fragment_item_test.cc",

@ -1323,6 +1323,7 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
void SetNeedsCollectInlines();
void SetChildNeedsCollectInlines();
void ClearNeedsCollectInlines() { SetNeedsCollectInlines(false); }
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
void MarkContainerChainForLayout(bool schedule_relayout = true,
SubtreeLayoutScope* = nullptr);
@ -3242,7 +3243,6 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
void SetNeedsSimplifiedNormalFlowLayout(bool b) {
bitfields_.SetNeedsSimplifiedNormalFlowLayout(b);
}
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
private:
friend class LineLayoutItem;

@ -1662,6 +1662,16 @@ void LayoutText::SetTextWithOffset(scoped_refptr<StringImpl> text,
}
}
if (NGInlineNode::SetTextWithOffset(this, text, offset, len)) {
DCHECK(!NeedsCollectInlines());
// Prevent |TextDidChange()| to propagate |NeedsCollectInlines|
SetNeedsCollectInlines(true);
TextDidChange();
valid_ng_items_ = true;
ClearNeedsCollectInlines();
return;
}
unsigned old_len = TextLength();
unsigned new_len = text->length();
int delta = new_len - old_len;

@ -192,6 +192,7 @@ class CORE_EXPORT LayoutText : public LayoutObject {
void SetTextWithOffset(scoped_refptr<StringImpl>,
unsigned offset,
unsigned len);
void SetTextInternal(scoped_refptr<StringImpl>);
virtual void TransformText();
@ -322,11 +323,12 @@ class CORE_EXPORT LayoutText : public LayoutObject {
void SetHasBidiControlInlineItems() { has_bidi_control_items_ = true; }
void ClearHasBidiControlInlineItems() { has_bidi_control_items_ = false; }
protected:
virtual const base::span<NGInlineItem>* GetNGInlineItems() const {
return nullptr;
}
virtual base::span<NGInlineItem>* GetNGInlineItems() { return nullptr; }
protected:
void WillBeDestroyed() override;
void StyleWillChange(StyleDifference, const ComputedStyle&) final {}
@ -334,7 +336,6 @@ class CORE_EXPORT LayoutText : public LayoutObject {
void InLayoutNGInlineFormattingContextWillChange(bool) final;
void SetTextInternal(scoped_refptr<StringImpl>);
virtual void TextDidChange();
virtual InlineTextBox* CreateTextBox(int start,

@ -0,0 +1,258 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/layout/ng/inline/layout_ng_text.h"
#include <sstream>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
namespace blink {
class LayoutNGTextTest : public PageTestBase {
protected:
std::string GetItemsAsString(const LayoutText& layout_text) {
if (layout_text.NeedsCollectInlines())
return "LayoutText has NeedsCollectInlines";
if (!layout_text.HasValidInlineItems())
return "No valid inline items in LayoutText";
const LayoutBlockFlow& block_flow = *layout_text.ContainingNGBlockFlow();
if (block_flow.NeedsCollectInlines())
return "LayoutBlockFlow has NeedsCollectInlines";
const NGInlineNodeData& data = *block_flow.GetNGInlineNodeData();
std::ostringstream stream;
for (const NGInlineItem& item : data.items) {
if (item.Type() != NGInlineItem::kText)
continue;
if (item.GetLayoutObject() == layout_text)
stream << "*";
stream << "{'"
<< data.text_content.Substring(item.StartOffset(), item.Length())
.Utf8()
<< "'";
if (item.TextShapeResult()) {
stream << ", ShapeResult=" << item.TextShapeResult()->StartIndex()
<< "+" << item.TextShapeResult()->NumCharacters();
}
stream << "}" << std::endl;
}
return stream.str();
}
};
TEST_F(LayoutNGTextTest, SetTextWithOffsetAppendCollapseWhiteSpace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>abc </p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.appendData("XYZ");
EXPECT_EQ("*{'abc XYZ', ShapeResult=0+7}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetAppend) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.appendData("xyz");
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XYZxyz', ShapeResult=3+6}\n"
"{'def', ShapeResult=9+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetDelete) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>xXYZyz<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.deleteData(1, 3, ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'xyz', ShapeResult=3+3}\n"
"{'def', ShapeResult=6+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetDeleteCollapseWhiteSpace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>ab XY cd</p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.deleteData(4, 2, ASSERT_NO_EXCEPTION); // remove "XY"
EXPECT_EQ("*{'ab cd', ShapeResult=0+5}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetDeleteCollapseWhiteSpaceEnd) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>a bc</p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.deleteData(2, 2, ASSERT_NO_EXCEPTION); // remove "bc"
EXPECT_EQ("*{'a', ShapeResult=0+1}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetInsert) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.insertData(1, "xyz", ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XxyzYZ', ShapeResult=3+6}\n"
"{'def', ShapeResult=9+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetInsertAfterSpace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>ab cd</p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.insertData(3, " XYZ ", ASSERT_NO_EXCEPTION);
EXPECT_EQ("*{'ab XYZ cd', ShapeResult=0+9}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetInserBeforetSpace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>ab cd</p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.insertData(2, " XYZ ", ASSERT_NO_EXCEPTION);
EXPECT_EQ("*{'ab XYZ cd', ShapeResult=0+9}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetNoRelocation) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
// Note: |CharacterData::setData()| is implementation of Node::setNodeValue()
// for |CharacterData|.
text.setData("xyz");
EXPECT_EQ("LayoutText has NeedsCollectInlines",
GetItemsAsString(*text.GetLayoutObject()))
<< "There are no optimization for setData()";
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetPrepend) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.insertData(1, "xyz", ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XxyzYZ', ShapeResult=3+6}\n"
"{'def', ShapeResult=9+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetReplace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.replaceData(1, 2, "yz", ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XyzW', ShapeResult=3+4}\n"
"{'def', ShapeResult=7+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceCollapseWhiteSpace) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<p id=target>ab XY cd</p>");
Text& text = To<Text>(*GetElementById("target")->firstChild());
text.replaceData(4, 2, " ", ASSERT_NO_EXCEPTION); // replace "XY" to " "
EXPECT_EQ("*{'ab cd', ShapeResult=0+5}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceToExtend) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.replaceData(1, 2, "xyz", ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XxyzW', ShapeResult=3+5}\n"
"{'def', ShapeResult=8+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceToShrink) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
text.replaceData(1, 2, "y", ASSERT_NO_EXCEPTION);
EXPECT_EQ(
"{'abc', ShapeResult=0+3}\n"
"*{'XyW', ShapeResult=3+3}\n"
"{'def', ShapeResult=6+3}\n",
GetItemsAsString(*text.GetLayoutObject()));
}
TEST_F(LayoutNGTextTest, SetTextWithOffsetToEmpty) {
if (!RuntimeEnabledFeatures::LayoutNGEnabled())
return;
SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>");
Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling());
// Note: |CharacterData::setData()| is implementation of Node::setNodeValue()
// for |CharacterData|.
// Note: |setData()| detaches layout object from |Text| node since
// |Text::TextLayoutObjectIsNeeded()| returns false for empty text.
text.setData("");
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ(nullptr, text.GetLayoutObject());
}
} // namespace blink

@ -250,6 +250,7 @@ class CORE_EXPORT NGInlineItem {
unsigned is_symbol_marker_ : 1;
unsigned is_generated_for_line_break_ : 1;
friend class NGInlineNode;
friend class NGInlineNodeDataEditor;
};
inline void NGInlineItem::AssertOffset(unsigned offset) const {

@ -131,6 +131,114 @@ class ItemsBuilderForMarkLineBoxesDirty {
bool has_floating_or_out_of_flow_positioned_ = false;
};
// Wrapper over ShapeText that re-uses existing shape results for items that
// haven't changed.
class ReusingTextShaper final {
public:
ReusingTextShaper(NGInlineItemsData* data,
const Vector<NGInlineItem>* reusable_items)
: data_(*data),
reusable_items_(reusable_items),
shaper_(data->text_content) {}
scoped_refptr<ShapeResult> Shape(const NGInlineItem& start_item,
unsigned end_offset) {
const unsigned start_offset = start_item.StartOffset();
DCHECK_LT(start_offset, end_offset);
if (!reusable_items_)
return Reshape(start_item, start_offset, end_offset);
// TODO(yosin): We should support segment text
if (data_.segments)
return Reshape(start_item, start_offset, end_offset);
const Vector<const ShapeResult*> reusable_shape_results =
CollectReusableShapeResults(start_offset, end_offset);
if (reusable_shape_results.IsEmpty())
return Reshape(start_item, start_offset, end_offset);
const scoped_refptr<ShapeResult> shape_result =
ShapeResult::CreateEmpty(*reusable_shape_results.front());
unsigned offset = start_offset;
for (const ShapeResult* reusable_shape_result : reusable_shape_results) {
DCHECK_LE(offset, reusable_shape_result->StartIndex());
if (offset < reusable_shape_result->StartIndex()) {
AppendShapeResult(
*Reshape(start_item, offset, reusable_shape_result->StartIndex()),
shape_result.get());
offset = shape_result->EndIndex();
}
DCHECK_EQ(offset, reusable_shape_result->StartIndex());
DCHECK(shape_result->NumCharacters() == 0 ||
shape_result->EndIndex() == offset);
reusable_shape_result->CopyRange(
offset, std::min(reusable_shape_result->EndIndex(), end_offset),
shape_result.get());
offset = shape_result->EndIndex();
if (offset == end_offset)
return shape_result;
}
DCHECK_LT(offset, end_offset);
AppendShapeResult(*Reshape(start_item, offset, end_offset),
shape_result.get());
return shape_result;
}
private:
void AppendShapeResult(const ShapeResult& shape_result, ShapeResult* target) {
DCHECK(target->NumCharacters() == 0 ||
target->EndIndex() == shape_result.StartIndex());
shape_result.CopyRange(shape_result.StartIndex(), shape_result.EndIndex(),
target);
}
Vector<const ShapeResult*> CollectReusableShapeResults(unsigned start_offset,
unsigned end_offset) {
DCHECK_LT(start_offset, end_offset);
Vector<const ShapeResult*> shape_results;
if (!reusable_items_)
return shape_results;
for (const NGInlineItem *item = std::lower_bound(
reusable_items_->begin(), reusable_items_->end(), start_offset,
[](const NGInlineItem&item, unsigned offset) {
return item.EndOffset() <= offset;
});
item != reusable_items_->end(); ++item) {
DCHECK_LE(start_offset, item->StartOffset());
if (end_offset <= item->StartOffset())
break;
if (item->EndOffset() < start_offset)
continue;
if (!item->TextShapeResult())
continue;
shape_results.push_back(item->TextShapeResult());
}
return shape_results;
}
scoped_refptr<ShapeResult> Reshape(const NGInlineItem& start_item,
unsigned start_offset,
unsigned end_offset) {
DCHECK_LT(start_offset, end_offset);
const TextDirection direction = start_item.Direction();
const Font& font = start_item.Style()->GetFont();
if (data_.segments) {
return data_.segments->ShapeText(&shaper_, &font, direction, start_offset,
end_offset,
&start_item - data_.items.begin());
}
RunSegmenter::RunSegmenterRange range =
start_item.CreateRunSegmenterRange();
range.end = end_offset;
return shaper_.Shape(&font, direction, start_offset, end_offset, range);
}
NGInlineItemsData& data_;
const Vector<NGInlineItem>* const reusable_items_;
HarfBuzzShaper shaper_;
};
// The function is templated to indicate the purpose of collected inlines:
// - With EmptyOffsetMappingBuilder: updating layout;
// - With NGOffsetMappingBuilder: building offset mapping on clean layout.
@ -366,7 +474,7 @@ void NGInlineNode::PrepareLayout(
DCHECK(data);
CollectInlines(data, previous_data.get(), dirty_lines);
SegmentText(data);
ShapeText(data, previous_data.get());
ShapeText(data, &previous_data->text_content);
ShapeTextForFirstLineIfNeeded(data);
AssociateItemsWithInlines(data);
DCHECK_EQ(data, MutableData());
@ -384,6 +492,288 @@ void NGInlineNode::PrepareLayout(
#endif
}
// Building |NGInlineNodeData| for |LayoutText::SetTextWithOffset()| with
// reusing data.
class NGInlineNodeDataEditor final {
STACK_ALLOCATED();
public:
explicit NGInlineNodeDataEditor(const LayoutText& layout_text)
: block_flow_(layout_text.ContainingNGBlockFlow()),
layout_text_(layout_text) {
DCHECK(layout_text_.HasValidInlineItems());
}
LayoutBlockFlow* GetLayoutBlockFlow() const { return block_flow_; }
// Note: We can't use |Position| for |layout_text_.GetNode()| because |Text|
// node is already changed.
NGInlineNodeData* Prepare(unsigned offset, unsigned length) {
if (!block_flow_ || block_flow_->NeedsCollectInlines() ||
block_flow_->NeedsLayout() ||
block_flow_->GetDocument().NeedsLayoutTreeUpdate() ||
!block_flow_->GetNGInlineNodeData() ||
block_flow_->GetNGInlineNodeData()->text_content.IsNull())
return nullptr;
// Because of current text content has secured text, e.g. whole text is
// "***", all characters including collapsed white spaces are marker, and
// new text is original text, we can't reuse shape result.
if (layout_text_.StyleRef().TextSecurity() != ETextSecurity::kNone)
return nullptr;
// Note: We should compute offset mapping before calling
// |LayoutBlockFlow::TakeNGInlineNodeData()|
const NGOffsetMapping* const offset_mapping =
NGInlineNode::GetOffsetMapping(block_flow_);
DCHECK(offset_mapping);
const auto units =
offset_mapping->GetMappingUnitsForLayoutObject(layout_text_);
start_offset_ = ConvertDOMOffsetToTextContent(units, offset);
end_offset_ = ConvertDOMOffsetToTextContent(units, offset + length);
DCHECK_LE(start_offset_, end_offset_);
data_.reset(block_flow_->TakeNGInlineNodeData());
return data_.get();
}
void Run() {
const NGInlineNodeData& new_data = *block_flow_->GetNGInlineNodeData();
const int diff =
new_data.text_content.length() - data_->text_content.length();
// |inserted_text_length| can be negative when white space is collapsed
// after text change.
// * "ab cd ef" => delete "cd" => "ab ef"
// We should not reuse " " before "ef"
// * "a bc" => delete "bc" => "a"
// There are no spaces after "a".
const int inserted_text_length = end_offset_ - start_offset_ + diff;
DCHECK_GE(inserted_text_length, -1);
const unsigned start_offset =
inserted_text_length < 0 && end_offset_ == data_->text_content.length()
? start_offset_ - 1
: start_offset_;
const unsigned end_offset =
inserted_text_length < 0 && start_offset_ == start_offset
? end_offset_ + 1
: end_offset_;
DCHECK_LE(end_offset, data_->text_content.length());
DCHECK_LE(start_offset, end_offset);
#if DCHECK_IS_ON()
if (start_offset_ != start_offset) {
DCHECK_EQ(data_->text_content[start_offset], ' ');
DCHECK_EQ(end_offset, end_offset_);
}
if (end_offset_ != end_offset) {
DCHECK_EQ(data_->text_content[end_offset_], ' ');
DCHECK_EQ(start_offset, start_offset_);
}
#endif
Vector<NGInlineItem> items;
// +3 for before and after replaced text.
items.ReserveInitialCapacity(data_->items.size() + 3);
// Copy items before replaced range
auto* it = data_->items.begin();
while (it->end_offset_ < start_offset ||
it->layout_object_ != layout_text_) {
DCHECK(it != data_->items.end());
items.push_back(*it);
++it;
}
DCHECK_EQ(it->layout_object_, layout_text_);
// Copy part of item before replaced range.
if (it->start_offset_ < start_offset)
items.push_back(CopyItemBefore(*it, start_offset));
// Skip items in replaced range.
while (it->end_offset_ < end_offset)
++it;
DCHECK_EQ(it->layout_object_, layout_text_);
// Inserted text
if (inserted_text_length > 0) {
const unsigned inserted_start_offset =
items.IsEmpty() ? 0 : items.back().end_offset_;
const unsigned inserted_end_offset =
inserted_start_offset + inserted_text_length;
items.push_back(NGInlineItem(*it, inserted_start_offset,
inserted_end_offset, nullptr));
}
// Copy part of item after replaced range.
if (end_offset < it->end_offset_) {
items.push_back(CopyItemAfter(*it, end_offset));
ShiftItem(&items.back(), diff);
}
// Copy items after replaced range
++it;
while (it != data_->items.end()) {
DCHECK_NE(it->layout_object_, layout_text_);
DCHECK_LE(end_offset, it->start_offset_);
items.push_back(*it);
ShiftItem(&items.back(), diff);
++it;
}
VerifyItems(items);
data_->items = std::move(items);
data_->text_content = new_data.text_content;
}
private:
static unsigned AdjustOffset(unsigned offset, int delta) {
if (delta > 0)
return offset + delta;
return offset - (-delta);
}
static unsigned ConvertDOMOffsetToTextContent(
base::span<const NGOffsetMappingUnit> units,
unsigned offset) {
auto it = std::find_if(
units.begin(), units.end(), [offset](const NGOffsetMappingUnit& unit) {
return unit.DOMStart() <= offset && offset <= unit.DOMEnd();
});
DCHECK(it != units.end());
return it->ConvertDOMOffsetToTextContent(offset);
}
// Returns copy of |item| after |start_offset| (inclusive).
NGInlineItem CopyItemAfter(const NGInlineItem& item,
unsigned start_offset) const {
DCHECK_LE(item.start_offset_, start_offset);
DCHECK_LT(start_offset, item.end_offset_);
DCHECK_EQ(item.layout_object_, layout_text_);
if (item.start_offset_ == start_offset)
return item;
const unsigned end_offset = item.end_offset_;
if (!item.shape_result_)
return NGInlineItem(item, start_offset, end_offset, nullptr);
// TODO(yosin): We should handle |shape_result| doesn't have safe-to-break
// at start and end, because of |ShapeText()| splits |ShapeResult| ignoring
// safe-to-break offset.
item.shape_result_->EnsurePositionData();
const unsigned safe_start_offset =
item.shape_result_->CachedNextSafeToBreakOffset(start_offset);
if (end_offset == safe_start_offset)
return NGInlineItem(item, start_offset, end_offset, nullptr);
return NGInlineItem(
item, start_offset, end_offset,
item.shape_result_->SubRange(safe_start_offset, end_offset));
}
// Returns copy of |item| before |end_offset| (exclusive).
NGInlineItem CopyItemBefore(const NGInlineItem& item,
unsigned end_offset) const {
DCHECK_LT(item.start_offset_, end_offset);
DCHECK_LE(end_offset, item.end_offset_);
DCHECK_EQ(item.layout_object_, layout_text_);
if (item.end_offset_ == end_offset)
return item;
const unsigned start_offset = item.start_offset_;
if (!item.shape_result_)
return NGInlineItem(item, start_offset, end_offset, nullptr);
// TODO(yosin): We should handle |shape_result| doesn't have safe-to-break
// at start and end, because of |ShapeText()| splits |ShapeResult| ignoring
// safe-to-break offset.
item.shape_result_->EnsurePositionData();
const unsigned safe_end_offset =
item.shape_result_->CachedPreviousSafeToBreakOffset(end_offset);
if (start_offset == safe_end_offset)
return NGInlineItem(item, start_offset, end_offset, nullptr);
return NGInlineItem(
item, start_offset, end_offset,
item.shape_result_->SubRange(start_offset, safe_end_offset));
}
static void ShiftItem(NGInlineItem* item, int delta) {
if (delta == 0)
return;
item->start_offset_ = AdjustOffset(item->start_offset_, delta);
item->end_offset_ = AdjustOffset(item->end_offset_, delta);
if (!item->shape_result_)
return;
item->shape_result_ =
item->shape_result_->CopyAdjustedOffset(item->start_offset_);
}
void VerifyItems(const Vector<NGInlineItem>& items) const {
#if DCHECK_IS_ON()
unsigned last_offset = items.front().start_offset_;
for (const NGInlineItem& item : items) {
DCHECK_LE(item.start_offset_, item.end_offset_);
DCHECK_EQ(last_offset, item.start_offset_);
last_offset = item.end_offset_;
if (!item.shape_result_ || item.layout_object_ != layout_text_)
continue;
DCHECK_LT(item.start_offset_, item.end_offset_);
if (item.shape_result_->StartIndex() == item.start_offset_) {
DCHECK_LE(item.shape_result_->EndIndex(), item.end_offset_);
} else {
DCHECK_LE(item.start_offset_, item.shape_result_->StartIndex());
DCHECK_EQ(item.end_offset_, item.shape_result_->EndIndex());
}
}
DCHECK_EQ(last_offset,
block_flow_->GetNGInlineNodeData()->text_content.length());
#endif
}
std::unique_ptr<NGInlineNodeData> data_;
LayoutBlockFlow* const block_flow_;
const LayoutText& layout_text_;
unsigned start_offset_ = 0;
unsigned end_offset_ = 0;
DISALLOW_COPY_AND_ASSIGN(NGInlineNodeDataEditor);
};
// static
bool NGInlineNode::SetTextWithOffset(LayoutText* layout_text,
scoped_refptr<StringImpl> new_text_in,
unsigned offset,
unsigned length) {
if (!layout_text->HasValidInlineItems() ||
!layout_text->IsInLayoutNGInlineFormattingContext())
return false;
const String old_text = layout_text->GetText();
if (offset == 0 && length == old_text.length()) {
// We'll run collect inline items since whole text of |layout_text| is
// changed.
return false;
}
NGInlineNodeDataEditor editor(*layout_text);
NGInlineNodeData* const previous_data = editor.Prepare(offset, length);
if (!previous_data)
return false;
String new_text(std::move(new_text_in));
layout_text->StyleRef().ApplyTextTransform(&new_text,
layout_text->PreviousCharacter());
layout_text->SetTextInternal(new_text.Impl());
NGInlineNode node(editor.GetLayoutBlockFlow());
NGInlineNodeData* data = node.MutableData();
data->items.ReserveCapacity(previous_data->items.size());
NGInlineItemsBuilder builder(&data->items, nullptr);
// TODO(yosin): We should reuse before/after |layout_text| during collecting
// inline items.
layout_text->ClearInlineItems();
CollectInlinesInternal(node.GetLayoutBlockFlow(), &builder, previous_data);
data->text_content = builder.ToString();
// Relocates |ShapeResult| in |previous_data| after |offset|+|length|
editor.Run();
node.SegmentText(data);
node.ShapeText(data, &previous_data->text_content, &previous_data->items);
node.ShapeTextForFirstLineIfNeeded(data);
node.AssociateItemsWithInlines(data);
return true;
}
const NGInlineNodeData& NGInlineNode::EnsureData() {
PrepareLayoutIfNeeded();
return Data();
@ -625,14 +1015,13 @@ void NGInlineNode::SegmentBidiRuns(NGInlineNodeData* data) {
}
void NGInlineNode::ShapeText(NGInlineItemsData* data,
NGInlineItemsData* previous_data) {
const String* previous_text,
const Vector<NGInlineItem>* previous_items) {
const String& text_content = data->text_content;
Vector<NGInlineItem>* items = &data->items;
const String* previous_text =
previous_data ? &previous_data->text_content : nullptr;
// Provide full context of the entire node to the shaper.
HarfBuzzShaper shaper(text_content);
ReusingTextShaper shaper(data, previous_items);
ShapeResultSpacing<String> spacing(text_content);
DCHECK(!data->segments ||
@ -717,43 +1106,35 @@ void NGInlineNode::ShapeText(NGInlineItemsData* data,
}
// Results may only be reused if all items in the range remain valid.
bool has_valid_shape_results = true;
for (unsigned item_index = index; item_index < end_index; item_index++) {
if (NeedsShaping((*items)[item_index])) {
has_valid_shape_results = false;
break;
if (previous_text) {
bool has_valid_shape_results = true;
for (unsigned item_index = index; item_index < end_index; item_index++) {
if (NeedsShaping((*items)[item_index])) {
has_valid_shape_results = false;
break;
}
}
// When shaping across multiple items checking whether the individual
// items has valid shape results isn't sufficient as items may have been
// re-ordered or removed.
// TODO(layout-dev): It would probably be faster to check for removed or
// moved items but for now comparing the string itself will do.
unsigned text_start = start_item.StartOffset();
DCHECK_GE(end_offset, text_start);
unsigned text_length = end_offset - text_start;
if (has_valid_shape_results && previous_text &&
end_offset <= previous_text->length() &&
StringView(text_content, text_start, text_length) ==
StringView(*previous_text, text_start, text_length)) {
index = end_index;
continue;
}
}
// When shaping across multiple items checking whether the individual
// items has valid shape results isn't sufficient as items may have been
// re-ordered or removed.
// TODO(layout-dev): It would probably be faster to check for removed or
// moved items but for now comparing the string itself will do.
unsigned text_start = start_item.StartOffset();
DCHECK_GE(end_offset, text_start);
unsigned text_length = end_offset - text_start;
if (has_valid_shape_results && previous_text &&
end_offset <= previous_text->length() &&
StringView(text_content, text_start, text_length) ==
StringView(*previous_text, text_start, text_length)) {
index = end_index;
continue;
}
// Shape each item with the full context of the entire node.
scoped_refptr<ShapeResult> shape_result;
if (!data->segments) {
RunSegmenter::RunSegmenterRange range =
start_item.CreateRunSegmenterRange();
range.end = end_offset;
shape_result = shaper.Shape(&font, direction, start_item.StartOffset(),
end_offset, range);
} else {
shape_result = data->segments->ShapeText(
&shaper, &font, direction, start_item.StartOffset(), end_offset,
&start_item - items->begin());
}
scoped_refptr<ShapeResult> shape_result =
shaper.Shape(start_item, end_offset);
if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription())))
shape_result->ApplySpacing(spacing);

@ -78,6 +78,15 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode {
static void ClearAssociatedFragments(const NGPhysicalFragment& fragment,
const NGBlockBreakToken* break_token);
// Returns true if we don't need to collect inline items after replacing
// |layout_text| after deleting replacing subtext from |offset| to |length|
// |new_text| is new text of |layout_text|.
// This is optimized version of |PrepareLayout()|.
static bool SetTextWithOffset(LayoutText* layout_text,
scoped_refptr<StringImpl> new_text,
unsigned offset,
unsigned length);
// Returns the DOM to text content offset mapping of this block. If it is not
// computed before, compute and store it in NGInlineNodeData.
// This funciton must be called with clean layout.
@ -124,7 +133,8 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode {
void SegmentFontOrientation(NGInlineNodeData*);
void SegmentBidiRuns(NGInlineNodeData*);
void ShapeText(NGInlineItemsData*,
NGInlineItemsData* previous_data = nullptr);
const String* previous_text = nullptr,
const Vector<NGInlineItem>* previous_items = nullptr);
void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*);
void AssociateItemsWithInlines(NGInlineNodeData*);

@ -945,15 +945,6 @@ TEST_F(NGInlineNodeTest, InvalidateSetText) {
EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines());
}
TEST_F(NGInlineNodeTest, InvalidateSetTextWithOffset) {
SetupHtml("t", "<div id=t>before</div>");
EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines());
LayoutText* text = ToLayoutText(layout_block_flow_->FirstChild());
text->SetTextWithOffset(String("after").Impl(), 1, 4);
EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines());
}
TEST_F(NGInlineNodeTest, InvalidateAddAbsolute) {
SetupHtml("t",
"<style>span { position: absolute; }</style>"