blob: 24703c60b68066899ff33673ad2e08438facc232 [file] [log] [blame]
/*
* Copyright 2011 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.javac;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
import com.google.gwt.util.tools.Utility;
import com.google.gwt.thirdparty.guava.common.hash.Hashing;
import com.google.gwt.thirdparty.guava.common.io.Files;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
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.net.JarURLConnection;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A class that manages a persistent cache of {@link CompilationUnit} instances.
* Writes out {@CompilationUnit} instances to a cache in a
* background thread.
* <p>
* The persistent cache is implemented as a directory of log files with a date
* timestamp. A new log file gets created each time a new PersistentUnitCache is
* instantiated, (once per invocation of the compiler or DevMode). The design is
* intended to support only a single PersistentUnitCache instance in the
* compiler at a time.
* <p>
* As new units are compiled, the cache data is appended to a log. This allows
* Java serialization to efficiently store references. The next time the cache
* is started, all logs are replayed and loaded into the cache in chronological
* order, with newer units taking precedence. A new cache file is created for
* any newly compiled units in this session. After a threshold of a certain
* number of files in the directory is reached
* {@link PersistentUnitCache#CACHE_FILE_THRESHOLD} , the cache files are
* consolidated back into a single file.
*
* <p>
* System Properties (see {@link UnitCacheFactory}).
*
* <ul>
* <li>gwt.persistentunitcache : enables the persistent cache (eventually will
* be default)</li>
* <li>gwt.persistentunitcachedir=<dir>: sets or overrides the cache directory</li>
* </ul>
*
* <p>
* Known Issues:
*
* <ul>
* <li>This design uses an eager cache to load every unit in the cache on the
* first reference to find() or add(). When the cache is large (10000 units), it
* uses lots of heap and takes 5-10 seconds. Once the PersistentUnitCache is
* created, it starts eagerly loading the cache in a background thread).</li>
*
* <li>Although units logged to disk with the same resource path are eventually
* cleaned up, the most recently compiled unit stays in the cache forever. This
* means that stale units that are no longer referenced will never be purged,
* unless by some external action (e.g. ant clean).</li>
*
* <li>Unless ant builds are made aware of the cache directory, the cache will
* persist if a user does an ant clean.</li>
* </ul>
*
*/
class PersistentUnitCache extends MemoryUnitCache {
/**
* If there are more than this many files in the cache, clean up the old
* files.
*/
static final int CACHE_FILE_THRESHOLD = 40;
/**
* Common prefix for creating directories and cache files.
*/
static final String UNIT_CACHE_PREFIX = "gwt-unitCache";
static final String CACHE_FILE_PREFIX = UNIT_CACHE_PREFIX + "-";
static final String CURRENT_VERSION_CACHE_FILE_PREFIX = computePersistentCacheFilenamePrefix();
/**
* Creates a new file with a name based on the current system time.
*/
private static File createCacheFile(TreeLogger logger, File cacheDirectory)
throws UnableToCompleteException {
File newFile = null;
long timestamp = System.currentTimeMillis();
try {
do {
newFile = new File(cacheDirectory, CURRENT_VERSION_CACHE_FILE_PREFIX +
String.format("%016X", timestamp++));
} while (!newFile.createNewFile());
} catch (IOException ex) {
logger.log(TreeLogger.WARN, "Unable to create new cache log file "
+ (newFile == null ? "<not created>" : newFile.getAbsolutePath()) + ".", ex);
throw new UnableToCompleteException();
}
if (!newFile.canWrite()) {
logger.log(TreeLogger.WARN, "Unable to write to new cache log file "
+ newFile.getAbsolutePath() + ".");
throw new UnableToCompleteException();
}
return newFile;
}
/**
* Finds all files matching a pattern in the cache directory.
*
* @return an array of sorted filenames. The file name pattern is such that
* sorting them alphabetically also sorts the files by age.
*/
private static File[] getCacheFiles(File cacheDirectory, boolean currentVersion) {
if (cacheDirectory.isDirectory()) {
File[] files = cacheDirectory.listFiles();
List<File> cacheFiles = new ArrayList<File>();
for (File file : files) {
if (file.getName().startsWith(currentVersion ? CURRENT_VERSION_CACHE_FILE_PREFIX :
CACHE_FILE_PREFIX)) {
cacheFiles.add(file);
}
}
File[] retFiles = cacheFiles.toArray(new File[cacheFiles.size()]);
Arrays.sort(retFiles);
return retFiles;
}
return new File[0];
}
/**
* There is no significance in the return value, we just want to be able
* to tell if the purgeOldCacheFilesTask has completed.
*/
private Future<Boolean> purgeTaskStatus;
private AtomicBoolean purgeInProgress = new AtomicBoolean(false);
private final Runnable purgeOldCacheFilesTask = new Runnable() {
@Override
public void run() {
try {
// Delete all cache files in the directory except for the currently open
// file.
SpeedTracerLogger.Event deleteEvent = SpeedTracerLogger.start(DevModeEventType.DELETE_CACHE);
File[] filesToDelete = getCacheFiles(cacheDirectory, false);
logger.log(TreeLogger.TRACE, "Purging cache files from " + cacheDirectory);
for (File toDelete : filesToDelete) {
if (!currentCacheFile.equals(toDelete)) {
if (!toDelete.delete()) {
logger.log(TreeLogger.WARN, "Couldn't delete file: " + toDelete);
}
}
}
deleteEvent.end();
rotateCurrentCacheFile();
} catch (UnableToCompleteException e) {
backgroundService.shutdownNow();
} finally {
purgeInProgress.set(false);
}
}
};
private final Runnable rotateCacheFilesTask = new Runnable() {
@Override
public void run() {
try {
rotateCurrentCacheFile();
} catch (UnableToCompleteException e) {
backgroundService.shutdownNow();
}
assert (currentCacheFile != null);
}
};
private final Runnable shutdownThreadTask = new Runnable() {
@Override
public void run() {
assert (currentCacheFile != null);
closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
logger.log(TreeLogger.TRACE, "Shutting down PersistentUnitCache thread");
backgroundService.shutdownNow();
}
};
/**
* Saved to be able to wait for UNIT_MAP_LOAD_TASK to complete.
*/
private Future<Boolean> unitMapLoadStatus;
private final Runnable unitMapLoadTask = new Runnable() {
@Override
public void run() {
loadUnitMap(logger, currentCacheFile);
}
};
/**
* Used to execute the above Runnables in a background thread.
*/
private final ExecutorService backgroundService;
private int unitsWritten = 0;
private int addedSinceLastCleanup = 0;
/**
* A directory to store the cache files that should persist between
* invocations.
*/
private final File cacheDirectory;
/**
* Current file and stream being written to.
*/
private File currentCacheFile;
private ObjectOutputStream currentCacheFileStream;
private final TreeLogger logger;
PersistentUnitCache(final TreeLogger logger, File cacheDir) throws UnableToCompleteException {
assert cacheDir != null;
this.logger = logger;
/*
* We must canonicalize the path here, otherwise we might set cacheDirectory
* to something like "/path/to/x/../gwt-unitCache". If this were to happen,
* the mkdirs() call below would create "/path/to/gwt-unitCache" but
* not "/path/to/x".
* Further accesses via the uncanonicalized path will fail, if "/path/to/x"
* had not been created by other means.
*
* Fixes issue 6443
*/
try {
this.cacheDirectory = new File(cacheDir.getCanonicalFile(), UNIT_CACHE_PREFIX);
} catch (IOException ex) {
logger.log(TreeLogger.WARN, "Unable to create canonical file of "
+ cacheDir.getAbsolutePath() + ".", ex);
throw new UnableToCompleteException();
}
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Persistent unit cache dir set to: "
+ this.cacheDirectory.getAbsolutePath());
}
if (!cacheDirectory.isDirectory() && !cacheDirectory.mkdirs()) {
logger.log(TreeLogger.WARN, "Unable to initialize cache. Couldn't create directory "
+ cacheDirectory.getAbsolutePath() + ".");
throw new UnableToCompleteException();
}
currentCacheFile = createCacheFile(logger, cacheDirectory);
backgroundService = Executors.newSingleThreadExecutor();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Future<Boolean> status = backgroundService.submit(shutdownThreadTask, Boolean.TRUE);
// Don't let the shutdown hang more than 5 seconds
status.get(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// ignore
} catch (RejectedExecutionException e) {
// already shutdown, ignore
} catch (ExecutionException e) {
logger.log(TreeLogger.ERROR, "Error during shutdown", e);
} catch (TimeoutException e) {
// ignore
} finally {
backgroundService.shutdownNow();
}
}
});
/**
* Load up cached units from the persistent store in the background. The
* {@link #add(CompilationUnit)} and {@link #find(String)} methods block if
* invoked before this thread finishes.
*/
unitMapLoadStatus = backgroundService.submit(unitMapLoadTask, Boolean.TRUE);
FileOutputStream fstream = null;
BufferedOutputStream bstream = null;
try {
fstream = new FileOutputStream(currentCacheFile);
bstream = new BufferedOutputStream(fstream);
currentCacheFileStream = new ObjectOutputStream(bstream);
} catch (IOException ex) {
closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
logger.log(TreeLogger.ERROR, "Error creating cache " + currentCacheFile
+ ". Disabling cache.", ex);
backgroundService.shutdownNow();
throw new UnableToCompleteException();
}
}
/**
* Enqueue a unit to be written by the background thread.
*/
@Override
public void add(CompilationUnit newUnit) {
awaitUnitCacheMapLoad();
addedSinceLastCleanup++;
super.add(newUnit);
addImpl(unitMap.get(newUnit.getResourcePath()));
}
/**
* Cleans up old cache files in the directory, migrating everything previously
* loaded in them to the current cache file.
*
* Normally, only newly compiled units are written to the current log, but
* when it is time to cleanup, valid units from older log files need to be
* re-written.
*/
@Override
public void cleanup(TreeLogger logger) {
awaitUnitCacheMapLoad();
if (backgroundService.isShutdown()) {
return;
}
boolean shouldRotate = addedSinceLastCleanup > 0;
logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup +
" units to cache since last cleanup.");
addedSinceLastCleanup = 0;
try {
File[] cacheFiles = getCacheFiles(cacheDirectory, true);
if (cacheFiles.length < CACHE_FILE_THRESHOLD) {
if (shouldRotate) {
backgroundService.execute(rotateCacheFilesTask);
}
return;
}
// Check to see if the previous purge task finished.
boolean inProgress = purgeInProgress.getAndSet(true);
if (inProgress) {
try {
purgeTaskStatus.get(0, TimeUnit.NANOSECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} catch (TimeoutException ex) {
// purge is currently in progress.
return;
}
}
/*
* Resend all units read in from the in-memory cache to the background
* thread. They will be re-written out and the old cache files removed.
*/
synchronized (unitMap) {
for (UnitCacheEntry unitCacheEntry : unitMap.values()) {
if (unitCacheEntry.getOrigin() == UnitOrigin.PERSISTENT) {
addImpl(unitCacheEntry);
}
}
}
purgeTaskStatus = backgroundService.submit(purgeOldCacheFilesTask, Boolean.TRUE);
} catch (ExecutionException ex) {
throw new InternalCompilerException("Error purging cache", ex);
} catch (RejectedExecutionException ex) {
// Cache background thread is not running - ignore
}
}
@Override
public CompilationUnit find(ContentId contentId) {
awaitUnitCacheMapLoad();
return super.find(contentId);
}
@Override
public CompilationUnit find(String resourcePath) {
awaitUnitCacheMapLoad();
return super.find(resourcePath);
}
public void rotateCurrentCacheFile() throws UnableToCompleteException {
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Wrote " + unitsWritten + " units to persistent cache.");
}
// Close and re-open a new log file to drop object references kept
// alive in the existing file by Java serialization.
closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
unitsWritten = 0;
currentCacheFile = createCacheFile(logger, cacheDirectory);
FileOutputStream fstream = null;
BufferedOutputStream bstream = null;
try {
fstream = new FileOutputStream(currentCacheFile);
bstream = new BufferedOutputStream(fstream);
currentCacheFileStream = new ObjectOutputStream(bstream);
} catch (IOException ex) {
// Close all 3 streams, not sure where the exception occurred.
Utility.close(bstream);
Utility.close(fstream);
closeCurrentCacheFile(currentCacheFile, currentCacheFileStream);
logger.log(TreeLogger.ERROR, "Error rotating file. Shutting down cache thread.", ex);
throw new UnableToCompleteException();
}
}
/**
* For Unit testing - shutdown the persistent cache.
*
* @throws ExecutionException
* @throws InterruptedException
*/
void shutdown() throws InterruptedException, ExecutionException {
try {
Future<Runnable> future = backgroundService.submit(shutdownThreadTask, shutdownThreadTask);
backgroundService.shutdown();
future.get();
} catch (RejectedExecutionException ex) {
// background thread is not running - ignore
}
}
private static String computePersistentCacheFilenamePrefix() {
try {
URLConnection urlConnection =
JNode.class.getResource("JNode.class").openConnection();
if (urlConnection instanceof JarURLConnection) {
String gwtdevJar = ((JarURLConnection) urlConnection).getJarFile().getName();
return CACHE_FILE_PREFIX +
Files.hash(new File(gwtdevJar), Hashing.sha1()).toString() + "-";
}
System.err.println("Could not find the GWT compiler jarfile. "
+ "Serialization errors might occur when accessing the persistent unit cache.");
} catch (IOException e) {
System.err.println("Could not compute the hash for the GWT compiler jarfile."
+ "Serialization errors might occur when accessing the persistent unit cache.");
e.printStackTrace();
}
return CACHE_FILE_PREFIX;
}
private void addImpl(final UnitCacheEntry entry) {
try {
backgroundService.execute(new Runnable() {
@Override
public void run() {
try {
assert entry.getOrigin() != UnitOrigin.ARCHIVE;
CompilationUnit unit = entry.getUnit();
assert unit != null;
currentCacheFileStream.writeObject(unit);
unitsWritten++;
} catch (IOException ex) {
backgroundService.shutdownNow();
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Error saving unit to cache in: "
+ cacheDirectory.getAbsolutePath(), ex);
}
}
}
});
} catch (RejectedExecutionException ex) {
// background thread is not running, ignore
}
}
private synchronized void awaitUnitCacheMapLoad() {
// wait on initial load of unit map to complete.
try {
if (unitMapLoadStatus != null) {
unitMapLoadStatus.get();
// no need to check any more.
unitMapLoadStatus = null;
}
} catch (InterruptedException e) {
throw new InternalCompilerException("Interrupted waiting for unit cache map to load.", e);
} catch (ExecutionException e) {
logger.log(TreeLogger.ERROR, "Failure in unit cache map load.", e);
// keep going
unitMapLoadStatus = null;
}
}
private void closeCurrentCacheFile(File openFile, ObjectOutputStream stream) {
Utility.close(stream);
if (unitsWritten == 0) {
// Remove useless empty file.
openFile.delete();
}
}
/**
* Load everything cached on disk into memory.
*/
private void loadUnitMap(TreeLogger logger, File currentCacheFile) {
Event loadPersistentUnitEvent =
SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE);
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in "
+ cacheDirectory.getAbsolutePath());
}
try {
if (cacheDirectory.isDirectory() && cacheDirectory.canRead()) {
File[] files = getCacheFiles(cacheDirectory, true);
for (File cacheFile : files) {
FileInputStream fis = null;
BufferedInputStream bis = null;
ObjectInputStream inputStream = null;
if (cacheFile.equals(currentCacheFile)) {
continue;
}
boolean deleteCacheFile = false;
try {
fis = new FileInputStream(cacheFile);
bis = new BufferedInputStream(fis);
/*
* It is possible for the next call to throw an exception, leaving
* inputStream null and fis still live.
*/
inputStream = new ObjectInputStream(bis);
while (true) {
CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject();
if (unit == null) {
break;
}
if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) {
continue;
}
UnitCacheEntry entry = new UnitCacheEntry(unit, UnitOrigin.PERSISTENT);
UnitCacheEntry existingEntry = unitMap.get(unit.getResourcePath());
/*
* Don't assume that an existing entry is stale - an entry might
* have been loaded already from another source like a
* CompilationUnitArchive that is more up to date. If the
* timestamps are the same, accept the latest version. If it turns
* out to be stale, it will be recompiled and the updated unit
* will win this test the next time the session starts.
*/
if (existingEntry != null
&& unit.getLastModified() >= existingEntry.getUnit().getLastModified()) {
super.remove(existingEntry.getUnit());
unitMap.put(unit.getResourcePath(), entry);
unitMapByContentId.put(unit.getContentId(), entry);
} else if (existingEntry == null) {
unitMap.put(unit.getResourcePath(), entry);
unitMapByContentId.put(unit.getContentId(), entry);
}
}
} catch (EOFException ex) {
// Go on to the next file.
} catch (IOException ex) {
deleteCacheFile = true;
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+ cacheFile.getAbsolutePath() + " due to read error.", ex);
}
} catch (ClassNotFoundException ex) {
deleteCacheFile = true;
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log "
+ cacheFile.getAbsolutePath() + " due to deserialization error.", ex);
}
} finally {
Utility.close(inputStream);
Utility.close(bis);
Utility.close(fis);
}
if (deleteCacheFile) {
cacheFile.delete();
} else {
if (logger.isLoggable(TreeLogger.TRACE)) {
logger.log(TreeLogger.TRACE, cacheFile.getName() + ": Load complete");
}
}
}
} else {
logger
.log(TreeLogger.TRACE,
"Starting with empty Cache: CompilationUnit cache "
+ "directory does not exist or is not readable.");
}
} finally {
loadPersistentUnitEvent.end();
}
}
}