diff --git a/apct-tests/perftests/core/src/android/text/TextUtilsPerfTest.java b/apct-tests/perftests/core/src/android/text/TextUtilsPerfTest.java new file mode 100644 index 0000000000000..c62269eb2978c --- /dev/null +++ b/apct-tests/perftests/core/src/android/text/TextUtilsPerfTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 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.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Supplier; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class TextUtilsPerfTest { + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + public static final String TEMPLATE = "Template that combines %s and %d together"; + + public String mVar1 = "example"; + public int mVar2 = 42; + + /** + * Measure overhead of formatting a string via {@link String#format}. + */ + @Test + public void timeFormatUpstream() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + String res = String.format(TEMPLATE, mVar1, mVar2); + } + } + + /** + * Measure overhead of formatting a string via + * {@link TextUtils#formatSimple}. + */ + @Test + public void timeFormatLocal() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + String res = TextUtils.formatSimple(TEMPLATE, mVar1, mVar2); + } + } + + /** + * Measure overhead of formatting a string inline. + */ + @Test + public void timeFormatInline() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + String res = "Template that combines " + mVar1 + " and " + mVar2 + " together"; + } + } + + /** + * Measure overhead of a passing null-check that uses a lambda to + * communicate a custom error message. + */ + @Test + public void timeFormat_Skip_Lambda() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + requireNonNull(this, () -> { + return String.format(TEMPLATE, mVar1, mVar2); + }); + } + } + + /** + * Measure overhead of a passing null-check that uses varargs to communicate + * a custom error message. + */ + @Test + public void timeFormat_Skip_Varargs() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + requireNonNull(this, TEMPLATE, mVar1, mVar2); + } + } + + private static T requireNonNull(T obj, Supplier messageSupplier) { + return obj; + } + + private static T requireNonNull(T obj, String format, Object... args) { + return obj; + } +} diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 984acfd2c860c..0cbb5c62eeb54 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -2079,6 +2079,94 @@ public class TextUtils { return Resources.getSystem().getQuantityString(R.plurals.selected_count, count, count); } + /** + * Simple alternative to {@link String#format} which purposefully supports + * only a small handful of substitutions to improve execution speed. + * Benchmarking reveals this optimized alternative performs 6.5x faster for + * a typical format string. + *

+ * Below is a summary of the limited grammar supported by this method; if + * you need advanced features, please continue using {@link String#format}. + *

    + *
  • {@code %b} for {@code boolean} + *
  • {@code %c} for {@code char} + *
  • {@code %d} for {@code int} or {@code long} + *
  • {@code %f} for {@code float} or {@code double} + *
  • {@code %s} for {@code String} + *
  • {@code %x} for hex representation of {@code int} or {@code long} + *
  • {@code %%} for literal {@code %} + *
+ * + * @throws IllegalArgumentException if the format string or arguments don't + * match the supported grammar described above. + * @hide + */ + public static @NonNull String formatSimple(@NonNull String format, Object... args) { + final StringBuilder sb = new StringBuilder(format); + int j = 0; + for (int i = 0; i < sb.length(); ) { + if (sb.charAt(i) == '%') { + final String repl; + final char code = sb.charAt(i + 1); + switch (code) { + case 'b': { + if (j == args.length) { + throw new IllegalArgumentException("Too few arguments"); + } + final Object arg = args[j++]; + if (arg instanceof Boolean) { + repl = Boolean.toString((boolean) arg); + } else { + repl = Boolean.toString(arg != null); + } + break; + } + case 'c': + case 'd': + case 'f': + case 's': { + if (j == args.length) { + throw new IllegalArgumentException("Too few arguments"); + } + final Object arg = args[j++]; + repl = String.valueOf(arg); + break; + } + case 'x': { + if (j == args.length) { + throw new IllegalArgumentException("Too few arguments"); + } + final Object arg = args[j++]; + if (arg instanceof Integer) { + repl = Integer.toHexString((int) arg); + } else if (arg instanceof Long) { + repl = Long.toHexString((long) arg); + } else { + throw new IllegalArgumentException( + "Unsupported hex type " + arg.getClass()); + } + break; + } + case '%': { + repl = "%"; + break; + } + default: { + throw new IllegalArgumentException("Unsupported format code " + code); + } + } + sb.replace(i, i + 2, repl); + i += repl.length(); + } else { + i++; + } + } + if (j != args.length) { + throw new IllegalArgumentException("Too many arguments"); + } + return sb.toString(); + } + /** * Returns whether or not the specified spanned text has a style span. * @hide diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java index dae649a903d5c..e80e5454f40e8 100644 --- a/core/java/com/android/internal/util/Preconditions.java +++ b/core/java/com/android/internal/util/Preconditions.java @@ -31,6 +31,12 @@ import java.util.Objects; */ public class Preconditions { + /** + * Ensures that an expression checking an argument is true. + * + * @param expression the expression to check + * @throws IllegalArgumentException if {@code expression} is false + */ @UnsupportedAppUsage public static void checkArgument(boolean expression) { if (!expression) { @@ -62,8 +68,9 @@ public class Preconditions { * @param messageArgs arguments for {@code messageTemplate} * @throws IllegalArgumentException if {@code expression} is false */ - public static void checkArgument(boolean expression, - final String messageTemplate, + public static void checkArgument( + final boolean expression, + final @NonNull String messageTemplate, final Object... messageArgs) { if (!expression) { throw new IllegalArgumentException(String.format(messageTemplate, messageArgs)); @@ -114,7 +121,9 @@ public class Preconditions { * @throws IllegalArgumentException if {@code string} is empty */ public static @NonNull T checkStringNotEmpty( - final T string, final String messageTemplate, final Object... messageArgs) { + final T string, + final @NonNull String messageTemplate, + final Object... messageArgs) { if (TextUtils.isEmpty(string)) { throw new IllegalArgumentException(String.format(messageTemplate, messageArgs)); } @@ -160,18 +169,22 @@ public class Preconditions { } /** - * Ensures the truth of an expression involving the state of the calling - * instance, but not involving any parameters to the calling method. + * Ensures that an object reference passed as a parameter to the calling + * method is not null. * - * @param expression a boolean expression - * @param message exception message - * @throws IllegalStateException if {@code expression} is false + * @param messageTemplate a printf-style message template to use if the check fails; will + * be converted to a string using {@link String#format(String, Object...)} + * @param messageArgs arguments for {@code messageTemplate} + * @throws NullPointerException if {@code reference} is null */ - @UnsupportedAppUsage - public static void checkState(final boolean expression, String message) { - if (!expression) { - throw new IllegalStateException(message); + public static @NonNull T checkNotNull( + final T reference, + final @NonNull String messageTemplate, + final Object... messageArgs) { + if (reference == null) { + throw new NullPointerException(String.format(messageTemplate, messageArgs)); } + return reference; } /** @@ -186,6 +199,41 @@ public class Preconditions { checkState(expression, null); } + /** + * Ensures the truth of an expression involving the state of the calling + * instance, but not involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @throws IllegalStateException if {@code expression} is false + */ + @UnsupportedAppUsage + public static void checkState(final boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalStateException(errorMessage); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling + * instance, but not involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param messageTemplate a printf-style message template to use if the check fails; will + * be converted to a string using {@link String#format(String, Object...)} + * @param messageArgs arguments for {@code messageTemplate} + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState( + final boolean expression, + final @NonNull String messageTemplate, + final Object... messageArgs) { + if (!expression) { + throw new IllegalStateException(String.format(messageTemplate, messageArgs)); + } + } + /** * Ensures the truth of an expression involving whether the calling identity is authorized to * call the calling method. diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java index be6ef195fa96c..b2cda28446a53 100644 --- a/core/tests/coretests/src/android/text/TextUtilsTest.java +++ b/core/tests/coretests/src/android/text/TextUtilsTest.java @@ -16,6 +16,8 @@ package android.text; +import static android.text.TextUtils.formatSimple; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -793,4 +795,53 @@ public class TextUtilsTest { assertEquals("ABC", TextUtils.trimToLengthWithEllipsis("ABC", 3)); assertEquals("", TextUtils.trimToLengthWithEllipsis("", 3)); } + + @Test + public void testFormatSimple_Types() { + assertEquals("true", formatSimple("%b", true)); + assertEquals("false", formatSimple("%b", false)); + assertEquals("true", formatSimple("%b", this)); + assertEquals("false", formatSimple("%b", new Object[] { null })); + + assertEquals("!", formatSimple("%c", '!')); + + assertEquals("42", formatSimple("%d", 42)); + assertEquals("281474976710656", formatSimple("%d", 281474976710656L)); + + assertEquals("3.14159", formatSimple("%f", 3.14159)); + assertEquals("NaN", formatSimple("%f", Float.NaN)); + + assertEquals("example", formatSimple("%s", "example")); + assertEquals("null", formatSimple("%s", new Object[] { null })); + + assertEquals("2a", formatSimple("%x", 42)); + assertEquals("1000000000000", formatSimple("%x", 281474976710656L)); + + assertEquals("%", formatSimple("%%")); + } + + @Test + public void testFormatSimple_Empty() { + assertEquals("", formatSimple("")); + } + + @Test + public void testFormatSimple_Typical() { + assertEquals("String foobar and %% number -42 together", + formatSimple("String %s%s and %%%% number %d%d together", "foo", "bar", -4, 2)); + } + + @Test + public void testFormatSimple_Mismatch() { + try { + formatSimple("%s"); + fail(); + } catch (IllegalArgumentException expected) { + } + try { + formatSimple("%s", "foo", "bar"); + fail(); + } catch (IllegalArgumentException expected) { + } + } }