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      */
 116     public GlobalConstants(final DebugLogger log) {
 117         this.log = log == null ? DebugLogger.DISABLED_LOGGER : log;
 118     }
 119 
 120     @Override
 121     public DebugLogger getLogger() {
 122         return log;
 123     }
 124 
 125     @Override
 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 
 148         private Access(final String name, final SwitchPoint sp) {
 149             this.name      = name;
 150             this.sp        = sp;
 151         }
 152 
 153         private boolean hasBeenInvalidated() {
 154             return sp.hasBeenInvalidated();
 155         }
 156 
 157         private boolean guardFailed() {
 158             return guardFailed;
 159         }
 160 
 161         private void failGuard() {
 162             invalidateOnce();
 163             guardFailed = true;
 164         }
 165 
 166         private void newSwitchPoint() {
 167             assert hasBeenInvalidated();
 168             sp = new SwitchPoint();
 169         }
 170 
 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          */
 181         private void invalidateUncounted() {
 182             invalidate(0);
 183         }
 184 
 185         /**
 186          * Invalidate the access, and contribute 1 to the invalidation count
 187          */
 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          */
 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          */
 205         private boolean mayRetry() {
 206             return invalidations < MAX_RETRIES;
 207         }
 208 
 209         @Override
 210         public String toString() {
 211             return "[" + quote(name) + " <id=" + Debug.id(this) + "> inv#=" + invalidations + '/' + MAX_RETRIES + " sp_inv=" + sp.hasBeenInvalidated() + ']';
 212         }
 213 
 214         String getName() {
 215             return name;
 216         }
 217 
 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      */
 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      */
 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")
 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 
 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      */
 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")
 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 
 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      */
 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      */
 401     public static MethodHandle staticConstantGetter(final Object c) {
 402         return MH.dropArguments(JSType.unboxConstant(c), 0, Object.class);
 403     }
 404 
 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      */
 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 }