1 /*
   2  * Copyright (c) 2010, 2017, 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.linker;
  27 
  28 import static jdk.nashorn.internal.lookup.Lookup.MH;
  29 
  30 import java.io.FileNotFoundException;
  31 import java.io.FileOutputStream;
  32 import java.io.PrintWriter;
  33 import java.lang.invoke.MethodHandle;
  34 import java.lang.invoke.MethodHandles;
  35 import java.lang.invoke.MethodType;
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.Comparator;
  39 import java.util.HashMap;
  40 import java.util.LinkedList;
  41 import java.util.Map;
  42 import java.util.Map.Entry;
  43 import java.util.Random;
  44 import java.util.Set;
  45 import java.util.concurrent.atomic.AtomicInteger;
  46 import java.util.concurrent.atomic.LongAdder;
  47 import jdk.dynalink.DynamicLinker;
  48 import jdk.dynalink.linker.GuardedInvocation;
  49 import jdk.dynalink.support.ChainedCallSite;
  50 import jdk.nashorn.internal.runtime.Context;
  51 import jdk.nashorn.internal.runtime.Debug;
  52 import jdk.nashorn.internal.runtime.ScriptObject;
  53 import jdk.nashorn.internal.runtime.ScriptRuntime;
  54 import jdk.nashorn.internal.runtime.options.Options;
  55 
  56 
  57 /**
  58  * Relinkable form of call site.
  59  */
  60 public class LinkerCallSite extends ChainedCallSite {
  61     /** Maximum number of arguments passed directly. */
  62     public static final int ARGLIMIT = 125;
  63 
  64     private static final String PROFILEFILE = Options.getStringProperty("nashorn.profilefile", "NashornProfile.txt");
  65 
  66     private static final MethodHandle INCREASE_MISS_COUNTER = MH.findStatic(MethodHandles.lookup(), LinkerCallSite.class, "increaseMissCount", MH.type(Object.class, String.class, Object.class));
  67 
  68     LinkerCallSite(final NashornCallSiteDescriptor descriptor) {
  69         super(descriptor);
  70         if (Context.DEBUG) {
  71             LinkerCallSite.count.increment();
  72         }
  73     }
  74 
  75     /**
  76      * Construct a new linker call site.
  77      * @param name     Name of method.
  78      * @param type     Method type.
  79      * @param flags    Call site specific flags.
  80      * @return New LinkerCallSite.
  81      */
  82     static LinkerCallSite newLinkerCallSite(final MethodHandles.Lookup lookup, final String name, final MethodType type, final int flags) {
  83         final NashornCallSiteDescriptor desc = NashornCallSiteDescriptor.get(lookup, name, type, flags);
  84 
  85         if (desc.isProfile()) {
  86             return ProfilingLinkerCallSite.newProfilingLinkerCallSite(desc);
  87         }
  88 
  89         if (desc.isTrace()) {
  90             return new TracingLinkerCallSite(desc);
  91         }
  92 
  93         return new LinkerCallSite(desc);
  94     }
  95 
  96     @Override
  97     public String toString() {
  98         return getDescriptor().toString();
  99     }
 100 
 101     /**
 102      * Get the descriptor for this callsite
 103      * @return a {@link NashornCallSiteDescriptor}
 104      */
 105     public NashornCallSiteDescriptor getNashornDescriptor() {
 106         return (NashornCallSiteDescriptor)getDescriptor();
 107     }
 108 
 109     @Override
 110     public void relink(final GuardedInvocation invocation, final MethodHandle relink) {
 111         super.relink(invocation, getDebuggingRelink(relink));
 112     }
 113 
 114     @Override
 115     public void resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink) {
 116         super.resetAndRelink(invocation, getDebuggingRelink(relink));
 117     }
 118 
 119     private MethodHandle getDebuggingRelink(final MethodHandle relink) {
 120         if (Context.DEBUG) {
 121             return MH.filterArguments(relink, 0, getIncreaseMissCounter(relink.type().parameterType(0)));
 122         }
 123         return relink;
 124     }
 125 
 126     private MethodHandle getIncreaseMissCounter(final Class<?> type) {
 127         final MethodHandle missCounterWithDesc = MH.bindTo(INCREASE_MISS_COUNTER, getDescriptor().getOperation() + " @ " + getScriptLocation());
 128         if (type == Object.class) {
 129             return missCounterWithDesc;
 130         }
 131         return MH.asType(missCounterWithDesc, missCounterWithDesc.type().changeParameterType(0, type).changeReturnType(type));
 132     }
 133 
 134     private static String getScriptLocation() {
 135         final StackTraceElement caller = DynamicLinker.getLinkedCallSiteLocation();
 136         return caller == null ? "unknown location" : (caller.getFileName() + ":" + caller.getLineNumber());
 137     }
 138 
 139     /**
 140      * Instrumentation - increase the miss count when a callsite misses. Used as filter
 141      * @param desc descriptor for table entry
 142      * @param self self reference
 143      * @return self reference
 144      */
 145     public static Object increaseMissCount(final String desc, final Object self) {
 146         missCount.increment();
 147         if (r.nextInt(100) < missSamplingPercentage) {
 148             final AtomicInteger i = missCounts.get(desc);
 149             if (i == null) {
 150                 missCounts.put(desc, new AtomicInteger(1));
 151             } else {
 152                 i.incrementAndGet();
 153             }
 154         }
 155         return self;
 156     }
 157 
 158     /*
 159      * Debugging call sites.
 160      */
 161 
 162     private static class ProfilingLinkerCallSite extends LinkerCallSite {
 163         /** List of all profiled call sites. */
 164         private static LinkedList<ProfilingLinkerCallSite> profileCallSites = null;
 165 
 166         /** Start time when entered at zero depth. */
 167         private long startTime;
 168 
 169         /** Depth of nested calls. */
 170         private int depth;
 171 
 172         /** Total time spent in this call site. */
 173         private long totalTime;
 174 
 175         /** Total number of times call site entered. */
 176         private long hitCount;
 177 
 178         private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
 179 
 180         private static final MethodHandle PROFILEENTRY    = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileEntry",    MH.type(Object.class, Object.class));
 181         private static final MethodHandle PROFILEEXIT     = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileExit",     MH.type(Object.class, Object.class));
 182         private static final MethodHandle PROFILEVOIDEXIT = MH.findVirtual(LOOKUP, ProfilingLinkerCallSite.class, "profileVoidExit", MH.type(void.class));
 183 
 184         /*
 185          * Constructor
 186          */
 187 
 188         ProfilingLinkerCallSite(final NashornCallSiteDescriptor desc) {
 189            super(desc);
 190         }
 191 
 192         public static ProfilingLinkerCallSite newProfilingLinkerCallSite(final NashornCallSiteDescriptor desc) {
 193             if (profileCallSites == null) {
 194                 profileCallSites = new LinkedList<>();
 195 
 196                 final Thread profileDumperThread = new Thread(new ProfileDumper());
 197                 Runtime.getRuntime().addShutdownHook(profileDumperThread);
 198             }
 199 
 200             final ProfilingLinkerCallSite callSite = new ProfilingLinkerCallSite(desc);
 201             profileCallSites.add(callSite);
 202 
 203             return callSite;
 204         }
 205 
 206         @Override
 207         public void setTarget(final MethodHandle newTarget) {
 208             final MethodType type   = type();
 209             final boolean    isVoid = type.returnType() == void.class;
 210             final Class<?> newSelfType = newTarget.type().parameterType(0);
 211 
 212             MethodHandle selfFilter = MH.bindTo(PROFILEENTRY, this);
 213             if (newSelfType != Object.class) {
 214                 // new target uses a more precise 'self' type than Object.class. We need to
 215                 // convert the filter type. Note that the profileEntry method returns "self"
 216                 // argument "as is" and so the cast introduced will succeed for any type.
 217                 final MethodType selfFilterType = MethodType.methodType(newSelfType, newSelfType);
 218                 selfFilter = selfFilter.asType(selfFilterType);
 219             }
 220 
 221             MethodHandle methodHandle = MH.filterArguments(newTarget, 0, selfFilter);
 222 
 223             if (isVoid) {
 224                 methodHandle = MH.filterReturnValue(methodHandle, MH.bindTo(PROFILEVOIDEXIT, this));
 225             } else {
 226                 final MethodType filter = MH.type(type.returnType(), type.returnType());
 227                 methodHandle = MH.filterReturnValue(methodHandle, MH.asType(MH.bindTo(PROFILEEXIT, this), filter));
 228             }
 229 
 230             super.setTarget(methodHandle);
 231         }
 232 
 233         /**
 234          * Start the clock for a profile entry and increase depth
 235          * @param self argument to filter
 236          * @return preserved argument
 237          */
 238         @SuppressWarnings("unused")
 239         public Object profileEntry(final Object self) {
 240             if (depth == 0) {
 241                 startTime = System.nanoTime();
 242             }
 243 
 244             depth++;
 245             hitCount++;
 246 
 247             return self;
 248         }
 249 
 250         /**
 251          * Decrease depth and stop the clock for a profile entry
 252          * @param result return value to filter
 253          * @return preserved argument
 254          */
 255         @SuppressWarnings("unused")
 256         public Object profileExit(final Object result) {
 257             depth--;
 258 
 259             if (depth == 0) {
 260                 totalTime += System.nanoTime() - startTime;
 261             }
 262 
 263             return result;
 264         }
 265 
 266         /**
 267          * Decrease depth without return value filter
 268          */
 269         @SuppressWarnings("unused")
 270         public void profileVoidExit() {
 271             depth--;
 272 
 273             if (depth == 0) {
 274                 totalTime += System.nanoTime() - startTime;
 275             }
 276         }
 277 
 278         static class ProfileDumper implements Runnable {
 279             @Override
 280             public void run() {
 281                 PrintWriter out    = null;
 282                 boolean fileOutput = false;
 283 
 284                 try {
 285                     try {
 286                         out = new PrintWriter(new FileOutputStream(PROFILEFILE));
 287                         fileOutput = true;
 288                     } catch (final FileNotFoundException e) {
 289                         out = Context.getCurrentErr();
 290                     }
 291 
 292                     dump(out);
 293                 } finally {
 294                     if (out != null && fileOutput) {
 295                         out.close();
 296                     }
 297                 }
 298             }
 299 
 300             private static void dump(final PrintWriter out) {
 301                 int index = 0;
 302                 for (final ProfilingLinkerCallSite callSite : profileCallSites) {
 303                    out.println("" + (index++) + '\t' +
 304                                   callSite.getDescriptor().getOperation() + '\t' +
 305                                   callSite.totalTime + '\t' +
 306                                   callSite.hitCount);
 307                 }
 308             }
 309         }
 310     }
 311 
 312     /**
 313      * Debug subclass for LinkerCallSite that allows tracing
 314      */
 315     private static class TracingLinkerCallSite extends LinkerCallSite {
 316         private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
 317 
 318         private static final MethodHandle TRACEOBJECT = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceObject", MH.type(Object.class, MethodHandle.class, Object[].class));
 319         private static final MethodHandle TRACEVOID   = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceVoid", MH.type(void.class, MethodHandle.class, Object[].class));
 320         private static final MethodHandle TRACEMISS   = MH.findVirtual(LOOKUP, TracingLinkerCallSite.class, "traceMiss", MH.type(void.class, String.class, Object[].class));
 321 
 322         TracingLinkerCallSite(final NashornCallSiteDescriptor desc) {
 323            super(desc);
 324         }
 325 
 326         @Override
 327         public void setTarget(final MethodHandle newTarget) {
 328             if (!getNashornDescriptor().isTraceEnterExit()) {
 329                 super.setTarget(newTarget);
 330                 return;
 331             }
 332 
 333             final MethodType type = type();
 334             final boolean isVoid = type.returnType() == void.class;
 335 
 336             MethodHandle traceMethodHandle = isVoid ? TRACEVOID : TRACEOBJECT;
 337             traceMethodHandle = MH.bindTo(traceMethodHandle, this);
 338             traceMethodHandle = MH.bindTo(traceMethodHandle, newTarget);
 339             traceMethodHandle = MH.asCollector(traceMethodHandle, Object[].class, type.parameterCount());
 340             traceMethodHandle = MH.asType(traceMethodHandle, type);
 341 
 342             super.setTarget(traceMethodHandle);
 343         }
 344 
 345         @Override
 346         public void initialize(final MethodHandle relinkAndInvoke) {
 347             super.initialize(getFallbackLoggingRelink(relinkAndInvoke));
 348         }
 349 
 350         @Override
 351         public void relink(final GuardedInvocation invocation, final MethodHandle relink) {
 352             super.relink(invocation, getFallbackLoggingRelink(relink));
 353         }
 354 
 355         @Override
 356         public void resetAndRelink(final GuardedInvocation invocation, final MethodHandle relink) {
 357             super.resetAndRelink(invocation, getFallbackLoggingRelink(relink));
 358         }
 359 
 360         private MethodHandle getFallbackLoggingRelink(final MethodHandle relink) {
 361             if (!getNashornDescriptor().isTraceMisses()) {
 362                 // If we aren't tracing misses, just return relink as-is
 363                 return relink;
 364             }
 365             final MethodType type = relink.type();
 366             return MH.foldArguments(relink, MH.asType(MH.asCollector(MH.insertArguments(TRACEMISS, 0, this, "MISS " + getScriptLocation() + " "), Object[].class, type.parameterCount()), type.changeReturnType(void.class)));
 367         }
 368 
 369         private void printObject(final PrintWriter out, final Object arg) {
 370             if (!getNashornDescriptor().isTraceObjects()) {
 371                 out.print((arg instanceof ScriptObject) ? "ScriptObject" : arg);
 372                 return;
 373             }
 374 
 375             if (arg instanceof ScriptObject) {
 376                 final ScriptObject object = (ScriptObject)arg;
 377 
 378                 boolean isFirst = true;
 379                 final Set<Object> keySet = object.keySet();
 380 
 381                 if (keySet.isEmpty()) {
 382                     out.print(ScriptRuntime.safeToString(arg));
 383                 } else {
 384                     out.print("{ ");
 385 
 386                     for (final Object key : keySet) {
 387                         if (!isFirst) {
 388                             out.print(", ");
 389                         }
 390 
 391                         out.print(key);
 392                         out.print(":");
 393 
 394                         final Object value = object.get(key);
 395 
 396                         if (value instanceof ScriptObject) {
 397                             out.print("...");
 398                         } else {
 399                             printObject(out, value);
 400                         }
 401 
 402                         isFirst = false;
 403                     }
 404 
 405                     out.print(" }");
 406                 }
 407              } else {
 408                 out.print(ScriptRuntime.safeToString(arg));
 409             }
 410         }
 411 
 412         private void tracePrint(final PrintWriter out, final String tag, final Object[] args, final Object result) {
 413             //boolean isVoid = type().returnType() == void.class;
 414             out.print(Debug.id(this) + " TAG " + tag);
 415             out.print(getDescriptor().getOperation() + "(");
 416 
 417             if (args.length > 0) {
 418                 printObject(out, args[0]);
 419                 for (int i = 1; i < args.length; i++) {
 420                     final Object arg = args[i];
 421                     out.print(", ");
 422 
 423                     if (!(arg instanceof ScriptObject && ((ScriptObject)arg).isScope())) {
 424                         printObject(out, arg);
 425                     } else {
 426                         out.print("SCOPE");
 427                     }
 428                 }
 429             }
 430 
 431             out.print(")");
 432 
 433             if (tag.equals("EXIT  ")) {
 434                 out.print(" --> ");
 435                 printObject(out, result);
 436             }
 437 
 438             out.println();
 439         }
 440 
 441         /**
 442          * Trace event. Wrap an invocation with a return value
 443          *
 444          * @param mh     invocation handle
 445          * @param args   arguments to call
 446          *
 447          * @return return value from invocation
 448          *
 449          * @throws Throwable if invocation fails or throws exception/error
 450          */
 451         @SuppressWarnings("unused")
 452         public Object traceObject(final MethodHandle mh, final Object... args) throws Throwable {
 453             final PrintWriter out = Context.getCurrentErr();
 454             tracePrint(out, "ENTER ", args, null);
 455             final Object result = mh.invokeWithArguments(args);
 456             tracePrint(out, "EXIT  ", args, result);
 457 
 458             return result;
 459         }
 460 
 461         /**
 462          * Trace event. Wrap an invocation that returns void
 463          *
 464          * @param mh     invocation handle
 465          * @param args   arguments to call
 466          *
 467          * @throws Throwable if invocation fails or throws exception/error
 468          */
 469         @SuppressWarnings("unused")
 470         public void traceVoid(final MethodHandle mh, final Object... args) throws Throwable {
 471             final PrintWriter out = Context.getCurrentErr();
 472             tracePrint(out, "ENTER ", args, null);
 473             mh.invokeWithArguments(args);
 474             tracePrint(out, "EXIT  ", args, null);
 475         }
 476 
 477         /**
 478          * Tracer function that logs a callsite miss
 479          *
 480          * @param desc callsite descriptor string
 481          * @param args arguments to function
 482          *
 483          * @throws Throwable if invocation fails or throws exception/error
 484          */
 485         @SuppressWarnings("unused")
 486         public void traceMiss(final String desc, final Object... args) throws Throwable {
 487             tracePrint(Context.getCurrentErr(), desc, args, null);
 488         }
 489     }
 490 
 491     // counters updated in debug mode
 492     private static LongAdder count;
 493     private static final HashMap<String, AtomicInteger> missCounts = new HashMap<>();
 494     private static LongAdder missCount;
 495     private static final Random r = new Random();
 496     private static final int missSamplingPercentage = Options.getIntProperty("nashorn.tcs.miss.samplePercent", 1);
 497 
 498     static {
 499         if (Context.DEBUG) {
 500             count = new LongAdder();
 501             missCount = new LongAdder();
 502         }
 503     }
 504 
 505     @Override
 506     protected int getMaxChainLength() {
 507         return 8;
 508     }
 509 
 510     /**
 511      * Get the callsite count
 512      * @return the count
 513      */
 514     public static long getCount() {
 515         return count.longValue();
 516     }
 517 
 518     /**
 519      * Get the callsite miss count
 520      * @return the missCount
 521      */
 522     public static long getMissCount() {
 523         return missCount.longValue();
 524     }
 525 
 526     /**
 527      * Get given miss sampling percentage for sampler. Default is 1%. Specified with -Dnashorn.tcs.miss.samplePercent=x
 528      * @return miss sampling percentage
 529      */
 530     public static int getMissSamplingPercentage() {
 531         return missSamplingPercentage;
 532     }
 533 
 534     /**
 535      * Dump the miss counts collected so far to a given output stream
 536      * @param out print stream
 537      */
 538     public static void getMissCounts(final PrintWriter out) {
 539         final ArrayList<Entry<String, AtomicInteger>> entries = new ArrayList<>(missCounts.entrySet());
 540 
 541         Collections.sort(entries, new Comparator<Map.Entry<String, AtomicInteger>>() {
 542             @Override
 543             public int compare(final Entry<String, AtomicInteger> o1, final Entry<String, AtomicInteger> o2) {
 544                 return o2.getValue().get() - o1.getValue().get();
 545             }
 546         });
 547 
 548         for (final Entry<String, AtomicInteger> entry : entries) {
 549             out.println("  " + entry.getKey() + "\t" + entry.getValue().get());
 550         }
 551     }
 552 
 553 }