blob: 7f93e706afd21736cd66ad09660cdcd3bef68190 [file] [log] [blame]
/*
* Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.nashorn.internal.runtime;
import static jdk.nashorn.internal.codegen.CompilerConstants.staticCall;
import static jdk.nashorn.internal.codegen.CompilerConstants.virtualCall;
import static jdk.nashorn.internal.lookup.Lookup.MH;
import static jdk.nashorn.internal.runtime.UnwarrantedOptimismException.INVALID_PROGRAM_POINT;
import static jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor.getProgramPoint;
import static jdk.nashorn.internal.runtime.logging.DebugLogger.quote;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.SwitchPoint;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import jdk.internal.dynalink.CallSiteDescriptor;
import jdk.internal.dynalink.DynamicLinker;
import jdk.internal.dynalink.linker.GuardedInvocation;
import jdk.internal.dynalink.linker.LinkRequest;
import jdk.nashorn.internal.lookup.Lookup;
import jdk.nashorn.internal.lookup.MethodHandleFactory;
import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
import jdk.nashorn.internal.runtime.logging.DebugLogger;
import jdk.nashorn.internal.runtime.logging.Loggable;
import jdk.nashorn.internal.runtime.logging.Logger;
/**
* Each context owns one of these. This is basically table of accessors
* for global properties. A global constant is evaluated to a MethodHandle.constant
* for faster access and to avoid walking to proto chain looking for it.
*
* We put a switchpoint on the global setter, which invalidates the
* method handle constant getters, and reverts to the standard access strategy
*
* However, there is a twist - while certain globals like "undefined" and "Math"
* are usually never reassigned, a global value can be reset once, and never again.
* This is a rather common pattern, like:
*
* x = function(something) { ...
*
* Thus everything registered as a global constant gets an extra chance. Set once,
* reregister the switchpoint. Set twice or more - don't try again forever, or we'd
* just end up relinking our way into megamorphism.
*
* Also it has to be noted that this kind of linking creates a coupling between a Global
* and the call sites in compiled code belonging to the Context. For this reason, the
* linkage becomes incorrect as soon as the Context has more than one Global. The
* {@link #invalidateForever()} is invoked by the Context to invalidate all linkages and
* turn off the functionality of this object as soon as the Context's {@link Context#newGlobal()} is invoked
* for second time.
*
* We can extend this to ScriptObjects in general (GLOBAL_ONLY=false), which requires
* a receiver guard on the constant getter, but it currently leaks memory and its benefits
* have not yet been investigated property.
*
* As long as all Globals in a Context share the same GlobalConstants instance, we need synchronization
* whenever we access it.
*/
@Logger(name="const")
public final class GlobalConstants implements Loggable {
/**
* Should we only try to link globals as constants, and not generic script objects.
* Script objects require a receiver guard, which is memory intensive, so this is currently
* disabled. We might implement a weak reference based approach to this later.
*/
public static final boolean GLOBAL_ONLY = true;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandle INVALIDATE_SP = virtualCall(LOOKUP, GlobalConstants.class, "invalidateSwitchPoint", Object.class, Object.class, Access.class).methodHandle();
private static final MethodHandle RECEIVER_GUARD = staticCall(LOOKUP, GlobalConstants.class, "receiverGuard", boolean.class, Access.class, Object.class, Object.class).methodHandle();
/** Logger for constant getters */
private final DebugLogger log;
/**
* Access map for this global - associates a symbol name with an Access object, with getter
* and invalidation information
*/
private final Map<String, Access> map = new HashMap<>();
private final AtomicBoolean invalidatedForever = new AtomicBoolean(false);
/**
* Constructor - used only by global
* @param log logger, or null if none
*/
public GlobalConstants(final DebugLogger log) {
this.log = log == null ? DebugLogger.DISABLED_LOGGER : log;
}
@Override
public DebugLogger getLogger() {
return log;
}
@Override
public DebugLogger initLogger(final Context context) {
return DebugLogger.DISABLED_LOGGER;
}
/**
* Information about a constant access and its potential invalidations
*/
private static class Access {
/** name of symbol */
private final String name;
/** switchpoint that invalidates the getters and setters for this access */
private SwitchPoint sp;
/** invalidation count for this access, i.e. how many times has this property been reset */
private int invalidations;
/** has a guard guarding this property getter failed? */
private boolean guardFailed;
private static final int MAX_RETRIES = 2;
private Access(final String name, final SwitchPoint sp) {
this.name = name;
this.sp = sp;
}
private boolean hasBeenInvalidated() {
return sp.hasBeenInvalidated();
}
private boolean guardFailed() {
return guardFailed;
}
private void failGuard() {
invalidateOnce();
guardFailed = true;
}
private void newSwitchPoint() {
assert hasBeenInvalidated();
sp = new SwitchPoint();
}
private void invalidate(final int count) {
if (!sp.hasBeenInvalidated()) {
SwitchPoint.invalidateAll(new SwitchPoint[] { sp });
invalidations += count;
}
}
/**
* Invalidate the access, but do not contribute to the invalidation count
*/
private void invalidateUncounted() {
invalidate(0);
}
/**
* Invalidate the access, and contribute 1 to the invalidation count
*/
private void invalidateOnce() {
invalidate(1);
}
/**
* Invalidate the access and make sure that we never try to turn this into
* a MethodHandle.constant getter again
*/
private void invalidateForever() {
invalidate(MAX_RETRIES);
}
/**
* Are we allowed to relink this as constant getter, even though it
* it has been reset
* @return true if we can relink as constant, one retry is allowed
*/
private boolean mayRetry() {
return invalidations < MAX_RETRIES;
}
@Override
public String toString() {
return "[" + quote(name) + " <id=" + Debug.id(this) + "> inv#=" + invalidations + '/' + MAX_RETRIES + " sp_inv=" + sp.hasBeenInvalidated() + ']';
}
String getName() {
return name;
}
SwitchPoint getSwitchPoint() {
return sp;
}
}
/**
* To avoid an expensive global guard "is this the same global", similar to the
* receiver guard on the ScriptObject level, we invalidate all getters once
* when we switch globals. This is used from the class cache. We _can_ reuse
* the same class for a new global, but the builtins and global scoped variables
* will have changed.
*/
public void invalidateAll() {
if (!invalidatedForever.get()) {
log.info("New global created - invalidating all constant callsites without increasing invocation count.");
synchronized (this) {
for (final Access acc : map.values()) {
acc.invalidateUncounted();
}
}
}
}
/**
* To avoid an expensive global guard "is this the same global", similar to the
* receiver guard on the ScriptObject level, we invalidate all getters when the
* second Global is created by the Context owning this instance. After this
* method is invoked, this GlobalConstants instance will both invalidate all the
* switch points it produced, and it will stop handing out new method handles
* altogether.
*/
public void invalidateForever() {
if (invalidatedForever.compareAndSet(false, true)) {
log.info("New global created - invalidating all constant callsites.");
synchronized (this) {
for (final Access acc : map.values()) {
acc.invalidateForever();
}
map.clear();
}
}
}
/**
* Invalidate the switchpoint of an access - we have written to
* the property
*
* @param obj receiver
* @param acc access
*
* @return receiver, so this can be used as param filter
*/
@SuppressWarnings("unused")
private synchronized Object invalidateSwitchPoint(final Object obj, final Access acc) {
if (log.isEnabled()) {
log.info("*** Invalidating switchpoint " + acc.getSwitchPoint() + " for receiver=" + obj + " access=" + acc);
}
acc.invalidateOnce();
if (acc.mayRetry()) {
if (log.isEnabled()) {
log.info("Retry is allowed for " + acc + "... Creating a new switchpoint.");
}
acc.newSwitchPoint();
} else {
if (log.isEnabled()) {
log.info("This was the last time I allowed " + quote(acc.getName()) + " to relink as constant.");
}
}
return obj;
}
private Access getOrCreateSwitchPoint(final String name) {
Access acc = map.get(name);
if (acc != null) {
return acc;
}
final SwitchPoint sp = new SwitchPoint();
map.put(name, acc = new Access(name, sp));
return acc;
}
/**
* Called from script object on property deletion to erase a property
* that might be linked as MethodHandle.constant and force relink
* @param name name of property
*/
void delete(final String name) {
if (!invalidatedForever.get()) {
synchronized (this) {
final Access acc = map.get(name);
if (acc != null) {
acc.invalidateForever();
}
}
}
}
/**
* Receiver guard is used if we extend the global constants to script objects in general.
* As the property can have different values in different script objects, while Global is
* by definition a singleton, we need this for ScriptObject constants (currently disabled)
*
* TODO: Note - this seems to cause memory leaks. Use weak references? But what is leaking seems
* to be the Access objects, which isn't the case for Globals. Weird.
*
* @param acc access
* @param boundReceiver the receiver bound to the callsite
* @param receiver the receiver to check against
*
* @return true if this receiver is still the one we bound to the callsite
*/
@SuppressWarnings("unused")
private static boolean receiverGuard(final Access acc, final Object boundReceiver, final Object receiver) {
final boolean id = receiver == boundReceiver;
if (!id) {
acc.failGuard();
}
return id;
}
private static boolean isGlobalSetter(final ScriptObject receiver, final FindProperty find) {
if (find == null) {
return receiver.isScope();
}
return find.getOwner().isGlobal();
}
/**
* Augment a setter with switchpoint for invalidating its getters, should the setter be called
*
* @param find property lookup
* @param inv normal guarded invocation for this setter, as computed by the ScriptObject linker
* @param desc callsite descriptor
* @param request link request
*
* @return null if failed to set up constant linkage
*/
GuardedInvocation findSetMethod(final FindProperty find, final ScriptObject receiver, final GuardedInvocation inv, final CallSiteDescriptor desc, final LinkRequest request) {
if (invalidatedForever.get() || (GLOBAL_ONLY && !isGlobalSetter(receiver, find))) {
return null;
}
final String name = desc.getNameToken(CallSiteDescriptor.NAME_OPERAND);
synchronized (this) {
final Access acc = getOrCreateSwitchPoint(name);
if (log.isEnabled()) {
log.fine("Trying to link constant SETTER ", acc);
}
if (!acc.mayRetry() || invalidatedForever.get()) {
if (log.isEnabled()) {
log.fine("*** SET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation());
}
return null;
}
if (acc.hasBeenInvalidated()) {
log.info("New chance for " + acc);
acc.newSwitchPoint();
}
assert !acc.hasBeenInvalidated();
// if we haven't given up on this symbol, add a switchpoint invalidation filter to the receiver parameter
final MethodHandle target = inv.getInvocation();
final Class<?> receiverType = target.type().parameterType(0);
final MethodHandle boundInvalidator = MH.bindTo(INVALIDATE_SP, this);
final MethodHandle invalidator = MH.asType(boundInvalidator, boundInvalidator.type().changeParameterType(0, receiverType).changeReturnType(receiverType));
final MethodHandle mh = MH.filterArguments(inv.getInvocation(), 0, MH.insertArguments(invalidator, 1, acc));
assert inv.getSwitchPoints() == null : Arrays.asList(inv.getSwitchPoints());
log.info("Linked setter " + quote(name) + " " + acc.getSwitchPoint());
return new GuardedInvocation(mh, inv.getGuard(), acc.getSwitchPoint(), inv.getException());
}
}
/**
* Try to reuse constant method handles for getters
* @param c constant value
* @return method handle (with dummy receiver) that returns this constant
*/
public static MethodHandle staticConstantGetter(final Object c) {
return MH.dropArguments(JSType.unboxConstant(c), 0, Object.class);
}
private MethodHandle constantGetter(final Object c) {
final MethodHandle mh = staticConstantGetter(c);
if (log.isEnabled()) {
return MethodHandleFactory.addDebugPrintout(log, Level.FINEST, mh, "getting as constant");
}
return mh;
}
/**
* Try to turn a getter into a MethodHandle.constant, if possible
*
* @param find property lookup
* @param receiver receiver
* @param desc callsite descriptor
*
* @return resulting getter, or null if failed to create constant
*/
GuardedInvocation findGetMethod(final FindProperty find, final ScriptObject receiver, final CallSiteDescriptor desc) {
// Only use constant getter for fast scope access, because the receiver may change between invocations
// for slow-scope and non-scope callsites.
// Also return null for user accessor properties as they may have side effects.
if (invalidatedForever.get() || !NashornCallSiteDescriptor.isFastScope(desc)
|| (GLOBAL_ONLY && !find.getOwner().isGlobal())
|| find.getProperty() instanceof UserAccessorProperty) {
return null;
}
final boolean isOptimistic = NashornCallSiteDescriptor.isOptimistic(desc);
final int programPoint = isOptimistic ? getProgramPoint(desc) : INVALID_PROGRAM_POINT;
final Class<?> retType = desc.getMethodType().returnType();
final String name = desc.getNameToken(CallSiteDescriptor.NAME_OPERAND);
synchronized (this) {
final Access acc = getOrCreateSwitchPoint(name);
log.fine("Starting to look up object value " + name);
final Object c = find.getObjectValue();
if (log.isEnabled()) {
log.fine("Trying to link constant GETTER " + acc + " value = " + c);
}
if (acc.hasBeenInvalidated() || acc.guardFailed() || invalidatedForever.get()) {
if (log.isEnabled()) {
log.info("*** GET: Giving up on " + quote(name) + " - retry count has exceeded " + DynamicLinker.getLinkedCallSiteLocation());
}
return null;
}
final MethodHandle cmh = constantGetter(c);
MethodHandle mh;
MethodHandle guard;
if (isOptimistic) {
if (JSType.getAccessorTypeIndex(cmh.type().returnType()) <= JSType.getAccessorTypeIndex(retType)) {
//widen return type - this is pessimistic, so it will always work
mh = MH.asType(cmh, cmh.type().changeReturnType(retType));
} else {
//immediately invalidate - we asked for a too wide constant as a narrower one
mh = MH.dropArguments(MH.insertArguments(JSType.THROW_UNWARRANTED.methodHandle(), 0, c, programPoint), 0, Object.class);
}
} else {
//pessimistic return type filter
mh = Lookup.filterReturnType(cmh, retType);
}
if (find.getOwner().isGlobal()) {
guard = null;
} else {
guard = MH.insertArguments(RECEIVER_GUARD, 0, acc, receiver);
}
if (log.isEnabled()) {
log.info("Linked getter " + quote(name) + " as MethodHandle.constant() -> " + c + " " + acc.getSwitchPoint());
mh = MethodHandleFactory.addDebugPrintout(log, Level.FINE, mh, "get const " + acc);
}
return new GuardedInvocation(mh, guard, acc.getSwitchPoint(), null);
}
}
}