# HG changeset patch # User attila # Date 1409747614 -7200 # Node ID 46647c4943ffb2db97324b457a567c44430c9ec2 # Parent 34c17c95665419ed76a98f5cf1210ed58eb2eca3 8056913: Limit the size of type info cache on disk Reviewed-by: jlaskey, lagergren diff -r 34c17c956654 -r 46647c4943ff src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java --- a/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java Thu Aug 28 16:10:38 2014 -0700 +++ b/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java Wed Sep 03 14:33:34 2014 +0200 @@ -31,9 +31,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.security.AccessController; import java.security.MessageDigest; import java.security.PrivilegedAction; @@ -41,6 +43,14 @@ import java.util.Base64; import java.util.Date; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.stream.Stream; import jdk.nashorn.internal.codegen.types.Type; import jdk.nashorn.internal.runtime.Context; import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData; @@ -49,30 +59,66 @@ import jdk.nashorn.internal.runtime.options.Options; /** - * Static utility that encapsulates persistence of decompilation information for functions. Normally, the type info - * persistence feature is enabled and operates in an operating-system specific per-user cache directory. You can - * override the directory by specifying it in the {@code nashorn.typeInfo.cacheDir} directory. Also, you can disable the - * type info persistence altogether by specifying the {@code nashorn.typeInfo.disabled} system property. + * Static utility that encapsulates persistence of type information for functions compiled with optimistic + * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized, + * the type information for deoptimization is stored in a cache file. If the same function is compiled in a + * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system + * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its + * optimistic types at (or near) the steady state. + *

+ * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system + * property is specified with a value greater than 0, it is enabled and operates in an operating-system + * specific per-user cache directory. You can override the directory by specifying it in the + * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that + * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is + * added to the cache. The default delay is 20 seconds, and can be set using the + * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word + * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is + * allowed to grow without limits. */ public final class OptimisticTypesPersistence { + // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is + // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will + // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one + // file per JavaScript function. + private static final int DEFAULT_MAX_FILES = 0; + // Constants for signifying that the cache should not be limited + private static final int UNLIMITED_FILES = -1; + // Maximum number of files that should be cached on disk. The maximum will be softly enforced. + private static final int MAX_FILES = getMaxFiles(); + // Number of seconds to wait between adding a new file to the cache and running a cleanup process + private static final int DEFAULT_CLEANUP_DELAY = 20; + private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty( + "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY)); // The name of the default subdirectory within the system cache directory where we store type info. private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo"; // The directory where we cache type info - private static final File cacheDir = createCacheDir(); + private static final File baseCacheDir = createBaseCacheDir(); + private static final File cacheDir = createCacheDir(baseCacheDir); // In-process locks to make sure we don't have a cross-thread race condition manipulating any file. private static final Object[] locks = cacheDir == null ? null : createLockArray(); - // Only report one read/write error every minute private static final long ERROR_REPORT_THRESHOLD = 60000L; private static volatile long lastReportedError; - + private static final AtomicBoolean scheduledCleanup; + private static final Timer cleanupTimer; + static { + if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) { + scheduledCleanup = null; + cleanupTimer = null; + } else { + scheduledCleanup = new AtomicBoolean(); + cleanupTimer = new Timer(true); + } + } /** - * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed to - * {@link #load(Object)} and {@link #store(Object, Map)} methods. + * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed + * to {@link #load(Object)} and {@link #store(Object, Map)} methods. * @param source the source where the function comes from * @param functionId the unique ID number of the function within the source - * @param paramTypes the types of the function parameters (as persistence is per parameter type specialization). + * @param paramTypes the types of the function parameters (as persistence is per parameter type + * specialization). * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled. */ public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) { @@ -82,7 +128,8 @@ final StringBuilder b = new StringBuilder(48); // Base64-encode the digest of the source, and append the function id. b.append(source.getDigest()).append('-').append(functionId); - // Finally, if this is a parameter-type specialized version of the function, add the parameter types to the file name. + // Finally, if this is a parameter-type specialized version of the function, add the parameter types + // to the file name. if(paramTypes != null && paramTypes.length > 0) { b.append('-'); for(final Type t: paramTypes) { @@ -118,6 +165,11 @@ @Override public Void run() { synchronized(getFileLock(file)) { + if (!file.exists()) { + // If the file already exists, we aren't increasing the number of cached files, so + // don't schedule cleanup. + scheduleCleanup(); + } try (final FileOutputStream out = new FileOutputStream(file)) { out.getChannel().lock(); // lock exclusive final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out)); @@ -174,19 +226,19 @@ } } - private static File createCacheDir() { - if(Options.getBooleanProperty("nashorn.typeInfo.disabled")) { + private static File createBaseCacheDir() { + if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) { return null; } try { - return createCacheDirPrivileged(); + return createBaseCacheDirPrivileged(); } catch(final Exception e) { getLogger().warning("Failed to create cache dir", e); return null; } } - private static File createCacheDirPrivileged() { + private static File createBaseCacheDirPrivileged() { return AccessController.doPrivileged(new PrivilegedAction() { @Override public File run() { @@ -195,14 +247,35 @@ if(explicitDir != null) { dir = new File(explicitDir); } else { - // When no directory is explicitly specified, get an operating system specific cache directory, - // and create "com.oracle.java.NashornTypeInfo" in it. + // When no directory is explicitly specified, get an operating system specific cache + // directory, and create "com.oracle.java.NashornTypeInfo" in it. final File systemCacheDir = getSystemCacheDir(); dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME); if (isSymbolicLink(dir)) { return null; } } + return dir; + } + }); + } + + private static File createCacheDir(final File baseDir) { + if (baseDir == null) { + return null; + } + try { + return createCacheDirPrivileged(baseDir); + } catch(final Exception e) { + getLogger().warning("Failed to create cache dir", e); + return null; + } + } + + private static File createCacheDirPrivileged(final File baseDir) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public File run() { final String versionDirName; try { versionDirName = getVersionDirName(); @@ -210,12 +283,12 @@ getLogger().warning("Failed to calculate version dir name", e); return null; } - final File versionDir = new File(dir, versionDirName); + final File versionDir = new File(baseDir, versionDirName); if (isSymbolicLink(versionDir)) { return null; } versionDir.mkdirs(); - if(versionDir.isDirectory()) { + if (versionDir.isDirectory()) { getLogger().info("Optimistic type persistence directory is " + versionDir); return versionDir; } @@ -235,12 +308,12 @@ // Mac OS X stores caches in ~/Library/Caches return new File(new File(System.getProperty("user.home"), "Library"), "Caches"); } else if(os.startsWith("Windows")) { - // On Windows, temp directory is the best approximation of a cache directory, as its contents persist across - // reboots and various cleanup utilities know about it. java.io.tmpdir normally points to a user-specific - // temp directory, %HOME%\LocalSettings\Temp. + // On Windows, temp directory is the best approximation of a cache directory, as its contents + // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally + // points to a user-specific temp directory, %HOME%\LocalSettings\Temp. return new File(System.getProperty("java.io.tmpdir")); } else { - // In all other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" + // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" return new File(System.getProperty("user.home"), ".cache"); } } @@ -278,7 +351,8 @@ final int packageNameLen = className.lastIndexOf('.'); final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1); final File dir = new File(dirStr); - return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(dir, 0L))); + return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile( + dir, 0L))); } else { throw new AssertionError(); } @@ -335,4 +409,108 @@ return DebugLogger.DISABLED_LOGGER; } } + + private static void scheduleCleanup() { + if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) { + cleanupTimer.schedule(new TimerTask() { + @Override + public void run() { + scheduledCleanup.set(false); + try { + doCleanup(); + } catch (final IOException e) { + // Ignore it. While this is unfortunate, we don't have good facility for reporting + // this, as we're running in a thread that has no access to Context, so we can't grab + // a DebugLogger. + } + } + }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY)); + } + } + + private static void doCleanup() throws IOException { + final long start = System.nanoTime(); + final Path[] files = getAllRegularFilesInLastModifiedOrder(); + final int nFiles = files.length; + final int filesToDelete = Math.max(0, nFiles - MAX_FILES); + int filesDeleted = 0; + for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) { + try { + Files.deleteIfExists(files[i]); + // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something + // else deleted it for us; that's okay with us. + filesDeleted++; + } catch (final Exception e) { + // does not increase filesDeleted + } + files[i] = null; // gc eligible + }; + final long duration = System.nanoTime() - start; + } + + private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException { + try (final Stream filesStream = Files.walk(baseCacheDir.toPath())) { + // TODO: rewrite below once we can use JDK8 syntactic constructs + return filesStream + .filter(new Predicate() { + @Override + public boolean test(final Path path) { + return !Files.isDirectory(path); + }; + }) + .map(new Function() { + @Override + public PathAndTime apply(final Path path) { + return new PathAndTime(path); + } + }) + .sorted() + .map(new Function() { + @Override + public Path apply(final PathAndTime pathAndTime) { + return pathAndTime.path; + } + }) + .toArray(new IntFunction() { // Replace with Path::new + @Override + public Path[] apply(final int length) { + return new Path[length]; + } + }); + } + } + + private static class PathAndTime implements Comparable { + private final Path path; + private final long time; + + PathAndTime(final Path path) { + this.path = path; + this.time = getTime(path); + } + + @Override + public int compareTo(final PathAndTime other) { + return Long.compare(time, other.time); + } + + private static long getTime(final Path path) { + try { + return Files.getLastModifiedTime(path).toMillis(); + } catch (IOException e) { + // All files for which we can't retrieve the last modified date will be considered oldest. + return -1L; + } + } + } + + private static int getMaxFiles() { + final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null); + if (str == null) { + return DEFAULT_MAX_FILES; + } else if ("unlimited".equals(str)) { + return UNLIMITED_FILES; + } + return Math.max(0, Integer.parseInt(str)); + } } diff -r 34c17c956654 -r 46647c4943ff src/jdk/nashorn/internal/codegen/types/Type.java --- a/src/jdk/nashorn/internal/codegen/types/Type.java Thu Aug 28 16:10:38 2014 -0700 +++ b/src/jdk/nashorn/internal/codegen/types/Type.java Wed Sep 03 14:33:34 2014 +0200 @@ -333,7 +333,7 @@ */ public static Map readTypeMap(final DataInput input) throws IOException { final int size = input.readInt(); - if (size == 0) { + if (size <= 0) { return null; } final Map map = new TreeMap<>(); @@ -345,7 +345,7 @@ case 'L': type = Type.OBJECT; break; case 'D': type = Type.NUMBER; break; case 'J': type = Type.LONG; break; - default: throw new AssertionError(); + default: continue; } map.put(pp, type); }