Merge changes Ieff6fd20,I16a59957 * changes: Use original static modifier when evaluating -if rules Use original final modifier when evaluating -if rules
diff --git a/build.gradle b/build.gradle index 00b8a46..c205b2d 100644 --- a/build.gradle +++ b/build.gradle
@@ -676,9 +676,6 @@ task testJar(type: ShadowJar, dependsOn: testClasses) { baseName = "r8tests" from sourceSets.test.output - if (!project.hasProperty('exclude_deps')) { - relocate('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm') - } } task testJarNoDeps(type: ShadowJar, dependsOn: testClasses) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java index 1be9b3a..14dd1ae 100644 --- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java +++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -75,7 +75,7 @@ if (!mainDexRootSet.checkDiscarded.isEmpty()) { new DiscardedChecker( - mainDexRootSet, mainDexClasses.getClasses(), appView.appInfo(), options) + mainDexRootSet, mainDexClasses.getClasses(), appView.appInfo(), options) .run(); } // Print -whyareyoukeeping results if any.
diff --git a/src/main/java/com/android/tools/r8/code/FillArrayData.java b/src/main/java/com/android/tools/r8/code/FillArrayData.java index 4569897..a49a31d 100644 --- a/src/main/java/com/android/tools/r8/code/FillArrayData.java +++ b/src/main/java/com/android/tools/r8/code/FillArrayData.java
@@ -44,4 +44,9 @@ public String toSmaliString(ClassNameMapper naming) { return formatSmaliString("v" + AA + ", :label_" + (getOffset() + BBBBBBBB)); } + + @Override + public boolean canThrow() { + return true; + } }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java index 73f7921..5e71072 100644 --- a/src/main/java/com/android/tools/r8/dex/FileWriter.java +++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -441,7 +441,7 @@ dest.putShort((short) code.incomingRegisterSize); dest.putShort((short) code.outgoingRegisterSize); dest.putShort((short) code.tries.length); - dest.putInt(mixedSectionOffsets.getOffsetFor(code.getDebugInfo())); + dest.putInt(mixedSectionOffsets.getOffsetFor(code.getDebugInfoForWriting())); // Jump over the size. int insnSizeOffset = dest.position(); dest.forward(4);
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java index b754c9a..e02869d 100644 --- a/src/main/java/com/android/tools/r8/graph/DexCode.java +++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -40,6 +40,7 @@ public DexString highestSortingString; private DexDebugInfo debugInfo; + private DexDebugInfoForWriting debugInfoForWriting; public DexCode( int registerSize, @@ -97,6 +98,9 @@ public void setDebugInfo(DexDebugInfo debugInfo) { this.debugInfo = debugInfo; + if (debugInfoForWriting != null) { + debugInfoForWriting = null; + } } public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) { @@ -382,7 +386,7 @@ } } if (debugInfo != null) { - debugInfo.collectIndexedItems(indexedItems); + getDebugInfoForWriting().collectIndexedItems(indexedItems); } if (handlers != null) { for (TryHandler handler : handlers) { @@ -391,6 +395,17 @@ } } + public DexDebugInfoForWriting getDebugInfoForWriting() { + if (debugInfo == null) { + return null; + } + if (debugInfoForWriting == null) { + debugInfoForWriting = new DexDebugInfoForWriting(debugInfo); + } + + return debugInfoForWriting; + } + private void updateHighestSortingString(DexString candidate) { assert candidate != null; if (highestSortingString == null || highestSortingString.slowCompareTo(candidate) < 0) { @@ -406,7 +421,7 @@ void collectMixedSectionItems(MixedSectionCollection mixedItems) { if (mixedItems.add(this)) { if (debugInfo != null) { - debugInfo.collectMixedSectionItems(mixedItems); + getDebugInfoForWriting().collectMixedSectionItems(mixedItems); } } }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java new file mode 100644 index 0000000..0777b85 --- /dev/null +++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java
@@ -0,0 +1,23 @@ +// Copyright (c) 2019, 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.graph; + +import com.android.tools.r8.graph.DexDebugEvent.SetInlineFrame; +import java.util.Arrays; + +/** + * Wraps DexDebugInfo to make comparison and hashcode not consider + * the SetInlineFrames + */ +public class DexDebugInfoForWriting extends DexDebugInfo { + + public DexDebugInfoForWriting(DexDebugInfo dexDebugInfo) { + super(dexDebugInfo.startLine, dexDebugInfo.parameters, + Arrays.stream(dexDebugInfo.events) + .filter(d -> !(d instanceof SetInlineFrame)) + .toArray(DexDebugEvent[]::new)); + } + +}
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java index 50c91eb..898afed 100644 --- a/src/main/java/com/android/tools/r8/graph/GraphLense.java +++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -300,23 +300,38 @@ private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create(); public void map(DexType from, DexType to) { + if (from == to) { + return; + } typeMap.put(from, to); } public void map(DexMethod from, DexMethod to) { + if (from == to) { + return; + } methodMap.put(from, to); } public void map(DexField from, DexField to) { + if (from == to) { + return; + } fieldMap.put(from, to); } public void move(DexMethod from, DexMethod to) { + if (from == to) { + return; + } map(from, to); originalMethodSignatures.put(to, from); } public void move(DexField from, DexField to) { + if (from == to) { + return; + } fieldMap.put(from, to); originalFieldSignatures.put(to, from); } @@ -840,9 +855,11 @@ @Override public String toString() { StringBuilder builder = new StringBuilder(); - for (Map.Entry<DexType, DexType> entry : typeMap.entrySet()) { - builder.append(entry.getKey().toSourceString()).append(" -> "); - builder.append(entry.getValue().toSourceString()).append(System.lineSeparator()); + if (typeMap != null) { + for (Map.Entry<DexType, DexType> entry : typeMap.entrySet()) { + builder.append(entry.getKey().toSourceString()).append(" -> "); + builder.append(entry.getValue().toSourceString()).append(System.lineSeparator()); + } } for (Map.Entry<DexMethod, DexMethod> entry : methodMap.entrySet()) { builder.append(entry.getKey().toSourceString()).append(" -> ");
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java index 85cde7c..3ce1467 100644 --- a/src/main/java/com/android/tools/r8/graph/JarCode.java +++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -118,6 +118,10 @@ return true; } + public boolean hasLocalVariableTable() { + return getNode().localVariables != null && !getNode().localVariables.isEmpty(); + } + @Override public IRCode buildIR( DexEncodedMethod encodedMethod, @@ -214,9 +218,13 @@ triggerDelayedParsingIfNeccessary(); node.instructions.accept( new JarRegisterEffectsVisitor(method.getHolder(), registry, application)); - node.tryCatchBlocks.forEach(tryCatchBlockNode -> + for (TryCatchBlockNode tryCatchBlockNode : node.tryCatchBlocks) { + // Exception type can be null for "catch all" used for try/finally. + if (tryCatchBlockNode.type != null) { registry.registerTypeReference(application.getTypeFromDescriptor( - DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type)))); + DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type))); + } + } } @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java index 1544570..9c3bcc3 100644 --- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java +++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
@@ -107,7 +107,6 @@ throw new Unreachable("unless a new type lattice is introduced."); } - public static TypeLatticeElement join( Iterable<TypeLatticeElement> typeLattices, AppInfo appInfo) { TypeLatticeElement result = BOTTOM;
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java index 77f7fb8..9b8a00e 100644 --- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java +++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
@@ -27,17 +27,6 @@ assert !ending.isEmpty() || !starting.isEmpty(); this.ending = ending; this.starting = starting; - super.setPosition(Position.none()); - } - - @Override - public void setPosition(Position position) { - throw new Unreachable(); - } - - @Override - public boolean verifyValidPositionInfo(boolean debug) { - return true; } public Int2ReferenceMap<DebugLocalInfo> getEnding() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java index 8e0d572..cb93a5e 100644 --- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java +++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -662,13 +662,9 @@ } // After the throwing instruction only debug instructions and the final jump // instruction is allowed. - // TODO(ager): For now allow const instructions due to the way consts are pushed - // towards their use if (seenThrowing) { assert instruction.isDebugInstruction() || instruction.isJumpInstruction() - || instruction.isConstInstruction() - || instruction.isNewArrayFilledData() || instruction.isStore() || instruction.isPop(); }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java index b98662c..cdb40bf 100644 --- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java +++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java
@@ -19,10 +19,7 @@ @Override public boolean hasNext() { - if (instructionIterator.hasNext()) { - return true; - } - return blockIterator.hasNext(); + return instructionIterator.hasNext() || blockIterator.hasNext(); } @Override @@ -39,6 +36,35 @@ } @Override + public boolean hasPrevious() { + return instructionIterator.hasPrevious() || blockIterator.hasPrevious(); + } + + @Override + public Instruction previous() { + if (instructionIterator.hasPrevious()) { + return instructionIterator.previous(); + } + if (!blockIterator.hasPrevious()) { + throw new NoSuchElementException(); + } + BasicBlock block = blockIterator.previous(); + instructionIterator = block.listIterator(block.getInstructions().size()); + assert instructionIterator.hasPrevious(); + return instructionIterator.previous(); + } + + @Override + public int nextIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int previousIndex() { + throw new UnsupportedOperationException(); + } + + @Override public void add(Instruction instruction) { instructionIterator.add(instruction); } @@ -49,6 +75,11 @@ } @Override + public void set(Instruction instruction) { + instructionIterator.set(instruction); + } + + @Override public void replaceCurrentInstruction(Instruction newInstruction) { instructionIterator.replaceCurrentInstruction(newInstruction); }
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java index 2979039..25d1fc4 100644 --- a/src/main/java/com/android/tools/r8/ir/code/If.java +++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -142,6 +142,7 @@ return super.toString() + " " + type + + (isZeroTest() ? "Z" : " ") + " block " + getTrueTarget().getNumberAsString() + " (fallthrough "
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java index 2f2b3f1..72aa3ee 100644 --- a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java +++ b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
@@ -4,7 +4,10 @@ package com.android.tools.r8.ir.code; -public interface InstructionIterator extends NextUntilIterator<Instruction> { +import java.util.ListIterator; + +public interface InstructionIterator + extends ListIterator<Instruction>, NextUntilIterator<Instruction> { /** * Replace the current instruction (aka the {@link Instruction} returned by the previous call to * {@link #next} with the passed in <code>newInstruction</code>. @@ -22,15 +25,6 @@ */ void replaceCurrentInstruction(Instruction newInstruction); - /** - * Adds an instruction. The instruction will be added just before the current - * cursor position. - * - * The instruction will be assigned to the block it is added to. - * - * @param instruction The instruction to add. - */ - void add(Instruction instruction); /** * Safe removal function that will insert a DebugLocalRead to take over the debug values if any
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java index a5e5d78..c232d3f 100644 --- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java +++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -11,9 +11,7 @@ import java.util.ListIterator; public interface InstructionListIterator - extends ListIterator<Instruction>, - NextUntilIterator<Instruction>, - PreviousUntilIterator<Instruction> { + extends InstructionIterator, PreviousUntilIterator<Instruction> { /** * Peek the previous instruction. @@ -50,27 +48,6 @@ } /** - * Safe removal function that will insert a DebugLocalRead to take over the debug values if any - * are associated with the current instruction. - */ - void removeOrReplaceByDebugLocalRead(); - - /** - * Replace the current instruction (aka the {@link Instruction} returned by the previous call to - * {@link #next} with the passed in <code>newInstruction</code>. - * - * The current instruction will be completely detached from the instruction stream with uses - * of its in-values removed. - * - * If the current instruction produces an out-value the new instruction must also produce - * an out-value, and all uses of the current instructions out-value will be replaced by the - * new instructions out-value. - * - * @param newInstruction the instruction to insert instead of the current. - */ - void replaceCurrentInstruction(Instruction newInstruction); - - /** * Split the block into two blocks at the point of the {@link ListIterator} cursor. The existing * block will have all the instructions before the cursor, and the new block all the * instructions after the cursor.
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java index de0f48c..ed51913 100644 --- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java +++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
@@ -8,7 +8,6 @@ import com.android.tools.r8.code.FillArrayDataPayload; import com.android.tools.r8.dex.Constants; import com.android.tools.r8.errors.Unreachable; -import com.android.tools.r8.graph.AppInfo; import com.android.tools.r8.graph.DexType; import com.android.tools.r8.ir.conversion.CfBuilder; import com.android.tools.r8.ir.conversion.DexBuilder; @@ -74,9 +73,8 @@ } @Override - public boolean canBeDeadCode(AppInfo appInfo, IRCode code) { - // Side-effects its input values. - return false; + public boolean instructionTypeCanThrow() { + return true; } @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java index f95b988..374acd5 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Value.java +++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -797,7 +797,17 @@ } public boolean knownToBeBoolean() { - return knownToBeBoolean; + if (knownToBeBoolean) { + return true; + } + if (getTypeLattice().isInt()) { + Value aliasedValue = getAliasedValue(); + if (!aliasedValue.isPhi() && aliasedValue.definition.isConstNumber()) { + ConstNumber definition = aliasedValue.definition.asConstNumber(); + return definition.isZero() || definition.getRawValue() == 1; + } + } + return false; } public void markAsThis(boolean receiverCanBeNull) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java index 6e5e5d7..4dcc839 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -149,7 +149,9 @@ computeInitializers(); typeVerificationHelper = new TypeVerificationHelper(code, factory, appInfo); typeVerificationHelper.computeVerificationTypes(); - splitExceptionalBlocks(); + if (!options.testing.noSplittingExceptionalEdges) { + splitExceptionalBlocks(); + } rewriter.converter.deadCodeRemover.run(code); rewriteNots(); LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, typeVerificationHelper, appInfo);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java index 4b2fabb..ec2fe4c 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -377,8 +377,11 @@ unresolvedPosition = instruction.asDebugPosition(); localsAtUnresolvedPosition = new Int2ReferenceOpenHashMap<>(locals); } - } else if (instruction.getPosition().isSome()) { - if (!isNopInstruction(instruction, nextBlock)) { + } else { + assert instruction.getPosition().isSome(); + if (instruction.isDebugLocalsChange()) { + instruction.asDebugLocalsChange().apply(locals); + } else if (!isNopInstruction(instruction, nextBlock)) { if (unresolvedPosition != null) { if (unresolvedPosition.getPosition() == instruction.getPosition() && locals.equals(localsAtUnresolvedPosition)) { @@ -389,12 +392,6 @@ } currentMaterializedPosition = instruction.getPosition(); } - } else { - // Only local-change instructions don't have positions in debug mode but fail gracefully. - assert instruction.isDebugLocalsChange(); - if (instruction.isDebugLocalsChange()) { - instruction.asDebugLocalsChange().apply(locals); - } } } }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java index 78e5e7e..881b255 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -356,6 +356,10 @@ return index; } if (dex.canThrow()) { + // TODO(zerny): Remove this from block computation. + if (dex.hasPayload()) { + arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex); + } // If the instruction can throw and is in a try block, add edges to its catch successors. Try tryRange = getTryForOffset(offset); if (tryRange != null) { @@ -398,10 +402,6 @@ builder.ensureNormalSuccessorBlock(offset, offset + dex.getSize()); return index; } - // TODO(zerny): Remove this from block computation. - if (dex.hasPayload()) { - arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex); - } // This instruction does not close the block. return -1; }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java index 4035611..045ba3e 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -1661,9 +1661,11 @@ } public void addNewArrayFilledData(int arrayRef, int elementWidth, long size, short[] data) { - add( + NewArrayFilledData instruction = new NewArrayFilledData( - readRegister(arrayRef, ValueTypeConstraint.OBJECT), elementWidth, size, data)); + readRegister(arrayRef, ValueTypeConstraint.OBJECT), elementWidth, size, data); + assert instruction.instructionTypeCanThrow(); + addInstruction(instruction); } public void addNewInstance(int dest, DexType type) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java index e96acfb..7b55543 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -879,6 +879,10 @@ printC1VisualizerHeader(method); String previous = printMethod(code, "Initial IR (SSA)", null); + if (options.testing.irModifier != null) { + options.testing.irModifier.accept(code); + } + if (options.canHaveArtStringNewInitBug()) { CodeRewriter.ensureDirectStringNewToInit(code); } @@ -993,6 +997,7 @@ codeRewriter.rewriteSwitch(code); codeRewriter.processMethodsNeverReturningNormally(code); codeRewriter.simplifyIf(code); + codeRewriter.redundantConstNumberRemoval(code); new RedundantFieldLoadElimination(appInfo, code, enableWholeProgramOptimizations).run(); if (options.testing.invertConditionals) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java index 62cddc9..cf88f0f 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -210,7 +210,15 @@ iterator.replaceCurrentInstruction(newInvoke); if (constantReturnMaterializingInstruction != null) { - iterator.add(constantReturnMaterializingInstruction); + if (block.hasCatchHandlers()) { + // Split the block to ensure no instructions after throwing instructions. + iterator + .split(code, blocks) + .listIterator() + .add(constantReturnMaterializingInstruction); + } else { + iterator.add(constantReturnMaterializingInstruction); + } } DexType actualReturnType = actualTarget.proto.returnType;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java index 5938781..3227d45 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -26,6 +26,7 @@ import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialInstanceInitializer; import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexItemFactory.ThrowableMethods; import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.graph.DexProto; import com.android.tools.r8.graph.DexString; @@ -120,6 +121,8 @@ import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntMap; @@ -2701,6 +2704,9 @@ if (contents == null) { continue; } + if (block.hasCatchHandlers()) { + continue; + } int arraySize = newArray.size().getConstInstruction().asConstNumber().getIntValue(); NewArrayFilledData fillArray = new NewArrayFilledData(newArray.outValue(), elementSize, arraySize, contents); @@ -3141,6 +3147,210 @@ simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(cond)); } + /** + * This optimization exploits that we can sometimes learn the constant value of an SSA value that + * flows into an if-eq of if-neq instruction. + * + * <p>Consider the following example: + * + * <pre> + * 1. if (obj != null) { + * 2. return doStuff(); + * 3. } + * 4. return null; + * </pre> + * + * <p>Since we know that `obj` is null in all blocks that are dominated by the false-target of the + * if-instruction in line 1, we can safely replace the null-constant in line 4 by `obj`, and + * thereby save a const-number instruction. + */ + public void redundantConstNumberRemoval(IRCode code) { + Supplier<Long2ReferenceMap<List<ConstNumber>>> constantsByValue = + Suppliers.memoize(() -> getConstantsByValue(code)); + Supplier<DominatorTree> dominatorTree = Suppliers.memoize(() -> new DominatorTree(code)); + + boolean changed = false; + for (BasicBlock block : code.blocks) { + Instruction lastInstruction = block.getInstructions().getLast(); + if (!lastInstruction.isIf()) { + continue; + } + + If ifInstruction = lastInstruction.asIf(); + Type type = ifInstruction.getType(); + + Value lhs = ifInstruction.inValues().get(0); + Value rhs = !ifInstruction.isZeroTest() ? ifInstruction.inValues().get(1) : null; + + if (!ifInstruction.isZeroTest() && !lhs.isConstNumber() && !rhs.isConstNumber()) { + // We can only conclude anything from an if-instruction if it is a zero-test or if one of + // the two operands is a constant. + continue; + } + + // If the type is neither EQ nor NE, we cannot conclude anything about any of the in-values + // of the if-instruction from the outcome of the if-instruction. + if (type != Type.EQ && type != Type.NE) { + continue; + } + + BasicBlock trueTarget, falseTarget; + if (type == Type.EQ) { + trueTarget = ifInstruction.getTrueTarget(); + falseTarget = ifInstruction.fallthroughBlock(); + } else { + falseTarget = ifInstruction.getTrueTarget(); + trueTarget = ifInstruction.fallthroughBlock(); + } + + if (ifInstruction.isZeroTest()) { + changed |= + replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, dominatorTree); + if (lhs.knownToBeBoolean()) { + changed |= + replaceDominatedConstNumbers(1, lhs, falseTarget, constantsByValue, dominatorTree); + } + } else { + assert rhs != null; + if (lhs.isConstNumber()) { + ConstNumber lhsAsNumber = lhs.getConstInstruction().asConstNumber(); + changed |= + replaceDominatedConstNumbers( + lhsAsNumber.getRawValue(), rhs, trueTarget, constantsByValue, dominatorTree); + if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) { + changed |= + replaceDominatedConstNumbers( + negateBoolean(lhsAsNumber), rhs, falseTarget, constantsByValue, dominatorTree); + } + } else { + assert rhs.isConstNumber(); + ConstNumber rhsAsNumber = rhs.getConstInstruction().asConstNumber(); + changed |= + replaceDominatedConstNumbers( + rhsAsNumber.getRawValue(), lhs, trueTarget, constantsByValue, dominatorTree); + if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) { + changed |= + replaceDominatedConstNumbers( + negateBoolean(rhsAsNumber), lhs, falseTarget, constantsByValue, dominatorTree); + } + } + } + + if (constantsByValue.get().isEmpty()) { + break; + } + } + + if (changed) { + code.removeAllTrivialPhis(); + } + assert code.isConsistentSSA(); + } + + private static Long2ReferenceMap<List<ConstNumber>> getConstantsByValue(IRCode code) { + // A map from the raw value of constants in `code` to the const number instructions that define + // the given raw value (irrespective of the type of the raw value). + Long2ReferenceMap<List<ConstNumber>> constantsByValue = new Long2ReferenceOpenHashMap<>(); + + // Initialize `constantsByValue`. + Iterable<Instruction> instructions = code::instructionIterator; + for (Instruction instruction : instructions) { + if (instruction.isConstNumber()) { + ConstNumber constNumber = instruction.asConstNumber(); + if (constNumber.outValue().hasLocalInfo()) { + // Not necessarily constant, because it could be changed in the debugger. + continue; + } + long rawValue = constNumber.getRawValue(); + if (constantsByValue.containsKey(rawValue)) { + constantsByValue.get(rawValue).add(constNumber); + } else { + List<ConstNumber> list = new ArrayList<>(); + list.add(constNumber); + constantsByValue.put(rawValue, list); + } + } + } + return constantsByValue; + } + + private static int negateBoolean(ConstNumber number) { + assert number.outValue().knownToBeBoolean(); + return number.getRawValue() == 0 ? 1 : 0; + } + + private boolean replaceDominatedConstNumbers( + long withValue, + Value newValue, + BasicBlock dominator, + Supplier<Long2ReferenceMap<List<ConstNumber>>> constantsByValueSupplier, + Supplier<DominatorTree> dominatorTree) { + if (newValue.hasLocalInfo()) { + // We cannot replace a constant with a value that has local info, because that could change + // debugging behavior. + return false; + } + + Long2ReferenceMap<List<ConstNumber>> constantsByValue = constantsByValueSupplier.get(); + List<ConstNumber> constantsWithValue = constantsByValue.get(withValue); + if (constantsWithValue == null || constantsWithValue.isEmpty()) { + return false; + } + + boolean changed = false; + + ListIterator<ConstNumber> constantWithValueIterator = constantsWithValue.listIterator(); + while (constantWithValueIterator.hasNext()) { + ConstNumber constNumber = constantWithValueIterator.next(); + Value value = constNumber.outValue(); + assert !value.hasLocalInfo(); + assert constNumber.getRawValue() == withValue; + + BasicBlock block = constNumber.getBlock(); + + // If the following condition does not hold, then the if-instruction does not dominate the + // block containing the constant, although the true or false target does. + if (block == dominator && block.getPredecessors().size() != 1) { + // This should generally not happen, but it is possible to write bytecode where it does. + assert false; + continue; + } + + if (value.knownToBeBoolean() && !newValue.knownToBeBoolean()) { + // We cannot replace a boolean by a none-boolean since that can lead to verification + // errors. For example, the following code fails with "register v1 has type Imprecise + // Constant: 127 but expected Boolean return-1nr". + // + // public boolean convertIntToBoolean(int v1) { + // const/4 v0, 0x1 + // if-eq v1, v0, :eq_true + // const/4 v1, 0x0 + // :eq_true + // return v1 + // } + continue; + } + + if (dominatorTree.get().dominatedBy(block, dominator)) { + if (newValue.getTypeLattice().lessThanOrEqual(value.getTypeLattice(), appInfo)) { + value.replaceUsers(newValue); + block.listIterator(constNumber).removeOrReplaceByDebugLocalRead(); + constantWithValueIterator.remove(); + changed = true; + } else if (value.getTypeLattice().isNullType()) { + // TODO(b/120257211): Need a mechanism to determine if `newValue` can be used at all of + // the use sites of `value` without introducing a type error. + } + } + } + + if (constantsWithValue.isEmpty()) { + constantsByValue.remove(withValue); + } + + return changed; + } + // Find all method invocations that never returns normally, split the block // after each such invoke instruction and follow it with a block throwing a // null value (which should result in NPE). Note that this throw is not @@ -3436,7 +3646,7 @@ // Note that addSuppressed() and getSuppressed() methods are final in // Throwable, so these changes don't have to worry about overrides. public void rewriteThrowableAddAndGetSuppressed(IRCode code) { - DexItemFactory.ThrowableMethods throwableMethods = dexItemFactory.throwableMethods; + ThrowableMethods throwableMethods = dexItemFactory.throwableMethods; for (BasicBlock block : code.blocks) { InstructionListIterator iterator = block.listIterator(); @@ -3576,7 +3786,7 @@ } else { // Insert "if (argument != null) ...". successor = block.unlinkSingleSuccessor(); - If theIf = new If(If.Type.NE, argument); + If theIf = new If(Type.NE, argument); theIf.setPosition(position); BasicBlock ifBlock = BasicBlock.createIfBlock(code.blocks.size(), theIf); code.blocks.add(ifBlock);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java index 4da9f8d..99f9731 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -17,11 +17,13 @@ import com.android.tools.r8.ir.analysis.type.Nullability; import com.android.tools.r8.ir.analysis.type.TypeAnalysis; import com.android.tools.r8.ir.analysis.type.TypeLatticeElement; +import com.android.tools.r8.ir.code.BasicBlock; import com.android.tools.r8.ir.code.ConstNumber; import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.InstancePut; import com.android.tools.r8.ir.code.Instruction; import com.android.tools.r8.ir.code.InstructionIterator; +import com.android.tools.r8.ir.code.InstructionListIterator; import com.android.tools.r8.ir.code.InvokeMethod; import com.android.tools.r8.ir.code.StaticGet; import com.android.tools.r8.ir.code.StaticPut; @@ -29,6 +31,7 @@ import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness; import com.android.tools.r8.shaking.ProguardMemberRule; import com.google.common.collect.Sets; +import java.util.ListIterator; import java.util.Set; import java.util.function.Predicate; @@ -133,151 +136,160 @@ public void rewriteWithConstantValues( IRCode code, DexType callingContext, Predicate<DexEncodedMethod> isProcessedConcurrently) { Set<Value> affectedValues = Sets.newIdentityHashSet(); - InstructionIterator iterator = code.instructionIterator(); - while (iterator.hasNext()) { - Instruction current = iterator.next(); - if (current.isInvokeMethod()) { - InvokeMethod invoke = current.asInvokeMethod(); - DexMethod invokedMethod = invoke.getInvokedMethod(); - DexType invokedHolder = invokedMethod.getHolder(); - if (!invokedHolder.isClassType()) { - continue; - } - // TODO(70550443): Maybe check all methods here. - DexEncodedMethod definition = appInfo - .lookup(invoke.getType(), invokedMethod, callingContext); + ListIterator<BasicBlock> blocks = code.blocks.listIterator(); + while (blocks.hasNext()) { + BasicBlock block = blocks.next(); + InstructionListIterator iterator = block.listIterator(); + while (iterator.hasNext()) { + Instruction current = iterator.next(); + if (current.isInvokeMethod()) { + InvokeMethod invoke = current.asInvokeMethod(); + DexMethod invokedMethod = invoke.getInvokedMethod(); + DexType invokedHolder = invokedMethod.getHolder(); + if (!invokedHolder.isClassType()) { + continue; + } + // TODO(70550443): Maybe check all methods here. + DexEncodedMethod definition = appInfo + .lookup(invoke.getType(), invokedMethod, callingContext); - boolean invokeReplaced = false; - ProguardMemberRuleLookup lookup = lookupMemberRule(definition); - if (lookup != null) { - if (lookup.type == RuleType.ASSUME_NO_SIDE_EFFECTS - && (invoke.outValue() == null || !invoke.outValue().isUsed())) { - // Remove invoke if marked as having no side effects and the return value is not used. - iterator.remove(); - invokeReplaced = true; - } else if (invoke.outValue() != null && invoke.outValue().isUsed()) { - // Check to see if a constant value can be assumed. - Instruction replacement = - constantReplacementFromProguardRule(lookup.rule, code, invoke); - if (replacement != null) { - affectedValues.add(replacement.outValue()); - replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement); + boolean invokeReplaced = false; + ProguardMemberRuleLookup lookup = lookupMemberRule(definition); + if (lookup != null) { + if (lookup.type == RuleType.ASSUME_NO_SIDE_EFFECTS + && (invoke.outValue() == null || !invoke.outValue().isUsed())) { + // Remove invoke if marked as having no side effects and the return value is not used. + iterator.remove(); invokeReplaced = true; - } else { - // Check to see if a value range can be assumed. - setValueRangeFromProguardRule(lookup.rule, current.outValue()); + } else if (invoke.outValue() != null && invoke.outValue().isUsed()) { + // Check to see if a constant value can be assumed. + Instruction replacement = + constantReplacementFromProguardRule(lookup.rule, code, invoke); + if (replacement != null) { + affectedValues.add(replacement.outValue()); + replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement); + invokeReplaced = true; + } else { + // Check to see if a value range can be assumed. + setValueRangeFromProguardRule(lookup.rule, current.outValue()); + } } } - } - // If no Proguard rule could replace the instruction check for knowledge about the - // return value. - if (!invokeReplaced && invoke.outValue() != null) { - DexEncodedMethod target = invoke.lookupSingleTarget(appInfo, callingContext); - if (target != null) { - if (target.getOptimizationInfo().neverReturnsNull() && invoke.outValue().canBeNull()) { - Value knownToBeNonNullValue = invoke.outValue(); - knownToBeNonNullValue.markNeverNull(); - TypeLatticeElement typeLattice = knownToBeNonNullValue.getTypeLattice(); - assert typeLattice.isNullable() && typeLattice.isReference(); - knownToBeNonNullValue.narrowing(appInfo, typeLattice.asNonNullable()); - affectedValues.addAll(knownToBeNonNullValue.affectedValues()); - } - if (target.getOptimizationInfo().returnsConstant()) { - long constant = target.getOptimizationInfo().getReturnedConstant(); - ConstNumber replacement = createConstNumberReplacement( - code, constant, invoke.outValue().getTypeLattice(), invoke.getLocalInfo()); - affectedValues.add(replacement.outValue()); - invoke.outValue().replaceUsers(replacement.outValue()); - invoke.setOutValue(null); - replacement.setPosition(invoke.getPosition()); - invoke.moveDebugValues(replacement); - iterator.add(replacement); - } - } - } - } else if (current.isInstancePut()) { - InstancePut instancePut = current.asInstancePut(); - DexField field = instancePut.getField(); - DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field); - if (target != null) { - // Remove writes to dead (i.e. never read) fields. - if (!isFieldRead(target, false) && instancePut.object().isNeverNull()) { - iterator.remove(); - } - } - } else if (current.isStaticGet()) { - StaticGet staticGet = current.asStaticGet(); - DexField field = staticGet.getField(); - DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field); - ProguardMemberRuleLookup lookup = null; - if (target != null) { - // Check if a this value is known const. - Instruction replacement = target.valueAsConstInstruction(appInfo, staticGet.dest()); - if (replacement == null) { - lookup = lookupMemberRule(target); - if (lookup != null) { - replacement = constantReplacementFromProguardRule(lookup.rule, code, staticGet); - } - } - if (replacement == null) { - // If no const replacement was found, at least store the range information. - if (lookup != null) { - setValueRangeFromProguardRule(lookup.rule, staticGet.dest()); - } - } - if (replacement != null) { - affectedValues.add(replacement.outValue()); - // Ignore assumenosideeffects for fields. - if (lookup != null && lookup.type == RuleType.ASSUME_VALUES) { - replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement); - } else { - iterator.replaceCurrentInstruction(replacement); - } - } else if (staticGet.dest() != null) { - // In case the class holder of this static field satisfying following criteria: - // -- cannot trigger other static initializer except for its own - // -- is final - // -- has a class initializer which is classified as trivial - // (see CodeRewriter::computeClassInitializerInfo) and - // initializes the field being accessed - // - // ... and the field itself is not pinned by keep rules (in which case it might - // be updated outside the class constructor, e.g. via reflections), it is safe - // to assume that the static-get instruction reads the value it initialized value - // in class initializer and is never null. - // - DexClass holderDefinition = appInfo.definitionFor(field.getHolder()); - if (holderDefinition != null - && holderDefinition.accessFlags.isFinal() - && !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) { - Value outValue = staticGet.dest(); - DexEncodedMethod classInitializer = holderDefinition.getClassInitializer(); - if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) { - TrivialInitializer info = - classInitializer.getOptimizationInfo().getTrivialInitializerInfo(); - if (info != null - && ((TrivialClassInitializer) info).field == field - && !appInfo.isPinned(field) - && outValue.canBeNull()) { - outValue.markNeverNull(); - TypeLatticeElement typeLattice = outValue.getTypeLattice(); - assert typeLattice.isNullable() && typeLattice.isReference(); - outValue.narrowing(appInfo, typeLattice.asNonNullable()); - affectedValues.addAll(outValue.affectedValues()); + // If no Proguard rule could replace the instruction check for knowledge about the + // return value. + if (!invokeReplaced && invoke.outValue() != null) { + DexEncodedMethod target = invoke.lookupSingleTarget(appInfo, callingContext); + if (target != null) { + if (target.getOptimizationInfo().neverReturnsNull() && invoke.outValue() + .canBeNull()) { + Value knownToBeNonNullValue = invoke.outValue(); + knownToBeNonNullValue.markNeverNull(); + TypeLatticeElement typeLattice = knownToBeNonNullValue.getTypeLattice(); + assert typeLattice.isNullable() && typeLattice.isReference(); + knownToBeNonNullValue.narrowing(appInfo, typeLattice.asNonNullable()); + affectedValues.addAll(knownToBeNonNullValue.affectedValues()); + } + if (target.getOptimizationInfo().returnsConstant()) { + long constant = target.getOptimizationInfo().getReturnedConstant(); + ConstNumber replacement = createConstNumberReplacement( + code, constant, invoke.outValue().getTypeLattice(), invoke.getLocalInfo()); + affectedValues.add(replacement.outValue()); + invoke.outValue().replaceUsers(replacement.outValue()); + invoke.setOutValue(null); + replacement.setPosition(invoke.getPosition()); + invoke.moveDebugValues(replacement); + if (current.getBlock().hasCatchHandlers()) { + iterator.split(code, blocks).listIterator().add(replacement); + } else { + iterator.add(replacement); } } } } - } - } else if (current.isStaticPut()) { - StaticPut staticPut = current.asStaticPut(); - DexField field = staticPut.getField(); - DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field); - if (target != null) { - // Remove writes to dead (i.e. never read) fields. - if (!isFieldRead(target, true)) { - iterator.removeOrReplaceByDebugLocalRead(); + } else if (current.isInstancePut()) { + InstancePut instancePut = current.asInstancePut(); + DexField field = instancePut.getField(); + DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field); + if (target != null) { + // Remove writes to dead (i.e. never read) fields. + if (!isFieldRead(target, false) && instancePut.object().isNeverNull()) { + iterator.remove(); + } + } + } else if (current.isStaticGet()) { + StaticGet staticGet = current.asStaticGet(); + DexField field = staticGet.getField(); + DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field); + ProguardMemberRuleLookup lookup = null; + if (target != null) { + // Check if a this value is known const. + Instruction replacement = target.valueAsConstInstruction(appInfo, staticGet.dest()); + if (replacement == null) { + lookup = lookupMemberRule(target); + if (lookup != null) { + replacement = constantReplacementFromProguardRule(lookup.rule, code, staticGet); + } + } + if (replacement == null) { + // If no const replacement was found, at least store the range information. + if (lookup != null) { + setValueRangeFromProguardRule(lookup.rule, staticGet.dest()); + } + } + if (replacement != null) { + affectedValues.add(replacement.outValue()); + // Ignore assumenosideeffects for fields. + if (lookup != null && lookup.type == RuleType.ASSUME_VALUES) { + replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement); + } else { + iterator.replaceCurrentInstruction(replacement); + } + } else if (staticGet.dest() != null) { + // In case the class holder of this static field satisfying following criteria: + // -- cannot trigger other static initializer except for its own + // -- is final + // -- has a class initializer which is classified as trivial + // (see CodeRewriter::computeClassInitializerInfo) and + // initializes the field being accessed + // + // ... and the field itself is not pinned by keep rules (in which case it might + // be updated outside the class constructor, e.g. via reflections), it is safe + // to assume that the static-get instruction reads the value it initialized value + // in class initializer and is never null. + // + DexClass holderDefinition = appInfo.definitionFor(field.getHolder()); + if (holderDefinition != null + && holderDefinition.accessFlags.isFinal() + && !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) { + Value outValue = staticGet.dest(); + DexEncodedMethod classInitializer = holderDefinition.getClassInitializer(); + if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) { + TrivialInitializer info = + classInitializer.getOptimizationInfo().getTrivialInitializerInfo(); + if (info != null + && ((TrivialClassInitializer) info).field == field + && !appInfo.isPinned(field) + && outValue.canBeNull()) { + outValue.markNeverNull(); + TypeLatticeElement typeLattice = outValue.getTypeLattice(); + assert typeLattice.isNullable() && typeLattice.isReference(); + outValue.narrowing(appInfo, typeLattice.asNonNullable()); + affectedValues.addAll(outValue.affectedValues()); + } + } + } + } + } + } else if (current.isStaticPut()) { + StaticPut staticPut = current.asStaticPut(); + DexField field = staticPut.getField(); + DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field); + if (target != null) { + // Remove writes to dead (i.e. never read) fields. + if (!isFieldRead(target, true)) { + iterator.removeOrReplaceByDebugLocalRead(); + } } } }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java index c46504c..5e32629 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -6,6 +6,7 @@ import com.android.tools.r8.graph.DebugLocalInfo; import com.android.tools.r8.ir.code.BasicBlock; import com.android.tools.r8.ir.code.ConstNumber; +import com.android.tools.r8.ir.code.DebugLocalsChange; import com.android.tools.r8.ir.code.Goto; import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.Instruction; @@ -22,12 +23,14 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Objects; +import java.util.Set; public class PeepholeOptimizer { /** @@ -36,10 +39,159 @@ public static void optimize(IRCode code, LinearScanRegisterAllocator allocator) { removeIdenticalPredecessorBlocks(code, allocator); removeRedundantInstructions(code, allocator); + shareIdenticalBlockPrefix(code, allocator); shareIdenticalBlockSuffix(code, allocator, 0); assert code.isConsistentGraph(); } + /** Identify common prefixes in successor blocks and share them. */ + private static void shareIdenticalBlockPrefix(IRCode code, RegisterAllocator allocator) { + InstructionEquivalence equivalence = new InstructionEquivalence(allocator); + Set<BasicBlock> blocksToBeRemoved = new HashSet<>(); + for (BasicBlock block : code.blocks) { + if (blocksToBeRemoved.contains(block)) { + // This block has already been removed entirely. + continue; + } + + List<BasicBlock> normalSuccessors = block.getNormalSuccessors(); + if (normalSuccessors.size() != 2) { + continue; + } + + // Exactly two normal successors. + BasicBlock normalSuccessor = normalSuccessors.get(0); + BasicBlock otherNormalSuccessor = normalSuccessors.get(1); + + // Ensure that the current block is on all paths to the two successor blocks. + if (normalSuccessor.getPredecessors().size() != 1 + || otherNormalSuccessor.getPredecessors().size() != 1) { + continue; + } + + // Only try to share instructions if the two successors have the same locals state on entry. + if (!Objects.equals( + normalSuccessor.getLocalsAtEntry(), otherNormalSuccessor.getLocalsAtEntry())) { + continue; + } + + // Share instructions from the two normal successors one-by-one. + while (true) { + // Check if all instructions were already removed from the two successors. + if (normalSuccessor.isEmpty() || otherNormalSuccessor.isEmpty()) { + assert blocksToBeRemoved.contains(normalSuccessor); + assert blocksToBeRemoved.contains(otherNormalSuccessor); + break; + } + + Instruction instruction = normalSuccessor.entry(); + Instruction otherInstruction = otherNormalSuccessor.entry(); + + // If the two instructions are not the same, we cannot merge them into the predecessor. + if (!equivalence.doEquivalent(instruction, otherInstruction)) { + break; + } + + // Each block with one or more catch handlers may have at most one throwing instruction. + if (instruction.instructionTypeCanThrow() && block.hasCatchHandlers()) { + break; + } + + // If the instruction can throw and one of the normal successor blocks has a catch handler, + // then we cannot merge the instruction into the predecessor, since this would change the + // exceptional control flow. + if (instruction.instructionInstanceCanThrow() + && (normalSuccessor.hasCatchHandlers() || otherNormalSuccessor.hasCatchHandlers())) { + break; + } + + // Check for commutativity (semantics). If the instruction writes to a register, then we + // need to make sure that the instruction commutes with the last instruction in the + // predecessor. Consider the following example. + // + // <Block A> + // if-eqz r0 then goto Block B else goto Block C + // / \ + // <Block B> <Block C> + // const r0, 1 const r0, 1 + // ... ... + // + // In the example, it is not possible to change the order of "if-eqz r0" and + // "const r0, 1" without changing the semantics. + if (instruction.outValue() != null && instruction.outValue().needsRegister()) { + int destinationRegister = + allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber()); + boolean commutative = + block.exit().inValues().stream() + .allMatch( + inValue -> { + int operandRegister = + allocator.getRegisterForValue(inValue, block.exit().getNumber()); + for (int i = 0; i < instruction.outValue().requiredRegisters(); i++) { + for (int j = 0; j < inValue.requiredRegisters(); j++) { + if (destinationRegister + i == operandRegister + j) { + // Overlap detected, the two instructions do not commute. + return false; + } + } + } + return true; + }); + if (!commutative) { + break; + } + } + + // Check for commutativity (debug info). + if (!instruction.getPosition().equals(block.exit().getPosition()) + && !(block.exit().getPosition().isNone() && !block.exit().getDebugValues().isEmpty())) { + break; + } + + // Remove the instruction from the two normal successors. + normalSuccessor.getInstructions().removeFirst(); + otherNormalSuccessor.getInstructions().removeFirst(); + + // Move the instruction into the predecessor. + if (instruction.isJumpInstruction()) { + // Replace jump instruction in predecessor with the jump instruction from the two normal + // successors. + LinkedList<Instruction> instructions = block.getInstructions(); + instructions.removeLast(); + instructions.add(instruction); + instruction.setBlock(block); + + // Update successors of predecessor block. + block.detachAllSuccessors(); + for (BasicBlock newNormalSuccessor : normalSuccessor.getNormalSuccessors()) { + block.link(newNormalSuccessor); + } + + // Detach the two normal successors from the rest of the graph. + normalSuccessor.detachAllSuccessors(); + otherNormalSuccessor.detachAllSuccessors(); + + // Record that the two normal successors should be removed entirely. + blocksToBeRemoved.add(normalSuccessor); + blocksToBeRemoved.add(otherNormalSuccessor); + } else { + // Insert instruction before the jump instruction in the predecessor. + block.getInstructions().listIterator(block.getInstructions().size() - 1).add(instruction); + instruction.setBlock(block); + + // Update locals-at-entry if needed. + if (instruction.isDebugLocalsChange()) { + DebugLocalsChange localsChange = instruction.asDebugLocalsChange(); + localsChange.apply(normalSuccessor.getLocalsAtEntry()); + localsChange.apply(otherNormalSuccessor.getLocalsAtEntry()); + } + } + } + } + + code.blocks.removeAll(blocksToBeRemoved); + } + /** Identify common suffixes in predecessor blocks and share them. */ public static void shareIdenticalBlockSuffix( IRCode code, RegisterAllocator allocator, int overhead) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java index 9d2bb32..061907f 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -211,7 +211,9 @@ int argumentCount = method.method.proto.parameters.size() + (method.accessFlags.isStatic() ? 0 : 1); // TODO(65810338): Implement for virtual methods as well. - if (method.accessFlags.isPrivate() || method.accessFlags.isStatic()) { + if (method.accessFlags.isPrivate() + || method.accessFlags.isStatic() + || method.isInstanceInitializer()) { CollectUsedArguments collector = new CollectUsedArguments(); if (!method.accessFlags.isStatic()) { // TODO(65810338): The receiver cannot be removed without transforming the method to being
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java index 59d8717..f8ddcb6 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -291,7 +291,7 @@ // invoke-virtual { s1, ... } mtd1 // goto Exit // b2: - // s2 <- static-get singleoton + // s2 <- static-get singleton // ... // invoke-virtual { s2, ... } mtd1 // goto Exit @@ -503,7 +503,7 @@ } } - if (!methodMapping.isEmpty() || fieldMapping.isEmpty()) { + if (!methodMapping.isEmpty() || !fieldMapping.isEmpty()) { classStaticizer.converter.appView.setGraphLense( new ClassStaticizerGraphLense( classStaticizer.converter.graphLense(),
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java index 1ac8555..a453346 100644 --- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java +++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -28,6 +28,7 @@ import com.android.tools.r8.ir.code.NumericType; import com.android.tools.r8.ir.code.Or; import com.android.tools.r8.ir.code.Phi; +import com.android.tools.r8.ir.code.Position; import com.android.tools.r8.ir.code.StackValue; import com.android.tools.r8.ir.code.StackValues; import com.android.tools.r8.ir.code.Sub; @@ -423,7 +424,8 @@ // Compute the final change in locals and insert it before nextInstruction. boolean localsChanged = !ending.isEmpty() || !starting.isEmpty(); if (localsChanged) { - DebugLocalsChange change = createLocalsChange(ending, starting); + DebugLocalsChange change = + createLocalsChange(ending, starting, instruction.getPosition()); if (change != null) { // Insert the DebugLocalsChange instruction before nextInstruction. instructionIterator.add(change); @@ -506,36 +508,43 @@ starting.put(finalLocal.getIntKey(), finalLocal.getValue()); } } - DebugLocalsChange change = createLocalsChange(ending, starting); + DebugLocalsChange change = createLocalsChange(ending, starting, block.getPosition()); if (change != null) { instructionIterator.add(change); } } private static DebugLocalsChange createLocalsChange( - Int2ReferenceMap<DebugLocalInfo> ending, Int2ReferenceMap<DebugLocalInfo> starting) { + Int2ReferenceMap<DebugLocalInfo> ending, + Int2ReferenceMap<DebugLocalInfo> starting, + Position position) { + assert position.isSome(); if (ending.isEmpty() && starting.isEmpty()) { return null; } + DebugLocalsChange localsChange; if (ending.isEmpty() || starting.isEmpty()) { - return new DebugLocalsChange(ending, starting); - } - IntSet unneeded = new IntArraySet(Math.min(ending.size(), starting.size())); - for (Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) { - if (starting.get(entry.getIntKey()) == entry.getValue()) { - unneeded.add(entry.getIntKey()); + localsChange = new DebugLocalsChange(ending, starting); + } else { + IntSet unneeded = new IntArraySet(Math.min(ending.size(), starting.size())); + for (Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) { + if (starting.get(entry.getIntKey()) == entry.getValue()) { + unneeded.add(entry.getIntKey()); + } } + if (unneeded.size() == ending.size() && unneeded.size() == starting.size()) { + return null; + } + IntIterator iterator = unneeded.iterator(); + while (iterator.hasNext()) { + int key = iterator.nextInt(); + ending.remove(key); + starting.remove(key); + } + localsChange = new DebugLocalsChange(ending, starting); } - if (unneeded.size() == ending.size() && unneeded.size() == starting.size()) { - return null; - } - IntIterator iterator = unneeded.iterator(); - while (iterator.hasNext()) { - int key = iterator.nextInt(); - ending.remove(key); - starting.remove(key); - } - return new DebugLocalsChange(ending, starting); + localsChange.setPosition(position); + return localsChange; } private void clearState() {
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 47d3743..709d27a 100644 --- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java +++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -218,7 +218,7 @@ } TextPosition optionStart = getPosition(); expectChar('-'); - if (parseIgnoredOption() || + if (parseIgnoredOption(optionStart) || parseIgnoredOptionAndWarn(optionStart) || parseUnsupportedOptionAndErr(optionStart)) { // Intentionally left empty. @@ -401,13 +401,22 @@ && (unknownOption.equals("forceinline") || unknownOption.equals("neverinline"))) { devMessage = ", this option needs to be turned on explicitly if used for tests."; } - reporter.error(new StringDiagnostic( - "Unknown option \"-" + unknownOption + "\"" + devMessage, - origin, getPosition(optionStart))); + unknownOption(unknownOption, optionStart, devMessage); } return true; } + private void unknownOption(String unknownOption, TextPosition optionStart) { + unknownOption(unknownOption, optionStart, ""); + } + + private void unknownOption( + String unknownOption, TextPosition optionStart, String additionalMessage) { + throw reporter.fatalError((new StringDiagnostic( + "Unknown option \"-" + unknownOption + "\"" + additionalMessage, + origin, getPosition(optionStart)))); + } + private boolean parseUnsupportedOptionAndErr(TextPosition optionStart) { String option = Iterables.find(UNSUPPORTED_FLAG_OPTIONS, this::skipFlag, null); if (option != null) { @@ -438,13 +447,13 @@ return true; } - private boolean parseIgnoredOption() { + private boolean parseIgnoredOption(TextPosition optionStart) { return Iterables.any(IGNORED_SINGLE_ARG_OPTIONS, this::skipOptionWithSingleArg) || Iterables.any(IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS, this::skipOptionWithOptionalSingleArg) || Iterables.any(IGNORED_FLAG_OPTIONS, this::skipFlag) || Iterables.any(IGNORED_CLASS_DESCRIPTOR_OPTIONS, this::skipOptionWithClassSpec) - || parseOptimizationOption(); + || parseOptimizationOption(optionStart); } private void parseInclude() throws ProguardRuleParserException { @@ -533,10 +542,11 @@ } - private boolean parseOptimizationOption() { + private boolean parseOptimizationOption(TextPosition optionStart) { if (!acceptString("optimizations")) { return false; } + warnIgnoringOptions("optimizations", optionStart); do { skipWhitespace(); skipOptimizationName(); @@ -768,14 +778,19 @@ TextPosition start = getPosition(); acceptString("-"); String unknownOption = acceptString(); - throw reporter.fatalError(new StringDiagnostic( - "Unknown option \"-" + unknownOption + "\"", - origin, - start)); + unknownOption(unknownOption, start); } } else { builder.setType(ProguardKeepRuleType.KEEP); } + if (!eof() && !Character.isWhitespace(peekChar()) && peekChar() != ',') { + // The only path to here is through "-keep" with an unsupported suffix. + unacceptString("-keep"); + TextPosition start = getPosition(); + acceptString("-"); + String unknownOption = acceptString(); + unknownOption(unknownOption, start); + } parseRuleModifiers(builder); }
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java index 46d3a68..898d06c 100644 --- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java +++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -885,6 +885,8 @@ if (context instanceof ProguardKeepRule) { if (item.isDexEncodedMethod() && item.asDexEncodedMethod().accessFlags.isSynthetic()) { // Don't keep synthetic methods (see b/120971047 for additional details). + // TODO(b/122819537): need to distinguish lambda desugared synthetic methods v.s. kotlinc + // synthetic methods? return; }
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java index 56dc071..2499ccb 100644 --- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java +++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -288,6 +288,7 @@ * @return a class descriptor i.e. "Ljava/lang/Object;" */ public static String getDescriptorFromClassBinaryName(String typeBinaryName) { + assert typeBinaryName != null; return ('L' + typeBinaryName + ';'); }
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java index 3e4a44d..78d47c2 100644 --- a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java +++ b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
@@ -9,6 +9,8 @@ import com.android.tools.r8.origin.Origin; import com.android.tools.r8.position.Position; import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.file.FileAlreadyExistsException; import java.nio.file.NoSuchFileException; @@ -45,6 +47,11 @@ if (e instanceof FileAlreadyExistsException) { return "File already exists: " + e.getMessage(); } - return e.getMessage(); + StringWriter stack = new StringWriter(); + e.printStackTrace(new PrintWriter(stack)); + String message = e.getMessage(); + return message != null + ? StringUtils.joinLines(message, "Stack trace:", stack.toString()) + : StringUtils.joinLines(stack.toString()); } }
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java index ee11bcf..d77adfd 100644 --- a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java +++ b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
@@ -11,8 +11,6 @@ import com.android.tools.r8.origin.Origin; import com.android.tools.r8.origin.PathOrigin; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.nio.file.FileSystemException; import java.nio.file.Paths; import java.util.function.Consumer; @@ -67,14 +65,7 @@ } catch (ResourceException e) { throw reporter.fatalError(new ExceptionDiagnostic(e, e.getOrigin())); } catch (AssertionError e) { - // Most of our assertions don't have a message, create a wrapper that has the stack as the - // message. - if (e.getMessage() == null) { - StringWriter stack = new StringWriter(); - e.printStackTrace(new PrintWriter(stack)); - e = new AssertionError(stack.toString(), e); - } - throw reporter.fatalError(new ExceptionDiagnostic(e, Origin.unknown()), e); + throw reporter.fatalError(new ExceptionDiagnostic(e, Origin.unknown())); } reporter.failIfPendingErrors(); } catch (AbortException e) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java index 4921c79..f34e059 100644 --- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java +++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -19,6 +19,7 @@ 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.optimize.Inliner; import com.android.tools.r8.origin.Origin; import com.android.tools.r8.shaking.ProguardConfiguration; @@ -532,6 +533,8 @@ public boolean noLocalsTableOnInput = false; public boolean forceNameReflectionOptimization = false; public boolean disallowLoadStoreOptimization = false; + public Consumer<IRCode> irModifier = null; + public boolean noSplittingExceptionalEdges = false; } private boolean hasMinApi(AndroidApiLevel level) {
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java index 1daaad3..9dfb30d 100644 --- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java +++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -4,6 +4,7 @@ package com.android.tools.r8; +import static com.android.tools.r8.ToolHelper.CLASSPATH_SEPARATOR; import static com.android.tools.r8.ToolHelper.getJavaExecutable; import static org.junit.Assert.assertEquals; @@ -76,7 +77,9 @@ String classPath = addR8ExternalDeps - ? r8jar.toAbsolutePath().toString() + ":" + ToolHelper.DEPS_NOT_RELOCATED + ? r8jar.toAbsolutePath().toString() + + CLASSPATH_SEPARATOR + + ToolHelper.DEPS_NOT_RELOCATED : r8jar.toAbsolutePath().toString(); List<String> command = new ArrayList<>();
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java new file mode 100644 index 0000000..392a72b --- /dev/null +++ b/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java
@@ -0,0 +1,17 @@ +// Copyright (c) 2019, 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; + +public class GenerateMainDexListResult + extends TestBaseResult<GenerateMainDexListResult, GenerateMainDexListRunResult> { + + GenerateMainDexListResult(TestState state) { + super(state); + } + + @Override + public GenerateMainDexListResult self() { + return this; + } +}
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java new file mode 100644 index 0000000..b9842f2 --- /dev/null +++ b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
@@ -0,0 +1,22 @@ +// Copyright (c) 2019, 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; + +import java.util.List; + +public class GenerateMainDexListRunResult extends TestRunResult<GenerateMainDexListRunResult> { + + List<String> mainDexList; + + public GenerateMainDexListRunResult(List<String> mainDexList) { + super(null, null); + this.mainDexList = mainDexList; + } + + @Override + protected GenerateMainDexListRunResult self() { + return this; + } +}
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java new file mode 100644 index 0000000..6eae670 --- /dev/null +++ b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
@@ -0,0 +1,64 @@ +// Copyright (c) 2019, 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; + +import com.android.tools.r8.GenerateMainDexListCommand.Builder; +import com.android.tools.r8.debug.DebugTestConfig; +import com.android.tools.r8.errors.Unimplemented; +import com.android.tools.r8.origin.Origin; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class GenerateMainDexListTestBuilder + extends TestBaseBuilder< + GenerateMainDexListCommand, + Builder, + GenerateMainDexListResult, + GenerateMainDexListRunResult, + GenerateMainDexListTestBuilder> { + + private GenerateMainDexListTestBuilder(TestState state, Builder builder) { + super(state, builder); + } + + public static GenerateMainDexListTestBuilder create(TestState state) { + return new GenerateMainDexListTestBuilder(state, GenerateMainDexListCommand.builder()); + } + + @Override + GenerateMainDexListTestBuilder self() { + return this; + } + + @Override + public GenerateMainDexListRunResult run(String mainClass) + throws IOException, CompilationFailedException { + throw new Unimplemented("No support for running with a main class"); + } + + @Override + public GenerateMainDexListRunResult run(Class mainClass) + throws IOException, CompilationFailedException { + throw new Unimplemented("No support for running with a main class"); + } + + public DebugTestConfig debugConfig() { + throw new Unimplemented("No support for debug configuration"); + } + + public GenerateMainDexListRunResult run() throws CompilationFailedException { + return new GenerateMainDexListRunResult(GenerateMainDexList.run(builder.build())); + } + + public GenerateMainDexListTestBuilder addMainDexRules(Collection<String> rules) { + builder.addMainDexRules(new ArrayList<>(rules), Origin.unknown()); + return self(); + } + + public GenerateMainDexListTestBuilder addMainDexRules(String... rules) { + return addMainDexRules(Arrays.asList(rules)); + } +}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java index e0baab1..1df6808 100644 --- a/src/test/java/com/android/tools/r8/TestBase.java +++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -103,6 +103,10 @@ return ProguardTestBuilder.create(new TestState(temp)); } + public static GenerateMainDexListTestBuilder testForMainDexListGenerator(TemporaryFolder temp) { + return GenerateMainDexListTestBuilder.create(new TestState(temp)); + } + public R8TestBuilder testForR8(Backend backend) { return testForR8(temp, backend); } @@ -131,6 +135,10 @@ return testForProguard(temp); } + public GenerateMainDexListTestBuilder testForMainDexListGenerator() { + return testForMainDexListGenerator(temp); + } + public enum Backend { CF, DEX @@ -163,6 +171,8 @@ /** * Write lines of text to a temporary file. + * + * The file will include a line separator after the last line. */ protected Path writeTextToTempFile(String... lines) throws IOException { return writeTextToTempFile(System.lineSeparator(), Arrays.asList(lines)); @@ -170,11 +180,28 @@ /** * Write lines of text to a temporary file, along with the specified line separator. + * + * The file will include a line separator after the last line. */ protected Path writeTextToTempFile(String lineSeparator, List<String> lines) throws IOException { + return writeTextToTempFile(lineSeparator, lines, true); + } + + /** + * Write lines of text to a temporary file, along with the specified line separator. + * + * The argument <code>includeTerminatingLineSeparator</code> control if the file will include + * a line separator after the last line. + */ + protected Path writeTextToTempFile( + String lineSeparator, List<String> lines, boolean includeTerminatingLineSeparator) + throws IOException { Path file = temp.newFile().toPath(); - String contents = String.join(lineSeparator, lines) + lineSeparator; + String contents = String.join(lineSeparator, lines); + if (includeTerminatingLineSeparator) { + contents += lineSeparator; + } Files.write(file, contents.getBytes(StandardCharsets.UTF_8)); return file; }
diff --git a/src/test/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/java/com/android/tools/r8/TestBaseBuilder.java new file mode 100644 index 0000000..5fcd1b5 --- /dev/null +++ b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
@@ -0,0 +1,50 @@ +// Copyright (c) 2019, 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; + +import com.android.tools.r8.origin.Origin; +import java.nio.file.Path; +import java.util.Collection; + +public abstract class TestBaseBuilder< + C extends BaseCommand, + B extends BaseCommand.Builder<C, B>, + CR extends TestBaseResult<CR, RR>, + RR extends TestRunResult, + T extends TestBaseBuilder<C, B, CR, RR, T>> + extends TestBuilder<RR, T> { + + final B builder; + + TestBaseBuilder(TestState state, B builder) { + super(state); + this.builder = builder; + } + + @Override + public T addProgramClassFileData(Collection<byte[]> classes) { + for (byte[] clazz : classes) { + builder.addClassProgramData(clazz, Origin.unknown()); + } + return self(); + } + + @Override + public T addProgramFiles(Collection<Path> files) { + builder.addProgramFiles(files); + return self(); + } + + @Override + public T addLibraryFiles(Collection<Path> files) { + builder.addLibraryFiles(files); + return self(); + } + + public T addMainDexListFiles(Collection<Path> files) { + builder.addMainDexListFiles(files); + return self(); + } +}
diff --git a/src/test/java/com/android/tools/r8/TestBaseResult.java b/src/test/java/com/android/tools/r8/TestBaseResult.java new file mode 100644 index 0000000..e7623a3 --- /dev/null +++ b/src/test/java/com/android/tools/r8/TestBaseResult.java
@@ -0,0 +1,15 @@ +// Copyright (c) 2019, 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; + +public abstract class TestBaseResult<CR extends TestBaseResult<CR, RR>, RR extends TestRunResult> { + final TestState state; + + TestBaseResult(TestState state) { + this.state = state; + } + + public abstract CR self(); +}
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java index 61bf1b3..c67c05c 100644 --- a/src/test/java/com/android/tools/r8/TestBuilder.java +++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -3,16 +3,12 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8; -import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION; - import com.android.tools.r8.debug.DebugTestConfig; import com.android.tools.r8.utils.ListUtils; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; -import java.util.Set; public abstract class TestBuilder<RR extends TestRunResult, T extends TestBuilder<RR, T>> { @@ -92,14 +88,6 @@ } static Collection<Path> getFilesForInnerClasses(Collection<Class<?>> classes) throws IOException { - Set<Path> paths = new HashSet<>(); - for (Class clazz : classes) { - Path path = ToolHelper.getClassFileForTestClass(clazz); - String prefix = path.toString().replace(CLASS_EXTENSION, "$"); - paths.addAll( - ToolHelper.getClassFilesForTestDirectory( - path.getParent(), p -> p.toString().startsWith(prefix))); - } - return paths; + return ToolHelper.getClassFilesForInnerClasses(classes); } }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java index 0c84e7c..d74fab5 100644 --- a/src/test/java/com/android/tools/r8/TestCompileResult.java +++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -4,9 +4,6 @@ package com.android.tools.r8; import static com.android.tools.r8.TestBase.Backend.DEX; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; import com.android.tools.r8.TestBase.Backend; import com.android.tools.r8.ToolHelper.DexVm; @@ -29,19 +26,17 @@ import org.hamcrest.Matcher; public abstract class TestCompileResult< - CR extends TestCompileResult<CR, RR>, RR extends TestRunResult> { + CR extends TestCompileResult<CR, RR>, RR extends TestRunResult> + extends TestBaseResult<CR, RR> { - final TestState state; public final AndroidApp app; final List<Path> additionalRunClassPath = new ArrayList<>(); TestCompileResult(TestState state, AndroidApp app) { - this.state = state; + super(state); this.app = app; } - public abstract CR self(); - public abstract Backend getBackend(); public abstract TestDiagnosticMessages getDiagnosticMessages(); @@ -83,57 +78,27 @@ } public CR assertNoMessages() { - assertEquals(0, getDiagnosticMessages().getInfos().size()); - assertEquals(0, getDiagnosticMessages().getWarnings().size()); - assertEquals(0, getDiagnosticMessages().getErrors().size()); + getDiagnosticMessages().assertNoMessages(); return self(); } public CR assertOnlyInfos() { - assertNotEquals(0, getDiagnosticMessages().getInfos().size()); - assertEquals(0, getDiagnosticMessages().getWarnings().size()); - assertEquals(0, getDiagnosticMessages().getErrors().size()); + getDiagnosticMessages().assertOnlyInfos(); return self(); } public CR assertOnlyWarnings() { - assertEquals(0, getDiagnosticMessages().getInfos().size()); - assertNotEquals(0, getDiagnosticMessages().getWarnings().size()); - assertEquals(0, getDiagnosticMessages().getErrors().size()); + getDiagnosticMessages().assertOnlyWarnings(); return self(); } public CR assertWarningMessageThatMatches(Matcher<String> matcher) { - assertNotEquals(0, getDiagnosticMessages().getWarnings().size()); - for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) { - if (matcher.matches(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage())) { - return self(); - } - } - StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString()); - builder.append(System.lineSeparator()); - if (getDiagnosticMessages().getWarnings().size() == 0) { - builder.append("There where no warnings."); - } else { - builder.append("There where " + getDiagnosticMessages().getWarnings().size() + " warnings:"); - builder.append(System.lineSeparator()); - for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) { - builder.append(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage()); - builder.append(System.lineSeparator()); - } - } - fail(builder.toString()); + getDiagnosticMessages().assertWarningMessageThatMatches(matcher); return self(); } public CR assertNoWarningMessageThatMatches(Matcher<String> matcher) { - assertNotEquals(0, getDiagnosticMessages().getWarnings().size()); - for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) { - String message = getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage(); - if (matcher.matches(message)) { - fail("The warning: \"" + message + "\" + matches " + matcher + "."); - } - } + getDiagnosticMessages().assertNoWarningMessageThatMatches(matcher); return self(); }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java index cbd0cb9..77dc0df 100644 --- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java +++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -5,7 +5,6 @@ import com.android.tools.r8.TestBase.Backend; import com.android.tools.r8.debug.DebugTestConfig; -import com.android.tools.r8.origin.Origin; import com.android.tools.r8.utils.AndroidApiLevel; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.AndroidAppConsumers; @@ -24,7 +23,7 @@ CR extends TestCompileResult<CR, RR>, RR extends TestRunResult, T extends TestCompilerBuilder<C, B, CR, RR, T>> - extends TestBuilder<RR, T> { + extends TestBaseBuilder<C, B, CR, RR, T> { public static final Consumer<InternalOptions> DEFAULT_OPTIONS = new Consumer<InternalOptions>() { @@ -32,7 +31,6 @@ public void accept(InternalOptions options) {} }; - final B builder; final Backend backend; // Default initialized setup. Can be overwritten if needed. @@ -44,8 +42,7 @@ private PrintStream stdout = null; TestCompilerBuilder(TestState state, B builder, Backend backend) { - super(state); - this.builder = builder; + super(state, builder); this.backend = backend; defaultLibrary = TestBase.runtimeJar(backend); programConsumer = TestBase.emptyConsumer(backend); @@ -135,30 +132,10 @@ return self(); } - public T addMainDexListFiles(Collection<Path> files) { - builder.addMainDexListFiles(files); - return self(); - } - - @Override - public T addProgramClassFileData(Collection<byte[]> classes) { - for (byte[] clazz : classes) { - builder.addClassProgramData(clazz, Origin.unknown()); - } - return self(); - } - - @Override - public T addProgramFiles(Collection<Path> files) { - builder.addProgramFiles(files); - return self(); - } - @Override public T addLibraryFiles(Collection<Path> files) { defaultLibrary = null; - builder.addLibraryFiles(files); - return self(); + return super.addLibraryFiles(files); } public T noDesugaring() {
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java index 3e734cc..d1e55f4 100644 --- a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java +++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
@@ -5,6 +5,8 @@ package com.android.tools.r8; import java.util.List; +import org.hamcrest.Matcher; + public interface TestDiagnosticMessages { @@ -13,4 +15,20 @@ public List<Diagnostic> getWarnings(); public List<Diagnostic> getErrors(); + + public TestDiagnosticMessages assertNoMessages(); + + public TestDiagnosticMessages assertOnlyInfos(); + + public TestDiagnosticMessages assertOnlyWarnings(); + + public TestDiagnosticMessages assertInfosCount(int count); + + public TestDiagnosticMessages assertWarningsCount(int count); + + public TestDiagnosticMessages assertErrorsCount(int count); + + public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher); + + public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher); }
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java index d0476b9..15c03dc 100644 --- a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java +++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
@@ -4,8 +4,13 @@ package com.android.tools.r8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + import java.util.ArrayList; import java.util.List; +import org.hamcrest.Matcher; public class TestDiagnosticMessagesImpl implements DiagnosticsHandler, TestDiagnosticMessages { private final List<Diagnostic> infos = new ArrayList<>(); @@ -38,4 +43,75 @@ public List<Diagnostic> getErrors() { return errors; } + + + public TestDiagnosticMessages assertNoMessages() { + assertEquals(0, getInfos().size()); + assertEquals(0, getWarnings().size()); + assertEquals(0, getErrors().size()); + return this; + } + + public TestDiagnosticMessages assertOnlyInfos() { + assertNotEquals(0, getInfos().size()); + assertEquals(0, getWarnings().size()); + assertEquals(0, getErrors().size()); + return this; + } + + public TestDiagnosticMessages assertOnlyWarnings() { + assertEquals(0, getInfos().size()); + assertNotEquals(0, getWarnings().size()); + assertEquals(0, getErrors().size()); + return this; + } + + public TestDiagnosticMessages assertInfosCount(int count) { + assertEquals(count, getInfos().size()); + return this; + } + + public TestDiagnosticMessages assertWarningsCount(int count) { + assertEquals(count, getWarnings().size()); + return this; + } + + public TestDiagnosticMessages assertErrorsCount(int count) { + assertEquals(count, getErrors().size()); + return this; + } + + public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher) { + assertNotEquals(0, getWarnings().size()); + for (int i = 0; i < getWarnings().size(); i++) { + if (matcher.matches(getWarnings().get(i).getDiagnosticMessage())) { + return this; + } + } + StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString()); + builder.append(System.lineSeparator()); + if (getWarnings().size() == 0) { + builder.append("There were no warnings."); + } else { + builder.append("There were " + getWarnings().size() + " warnings:"); + builder.append(System.lineSeparator()); + for (int i = 0; i < getWarnings().size(); i++) { + builder.append(getWarnings().get(i).getDiagnosticMessage()); + builder.append(System.lineSeparator()); + } + } + fail(builder.toString()); + return this; + } + + public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher) { + assertNotEquals(0, getWarnings().size()); + for (int i = 0; i < getWarnings().size(); i++) { + String message = getWarnings().get(i).getDiagnosticMessage(); + if (matcher.matches(message)) { + fail("The warning: \"" + message + "\" + matches " + matcher + "."); + } + } + return this; + } }
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java index 65a57e6..a032fd6 100644 --- a/src/test/java/com/android/tools/r8/TestRunResult.java +++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -29,6 +29,10 @@ abstract RR self(); + public AndroidApp app() { + return app; + } + public String getStdOut() { return result.stdout; }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java index c9fe628..bab857c 100644 --- a/src/test/java/com/android/tools/r8/ToolHelper.java +++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8; +import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION; import static com.android.tools.r8.utils.FileUtils.isDexFile; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -57,6 +58,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -95,7 +97,8 @@ public static final String SMALI_BUILD_DIR = TESTS_BUILD_DIR + "smali/"; public static final String LINE_SEPARATOR = StringUtils.LINE_SEPARATOR; - public static final String PATH_SEPARATOR = File.pathSeparator; + public static final String CLASSPATH_SEPARATOR = File.pathSeparator; + public static final String DEFAULT_DEX_FILENAME = "classes.dex"; public static final String DEFAULT_PROGUARD_MAP_FILE = "proguard.map"; @@ -856,6 +859,28 @@ Paths.get("", parts.toArray(new String[parts.size() - 1]))); } + public static Collection<Path> getClassFilesForInnerClasses(Path path) throws IOException { + Set<Path> paths = new HashSet<>(); + String prefix = path.toString().replace(CLASS_EXTENSION, "$"); + paths.addAll( + ToolHelper.getClassFilesForTestDirectory( + path.getParent(), p -> p.toString().startsWith(prefix))); + return paths; + } + + public static Collection<Path> getClassFilesForInnerClasses(Collection<Class<?>> classes) + throws IOException { + Set<Path> paths = new HashSet<>(); + for (Class clazz : classes) { + Path path = ToolHelper.getClassFileForTestClass(clazz); + String prefix = path.toString().replace(CLASS_EXTENSION, "$"); + paths.addAll( + ToolHelper.getClassFilesForTestDirectory( + path.getParent(), p -> p.toString().startsWith(prefix))); + } + return paths; + } + public static Path getFileNameForTestClass(Class clazz) { List<String> parts = getNamePartsForTestClass(clazz); return Paths.get("", parts.toArray(new String[parts.size() - 1])); @@ -1062,7 +1087,8 @@ } public static ProcessResult runJava(List<Path> classpath, String... args) throws IOException { - String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR)); + String cp = + classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR)); List<String> cmdline = new ArrayList<String>(Arrays.asList(getJavaExecutable(), "-cp", cp)); cmdline.addAll(Arrays.asList(args)); ProcessBuilder builder = new ProcessBuilder(cmdline); @@ -1082,7 +1108,8 @@ public static ProcessResult runJavaNoVerify( List<Path> classpath, String mainClass, List<String> args) throws IOException { - String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR)); + String cp = + classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR)); ArrayList<String> cmdline = Lists.newArrayList( getJavaExecutable(), "-cp", cp, "-noverify", mainClass); cmdline.addAll(args);
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java new file mode 100644 index 0000000..5e97ab0 --- /dev/null +++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java
@@ -0,0 +1,29 @@ +// Copyright (c) 2019, 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.cf; + +import com.android.tools.r8.NeverInline; + +public class TryRangeTestLimitRange { + + @NeverInline + public static float doSomething(int x) throws Exception { + if (x == 42) { + throw new Exception("is 42"); + } else { + return 1; + } + } + + public static void main(String[] args) { + int x = args.length; + int y = x + 1; + try { + doSomething(y); + } catch (Exception ex) { + System.out.println(x + ": " + y); + } + } +}
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java index 98542ff..e4c7098 100644 --- a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java +++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
@@ -4,8 +4,14 @@ package com.android.tools.r8.cf; +import static org.junit.Assert.assertTrue; + import com.android.tools.r8.CompilationMode; import com.android.tools.r8.TestBase; +import com.android.tools.r8.ir.code.BasicBlock; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.Instruction; +import java.util.ListIterator; import org.junit.Test; /** @@ -18,7 +24,7 @@ public class TryRangeTestRunner extends TestBase { @Test - public void test() throws Exception { + public void testRegisterAllocationLimitTrailingRange() throws Exception { testForR8(Backend.CF) .addProgramClasses(TryRangeTest.class) .addKeepMainRule(TryRangeTest.class) @@ -26,8 +32,55 @@ .minification(false) .noTreeShaking() .enableInliningAnnotations() - .addOptionsModification(o -> o.testing.disallowLoadStoreOptimization = true) + .addOptionsModification( + o -> { + o.testing.disallowLoadStoreOptimization = true; + }) .run(TryRangeTest.class) .assertSuccess(); } + + @Test + public void testRegisterAllocationLimitLeadingRange() throws Exception { + testForR8(Backend.CF) + .addProgramClasses(TryRangeTestLimitRange.class) + .addKeepMainRule(TryRangeTestLimitRange.class) + .setMode(CompilationMode.RELEASE) + .minification(false) + .noTreeShaking() + .enableInliningAnnotations() + .addOptionsModification( + o -> { + o.testing.disallowLoadStoreOptimization = true; + o.testing.irModifier = this::processIR; + // TODO(mkroghj) Remove this option entirely when splittingExceptionalEdges is moved. + o.testing.noSplittingExceptionalEdges = true; + }) + .run(TryRangeTestLimitRange.class) + .assertFailure(); + } + + private void processIR(IRCode code) { + if (!code.method.qualifiedName().equals(TryRangeTestLimitRange.class.getName() + ".main")) { + return; + } + BasicBlock entryBlock = code.blocks.get(0); + BasicBlock tryBlock = code.blocks.get(1); + assertTrue(tryBlock.hasCatchHandlers()); + ListIterator<Instruction> it = entryBlock.getInstructions().listIterator(); + Instruction constNumber = it.next(); + while (!constNumber.isConstNumber()) { + constNumber = it.next(); + } + it.remove(); + Instruction add = it.next(); + while (!add.isAdd()) { + add = it.next(); + } + it.remove(); + constNumber.setBlock(tryBlock); + add.setBlock(tryBlock); + tryBlock.getInstructions().add(0, add); + tryBlock.getInstructions().add(0, constNumber); + } }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java new file mode 100644 index 0000000..5d6e151 --- /dev/null +++ b/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java
@@ -0,0 +1,78 @@ +// Copyright (c) 2019, 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.debuginfo; + +import com.android.tools.r8.OutputMode; +import com.android.tools.r8.R8TestCompileResult; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.dex.Constants; +import com.android.tools.r8.dex.DexParser; +import com.android.tools.r8.dex.DexSection; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.Assert; +import org.junit.Test; + +public class CannonicalizeWithInline extends TestBase { + + private int getNumberOfDebugInfos(Path file) throws IOException { + DexSection[] dexSections = DexParser.parseMapFrom(file); + for (DexSection dexSection : dexSections) { + if (dexSection.type == Constants.TYPE_DEBUG_INFO_ITEM) { + return dexSection.length; + } + } + return 0; + } + + @Test + public void testCannonicalize() throws Exception { + Class clazzA = ClassA.class; + Class clazzB = ClassB.class; + + R8TestCompileResult result = testForR8(Backend.DEX) + .addProgramClasses(clazzA, clazzB) + .addKeepRules( + "-keepattributes SourceFile,LineNumberTable", + "-keep class ** {\n" + + "public void call(int);\n" + + "}" + ) + .compile(); + Path classesPath = temp.getRoot().toPath(); + result.app.write(classesPath, OutputMode.DexIndexed); + int numberOfDebugInfos = getNumberOfDebugInfos( + Paths.get(temp.getRoot().getCanonicalPath(), "classes.dex")); + Assert.assertEquals(1, numberOfDebugInfos); + } + + // Two classes which has debug info that looks exactly the same, except for SetInlineFrame. + // R8 will inline the call to foobar in both classes, causing us to store a SetInlineFrame in the + // debug info. + // Ensure that we still canonicalize when writing. + public static class ClassA { + + public void call(int a) { + foobar(a); + } + + private String foobar(int a) { + String s = "aFoobar" + a; + return s; + } + } + + public static class ClassB { + + public void call(int a) { + foobar(a); + } + + private String foobar(int a) { + String s = "bFoobar" + a; + return s; + } + } +}
diff --git a/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java index 97047e8..8af2f8e 100644 --- a/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java +++ b/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java
@@ -3,6 +3,8 @@ // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8.desugar; +import static com.android.tools.r8.ToolHelper.CLASSPATH_SEPARATOR; + import com.android.tools.r8.D8Command; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.errors.CompilationError; @@ -30,8 +32,6 @@ @RunWith(Parameterized.class) public class BasicTestDependenciesDesugaringTest { - private static final String CLASSPATH_SEPARATOR = File.pathSeparator; - private static final String[] allLibs; static { try {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java index c140610..798f1b0 100644 --- a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java +++ b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
@@ -10,10 +10,12 @@ import static com.android.tools.r8.ir.analysis.type.TypeLatticeElement.LONG; import static org.junit.Assert.assertEquals; +import com.android.tools.r8.NeverInline; import com.android.tools.r8.ir.analysis.AnalysisTestBase; import com.android.tools.r8.ir.code.ConstNumber; import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.Instruction; +import com.android.tools.r8.utils.StringUtils; import com.google.common.collect.Streams; import java.util.function.Consumer; import org.junit.Test; @@ -25,6 +27,21 @@ } @Test + public void testOutput() throws Exception { + String expectedOutput = + StringUtils.lines("1", "2", "3", "1.0", "1.0", "2.0", "1", "1", "2", "1.0", "1.0", "2.0"); + + testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput); + + testForR8(Backend.DEX) + .addInnerClasses(ConstrainedPrimitiveTypeTest.class) + .addKeepMainRule(TestClass.class) + .enableInliningAnnotations() + .run(TestClass.class) + .assertSuccessWithOutput(expectedOutput); + } + + @Test public void testIntWithInvokeUser() throws Exception { buildAndCheckIR("intWithInvokeUserTest", testInspector(INT, 1)); } @@ -83,26 +100,47 @@ static class TestClass { - public static void intWithInvokeUserTest() { - int x = 1; - Integer.toString(x); + public static void main(String[] args) { + boolean unknownButTrue = args.length >= 0; + boolean unknownButFalse = args.length < 0; + intWithInvokeUserTest(); + intWithIndirectInvokeUserTest(unknownButTrue); + intWithIndirectInvokeUserTest(unknownButFalse); + floatWithInvokeUserTest(); + floatWithIndirectInvokeUserTest(unknownButTrue); + floatWithIndirectInvokeUserTest(unknownButFalse); + longWithInvokeUserTest(); + longWithIndirectInvokeUserTest(unknownButTrue); + longWithIndirectInvokeUserTest(unknownButFalse); + doubleWithInvokeUserTest(); + doubleWithIndirectInvokeUserTest(unknownButTrue); + doubleWithIndirectInvokeUserTest(unknownButFalse); } + @NeverInline + public static void intWithInvokeUserTest() { + int x = 1; + System.out.println(Integer.toString(x)); + } + + @NeverInline public static void intWithIndirectInvokeUserTest(boolean unknown) { int x; if (unknown) { - x = 1; - } else { x = 2; + } else { + x = 3; } - Integer.toString(x); + System.out.println(Integer.toString(x)); } + @NeverInline public static void floatWithInvokeUserTest() { float x = 1f; - Float.toString(x); + System.out.println(Float.toString(x)); } + @NeverInline public static void floatWithIndirectInvokeUserTest(boolean unknown) { float x; if (unknown) { @@ -110,14 +148,16 @@ } else { x = 2f; } - Float.toString(x); + System.out.println(Float.toString(x)); } + @NeverInline public static void longWithInvokeUserTest() { long x = 1L; - Long.toString(x); + System.out.println(Long.toString(x)); } + @NeverInline public static void longWithIndirectInvokeUserTest(boolean unknown) { long x; if (unknown) { @@ -125,14 +165,16 @@ } else { x = 2L; } - Long.toString(x); + System.out.println(Long.toString(x)); } + @NeverInline public static void doubleWithInvokeUserTest() { double x = 1.0; - Double.toString(x); + System.out.println(Double.toString(x)); } + @NeverInline public static void doubleWithIndirectInvokeUserTest(boolean unknown) { double x; if (unknown) { @@ -140,7 +182,7 @@ } else { x = 2f; } - Double.toString(x); + System.out.println(Double.toString(x)); } } }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java new file mode 100644 index 0000000..fc382ee --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java
@@ -0,0 +1,248 @@ +// Copyright (c) 2019, 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; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.R8TestRunResult; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.ir.code.BasicBlock; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.Instruction; +import com.android.tools.r8.utils.StringUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.InstructionSubject; +import com.android.tools.r8.utils.codeinspector.MethodSubject; +import com.google.common.collect.Streams; +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 RedundantConstNumberRemovalTest extends TestBase { + + private final Backend backend; + + @Parameters(name = "Backend: {0}") + public static Backend[] data() { + return Backend.values(); + } + + public RedundantConstNumberRemovalTest(Backend backend) { + this.backend = backend; + } + + @Test + public void test() throws Exception { + String expectedOutput = + StringUtils.lines( + "true", "true", "true", "true", "true", "true", "true", "true", "true", "true", "true", + "true", "true", "true", "true", "true"); + + if (backend == Backend.CF) { + testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput); + } + + R8TestRunResult result = + testForR8(backend) + .addInnerClasses(RedundantConstNumberRemovalTest.class) + .addKeepMainRule(TestClass.class) + .enableInliningAnnotations() + .run(TestClass.class) + .assertSuccessWithOutput(expectedOutput); + + ClassSubject classSubject = result.inspector().clazz(TestClass.class); + verifyBooleanCheckTest(classSubject.uniqueMethodWithName("booleanCheckTest")); + verifyBooleanCheckTest(classSubject.uniqueMethodWithName("negateBooleanCheckTest")); + verifyIntCheckTest(classSubject.uniqueMethodWithName("intCheckTest")); + verifyIntCheckTest(classSubject.uniqueMethodWithName("negateIntCheckTest")); + verifyNullCheckTest(classSubject.uniqueMethodWithName("nullCheckTest")); + verifyNullCheckTest(classSubject.uniqueMethodWithName("invertedNullCheckTest")); + verifyNullCheckTest(classSubject.uniqueMethodWithName("nonNullCheckTest")); + verifyNullCheckWithWrongTypeTest( + classSubject.uniqueMethodWithName("nullCheckWithWrongTypeTest")); + } + + private void verifyBooleanCheckTest(MethodSubject methodSubject) { + assertThat(methodSubject, isPresent()); + + if (backend == Backend.DEX) { + // Check that the generated code for booleanCheckTest() only has a return instruction. + assertEquals(1, methodSubject.streamInstructions().count()); + assertTrue(methodSubject.iterateInstructions().next().isReturn()); + } else { + assert backend == Backend.CF; + // Check that the generated code for booleanCheckTest() only has return instructions that + // return the argument. + // TODO(christofferqa): CF backend does not share identical prefix of successors. + IRCode code = methodSubject.buildIR(); + assertTrue( + Streams.stream(code.instructionIterator()) + .filter(Instruction::isReturn) + .allMatch( + instruction -> instruction.asReturn().returnValue().definition.isArgument())); + } + } + + private void verifyIntCheckTest(MethodSubject methodSubject) { + assertThat(methodSubject, isPresent()); + IRCode code = methodSubject.buildIR(); + + if (backend == Backend.DEX) { + // Only a single basic block. + assertEquals(1, code.blocks.size()); + // The block only has three instructions. + BasicBlock entryBlock = code.blocks.get(0); + assertEquals(3, entryBlock.getInstructions().size()); + // The first one is the `argument` instruction. + Instruction argument = entryBlock.getInstructions().getFirst(); + assertTrue(argument.isArgument()); + // The next one is a `const-number` instruction is not used for anything. + // TODO(christofferqa): D8 should be able to get rid of the unused const-number instruction. + Instruction unused = entryBlock.getInstructions().get(1); + assertTrue(unused.isConstNumber()); + assertEquals(0, unused.outValue().numberOfAllUsers()); + // The `return` instruction returns the argument. + Instruction ret = entryBlock.getInstructions().getLast(); + assertTrue(ret.isReturn()); + assertTrue(ret.asReturn().returnValue().definition.isArgument()); + } else { + // Check that the generated code for intCheckTest() only has return instructions that + // return the argument. + // TODO(christofferqa): CF backend does not share identical prefix of successors. + assertTrue( + Streams.stream(code.instructionIterator()) + .filter(Instruction::isReturn) + .allMatch( + instruction -> instruction.asReturn().returnValue().definition.isArgument())); + } + } + + private void verifyNullCheckTest(MethodSubject methodSubject) { + // Check that the generated code for nullCheckTest() only has a single `const-null` instruction. + assertThat(methodSubject, isPresent()); + assertEquals( + 1, methodSubject.streamInstructions().filter(InstructionSubject::isConstNull).count()); + + // Also check that one of the return instructions actually returns the argument. + IRCode code = methodSubject.buildIR(); + assertEquals(1, code.collectArguments().size()); + // TODO(b/120257211): D8 should replace `return null` by `return arg`. + assertFalse( + code.collectArguments().get(0).uniqueUsers().stream().anyMatch(Instruction::isReturn)); + } + + private void verifyNullCheckWithWrongTypeTest(MethodSubject methodSubject) { + // Check that the generated code for nullCheckWithWrongTypeTest() still has a `return null` + // instruction. + assertThat(methodSubject, isPresent()); + IRCode code = methodSubject.buildIR(); + + // Check that the code returns null. + assertTrue( + Streams.stream(code.instructionIterator()) + .anyMatch( + instruction -> + instruction.isReturn() + && instruction.asReturn().returnValue().definition.isConstNumber())); + + // Also check that none of the return instructions actually returns the argument. + assertEquals(1, code.collectArguments().size()); + assertTrue( + code.collectArguments().get(0).uniqueUsers().stream().noneMatch(Instruction::isReturn)); + } + + static class TestClass { + + public static void main(String[] args) { + System.out.println(booleanCheckTest(true) == true); + System.out.println(booleanCheckTest(false) == false); + System.out.println(negateBooleanCheckTest(true) == true); + System.out.println(negateBooleanCheckTest(false) == false); + System.out.println(intCheckTest(0) == 0); + System.out.println(intCheckTest(42) == 42); + System.out.println(negateIntCheckTest(0) == 0); + System.out.println(negateIntCheckTest(42) == 42); + System.out.println(nullCheckTest(new Object()) != null); + System.out.println(nullCheckTest(null) == null); + System.out.println(invertedNullCheckTest(new Object()) != null); + System.out.println(invertedNullCheckTest(null) == null); + System.out.println(nonNullCheckTest(new Object()) != null); + System.out.println(nonNullCheckTest(null) == null); + System.out.println(nullCheckWithWrongTypeTest(new Object()) != null); + System.out.println(nullCheckWithWrongTypeTest(null) == null); + } + + @NeverInline + private static boolean booleanCheckTest(boolean x) { + if (x) { + return true; // should be replaced by `x`. + } + return false; // should be replaced by `x`. + } + + @NeverInline + private static boolean negateBooleanCheckTest(boolean x) { + if (!x) { + return false; // should be replaced by `x` + } + return true; // should be replaced by `x` + } + + @NeverInline + private static int intCheckTest(int x) { + if (x == 42) { + return 42; // should be replaced by `x`. + } + return x; + } + + @NeverInline + private static int negateIntCheckTest(int x) { + if (x != 42) { + return x; + } + return 42; // should be replaced by `x` + } + + @NeverInline + private static Object nullCheckTest(Object x) { + if (x != null) { + return new Object(); + } + return null; // should be replaced by `x`. + } + + @NeverInline + private static Object invertedNullCheckTest(Object x) { + if (null == x) { + return null; // should be replaced by `x`. + } + return new Object(); + } + + @NeverInline + private static Object nonNullCheckTest(Object x) { + if (x == null) { + return null; // should be replaced by `x` + } + return new Object(); + } + + @NeverInline + private static Throwable nullCheckWithWrongTypeTest(Object x) { + if (x != null) { + return new Throwable(); + } + return null; // cannot be replaced by `x` because Object is not a subtype of Throwable. + } + } +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java index df797ad..7b3ed4a 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -29,11 +29,14 @@ import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictField; import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictMethod; import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOk; +import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkFieldOnly; import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkSideEffects; import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictField; import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictMethod; import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOk; +import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkFieldOnly; import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkSideEffects; +import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostFieldOnlyTestClass; import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostTestClass; import com.android.tools.r8.ir.optimize.staticizer.trivial.Simple; import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithGetter; @@ -150,6 +153,36 @@ } @Test + public void testMoveToHost_fieldOnly() throws Exception { + assumeTrue("b/112831361", backend == Backend.DEX); + Class<?> main = MoveToHostFieldOnlyTestClass.class; + Class<?>[] classes = { + NeverInline.class, + MoveToHostFieldOnlyTestClass.class, + HostOkFieldOnly.class, + CandidateOkFieldOnly.class + }; + TestRunResult result = testForR8(backend) + .addProgramClasses(classes) + .enableProguardTestOptions() + .enableInliningAnnotations() + .addKeepMainRule(main) + .noMinification() + .addKeepRules("-allowaccessmodification") + .addOptionsModification(this::configure) + .run(main); + + CodeInspector inspector = result.inspector(); + ClassSubject clazz = inspector.clazz(main); + + assertEquals( + Lists.newArrayList(), + references(clazz, "testOk_fieldOnly", "void")); + + assertFalse(inspector.clazz(CandidateOkFieldOnly.class).isPresent()); + } + + @Test public void testMoveToHost() throws Exception { assumeTrue("b/112831361", backend == Backend.DEX); Class<?> main = MoveToHostTestClass.class;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java new file mode 100644 index 0000000..48963f4 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java
@@ -0,0 +1,8 @@ +// Copyright (c) 2019, 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.staticizer.movetohost; + +public class CandidateOkFieldOnly { + // No instance methods. +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java new file mode 100644 index 0000000..e132627 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java
@@ -0,0 +1,8 @@ +// Copyright (c) 2019, 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.staticizer.movetohost; + +public class HostOkFieldOnly { + static CandidateOkFieldOnly INSTANCE = new CandidateOkFieldOnly(); +}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java new file mode 100644 index 0000000..f725d21 --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
@@ -0,0 +1,29 @@ +// Copyright (c) 2019, 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.staticizer.movetohost; + +import com.android.tools.r8.NeverInline; + +public class MoveToHostFieldOnlyTestClass { + + public static void main(String[] args) { + MoveToHostFieldOnlyTestClass test = new MoveToHostFieldOnlyTestClass(); + test.testOk_fieldOnly(); + } + + @NeverInline + private void testOk_fieldOnly() { + // Any instance method call whose target holder is not the candidate will invalidate candidacy, + // for example, toString() without overriding, getClass(), etc. + // Note that having instance methods in the candidate class guarantees that method mappings will + // exist when field mappings do so. + // Any other uses other than invoke-virtual or invoke-direct (to either <init> or private) are + // not allowed, e.g., System.out.println(INSTANCE), null check, or static-put to somewhere else. + // Therefore, it's merely dead code, and thus it has not been harmful to forget to create a + // staticizer lense when there is no method mapping (for instance methods to staticized ones) + // while there are field mappings as shown in this example. + Object x = HostOkFieldOnly.INSTANCE; + } +} +
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java new file mode 100644 index 0000000..20d7bae --- /dev/null +++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java
@@ -0,0 +1,95 @@ +// Copyright (c) 2019, 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.unusedarguments; + +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import com.android.tools.r8.NeverClassInline; +import com.android.tools.r8.NeverInline; +import com.android.tools.r8.TestBase; +import com.android.tools.r8.utils.StringUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; +import com.android.tools.r8.utils.codeinspector.CodeInspector; +import com.android.tools.r8.utils.codeinspector.MethodSubject; +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 UnusedArgumentsInstanceConstructorTest extends TestBase { + + private final Backend backend; + + @Parameters(name = "Backend: {0}") + public static Backend[] data() { + return Backend.values(); + } + + public UnusedArgumentsInstanceConstructorTest(Backend backend) { + this.backend = backend; + } + + @Test + public void test() throws Exception { + String expectedOutput = StringUtils.lines("Hello world"); + + if (backend == Backend.CF) { + testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput); + } + + CodeInspector inspector = + testForR8(backend) + .addInnerClasses(UnusedArgumentsInstanceConstructorTest.class) + .addKeepMainRule(TestClass.class) + .enableInliningAnnotations() + .enableClassInliningAnnotations() + .run(TestClass.class) + .assertSuccessWithOutput(expectedOutput) + .inspector(); + + ClassSubject classSubject = inspector.clazz(A.class); + assertThat(classSubject, isPresent()); + + MethodSubject methodSubject = classSubject.uniqueMethodWithName("<init>"); + assertThat(methodSubject, isPresent()); + assertTrue(methodSubject.getMethod().method.proto.parameters.isEmpty()); + + assertThat(inspector.clazz(B.class), not(isPresent())); + assertThat(inspector.clazz(C.class), not(isPresent())); + } + + static class TestClass { + + public static void main(String[] args) { + new A(null, new C()).doSomething(); + } + } + + @NeverClassInline + static class A { + + public A(B uninstantiated, C unused) { + System.out.print("Hello"); + if (uninstantiated != null) { + throw new RuntimeException("Unreachable"); + } else { + System.out.print(" "); + } + } + + @NeverInline + public void doSomething() { + System.out.println("world"); + } + } + + static class B {} + + static class C {} +}
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java new file mode 100644 index 0000000..700e2d4 --- /dev/null +++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -0,0 +1,66 @@ +// Copyright (c) 2019, 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.kotlin; + +import com.android.tools.r8.KotlinTestBase; +import com.android.tools.r8.ToolHelper; +import com.android.tools.r8.ToolHelper.KotlinTargetVersion; +import java.util.Collection; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ProcessKotlinReflectionLibTest extends KotlinTestBase { + private final Backend backend; + + public ProcessKotlinReflectionLibTest(Backend backend, KotlinTargetVersion targetVersion) { + super(targetVersion); + this.backend = backend; + } + + @Parameterized.Parameters(name = "Backend: {0} target: {1}") + public static Collection<Object[]> data() { + return buildParameters(Backend.values(), KotlinTargetVersion.values()); + } + + private void test(String... rules) throws Exception { + testForR8(backend) + .addLibraryFiles(ToolHelper.getDefaultAndroidJar(), ToolHelper.getKotlinStdlibJar()) + .addProgramFiles(ToolHelper.getKotlinReflectJar()) + .addKeepRules(rules) + .compile(); + } + + @Test + public void testAsIs() throws Exception { + test("-dontshrink", "-dontoptimize", "-dontobfuscate"); + } + + @Test + public void testDontShrinkAndDontOptimize() throws Exception { + test("-dontshrink", "-dontoptimize"); + } + + @Test + public void testDontShrinkAndDontObfuscate() throws Exception { + test("-dontshrink", "-dontobfuscate"); + } + + @Test + public void testDontShrink() throws Exception { + test("-dontshrink"); + } + + @Test + public void testDontOptimize() throws Exception { + test("-dontoptimize"); + } + + @Test + public void testDontObfuscate() throws Exception { + test("-dontobfuscate"); + } + +}
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java new file mode 100644 index 0000000..f19b089 --- /dev/null +++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
@@ -0,0 +1,73 @@ +// Copyright (c) 2019, 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.kotlin; + +import com.android.tools.r8.KotlinTestBase; +import com.android.tools.r8.ToolHelper; +import com.android.tools.r8.ToolHelper.KotlinTargetVersion; +import java.util.Collection; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ProcessKotlinStdlibTest extends KotlinTestBase { + private final Backend backend; + + public ProcessKotlinStdlibTest(Backend backend, KotlinTargetVersion targetVersion) { + super(targetVersion); + this.backend = backend; + } + + @Parameterized.Parameters(name = "Backend: {0} target: {1}") + public static Collection<Object[]> data() { + return buildParameters(Backend.values(), KotlinTargetVersion.values()); + } + + private void test(String... rules) throws Exception { + testForR8(backend) + .addProgramFiles(ToolHelper.getKotlinStdlibJar()) + .addKeepRules(rules) + .compile(); + } + + @Test + public void testAsIs() throws Exception { + test("-dontshrink", "-dontoptimize", "-dontobfuscate"); + } + + @Test + public void testDontShrinkAndDontOptimize() throws Exception { + // TODO(b/122819537): need to trace kotlinc-generated synthetic methods. + if (backend == Backend.DEX) { + return; + } + test("-dontshrink", "-dontoptimize"); + } + + @Test + public void testDontShrinkAndDontObfuscate() throws Exception { + test("-dontshrink", "-dontobfuscate"); + } + + @Test + public void testDontShrink() throws Exception { + // TODO(b/122819537): need to trace kotlinc-generated synthetic methods. + if (backend == Backend.DEX) { + return; + } + test("-dontshrink"); + } + + @Test + public void testDontOptimize() throws Exception { + test("-dontoptimize"); + } + + @Test + public void testDontObfuscate() throws Exception { + test("-dontobfuscate"); + } + +}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java index 16958a6..125c364 100644 --- a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java +++ b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
@@ -5,16 +5,10 @@ package com.android.tools.r8.maindexlist.checkdiscard; import com.android.tools.r8.CompilationFailedException; -import com.android.tools.r8.CompilationMode; -import com.android.tools.r8.GenerateMainDexList; -import com.android.tools.r8.GenerateMainDexListCommand; -import com.android.tools.r8.OutputMode; -import com.android.tools.r8.R8Command; import com.android.tools.r8.TestBase; import com.android.tools.r8.ToolHelper; import com.android.tools.r8.errors.Unreachable; -import com.android.tools.r8.origin.Origin; -import com.android.tools.r8.utils.ListUtils; +import com.android.tools.r8.utils.AndroidApiLevel; import com.google.common.collect.ImmutableList; import java.util.List; import org.junit.Assert; @@ -54,33 +48,24 @@ this.command = command; } - public void runTestWithR8(String checkDiscardRule) throws Exception { - R8Command command = - ToolHelper.prepareR8CommandBuilder( - readClasses(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class)) - .addMainDexRules( - ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)), - Origin.unknown()) - .addMainDexRules(ImmutableList.of(checkDiscardRule), Origin.unknown()) - .setOutput(temp.getRoot().toPath(), OutputMode.DexIndexed) - .setMode(CompilationMode.RELEASE) - .setDisableTreeShaking(true) - .setDisableMinification(true) - .build(); - ToolHelper.runR8(command); + testForR8(Backend.DEX) + .addProgramClasses(CLASSES) + .setMinApi(AndroidApiLevel.K) + .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class)) + .addMainDexRules(checkDiscardRule) + .noTreeShaking() + .noMinification() + .compile(); } public void runTestWithGenerator(String checkDiscardRule) throws Exception { - GenerateMainDexListCommand.Builder builder = - GenerateMainDexListCommand.builder() - .addProgramFiles(ListUtils.map(CLASSES, ToolHelper::getClassFileForTestClass)) - .addLibraryFiles(ToolHelper.getDefaultAndroidJar()) - .addMainDexRules( - ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)), - Origin.unknown()) - .addMainDexRules(ImmutableList.of(checkDiscardRule), Origin.unknown()); - GenerateMainDexList.run(builder.build()); + testForMainDexListGenerator() + .addProgramClasses(CLASSES) + .addLibraryFiles(ToolHelper.getDefaultAndroidJar()) + .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class)) + .addMainDexRules(checkDiscardRule) + .run(); } public void runTest(String checkDiscardRule, boolean shouldFail) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java index 1038f5c..b175785 100644 --- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java +++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
@@ -35,6 +35,7 @@ import java.util.Iterator; import java.util.concurrent.ExecutionException; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -75,6 +76,7 @@ out = temp.newFolder("out").toPath(); } + @Ignore("b/121305642") @Test public void test044_obfuscate_and_apply() throws Exception { // keep rules that allow obfuscations while keeping everything. @@ -110,7 +112,7 @@ AndroidApp instrApp = runR8( ToolHelper.addProguardConfigurationConsumer( - getCommandForInstrumentation(instrOut, flag, NAMING044_JAR, APPLYMAPPING044_JAR) + getCommandForInstrumentation(instrOut, flag, out, APPLYMAPPING044_JAR) .setDisableMinification(true), pgConfig -> pgConfig.setApplyMappingFile(proguardMap)) .build());
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java index 2258d67..f7b95f9 100644 --- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java +++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
@@ -56,6 +56,10 @@ .assertFailure(); // Extract actual stack trace and retraced stack trace from failed run result. + // TODO(122940268): Remove test code when fixed. + System.out.println("<--- TEST RESULT START --->"); + System.out.println(result); + System.out.println("<--- TEST RESULT END --->"); StackTrace actualStackTrace = StackTrace.extractFromArt(result.getStdErr()); StackTrace retracedStackTrace = actualStackTrace.retrace(result.proguardMap(), temp.newFolder().toPath());
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java index 0ecd3e4..dd8bc9c 100644 --- a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java +++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -165,9 +165,27 @@ } // Take all lines from the bottom starting with "\tat ". int first = last; + // TODO(122940268): Remove test code when fixed. + System.out.println("TOTAL STDERR LINES: " + stderrLines.size()); + for (int i = 0; i < last; i++) { + System.out.print("LINE " + i + ": " + stderrLines.get(i)); + if (stderrLines.get(i).length() > 3) { + System.out.print(" (" + ((int) stderrLines.get(i).charAt(0))); + System.out.print(", " + ((int) stderrLines.get(i).charAt(1))); + System.out.print(", " + ((int) stderrLines.get(i).charAt(2) + ")")); + } else { + System.out.print(" (less than three chars)"); + } + if (stderrLines.get(i).startsWith(TAB_AT_PREFIX)) { + System.out.println(" IS STACKTRACE LINE"); + } else { + System.out.println(" IS NOT STACKTRACE LINE"); + } + } while (first - 1 >= 0 && stderrLines.get(first - 1).startsWith(TAB_AT_PREFIX)) { first--; } + System.out.println("STACKTRACE LINES ARE " + first + " to " + (last - 1)); for (int i = first; i < last; i++) { stackTraceLines.add(StackTraceLine.parse(stderrLines.get(i))); }
diff --git a/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java new file mode 100644 index 0000000..23650c9 --- /dev/null +++ b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java
@@ -0,0 +1,28 @@ +// Copyright (c) 2019, 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.regress.b122887884; + +public class Regress122887884 { + + public static int[] foo(String[] args) { + if (args.length == 0) { + return new int[] {0, 0}; + } + String first = args[0]; + try { + String[] split = first.split(""); + if (split.length != 2) { + // This results in a new-array v2 v2 int[] at which point the exception handler must split. + return new int[] {0, 0}; + } + return new int[] {1, 1}; + } catch (Throwable t) { + return new int[] {0, 0}; + } + } + + public static void main(String[] args) { + System.out.println(foo(args)[0]); + } +}
diff --git a/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java new file mode 100644 index 0000000..54f32fd --- /dev/null +++ b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java
@@ -0,0 +1,19 @@ +// Copyright (c) 2019, 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.regress.b122887884; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.utils.StringUtils; +import org.junit.Test; + +public class Regress122887884Runner extends TestBase { + + private final Class<?> CLASS = Regress122887884.class; + private final String EXPECTED = StringUtils.lines("0"); + + @Test + public void test() throws Exception { + testForD8().addProgramClasses(CLASS).run(CLASS).assertSuccessWithOutput(EXPECTED); + } +}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java index ff84486..26e5356 100644 --- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java +++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1053,8 +1053,10 @@ "-optimizations ! xxx, ! yyy", "-optimizations !code/simplification/advanced,code/simplification/*")) { reset(); - parser.parse(createConfigurationForTesting(ImmutableList.of(option))); - verifyParserEndsCleanly(); + Path proguardConfig = writeTextToTempFile(option); + parser.parse(proguardConfig); + assertEquals(1, handler.warnings.size()); + checkDiagnostics(handler.warnings, proguardConfig, 1, 1, "Ignoring", "-optimizations"); } } @@ -1070,15 +1072,35 @@ ProguardConfigurationParser parser = new ProguardConfigurationParser(new DexItemFactory(), reporter); parser.parse(proguardConfig); - checkDiagnostics(handler.warnings, proguardConfig, 2, 1, - "Ignoring", "-optimizationpasses"); - Position p = handler.warnings.get(0).getPosition(); - assertTrue(p instanceof TextRange); - TextRange r = (TextRange) p; - assertEquals(2, r.getStart().getLine()); - assertEquals(1, r.getStart().getColumn()); - assertEquals(2, r.getEnd().getLine()); - assertEquals(22, r.getEnd().getColumn()); + assertEquals(3, handler.warnings.size()); + + checkDiagnostics(handler.warnings, 0, proguardConfig, 1, 1, "Ignoring", "-optimizations"); + Position p1 = handler.warnings.get(0).getPosition(); + assertTrue(p1 instanceof TextRange); + TextRange r1 = (TextRange) p1; + assertEquals(1, r1.getStart().getLine()); + assertEquals(1, r1.getStart().getColumn()); + assertEquals(1, r1.getEnd().getLine()); + assertEquals(15, r1.getEnd().getColumn()); + + checkDiagnostics( + handler.warnings, 1, proguardConfig, 2, 1, "Ignoring", "-optimizationpasses"); + Position p2 = handler.warnings.get(1).getPosition(); + assertTrue(p2 instanceof TextRange); + TextRange r2 = (TextRange) p2; + assertEquals(2, r2.getStart().getLine()); + assertEquals(1, r2.getStart().getColumn()); + assertEquals(2, r2.getEnd().getLine()); + assertEquals(22, r2.getEnd().getColumn()); + + checkDiagnostics(handler.warnings, 2, proguardConfig, 3, 1, "Ignoring", "-optimizations"); + Position p3 = handler.warnings.get(2).getPosition(); + assertTrue(p3 instanceof TextRange); + TextRange r3 = (TextRange) p3; + assertEquals(3, r3.getStart().getLine()); + assertEquals(1, r3.getStart().getColumn()); + assertEquals(3, r3.getEnd().getLine()); + assertEquals(15, r3.getEnd().getColumn()); } } @@ -1113,6 +1135,39 @@ } @Test + public void parseInvalidKeepOption() throws Exception { + Path proguardConfig = writeTextToTempFile( + "-keepx public class * { ", + " native <methods>; ", + "} " + ); + try { + ProguardConfigurationParser parser = + new ProguardConfigurationParser(new DexItemFactory(), reporter); + parser.parse(proguardConfig); + fail(); + } catch (AbortException e) { + checkDiagnostics(handler.errors, proguardConfig, 1, 1, + "Unknown option", "-keepx"); + } + } + + @Test + public void parseKeepOptionEOF() throws Exception { + Path proguardConfig = writeTextToTempFile( + System.lineSeparator(), ImmutableList.of("-keep"), false); + try { + ProguardConfigurationParser parser = + new ProguardConfigurationParser(new DexItemFactory(), reporter); + parser.parse(proguardConfig); + fail(); + } catch (AbortException e) { + checkDiagnostics(handler.errors, proguardConfig, 1, 6, + "Expected [!]interface|@interface|class|enum"); + } + } + + @Test public void parseInvalidKeepClassOption() throws Exception { Path proguardConfig = writeTextToTempFile( "-keepclassx public class * { ", @@ -1493,7 +1548,7 @@ parser.parse(createConfigurationForTesting(ImmutableList.of(option + " class A { *; }"))); fail("Expect to fail due to testing option being turned off."); } catch (AbortException e) { - assertEquals(2, handler.errors.size()); + assertEquals(1, handler.errors.size()); checkDiagnostics(handler.errors, 0, null, 1, 1, "Unknown option \"" + option + "\""); } }
diff --git a/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java b/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java new file mode 100644 index 0000000..d104272 --- /dev/null +++ b/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java
@@ -0,0 +1,113 @@ +// Copyright (c) 2019, 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.shaking.synthetic; + +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +// Generated by running tools/asmifier.py on the following code snippet: +// +// class Base { +// public static void foo() { +// System.out.println("Base::foo"); +// } +// static void bar() { +// Sub.foo(); +// } +// } +// +// then make bar _synthetic_ as `kotlinc` does. +class Base implements Opcodes { + + public static byte[] dump () throws Exception { + + ClassWriter classWriter = new ClassWriter(0); + MethodVisitor methodVisitor; + + classWriter.visit(V1_8, ACC_SUPER, "Base", null, "java/lang/Object", null); + + classWriter.visitSource("Arrays.kt", null); + + { + methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(3, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); + methodVisitor.visitInsn(RETURN); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "foo", "()V", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(5, label0); + methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + methodVisitor.visitLdcInsn("Base::foo"); + methodVisitor.visitMethodInsn( + INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLineNumber(6, label1); + methodVisitor.visitInsn(RETURN); + methodVisitor.visitMaxs(2, 0); + methodVisitor.visitEnd(); + } + { + methodVisitor = classWriter.visitMethod(ACC_STATIC | ACC_SYNTHETIC, "bar", "()V", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(9, label0); + methodVisitor.visitMethodInsn(INVOKESTATIC, "Sub", "foo", "()V", false); + Label label1 = new Label(); + methodVisitor.visitLabel(label1); + methodVisitor.visitLineNumber(10, label1); + methodVisitor.visitInsn(RETURN); + methodVisitor.visitMaxs(0, 0); + methodVisitor.visitEnd(); + } + classWriter.visitEnd(); + + return classWriter.toByteArray(); + } +} + +// Generated by running tools/asmifier.py on the following code snippet: +// class Sub extends Base {} +class Sub implements Opcodes { + + public static byte[] dump () throws Exception { + + ClassWriter classWriter = new ClassWriter(0); + MethodVisitor methodVisitor; + + classWriter.visit(V1_8, ACC_SUPER, "Sub", null, "Base", null); + + classWriter.visitSource("Arrays.kt", null); + + { + methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null); + methodVisitor.visitCode(); + Label label0 = new Label(); + methodVisitor.visitLabel(label0); + methodVisitor.visitLineNumber(13, label0); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "Base", "<init>", "()V", false); + methodVisitor.visitInsn(RETURN); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + classWriter.visitEnd(); + + return classWriter.toByteArray(); + } +} +
diff --git a/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java b/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java new file mode 100644 index 0000000..cad2410 --- /dev/null +++ b/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java
@@ -0,0 +1,46 @@ +// Copyright (c) 2019, 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.shaking.synthetic; + +import com.android.tools.r8.AsmTestBase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class StaticCallInSyntheticMethodAsmTest extends AsmTestBase { + private final Backend backend; + + @Parameterized.Parameters(name = "backend: {0}") + public static Backend[] data() { + return Backend.values(); + } + + public StaticCallInSyntheticMethodAsmTest(Backend backend) { + this.backend = backend; + } + + // class Base { + // static void foo() { ... } + // static synthetic void bar() { Sub.foo(); } + // } + // class Sub extends Base {} + // + // As per b/120971047, we do not add synthetic methods to the root set. + // When running the above example with -dontshrink, the static call in the synthetic method is not + // traced, so no chance to rebind that call to Base#foo. Then, at the end of dex writing, it hits + // assertion error in the naming lense that checks if call targets are eligible for renaming. + @Test + public void test() throws Exception { + // TODO(b/122819537): need to trace kotlinc-generated synthetic methods. + if (backend == Backend.DEX) { + return; + } + testForR8(backend) + .addProgramClassFileData(Base.dump(), Sub.dump()) + .addKeepRules("-dontshrink") + .compile(); + } + +}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java index c2ced01..8e24088 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -6,12 +6,18 @@ import com.android.tools.r8.errors.Unreachable; import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.naming.MemberNaming.MethodSignature; import com.android.tools.r8.naming.MemberNaming.Signature; public class AbsentMethodSubject extends MethodSubject { @Override + public IRCode buildIR() { + throw new Unreachable("Cannot build IR for an absent method"); + } + + @Override public boolean isPresent() { return false; }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java index 924ec2d..ee2155e 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -4,7 +4,6 @@ package com.android.tools.r8.utils.codeinspector; -import static org.junit.Assert.assertTrue; import com.android.tools.r8.cf.code.CfArithmeticBinop; import com.android.tools.r8.cf.code.CfCheckCast; @@ -31,7 +30,6 @@ import com.android.tools.r8.cf.code.CfStackInstruction; import com.android.tools.r8.cf.code.CfSwitch; import com.android.tools.r8.cf.code.CfThrow; -import com.android.tools.r8.graph.CfCode; import com.android.tools.r8.graph.DexField; import com.android.tools.r8.graph.DexMethod; import com.android.tools.r8.ir.code.Monitor.Type; @@ -167,6 +165,11 @@ } @Override + public boolean isReturn() { + return instruction instanceof CfReturn; + } + + @Override public boolean isReturnVoid() { return instruction instanceof CfReturnVoid; }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java index 1f520ff..9983320 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -58,7 +58,7 @@ public class CodeInspector { - private final DexApplication application; + final DexApplication application; final DexItemFactory dexItemFactory; private final ClassNameMapper mapping; final Map<String, String> originalToObfuscatedMapping;
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java index 6d463da..7051226 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -70,6 +70,7 @@ import com.android.tools.r8.code.NewInstance; import com.android.tools.r8.code.Nop; import com.android.tools.r8.code.PackedSwitch; +import com.android.tools.r8.code.Return; import com.android.tools.r8.code.ReturnObject; import com.android.tools.r8.code.ReturnVoid; import com.android.tools.r8.code.Sget; @@ -275,6 +276,11 @@ } @Override + public boolean isReturn() { + return instruction instanceof Return; + } + + @Override public boolean isReturnVoid() { return instruction instanceof ReturnVoid; }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java index d878a86..796532d 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -9,6 +9,7 @@ import com.android.tools.r8.code.Instruction; import com.android.tools.r8.errors.Unimplemented; import com.android.tools.r8.errors.Unreachable; +import com.android.tools.r8.graph.AppInfo; import com.android.tools.r8.graph.CfCode; import com.android.tools.r8.graph.Code; import com.android.tools.r8.graph.DexCode; @@ -17,10 +18,14 @@ import com.android.tools.r8.graph.DexDebugPositionState; import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexString; +import com.android.tools.r8.graph.GraphLense; import com.android.tools.r8.graph.JarCode; +import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.naming.MemberNaming; import com.android.tools.r8.naming.MemberNaming.MethodSignature; import com.android.tools.r8.naming.signature.GenericSignatureParser; +import com.android.tools.r8.origin.Origin; +import com.android.tools.r8.utils.InternalOptions; import it.unimi.dsi.fastutil.objects.Reference2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import java.util.Arrays; @@ -41,6 +46,19 @@ } @Override + public IRCode buildIR() { + DexEncodedMethod method = getMethod(); + return method + .getCode() + .buildIR( + method, + new AppInfo(codeInspector.application), + GraphLense.getIdentityLense(), + new InternalOptions(), + Origin.unknown()); + } + + @Override public boolean isPresent() { return true; } @@ -202,8 +220,7 @@ return !code.asCfCode().getLocalVariables().isEmpty(); } if (code.isJarCode()) { - return code.asJarCode().getNode().localVariables != null - && !code.asJarCode().getNode().localVariables.isEmpty(); + return code.asJarCode().hasLocalVariableTable(); } throw new Unreachable("Unexpected code type: " + code.getClass().getSimpleName()); }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java index 6abeacf..294cc8c 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -58,6 +58,8 @@ boolean isIfEqz(); + boolean isReturn(); + boolean isReturnVoid(); boolean isReturnObject();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java index b0d2c9b..8fb4001 100644 --- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java +++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -5,6 +5,7 @@ package com.android.tools.r8.utils.codeinspector; import com.android.tools.r8.graph.DexEncodedMethod; +import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.naming.MemberNaming.MethodSignature; import com.google.common.collect.Streams; import java.util.Iterator; @@ -13,6 +14,8 @@ public abstract class MethodSubject extends MemberSubject { + public abstract IRCode buildIR(); + public abstract boolean isAbstract(); public abstract boolean isBridge();
diff --git a/tools/apk-masseur.py b/tools/apk_masseur.py similarity index 85% rename from tools/apk-masseur.py rename to tools/apk_masseur.py index 4f24f0f..6013e6f 100755 --- a/tools/apk-masseur.py +++ b/tools/apk_masseur.py
@@ -36,10 +36,6 @@ if len(args) != 1: parser.error('Expected <apk> argument, got: ' + ' '.join(args)) apk = args[0] - if not options.out: - options.out = os.path.basename(apk) - if not options.keystore: - options.keystore = findKeystore() return (options, apk) def findKeystore(): @@ -81,28 +77,36 @@ subprocess.check_call(cmd) return signed_apk -def main(): - (options, apk) = parse_options() +def masseur( + apk, dex=None, out=None, adb_options=None, keystore=None, install=False): + if not out: + out = os.path.basename(apk) + if not keystore: + keystore = findKeystore() with utils.TempDir() as temp: processed_apk = None - if options.dex: - processed_apk = repack(options.dex, apk, temp) + if dex: + processed_apk = repack(dex, apk, temp) else: print 'Signing original APK without modifying dex files' processed_apk = os.path.join(temp, 'processed.apk') shutil.copyfile(apk, processed_apk) - signed_apk = sign(processed_apk, options.keystore, temp) + signed_apk = sign(processed_apk, keystore, temp) aligned_apk = align(signed_apk, temp) - print 'Writing result to', options.out - shutil.copyfile(aligned_apk, options.out) + print 'Writing result to', out + shutil.copyfile(aligned_apk, out) adb_cmd = ['adb'] - if options.adb_options: + if adb_options: adb_cmd.extend( - [option for option in options.adb_options.split(' ') if option]) - if options.install: - adb_cmd.extend(['install', '-t', '-r', '-d', options.out]); + [option for option in adb_options.split(' ') if option]) + if install: + adb_cmd.extend(['install', '-t', '-r', '-d', out]); utils.PrintCmd(adb_cmd) subprocess.check_call(adb_cmd) + +def main(): + (options, apk) = parse_options() + masseur(apk, **vars(options)) return 0 if __name__ == '__main__':
diff --git a/tools/as_utils.py b/tools/as_utils.py index d5db8a3..65ab470 100644 --- a/tools/as_utils.py +++ b/tools/as_utils.py
@@ -4,6 +4,7 @@ # BSD-style license that can be found in the LICENSE file. from distutils.version import LooseVersion +from HTMLParser import HTMLParser import os import shutil @@ -56,7 +57,8 @@ def remove_r8_dependency(checkout_dir): build_file = os.path.join(checkout_dir, 'build.gradle') - assert os.path.isfile(build_file), 'Expected a file to be present at {}'.format(build_file) + assert os.path.isfile(build_file), ( + 'Expected a file to be present at {}'.format(build_file)) with open(build_file) as f: lines = f.readlines() with open(build_file, 'w') as f: @@ -64,6 +66,54 @@ if (utils.R8_JAR not in line) and (utils.R8LIB_JAR not in line): f.write(line) +def IsGradleTaskName(x): + # Check that it is non-empty. + if not x: + return False + # Check that there is no whitespace. + for c in x: + if c.isspace(): + return False + # Check that the first character following an optional ':' is a lower-case + # alphabetic character. + c = x[0] + if c == ':' and len(x) >= 2: + c = x[1] + return c.isalpha() and c.islower() + +def IsGradleCompilerTask(x, shrinker): + if 'r8' in shrinker: + assert 'transformClassesWithDexBuilderFor' not in x + assert 'transformDexArchiveWithDexMergerFor' not in x + return 'transformClassesAndResourcesWithR8For' in x + + assert shrinker == 'proguard' + return ('transformClassesAndResourcesWithProguard' in x + or 'transformClassesWithDexBuilderFor' in x + or 'transformDexArchiveWithDexMergerFor' in x) + +def SetPrintConfigurationDirective(app, config, checkout_dir, destination): + proguard_config_file = FindProguardConfigurationFile( + app, config, checkout_dir) + with open(proguard_config_file) as f: + lines = f.readlines() + with open(proguard_config_file, 'w') as f: + for line in lines: + if '-printconfiguration' not in line: + f.write(line) + f.write('-printconfiguration {}\n'.format(destination)) + +def FindProguardConfigurationFile(app, config, checkout_dir): + app_module = config.get('app_module', 'app') + candidates = ['proguard-rules.pro', 'proguard-rules.txt', 'proguard.cfg'] + for candidate in candidates: + proguard_config_file = os.path.join(checkout_dir, app_module, candidate) + if os.path.isfile(proguard_config_file): + return proguard_config_file + # Currently assuming that the Proguard configuration file can be found at + # one of the predefined locations. + assert False + def Move(src, dst): print('Moving `{}` to `{}`'.format(src, dst)) dst_parent = os.path.dirname(dst) @@ -103,3 +153,60 @@ html_dir = os.path.dirname(html_file) for dir_name in ['css', 'js']: MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name)) + +def ParseProfileReport(profile_dir): + html_file = os.path.join(profile_dir, 'index.html') + assert os.path.isfile(html_file) + + parser = ProfileReportParser() + with open(html_file) as f: + for line in f.readlines(): + parser.feed(line) + return parser.result + +# A simple HTML parser that recognizes the following pattern: +# +# <tr> +# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td> +# <td class="numeric">3.490s</td> +# <td></td> +# </tr> +class ProfileReportParser(HTMLParser): + entered_table_row = False + entered_task_name_cell = False + entered_duration_cell = False + + current_task_name = None + current_duration = None + + result = {} + + def handle_starttag(self, tag, attrs): + entered_table_row_before = self.entered_table_row + entered_task_name_cell_before = self.entered_task_name_cell + + self.entered_table_row = (tag == 'tr') + self.entered_task_name_cell = (tag == 'td' and entered_table_row_before) + self.entered_duration_cell = ( + self.current_task_name + and tag == 'td' + and entered_task_name_cell_before) + + def handle_endtag(self, tag): + if tag == 'tr': + if self.current_task_name and self.current_duration: + self.result[self.current_task_name] = self.current_duration + self.current_task_name = None + self.current_duration = None + self.entered_table_row = False + + def handle_data(self, data): + stripped = data.strip() + if not stripped: + return + if self.entered_task_name_cell: + if IsGradleTaskName(stripped): + self.current_task_name = stripped + elif self.entered_duration_cell and stripped.endswith('s'): + self.current_duration = float(stripped[:-1]) + self.entered_table_row = False
diff --git a/tools/compare_apk_sizes.py b/tools/compare_apk_sizes.py index e477320..a8137c2 100755 --- a/tools/compare_apk_sizes.py +++ b/tools/compare_apk_sizes.py
@@ -23,6 +23,9 @@ result = optparse.OptionParser(usage=USAGE) result.add_option('--temp', help='Temporary directory to store extracted classes in') + result.add_option('--use_code_size', + help='Use the size of code segments instead of the full size of the dex.', + default=False, action='store_true') result.add_option('--report', help='Print comparison to this location instead of stdout') return result.parse_args() @@ -50,20 +53,37 @@ if toolhelper.run('d8', args) is not 0: raise Exception('Failed running d8') +def get_code_size(path): + segments = toolhelper.run('dexsegments', + [path], + build=False, + return_stdout=True) + for line in segments.splitlines(): + if 'Code' in line: + # The code size line looks like: + # - Code: 264 / 4 + splits = line.split(' ') + return int(splits[3]) + # Some classes has no code. + return 0 + class FileInfo: - def __init__(self, path, root): + def __init__(self, path, root, use_code_size): self.path = path self.full_path = os.path.join(root, path) - self.size = os.path.getsize(self.full_path) + if use_code_size: + self.size = get_code_size(self.full_path) + else: + self.size = os.path.getsize(self.full_path) -def generate_file_info(path): +def generate_file_info(path, options): file_info_map = {} with utils.ChangedWorkingDirectory(path): for root, dirs, files in os.walk('.'): for f in files: assert f.endswith('dex') file_path = os.path.join(root, f) - entry = FileInfo(file_path, path) + entry = FileInfo(file_path, path, use_code_size=options.use_code_size) file_info_map[file_path] = entry return file_info_map @@ -85,9 +105,9 @@ output.write('\n\n') -def compare(app1_classes_dir, app2_classes_dir, app1, app2, report): - app1_files = generate_file_info(app1_classes_dir) - app2_files = generate_file_info(app2_classes_dir) +def compare(app1_classes_dir, app2_classes_dir, app1, app2, options): + app1_files = generate_file_info(app1_classes_dir, options) + app2_files = generate_file_info(app2_classes_dir, options) only_in_app1 = [k for k in app1_files if k not in app2_files] only_in_app2 = [k for k in app2_files if k not in app1_files] in_both = [k for k in app2_files if k in app1_files] @@ -105,12 +125,12 @@ bigger_in_app2[f] = app2_entry.size - app1_entry.size else: same_size.append(f) - output = open(report, 'w') if report else sys.stdout + output = open(options.report, 'w') if options.report else sys.stdout print_info(app1, app1_files, only_in_app1, bigger_in_app1, output) print_info(app2, app2_files, only_in_app2, bigger_in_app2, output) output.write('Same size\n') output.write('\n'.join([' %s' % x for x in same_size])) - if report: + if options.report: output.close() def Main(): @@ -137,7 +157,7 @@ extract_classes(app1_input, app1_classes_dir) extract_classes(app2_input, app2_classes_dir) - compare(app1_classes_dir, app2_classes_dir, app1, app2, options.report) + compare(app1_classes_dir, app2_classes_dir, app1, app2, options) if __name__ == '__main__': sys.exit(Main())
diff --git a/tools/dex2oat.py b/tools/dex2oat.py index 1e2cffd..e21dc7d 100755 --- a/tools/dex2oat.py +++ b/tools/dex2oat.py
@@ -14,6 +14,8 @@ VERSIONS = [ 'default', + '9.0.0', + '8.1.0', '7.0.0', '6.0.1', '5.1.1', @@ -21,6 +23,8 @@ DIRS = { 'default': 'art', + '9.0.0': 'art-9.0.0', + '8.1.0': 'art-8.1.0', '7.0.0': 'art-7.0.0', '6.0.1': 'art-6.0.1', '5.1.1': 'art-5.1.1', @@ -28,6 +32,8 @@ PRODUCTS = { 'default': 'angler', + '9.0.0': 'marlin', + '8.1.0': 'marlin', '7.0.0': 'angler', '6.0.1': 'angler', '5.1.1': 'mako', @@ -35,11 +41,23 @@ ARCHS = { 'default': 'arm64', + '9.0.0': 'arm64', + '8.1.0': 'arm64', '7.0.0': 'arm64', '6.0.1': 'arm64', '5.1.1': 'arm', } +VERBOSE_OPTIONS = [ + 'verifier', + 'compiler', + 'gc', + 'jit', + 'jni', + 'class', + 'all', +] + def ParseOptions(): parser = optparse.OptionParser() parser.add_option('--version', @@ -53,6 +71,10 @@ parser.add_option('--output', help='Where to place the output oat (defaults to no output / temp file).', default=None) + parser.add_option('--verbose', + help='Enable verbose dex2oat logging.', + choices=VERBOSE_OPTIONS, + default=None) return parser.parse_args() def Main(): @@ -66,12 +88,15 @@ dexfile = args[0] oatfile = options.output versions = VERSIONS if options.all else [options.version] + verbose = [options.verbose] if options.verbose else [] + if 'all' in verbose: + verbose = [x for x in VERBOSE_OPTIONS if x is not 'all'] for version in versions: - run(dexfile, oatfile, version) + run(dexfile, oatfile, version, verbose) print return 0 -def run(dexfile, oatfile=None, version='default'): +def run(dexfile, oatfile=None, version='default', verbose=[]): # dex2oat accepts non-existent dex files, check here instead if not os.path.exists(dexfile): raise Exception('DEX file not found: "{}"'.format(dexfile)) @@ -90,6 +115,8 @@ '--oat-file=' + oatfile, '--instruction-set=' + arch, ] + for flag in verbose: + cmd += ['--runtime-arg', '-verbose:' + flag] env = {"LD_LIBRARY_PATH": os.path.join(base, 'lib')} utils.PrintCmd(cmd) subprocess.check_call(cmd, env = env)
diff --git a/tools/internal_test.py b/tools/internal_test.py index be9779a..5bf1da2 100755 --- a/tools/internal_test.py +++ b/tools/internal_test.py
@@ -67,6 +67,7 @@ def log(str): print("%s: %s" % (time.strftime("%c"), str)) + sys.stdout.flush() def ParseOptions(): result = optparse.OptionParser() @@ -90,6 +91,9 @@ def restart_if_new_version(original_content): new_content = get_own_file_content() + log('Lengths %s %s' % (len(original_content), len(new_content))) + log('is master %s ' % utils.is_master()) + # Restart if the script got updated. if new_content != original_content: log('Restarting tools/internal_test.py, content changed') os.execv(sys.argv[0], sys.argv)
diff --git a/tools/retrace.py b/tools/retrace.py index 3b8da1d..1b77e9a 100755 --- a/tools/retrace.py +++ b/tools/retrace.py
@@ -3,13 +3,58 @@ # 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. +import archive +import argparse import subprocess import sys +import tempfile import utils +def parse_arguments(): + parser = argparse.ArgumentParser( + description = 'R8lib wrapper for retrace tool.') + parser.add_argument( + '-c', + '--commit_hash', + help='Commit hash to download r8lib map file for.', + default=None) + parser.add_argument( + '--version', + help='Version to download r8lib map file for.', + default=None) + parser.add_argument( + '--map', + help='Path to r8lib map.', + default=utils.R8LIB_JAR + '.map') + parser.add_argument( + '--stacktrace', + help='Path to stacktrace file.', + default=None) + return parser.parse_args() + + def main(): - # Run the retrace tool with standard r8lib arguments. - subprocess.call(['java', '-jar', utils.RETRACE_JAR, '-verbose', utils.R8LIB_JAR + '.map']) + args = parse_arguments() + r8lib_map_path = args.map + hashOrVersion = args.commit_hash or args.version + if hashOrVersion: + download_path = archive.GetUploadDestination( + hashOrVersion, + 'r8lib.jar.map', + args.commit_hash is not None) + if utils.file_exists_on_cloud_storage(download_path): + r8lib_map_path = tempfile.NamedTemporaryFile().name + utils.download_file_from_cloud_storage(download_path, r8lib_map_path) + else: + print('Could not find map file from argument: %s.' % hashOrVersion) + return 1 + + retrace_args = ['java', '-jar', utils.RETRACE_JAR, r8lib_map_path] + if args.stacktrace: + retrace_args.append(args.stacktrace) + + return subprocess.call(retrace_args) + if __name__ == '__main__': sys.exit(main())
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py index 7c96506..5dc42ec 100755 --- a/tools/run_on_as_app.py +++ b/tools/run_on_as_app.py
@@ -3,19 +3,21 @@ # 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. +import apk_masseur import apk_utils import gradle import os import optparse import subprocess import sys +import tempfile import time import utils import zipfile import as_utils -SHRINKERS = ['r8', 'r8full', 'r8-minified', 'r8full-minified', 'proguard'] +SHRINKERS = ['r8', 'r8-minified', 'r8full', 'r8full-minified', 'proguard'] WORKING_DIR = utils.BUILD if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']): @@ -77,6 +79,14 @@ 'flavor': 'standard', 'releaseTarget': 'app:assembleRelease', }, + 'tivi': { + 'app_id': 'app.tivi', + # Forked from https://github.com/chrisbanes/tivi.git removing + # signingConfigs. + 'git_repo': 'https://github.com/sgjesse/tivi.git', + # TODO(123047413): Fails with R8. + 'skip': True, + }, # This does not build yet. 'muzei': { 'git_repo': 'https://github.com/sgjesse/muzei.git', @@ -128,10 +138,35 @@ subprocess.check_call( ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest]) +def PercentageDiffAsString(before, after): + if after < before: + return '-' + str(round((1.0 - after / before) * 100)) + '%' + else: + return '+' + str(round((after - before) / before * 100)) + '%' + +def UninstallApkOnEmulator(app, config): + app_id = config.get('app_id') + process = subprocess.Popen( + ['adb', 'uninstall', app_id], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + if stdout.strip() == 'Success': + # Successfully uninstalled + return + + if 'Unknown package: {}'.format(app_id) in stderr: + # Application not installed + return + + raise Exception( + 'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format( + app_id, stdout, stderr)) + def WaitForEmulator(): stdout = subprocess.check_output(['adb', 'devices']) if '{}\tdevice'.format(emulator_id) in stdout: - return + return True print('Emulator \'{}\' not connected; waiting for connection'.format( emulator_id)) @@ -144,9 +179,9 @@ if '{}\tdevice'.format(emulator_id) not in stdout: print('... still waiting for connection') if time_waited >= 5 * 60: - raise Exception('No emulator connected for 5 minutes') + return False else: - return + return True def GetResultsForApp(app, config, options): git_repo = config['git_repo'] @@ -190,34 +225,63 @@ apk_dest = None result = {} try: - (apk_dest, profile_dest_dir) = BuildAppWithShrinker( - app, config, shrinker, checkout_dir, options) + (apk_dest, profile_dest_dir, proguard_config_file) = \ + BuildAppWithShrinker(app, config, shrinker, checkout_dir, options) dex_size = ComputeSizeOfDexFilesInApk(apk_dest) result['apk_dest'] = apk_dest, result['build_status'] = 'success' result['dex_size'] = dex_size result['profile_dest_dir'] = profile_dest_dir + + profile = as_utils.ParseProfileReport(profile_dest_dir) + result['profile'] = { + task_name:duration for task_name, duration in profile.iteritems() + if as_utils.IsGradleCompilerTask(task_name, shrinker)} except Exception as e: warn('Failed to build {} with {}'.format(app, shrinker)) if e: print('Error: ' + str(e)) result['build_status'] = 'failed' - if options.monkey: - if result.get('build_status') == 'success': + if result.get('build_status') == 'success': + if options.monkey: result['monkey_status'] = 'success' if RunMonkey( - app, config, apk_dest) else 'failed' + app, config, options, apk_dest) else 'failed' + + if 'r8' in shrinker and options.r8_compilation_steps > 1: + recompilation_results = [] + previous_apk = apk_dest + for i in range(1, options.r8_compilation_steps): + try: + recompiled_apk_dest = os.path.join( + checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i)) + RebuildAppWithShrinker( + previous_apk, recompiled_apk_dest, proguard_config_file, shrinker) + recompilation_result = { + 'apk_dest': recompiled_apk_dest, + 'build_status': 'success', + 'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest) + } + if options.monkey: + recompilation_result['monkey_status'] = 'success' if RunMonkey( + app, config, options, recompiled_apk_dest) else 'failed' + recompilation_results.append(recompilation_result) + previous_apk = recompiled_apk_dest + except Exception as e: + warn('Failed to recompile {} with {}'.format(app, shrinker)) + recompilation_results.append({ 'build_status': 'failed' }) + break + result['recompilation_results'] = recompilation_results result_per_shrinker[shrinker] = result - if IsTrackedByGit('gradle.properties'): - GitCheckout('gradle.properties') - return result_per_shrinker def BuildAppWithShrinker(app, config, shrinker, checkout_dir, options): + print() print('Building {} with {}'.format(app, shrinker)) + # Add/remove 'r8.jar' from top-level build.gradle. if options.disable_tot: as_utils.remove_r8_dependency(checkout_dir) else: @@ -227,23 +291,16 @@ archives_base_name = config.get(' archives_base_name', app_module) flavor = config.get('flavor') - # Ensure that gradle.properties are not modified before modifying it to - # select shrinker. - if IsTrackedByGit('gradle.properties'): - GitCheckout('gradle.properties') - with open("gradle.properties", "a") as gradle_properties: - if 'r8' in shrinker: - gradle_properties.write('\nandroid.enableR8=true\n') - if shrinker == 'r8full' or shrinker == 'r8full-minified': - gradle_properties.write('android.enableR8.fullMode=true\n') - else: - assert shrinker == 'proguard' - gradle_properties.write('\nandroid.enableR8=false\n') - out = os.path.join(checkout_dir, 'out', shrinker) if not os.path.exists(out): os.makedirs(out) + # Set -printconfiguration in Proguard rules. + proguard_config_dest = os.path.abspath( + os.path.join(out, 'proguard-rules.pro')) + as_utils.SetPrintConfigurationDirective( + app, config, checkout_dir, proguard_config_dest) + env = os.environ.copy() env['ANDROID_HOME'] = android_home env['JAVA_OPTS'] = '-ea' @@ -252,8 +309,17 @@ releaseTarget = app_module + ':' + 'assemble' + ( flavor.capitalize() if flavor else '') + 'Release' - cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget, '--profile', - '--stacktrace'] + # Value for property android.enableR8. + enableR8 = 'r8' in shrinker + # Value for property android.enableR8.fullMode. + enableR8FullMode = shrinker == 'r8full' or shrinker == 'r8full-minified' + # Build gradlew command line. + cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget, + '--profile', '--stacktrace', + '-Pandroid.enableR8=' + str(enableR8).lower(), + '-Pandroid.enableR8.fullMode=' + str(enableR8FullMode).lower()] + if options.gradle_flags: + cmd.extend(options.gradle_flags.split(' ')) utils.PrintCmd(cmd) build_process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE) @@ -307,16 +373,43 @@ profile_dest_dir = os.path.join(out, 'profile') as_utils.MoveProfileReportTo(profile_dest_dir, stdout) - return (apk_dest, profile_dest_dir) + return (apk_dest, profile_dest_dir, proguard_config_dest) -def RunMonkey(app, config, apk_dest): - WaitForEmulator() +def RebuildAppWithShrinker(apk, apk_dest, proguard_config_file, shrinker): + assert 'r8' in shrinker + assert apk_dest.endswith('.apk') + + # Compile given APK with shrinker to temporary zip file. + api = 28 # TODO(christofferqa): Should be the one from build.gradle + android_jar = os.path.join(utils.REPO_ROOT, utils.ANDROID_JAR.format(api=api)) + r8_jar = utils.R8LIB_JAR if IsMinifiedR8(shrinker) else utils.R8_JAR + zip_dest = apk_dest[:-3] + '.zip' + + cmd = ['java', '-ea', '-jar', r8_jar, '--release', '--pg-conf', + proguard_config_file, '--lib', android_jar, '--output', zip_dest, apk] + utils.PrintCmd(cmd) + + subprocess.check_output(cmd) + + # Make a copy of the given APK, move the newly generated dex files into the + # copied APK, and then sign the APK. + apk_masseur.masseur(apk, dex=zip_dest, out=apk_dest) + +def RunMonkey(app, config, options, apk_dest): + if not WaitForEmulator(): + return False + + UninstallApkOnEmulator(app, config) InstallApkOnEmulator(apk_dest) app_id = config.get('app_id') - number_of_events_to_generate = 50 + number_of_events_to_generate = options.monkey_events - cmd = ['adb', 'shell', 'monkey', '-p', app_id, + # Intentionally using a constant seed such that the monkey generates the same + # event sequence for each shrinker. + random_seed = 42 + + cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed), str(number_of_events_to_generate)] utils.PrintCmd(cmd) @@ -336,8 +429,10 @@ print(' skipped ({})'.format(error_message)) continue - baseline = float( - result_per_shrinker.get('proguard', {}).get('dex_size', -1)) + proguard_result = result_per_shrinker.get('proguard', {}) + proguard_dex_size = float(proguard_result.get('dex_size', -1)) + proguard_duration = sum(proguard_result.get('profile', {}).values()) + for shrinker in SHRINKERS: if shrinker not in result_per_shrinker: continue @@ -348,23 +443,50 @@ else: print(' {}:'.format(shrinker)) dex_size = result.get('dex_size') - if dex_size != baseline and baseline >= 0: - if dex_size < baseline: - success(' dex size: {} ({}, -{}%)'.format( - dex_size, dex_size - baseline, - round((1.0 - dex_size / baseline) * 100), 1)) - elif dex_size >= baseline: - warn(' dex size: {} ({}, +{}%)'.format( - dex_size, dex_size - baseline, - round((baseline - dex_size) / dex_size * 100, 1))) + msg = ' dex size: {}'.format(dex_size) + if dex_size != proguard_dex_size and proguard_dex_size >= 0: + msg = '{} ({}, {})'.format( + msg, dex_size - proguard_dex_size, + PercentageDiffAsString(proguard_dex_size, dex_size)) + success(msg) if dex_size < proguard_dex_size else warn(msg) else: - print(' dex size: {}'.format(dex_size)) + print(msg) + + profile = result.get('profile') + duration = sum(profile.values()) + msg = ' performance: {}s'.format(duration) + if duration != proguard_duration and proguard_duration > 0: + msg = '{} ({}s, {})'.format( + msg, duration - proguard_duration, + PercentageDiffAsString(proguard_duration, duration)) + success(msg) if duration < proguard_duration else warn(msg) + else: + print(msg) + if len(profile) >= 2: + for task_name, task_duration in profile.iteritems(): + print(' {}: {}s'.format(task_name, task_duration)) + if options.monkey: monkey_status = result.get('monkey_status') if monkey_status != 'success': warn(' monkey: {}'.format(monkey_status)) else: success(' monkey: {}'.format(monkey_status)) + recompilation_results = result.get('recompilation_results', []) + i = 1 + for recompilation_result in recompilation_results: + build_status = recompilation_result.get('build_status') + if build_status != 'success': + print(' recompilation #{}: {}'.format(i, build_status)) + else: + dex_size = recompilation_result.get('dex_size') + print(' recompilation #{}'.format(i)) + print(' dex size: {}'.format(dex_size)) + if options.monkey: + monkey_status = recompilation_result.get('monkey_status') + msg = ' monkey: {}'.format(monkey_status) + success(msg) if monkey_status == 'success' else warn(msg) + i += 1 def ParseOptions(argv): result = optparse.OptionParser() @@ -375,6 +497,10 @@ help='Whether to install and run app(s) with monkey', default=False, action='store_true') + result.add_option('--monkey_events', + help='Number of events that the monkey should trigger', + default=250, + type=int) result.add_option('--pull', help='Whether to pull the latest version of each app', default=False, @@ -386,6 +512,10 @@ result.add_option('--shrinker', help='The shrinkers to use (by default, all are run)', action='append') + result.add_option('--r8-compilation-steps', '--r8_compilation_steps', + help='Number of times R8 should be run on each app', + default=2, + type=int) result.add_option('--disable-tot', '--disable_tot', help='Whether to disable the use of the ToT version of R8', default=False, @@ -394,7 +524,12 @@ help='Run without building ToT first (only when using ToT)', default=False, action='store_true') + result.add_option('--gradle-flags', '--gradle_flags', + help='Flags to pass in to gradle') (options, args) = result.parse_args(argv) + if options.disable_tot: + # r8.jar is required for recompiling the generated APK + options.r8_compilation_steps = 1 if options.shrinker: for shrinker in options.shrinker: assert shrinker in SHRINKERS
diff --git a/tools/toolhelper.py b/tools/toolhelper.py index 86787be..d7cf222 100644 --- a/tools/toolhelper.py +++ b/tools/toolhelper.py
@@ -10,7 +10,7 @@ def run(tool, args, build=None, debug=True, profile=False, track_memory_file=None, extra_args=None, - stderr=None, stdout=None): + stderr=None, stdout=None, return_stdout=False): if build is None: build, args = extract_build_from_args(args) if build: @@ -36,6 +36,8 @@ cmd.extend(["--lib", lib]) cmd.extend(args) utils.PrintCmd(cmd) + if return_stdout: + return subprocess.check_output(cmd) return subprocess.call(cmd, stdout=stdout, stderr=stderr) def run_in_tests(tool, args, build=None, debug=True, extra_args=None):
diff --git a/tools/utils.py b/tools/utils.py index bd54dbc..f8b4469 100644 --- a/tools/utils.py +++ b/tools/utils.py
@@ -87,6 +87,11 @@ sha1.update(chunk) return sha1.hexdigest() +def is_master(): + remotes = subprocess.check_output(['git', 'branch', '-r', '--contains', + 'HEAD']) + return 'origin/master' in remotes + def get_HEAD_sha1(): cmd = ['git', 'rev-parse', 'HEAD'] PrintCmd(cmd)