Wed, 03 Sep 2014 14:33:34 +0200
8056913: Limit the size of type info cache on disk
Reviewed-by: jlaskey, lagergren
1.1 --- a/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java Thu Aug 28 16:10:38 2014 -0700 1.2 +++ b/src/jdk/nashorn/internal/codegen/OptimisticTypesPersistence.java Wed Sep 03 14:33:34 2014 +0200 1.3 @@ -31,9 +31,11 @@ 1.4 import java.io.File; 1.5 import java.io.FileInputStream; 1.6 import java.io.FileOutputStream; 1.7 +import java.io.IOException; 1.8 import java.io.InputStream; 1.9 import java.net.URL; 1.10 import java.nio.file.Files; 1.11 +import java.nio.file.Path; 1.12 import java.security.AccessController; 1.13 import java.security.MessageDigest; 1.14 import java.security.PrivilegedAction; 1.15 @@ -41,6 +43,14 @@ 1.16 import java.util.Base64; 1.17 import java.util.Date; 1.18 import java.util.Map; 1.19 +import java.util.Timer; 1.20 +import java.util.TimerTask; 1.21 +import java.util.concurrent.TimeUnit; 1.22 +import java.util.concurrent.atomic.AtomicBoolean; 1.23 +import java.util.function.Function; 1.24 +import java.util.function.IntFunction; 1.25 +import java.util.function.Predicate; 1.26 +import java.util.stream.Stream; 1.27 import jdk.nashorn.internal.codegen.types.Type; 1.28 import jdk.nashorn.internal.runtime.Context; 1.29 import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData; 1.30 @@ -49,30 +59,66 @@ 1.31 import jdk.nashorn.internal.runtime.options.Options; 1.32 1.33 /** 1.34 - * Static utility that encapsulates persistence of decompilation information for functions. Normally, the type info 1.35 - * persistence feature is enabled and operates in an operating-system specific per-user cache directory. You can 1.36 - * override the directory by specifying it in the {@code nashorn.typeInfo.cacheDir} directory. Also, you can disable the 1.37 - * type info persistence altogether by specifying the {@code nashorn.typeInfo.disabled} system property. 1.38 + * Static utility that encapsulates persistence of type information for functions compiled with optimistic 1.39 + * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized, 1.40 + * the type information for deoptimization is stored in a cache file. If the same function is compiled in a 1.41 + * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system 1.42 + * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its 1.43 + * optimistic types at (or near) the steady state. 1.44 + * </p><p> 1.45 + * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system 1.46 + * property is specified with a value greater than 0, it is enabled and operates in an operating-system 1.47 + * specific per-user cache directory. You can override the directory by specifying it in the 1.48 + * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that 1.49 + * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is 1.50 + * added to the cache. The default delay is 20 seconds, and can be set using the 1.51 + * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word 1.52 + * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is 1.53 + * allowed to grow without limits. 1.54 */ 1.55 public final class OptimisticTypesPersistence { 1.56 + // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is 1.57 + // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will 1.58 + // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one 1.59 + // file per JavaScript function. 1.60 + private static final int DEFAULT_MAX_FILES = 0; 1.61 + // Constants for signifying that the cache should not be limited 1.62 + private static final int UNLIMITED_FILES = -1; 1.63 + // Maximum number of files that should be cached on disk. The maximum will be softly enforced. 1.64 + private static final int MAX_FILES = getMaxFiles(); 1.65 + // Number of seconds to wait between adding a new file to the cache and running a cleanup process 1.66 + private static final int DEFAULT_CLEANUP_DELAY = 20; 1.67 + private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty( 1.68 + "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY)); 1.69 // The name of the default subdirectory within the system cache directory where we store type info. 1.70 private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo"; 1.71 // The directory where we cache type info 1.72 - private static final File cacheDir = createCacheDir(); 1.73 + private static final File baseCacheDir = createBaseCacheDir(); 1.74 + private static final File cacheDir = createCacheDir(baseCacheDir); 1.75 // In-process locks to make sure we don't have a cross-thread race condition manipulating any file. 1.76 private static final Object[] locks = cacheDir == null ? null : createLockArray(); 1.77 - 1.78 // Only report one read/write error every minute 1.79 private static final long ERROR_REPORT_THRESHOLD = 60000L; 1.80 1.81 private static volatile long lastReportedError; 1.82 - 1.83 + private static final AtomicBoolean scheduledCleanup; 1.84 + private static final Timer cleanupTimer; 1.85 + static { 1.86 + if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) { 1.87 + scheduledCleanup = null; 1.88 + cleanupTimer = null; 1.89 + } else { 1.90 + scheduledCleanup = new AtomicBoolean(); 1.91 + cleanupTimer = new Timer(true); 1.92 + } 1.93 + } 1.94 /** 1.95 - * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed to 1.96 - * {@link #load(Object)} and {@link #store(Object, Map)} methods. 1.97 + * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed 1.98 + * to {@link #load(Object)} and {@link #store(Object, Map)} methods. 1.99 * @param source the source where the function comes from 1.100 * @param functionId the unique ID number of the function within the source 1.101 - * @param paramTypes the types of the function parameters (as persistence is per parameter type specialization). 1.102 + * @param paramTypes the types of the function parameters (as persistence is per parameter type 1.103 + * specialization). 1.104 * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled. 1.105 */ 1.106 public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) { 1.107 @@ -82,7 +128,8 @@ 1.108 final StringBuilder b = new StringBuilder(48); 1.109 // Base64-encode the digest of the source, and append the function id. 1.110 b.append(source.getDigest()).append('-').append(functionId); 1.111 - // Finally, if this is a parameter-type specialized version of the function, add the parameter types to the file name. 1.112 + // Finally, if this is a parameter-type specialized version of the function, add the parameter types 1.113 + // to the file name. 1.114 if(paramTypes != null && paramTypes.length > 0) { 1.115 b.append('-'); 1.116 for(final Type t: paramTypes) { 1.117 @@ -118,6 +165,11 @@ 1.118 @Override 1.119 public Void run() { 1.120 synchronized(getFileLock(file)) { 1.121 + if (!file.exists()) { 1.122 + // If the file already exists, we aren't increasing the number of cached files, so 1.123 + // don't schedule cleanup. 1.124 + scheduleCleanup(); 1.125 + } 1.126 try (final FileOutputStream out = new FileOutputStream(file)) { 1.127 out.getChannel().lock(); // lock exclusive 1.128 final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out)); 1.129 @@ -174,19 +226,19 @@ 1.130 } 1.131 } 1.132 1.133 - private static File createCacheDir() { 1.134 - if(Options.getBooleanProperty("nashorn.typeInfo.disabled")) { 1.135 + private static File createBaseCacheDir() { 1.136 + if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) { 1.137 return null; 1.138 } 1.139 try { 1.140 - return createCacheDirPrivileged(); 1.141 + return createBaseCacheDirPrivileged(); 1.142 } catch(final Exception e) { 1.143 getLogger().warning("Failed to create cache dir", e); 1.144 return null; 1.145 } 1.146 } 1.147 1.148 - private static File createCacheDirPrivileged() { 1.149 + private static File createBaseCacheDirPrivileged() { 1.150 return AccessController.doPrivileged(new PrivilegedAction<File>() { 1.151 @Override 1.152 public File run() { 1.153 @@ -195,14 +247,35 @@ 1.154 if(explicitDir != null) { 1.155 dir = new File(explicitDir); 1.156 } else { 1.157 - // When no directory is explicitly specified, get an operating system specific cache directory, 1.158 - // and create "com.oracle.java.NashornTypeInfo" in it. 1.159 + // When no directory is explicitly specified, get an operating system specific cache 1.160 + // directory, and create "com.oracle.java.NashornTypeInfo" in it. 1.161 final File systemCacheDir = getSystemCacheDir(); 1.162 dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME); 1.163 if (isSymbolicLink(dir)) { 1.164 return null; 1.165 } 1.166 } 1.167 + return dir; 1.168 + } 1.169 + }); 1.170 + } 1.171 + 1.172 + private static File createCacheDir(final File baseDir) { 1.173 + if (baseDir == null) { 1.174 + return null; 1.175 + } 1.176 + try { 1.177 + return createCacheDirPrivileged(baseDir); 1.178 + } catch(final Exception e) { 1.179 + getLogger().warning("Failed to create cache dir", e); 1.180 + return null; 1.181 + } 1.182 + } 1.183 + 1.184 + private static File createCacheDirPrivileged(final File baseDir) { 1.185 + return AccessController.doPrivileged(new PrivilegedAction<File>() { 1.186 + @Override 1.187 + public File run() { 1.188 final String versionDirName; 1.189 try { 1.190 versionDirName = getVersionDirName(); 1.191 @@ -210,12 +283,12 @@ 1.192 getLogger().warning("Failed to calculate version dir name", e); 1.193 return null; 1.194 } 1.195 - final File versionDir = new File(dir, versionDirName); 1.196 + final File versionDir = new File(baseDir, versionDirName); 1.197 if (isSymbolicLink(versionDir)) { 1.198 return null; 1.199 } 1.200 versionDir.mkdirs(); 1.201 - if(versionDir.isDirectory()) { 1.202 + if (versionDir.isDirectory()) { 1.203 getLogger().info("Optimistic type persistence directory is " + versionDir); 1.204 return versionDir; 1.205 } 1.206 @@ -235,12 +308,12 @@ 1.207 // Mac OS X stores caches in ~/Library/Caches 1.208 return new File(new File(System.getProperty("user.home"), "Library"), "Caches"); 1.209 } else if(os.startsWith("Windows")) { 1.210 - // On Windows, temp directory is the best approximation of a cache directory, as its contents persist across 1.211 - // reboots and various cleanup utilities know about it. java.io.tmpdir normally points to a user-specific 1.212 - // temp directory, %HOME%\LocalSettings\Temp. 1.213 + // On Windows, temp directory is the best approximation of a cache directory, as its contents 1.214 + // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally 1.215 + // points to a user-specific temp directory, %HOME%\LocalSettings\Temp. 1.216 return new File(System.getProperty("java.io.tmpdir")); 1.217 } else { 1.218 - // In all other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" 1.219 + // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" 1.220 return new File(System.getProperty("user.home"), ".cache"); 1.221 } 1.222 } 1.223 @@ -278,7 +351,8 @@ 1.224 final int packageNameLen = className.lastIndexOf('.'); 1.225 final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1); 1.226 final File dir = new File(dirStr); 1.227 - return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile(dir, 0L))); 1.228 + return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile( 1.229 + dir, 0L))); 1.230 } else { 1.231 throw new AssertionError(); 1.232 } 1.233 @@ -335,4 +409,108 @@ 1.234 return DebugLogger.DISABLED_LOGGER; 1.235 } 1.236 } 1.237 + 1.238 + private static void scheduleCleanup() { 1.239 + if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) { 1.240 + cleanupTimer.schedule(new TimerTask() { 1.241 + @Override 1.242 + public void run() { 1.243 + scheduledCleanup.set(false); 1.244 + try { 1.245 + doCleanup(); 1.246 + } catch (final IOException e) { 1.247 + // Ignore it. While this is unfortunate, we don't have good facility for reporting 1.248 + // this, as we're running in a thread that has no access to Context, so we can't grab 1.249 + // a DebugLogger. 1.250 + } 1.251 + } 1.252 + }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY)); 1.253 + } 1.254 + } 1.255 + 1.256 + private static void doCleanup() throws IOException { 1.257 + final long start = System.nanoTime(); 1.258 + final Path[] files = getAllRegularFilesInLastModifiedOrder(); 1.259 + final int nFiles = files.length; 1.260 + final int filesToDelete = Math.max(0, nFiles - MAX_FILES); 1.261 + int filesDeleted = 0; 1.262 + for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) { 1.263 + try { 1.264 + Files.deleteIfExists(files[i]); 1.265 + // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something 1.266 + // else deleted it for us; that's okay with us. 1.267 + filesDeleted++; 1.268 + } catch (final Exception e) { 1.269 + // does not increase filesDeleted 1.270 + } 1.271 + files[i] = null; // gc eligible 1.272 + }; 1.273 + final long duration = System.nanoTime() - start; 1.274 + } 1.275 + 1.276 + private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException { 1.277 + try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) { 1.278 + // TODO: rewrite below once we can use JDK8 syntactic constructs 1.279 + return filesStream 1.280 + .filter(new Predicate<Path>() { 1.281 + @Override 1.282 + public boolean test(final Path path) { 1.283 + return !Files.isDirectory(path); 1.284 + }; 1.285 + }) 1.286 + .map(new Function<Path, PathAndTime>() { 1.287 + @Override 1.288 + public PathAndTime apply(final Path path) { 1.289 + return new PathAndTime(path); 1.290 + } 1.291 + }) 1.292 + .sorted() 1.293 + .map(new Function<PathAndTime, Path>() { 1.294 + @Override 1.295 + public Path apply(final PathAndTime pathAndTime) { 1.296 + return pathAndTime.path; 1.297 + } 1.298 + }) 1.299 + .toArray(new IntFunction<Path[]>() { // Replace with Path::new 1.300 + @Override 1.301 + public Path[] apply(final int length) { 1.302 + return new Path[length]; 1.303 + } 1.304 + }); 1.305 + } 1.306 + } 1.307 + 1.308 + private static class PathAndTime implements Comparable<PathAndTime> { 1.309 + private final Path path; 1.310 + private final long time; 1.311 + 1.312 + PathAndTime(final Path path) { 1.313 + this.path = path; 1.314 + this.time = getTime(path); 1.315 + } 1.316 + 1.317 + @Override 1.318 + public int compareTo(final PathAndTime other) { 1.319 + return Long.compare(time, other.time); 1.320 + } 1.321 + 1.322 + private static long getTime(final Path path) { 1.323 + try { 1.324 + return Files.getLastModifiedTime(path).toMillis(); 1.325 + } catch (IOException e) { 1.326 + // All files for which we can't retrieve the last modified date will be considered oldest. 1.327 + return -1L; 1.328 + } 1.329 + } 1.330 + } 1.331 + 1.332 + private static int getMaxFiles() { 1.333 + final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null); 1.334 + if (str == null) { 1.335 + return DEFAULT_MAX_FILES; 1.336 + } else if ("unlimited".equals(str)) { 1.337 + return UNLIMITED_FILES; 1.338 + } 1.339 + return Math.max(0, Integer.parseInt(str)); 1.340 + } 1.341 }
2.1 --- a/src/jdk/nashorn/internal/codegen/types/Type.java Thu Aug 28 16:10:38 2014 -0700 2.2 +++ b/src/jdk/nashorn/internal/codegen/types/Type.java Wed Sep 03 14:33:34 2014 +0200 2.3 @@ -333,7 +333,7 @@ 2.4 */ 2.5 public static Map<Integer, Type> readTypeMap(final DataInput input) throws IOException { 2.6 final int size = input.readInt(); 2.7 - if (size == 0) { 2.8 + if (size <= 0) { 2.9 return null; 2.10 } 2.11 final Map<Integer, Type> map = new TreeMap<>(); 2.12 @@ -345,7 +345,7 @@ 2.13 case 'L': type = Type.OBJECT; break; 2.14 case 'D': type = Type.NUMBER; break; 2.15 case 'J': type = Type.LONG; break; 2.16 - default: throw new AssertionError(); 2.17 + default: continue; 2.18 } 2.19 map.put(pp, type); 2.20 }