1 /* 2 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package jdk.nashorn.internal.codegen; 26 27 import java.io.BufferedInputStream; 28 import java.io.BufferedOutputStream; 29 import java.io.DataInputStream; 30 import java.io.DataOutputStream; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.PrintWriter; 37 import java.io.StringWriter; 38 import java.net.URL; 39 import java.nio.file.Files; 40 import java.nio.file.Path; 41 import java.security.AccessController; 42 import java.security.MessageDigest; 43 import java.security.PrivilegedAction; 44 import java.text.SimpleDateFormat; 45 import java.util.Base64; 46 import java.util.Date; 47 import java.util.Map; 48 import java.util.Timer; 49 import java.util.TimerTask; 50 import java.util.concurrent.TimeUnit; 51 import java.util.concurrent.atomic.AtomicBoolean; 52 import java.util.function.Function; 53 import java.util.function.IntFunction; 54 import java.util.function.Predicate; 55 import java.util.stream.Stream; 56 import jdk.nashorn.internal.codegen.types.Type; 57 import jdk.nashorn.internal.runtime.Context; 58 import jdk.nashorn.internal.runtime.RecompilableScriptFunctionData; 59 import jdk.nashorn.internal.runtime.Source; 60 import jdk.nashorn.internal.runtime.logging.DebugLogger; 61 import jdk.nashorn.internal.runtime.options.Options; 62 63 /** 64 * Static utility that encapsulates persistence of type information for functions compiled with optimistic 65 * typing. With this feature enabled, when a JavaScript function is recompiled because it gets deoptimized, 66 * the type information for deoptimization is stored in a cache file. If the same function is compiled in a 67 * subsequent JVM invocation, the type information is used for initial compilation, thus allowing the system 68 * to skip a lot of intermediate recompilations and immediately emit a version of the code that has its 69 * optimistic types at (or near) the steady state. 70 * </p><p> 71 * Normally, the type info persistence feature is disabled. When the {@code nashorn.typeInfo.maxFiles} system 72 * property is specified with a value greater than 0, it is enabled and operates in an operating-system 73 * specific per-user cache directory. You can override the directory by specifying it in the 74 * {@code nashorn.typeInfo.cacheDir} directory. The maximum number of files is softly enforced by a task that 75 * cleans up the directory periodically on a separate thread. It is run after some delay after a new file is 76 * added to the cache. The default delay is 20 seconds, and can be set using the 77 * {@code nashorn.typeInfo.cleanupDelaySeconds} system property. You can also specify the word 78 * {@code unlimited} as the value for {@code nashorn.typeInfo.maxFiles} in which case the type info cache is 79 * allowed to grow without limits. 80 */ 81 public final class OptimisticTypesPersistence { 82 // Default is 0, for disabling the feature when not specified. A reasonable default when enabled is 83 // dependent on the application; setting it to e.g. 20000 is probably good enough for most uses and will 84 // usually cap the cache directory to about 80MB presuming a 4kB filesystem allocation unit. There is one 85 // file per JavaScript function. 86 private static final int DEFAULT_MAX_FILES = 0; 87 // Constants for signifying that the cache should not be limited 88 private static final int UNLIMITED_FILES = -1; 89 // Maximum number of files that should be cached on disk. The maximum will be softly enforced. 90 private static final int MAX_FILES = getMaxFiles(); 91 // Number of seconds to wait between adding a new file to the cache and running a cleanup process 92 private static final int DEFAULT_CLEANUP_DELAY = 20; 93 private static final int CLEANUP_DELAY = Math.max(0, Options.getIntProperty( 94 "nashorn.typeInfo.cleanupDelaySeconds", DEFAULT_CLEANUP_DELAY)); 95 // The name of the default subdirectory within the system cache directory where we store type info. 96 private static final String DEFAULT_CACHE_SUBDIR_NAME = "com.oracle.java.NashornTypeInfo"; 97 // The directory where we cache type info 98 private static final File baseCacheDir = createBaseCacheDir(); 99 private static final File cacheDir = createCacheDir(baseCacheDir); 100 // In-process locks to make sure we don't have a cross-thread race condition manipulating any file. 101 private static final Object[] locks = cacheDir == null ? null : createLockArray(); 102 // Only report one read/write error every minute 103 private static final long ERROR_REPORT_THRESHOLD = 60000L; 104 105 private static volatile long lastReportedError; 106 private static final AtomicBoolean scheduledCleanup; 107 private static final Timer cleanupTimer; 108 static { 109 if (baseCacheDir == null || MAX_FILES == UNLIMITED_FILES) { 110 scheduledCleanup = null; 111 cleanupTimer = null; 112 } else { 113 scheduledCleanup = new AtomicBoolean(); 114 cleanupTimer = new Timer(true); 115 } 116 } 117 /** 118 * Retrieves an opaque descriptor for the persistence location for a given function. It should be passed 119 * to {@link #load(Object)} and {@link #store(Object, Map)} methods. 120 * @param source the source where the function comes from 121 * @param functionId the unique ID number of the function within the source 122 * @param paramTypes the types of the function parameters (as persistence is per parameter type 123 * specialization). 124 * @return an opaque descriptor for the persistence location. Can be null if persistence is disabled. 125 */ 126 public static Object getLocationDescriptor(final Source source, final int functionId, final Type[] paramTypes) { 127 if(cacheDir == null) { 128 return null; 129 } 130 final StringBuilder b = new StringBuilder(48); 131 // Base64-encode the digest of the source, and append the function id. 132 b.append(source.getDigest()).append('-').append(functionId); 133 // Finally, if this is a parameter-type specialized version of the function, add the parameter types 134 // to the file name. 135 if(paramTypes != null && paramTypes.length > 0) { 136 b.append('-'); 137 for(final Type t: paramTypes) { 138 b.append(Type.getShortSignatureDescriptor(t)); 139 } 140 } 141 return new LocationDescriptor(new File(cacheDir, b.toString())); 142 } 143 144 private static final class LocationDescriptor { 145 private final File file; 146 147 LocationDescriptor(final File file) { 148 this.file = file; 149 } 150 } 151 152 153 /** 154 * Stores the map of optimistic types for a given function. 155 * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling 156 * {@link #getLocationDescriptor(Source, int, Type[])}. 157 * @param optimisticTypes the map of optimistic types. 158 */ 159 @SuppressWarnings("resource") 160 public static void store(final Object locationDescriptor, final Map<Integer, Type> optimisticTypes) { 161 if(locationDescriptor == null || optimisticTypes.isEmpty()) { 162 return; 163 } 164 final File file = ((LocationDescriptor)locationDescriptor).file; 165 166 AccessController.doPrivileged(new PrivilegedAction<Void>() { 167 @Override 168 public Void run() { 169 synchronized(getFileLock(file)) { 170 if (!file.exists()) { 171 // If the file already exists, we aren't increasing the number of cached files, so 172 // don't schedule cleanup. 173 scheduleCleanup(); 174 } 175 try (final FileOutputStream out = new FileOutputStream(file)) { 176 out.getChannel().lock(); // lock exclusive 177 final DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out)); 178 Type.writeTypeMap(optimisticTypes, dout); 179 dout.flush(); 180 } catch(final Exception e) { 181 reportError("write", file, e); 182 } 183 } 184 return null; 185 } 186 }); 187 } 188 189 /** 190 * Loads the map of optimistic types for a given function. 191 * @param locationDescriptor the opaque persistence location descriptor, retrieved by calling 192 * {@link #getLocationDescriptor(Source, int, Type[])}. 193 * @return the map of optimistic types, or null if persisted type information could not be retrieved. 194 */ 195 @SuppressWarnings("resource") 196 public static Map<Integer, Type> load(final Object locationDescriptor) { 197 if (locationDescriptor == null) { 198 return null; 199 } 200 final File file = ((LocationDescriptor)locationDescriptor).file; 201 return AccessController.doPrivileged(new PrivilegedAction<Map<Integer, Type>>() { 202 @Override 203 public Map<Integer, Type> run() { 204 try { 205 if(!file.isFile()) { 206 return null; 207 } 208 synchronized(getFileLock(file)) { 209 try (final FileInputStream in = new FileInputStream(file)) { 210 in.getChannel().lock(0, Long.MAX_VALUE, true); // lock shared 211 final DataInputStream din = new DataInputStream(new BufferedInputStream(in)); 212 return Type.readTypeMap(din); 213 } 214 } 215 } catch (final Exception e) { 216 reportError("read", file, e); 217 return null; 218 } 219 } 220 }); 221 } 222 223 private static void reportError(final String msg, final File file, final Exception e) { 224 final long now = System.currentTimeMillis(); 225 if(now - lastReportedError > ERROR_REPORT_THRESHOLD) { 226 reportError(String.format("Failed to %s %s", msg, file), e); 227 lastReportedError = now; 228 } 229 } 230 231 /** 232 * Logs an error message with warning severity (reasoning being that we're reporting an error that'll disable the 233 * type info cache, but it's only logged as a warning because that doesn't prevent Nashorn from running, it just 234 * disables a performance-enhancing cache). 235 * @param msg the message to log 236 * @param e the exception that represents the error. 237 */ 238 private static void reportError(final String msg, final Exception e) { 239 getLogger().warning(msg, "\n", exceptionToString(e)); 240 } 241 242 /** 243 * A helper that prints an exception stack trace into a string. We have to do this as if we just pass the exception 244 * to {@link DebugLogger#warning(Object...)}, it will only log the exception message and not the stack, making 245 * problems harder to diagnose. 246 * @param e the exception 247 * @return the string representation of {@link Exception#printStackTrace()} output. 248 */ 249 private static String exceptionToString(final Exception e) { 250 final StringWriter sw = new StringWriter(); 251 final PrintWriter pw = new PrintWriter(sw, false); 252 e.printStackTrace(pw); 253 pw.flush(); 254 return sw.toString(); 255 } 256 257 private static File createBaseCacheDir() { 258 if(MAX_FILES == 0 || Options.getBooleanProperty("nashorn.typeInfo.disabled")) { 259 return null; 260 } 261 try { 262 return createBaseCacheDirPrivileged(); 263 } catch(final Exception e) { 264 reportError("Failed to create cache dir", e); 265 return null; 266 } 267 } 268 269 private static File createBaseCacheDirPrivileged() { 270 return AccessController.doPrivileged(new PrivilegedAction<File>() { 271 @Override 272 public File run() { 273 final String explicitDir = System.getProperty("nashorn.typeInfo.cacheDir"); 274 final File dir; 275 if(explicitDir != null) { 276 dir = new File(explicitDir); 277 } else { 278 // When no directory is explicitly specified, get an operating system specific cache 279 // directory, and create "com.oracle.java.NashornTypeInfo" in it. 280 final File systemCacheDir = getSystemCacheDir(); 281 dir = new File(systemCacheDir, DEFAULT_CACHE_SUBDIR_NAME); 282 if (isSymbolicLink(dir)) { 283 return null; 284 } 285 } 286 return dir; 287 } 288 }); 289 } 290 291 private static File createCacheDir(final File baseDir) { 292 if (baseDir == null) { 293 return null; 294 } 295 try { 296 return createCacheDirPrivileged(baseDir); 297 } catch(final Exception e) { 298 reportError("Failed to create cache dir", e); 299 return null; 300 } 301 } 302 303 private static File createCacheDirPrivileged(final File baseDir) { 304 return AccessController.doPrivileged(new PrivilegedAction<File>() { 305 @Override 306 public File run() { 307 final String versionDirName; 308 try { 309 versionDirName = getVersionDirName(); 310 } catch(final Exception e) { 311 reportError("Failed to calculate version dir name", e); 312 return null; 313 } 314 final File versionDir = new File(baseDir, versionDirName); 315 if (isSymbolicLink(versionDir)) { 316 return null; 317 } 318 versionDir.mkdirs(); 319 if (versionDir.isDirectory()) { 320 getLogger().info("Optimistic type persistence directory is " + versionDir); 321 return versionDir; 322 } 323 getLogger().warning("Could not create optimistic type persistence directory " + versionDir); 324 return null; 325 } 326 }); 327 } 328 329 /** 330 * Returns an operating system specific root directory for cache files. 331 * @return an operating system specific root directory for cache files. 332 */ 333 private static File getSystemCacheDir() { 334 final String os = System.getProperty("os.name", "generic"); 335 if("Mac OS X".equals(os)) { 336 // Mac OS X stores caches in ~/Library/Caches 337 return new File(new File(System.getProperty("user.home"), "Library"), "Caches"); 338 } else if(os.startsWith("Windows")) { 339 // On Windows, temp directory is the best approximation of a cache directory, as its contents 340 // persist across reboots and various cleanup utilities know about it. java.io.tmpdir normally 341 // points to a user-specific temp directory, %HOME%\LocalSettings\Temp. 342 return new File(System.getProperty("java.io.tmpdir")); 343 } else { 344 // In other cases we're presumably dealing with a UNIX flavor (Linux, Solaris, etc.); "~/.cache" 345 return new File(System.getProperty("user.home"), ".cache"); 346 } 347 } 348 349 /** 350 * In order to ensure that changes in Nashorn code don't cause corruption in the data, we'll create a 351 * per-code-version directory. Normally, this will create the SHA-1 digest of the nashorn.jar. In case the classpath 352 * for nashorn is local directory (e.g. during development), this will create the string "dev-" followed by the 353 * timestamp of the most recent .class file. 354 * 355 * @return digest of currently running nashorn 356 * @throws Exception if digest could not be created 357 */ 358 public static String getVersionDirName() throws Exception { 359 // NOTE: getResource("") won't work if the JAR file doesn't have directory entries (and JAR files in JDK distro 360 // don't, or at least it's a bad idea counting on it). Alternatively, we could've tried 361 // getResource("OptimisticTypesPersistence.class") but behavior of getResource with regard to its willingness 362 // to hand out URLs to .class files is also unspecified. Therefore, the most robust way to obtain an URL to our 363 // package is to have a small non-class anchor file and start out from its URL. 364 final URL url = OptimisticTypesPersistence.class.getResource("anchor.properties"); 365 final String protocol = url.getProtocol(); 366 if (protocol.equals("jar")) { 367 // Normal deployment: nashorn.jar 368 final String jarUrlFile = url.getFile(); 369 final String filePath = jarUrlFile.substring(0, jarUrlFile.indexOf('!')); 370 final URL file = new URL(filePath); 371 try (final InputStream in = file.openStream()) { 372 final byte[] buf = new byte[128*1024]; 373 final MessageDigest digest = MessageDigest.getInstance("SHA-1"); 374 for(;;) { 375 final int l = in.read(buf); 376 if(l == -1) { 377 return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest()); 378 } 379 digest.update(buf, 0, l); 380 } 381 } 382 } else if(protocol.equals("file")) { 383 // Development 384 final String fileStr = url.getFile(); 385 final String className = OptimisticTypesPersistence.class.getName(); 386 final int packageNameLen = className.lastIndexOf('.'); 387 final String dirStr = fileStr.substring(0, fileStr.length() - packageNameLen - 1); 388 final File dir = new File(dirStr); 389 return "dev-" + new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(getLastModifiedClassFile( 390 dir, 0L))); 391 } else if(protocol.equals("jrt")) { 392 // FIXME: revisit this for a better option with jrt 393 return "jrt"; 394 } else { 395 throw new AssertionError(); 396 } 397 } 398 399 private static long getLastModifiedClassFile(final File dir, final long max) { 400 long currentMax = max; 401 for(final File f: dir.listFiles()) { 402 if(f.getName().endsWith(".class")) { 403 final long lastModified = f.lastModified(); 404 if (lastModified > currentMax) { 405 currentMax = lastModified; 406 } 407 } else if (f.isDirectory()) { 408 final long lastModified = getLastModifiedClassFile(f, currentMax); 409 if (lastModified > currentMax) { 410 currentMax = lastModified; 411 } 412 } 413 } 414 return currentMax; 415 } 416 417 /** 418 * Returns true if the specified file is a symbolic link, and also logs a warning if it is. 419 * @param file the file 420 * @return true if file is a symbolic link, false otherwise. 421 */ 422 private static boolean isSymbolicLink(final File file) { 423 if (Files.isSymbolicLink(file.toPath())) { 424 getLogger().warning("Directory " + file + " is a symlink"); 425 return true; 426 } 427 return false; 428 } 429 430 private static Object[] createLockArray() { 431 final Object[] lockArray = new Object[Runtime.getRuntime().availableProcessors() * 2]; 432 for (int i = 0; i < lockArray.length; ++i) { 433 lockArray[i] = new Object(); 434 } 435 return lockArray; 436 } 437 438 private static Object getFileLock(final File file) { 439 return locks[(file.hashCode() & Integer.MAX_VALUE) % locks.length]; 440 } 441 442 private static DebugLogger getLogger() { 443 try { 444 return Context.getContext().getLogger(RecompilableScriptFunctionData.class); 445 } catch (final Exception e) { 446 e.printStackTrace(); 447 return DebugLogger.DISABLED_LOGGER; 448 } 449 } 450 451 private static void scheduleCleanup() { 452 if (MAX_FILES != UNLIMITED_FILES && scheduledCleanup.compareAndSet(false, true)) { 453 cleanupTimer.schedule(new TimerTask() { 454 @Override 455 public void run() { 456 scheduledCleanup.set(false); 457 try { 458 doCleanup(); 459 } catch (final IOException e) { 460 // Ignore it. While this is unfortunate, we don't have good facility for reporting 461 // this, as we're running in a thread that has no access to Context, so we can't grab 462 // a DebugLogger. 463 } 464 } 465 }, TimeUnit.SECONDS.toMillis(CLEANUP_DELAY)); 466 } 467 } 468 469 private static void doCleanup() throws IOException { 470 final Path[] files = getAllRegularFilesInLastModifiedOrder(); 471 final int nFiles = files.length; 472 final int filesToDelete = Math.max(0, nFiles - MAX_FILES); 473 int filesDeleted = 0; 474 for (int i = 0; i < nFiles && filesDeleted < filesToDelete; ++i) { 475 try { 476 Files.deleteIfExists(files[i]); 477 // Even if it didn't exist, we increment filesDeleted; it existed a moment earlier; something 478 // else deleted it for us; that's okay with us. 479 filesDeleted++; 480 } catch (final Exception e) { 481 // does not increase filesDeleted 482 } 483 files[i] = null; // gc eligible 484 } 485 } 486 487 private static Path[] getAllRegularFilesInLastModifiedOrder() throws IOException { 488 try (final Stream<Path> filesStream = Files.walk(baseCacheDir.toPath())) { 489 // TODO: rewrite below once we can use JDK8 syntactic constructs 490 return filesStream 491 .filter(new Predicate<Path>() { 492 @Override 493 public boolean test(final Path path) { 494 return !Files.isDirectory(path); 495 } 496 }) 497 .map(new Function<Path, PathAndTime>() { 498 @Override 499 public PathAndTime apply(final Path path) { 500 return new PathAndTime(path); 501 } 502 }) 503 .sorted() 504 .map(new Function<PathAndTime, Path>() { 505 @Override 506 public Path apply(final PathAndTime pathAndTime) { 507 return pathAndTime.path; 508 } 509 }) 510 .toArray(new IntFunction<Path[]>() { // Replace with Path::new 511 @Override 512 public Path[] apply(final int length) { 513 return new Path[length]; 514 } 515 }); 516 } 517 } 518 519 private static class PathAndTime implements Comparable<PathAndTime> { 520 private final Path path; 521 private final long time; 522 523 PathAndTime(final Path path) { 524 this.path = path; 525 this.time = getTime(path); 526 } 527 528 @Override 529 public int compareTo(final PathAndTime other) { 530 return Long.compare(time, other.time); 531 } 532 533 private static long getTime(final Path path) { 534 try { 535 return Files.getLastModifiedTime(path).toMillis(); 536 } catch (final IOException e) { 537 // All files for which we can't retrieve the last modified date will be considered oldest. 538 return -1L; 539 } 540 } 541 } 542 543 private static int getMaxFiles() { 544 final String str = Options.getStringProperty("nashorn.typeInfo.maxFiles", null); 545 if (str == null) { 546 return DEFAULT_MAX_FILES; 547 } else if ("unlimited".equals(str)) { 548 return UNLIMITED_FILES; 549 } 550 return Math.max(0, Integer.parseInt(str)); 551 } 552 } --- EOF ---