Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fbe63bdd authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Introduce PremeasuredText

By measuring the character widths beforehand, we can save at least 40%
of the StaticLayout construction time which typically happens on UI
thread.
Also verified this doesn't cause performance regression for not
premeasured text.

Raw performance score (Not premeasured -> premeasured, median, N=100)

No Style,   Greedy, Hyphenation OFF:  7,812,975 ->    503,245 (-93.6%)
No Style, Balanced, Hyphenation OFF:  7,843,254 ->    396,892 (-95.0%)

No Style,   Greedy, Hyphenation ON : 19,134,214 -> 11,658,928 (-39.1%)
No Style, Balanced, Hyphenation ON : 19,348,062 -> 11,634,942 (-39.9%)

Styled,     Greedy, Hyphenation OFF: 14,353,673 ->    572,840 (-96.0%)

Raw performance score (w/o patch -> w/ patch, median, N=100):

No Style,   Greedy, Hyphenation OFF:  7,732,894 ->  7,812,975 (+1.04%)
No Style, Balanced, Hyphenation OFF:  7,884,510 ->  7,843,254 (-0.52%)

No Style,   Greedy, Hyphenation ON : 18,986,958 -> 19,134,214 (+0.78%)
No Style, Balanced, Hyphenation ON : 19,232,791 -> 19,348,062 (+0.60%)

Styled,     Greedy, Hyphenation OFF: 14,319,690 -> 14,353,673 (+0.24%)

Bug: 67504091
Test: bit CtsTextTestCases:*
Test: bit CtsGraphicsTestCases:*
Test: bit CtsWidgetTestCases:*
Test: FrameworksCoreTests:android.text.MeasuredTextTest
Change-Id: I0b46f04b42cc012606a9c722eca0d51147a0dcc7
parent 0d137172
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.text;

import static android.text.TextDirectionHeuristics.LTR;

import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;

@@ -182,4 +184,84 @@ public class StaticLayoutPerfTest {
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Greedy_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Greedy_Hyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Balanced_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Balanced_Hyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_Styled_Greedy_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }
}
+21 −0
Original line number Diff line number Diff line
@@ -42276,6 +42276,27 @@ package android.text {
    method public abstract int getSpanTypeId();
  }
  public class PremeasuredText implements android.text.Spanned {
    method public static android.text.PremeasuredText build(java.lang.CharSequence, android.text.TextPaint, android.text.TextDirectionHeuristic);
    method public static android.text.PremeasuredText build(java.lang.CharSequence, android.text.TextPaint, android.text.TextDirectionHeuristic, int, int);
    method public char charAt(int);
    method public int getEnd();
    method public android.text.TextPaint getPaint();
    method public int getParagraphCount();
    method public int getParagraphEnd(int);
    method public int getParagraphStart(int);
    method public int getSpanEnd(java.lang.Object);
    method public int getSpanFlags(java.lang.Object);
    method public int getSpanStart(java.lang.Object);
    method public <T> T[] getSpans(int, int, java.lang.Class<T>);
    method public int getStart();
    method public java.lang.CharSequence getText();
    method public android.text.TextDirectionHeuristic getTextDir();
    method public int length();
    method public int nextSpanTransition(int, int, java.lang.Class);
    method public java.lang.CharSequence subSequence(int, int);
  }
  public class Selection {
    method public static boolean extendDown(android.text.Spannable, android.text.Layout);
    method public static boolean extendLeft(android.text.Spannable, android.text.Layout);
+272 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.util.IntArray;

import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;

/**
 * A text which has already been measured.
 *
 * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc.
 */
public class PremeasuredText implements Spanned {
    private static final char LINE_FEED = '\n';

    // The original text.
    private final @NonNull CharSequence mText;

    // The inclusive start offset of the measuring target.
    private final @IntRange(from = 0) int mStart;

    // The exclusive end offset of the measuring target.
    private final @IntRange(from = 0) int mEnd;

    // The TextPaint used for measurement.
    private final @NonNull TextPaint mPaint;

    // The requested text direction.
    private final @NonNull TextDirectionHeuristic mTextDir;

    // The measured paragraph texts.
    private final @NonNull MeasuredText[] mMeasuredTexts;

    // The sorted paragraph end offsets.
    private final @NonNull int[] mParagraphBreakPoints;

    /**
     * Build PremeasuredText from the text.
     *
     * @param text The text to be measured.
     * @param paint The paint to be used for drawing.
     * @param textDir The text direction.
     * @return The measured text.
     */
    public static @NonNull PremeasuredText build(@NonNull CharSequence text,
                                                 @NonNull TextPaint paint,
                                                 @NonNull TextDirectionHeuristic textDir) {
        return PremeasuredText.build(text, paint, textDir, 0, text.length());
    }

    /**
     * Build PremeasuredText from the specific range of the text..
     *
     * @param text The text to be measured.
     * @param paint The paint to be used for drawing.
     * @param textDir The text direction.
     * @param start The inclusive start offset of the text.
     * @param end The exclusive start offset of the text.
     * @return The measured text.
     */
    public static @NonNull PremeasuredText build(@NonNull CharSequence text,
                                                 @NonNull TextPaint paint,
                                                 @NonNull TextDirectionHeuristic textDir,
                                                 @IntRange(from = 0) int start,
                                                 @IntRange(from = 0) int end) {
        Preconditions.checkNotNull(text);
        Preconditions.checkNotNull(paint);
        Preconditions.checkNotNull(textDir);
        Preconditions.checkArgumentInRange(start, 0, text.length(), "start");
        Preconditions.checkArgumentInRange(end, 0, text.length(), "end");

        final IntArray paragraphEnds = new IntArray();
        final ArrayList<MeasuredText> measuredTexts = new ArrayList<>();

        int paraEnd = 0;
        for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
            paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
            if (paraEnd < 0) {
                // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph end.
                paraEnd = end;
            } else {
                paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
            }

            paragraphEnds.add(paraEnd);
            measuredTexts.add(MeasuredText.buildForStaticLayout(
                    paint, text, paraStart, paraEnd, textDir, null /* no recycle */));
        }

        return new PremeasuredText(text, start, end, paint, textDir,
                                   measuredTexts.toArray(new MeasuredText[measuredTexts.size()]),
                                   paragraphEnds.toArray());
    }

    // Use PremeasuredText.build instead.
    private PremeasuredText(@NonNull CharSequence text,
                            @IntRange(from = 0) int start,
                            @IntRange(from = 0) int end,
                            @NonNull TextPaint paint,
                            @NonNull TextDirectionHeuristic textDir,
                            @NonNull MeasuredText[] measuredTexts,
                            @NonNull int[] paragraphBreakPoints) {
        mText = text;
        mStart = start;
        mEnd = end;
        mPaint = paint;
        mMeasuredTexts = measuredTexts;
        mParagraphBreakPoints = paragraphBreakPoints;
        mTextDir = textDir;
    }

    /**
     * Return the underlying text.
     */
    public @NonNull CharSequence getText() {
        return mText;
    }

    /**
     * Returns the inclusive start offset of measured region.
     */
    public @IntRange(from = 0) int getStart() {
        return mStart;
    }

    /**
     * Returns the exclusive end offset of measured region.
     */
    public @IntRange(from = 0) int getEnd() {
        return mEnd;
    }

    /**
     * Returns the text direction associated with char sequence.
     */
    public @NonNull TextDirectionHeuristic getTextDir() {
        return mTextDir;
    }

    /**
     * Returns the paint used to measure this text.
     */
    public @NonNull TextPaint getPaint() {
        return mPaint;
    }

    /**
     * Returns the length of the paragraph of this text.
     */
    public @IntRange(from = 0) int getParagraphCount() {
        return mParagraphBreakPoints.length;
    }

    /**
     * Returns the paragraph start offset of the text.
     */
    public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
        return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1];
    }

    /**
     * Returns the paragraph end offset of the text.
     */
    public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
        return mParagraphBreakPoints[paraIndex];
    }

    /** @hide */
    public @NonNull MeasuredText getMeasuredText(@IntRange(from = 0) int paraIndex) {
        return mMeasuredTexts[paraIndex];
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Spanned overrides
    //
    // Just proxy for underlying mText if appropriate.

    @Override
    public <T> T[] getSpans(int start, int end, Class<T> type) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpans(start, end, type);
        } else {
            return ArrayUtils.emptyArray(type);
        }
    }

    @Override
    public int getSpanStart(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanStart(tag);
        } else {
            return -1;
        }
    }

    @Override
    public int getSpanEnd(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanEnd(tag);
        } else {
            return -1;
        }
    }

    @Override
    public int getSpanFlags(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanFlags(tag);
        } else {
            return 0;
        }
    }

    @Override
    public int nextSpanTransition(int start, int limit, Class type) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).nextSpanTransition(start, limit, type);
        } else {
            return mText.length();
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // CharSequence overrides.
    //
    // Just proxy for underlying mText.

    @Override
    public int length() {
        return mText.length();
    }

    @Override
    public char charAt(int index) {
        // TODO: Should this be index + mStart ?
        return mText.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        // TODO: return PremeasuredText.
        // TODO: Should this be index + mStart, end + mStart ?
        return mText.subSequence(start, end);
    }

    @Override
    public String toString() {
        return mText.toString();
    }
}
+32 −21
Original line number Diff line number Diff line
@@ -610,8 +610,8 @@ public class StaticLayout extends Layout {

    /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
        final CharSequence source = b.mText;
        int bufStart = b.mStart;
        int bufEnd = b.mEnd;
        final int bufStart = b.mStart;
        final int bufEnd = b.mEnd;
        TextPaint paint = b.mPaint;
        int outerWidth = b.mWidth;
        TextDirectionHeuristic textDir = b.mTextDir;
@@ -634,10 +634,6 @@ public class StaticLayout extends Layout {
        Paint.FontMetricsInt fm = b.mFontMetricsInt;
        int[] chooseHtv = null;

        Spanned spanned = null;
        if (source instanceof Spanned)
            spanned = (Spanned) source;

        final int[] indents;
        if (mLeftIndents != null || mRightIndents != null) {
            final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
@@ -660,17 +656,35 @@ public class StaticLayout extends Layout {
                b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
                indents, mLeftPaddings, mRightPaddings);

        MeasuredText measured = null;
        try {
            int paraEnd;
            for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
                paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
                if (paraEnd < 0) {
                    paraEnd = bufEnd;
        PremeasuredText premeasured = null;
        final Spanned spanned;
        if (source instanceof PremeasuredText) {
            premeasured = (PremeasuredText) source;

            final CharSequence original = premeasured.getText();
            spanned = (original instanceof Spanned) ? (Spanned) original : null;

            if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) {
                // The buffer position has changed. Re-measure here.
                premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd);
            } else {
                // We can use premeasured information.

                // Overwrite with the one when premeasured.
                // TODO: Give an option for developer not to overwrite and measure again here?
                textDir = premeasured.getTextDir();
                paint = premeasured.getPaint();
            }
        } else {
                    paraEnd++;
            premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
            spanned = (source instanceof Spanned) ? (Spanned) source : null;
        }

        try {
            for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) {
                final int paraStart = premeasured.getParagraphStart(paraIndex);
                final int paraEnd = premeasured.getParagraphEnd(paraIndex);

                int firstWidthLineCount = 1;
                int firstWidth = outerWidth;
                int restWidth = outerWidth;
@@ -735,8 +749,7 @@ public class StaticLayout extends Layout {
                    }
                }

                measured = MeasuredText.buildForStaticLayout(
                        paint, source, paraStart, paraEnd, textDir, measured);
                final MeasuredText measured = premeasured.getMeasuredText(paraIndex);
                final char[] chs = measured.getChars();
                final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
                final int[] fmCache = measured.getFontMetrics().getRawArray();
@@ -887,7 +900,8 @@ public class StaticLayout extends Layout {

            if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                    && mLineCount < mMaximumVisibleLineCount) {
                measured = MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, measured);
                final MeasuredText measured =
                        MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null);
                paint.getFontMetricsInt(fm);
                v = out(source,
                        bufEnd, bufEnd, fm.ascent, fm.descent,
@@ -901,9 +915,6 @@ public class StaticLayout extends Layout {
                        ellipsizedWidth, 0, paint, false);
            }
        } finally {
            if (measured != null) {
                measured.recycle();
            }
            nFinish(nativePtr);
        }
    }
+8 −2
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
import android.text.ParcelableSpan;
import android.text.PremeasuredText;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -5326,7 +5327,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            if (imm != null) imm.restartInput(this);
        } else if (type == BufferType.SPANNABLE || mMovement != null) {
            text = mSpannableFactory.newSpannable(text);
        } else if (!(text instanceof CharWrapper)) {
        } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) {
            text = TextUtils.stringOrSpannedString(text);
        }

@@ -5610,10 +5611,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                spannable = (Spannable) text;
            } else {
                spannable = mSpannableFactory.newSpannable(text);
                text = spannable;
            }

            SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
            if (spans.length == 0) {
                return text;
            } else {
                text = spannable;
            }

            for (int i = 0; i < spans.length; i++) {
                spannable.removeSpan(spans[i]);
            }