From 8cdfbfed4996b469bbaaaa62885d72948beb2dce Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 10 Jan 2026 12:26:20 +0100 Subject: [PATCH] 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. --- Libraries/LibUnicode/Segmenter.cpp | 91 ++++++++++++++++++++++++++++ Libraries/LibUnicode/Segmenter.h | 1 + Libraries/LibWeb/Layout/TextNode.cpp | 11 +++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/Libraries/LibUnicode/Segmenter.cpp b/Libraries/LibUnicode/Segmenter.cpp index 254103fb12..a834492c03 100644 --- a/Libraries/LibUnicode/Segmenter.cpp +++ b/Libraries/LibUnicode/Segmenter.cpp @@ -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 clone() const override + { + return make(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 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 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 segmenter, SegmenterGranularity segmenter_granularity) @@ -247,6 +333,11 @@ NonnullOwnPtr Segmenter::create(StringView locale, SegmenterGranulari return make(segmenter.release_nonnull(), segmenter_granularity); } +NonnullOwnPtr Segmenter::create_for_ascii_grapheme(size_t length) +{ + return make(length); +} + bool Segmenter::should_continue_beyond_word(Utf16View const& word) { for (auto code_point : word) { diff --git a/Libraries/LibUnicode/Segmenter.h b/Libraries/LibUnicode/Segmenter.h index 6acf13bd48..a80c0d2e72 100644 --- a/Libraries/LibUnicode/Segmenter.h +++ b/Libraries/LibUnicode/Segmenter.h @@ -25,6 +25,7 @@ class Segmenter { public: static NonnullOwnPtr create(SegmenterGranularity segmenter_granularity); static NonnullOwnPtr create(StringView locale, SegmenterGranularity segmenter_granularity); + static NonnullOwnPtr create_for_ascii_grapheme(size_t length); virtual ~Segmenter() = default; static bool should_continue_beyond_word(Utf16View const&); diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index cdbd6ee20e..92559c27c4 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -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;