Optimize QTextLayout/QTextEngine usage outside of QTextDocument.
authorMilian Wolff <milian.wolff@kdab.com>
Mon, 3 Sep 2012 16:06:17 +0000 (18:06 +0200)
committerQt by Nokia <qt-info@nokia.com>
Wed, 5 Sep 2012 01:02:59 +0000 (03:02 +0200)
When QTextLayout is used in a QTextDocument, many code paths use
special caches and thus greatly outperform the raw QTextLayout version
that operates directly on a QString.

This patch brings some of these optimizations also to the raw version.
We now also use a QFormatCollection in such cases and enable the
functionality of QTextEngine::indexAdditionalFormats() and
QTextEngine::resolveAdditionalFormats(). Thanks to that, we can greatly
speed up QTextEngine::format(), which now uses an amort O(1) hash table
lookup instead of a O(N) linear search.

The added benchmark shows a gain in the order of one magnitude:

./tst_bench_QText formattedLayout:long-many

before applying the patch:
378.19 msecs per iteration (total: 37,820, iterations: 100)
after applying the patch:
25.80 msecs per iteration (total: 2,580, iterations: 100)

Note: This change is source-incompatible for applications using the private
QTextEngine API.

Task-number: QTBUG-8389
Change-Id: Ifcf7a8902a394428979ea06a6d955f886ee739c7
Reviewed-by: Konstantin Ritt <ritt.ks@gmail.com>
Reviewed-by: Olivier Goffart <ogoffart@woboq.com>
Reviewed-by: Lars Knoll <lars.knoll@nokia.com>
src/gui/text/qtextengine.cpp
src/gui/text/qtextengine_p.h
tests/benchmarks/gui/text/qtext/main.cpp

index 6abdbda..448907b 100644 (file)
@@ -2198,11 +2198,11 @@ int QTextEngine::formatIndex(const QScriptItem *si) const
 QTextCharFormat QTextEngine::format(const QScriptItem *si) const
 {
     QTextCharFormat format;
-    const QTextFormatCollection *formats = 0;
-    if (block.docHandle()) {
-        formats = this->formats();
+    const QTextFormatCollection *formats = this->formats();
+
+    if (formats)
         format = formats->charFormat(formatIndex(si));
-    }
+
     if (specialData && specialData->resolvedFormatIndices.isEmpty()) {
         int end = si->position + length(si);
         for (int i = 0; i < specialData->addFormats.size(); ++i) {
@@ -2289,11 +2289,15 @@ bool QTextEngine::atSpace(int position) const
 
 void QTextEngine::indexAdditionalFormats()
 {
-    if (!block.docHandle())
-        return;
-
     specialData->addFormatIndices.resize(specialData->addFormats.count());
-    QTextFormatCollection * const formats = this->formats();
+
+    QTextFormatCollection *formats = this->formats();
+
+    if (!formats) {
+        Q_ASSERT(!block.docHandle());
+        specialData->formats.reset(new QTextFormatCollection);
+        formats = specialData->formats.data();
+    }
 
     for (int i = 0; i < specialData->addFormats.count(); ++i) {
         specialData->addFormatIndices[i] = formats->indexForFormat(specialData->addFormats.at(i).format);
@@ -2708,11 +2712,10 @@ public:
 void QTextEngine::resolveAdditionalFormats() const
 {
     if (!specialData || specialData->addFormats.isEmpty()
-        || !block.docHandle()
         || !specialData->resolvedFormatIndices.isEmpty())
         return;
 
-    QTextFormatCollection *collection = this->formats();
+    QTextFormatCollection *collection = formats();
 
     specialData->resolvedFormatIndices.clear();
     QVector<int> indices(layoutData->items.count());
@@ -2748,16 +2751,17 @@ void QTextEngine::resolveAdditionalFormats() const
             ++endIt;
         }
         QTextCharFormat format;
-        const QTextFormatCollection *formats = 0;
         if (block.docHandle()) {
-            formats = this->formats();
-            format = formats->charFormat(formatIndex(si));
+            // when we have a docHandle, formatIndex might still return a valid index based
+            // on the preeditPosition. for all other cases, we cleared the resolved format indices
+            format = collection->charFormat(formatIndex(si));
         }
+
         foreach (int cur, currentFormats) {
             const QTextLayout::FormatRange &r = specialData->addFormats.at(cur);
             Q_ASSERT (r.start <= si->position && r.start + r.length >= end);
             if (!specialData->addFormatIndices.isEmpty()) {
-                format.merge(formats->format(specialData->addFormatIndices.at(cur)));
+                format.merge(collection->format(specialData->addFormatIndices.at(cur)));
             } else {
                 format.merge(r.format);
             }
index d8ab222..c2362e6 100644 (file)
@@ -541,7 +541,12 @@ public:
 #ifdef QT_BUILD_COMPAT_LIB
         return 0; // Compat should never reference this symbol
 #else
-        return block.docHandle()->formatCollection();
+        if (block.docHandle())
+            return block.docHandle()->formatCollection();
+        else if (specialData)
+            return specialData->formats.data();
+
+        return 0;
 #endif
     }
     QTextCharFormat format(const QScriptItem *si) const;
@@ -619,6 +624,8 @@ public:
         QList<QTextLayout::FormatRange> addFormats;
         QVector<int> addFormatIndices;
         QVector<int> resolvedFormatIndices;
+        // only used when no docHandle is available
+        QScopedPointer<QTextFormatCollection> formats;
     };
     SpecialData *specialData;
 
index 5a4745f..8188a01 100644 (file)
@@ -51,6 +51,7 @@
 #include <qtest.h>
 
 Q_DECLARE_METATYPE(QTextDocument*)
+Q_DECLARE_METATYPE(QList<QTextLayout::FormatRange>)
 
 class tst_QText: public QObject
 {
@@ -80,6 +81,7 @@ private slots:
 
     void layout_data();
     void layout();
+    void formattedLayout_data();
     void formattedLayout();
     void paintLayoutToPixmap();
     void paintLayoutToPixmap_painterFill();
@@ -328,28 +330,53 @@ void tst_QText::layout()
     }
 }*/
 
-void tst_QText::formattedLayout()
+void tst_QText::formattedLayout_data()
 {
-    //set up formatting
-    QList<QTextLayout::FormatRange> ranges;
+    QTest::addColumn<QString>("text");
+    QTest::addColumn<QList<QTextLayout::FormatRange> >("ranges");
+
+    QTextCharFormat format;
+    format.setForeground(QColor("steelblue"));
+
     {
-        QTextCharFormat format;
-        format.setForeground(QColor("steelblue"));
+        QList<QTextLayout::FormatRange> ranges;
 
         QTextLayout::FormatRange formatRange;
         formatRange.format = format;
         formatRange.start = 0;
         formatRange.length = 50;
-
         ranges.append(formatRange);
+
+        QTest::newRow("short-single") << m_shortLorem << ranges;
+    }
+    {
+        QList<QTextLayout::FormatRange> ranges;
+
+        QString text = m_lorem.repeated(100);
+        const int width = 1;
+        for (int i = 0; i < text.size(); i += width) {
+            QTextLayout::FormatRange formatRange;
+            formatRange.format.setForeground(QBrush(QColor(i % 255, 255, 255)));
+            formatRange.start = i;
+            formatRange.length = width;
+            ranges.append(formatRange);
+        }
+
+        QTest::newRow("long-many") << m_shortLorem << ranges;
     }
+}
 
-    QTextLayout layout(m_shortLorem);
+void tst_QText::formattedLayout()
+{
+    QFETCH(QString, text);
+    QFETCH(QList<QTextLayout::FormatRange>, ranges);
+
+    QTextLayout layout(text);
     layout.setAdditionalFormats(ranges);
     setupTextLayout(&layout);
 
     QBENCHMARK {
-        QTextLayout layout(m_shortLorem);
+        QTextLayout layout(text);
         layout.setAdditionalFormats(ranges);
         setupTextLayout(&layout);
     }