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