Initial support for maximumremovedandroidloglevel directive
This adds support for `-maximumremovedandroidloglevel <int>`. The syntax `-maximumremovedandroidloglevel <int> <class_specification>`, which can be used to set the maximum-removed-android-log-level per method, is still not supported.
Example: The value of android.util.log.INFO is 4. Therefore, specifying `-maximumremovedandroidloglevel 4` will remove all calls to Log.v(), Log.d(), and Log.i(), as well as it will replace calls to Log.isLoggable(X, {2, 3, 4}) by false.
Bug: 147870129
Change-Id: I8bf9ecd562c22a99c7aa53031857300829f77abediff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
index 9c5a8d1..91e952a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
@@ -32,6 +32,10 @@
public LibraryMethodOptimizer(AppView<? extends AppInfoWithSubtyping> appView) {
this.appView = appView;
register(new BooleanMethodOptimizer(appView));
+
+ if (LogMethodOptimizer.isEnabled(appView)) {
+ register(new LogMethodOptimizer(appView));
+ }
}
private void register(LibraryMethodModelCollection optimizer) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
new file mode 100644
index 0000000..f65b57a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
@@ -0,0 +1,154 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import java.util.Set;
+
+public class LogMethodOptimizer implements LibraryMethodModelCollection {
+
+ private static final int VERBOSE = 2;
+ private static final int DEBUG = 3;
+ private static final int INFO = 4;
+ private static final int WARN = 5;
+ private static final int ERROR = 6;
+ private static final int ASSERT = 7;
+
+ private final AppView<? extends AppInfoWithSubtyping> appView;
+
+ private final DexType logType;
+
+ private final DexMethod isLoggableMethod;
+ private final DexMethod vMethod;
+ private final DexMethod dMethod;
+ private final DexMethod iMethod;
+ private final DexMethod wMethod;
+ private final DexMethod eMethod;
+ private final DexMethod wtfMethod;
+
+ LogMethodOptimizer(AppView<? extends AppInfoWithSubtyping> appView) {
+ this.appView = appView;
+
+ DexItemFactory dexItemFactory = appView.dexItemFactory();
+ DexType logType = dexItemFactory.createType("Landroid/util/Log;");
+ this.logType = logType;
+ this.isLoggableMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.booleanType, dexItemFactory.stringType, dexItemFactory.intType),
+ "isLoggable");
+ this.vMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "v");
+ this.dMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "d");
+ this.iMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "i");
+ this.wMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "w");
+ this.eMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "e");
+ this.wtfMethod =
+ dexItemFactory.createMethod(
+ logType,
+ dexItemFactory.createProto(
+ dexItemFactory.intType, dexItemFactory.stringType, dexItemFactory.stringType),
+ "wtf");
+ }
+
+ public static boolean isEnabled(AppView<?> appView) {
+ return appView.options().getProguardConfiguration().getMaxRemovedAndroidLogLevel() >= VERBOSE;
+ }
+
+ @Override
+ public DexType getType() {
+ return logType;
+ }
+
+ @Override
+ public void optimize(
+ IRCode code,
+ InstructionListIterator instructionIterator,
+ InvokeMethod invoke,
+ DexEncodedMethod singleTarget,
+ Set<Value> affectedValues) {
+ int maxRemovedAndroidLogLevel =
+ appView.options().getProguardConfiguration().getMaxRemovedAndroidLogLevel();
+ if (singleTarget.method == isLoggableMethod) {
+ Value logLevelValue = invoke.arguments().get(1).getAliasedValue();
+ if (!logLevelValue.isPhi() && !logLevelValue.hasLocalInfo()) {
+ Instruction definition = logLevelValue.definition;
+ if (definition.isConstNumber()) {
+ int logLevel = definition.asConstNumber().getIntValue();
+ replaceInvokeWithConstNumber(
+ code, instructionIterator, invoke, maxRemovedAndroidLogLevel >= logLevel ? 0 : 1);
+ }
+ }
+ } else if (singleTarget.method == vMethod) {
+ if (maxRemovedAndroidLogLevel >= VERBOSE) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ } else if (singleTarget.method == dMethod) {
+ if (maxRemovedAndroidLogLevel >= DEBUG) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ } else if (singleTarget.method == iMethod) {
+ if (maxRemovedAndroidLogLevel >= INFO) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ } else if (singleTarget.method == wMethod) {
+ if (maxRemovedAndroidLogLevel >= WARN) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ } else if (singleTarget.method == eMethod) {
+ if (maxRemovedAndroidLogLevel >= ERROR) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ } else if (singleTarget.method == wtfMethod) {
+ if (maxRemovedAndroidLogLevel >= ASSERT) {
+ replaceInvokeWithConstNumber(code, instructionIterator, invoke, 0);
+ }
+ }
+ }
+
+ private void replaceInvokeWithConstNumber(
+ IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke, int value) {
+ if (invoke.hasOutValue() && invoke.outValue().hasAnyUsers()) {
+ instructionIterator.replaceCurrentInstructionWithConstInt(appView, code, value);
+ } else {
+ instructionIterator.removeOrReplaceByDebugLocalRead();
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
index bfad990..3c91bc8 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfiguration.java
@@ -70,6 +70,7 @@
private boolean keepRuleSynthesisForRecompilation = false;
private boolean configurationDebugging = false;
private boolean dontUseMixedCaseClassnames = false;
+ private int maxRemovedAndroidLogLevel = 1;
private Builder(DexItemFactory dexItemFactory, Reporter reporter) {
this.dexItemFactory = dexItemFactory;
@@ -288,6 +289,14 @@
this.dontUseMixedCaseClassnames = dontUseMixedCaseClassnames;
}
+ public int getMaxRemovedAndroidLogLevel() {
+ return maxRemovedAndroidLogLevel;
+ }
+
+ public void setMaxRemovedAndroidLogLevel(int maxRemovedAndroidLogLevel) {
+ this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
+ }
+
/**
* This synthesizes a set of keep rules that are necessary in order to be able to successfully
* recompile the generated dex files with the same keep rules.
@@ -346,7 +355,8 @@
adaptResourceFileContents.build(),
keepDirectories.build(),
configurationDebugging,
- dontUseMixedCaseClassnames);
+ dontUseMixedCaseClassnames,
+ maxRemovedAndroidLogLevel);
reporter.failIfPendingErrors();
@@ -415,6 +425,7 @@
private final ProguardPathFilter keepDirectories;
private final boolean configurationDebugging;
private final boolean dontUseMixedCaseClassnames;
+ private final int maxRemovedAndroidLogLevel;
private ProguardConfiguration(
String parsedConfiguration,
@@ -454,7 +465,8 @@
ProguardPathFilter adaptResourceFileContents,
ProguardPathFilter keepDirectories,
boolean configurationDebugging,
- boolean dontUseMixedCaseClassnames) {
+ boolean dontUseMixedCaseClassnames,
+ int maxRemovedAndroidLogLevel) {
this.parsedConfiguration = parsedConfiguration;
this.dexItemFactory = factory;
this.injars = ImmutableList.copyOf(injars);
@@ -493,6 +505,7 @@
this.keepDirectories = keepDirectories;
this.configurationDebugging = configurationDebugging;
this.dontUseMixedCaseClassnames = dontUseMixedCaseClassnames;
+ this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
}
/**
@@ -659,6 +672,10 @@
return dontUseMixedCaseClassnames;
}
+ public int getMaxRemovedAndroidLogLevel() {
+ return maxRemovedAndroidLogLevel;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index a589f9a..cc05e23 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -58,7 +58,7 @@
"maximuminlinedcodelength");
private static final List<String> IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS =
- ImmutableList.of("runtype", "laststageoutput", "maximumremovedandroidloglevel");
+ ImmutableList.of("runtype", "laststageoutput");
private static final List<String> IGNORED_FLAG_OPTIONS = ImmutableList.of(
"forceprocessing",
@@ -398,6 +398,14 @@
configurationBuilder.setConfigurationDebugging(true);
} else if (acceptString("dontusemixedcaseclassnames")) {
configurationBuilder.setDontUseMixedCaseClassnames(true);
+ } else if (acceptString("maximumremovedandroidloglevel")) {
+ skipWhitespace();
+ Integer maxRemovedAndroidLogLevel = acceptInteger();
+ if (maxRemovedAndroidLogLevel != null) {
+ configurationBuilder.setMaxRemovedAndroidLogLevel(maxRemovedAndroidLogLevel);
+ } else {
+ throw parseError("Expected integer", getPosition());
+ }
} else {
String unknownOption = acceptString();
String devMessage = "";
diff --git a/src/test/java/com/android/tools/r8/ir/InlineTest.java b/src/test/java/com/android/tools/r8/ir/InlineTest.java
index e20e882..12c9268 100644
--- a/src/test/java/com/android/tools/r8/ir/InlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/InlineTest.java
@@ -10,6 +10,7 @@
import com.android.tools.r8.graph.AppServices;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
@@ -17,11 +18,13 @@
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.EnqueuerFactory;
import com.android.tools.r8.shaking.ProguardClassFilter;
+import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardKeepRule;
import com.android.tools.r8.shaking.RootSetBuilder;
import com.android.tools.r8.smali.SmaliBuilder;
import com.android.tools.r8.smali.SmaliBuilder.MethodSignature;
import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -62,6 +65,13 @@
return new TestApplication(appView, method, additionalCode);
}
+ private static InternalOptions createOptions() {
+ Reporter reporter = new Reporter();
+ ProguardConfiguration proguardConfiguration =
+ ProguardConfiguration.builder(new DexItemFactory(), reporter).build();
+ return new InternalOptions(proguardConfiguration, reporter);
+ }
+
private TestApplication codeForMethodReplaceTest(int a, int b) throws ExecutionException {
SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
@@ -108,7 +118,7 @@
" return-void"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -190,7 +200,7 @@
" return-void"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -267,7 +277,7 @@
" return-void"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -400,7 +410,7 @@
" return-void"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -513,7 +523,7 @@
" goto :print_result"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -624,7 +634,7 @@
" goto :print_result"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -737,7 +747,7 @@
" goto :print_result"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -893,7 +903,7 @@
" goto :print_result"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
@@ -1139,7 +1149,7 @@
" goto :print_result"
);
- InternalOptions options = new InternalOptions();
+ InternalOptions options = createOptions();
DexApplication application = buildApplication(builder, options).toDirect();
// Return the processed method for inspection.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java
new file mode 100644
index 0000000..1314274
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/logging/AndroidLogRemovalTest.java
@@ -0,0 +1,195 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.logging;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AndroidLogRemovalTest extends TestBase {
+
+ private final int maxRemovedAndroidLogLevel;
+ private final TestParameters parameters;
+
+ @Parameters(name = "{1}, log level: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ ImmutableList.of(1, 2, 3, 4, 5, 6, 7),
+ getTestParameters().withAllRuntimesAndApiLevels().build());
+ }
+
+ public AndroidLogRemovalTest(int maxRemovedAndroidLogLevel, TestParameters parameters) {
+ this.maxRemovedAndroidLogLevel = maxRemovedAndroidLogLevel;
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ Path libraryFile =
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(
+ transformer(Log.class).setClassDescriptor("Landroid/util/Log;").transform())
+ .addKeepAllClassesRule()
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .writeToZip();
+
+ testForR8(parameters.getBackend())
+ .addProgramClassFileData(
+ transformer(TestClass.class)
+ .transformMethodInsnInMethod(
+ "main",
+ (opcode, owner, name, descriptor, isInterface, continuation) ->
+ continuation.apply(
+ opcode,
+ owner.endsWith("$Log") ? "android/util/Log" : owner,
+ name,
+ descriptor,
+ isInterface))
+ .transform())
+ .addLibraryFiles(libraryFile, runtimeJar(parameters.getBackend()))
+ .addKeepMainRule(TestClass.class)
+ .addKeepRules("-maximumremovedandroidloglevel " + maxRemovedAndroidLogLevel)
+ .setMinApi(parameters.getApiLevel())
+ .compile()
+ .addRunClasspathFiles(libraryFile)
+ .run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(getExpectedOutputForMaxLogLevel());
+ }
+
+ private String getExpectedOutputForMaxLogLevel() {
+ switch (maxRemovedAndroidLogLevel) {
+ case 1:
+ return StringUtils.join(
+ "",
+ StringUtils.times(StringUtils.lines("[R8] VERBOSE."), 2),
+ StringUtils.times(StringUtils.lines("[R8] DEBUG."), 2),
+ StringUtils.times(StringUtils.lines("[R8] INFO."), 2),
+ StringUtils.times(StringUtils.lines("[R8] WARN."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ERROR."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 2:
+ return StringUtils.join(
+ "",
+ StringUtils.times(StringUtils.lines("[R8] DEBUG."), 2),
+ StringUtils.times(StringUtils.lines("[R8] INFO."), 2),
+ StringUtils.times(StringUtils.lines("[R8] WARN."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ERROR."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 3:
+ return StringUtils.join(
+ "",
+ StringUtils.times(StringUtils.lines("[R8] INFO."), 2),
+ StringUtils.times(StringUtils.lines("[R8] WARN."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ERROR."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 4:
+ return StringUtils.join(
+ "",
+ StringUtils.times(StringUtils.lines("[R8] WARN."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ERROR."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 5:
+ return StringUtils.join(
+ "",
+ StringUtils.times(StringUtils.lines("[R8] ERROR."), 2),
+ StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 6:
+ return StringUtils.join("", StringUtils.times(StringUtils.lines("[R8] ASSERT."), 2));
+ case 7:
+ return "";
+ default:
+ throw new Unreachable();
+ }
+ }
+
+ static class TestClass {
+
+ public static void main(String[] args) {
+ Log.v("R8", "VERBOSE.");
+ if (Log.isLoggable("R8", Log.VERBOSE)) {
+ System.out.println("[R8] VERBOSE.");
+ }
+
+ Log.d("R8", "DEBUG.");
+ if (Log.isLoggable("R8", Log.DEBUG)) {
+ System.out.println("[R8] DEBUG.");
+ }
+
+ Log.i("R8", "INFO.");
+ if (Log.isLoggable("R8", Log.INFO)) {
+ System.out.println("[R8] INFO.");
+ }
+
+ Log.w("R8", "WARN.");
+ if (Log.isLoggable("R8", Log.WARN)) {
+ System.out.println("[R8] WARN.");
+ }
+
+ Log.e("R8", "ERROR.");
+ if (Log.isLoggable("R8", Log.ERROR)) {
+ System.out.println("[R8] ERROR.");
+ }
+
+ Log.wtf("R8", "ASSERT.");
+ if (Log.isLoggable("R8", Log.ASSERT)) {
+ System.out.println("[R8] ASSERT.");
+ }
+ }
+ }
+
+ public static class Log {
+
+ public static final int VERBOSE = 2;
+ public static final int DEBUG = 3;
+ public static final int INFO = 4;
+ public static final int WARN = 5;
+ public static final int ERROR = 6;
+ public static final int ASSERT = 7;
+
+ public static boolean isLoggable(String tag, int level) {
+ return true;
+ }
+
+ public static int v(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+
+ public static int d(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+
+ public static int i(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+
+ public static int w(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+
+ public static int e(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+
+ public static int wtf(String tag, String message) {
+ System.out.println("[" + tag + "] " + message);
+ return 42;
+ }
+ }
+}