All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.builder.internal.compiler.PreDexCache Maven / Gradle / Ivy

/*
 * 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.builder.core.AndroidBuilder;
import com.android.builder.core.DexOptions;
import com.android.ide.common.internal.CommandLineRunner;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.sdklib.BuildToolInfo;
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.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.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Pre Dexing cache.
 *
 * Since we cannot yet have a single task for each library that needs to be pre-dexed (because
 * there is no task-level parallelization), this class allows reusing the output of the pre-dexing
 * of a library in a project to write the output of the pre-dexing of the same library in
 * a different project.
 *
 * Because different project could use different build-tools, both the library to pre-dex and the
 * version of the build tools are used as keys in the cache.
 *
 * The API is fairly simple, just call {@link #preDexLibrary(java.io.File, java.io.File, com.android.builder.core.DexOptions, com.android.sdklib.BuildToolInfo, boolean, com.android.ide.common.internal.CommandLineRunner)}
 *
 * The call will be blocking until the pre-dexing happened, either through actual pre-dexing or
 * through copying the output of a previous pre-dex run.
 *
 * After a build a call to {@link #clear(java.io.File, com.android.utils.ILogger)} with a file
 * will allow saving the known pre-dexed libraries for future reuse.
 */
public class PreDexCache {

    private static final String NODE_ITEMS = "pre-dex-items";
    private static final String NODE_ITEM = "item";
    private static final String ATTR_JUMBO_MODE = "jumboMode";
    private static final String ATTR_REVISION = "revision";
    private static final String ATTR_JAR = "jar";
    private static final String ATTR_DEX = "dex";
    private static final String ATTR_SHA1 = "sha1";

    /**
     * Items representing jar/dex files that have been processed during a build.
     */
    @Immutable
    private static class Item {
        @NonNull
        private final File mSourceFile;
        @NonNull
        private final File mOutputFile;
        @NonNull
        private final CountDownLatch mLatch;

        Item(
                @NonNull File sourceFile,
                @NonNull File outputFile,
                @NonNull CountDownLatch latch) {
            mSourceFile = sourceFile;
            mOutputFile = outputFile;
            mLatch = latch;
        }

        @NonNull
        private File getSourceFile() {
            return mSourceFile;
        }

        @NonNull
        private File getOutputFile() {
            return mOutputFile;
        }

        @NonNull
        private CountDownLatch getLatch() {
            return mLatch;
        }
    }

    /**
     * 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
    private static class StoredItem {
        @NonNull
        private final File mSourceFile;
        @NonNull
        private final File mOutputFile;
        @NonNull
        private final HashCode mSourceHash;

        StoredItem(
                @NonNull File sourceFile,
                @NonNull File outputFile,
                @NonNull HashCode sourceHash) {
            mSourceFile = sourceFile;
            mOutputFile = outputFile;
            mSourceHash = sourceHash;
        }

        @NonNull
        private File getSourceFile() {
            return mSourceFile;
        }

        @NonNull
        private File getOutputFile() {
            return mOutputFile;
        }

        @NonNull
        private HashCode getSourceHash() {
            return 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
    private static class Key {
        @NonNull
        private final File mSourceFile;
        @NonNull
        private final FullRevision mBuildToolsRevision;
        private final boolean mJumboMode;

        private static Key of(@NonNull File sourceFile, @NonNull FullRevision buildToolsRevision,
                boolean jumboMode) {
            return new Key(sourceFile, buildToolsRevision, jumboMode);
        }

        private Key(@NonNull File sourceFile, @NonNull FullRevision buildToolsRevision,
                boolean jumboMode) {
            mSourceFile = sourceFile;
            mBuildToolsRevision = buildToolsRevision;
            mJumboMode = jumboMode;
        }

        @NonNull
        private FullRevision getBuildToolsRevision() {
            return mBuildToolsRevision;
        }

        public boolean isJumboMode() {
            return mJumboMode;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            Key key = (Key) o;

            if (mJumboMode != key.mJumboMode) {
                return false;
            }
            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, mJumboMode);
        }
    }

    private static final PreDexCache sSingleton = new PreDexCache();

    public static PreDexCache getCache() {
        return sSingleton;
    }

    @GuardedBy("this")
    private boolean mLoaded = false;

    @GuardedBy("this")
    private final Map mMap = Maps.newHashMap();
    @GuardedBy("this")
    private final Map mStoredItems = Maps.newHashMap();

    @GuardedBy("this")
    private int mMisses = 0;
    @GuardedBy("this")
    private int mHits = 0;

    /**
     * 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;
    }

    /**
     * Pre-dex a given library to a given output with a specific version of the build-tools.
     * @param inputFile the jar to pre-dex
     * @param outFile the output file.
     * @param dexOptions the dex options to run pre-dex
     * @param buildToolInfo the build tools info
     * @param verbose verbose flag
     * @param commandLineRunner the command line runner.
     * @throws IOException
     * @throws LoggedErrorException
     * @throws InterruptedException
     */
    public void preDexLibrary(
            @NonNull File inputFile,
            @NonNull File outFile,
            @NonNull DexOptions dexOptions,
            @NonNull BuildToolInfo buildToolInfo,
            boolean verbose,
            @NonNull CommandLineRunner commandLineRunner)
            throws IOException, LoggedErrorException, InterruptedException {
        Pair pair = getItem(inputFile, outFile, buildToolInfo, dexOptions);

        // if this is a new item
        if (pair.getSecond()) {
            try {
                // haven't process this file yet so do it and record it.
                AndroidBuilder.preDexLibrary(inputFile, outFile, dexOptions, buildToolInfo,
                        verbose, commandLineRunner);

                synchronized (this) {
                    mMisses++;
                }
            } catch (IOException exception) {
                // in case of error, delete (now obsolete) output file
                outFile.delete();
                // and rethrow the error
                throw exception;
            } catch (LoggedErrorException exception) {
                // in case of error, delete (now obsolete) output file
                outFile.delete();
                // and rethrow the error
                throw exception;
            } catch (InterruptedException exception) {
                // in case of error, delete (now obsolete) output file
                outFile.delete();
                // and rethrow the error
                throw exception;
            } finally {
                // enable other threads to use the output of this pre-dex.
                // if something was thrown they'll handle the missing output file.
                pair.getFirst().getLatch().countDown();
            }
        } else {
            // wait until the file is pre-dexed by the first thread.
            pair.getFirst().getLatch().await();

            // check that the generated file actually exists
            File fromFile = pair.getFirst().getOutputFile();

            if (fromFile.isFile()) {
                // file already pre-dex, just copy the output.
                Files.copy(pair.getFirst().getOutputFile(), outFile);
                synchronized (this) {
                    mHits++;
                }
            }
        }
    }

    @VisibleForTesting
    /*package*/ synchronized int getMisses() {
        return mMisses;
    }

    @VisibleForTesting
    /*package*/ synchronized int getHits() {
        return mHits;
    }

    /**
     * Returns a Pair of {@link Item}, and a boolean which indicates whether the item is new (true)
     * or if it already existed (false).
     *
     * @param inputFile the input file
     * @param outFile the output file
     * @param buildToolInfo the build tools info.
     * @return a pair of item, boolean
     * @throws IOException
     */
    private synchronized Pair getItem(
            @NonNull File inputFile,
            @NonNull File outFile,
            @NonNull BuildToolInfo buildToolInfo,
            @NonNull DexOptions dexOptions) throws IOException {

        Key itemKey = Key.of(inputFile, buildToolInfo.getRevision(), dexOptions.getJumboMode());

        // 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);

            if (storedItem != null) {
                // check the sha1 is still valid, and the pre-dex file is still there.
                File dexFile = storedItem.getOutputFile();
                if (dexFile.isFile() &&
                        storedItem.getSourceHash().equals(Files.hash(inputFile, Hashing.sha1()))) {

                    // 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, dexFile, new CountDownLatch(0));
                }
            }

            // if we didn't find a valid stored item, create a new one.
            if (item == null) {
                item = new Item(inputFile, outFile, new CountDownLatch(1));
                newItem = true;
            }

            mMap.put(itemKey, item);
        }

        return Pair.of(item, newItem);
    }

    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;
            }

            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());

                StoredItem item = new StoredItem(
                        sourceFile,
                        new File(attrMap.getNamedItem(ATTR_DEX).getNodeValue()),
                        HashCode.fromString(attrMap.getNamedItem(ATTR_SHA1).getNodeValue()));

                Key key = Key.of(sourceFile, revision,
                        Boolean.parseBoolean(attrMap.getNamedItem(ATTR_JUMBO_MODE).getNodeValue()));

                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.
        }
    }

    private 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 keys = Sets.newHashSetWithExpectedSize(mMap.size() + mStoredItems.size());
            keys.addAll(mMap.keySet());
            keys.addAll(mStoredItems.keySet());

            for (Key key : keys) {
                Item item = mMap.get(key);

                if (item != null) {

                    Node itemNode = createItemNode(document,
                            item.getSourceFile(),
                            item.getOutputFile(),
                            key.getBuildToolsRevision(),
                            key.isJumboMode(),
                            Files.hash(item.getSourceFile(), Hashing.sha1()));
                    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.getOutputFile().isFile()) {
                        Node itemNode = createItemNode(document,
                                storedItem.getSourceFile(),
                                storedItem.getOutputFile(),
                                key.getBuildToolsRevision(),
                                key.isJumboMode(),
                                storedItem.getSourceHash());
                        rootNode.appendChild(itemNode);
                    }
                }
            }

            String content = XmlPrettyPrinter.prettyPrint(document, true);

            itemStorage.getParentFile().mkdirs();
            Files.write(content, itemStorage, Charsets.UTF_8);
        } catch (ParserConfigurationException e) {
        }
    }

    private static Node createItemNode(
            @NonNull Document document,
            @NonNull File sourceFile,
            @NonNull File outputFile,
            @NonNull FullRevision toolsRevision,
                     boolean jumboMode,
            @NonNull HashCode hashCode) {
        Node itemNode = document.createElement(NODE_ITEM);

        Attr attr = document.createAttribute(ATTR_JAR);
        attr.setValue(sourceFile.getPath());
        itemNode.getAttributes().setNamedItem(attr);

        attr = document.createAttribute(ATTR_DEX);
        attr.setValue(outputFile.getPath());
        itemNode.getAttributes().setNamedItem(attr);

        attr = document.createAttribute(ATTR_REVISION);
        attr.setValue(toolsRevision.toString());
        itemNode.getAttributes().setNamedItem(attr);

        attr = document.createAttribute(ATTR_JUMBO_MODE);
        attr.setValue(Boolean.toString(jumboMode));
        itemNode.getAttributes().setNamedItem(attr);

        attr = document.createAttribute(ATTR_SHA1);
        attr.setValue(hashCode.toString());
        itemNode.getAttributes().setNamedItem(attr);

        return itemNode;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy