From fa05ba0b0d39fae1d2cb3d98fbee0aef6a9fed88 Mon Sep 17 00:00:00 2001 From: Siyamed Sinir Date: Tue, 12 Jan 2016 10:54:43 -0800 Subject: [PATCH] Sort the result of SpannableStringBuilder.getSpans SpannableStringBuilder used to return the result of getSpans in the order of start indices because of the interval tree it uses. However, style spans has to be applied in the order of insertion to get predictable results. Sorted the results of getSpans ordered first by priority flag and then by insertion time. Moreover improved the performance of SpannableStringInternal copy constructor. Bug: 26240132 Change-Id: I0b0fa7bb30a3bd9ca37dedca66d8993718586027 --- core/java/android/text/Layout.java | 6 +- core/java/android/text/SpannableStringBuilder.java | 204 ++++++++++++++++++--- .../java/android/text/SpannableStringInternal.java | 99 ++++++++-- .../text/SpannableStringBuilderBenchmark.java | 138 ++++++++++++++ .../text/SpannableStringInternalCopyBenchmark.java | 61 ++++++ 5 files changed, 472 insertions(+), 36 deletions(-) create mode 100644 core/tests/benchmarks/src/android/text/SpannableStringBuilderBenchmark.java create mode 100644 core/tests/benchmarks/src/android/text/SpannableStringInternalCopyBenchmark.java diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 2c4241b7eceb..692d848abd18 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1826,7 +1826,11 @@ public abstract class Layout { return ArrayUtils.emptyArray(type); } - return text.getSpans(start, end, type); + if(text instanceof SpannableStringBuilder) { + return ((SpannableStringBuilder) text).getSpans(start, end, type, false); + } else { + return text.getSpans(start, end, type); + } } private char getEllipsisChar(TextUtils.TruncateAt method) { diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 42672384f316..e34560b80bda 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -19,6 +19,7 @@ package android.text; import android.annotation.Nullable; import android.graphics.Canvas; import android.graphics.Paint; +import android.text.style.ParagraphStyle; import android.util.Log; import com.android.internal.util.ArrayUtils; @@ -66,11 +67,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable TextUtils.getChars(text, start, end, mText, 0); mSpanCount = 0; + mSpanInsertCount = 0; mSpans = EmptyArray.OBJECT; mSpanStarts = EmptyArray.INT; mSpanEnds = EmptyArray.INT; mSpanFlags = EmptyArray.INT; mSpanMax = EmptyArray.INT; + mSpanOrder = EmptyArray.INT; + mPrioSortBuffer = EmptyArray.INT; + mOrderSortBuffer = EmptyArray.INT; if (text instanceof Spanned) { Spanned sp = (Spanned) text; @@ -234,6 +239,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // Documentation from interface public void clear() { replace(0, length(), "", 0, 0); + mSpanInsertCount = 0; } // Documentation from interface @@ -256,6 +262,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (mIndexOfSpan != null) { mIndexOfSpan.clear(); } + mSpanInsertCount = 0; } // Documentation from interface @@ -485,6 +492,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count); System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count); System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count); + System.arraycopy(mSpanOrder, i + 1, mSpanOrder, i, count); mSpanCount--; @@ -712,9 +720,6 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable end += mGapLength; } - int count = mSpanCount; - Object[] spans = mSpans; - if (mIndexOfSpan != null) { Integer index = mIndexOfSpan.get(what); if (index != null) { @@ -744,8 +749,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start); mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end); mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags); + mSpanOrder = GrowingArrayUtils.append(mSpanOrder, mSpanCount, mSpanInsertCount); invalidateIndex(mSpanCount); mSpanCount++; + mSpanInsertCount++; // Make sure there is enough room for empty interior nodes. // This magic formula computes the size of the smallest perfect binary // tree no smaller than mSpanCount. @@ -837,6 +844,25 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable */ @SuppressWarnings("unchecked") public T[] getSpans(int queryStart, int queryEnd, @Nullable Class kind) { + return getSpans(queryStart, queryEnd, kind, true); + } + + /** + * Return an array of the spans of the specified type that overlap + * the specified range of the buffer. The kind may be Object.class to get + * a list of all the spans regardless of type. + * + * @param queryStart Start index. + * @param queryEnd End index. + * @param kind Class type to search for. + * @param sort If true the results are sorted by the insertion order. + * @param + * @return Array of the spans. Empty array if no results are found. + * + * @hide + */ + public T[] getSpans(int queryStart, int queryEnd, @Nullable Class kind, + boolean sort) { if (kind == null) return (T[]) ArrayUtils.emptyArray(Object.class); if (mSpanCount == 0) return ArrayUtils.emptyArray(kind); int count = countSpans(queryStart, queryEnd, kind, treeRoot()); @@ -846,7 +872,13 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable // Safe conversion, but requires a suppressWarning T[] ret = (T[]) Array.newInstance(kind, count); - getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, 0); + if (sort) { + mPrioSortBuffer = checkSortBuffer(mPrioSortBuffer, count); + mOrderSortBuffer = checkSortBuffer(mOrderSortBuffer, count); + } + getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, mPrioSortBuffer, + mOrderSortBuffer, 0, sort); + if (sort) sort(ret, mPrioSortBuffer, mOrderSortBuffer); return ret; } @@ -876,7 +908,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (spanEnd >= queryStart && (spanStart == spanEnd || queryStart == queryEnd || (spanStart != queryEnd && spanEnd != queryStart)) && - kind.isInstance(mSpans[i])) { + (Object.class == kind || kind.isInstance(mSpans[i]))) { count++; } if ((i & 1) != 0) { @@ -887,9 +919,25 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable return count; } + /** + * Fills the result array with the spans found under the current interval tree node. + * + * @param queryStart Start index for the interval query. + * @param queryEnd End index for the interval query. + * @param kind Class type to search for. + * @param i Index of the current tree node. + * @param ret Array to be filled with results. + * @param priority Buffer to keep record of the priorities of spans found. + * @param insertionOrder Buffer to keep record of the insertion orders of spans found. + * @param count The number of found spans. + * @param sort Flag to fill the priority and insertion order buffers. If false then + * the spans with priority flag will be sorted in the result array. + * @param + * @return The total number of spans found. + */ @SuppressWarnings("unchecked") private int getSpansRec(int queryStart, int queryEnd, Class kind, - int i, T[] ret, int count) { + int i, T[] ret, int[] priority, int[] insertionOrder, int count, boolean sort) { if ((i & 1) != 0) { // internal tree node int left = leftChild(i); @@ -898,7 +946,8 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable spanMax -= mGapLength; } if (spanMax >= queryStart) { - count = getSpansRec(queryStart, queryEnd, kind, left, ret, count); + count = getSpansRec(queryStart, queryEnd, kind, left, ret, priority, + insertionOrder, count, sort); } } if (i >= mSpanCount) return count; @@ -914,36 +963,137 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable if (spanEnd >= queryStart && (spanStart == spanEnd || queryStart == queryEnd || (spanStart != queryEnd && spanEnd != queryStart)) && - kind.isInstance(mSpans[i])) { - int prio = mSpanFlags[i] & SPAN_PRIORITY; - if (prio != 0) { - int j; - - for (j = 0; j < count; j++) { + (Object.class == kind || kind.isInstance(mSpans[i]))) { + int spanPriority = mSpanFlags[i] & SPAN_PRIORITY; + if(sort) { + ret[count] = (T) mSpans[i]; + priority[count] = spanPriority; + insertionOrder[count] = mSpanOrder[i]; + } else if (spanPriority != 0) { + //insertion sort for elements with priority + int j = 0; + for (; j < count; j++) { int p = getSpanFlags(ret[j]) & SPAN_PRIORITY; - - if (prio > p) { - break; - } + if (spanPriority > p) break; } - System.arraycopy(ret, j, ret, j + 1, count - j); - // Safe conversion thanks to the isInstance test above ret[j] = (T) mSpans[i]; - } else { - // Safe conversion thanks to the isInstance test above - ret[count] = (T) mSpans[i]; } count++; } if (count < ret.length && (i & 1) != 0) { - count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, count); + count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, priority, + insertionOrder, count, sort); } } return count; } /** + * Check the size of the buffer and grow if required. + * + * @param buffer Buffer to be checked. + * @param size Required size. + * @return Same buffer instance if the current size is greater than required size. Otherwise a + * new instance is created and returned. + */ + private final int[] checkSortBuffer(int[] buffer, int size) { + if(size > buffer.length) { + return ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(size)); + } + return buffer; + } + + /** + * An iterative heap sort implementation. It will sort the spans using first their priority + * then insertion order. A span with higher priority will be before a span with lower + * priority. If priorities are the same, the spans will be sorted with insertion order. A + * span with a lower insertion order will be before a span with a higher insertion order. + * + * @param array Span array to be sorted. + * @param priority Priorities of the spans + * @param insertionOrder Insertion orders of the spans + * @param Span object type. + * @param + */ + private final void sort(T[] array, int[] priority, int[] insertionOrder) { + int size = array.length; + for (int i = size / 2 - 1; i >= 0; i--) { + siftDown(i, array, size, priority, insertionOrder); + } + + for (int i = size - 1; i > 0; i--) { + T v = array[0]; + int prio = priority[0]; + int insertOrder = insertionOrder[0]; + array[0] = array[i]; + priority[0] = priority[i]; + insertionOrder[0] = insertionOrder[i]; + siftDown(0, array, i, priority, insertionOrder); + array[i] = v; + priority[i] = prio; + insertionOrder[i] = insertOrder; + } + } + + /** + * Helper function for heap sort. + * + * @param index Index of the element to sift down. + * @param array Span array to be sorted. + * @param size Current heap size. + * @param priority Priorities of the spans + * @param insertionOrder Insertion orders of the spans + * @param Span object type. + */ + private final void siftDown(int index, T[] array, int size, int[] priority, + int[] insertionOrder) { + T v = array[index]; + int prio = priority[index]; + int insertOrder = insertionOrder[index]; + + int left = 2 * index + 1; + while (left < size) { + if (left < size - 1 && compareSpans(left, left + 1, priority, insertionOrder) < 0) { + left++; + } + if (compareSpans(index, left, priority, insertionOrder) >= 0) { + break; + } + array[index] = array[left]; + priority[index] = priority[left]; + insertionOrder[index] = insertionOrder[left]; + index = left; + left = 2 * index + 1; + } + array[index] = v; + priority[index] = prio; + insertionOrder[index] = insertOrder; + } + + /** + * Compare two span elements in an array. Comparison is based first on the priority flag of + * the span, and then the insertion order of the span. + * + * @param left Index of the element to compare. + * @param right Index of the other element to compare. + * @param priority Priorities of the spans + * @param insertionOrder Insertion orders of the spans + * @return + */ + private final int compareSpans(int left, int right, int[] priority, + int[] insertionOrder) { + int priority1 = priority[left]; + int priority2 = priority[right]; + if (priority1 == priority2) { + return Integer.compare(insertionOrder[left], insertionOrder[right]); + } + // since high priority has to be before a lower priority, the arguments to compare are + // opposite of the insertion order check. + return Integer.compare(priority2, priority1); + } + + /** * Return the next offset after start but less than or * equal to limit where a span of the specified type * begins or ends. @@ -1509,18 +1659,21 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable int start = mSpanStarts[i]; int end = mSpanEnds[i]; int flags = mSpanFlags[i]; + int insertionOrder = mSpanOrder[i]; int j = i; do { mSpans[j] = mSpans[j - 1]; mSpanStarts[j] = mSpanStarts[j - 1]; mSpanEnds[j] = mSpanEnds[j - 1]; mSpanFlags[j] = mSpanFlags[j - 1]; + mSpanOrder[j] = mSpanOrder[j - 1]; j--; } while (j > 0 && start < mSpanStarts[j - 1]); mSpans[j] = span; mSpanStarts[j] = start; mSpanEnds[j] = end; mSpanFlags[j] = flags; + mSpanOrder[j] = insertionOrder; invalidateIndex(j); } } @@ -1558,6 +1711,11 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable private int[] mSpanEnds; private int[] mSpanMax; // see calcMax() for an explanation of what this array stores private int[] mSpanFlags; + private int[] mSpanOrder; // store the order of span insertion + private int mSpanInsertCount; // counter for the span insertion + private int[] mPrioSortBuffer; // buffer used to sort getSpans result + private int[] mOrderSortBuffer; // buffer used to sort getSpans result + private int mSpanCount; private IdentityHashMap mIndexOfSpan; private int mLowWaterMark; // indices below this have not been touched diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java index 5c5deb45c391..47e71be333d8 100644 --- a/core/java/android/text/SpannableStringInternal.java +++ b/core/java/android/text/SpannableStringInternal.java @@ -36,24 +36,99 @@ import java.lang.reflect.Array; mSpanData = EmptyArray.INT; if (source instanceof Spanned) { - Spanned sp = (Spanned) source; - Object[] spans = sp.getSpans(start, end, Object.class); + if (source instanceof SpannableStringInternal) { + copySpans((SpannableStringInternal) source, start, end); + } else { + copySpans((Spanned) source, start, end); + } + } + } - for (int i = 0; i < spans.length; i++) { - int st = sp.getSpanStart(spans[i]); - int en = sp.getSpanEnd(spans[i]); - int fl = sp.getSpanFlags(spans[i]); + /** + * Copies another {@link Spanned} object's spans between [start, end] into this object. + * + * @param src Source object to copy from. + * @param start Start index in the source object. + * @param end End index in the source object. + */ + private final void copySpans(Spanned src, int start, int end) { + Object[] spans = src.getSpans(start, end, Object.class); + + for (int i = 0; i < spans.length; i++) { + int st = src.getSpanStart(spans[i]); + int en = src.getSpanEnd(spans[i]); + int fl = src.getSpanFlags(spans[i]); + + if (st < start) + st = start; + if (en > end) + en = end; + + setSpan(spans[i], st - start, en - start, fl); + } + } - if (st < start) - st = start; - if (en > end) - en = end; + /** + * Copies a {@link SpannableStringInternal} object's spans between [start, end] into this + * object. + * + * @param src Source object to copy from. + * @param start Start index in the source object. + * @param end End index in the source object. + */ + private final void copySpans(SpannableStringInternal src, int start, int end) { + if (start == 0 && end == src.length()) { + mSpans = ArrayUtils.newUnpaddedObjectArray(src.mSpans.length); + mSpanData = new int[src.mSpanData.length]; + mSpanCount = src.mSpanCount; + System.arraycopy(src.mSpans, 0, mSpans, 0, src.mSpans.length); + System.arraycopy(src.mSpanData, 0, mSpanData, 0, mSpanData.length); + } else { + int count = 0; + int[] srcData = src.mSpanData; + int limit = src.mSpanCount; + for (int i = 0; i < limit; i++) { + int spanStart = srcData[i * COLUMNS + START]; + int spanEnd = srcData[i * COLUMNS + END]; + if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue; + count++; + } - setSpan(spans[i], st - start, en - start, fl); + if (count == 0) return; + + Object[] srcSpans = src.mSpans; + mSpanCount = count; + mSpans = ArrayUtils.newUnpaddedObjectArray(mSpanCount); + mSpanData = new int[mSpanCount * COLUMNS]; + for (int i = 0, j = 0; i < limit; i++) { + int spanStart = srcData[i * COLUMNS + START]; + int spanEnd = srcData[i * COLUMNS + END]; + if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue; + if (spanStart < start) spanStart = start; + if (spanEnd > end) spanEnd = end; + + mSpans[j] = srcSpans[i]; + mSpanData[j * COLUMNS + START] = spanStart - start; + mSpanData[j * COLUMNS + END] = spanEnd - start; + mSpanData[j * COLUMNS + FLAGS] = srcData[i * COLUMNS + FLAGS]; + j++; } } } + /** + * Checks if [spanStart, spanEnd] interval is excluded from [start, end]. + * + * @return True if excluded, false if included. + */ + private final boolean isOutOfCopyRange(int start, int end, int spanStart, int spanEnd) { + if (spanStart > end || spanEnd < start) return true; + if (spanStart != spanEnd && start != end) { + if (spanStart == end || spanEnd == start) return true; + } + return false; + } + public final int length() { return mText.length(); } @@ -234,7 +309,7 @@ import java.lang.reflect.Array; } // verify span class as late as possible, since it is expensive - if (kind != null && !kind.isInstance(spans[i])) { + if (kind != null && kind != Object.class && !kind.isInstance(spans[i])) { continue; } diff --git a/core/tests/benchmarks/src/android/text/SpannableStringBuilderBenchmark.java b/core/tests/benchmarks/src/android/text/SpannableStringBuilderBenchmark.java new file mode 100644 index 000000000000..23f8810feff8 --- /dev/null +++ b/core/tests/benchmarks/src/android/text/SpannableStringBuilderBenchmark.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2015 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 com.google.caliper.AfterExperiment; +import com.google.caliper.BeforeExperiment; +import com.google.caliper.Benchmark; +import com.google.caliper.Param; + +public class SpannableStringBuilderBenchmark { + + @Param({"android.text.style.ImageSpan", + "android.text.style.ParagraphStyle", + "android.text.style.CharacterStyle", + "java.lang.Object"}) + private String paramType; + + @Param({"1", "4", "16"}) + private String paramStringMult; + + private Class clazz; + private SpannableStringBuilder builder; + + @BeforeExperiment + protected void setUp() throws Exception { + clazz = Class.forName(paramType); + int strSize = Integer.valueOf(paramStringMult); + StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < strSize; i++) { + strBuilder.append(TEST_STRING); + } + builder = new SpannableStringBuilder(Html.fromHtml(strBuilder.toString())); + } + + @AfterExperiment + protected void tearDown() { + builder.clear(); + builder = null; + } + + @Benchmark + public void timeGetSpans(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + builder.getSpans(0, builder.length(), clazz); + } + } + + //contains 0 ImageSpans, 2 ParagraphSpans, 53 CharacterStyleSpans + public static String TEST_STRING = + "

some link

\n" + + "

some title

\n" + + "

by some name\n" + + " some text

\n" + + "

some date

\n" + + "\n" + + " \n" + + "
\n" + + "

a paragraph

\n" + + "
\n" + + "

" + + "some header two

\n" + + "

Lorem ipsum dolor concludaturque.

\n" + + "


\n" + + "

Vix te doctus

\n" + + "

Error mel, est ei. " + + "asda ullamcorper eam.

\n" + + "

adversarium efficiantur, " + + "mea te.

\n" + + "


\n" + + "

Testing display of HTML elements

\n" + + "

2nd level heading

\n" + + "

test paragraph.

\n" + + "

3rd level heading

\n" + + "

test paragraph.

\n" + + "

4th level heading

\n" + + "

test paragraph.

\n" + + "
5th level heading
\n" + + "

test paragraph.

\n" + + "
6th level heading
\n" + + "

test paragraph.

\n" + + "

level elements

\n" + + "

a normap paragraph(p element).\n" + + " with some strong.

\n" + + "
This is a div element.
\n" + + "

This is a block quotation with some style

\n" + + "
an address element
\n" + + "

Text-level markup

\n" + + "
    \n" + + "
  • CSS (an abbreviation;\n" + + " abbr markup used)\n" + + "
  • radar\n" + + "
  • bolded\n" + + "
  • big thing\n" + + "
  • large size\n" + + "
  • Courier font\n" + + "
  • red text\n" + + "
  • Origin of Species\n" + + "
  • a[i] = b[i] + c[i);\n" + + "
  • some deleted text\n" + + "
  • an octet is an\n" + + "
  • this is very simple\n" + + "
  • Homo sapiens\n" + + "
  • some inserted text\n" + + "
  • type yes when\n" + + "
  • Hello!\n" + + "
  • She said Hello!\n" + + "
  • ccc\n" + + "
  • important\n" + + "
  • overstruck\n" + + "
  • this is highlighted text\n" + + "
  • sub and\n" + + " sup x1 and H2O\n" + + " Mlle, 1st, ex, sin2 x,\n" + + " ex2 and f(x)g(x)a+b+c\n" + + " (where 2 and a+b+c should appear as exponents of exponents).\n" + + "
  • text in monospace font\n" + + "
  • underlined text\n" + + "
  • cat filename displays the\n" + + " the filename.\n" + + "
\n"; + +} diff --git a/core/tests/benchmarks/src/android/text/SpannableStringInternalCopyBenchmark.java b/core/tests/benchmarks/src/android/text/SpannableStringInternalCopyBenchmark.java new file mode 100644 index 000000000000..dc5fed04a01a --- /dev/null +++ b/core/tests/benchmarks/src/android/text/SpannableStringInternalCopyBenchmark.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2013 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 com.google.caliper.AfterExperiment; +import com.google.caliper.BeforeExperiment; +import com.google.caliper.Benchmark; +import com.google.caliper.Param; + +public class SpannableStringInternalCopyBenchmark { + + @Param({"1", "4", "16"}) + private String paramStringMult; + + private SpannedString spanned; + + @BeforeExperiment + protected void setUp() throws Exception { + int strSize = Integer.valueOf(paramStringMult); + StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < strSize; i++) { + strBuilder.append(SpannableStringBuilderBenchmark.TEST_STRING); + } + Spanned source = Html.fromHtml(strBuilder.toString()); + spanned = new SpannedString(source); + } + + @AfterExperiment + protected void tearDown() { + spanned = null; + } + + @Benchmark + public void timeCopyConstructor(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + new SpannedString(spanned); + } + } + + @Benchmark + public void timeSubsequence(int reps) throws Exception { + for (int i = 0; i < reps; i++) { + spanned.subSequence(1, spanned.length()-1); + } + } + +} -- 2.11.0