OptimisticTypesPersistence.java revision 1036:f0b5e3900a10
1251881Speter/* 2251881Speter * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. 3251881Speter * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4251881Speter * 5251881Speter * This code is free software; you can redistribute it and/or modify it 6251881Speter * under the terms of the GNU General Public License version 2 only, as 7251881Speter * published by the Free Software Foundation. Oracle designates this 8251881Speter * particular file as subject to the "Classpath" exception as provided 9251881Speter * by Oracle in the LICENSE file that accompanied this code. 10251881Speter * 11251881Speter * This code is distributed in the hope that it will be useful, but WITHOUT 12251881Speter * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13251881Speter * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14251881Speter * version 2 for more details (a copy is included in the LICENSE file that 15251881Speter * accompanied this code). 16251881Speter * 17251881Speter * You should have received a copy of the GNU General Public License version 18251881Speter * 2 along with this work; if not, write to the Free Software Foundation, 19251881Speter * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20251881Speter * 21251881Speter * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22251881Speter * or visit www.oracle.com if you need additional information or have any 23251881Speter * questions. 24251881Speter */ 25251881Speterpackage jdk.nashorn.internal.codegen; 26251881Speter 27251881Speterimport java.io.BufferedInputStream; 28251881Speterimport java.io.BufferedOutputStream; 29251881Speterimport java.io.DataInputStream; 30251881Speterimport java.io.DataOutputStream; 31251881Speterimport java.io.File; 32251881Speterimport java.io.FileInputStream; 33251881Speterimport java.io.FileOutputStream; 34251881Speterimport java.io.IOException; 35251881Speterimport java.io.InputStream; 36251881Speterimport java.net.URL; 37251881Speterimport java.nio.file.Files; 38251881Speterimport java.nio.file.Path; 39251881Speterimport java.security.AccessController; 40251881Speterimport java.security.MessageDigest; 41251881Speterimport java.security.PrivilegedAction; 42251881Speterimport java.text.SimpleDateFormat; 43251881Speterimport java.util.Base64; 44251881Speterimport java.util.Date; 45251881Speterimport java.util.Map; 46251881Speterimport java.util.Timer; 47251881Speterimport java.util.TimerTask; 48251881Speterimport java.util.concurrent.TimeUnit; 49251881Speterimport java.util.concurrent.atomic.AtomicBoolean; 50251881Speterimport java.util.function.Function; 51251881Speterimport java.util.function.IntFunction; 52251881Speterimport java.util.function.Predicate; 53251881Speterimport java.util.stream.Stream; 54251881Speterimport jdk.nashorn.internal.codegen.types.Type; 55251881Speterimport jdk.nashorn.internal.runtime.Context; 56251881Speterimport jdk.nashorn.internal.runtime.RecompilableScriptFunctionData; 57251881Speterimport jdk.nashorn.internal.runtime.Source; 58251881Speterimport jdk.nashorn.internal.runtime.logging.DebugLogger; 59251881Speterimport jdk.nashorn.internal.runtime.options.Options; 60251881Speter 61251881Speter/** 62251881Speter * Static utility that encapsulates persistence of type information for functions compiled with optimistic 63251881Speter * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized, 64251881Speter * the type information for deoptimization is stored in a cache file. If the same function is compiled in a 65251881Speter * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system 66251881Speter * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its 67251881Speter * optimistic types at (or near) the steady state. 68251881Speter * </p><p> 69251881Speter * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system 70251881Speter * property is specified with a value greater than 0, it is enabled and operates in an operating-system 71251881Speter * specific per-user cache directory. You can override the directory by specifying it in the 72251881Speter * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that 73251881Speter * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is 74251881Speter * added to the cache. The default delay is 20 seconds, and can be set using the 75251881Speter * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word 76251881Speter * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is 77251881Speter * allowed to grow without limits. 78251881Speter */ 79251881Speterpublic final class OptimisticTypesPersistence { 80251881Speter // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is 81251881Speter // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will 82251881Speter // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one 83251881Speter // file per JavaScript function. 84251881Speter private static final int DEFAULT_MAX_FILES = 0; 85251881Speter // Constants for signifying that the cache should not be limited 86251881Speter private static final int UNLIMITED_FILES = -1; 87251881Speter // Maximum number of files that should be cached on disk. The maximum will be softly enforced. 88251881Speter private static final int MAX_FILES = getMaxFiles(); 89251881Speter // Number of seconds to wait between adding a new file to the cache and running a cleanup process 90251881Speter private static final int DEFAULT_CLEANUP_DELAY = 20; 91251881Speter private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty( 92251881Speter "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY)); 93251881Speter // The name of the default subdirectory within the system cache directory where we store type info. 94251881Speter private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo"; 95251881Speter // The directory where we cache type info 96251881Speter private static final File baseCacheDir = createBaseCacheDir(); 97251881Speter private static final File cacheDir = createCacheDir(baseCacheDir); 98251881Speter // In-process locks to make sure we don't have a cross-thread race condition manipulating any file. 99251881Speter private static final Object[] locks = cacheDir == null ? null : createLockArray(); 100251881Speter // Only report one read/write error every minute 101251881Speter private static final long ERROR_REPORT_THRESHOLD = 60000L; 102251881Speter 103251881Speter private static volatile long lastReportedError; 104251881Speter private static final AtomicBoolean scheduledCleanup; 105251881Speter private static final Timer cleanupTimer; 106251881Speter static { 107251881Speter if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) { 108251881Speter scheduledCleanup = null; 109251881Speter cleanupTimer = null; 110251881Speter } else { 111251881Speter scheduledCleanup = new AtomicBoolean(); 112251881Speter cleanupTimer = new Timer(true); 113251881Speter } 114251881Speter } 115251881Speter /** 116251881Speter * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed 117251881Speter * to {@link #load(Object)} and {@link #store(Object, Map)} methods. 118251881Speter * @param source the source where the function comes from 119 * @param functionId the unique ID number of the function within the source 120 * @param paramTypes the types of the function parameters (as persistence is per parameter type 121 * specialization). 122 * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled. 123 */ 124 public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) { 125 if(cacheDir == null) { 126 return null; 127 } 128 final StringBuilder b = new StringBuilder(48); 129 // Base64-encode the digest of the source, and append the function id. 130 b.append(source.getDigest()).append('-').append(functionId); 131 // Finally, if this is a parameter-type specialized version of the function, add the parameter types 132 // to the file name. 133 if(paramTypes != null && paramTypes.length > 0) { 134 b.append('-'); 135 for(final Type t: paramTypes) { 136 b.append(Type.getShortSignatureDescriptor(t)); 137 } 138 } 139 return new LocationDescriptor(new File(cacheDir, b.toString())); 140 } 141 142 private static final class LocationDescriptor { 143 private final File file; 144 145 LocationDescriptor(final File file) { 146 this.file = file; 147 } 148 } 149 150 151 /** 152 * Stores the map of optimistic types for a given function. 153 * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling 154 * {@link #getLocationDescriptor(Source, int, Type[])}. 155 * @param optimisticTypes the map of optimistic types. 156 */ 157 @SuppressWarnings("resource") 158 public static void store(final Object locationDescriptor, final Map<Integer, Type> optimisticTypes) { 159 if(locationDescriptor == null || optimisticTypes.isEmpty()) { 160 return; 161 } 162 final File file = ((LocationDescriptor)locationDescriptor).file; 163 164 AccessController.doPrivileged(new PrivilegedAction<Void>() { 165 @Override 166 public Void run() { 167 synchronized(getFileLock(file)) { 168 if (!file.exists()) { 169 // If the file already exists, we aren't increasing the number of cached files, so 170 // don't schedule cleanup. 171 scheduleCleanup(); 172 } 173 try (final FileOutputStream out = new FileOutputStream(file)) { 174 out.getChannel().lock(); // lock exclusive 175 final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out)); 176 Type.writeTypeMap(optimisticTypes, dout); 177 dout.flush(); 178 } catch(final Exception e) { 179 reportError("write", file, e); 180 } 181 } 182 return null; 183 } 184 }); 185 } 186 187 /** 188 * Loads the map of optimistic types for a given function. 189 * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling 190 * {@link #getLocationDescriptor(Source, int, Type[])}. 191 * @return the map of optimistic types, or null if persisted type information could not be retrieved. 192 */ 193 @SuppressWarnings("resource") 194 public static Map<Integer, Type> load(final Object locationDescriptor) { 195 if (locationDescriptor == null) { 196 return null; 197 } 198 final File file = ((LocationDescriptor)locationDescriptor).file; 199 return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() { 200 @Override 201 public Map<Integer, Type> run() { 202 try { 203 if(!file.isFile()) { 204 return null; 205 } 206 synchronized(getFileLock(file)) { 207 try (final FileInputStream in = new FileInputStream(file)) { 208 in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared 209 final DataInputStream din = new DataInputStream(new BufferedInputStream(in)); 210 return Type.readTypeMap(din); 211 } 212 } 213 } catch (final Exception e) { 214 reportError("read", file, e); 215 return null; 216 } 217 } 218 }); 219 } 220 221 private static void reportError(final String msg, final File file, final Exception e) { 222 final long now = System.currentTimeMillis(); 223 if(now - lastReportedError > ERROR_REPORT_THRESHOLD) { 224 getLogger().warning(String.format("Failed to %s %s", msg, file), e); 225 lastReportedError = now; 226 } 227 } 228 229 private static File createBaseCacheDir() { 230 if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) { 231 return null; 232 } 233 try { 234 return createBaseCacheDirPrivileged(); 235 } catch(final Exception e) { 236 getLogger().warning("Failed to create cache dir", e); 237 return null; 238 } 239 } 240 241 private static File createBaseCacheDirPrivileged() { 242 return AccessController.doPrivileged(new PrivilegedAction<File>() { 243 @Override 244 public File run() { 245 final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir"); 246 final File dir; 247 if(explicitDir != null) { 248 dir = new File(explicitDir); 249 } else { 250 // When no directory is explicitly specified, get an operating system specific cache 251 // directory, and create "com.oracle.java.NashornTypeInfo" in it. 252 final File systemCacheDir = getSystemCacheDir(); 253 dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME); 254 if (isSymbolicLink(dir)) { 255 return null; 256 } 257 } 258 return dir; 259 } 260 }); 261 } 262 263 private static File createCacheDir(final File baseDir) { 264 if (baseDir == null) { 265 return null; 266 } 267 try { 268 return createCacheDirPrivileged(baseDir); 269 } catch(final Exception e) { 270 getLogger().warning("Failed to create cache dir", e); 271 return null; 272 } 273 } 274 275 private static File createCacheDirPrivileged(final File baseDir) { 276 return AccessController.doPrivileged(new PrivilegedAction<File>() { 277 @Override 278 public File run() { 279 final String versionDirName; 280 try { 281 versionDirName = getVersionDirName(); 282 } catch(final Exception e) { 283 getLogger().warning("Failed to calculate version dir name", e); 284 return null; 285 } 286 final File versionDir = new File(baseDir, versionDirName); 287 if (isSymbolicLink(versionDir)) { 288 return null; 289 } 290 versionDir.mkdirs(); 291 if (versionDir.isDirectory()) { 292 getLogger().info("Optimistic type persistence directory is " + versionDir); 293 return versionDir; 294 } 295 getLogger().warning("Could not create optimistic type persistence directory " + versionDir); 296 return null; 297 } 298 }); 299 } 300 301 /** 302 * Returns an operating system specific root directory for cache files. 303 * @return an operating system specific root directory for cache files. 304 */ 305 private static File getSystemCacheDir() { 306 final String os = System.getProperty("os.name", "generic"); 307 if("Mac OS X".equals(os)) { 308 // Mac OS X stores caches in ~/Library/Caches 309 return new File(new File(System.getProperty("user.home"), "Library"), "Caches"); 310 } else if(os.startsWith("Windows")) { 311 // On Windows, temp directory is the best approximation of a cache directory, as its contents 312 // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally 313 // points to a user-specific temp directory, %HOME%\LocalSettings\Temp. 314 return new File(System.getProperty("java.io.tmpdir")); 315 } else { 316 // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" 317 return new File(System.getProperty("user.home"), ".cache"); 318 } 319 } 320 321 /** 322 * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a 323 * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath 324 * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the 325 * timestamp of the most recent .class file. 326 * @return 327 */ 328 private static String getVersionDirName() throws Exception { 329 final URL url = OptimisticTypesPersistence.class.getResource(""); 330 final String protocol = url.getProtocol(); 331 if (protocol.equals("jar")) { 332 // Normal deployment: nashorn.jar 333 final String jarUrlFile = url.getFile(); 334 final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!')); 335 final URL file = new URL(filePath); 336 try (final InputStream in = file.openStream()) { 337 final byte[] buf = new byte[128*1024]; 338 final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 339 for(;;) { 340 final int l = in.read(buf); 341 if(l == -1) { 342 return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest()); 343 } 344 digest.update(buf, 0, l); 345 } 346 } 347 } else if(protocol.equals("file")) { 348 // Development 349 final String fileStr = url.getFile(); 350 final String className = OptimisticTypesPersistence.class.getName(); 351 final int packageNameLen = className.lastIndexOf('.'); 352 final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1); 353 final File dir = new File(dirStr); 354 return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile( 355 dir, 0L))); 356 } else { 357 throw new AssertionError(); 358 } 359 } 360 361 private static long getLastModifiedClassFile(final File dir, final long max) { 362 long currentMax = max; 363 for(final File f: dir.listFiles()) { 364 if(f.getName().endsWith(".class")) { 365 final long lastModified = f.lastModified(); 366 if (lastModified > currentMax) { 367 currentMax = lastModified; 368 } 369 } else if (f.isDirectory()) { 370 final long lastModified = getLastModifiedClassFile(f, currentMax); 371 if (lastModified > currentMax) { 372 currentMax = lastModified; 373 } 374 } 375 } 376 return currentMax; 377 } 378 379 /** 380 * Returns true if the specified file is a symbolic link, and also logs a warning if it is. 381 * @param file the file 382 * @return true if file is a symbolic link, false otherwise. 383 */ 384 private static boolean isSymbolicLink(final File file) { 385 if (Files.isSymbolicLink(file.toPath())) { 386 getLogger().warning("Directory " + file + " is a symlink"); 387 return true; 388 } 389 return false; 390 } 391 392 private static Object[] createLockArray() { 393 final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2]; 394 for (int i = 0; i < lockArray.length; ++i) { 395 lockArray[i] = new Object(); 396 } 397 return lockArray; 398 } 399 400 private static Object getFileLock(final File file) { 401 return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length]; 402 } 403 404 private static DebugLogger getLogger() { 405 try { 406 return Context.getContext().getLogger(RecompilableScriptFunctionData.class); 407 } catch (final Exception e) { 408 e.printStackTrace(); 409 return DebugLogger.DISABLED_LOGGER; 410 } 411 } 412 413 private static void scheduleCleanup() { 414 if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) { 415 cleanupTimer.schedule(new TimerTask() { 416 @Override 417 public void run() { 418 scheduledCleanup.set(false); 419 try { 420 doCleanup(); 421 } catch (final IOException e) { 422 // Ignore it. While this is unfortunate, we don't have good facility for reporting 423 // this, as we're running in a thread that has no access to Context, so we can't grab 424 // a DebugLogger. 425 } 426 } 427 }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY)); 428 } 429 } 430 431 private static void doCleanup() throws IOException { 432 final Path[] files = getAllRegularFilesInLastModifiedOrder(); 433 final int nFiles = files.length; 434 final int filesToDelete = Math.max(0, nFiles - MAX_FILES); 435 int filesDeleted = 0; 436 for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) { 437 try { 438 Files.deleteIfExists(files[i]); 439 // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something 440 // else deleted it for us; that's okay with us. 441 filesDeleted++; 442 } catch (final Exception e) { 443 // does not increase filesDeleted 444 } 445 files[i] = null; // gc eligible 446 }; 447 } 448 449 private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException { 450 try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) { 451 // TODO: rewrite below once we can use JDK8 syntactic constructs 452 return filesStream 453 .filter(new Predicate<Path>() { 454 @Override 455 public boolean test(final Path path) { 456 return !Files.isDirectory(path); 457 }; 458 }) 459 .map(new Function<Path, PathAndTime>() { 460 @Override 461 public PathAndTime apply(final Path path) { 462 return new PathAndTime(path); 463 } 464 }) 465 .sorted() 466 .map(new Function<PathAndTime, Path>() { 467 @Override 468 public Path apply(final PathAndTime pathAndTime) { 469 return pathAndTime.path; 470 } 471 }) 472 .toArray(new IntFunction<Path[]>() { // Replace with Path::new 473 @Override 474 public Path[] apply(final int length) { 475 return new Path[length]; 476 } 477 }); 478 } 479 } 480 481 private static class PathAndTime implements Comparable<PathAndTime> { 482 private final Path path; 483 private final long time; 484 485 PathAndTime(final Path path) { 486 this.path = path; 487 this.time = getTime(path); 488 } 489 490 @Override 491 public int compareTo(final PathAndTime other) { 492 return Long.compare(time, other.time); 493 } 494 495 private static long getTime(final Path path) { 496 try { 497 return Files.getLastModifiedTime(path).toMillis(); 498 } catch (final IOException e) { 499 // All files for which we can't retrieve the last modified date will be considered oldest. 500 return -1L; 501 } 502 } 503 } 504 505 private static int getMaxFiles() { 506 final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null); 507 if (str == null) { 508 return DEFAULT_MAX_FILES; 509 } else if ("unlimited".equals(str)) { 510 return UNLIMITED_FILES; 511 } 512 return Math.max(0, Integer.parseInt(str)); 513 } 514} 515