diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2020-10-16 22:27:50 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-10-16 22:27:50 +0000 |
commit | eac0321ff317049f020a9f48d07742b8cce99be2 (patch) | |
tree | b0ecb1872fa22168eaba5d69df1755f3874ad996 | |
parent | fe82ad21fe6bac1075e5e4a32278a4039d7cdcaa (diff) | |
parent | a52266b3e352e33bae1ce77cfef7b16219f4817e (diff) |
Merge changes from topic "oct16b"
* changes:
Recommend efficient String operations.
Simple alternative to String.format().
7 files changed, 613 insertions, 9 deletions
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 000000000000..c62269eb2978 --- /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> T requireNonNull(T obj, Supplier<String> messageSupplier) { + return obj; + } + + private static <T> 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 d441e6bbf8cb..72b35b9253eb 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -2080,6 +2080,94 @@ public class TextUtils { } /** + * 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 */ diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java index dae649a903d5..e80e5454f40e 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 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,17 +169,49 @@ public class Preconditions { } /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @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 + */ + 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; + } + + /** * 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 message exception message * @throws IllegalStateException if {@code expression} is false */ @UnsupportedAppUsage - public static void checkState(final boolean expression, String message) { + public static void checkState(final boolean expression) { + 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(message); + throw new IllegalStateException(errorMessage); } } @@ -179,11 +220,18 @@ public class Preconditions { * 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 */ - @UnsupportedAppUsage - public static void checkState(final boolean expression) { - checkState(expression, null); + public static void checkState( + final boolean expression, + final @NonNull String messageTemplate, + final Object... messageArgs) { + if (!expression) { + throw new IllegalStateException(String.format(messageTemplate, messageArgs)); + } } /** diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java index 285ad9fae13c..4e49118f438c 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; @@ -794,4 +796,53 @@ public class TextUtilsTest { assertEquals("", TextUtils.trimToLengthWithEllipsis("", 3)); assertNull(TextUtils.trimToLengthWithEllipsis(null, 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) { + } + } } diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientStringsChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientStringsChecker.java new file mode 100644 index 000000000000..3fbd51deec65 --- /dev/null +++ b/errorprone/java/com/google/errorprone/bugpatterns/android/EfficientStringsChecker.java @@ -0,0 +1,146 @@ +/* + * 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 com.google.errorprone.bugpatterns.android; + +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Matchers.allOf; +import static com.google.errorprone.matchers.Matchers.contains; +import static com.google.errorprone.matchers.Matchers.kindIs; +import static com.google.errorprone.matchers.Matchers.methodInvocation; +import static com.google.errorprone.matchers.Matchers.not; +import static com.google.errorprone.matchers.Matchers.staticMethod; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.google.errorprone.predicates.TypePredicate; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree.Kind; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; + +import java.util.List; + +/** + * Android offers several efficient alternatives to some upstream {@link String} + * operations. + */ +@AutoService(BugChecker.class) +@BugPattern( + name = "AndroidFrameworkEfficientStrings", + summary = "Verifies efficient Strings best-practices", + severity = WARNING) +public final class EfficientStringsChecker extends BugChecker + implements MethodInvocationTreeMatcher { + + private static final Matcher<ExpressionTree> FORMAT_CALL = methodInvocation( + staticMethod().onClass("java.lang.String").named("format")); + private static final Matcher<ExpressionTree> PRECONDITIONS_CALL = methodInvocation( + staticMethod().onClass(withSimpleName("Preconditions")).withAnyName()); + private static final Matcher<ExpressionTree> OBJECTS_CALL = methodInvocation( + staticMethod().onClass("java.util.Objects").named("requireNonNull")); + + /** + * Match an expression which combines both string literals any other dynamic + * values, since these allocate a transparent StringBuilder. + * <p> + * This won't match a single isolated string literal, or a chain consisting + * of only string literals, since those don't require dynamic construction. + */ + private static final Matcher<ExpressionTree> CONTAINS_DYNAMIC_STRING = allOf( + contains(ExpressionTree.class, kindIs(Kind.STRING_LITERAL)), + contains(ExpressionTree.class, not(kindIs(Kind.STRING_LITERAL)))); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (FORMAT_CALL.matches(tree, state)) { + // Skip over possible locale to find format string + final List<? extends ExpressionTree> args = tree.getArguments(); + final ExpressionTree formatArg; + final List<VarSymbol> vars = ASTHelpers.getSymbol(tree).params(); + if (vars.get(0).type.toString().equals("java.util.Locale")) { + formatArg = args.get(1); + } else { + formatArg = args.get(0); + } + + // Determine if format string is "simple" enough to replace + if (formatArg.getKind() == Kind.STRING_LITERAL) { + final String format = String.valueOf(((LiteralTree) formatArg).getValue()); + if (isSimple(format)) { + return buildDescription(formatArg) + .setMessage("Simple format strings can be replaced with " + + "TextUtils.formatSimple() for a 6x performance improvement") + .build(); + } + } + } else if (PRECONDITIONS_CALL.matches(tree, state) + || OBJECTS_CALL.matches(tree, state)) { + final List<? extends ExpressionTree> args = tree.getArguments(); + for (int i = 1 ; i < args.size(); i++) { + final ExpressionTree arg = args.get(i); + if (CONTAINS_DYNAMIC_STRING.matches(arg, state)) { + return buildDescription(arg) + .setMessage("Building dynamic messages is discouraged, since they " + + "always allocate a transparent StringBuilder, even in " + + "the successful case") + .build(); + } + } + } + return Description.NO_MATCH; + } + + static boolean isSimple(String format) { + for (int i = 0; i < format.length(); i++) { + char c = format.charAt(i); + if (c == '%') { + i++; + c = format.charAt(i); + switch (c) { + case 'b': + case 'c': + case 'd': + case 'f': + case 's': + case 'x': + case '%': + break; + default: + return false; + } + } + } + return true; + } + + static TypePredicate withSimpleName(final String filter) { + return new TypePredicate() { + @Override + public boolean apply(Type type, VisitorState state) { + return type.tsym.getSimpleName().toString().equals(filter); + } + }; + } +} diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/EfficientStringsCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/EfficientStringsCheckerTest.java new file mode 100644 index 000000000000..a755564d52dd --- /dev/null +++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/EfficientStringsCheckerTest.java @@ -0,0 +1,119 @@ +/* + * 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 com.google.errorprone.bugpatterns.android; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import com.google.errorprone.CompilationTestHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class EfficientStringsCheckerTest { + private CompilationTestHelper compilationHelper; + + @Before + public void setUp() { + compilationHelper = CompilationTestHelper.newInstance( + EfficientStringsChecker.class, getClass()); + } + + @Test + public void testSimple() { + assertTrue(EfficientStringsChecker.isSimple("")); + assertTrue(EfficientStringsChecker.isSimple("%s")); + assertTrue(EfficientStringsChecker.isSimple("String %s%s and %%%% number %d%d together")); + + assertFalse(EfficientStringsChecker.isSimple("%04d")); + assertFalse(EfficientStringsChecker.isSimple("%02x:%02x:%02x")); + } + + @Test + public void testFormat() { + compilationHelper + .addSourceLines("Example.java", + "import java.util.Locale;", + "public class Example {", + " public void example(String str) {", + " String.format(str, str);", + " // BUG: Diagnostic contains:", + " String.format(\"foo %s bar\", str);", + " // BUG: Diagnostic contains:", + " String.format(\"foo %d bar\", 42);", + " String.format(\"foo %04d bar\", 42);", + " }", + " public void exampleLocale(String str) {", + " String.format(Locale.US, str, str);", + " // BUG: Diagnostic contains:", + " String.format(Locale.US, \"foo %s bar\", str);", + " // BUG: Diagnostic contains:", + " String.format(Locale.US, \"foo %d bar\", 42);", + " String.format(Locale.US, \"foo %04d bar\", 42);", + " }", + "}") + .doTest(); + } + + @Test + public void testPreconditions() { + compilationHelper + .addSourceFile("/android/util/Preconditions.java") + .addSourceLines("Example.java", + "import android.util.Preconditions;", + "import java.util.Objects;", + "public class Example {", + " String str;", + " public void checkState(boolean val) {", + " Preconditions.checkState(val);", + " Preconditions.checkState(val, str);", + " Preconditions.checkState(val, \"foo\");", + " Preconditions.checkState(val, \"foo\" + \"bar\");", + " // BUG: Diagnostic contains:", + " Preconditions.checkState(val, \"foo \" + val);", + " }", + " public void checkArgument(boolean val) {", + " Preconditions.checkArgument(val);", + " Preconditions.checkArgument(val, str);", + " Preconditions.checkArgument(val, \"foo\");", + " Preconditions.checkArgument(val, \"foo\" + \"bar\");", + " // BUG: Diagnostic contains:", + " Preconditions.checkArgument(val, \"foo \" + val);", + " }", + " public void checkNotNull(Object val) {", + " Preconditions.checkNotNull(val);", + " Preconditions.checkNotNull(val, str);", + " Preconditions.checkNotNull(val, \"foo\");", + " Preconditions.checkNotNull(val, \"foo\" + \"bar\");", + " // BUG: Diagnostic contains:", + " Preconditions.checkNotNull(val, \"foo \" + val);", + " }", + " public void requireNonNull(Object val) {", + " Objects.requireNonNull(val);", + " Objects.requireNonNull(val, str);", + " Objects.requireNonNull(val, \"foo\");", + " Objects.requireNonNull(val, \"foo\" + \"bar\");", + " // BUG: Diagnostic contains:", + " Objects.requireNonNull(val, \"foo \" + val);", + " }", + "}") + .doTest(); + } +} diff --git a/errorprone/tests/res/android/util/Preconditions.java b/errorprone/tests/res/android/util/Preconditions.java new file mode 100644 index 000000000000..558cdaf483a4 --- /dev/null +++ b/errorprone/tests/res/android/util/Preconditions.java @@ -0,0 +1,43 @@ +/* + * 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.util; + +public class Preconditions { + public static void checkArgument(boolean expression) { + throw new UnsupportedOperationException(); + } + + public static void checkArgument(boolean expression, Object errorMessage) { + throw new UnsupportedOperationException(); + } + + public static <T> T checkNotNull(T reference) { + throw new UnsupportedOperationException(); + } + + public static <T> T checkNotNull(T reference, Object errorMessage) { + throw new UnsupportedOperationException(); + } + + public static void checkState(boolean expression) { + throw new UnsupportedOperationException(); + } + + public static void checkState(boolean expression, String message) { + throw new UnsupportedOperationException(); + } +} |