/* * Copyright 2014 Google Inc. * * 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.google.gwt.dev; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.dev.cfg.PropertyCombinations.PermutationDescription; import com.google.gwt.dev.util.CompilerVersion; import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting; import com.google.gwt.thirdparty.guava.common.cache.Cache; import com.google.gwt.thirdparty.guava.common.cache.CacheBuilder; import com.google.gwt.thirdparty.guava.common.util.concurrent.Futures; import com.google.gwt.thirdparty.guava.common.util.concurrent.MoreExecutors; import com.google.gwt.util.tools.Utility; import com.google.gwt.util.tools.shared.Md5Utils; import com.google.gwt.util.tools.shared.StringUtils; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Manages caching of MinimalRebuildCache instances. * <p> * Changes are immediately performed in memory and are asynchronously persisted to disk in original * request order. */ public class MinimalRebuildCacheManager { private static final int MEMORY_CACHE_COUNT_LIMIT = 3; private static final String REBUILD_CACHE_PREFIX = "gwt-rebuildCache"; private final ExecutorService executorService = MoreExecutors.getExitingExecutorService((ThreadPoolExecutor) Executors.newFixedThreadPool(1)); private final TreeLogger logger; private final File minimalRebuildCacheDir; private final Cache<String, MinimalRebuildCache> minimalRebuildCachesByName = CacheBuilder.newBuilder().maximumSize(MEMORY_CACHE_COUNT_LIMIT).build(); private final Map<String, String> options = new LinkedHashMap<>(); public MinimalRebuildCacheManager( TreeLogger logger, File baseCacheDir, Map<String, String> options) { this.logger = logger; this.options.putAll(options); if (baseCacheDir != null) { minimalRebuildCacheDir = new File(baseCacheDir, REBUILD_CACHE_PREFIX); minimalRebuildCacheDir.mkdir(); } else { minimalRebuildCacheDir = null; } } /** * Synchronously delete all in memory caches managed here and all on disk in the managed folder. */ public synchronized void deleteCaches() { syncDeleteMemoryCaches(); if (haveCacheDir()) { Futures.getUnchecked(enqueueAsyncDeleteDiskCaches()); } } /** * Synchronously return the MinimalRebuildCache specific to the given module and binding * properties. * <p> * If no cache is found in memory then it will be synchronously loaded from disk. * <p> * If it is still not found a new empty cache will be returned. */ public synchronized MinimalRebuildCache getCache(String moduleName, PermutationDescription permutationDescription) { String cacheName = computeMinimalRebuildCacheName(moduleName, permutationDescription); MinimalRebuildCache minimalRebuildCache = minimalRebuildCachesByName.getIfPresent(cacheName); // If there's no cache already in memory, try to load a cache from disk. if (minimalRebuildCache == null && haveCacheDir()) { // Might return null. minimalRebuildCache = syncReadDiskCache(moduleName, permutationDescription); if (minimalRebuildCache != null) { minimalRebuildCachesByName.put(cacheName, minimalRebuildCache); } } // If there's still no cache loaded, just create a blank one. if (minimalRebuildCache == null) { minimalRebuildCache = new MinimalRebuildCache(); minimalRebuildCachesByName.put(cacheName, minimalRebuildCache); return minimalRebuildCache; } // Return a copy. MinimalRebuildCache mutableMinimalRebuildCache = new MinimalRebuildCache(); mutableMinimalRebuildCache.copyFrom(minimalRebuildCache); return mutableMinimalRebuildCache; } /** * Stores a MinimalRebuildCache specific to the given module and binding properties. * <p> * A copy of the cache will be lazily persisted to disk as well. */ public synchronized void putCache(String moduleName, PermutationDescription permutationDescription, MinimalRebuildCache knownGoodMinimalRebuildCache) { syncPutMemoryCache(moduleName, permutationDescription, knownGoodMinimalRebuildCache); if (haveCacheDir()) { enqueueAsyncWriteDiskCache(moduleName, permutationDescription, knownGoodMinimalRebuildCache); } } /** * Enqueue to asynchronously delete all on disk caches in the managed cache folder. */ @VisibleForTesting synchronized Future<Void> enqueueAsyncDeleteDiskCaches() { return executorService.submit(new Callable<Void>() { @Override public Void call() { for (File cacheFile : minimalRebuildCacheDir.listFiles()) { if (!cacheFile.delete()) { logger.log(TreeLogger.WARN, "Couldn't delete " + cacheFile); } } return null; } }); } /** * Enqueue to asynchronously find, read and return the MinimalRebuildCache unique to this module * and binding properties combination in the managed cache folder. */ @VisibleForTesting synchronized Future<MinimalRebuildCache> enqueueAsyncReadDiskCache(final String moduleName, final PermutationDescription permutationDescription) { return executorService.submit(new Callable<MinimalRebuildCache>() { @Override public MinimalRebuildCache call() { // Find the cache file unique to this module, binding properties and working directory. File minimalRebuildCacheFile = computeMinimalRebuildCacheFile(moduleName, permutationDescription); // If the file exists. if (minimalRebuildCacheFile.exists()) { ObjectInputStream objectInputStream = null; // Try to read it. try { objectInputStream = new ObjectInputStream( new BufferedInputStream(new FileInputStream(minimalRebuildCacheFile))); return (MinimalRebuildCache) objectInputStream.readObject(); } catch (IOException e) { logger.log(TreeLogger.WARN, "Unable to read the rebuild cache in " + minimalRebuildCacheFile + "."); Utility.close(objectInputStream); minimalRebuildCacheFile.delete(); } catch (ClassNotFoundException e) { logger.log(TreeLogger.WARN, "Unable to read the rebuild cache in " + minimalRebuildCacheFile + "."); Utility.close(objectInputStream); minimalRebuildCacheFile.delete(); } finally { Utility.close(objectInputStream); } } return null; } }); } /** * Enqueue to asynchronously write the provided MinimalRebuildCache to disk. * <p> * Persisted caches are uniquely named based on the compiler version, current module name, binding * properties and the location where the JVM was launched. * <p> * Care is taken to completely and successfully write a new cache (to a different location on * disk) before replacing the old cache (at the regular location on disk). * <p> * Write requests will occur in the order requested and will queue up if requests are made faster * than they can be completed. */ @VisibleForTesting synchronized Future<Void> enqueueAsyncWriteDiskCache(final String moduleName, final PermutationDescription permutationDescription, final MinimalRebuildCache minimalRebuildCache) { return executorService.submit(new Callable<Void>() { @Override public Void call() { File oldMinimalRebuildCacheFile = computeMinimalRebuildCacheFile(moduleName, permutationDescription); File newMinimalRebuildCacheFile = new File(oldMinimalRebuildCacheFile.getAbsoluteFile() + ".new"); // Ensure the cache folder exists. oldMinimalRebuildCacheFile.getParentFile().mkdirs(); // Write the new cache to disk. ObjectOutputStream objectOutputStream = null; try { objectOutputStream = new ObjectOutputStream( new BufferedOutputStream(new FileOutputStream(newMinimalRebuildCacheFile))); objectOutputStream.writeObject(minimalRebuildCache); Utility.close(objectOutputStream); // Replace the old cache file with the new one. oldMinimalRebuildCacheFile.delete(); newMinimalRebuildCacheFile.renameTo(oldMinimalRebuildCacheFile); } catch (IOException e) { logger.log(TreeLogger.WARN, "Unable to update the cache in " + oldMinimalRebuildCacheFile + "."); newMinimalRebuildCacheFile.delete(); } finally { if (objectOutputStream != null) { Utility.close(objectOutputStream); } } return null; } }); } /** * For testing only. Stops accepting any new tasks and waits for current tasks to complete. */ @VisibleForTesting boolean shutdown() throws InterruptedException { executorService.shutdown(); return executorService.awaitTermination(30, TimeUnit.SECONDS); } /** * Find, read and return the MinimalRebuildCache unique to this module, binding properties and * working directory. */ @VisibleForTesting synchronized MinimalRebuildCache syncReadDiskCache(String moduleName, PermutationDescription permutationDescription) { return Futures.getUnchecked(enqueueAsyncReadDiskCache(moduleName, permutationDescription)); } private File computeMinimalRebuildCacheFile(String moduleName, PermutationDescription permutationDescription) { return new File(minimalRebuildCacheDir, computeMinimalRebuildCacheName(moduleName, permutationDescription)); } private String computeMinimalRebuildCacheName(String moduleName, PermutationDescription permutationDescription) { String currentWorkingDirectory = System.getProperty("user.dir"); String compilerVersionHash = CompilerVersion.getHash(); String permutationDescriptionString = permutationDescription.toString(); String optionsDescriptionString = " Options ["; String separator = ""; for (Map.Entry entry : options.entrySet()) { optionsDescriptionString += String.format("%s%s = %s", separator, entry.getKey(), entry.getValue()); separator = ","; } optionsDescriptionString += "]"; String consistentHash = StringUtils.toHexString(Md5Utils.getMd5Digest(( compilerVersionHash + moduleName + currentWorkingDirectory + permutationDescriptionString + optionsDescriptionString) .getBytes())); return REBUILD_CACHE_PREFIX + "-" + consistentHash; } private boolean haveCacheDir() { return minimalRebuildCacheDir != null && minimalRebuildCacheDir.isDirectory(); } private void syncDeleteMemoryCaches() { minimalRebuildCachesByName.invalidateAll(); } private void syncPutMemoryCache(String moduleName, PermutationDescription permutationDescription, MinimalRebuildCache knownGoodMinimalRebuildCache) { String cacheName = computeMinimalRebuildCacheName(moduleName, permutationDescription); minimalRebuildCachesByName.put(cacheName, knownGoodMinimalRebuildCache); } }