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 26 package jdk.nashorn.internal.runtime; 27 28 import static jdk.nashorn.internal.codegen.CompilerConstants.staticCall; 29 import static jdk.nashorn.internal.codegen.CompilerConstants.virtualCall; 30 import static jdk.nashorn.internal.lookup.Lookup.MH; 31 import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT; 32 import static jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor.getProgramPoint; 33 import static jdk.nashorn.internal.runtime.logging.DebugLogger.quote; 34 35 import java.lang.invoke.MethodHandle; 36 import java.lang.invoke.MethodHandles; 37 import java.lang.invoke.SwitchPoint; 38 import java.util.Arrays; 39 import java.util.HashMap; 40 import java.util.Map; 41 import java.util.concurrent.atomic.AtomicBoolean; 42 import java.util.logging.Level; 43 import jdk.dynalink.CallSiteDescriptor; 44 import jdk.dynalink.DynamicLinker; 45 import jdk.dynalink.linker.GuardedInvocation; 46 import jdk.dynalink.linker.LinkRequest; 47 import jdk.nashorn.internal.lookup.Lookup; 48 import jdk.nashorn.internal.lookup.MethodHandleFactory; 49 import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor; 50 import jdk.nashorn.internal.runtime.logging.DebugLogger; 51 import jdk.nashorn.internal.runtime.logging.Loggable; 52 import jdk.nashorn.internal.runtime.logging.Logger; 53 54 /** 55 * Each context owns one of these. This is basically table of accessors 56 * for global properties. A global constant is evaluated to a MethodHandle.constant 57 * for faster access and to avoid walking to proto chain looking for it. 58 * 59 * We put a switchpoint on the global setter, which invalidates the 60 * method handle constant getters, and reverts to the standard access strategy 61 * 62 * However, there is a twist - while certain globals like "undefined" and "Math" 63 * are usually never reassigned, a global value can be reset once, and never again. 64 * This is a rather common pattern, like: 65 * 66 * x = function(something) { ... 67 * 68 * Thus everything registered as a global constant gets an extra chance. Set once, 69 * reregister the switchpoint. Set twice or more - don't try again forever, or we'd 70 * just end up relinking our way into megamorphism. 71 * 72 * Also it has to be noted that this kind of linking creates a coupling between a Global 73 * and the call sites in compiled code belonging to the Context. For this reason, the 74 * linkage becomes incorrect as soon as the Context has more than one Global. The 75 * {@link #invalidateForever()} is invoked by the Context to invalidate all linkages and 76 * turn off the functionality of this object as soon as the Context's {@link Context#newGlobal()} is invoked 77 * for second time. 78 * 79 * We can extend this to ScriptObjects in general (GLOBAL_ONLY=false), which requires 80 * a receiver guard on the constant getter, but it currently leaks memory and its benefits 81 * have not yet been investigated property. 82 * 83 * As long as all Globals in a Context share the same GlobalConstants instance, we need synchronization 84 * whenever we access it. 85 */ 86 @Logger(name="const") 87 public final class GlobalConstants implements Loggable { 88 89 /** 90 * Should we only try to link globals as constants, and not generic script objects. 91 * Script objects require a receiver guard, which is memory intensive, so this is currently 92 * disabled. We might implement a weak reference based approach to this later. 93 */ 94 public static final boolean GLOBAL_ONLY = true; 95 96 private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); 97 98 private static final MethodHandle INVALIDATE_SP = virtualCall(LOOKUP, GlobalConstants.class, "invalidateSwitchPoint", Object.class, Object.class, Access.class).methodHandle(); 99 private static final MethodHandle RECEIVER_GUARD = staticCall(LOOKUP, GlobalConstants.class, "receiverGuard", boolean.class, Access.class, Object.class, Object.class).methodHandle(); 100 101 /** Logger for constant getters */ 102 private final DebugLogger log; 103 104 /** 105 * Access map for this global - associates a symbol name with an Access object, with getter 106 * and invalidation information 107 */ 108 private final Map<Object, Access> map = new HashMap<>(); 109 110 private final AtomicBoolean invalidatedForever = new AtomicBoolean(false); 111 112 /** 113 * Constructor - used only by global 114 * @param log logger, or null if none 115 */ GlobalConstants(final DebugLogger log)116 public GlobalConstants(final DebugLogger log) { 117 this.log = log == null ? DebugLogger.DISABLED_LOGGER : log; 118 } 119 120 @Override getLogger()121 public DebugLogger getLogger() { 122 return log; 123 } 124 125 @Override initLogger(final Context context)126 public DebugLogger initLogger(final Context context) { 127 return DebugLogger.DISABLED_LOGGER; 128 } 129 130 /** 131 * Information about a constant access and its potential invalidations 132 */ 133 private static class Access { 134 /** name of symbol */ 135 private final String name; 136 137 /** switchpoint that invalidates the getters and setters for this access */ 138 private SwitchPoint sp; 139 140 /** invalidation count for this access, i.e. how many times has this property been reset */ 141 private int invalidations; 142 143 /** has a guard guarding this property getter failed? */ 144 private boolean guardFailed; 145 146 private static final int MAX_RETRIES = 2; 147 Access(final String name, final SwitchPoint sp)148 private Access(final String name, final SwitchPoint sp) { 149 this.name = name; 150 this.sp = sp; 151 } 152 hasBeenInvalidated()153 private boolean hasBeenInvalidated() { 154 return sp.hasBeenInvalidated(); 155 } 156 guardFailed()157 private boolean guardFailed() { 158 return guardFailed; 159 } 160 failGuard()161 private void failGuard() { 162 invalidateOnce(); 163 guardFailed = true; 164 } 165 newSwitchPoint()166 private void newSwitchPoint() { 167 assert hasBeenInvalidated(); 168 sp = new SwitchPoint(); 169 } 170 invalidate(final int count)171 private void invalidate(final int count) { 172 if (!sp.hasBeenInvalidated()) { 173 SwitchPoint.invalidateAll(new SwitchPoint[] { sp }); 174 invalidations += count; 175 } 176 } 177 178 /** 179 * Invalidate the access, but do not contribute to the invalidation count 180 */ invalidateUncounted()181 private void invalidateUncounted() { 182 invalidate(0); 183 } 184 185 /** 186 * Invalidate the access, and contribute 1 to the invalidation count 187 */ invalidateOnce()188 private void invalidateOnce() { 189 invalidate(1); 190 } 191 192 /** 193 * Invalidate the access and make sure that we never try to turn this into 194 * a MethodHandle.constant getter again 195 */ invalidateForever()196 private void invalidateForever() { 197 invalidate(MAX_RETRIES); 198 } 199 200 /** 201 * Are we allowed to relink this as constant getter, even though it 202 * it has been reset 203 * @return true if we can relink as constant, one retry is allowed 204 */ mayRetry()205 private boolean mayRetry() { 206 return invalidations < MAX_RETRIES; 207 } 208 209 @Override toString()210 public String toString() { 211 return "[" + quote(name) + " <id=" + Debug.id(this) + "> inv#=" + invalidations + '/' + MAX_RETRIES + " sp_inv=" + sp.hasBeenInvalidated() + ']'; 212 } 213 getName()214 String getName() { 215 return name; 216 } 217 getSwitchPoint()218 SwitchPoint getSwitchPoint() { 219 return sp; 220 } 221 } 222 223 /** 224 * To avoid an expensive global guard "is this the same global", similar to the 225 * receiver guard on the ScriptObject level, we invalidate all getters once 226 * when we switch globals. This is used from the class cache. We _can_ reuse 227 * the same class for a new global, but the builtins and global scoped variables 228 * will have changed. 229 */ invalidateAll()230 public void invalidateAll() { 231 if (!invalidatedForever.get()) { 232 log.info("New global created - invalidating all constant callsites without increasing invocation count."); 233 synchronized (this) { 234 for (final Access acc : map.values()) { 235 acc.invalidateUncounted(); 236 } 237 } 238 } 239 } 240 241 /** 242 * To avoid an expensive global guard "is this the same global", similar to the 243 * receiver guard on the ScriptObject level, we invalidate all getters when the 244 * second Global is created by the Context owning this instance. After this 245 * method is invoked, this GlobalConstants instance will both invalidate all the 246 * switch points it produced, and it will stop handing out new method handles 247 * altogether. 248 */ invalidateForever()249 public void invalidateForever() { 250 if (invalidatedForever.compareAndSet(false, true)) { 251 log.info("New global created - invalidating all constant callsites."); 252 synchronized (this) { 253 for (final Access acc : map.values()) { 254 acc.invalidateForever(); 255 } 256 map.clear(); 257 } 258 } 259 } 260 261 /** 262 * Invalidate the switchpoint of an access - we have written to 263 * the property 264 * 265 * @param obj receiver 266 * @param acc access 267 * 268 * @return receiver, so this can be used as param filter 269 */ 270 @SuppressWarnings("unused") invalidateSwitchPoint(final Object obj, final Access acc)271 private synchronized Object invalidateSwitchPoint(final Object obj, final Access acc) { 272 if (log.isEnabled()) { 273 log.info("*** Invalidating switchpoint " + acc.getSwitchPoint() + " for receiver=" + obj + " access=" + acc); 274 } 275 acc.invalidateOnce(); 276 if (acc.mayRetry()) { 277 if (log.isEnabled()) { 278 log.info("Retry is allowed for " + acc + "... Creating a new switchpoint."); 279 } 280 acc.newSwitchPoint(); 281 } else { 282 if (log.isEnabled()) { 283 log.info("This was the last time I allowed " + quote(acc.getName()) + " to relink as constant."); 284 } 285 } 286 return obj; 287 } 288 getOrCreateSwitchPoint(final String name)289 private Access getOrCreateSwitchPoint(final String name) { 290 Access acc = map.get(name); 291 if (acc != null) { 292 return acc; 293 } 294 final SwitchPoint sp = new SwitchPoint(); 295 map.put(name, acc = new Access(name, sp)); 296 return acc; 297 } 298 299 /** 300 * Called from script object on property deletion to erase a property 301 * that might be linked as MethodHandle.constant and force relink 302 * @param name name of property 303 */ delete(final Object name)304 void delete(final Object name) { 305 if (!invalidatedForever.get()) { 306 synchronized (this) { 307 final Access acc = map.get(name); 308 if (acc != null) { 309 acc.invalidateForever(); 310 } 311 } 312 } 313 } 314 315 /** 316 * Receiver guard is used if we extend the global constants to script objects in general. 317 * As the property can have different values in different script objects, while Global is 318 * by definition a singleton, we need this for ScriptObject constants (currently disabled) 319 * 320 * TODO: Note - this seems to cause memory leaks. Use weak references? But what is leaking seems 321 * to be the Access objects, which isn't the case for Globals. Weird. 322 * 323 * @param acc access 324 * @param boundReceiver the receiver bound to the callsite 325 * @param receiver the receiver to check against 326 * 327 * @return true if this receiver is still the one we bound to the callsite 328 */ 329 @SuppressWarnings("unused") receiverGuard(final Access acc, final Object boundReceiver, final Object receiver)330 private static boolean receiverGuard(final Access acc, final Object boundReceiver, final Object receiver) { 331 final boolean id = receiver == boundReceiver; 332 if (!id) { 333 acc.failGuard(); 334 } 335 return id; 336 } 337 isGlobalSetter(final ScriptObject receiver, final FindProperty find)338 private static boolean isGlobalSetter(final ScriptObject receiver, final FindProperty find) { 339 if (find == null) { 340 return receiver.isScope(); 341 } 342 return find.getOwner().isGlobal(); 343 } 344 345 /** 346 * Augment a setter with switchpoint for invalidating its getters, should the setter be called 347 * 348 * @param find property lookup 349 * @param inv normal guarded invocation for this setter, as computed by the ScriptObject linker 350 * @param desc callsite descriptor 351 * @param request link request 352 * 353 * @return null if failed to set up constant linkage 354 */ findSetMethod(final FindProperty find, final ScriptObject receiver, final GuardedInvocation inv, final CallSiteDescriptor desc, final LinkRequest request)355 GuardedInvocation findSetMethod(final FindProperty find, final ScriptObject receiver, final GuardedInvocation inv, final CallSiteDescriptor desc, final LinkRequest request) { 356 if (invalidatedForever.get() || (GLOBAL_ONLY && !isGlobalSetter(receiver, find))) { 357 return null; 358 } 359 360 final String name = NashornCallSiteDescriptor.getOperand(desc); 361 362 synchronized (this) { 363 final Access acc = getOrCreateSwitchPoint(name); 364 365 if (log.isEnabled()) { 366 log.fine("Trying to link constant SETTER ", acc); 367 } 368 369 if (!acc.mayRetry() || invalidatedForever.get()) { 370 if (log.isEnabled()) { 371 log.fine("*** SET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation()); 372 } 373 return null; 374 } 375 376 if (acc.hasBeenInvalidated()) { 377 log.info("New chance for " + acc); 378 acc.newSwitchPoint(); 379 } 380 381 assert !acc.hasBeenInvalidated(); 382 383 // if we haven't given up on this symbol, add a switchpoint invalidation filter to the receiver parameter 384 final MethodHandle target = inv.getInvocation(); 385 final Class<?> receiverType = target.type().parameterType(0); 386 final MethodHandle boundInvalidator = MH.bindTo(INVALIDATE_SP, this); 387 final MethodHandle invalidator = MH.asType(boundInvalidator, boundInvalidator.type().changeParameterType(0, receiverType).changeReturnType(receiverType)); 388 final MethodHandle mh = MH.filterArguments(inv.getInvocation(), 0, MH.insertArguments(invalidator, 1, acc)); 389 390 assert inv.getSwitchPoints() == null : Arrays.asList(inv.getSwitchPoints()); 391 log.info("Linked setter " + quote(name) + " " + acc.getSwitchPoint()); 392 return new GuardedInvocation(mh, inv.getGuard(), acc.getSwitchPoint(), inv.getException()); 393 } 394 } 395 396 /** 397 * Try to reuse constant method handles for getters 398 * @param c constant value 399 * @return method handle (with dummy receiver) that returns this constant 400 */ staticConstantGetter(final Object c)401 public static MethodHandle staticConstantGetter(final Object c) { 402 return MH.dropArguments(JSType.unboxConstant(c), 0, Object.class); 403 } 404 constantGetter(final Object c)405 private MethodHandle constantGetter(final Object c) { 406 final MethodHandle mh = staticConstantGetter(c); 407 if (log.isEnabled()) { 408 return MethodHandleFactory.addDebugPrintout(log, Level.FINEST, mh, "getting as constant"); 409 } 410 return mh; 411 } 412 413 /** 414 * Try to turn a getter into a MethodHandle.constant, if possible 415 * 416 * @param find property lookup 417 * @param receiver receiver 418 * @param desc callsite descriptor 419 * 420 * @return resulting getter, or null if failed to create constant 421 */ findGetMethod(final FindProperty find, final ScriptObject receiver, final CallSiteDescriptor desc)422 GuardedInvocation findGetMethod(final FindProperty find, final ScriptObject receiver, final CallSiteDescriptor desc) { 423 // Only use constant getter for fast scope access, because the receiver may change between invocations 424 // for slow-scope and non-scope callsites. 425 // Also return null for user accessor properties as they may have side effects. 426 if (invalidatedForever.get() || !NashornCallSiteDescriptor.isFastScope(desc) 427 || (GLOBAL_ONLY && !find.getOwner().isGlobal()) 428 || find.getProperty() instanceof UserAccessorProperty) { 429 return null; 430 } 431 432 final boolean isOptimistic = NashornCallSiteDescriptor.isOptimistic(desc); 433 final int programPoint = isOptimistic ? getProgramPoint(desc) : INVALID_PROGRAM_POINT; 434 final Class<?> retType = desc.getMethodType().returnType(); 435 final String name = NashornCallSiteDescriptor.getOperand(desc); 436 437 synchronized (this) { 438 final Access acc = getOrCreateSwitchPoint(name); 439 440 log.fine("Starting to look up object value " + name); 441 final Object c = find.getObjectValue(); 442 443 if (log.isEnabled()) { 444 log.fine("Trying to link constant GETTER " + acc + " value = " + c); 445 } 446 447 if (acc.hasBeenInvalidated() || acc.guardFailed() || invalidatedForever.get()) { 448 if (log.isEnabled()) { 449 log.info("*** GET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation()); 450 } 451 return null; 452 } 453 454 final MethodHandle cmh = constantGetter(c); 455 456 MethodHandle mh; 457 MethodHandle guard; 458 459 if (isOptimistic) { 460 if (JSType.getAccessorTypeIndex(cmh.type().returnType()) <= JSType.getAccessorTypeIndex(retType)) { 461 //widen return type - this is pessimistic, so it will always work 462 mh = MH.asType(cmh, cmh.type().changeReturnType(retType)); 463 } else { 464 //immediately invalidate - we asked for a too wide constant as a narrower one 465 mh = MH.dropArguments(MH.insertArguments(JSType.THROW_UNWARRANTED.methodHandle(), 0, c, programPoint), 0, Object.class); 466 } 467 } else { 468 //pessimistic return type filter 469 mh = Lookup.filterReturnType(cmh, retType); 470 } 471 472 if (find.getOwner().isGlobal()) { 473 guard = null; 474 } else { 475 guard = MH.insertArguments(RECEIVER_GUARD, 0, acc, receiver); 476 } 477 478 if (log.isEnabled()) { 479 log.info("Linked getter " + quote(name) + " as MethodHandle.constant() -> " + c + " " + acc.getSwitchPoint()); 480 mh = MethodHandleFactory.addDebugPrintout(log, Level.FINE, mh, "get const " + acc); 481 } 482 483 return new GuardedInvocation(mh, guard, acc.getSwitchPoint(), null); 484 } 485 } 486 } 487