LibUnicode+LibWeb: Add fast path grapheme segmenter for ASCII text

For ASCII text, every character is its own grapheme - there are no
combining characters or emoji sequences. This means grapheme boundary
detection is trivial: next_boundary(i) is simply i+1.

This commit adds AsciiGraphemeSegmenter, a simple Segmenter subclass
that performs O(1) boundary lookups without any ICU overhead.

TextNode::grapheme_segmenter() now checks if the text is ASCII and uses
this fast path, avoiding expensive ICU BreakIterator cloning and
boundary detection for the common case of ASCII-only text.
This commit is contained in:
Andreas Kling 2026-01-10 12:26:20 +01:00 committed by Andreas Kling
parent 10737e22d1
commit 8cdfbfed49
3 changed files with 101 additions and 2 deletions

View file

@ -41,6 +41,92 @@ StringView segmenter_granularity_to_string(SegmenterGranularity segmenter_granul
VERIFY_NOT_REACHED();
}
// Fast path segmenter for ASCII text where every character is its own grapheme.
// This avoids all ICU overhead for the common case of ASCII-only text.
class AsciiGraphemeSegmenter : public Segmenter {
public:
explicit AsciiGraphemeSegmenter(size_t length)
: Segmenter(SegmenterGranularity::Grapheme)
, m_length(length)
{
}
virtual ~AsciiGraphemeSegmenter() override = default;
virtual NonnullOwnPtr<Segmenter> clone() const override
{
return make<AsciiGraphemeSegmenter>(m_length);
}
virtual void set_segmented_text(String text) override
{
m_length = text.byte_count();
}
virtual void set_segmented_text(Utf16View const& text) override
{
m_length = text.length_in_code_units();
}
virtual size_t current_boundary() override
{
return m_current;
}
virtual Optional<size_t> previous_boundary(size_t index, Inclusive inclusive) override
{
if (inclusive == Inclusive::Yes && index <= m_length)
return index;
if (index == 0)
return {};
return index - 1;
}
virtual Optional<size_t> next_boundary(size_t index, Inclusive inclusive) override
{
if (inclusive == Inclusive::Yes && index <= m_length)
return index;
if (index >= m_length)
return {};
return index + 1;
}
virtual void for_each_boundary(String text, SegmentationCallback callback) override
{
set_segmented_text(move(text));
for_each_boundary_impl(callback);
}
virtual void for_each_boundary(Utf16View const& text, SegmentationCallback callback) override
{
set_segmented_text(text);
for_each_boundary_impl(callback);
}
virtual void for_each_boundary(Utf32View const& text, SegmentationCallback callback) override
{
m_length = text.length();
for_each_boundary_impl(callback);
}
virtual bool is_current_boundary_word_like() const override
{
return false;
}
private:
void for_each_boundary_impl(SegmentationCallback& callback)
{
for (size_t i = 0; i <= m_length; ++i) {
if (callback(i) == IterationDecision::Break)
return;
}
}
size_t m_length { 0 };
size_t m_current { 0 };
};
class SegmenterImpl : public Segmenter {
public:
SegmenterImpl(NonnullOwnPtr<icu::BreakIterator> segmenter, SegmenterGranularity segmenter_granularity)
@ -247,6 +333,11 @@ NonnullOwnPtr<Segmenter> Segmenter::create(StringView locale, SegmenterGranulari
return make<SegmenterImpl>(segmenter.release_nonnull(), segmenter_granularity);
}
NonnullOwnPtr<Segmenter> Segmenter::create_for_ascii_grapheme(size_t length)
{
return make<AsciiGraphemeSegmenter>(length);
}
bool Segmenter::should_continue_beyond_word(Utf16View const& word)
{
for (auto code_point : word) {

View file

@ -25,6 +25,7 @@ class Segmenter {
public:
static NonnullOwnPtr<Segmenter> create(SegmenterGranularity segmenter_granularity);
static NonnullOwnPtr<Segmenter> create(StringView locale, SegmenterGranularity segmenter_granularity);
static NonnullOwnPtr<Segmenter> create_for_ascii_grapheme(size_t length);
virtual ~Segmenter() = default;
static bool should_continue_beyond_word(Utf16View const&);

View file

@ -396,8 +396,15 @@ void TextNode::compute_text_for_rendering()
Unicode::Segmenter& TextNode::grapheme_segmenter() const
{
if (!m_grapheme_segmenter) {
m_grapheme_segmenter = document().grapheme_segmenter().clone();
m_grapheme_segmenter->set_segmented_text(text_for_rendering());
auto const& text = text_for_rendering();
// Fast path: For ASCII text, every character is its own grapheme.
// We can use a trivial segmenter that avoids all ICU overhead.
if (text.is_ascii()) {
m_grapheme_segmenter = Unicode::Segmenter::create_for_ascii_grapheme(text.length_in_code_units());
} else {
m_grapheme_segmenter = document().grapheme_segmenter().clone();
m_grapheme_segmenter->set_segmented_text(text);
}
}
return *m_grapheme_segmenter;