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
This commit is contained in:
Seigo Nonaka
2017-12-02 19:28:05 -08:00
parent 0d13717248
commit fbe63bddd6
5 changed files with 415 additions and 23 deletions

View File

@@ -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();
}
}
}

View File

@@ -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);

View File

@@ -0,0 +1,272 @@
/*
* 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();
}
}

View File

@@ -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,16 +656,34 @@ public class StaticLayout extends Layout {
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
MeasuredText measured = null;
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 {
premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
spanned = (source instanceof Spanned) ? (Spanned) source : 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;
} else {
paraEnd++;
}
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;
@@ -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);
}
}

View File

@@ -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]);
}