Simple alternative to String.format().

This 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.

Add Preconditions overloads that accept varargs to avoid string
formatting overhead in successful case.

Bug: 170978902
Test: atest FrameworksCoreTests:android.text.TextUtilsTest
Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest CorePerfTests:android.text.TextUtilsPerfTest
Change-Id: I3fae4dc95cfc98a61c4a7f07ca0781c4a2ee3be9
This commit is contained in:
Jeff Sharkey
2020-10-15 14:33:04 -06:00
parent a3e52bf4e4
commit af7c5f95cb
4 changed files with 308 additions and 12 deletions

View File

@@ -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> T requireNonNull(T obj, Supplier<String> messageSupplier) {
return obj;
}
private static <T> T requireNonNull(T obj, String format, Object... args) {
return obj;
}
}

View File

@@ -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.
* <p>
* Below is a summary of the limited grammar supported by this method; if
* you need advanced features, please continue using {@link String#format}.
* <ul>
* <li>{@code %b} for {@code boolean}
* <li>{@code %c} for {@code char}
* <li>{@code %d} for {@code int} or {@code long}
* <li>{@code %f} for {@code float} or {@code double}
* <li>{@code %s} for {@code String}
* <li>{@code %x} for hex representation of {@code int} or {@code long}
* <li>{@code %%} for literal {@code %}
* </ul>
*
* @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

View File

@@ -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 extends CharSequence> 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> 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.

View File

@@ -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) {
}
}
}