/* * Copyright (C) 2014 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.builder.internal.compiler; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.annotations.concurrency.GuardedBy; import com.android.annotations.concurrency.Immutable; import com.android.ide.common.xml.XmlPrettyPrinter; import com.android.sdklib.repository.FullRevision; import com.android.utils.ILogger; import com.android.utils.Pair; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.base.Objects; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.google.common.io.Files; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; /** */ public abstract class PreProcessCache<T extends PreProcessCache.Key> { private static final String NODE_ITEMS = "items"; private static final String NODE_ITEM = "item"; private static final String NODE_DEX = "dex"; private static final String ATTR_VERSION = "version"; private static final String ATTR_JAR = "jar"; private static final String ATTR_DEX = "dex"; private static final String ATTR_SHA1 = "sha1"; private static final String ATTR_REVISION = "revision"; private static final String XML_VERSION = "2"; protected interface BaseItem { @NonNull File getSourceFile(); @NonNull List<File> getOutputFiles(); @Nullable HashCode getSourceHash(); boolean areOutputFilesPresent(); } /** * Items representing jar/dex files that have been processed during a build. */ @Immutable protected static class Item implements BaseItem { @NonNull private final File mSourceFile; @NonNull private final List<File> mOutputFiles; @NonNull private final CountDownLatch mLatch; Item( @NonNull File sourceFile, @NonNull List<File> outputFiles, @NonNull CountDownLatch latch) { mSourceFile = sourceFile; mOutputFiles = Lists.newArrayList(outputFiles); mLatch = latch; } Item( @NonNull File sourceFile, @NonNull CountDownLatch latch) { mSourceFile = sourceFile; mOutputFiles = Lists.newArrayList(); mLatch = latch; } @Override @NonNull public File getSourceFile() { return mSourceFile; } @Override @NonNull public List<File> getOutputFiles() { return mOutputFiles; } @Nullable @Override public HashCode getSourceHash() { return null; } @NonNull protected CountDownLatch getLatch() { return mLatch; } @Override public boolean areOutputFilesPresent() { boolean filesOk = !mOutputFiles.isEmpty(); for (File outputFile : mOutputFiles) { filesOk &= outputFile.isFile(); } return filesOk; } @Override public String toString() { return "Item{" + "mOutputFiles=" + mOutputFiles + ", mSourceFile=" + mSourceFile + '}'; } } /** * Items representing jar/dex files that have been processed in a previous build, then were * stored in a cache file and then reloaded during the current build. */ @Immutable protected static class StoredItem implements BaseItem { @NonNull private final File mSourceFile; @NonNull private final List<File> mOutputFiles; @NonNull private final HashCode mSourceHash; StoredItem( @NonNull File sourceFile, @NonNull List<File> outputFiles, @NonNull HashCode sourceHash) { mSourceFile = sourceFile; mOutputFiles = Lists.newArrayList(outputFiles); mSourceHash = sourceHash; } @Override @NonNull public File getSourceFile() { return mSourceFile; } @Override @NonNull public List<File> getOutputFiles() { return mOutputFiles; } @Override @NonNull public HashCode getSourceHash() { return mSourceHash; } @Override public boolean areOutputFilesPresent() { boolean filesOk = !mOutputFiles.isEmpty(); for (File outputFile : mOutputFiles) { filesOk &= outputFile.isFile(); } return filesOk; } @Override public String toString() { return "StoredItem{" + "mSourceFile=" + mSourceFile + ", mOutputFiles=" + mOutputFiles + ", mSourceHash=" + mSourceHash + '}'; } } /** * Key to store Item/StoredItem in maps. * The key contains the element that are used for the dex call: * - source file * - build tools revision * - jumbo mode */ @Immutable protected static class Key { @NonNull private final File mSourceFile; @NonNull private final FullRevision mBuildToolsRevision; public static Key of(@NonNull File sourceFile, @NonNull FullRevision buildToolsRevision) { return new Key(sourceFile, buildToolsRevision); } protected Key(@NonNull File sourceFile, @NonNull FullRevision buildToolsRevision) { mSourceFile = sourceFile; mBuildToolsRevision = buildToolsRevision; } @NonNull public FullRevision getBuildToolsRevision() { return mBuildToolsRevision; } @NonNull public File getSourceFile() { return mSourceFile; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Key)) { return false; } Key key = (Key) o; if (!mBuildToolsRevision.equals(key.mBuildToolsRevision)) { return false; } if (!mSourceFile.equals(key.mSourceFile)) { return false; } return true; } @Override public int hashCode() { return Objects.hashCode(mSourceFile, mBuildToolsRevision); } } protected interface KeyFactory<T> { T of(@NonNull File sourceFile, @NonNull FullRevision revision, @NonNull NamedNodeMap attrMap); } @GuardedBy("this") private boolean mLoaded = false; @GuardedBy("this") private final Map<T, Item> mMap = Maps.newHashMap(); @GuardedBy("this") private final Map<T, StoredItem> mStoredItems = Maps.newHashMap(); @GuardedBy("this") private int mMisses = 0; @GuardedBy("this") private int mHits = 0; @NonNull protected abstract KeyFactory<T> getKeyFactory(); /** * Loads the stored item. This can be called several times (per subproject), so only * the first call should do something. */ public synchronized void load(@NonNull File itemStorage) { if (mLoaded) { return; } loadItems(itemStorage); mLoaded = true; } /** * Returns an {@link Item} loaded from the cache. If no item can be found this, throws an * exception. * * @param itemKey the key of the item * @return a pair of item, boolean */ protected synchronized Pair<Item, Boolean> getItem(@NonNull T itemKey) { // get the item Item item = mMap.get(itemKey); boolean newItem = false; if (item == null) { // check if we have a stored version. StoredItem storedItem = mStoredItems.get(itemKey); File inputFile = itemKey.getSourceFile(); if (storedItem != null) { // check the sha1 is still valid, and the pre-dex files are still there. if (storedItem.areOutputFilesPresent() && storedItem.getSourceHash().equals(getHash(inputFile))) { Logger.getAnonymousLogger().info("Cached result for getItem(" + inputFile + "): " + storedItem.getOutputFiles()); for (File f : storedItem.getOutputFiles()) { Logger.getAnonymousLogger().info( String.format("%s l:%d ts:%d", f, f.length(), f.lastModified())); } // create an item where the outFile is the one stored since it // represent the pre-dexed library already. // Next time this lib needs to be pre-dexed, we'll use the item // rather than the stored item, allowing us to not compute the sha1 again. // Use a 0-count latch since there is nothing to do. item = new Item(inputFile, storedItem.getOutputFiles(), new CountDownLatch(0)); } } // if we didn't find a valid stored item, create a new one. if (item == null) { item = new Item(inputFile, new CountDownLatch(1)); newItem = true; } mMap.put(itemKey, item); } return Pair.of(item, newItem); } @Nullable private static HashCode getHash(@NonNull File file) { try { return Files.hash(file, Hashing.sha1()); } catch (IOException ignored) { } return null; } public synchronized void clear(@Nullable File itemStorage, @Nullable ILogger logger) throws IOException { if (!mMap.isEmpty()) { if (itemStorage != null) { saveItems(itemStorage); } if (logger != null) { logger.info("PREDEX CACHE HITS: " + mHits); logger.info("PREDEX CACHE MISSES: " + mMisses); } } mMap.clear(); mStoredItems.clear(); mHits = 0; mMisses = 0; } private synchronized void loadItems(@NonNull File itemStorage) { if (!itemStorage.isFile()) { return; } try { Document document = XmlUtils.parseUtfXmlFile(itemStorage, true); // get the root node Node rootNode = document.getDocumentElement(); if (rootNode == null || !NODE_ITEMS.equals(rootNode.getLocalName())) { return; } // check the version of the XML NamedNodeMap rootAttrMap = rootNode.getAttributes(); Node versionAttr = rootAttrMap.getNamedItem(ATTR_VERSION); if (versionAttr == null || !XML_VERSION.equals(versionAttr.getNodeValue())) { return; } NodeList nodes = rootNode.getChildNodes(); for (int i = 0, n = nodes.getLength(); i < n; i++) { Node node = nodes.item(i); if (node.getNodeType() != Node.ELEMENT_NODE || !NODE_ITEM.equals(node.getLocalName())) { continue; } NamedNodeMap attrMap = node.getAttributes(); File sourceFile = new File(attrMap.getNamedItem(ATTR_JAR).getNodeValue()); FullRevision revision = FullRevision.parseRevision(attrMap.getNamedItem( ATTR_REVISION).getNodeValue()); List<File> outputFiles = Lists.newArrayList(); NodeList dexNodes = node.getChildNodes(); for (int j = 0, m = dexNodes.getLength(); j < m; j++) { Node dexNode = dexNodes.item(j); if (dexNode.getNodeType() != Node.ELEMENT_NODE || !NODE_DEX.equals(dexNode.getLocalName())) { continue; } NamedNodeMap dexAttrMap = dexNode.getAttributes(); outputFiles.add(new File(dexAttrMap.getNamedItem(ATTR_DEX).getNodeValue())); } StoredItem item = new StoredItem( sourceFile, outputFiles, HashCode.fromString(attrMap.getNamedItem(ATTR_SHA1).getNodeValue())); T key = getKeyFactory().of(sourceFile, revision, attrMap); mStoredItems.put(key, item); } } catch (Exception ignored) { // if we fail to read parts or any of the file, all it'll do is fail to reuse an // already pre-dexed library, so that's not a super big deal. } } protected synchronized void saveItems(@NonNull File itemStorage) throws IOException { // write "compact" blob DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setValidating(false); factory.setIgnoringComments(true); DocumentBuilder builder; try { builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); Node rootNode = document.createElement(NODE_ITEMS); document.appendChild(rootNode); // Set the version Attr attr = document.createAttribute(ATTR_VERSION); attr.setValue(XML_VERSION); rootNode.getAttributes().setNamedItem(attr); Set<T> keys = Sets.newHashSetWithExpectedSize(mMap.size() + mStoredItems.size()); keys.addAll(mMap.keySet()); keys.addAll(mStoredItems.keySet()); for (T key : keys) { Item item = mMap.get(key); if (item != null) { Node itemNode = createItemNode(document, key, item); if (itemNode != null) { rootNode.appendChild(itemNode); } } else { StoredItem storedItem = mStoredItems.get(key); // check that the source file still exists in order to avoid // storing libraries that are gone. if (storedItem != null && storedItem.getSourceFile().isFile() && storedItem.areOutputFilesPresent()) { Node itemNode = createItemNode(document, key, storedItem); if (itemNode != null) { rootNode.appendChild(itemNode); } } } } String content = XmlPrettyPrinter.prettyPrint(document, true); itemStorage.getParentFile().mkdirs(); Files.write(content, itemStorage, Charsets.UTF_8); } catch (ParserConfigurationException e) { } } @Nullable protected Node createItemNode( @NonNull Document document, @NonNull T itemKey, @NonNull BaseItem item) throws IOException { if (!item.areOutputFilesPresent()) { return null; } Node itemNode = document.createElement(NODE_ITEM); Attr attr = document.createAttribute(ATTR_JAR); attr.setValue(item.getSourceFile().getPath()); itemNode.getAttributes().setNamedItem(attr); attr = document.createAttribute(ATTR_REVISION); attr.setValue(itemKey.getBuildToolsRevision().toString()); itemNode.getAttributes().setNamedItem(attr); HashCode hashCode = item.getSourceHash(); if (hashCode == null) { try { hashCode = Files.hash(item.getSourceFile(), Hashing.sha1()); } catch (IOException ex) { // If we can't compute the hash for whatever reason, simply skip this entry. return null; } } attr = document.createAttribute(ATTR_SHA1); attr.setValue(hashCode.toString()); itemNode.getAttributes().setNamedItem(attr); for (File dexFile : item.getOutputFiles()) { Node dexNode = document.createElement(NODE_DEX); itemNode.appendChild(dexNode); attr = document.createAttribute(ATTR_DEX); attr.setValue(dexFile.getPath()); dexNode.getAttributes().setNamedItem(attr); } return itemNode; } protected synchronized void incrementMisses() { mMisses++; } protected synchronized void incrementHits() { mHits++; } @VisibleForTesting /*package*/ synchronized int getMisses() { return mMisses; } @VisibleForTesting /*package*/ synchronized int getHits() { return mHits; } }