com.android.build.gradle.shrinker.IncrementalShrinker Maven / Gradle / Ivy
Show all versions of gradle-core Show documentation
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.shrinker;
import static com.google.common.base.Preconditions.checkState;
import com.android.annotations.NonNull;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.ide.common.internal.WaitableExecutor;
import com.google.common.base.Optional;
import com.google.common.base.Stopwatch;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.objectweb.asm.ClassReader;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* Code for incremental shrinking.
*/
public class IncrementalShrinker extends AbstractShrinker {
/**
* Exception thrown when the incremental shrinker detects incompatible changes and requests
* a full run instead.
*/
public static class IncrementalRunImpossibleException extends RuntimeException {
IncrementalRunImpossibleException(String message) {
super(message);
}
IncrementalRunImpossibleException(String message, Throwable cause) {
super(message, cause);
}
}
public IncrementalShrinker(
WaitableExecutor executor,
ShrinkerGraph graph,
ShrinkerLogger shrinkerLogger) {
super(graph, executor, shrinkerLogger);
}
/**
* Perform incremental shrinking, in the supported cases (where only code in pre-existing
* methods has been modified).
*
* The general idea is this: for every method in modified classes, remove all outgoing
* "code reference" edges, add them again based on the current code and then set the counters
* again (traverse the graph) using the new set of edges.
*
*
The counters are re-calculated every time from scratch (starting from known entry points
* from the config file) to avoid cycles being left in the output.
*
* @throws IncrementalRunImpossibleException If incremental shrinking is impossible and a full
* run should be done instead.
*/
public void incrementalRun(
@NonNull Iterable inputs,
@NonNull TransformOutputProvider output)
throws IOException, IncrementalRunImpossibleException {
final Set classesToWrite = Sets.newConcurrentHashSet();
final Set classFilesToDelete = Sets.newConcurrentHashSet();
final Set> unresolvedReferences = Sets.newConcurrentHashSet();
Stopwatch stopwatch = Stopwatch.createStarted();
SetMultimap oldState = resetState();
logTime("resetState()", stopwatch);
processInputs(inputs, classesToWrite, unresolvedReferences);
logTime("processInputs", stopwatch);
finishGraph(unresolvedReferences);
logTime("finish graph", stopwatch);
setCounters(CounterSet.SHRINK);
logTime("set counters", stopwatch);
chooseClassesToWrite(inputs, output, classesToWrite, classFilesToDelete, oldState);
logTime("choose classes", stopwatch);
updateClassFiles(classesToWrite, classFilesToDelete, inputs, output);
logTime("update class files", stopwatch);
mGraph.saveState();
logTime("save state", stopwatch);
}
/**
* Decides which classes need to be updated on disk and which need to be deleted. It puts
* appropriate entries in the lists passed as arguments.
*/
private void chooseClassesToWrite(
@NonNull Iterable inputs,
@NonNull TransformOutputProvider output,
@NonNull Collection classesToWrite,
@NonNull Collection classFilesToDelete,
@NonNull SetMultimap oldState) {
for (T klass : mGraph.getReachableClasses(CounterSet.SHRINK)) {
if (!oldState.containsKey(klass)) {
classesToWrite.add(klass);
} else {
Set newMembers = mGraph.getReachableMembersLocalNames(klass, CounterSet.SHRINK);
Set oldMembers = oldState.get(klass);
// Reverse of the trick above, where we store one artificial member for empty
// classes.
if (oldMembers.size() == 1) {
oldMembers.remove(mGraph.getClassName(klass));
}
if (!newMembers.equals(oldMembers)) {
classesToWrite.add(klass);
}
}
oldState.removeAll(klass);
}
// All keys that remained in oldState should be deleted.
for (T klass : oldState.keySet()) {
File sourceFile = mGraph.getSourceFile(klass);
checkState(sourceFile != null, "One of the inputs has no source file.");
Optional outputFile = chooseOutputFile(klass, sourceFile, inputs, output);
if (!outputFile.isPresent()) {
throw new IllegalStateException(
"Can't determine path of " + mGraph.getClassName(klass));
}
classFilesToDelete.add(outputFile.get());
}
}
/**
* Saves all reachable classes and members in a {@link SetMultimap} and clears all counters, so
* that the graph can be traversed again, using the new edges.
*
* Returns a multimap that contains names of all reachable members for every reachable class.
*/
@NonNull
private SetMultimap resetState() {
SetMultimap oldState = HashMultimap.create();
for (T klass : mGraph.getReachableClasses(CounterSet.SHRINK)) {
Set reachableMembers = mGraph.getReachableMembersLocalNames(klass, CounterSet.SHRINK);
for (String member : reachableMembers) {
oldState.put(klass, member);
}
// Make sure the key is in the map.
if (reachableMembers.isEmpty()) {
oldState.put(klass, mGraph.getClassName(klass));
}
}
mGraph.clearCounters(mExecutor);
waitForAllTasks();
return oldState;
}
private void finishGraph(@NonNull Iterable> unresolvedReferences) {
resolveReferences(unresolvedReferences);
waitForAllTasks();
}
private void processInputs(
@NonNull Iterable inputs,
@NonNull final Collection classesToWrite,
@NonNull final Collection> unresolvedReferences)
throws IncrementalRunImpossibleException {
for (final TransformInput input : inputs) {
for (JarInput jarInput : input.getJarInputs()) {
switch (jarInput.getStatus()) {
case ADDED:
case REMOVED:
case CHANGED:
//noinspection StringToUpperCaseOrToLowerCaseWithoutLocale
throw new IncrementalRunImpossibleException(
String.format(
"Input jar %s has been %s.",
jarInput.getFile(),
jarInput.getStatus().name().toLowerCase()));
case NOTCHANGED:
break;
}
}
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
for (final Map.Entry changedFile : directoryInput.getChangedFiles().entrySet()) {
mExecutor.execute(new Callable() {
@Override
public Void call() throws Exception {
switch (changedFile.getValue()) {
case ADDED:
throw new IncrementalRunImpossibleException(
String.format(
"File %s added.", changedFile.getKey()));
case REMOVED:
throw new IncrementalRunImpossibleException(
String.format(
"File %s removed.", changedFile.getKey()));
case CHANGED:
processChangedClassFile(
changedFile.getKey(),
unresolvedReferences,
classesToWrite);
break;
}
return null;
}
});
}
}
}
waitForAllTasks();
}
/**
* Handles a changed class file by removing old code references (graph edges) and adding
* up-to-date edges, according to the current state of the class.
*
* This only works on {@link DependencyType#REQUIRED_CODE_REFERENCE} edges, which are only
* ever created from method containing the opcode to target member. The first pass is equivalent
* to removing all code from the method, the second to adding "current" opcodes to it.
*
* @throws IncrementalRunImpossibleException If current members of the class are not the same as
* they used to be. This means that edges of other types need to be updated, and we don't
* handle this incrementally. It also means that -keep rules would need to be re-applied,
* which is something we also don't do incrementally.
*/
private void processChangedClassFile(
@NonNull File file,
@NonNull Collection> unresolvedReferences,
@NonNull Collection classesToWrite)
throws IncrementalRunImpossibleException {
try {
ClassReader classReader = new ClassReader(Files.toByteArray(file));
IncrementalRunVisitor visitor =
new IncrementalRunVisitor<>(mGraph, classesToWrite, unresolvedReferences);
DependencyRemoverVisitor remover = new DependencyRemoverVisitor<>(mGraph, visitor);
classReader.accept(remover, 0);
} catch (IncrementalRunImpossibleException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("Failed to process " + file.getAbsolutePath(), e);
}
}
@Override
protected void waitForAllTasks() {
try {
super.waitForAllTasks();
} catch (RuntimeException e) {
if (e.getCause() instanceof IncrementalRunImpossibleException) {
throw (IncrementalRunImpossibleException) e.getCause();
} else {
throw e;
}
}
}
}