JITCompiler.java

  1. /***** BEGIN LICENSE BLOCK *****
  2.  * Version: EPL 1.0/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Eclipse Public
  5.  * License Version 1.0 (the "License"); you may not use this file
  6.  * except in compliance with the License. You may obtain a copy of
  7.  * the License at http://www.eclipse.org/legal/epl-v10.html
  8.  *
  9.  * Software distributed under the License is distributed on an "AS
  10.  * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  11.  * implied. See the License for the specific language governing
  12.  * rights and limitations under the License.
  13.  *
  14.  * Copyright (C) 2006-2008 Charles O Nutter <headius@headius.com>
  15.  * Copyright (C) 2008 Thomas E Enebo <enebo@acm.org>
  16.  *
  17.  * Alternatively, the contents of this file may be used under the terms of
  18.  * either of the GNU General Public License Version 2 or later (the "GPL"),
  19.  * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  20.  * in which case the provisions of the GPL or the LGPL are applicable instead
  21.  * of those above. If you wish to allow use of your version of this file only
  22.  * under the terms of either the GPL or the LGPL, and not to allow others to
  23.  * use your version of this file under the terms of the EPL, indicate your
  24.  * decision by deleting the provisions above and replace them with the notice
  25.  * and other provisions required by the GPL or the LGPL. If you do not delete
  26.  * the provisions above, a recipient may use your version of this file under
  27.  * the terms of any one of the EPL, the GPL or the LGPL.
  28.  ***** END LICENSE BLOCK *****/
  29. package org.jruby.compiler;


  30. import org.jruby.MetaClass;
  31. import org.jruby.Ruby;
  32. import org.jruby.RubyEncoding;
  33. import org.jruby.RubyInstanceConfig;
  34. import org.jruby.RubyModule;
  35. import org.jruby.ast.util.SexpMaker;
  36. import org.jruby.internal.runtime.methods.CompiledIRMethod;
  37. import org.jruby.internal.runtime.methods.InterpretedIRMethod;
  38. import org.jruby.ir.IRMethod;
  39. import org.jruby.ir.targets.JVMVisitor;
  40. import org.jruby.parser.StaticScope;
  41. import org.jruby.runtime.Block;
  42. import org.jruby.runtime.Helpers;
  43. import org.jruby.runtime.ThreadContext;
  44. import org.jruby.runtime.builtin.IRubyObject;
  45. import org.jruby.threading.DaemonThreadFactory;
  46. import org.jruby.util.JavaNameMangler;
  47. import org.jruby.util.OneShotClassLoader;
  48. import org.jruby.util.cli.Options;
  49. import org.jruby.util.log.Logger;
  50. import org.jruby.util.log.LoggerFactory;
  51. import org.objectweb.asm.Opcodes;

  52. import java.lang.invoke.MethodHandle;
  53. import java.lang.invoke.MethodHandles;
  54. import java.lang.invoke.MethodType;
  55. import java.lang.reflect.Method;
  56. import java.security.MessageDigest;
  57. import java.security.NoSuchAlgorithmException;
  58. import java.util.Locale;
  59. import java.util.Map;
  60. import java.util.concurrent.ExecutorService;
  61. import java.util.concurrent.LinkedBlockingQueue;
  62. import java.util.concurrent.RejectedExecutionException;
  63. import java.util.concurrent.ThreadPoolExecutor;
  64. import java.util.concurrent.TimeUnit;
  65. import java.util.concurrent.atomic.AtomicLong;

  66. public class JITCompiler implements JITCompilerMBean {
  67.     private static final Logger LOG = LoggerFactory.getLogger("JITCompiler");

  68.     public static final String RUBY_JIT_PREFIX = "rubyjit";

  69.     public static final String CLASS_METHOD_DELIMITER = "$$";

  70.     public static class JITCounts {
  71.         private final AtomicLong compiledCount = new AtomicLong(0);
  72.         private final AtomicLong successCount = new AtomicLong(0);
  73.         private final AtomicLong failCount = new AtomicLong(0);
  74.         private final AtomicLong abandonCount = new AtomicLong(0);
  75.         private final AtomicLong compileTime = new AtomicLong(0);
  76.         private final AtomicLong averageCompileTime = new AtomicLong(0);
  77.         private final AtomicLong codeSize = new AtomicLong(0);
  78.         private final AtomicLong averageCodeSize = new AtomicLong(0);
  79.         private final AtomicLong largestCodeSize = new AtomicLong(0);
  80.     }

  81.     private final JITCounts counts = new JITCounts();
  82.     private final ExecutorService executor;
  83.    
  84.     private final Ruby runtime;
  85.     private final RubyInstanceConfig config;
  86.    
  87.     public JITCompiler(Ruby runtime) {
  88.         this.runtime = runtime;
  89.         this.config = runtime.getInstanceConfig();

  90.         this.executor = new ThreadPoolExecutor(
  91.                 0, // don't start threads until needed
  92.                 2, // two max
  93.                 60, // stop then if no jitting for 60 seconds
  94.                 TimeUnit.SECONDS,
  95.                 new LinkedBlockingQueue<Runnable>(),
  96.                 new DaemonThreadFactory("Ruby-" + runtime.getRuntimeNumber() + "-JIT", Thread.MIN_PRIORITY));
  97.     }

  98.     public long getSuccessCount() {
  99.         return counts.successCount.get();
  100.     }

  101.     public long getCompileCount() {
  102.         return counts.compiledCount.get();
  103.     }

  104.     public long getFailCount() {
  105.         return counts.failCount.get();
  106.     }

  107.     public long getCompileTime() {
  108.         return counts.compileTime.get() / 1000;
  109.     }

  110.     public long getAbandonCount() {
  111.         return counts.abandonCount.get();
  112.     }
  113.    
  114.     public long getCodeSize() {
  115.         return counts.codeSize.get();
  116.     }
  117.    
  118.     public long getAverageCodeSize() {
  119.         return counts.averageCodeSize.get();
  120.     }
  121.    
  122.     public long getAverageCompileTime() {
  123.         return counts.averageCompileTime.get() / 1000;
  124.     }
  125.    
  126.     public long getLargestCodeSize() {
  127.         return counts.largestCodeSize.get();
  128.     }

  129.     public void tearDown() {
  130.         if (executor != null) {
  131.             try {
  132.                 executor.shutdown();
  133.             } catch (SecurityException se) {
  134.                 // ignore, can't shut down executor
  135.             }
  136.         }
  137.     }
  138.    
  139.     public void jitThresholdReached(final InterpretedIRMethod method, final RubyInstanceConfig config, ThreadContext context, final String className, final String methodName) {
  140.         // Disable any other jit tasks from entering queue
  141.         method.setCallCount(-1);
  142.        
  143.         Runnable jitTask = new JITTask(className, method, methodName);

  144.         // if background JIT is enabled and threshold is > 0 and we have an executor...
  145.         if (config.getJitBackground() &&
  146.                 config.getJitThreshold() > 0 &&
  147.                 executor != null) {
  148.             // JIT in background
  149.             try {
  150.                 executor.submit(jitTask);
  151.             } catch (RejectedExecutionException ree) {
  152.                 // failed to submit, just run it directly
  153.                 jitTask.run();
  154.             }
  155.         } else {
  156.             // just run directly
  157.             jitTask.run();
  158.         }
  159.     }

  160.     private static final MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.publicLookup().in(Ruby.class);

  161.     private class JITTask implements Runnable {
  162.         private final String className;
  163.         private final InterpretedIRMethod method;
  164.         private final String methodName;

  165.         public JITTask(String className, InterpretedIRMethod method, String methodName) {
  166.             this.className = className;
  167.             this.method = method;
  168.             this.methodName = methodName;
  169.         }

  170.         public void run() {
  171.             try {
  172.                 // Check if the method has been explicitly excluded
  173.                 if (config.getExcludedMethods().size() > 0) {
  174.                     String excludeModuleName = className;
  175.                     if (method.getImplementationClass().getMethodLocation().isSingleton()) {
  176.                         IRubyObject possibleRealClass = ((MetaClass) method.getImplementationClass()).getAttached();
  177.                         if (possibleRealClass instanceof RubyModule) {
  178.                             excludeModuleName = "Meta:" + ((RubyModule) possibleRealClass).getName();
  179.                         }
  180.                     }

  181.                     if ((config.getExcludedMethods().contains(excludeModuleName)
  182.                             || config.getExcludedMethods().contains(excludeModuleName + "#" + methodName)
  183.                             || config.getExcludedMethods().contains(methodName))) {
  184.                         method.setCallCount(-1);
  185.                         log(method, methodName, "skipping method: " + excludeModuleName + "#" + methodName);
  186.                         return;
  187.                     }
  188.                 }

  189.                 String key = SexpMaker.sha1(method.getIRMethod());
  190.                 JVMVisitor visitor = new JVMVisitor();
  191.                 JITClassGenerator generator = new JITClassGenerator(className, methodName, key, runtime, method, visitor);

  192.                 generator.compile();

  193.                 // FIXME: reinstate active bytecode size check
  194.                 // At this point we still need to reinstate the bytecode size check, to ensure we're not loading code
  195.                 // that's so big that JVMs won't even try to compile it. Removed the check because with the new IR JIT
  196.                 // bytecode counts often include all nested scopes, even if they'd be different methods. We need a new
  197.                 // mechanism of getting all method sizes.
  198.                 Class sourceClass = visitor.defineFromBytecode(method.getIRMethod(), generator.bytecode(), new OneShotClassLoader(runtime.getJRubyClassLoader()));

  199.                 if (sourceClass == null) {
  200.                     // class could not be found nor generated; give up on JIT and bail out
  201.                     counts.failCount.incrementAndGet();
  202.                     return;
  203.                 } else {
  204.                     generator.updateCounters(counts);
  205.                 }

  206.                 // successfully got back a jitted method
  207.                 long methodCount = counts.successCount.incrementAndGet();

  208.                 // logEvery n methods based on configuration
  209.                 if (config.getJitLogEvery() > 0) {
  210.                     if (methodCount % config.getJitLogEvery() == 0) {
  211.                         log(method, methodName, "live compiled methods: " + methodCount);
  212.                     }
  213.                 }

  214.                 if (config.isJitLogging()) {
  215.                     log(method, className + "." + methodName, "done jitting");
  216.                 }

  217.                 Map<Integer, MethodType> signatures = ((IRMethod)method.getIRMethod()).getNativeSignatures();
  218.                 String jittedName = ((IRMethod)method.getIRMethod()).getJittedName();
  219.                 if (signatures.size() == 1) {
  220.                     // only variable-arity
  221.                     method.switchToJitted(
  222.                             new CompiledIRMethod(
  223.                                     PUBLIC_LOOKUP.findStatic(sourceClass, jittedName, signatures.get(-1)),
  224.                                     method.getIRMethod(),
  225.                                     method.getVisibility(),
  226.                                     method.getImplementationClass()));
  227.                 } else {
  228.                     // also specific-arity
  229.                     for (Map.Entry<Integer, MethodType> entry : signatures.entrySet()) {
  230.                         if (entry.getKey() == -1) continue; // variable arity handle pushed above

  231.                         method.switchToJitted(
  232.                                 new CompiledIRMethod(
  233.                                         PUBLIC_LOOKUP.findStatic(sourceClass, jittedName, signatures.get(-1)),
  234.                                         PUBLIC_LOOKUP.findStatic(sourceClass, jittedName, entry.getValue()),
  235.                                         entry.getKey(),
  236.                                         method.getIRMethod(),
  237.                                         method.getVisibility(),
  238.                                         method.getImplementationClass()));
  239.                         break;
  240.                     }
  241.                 }

  242.                 return;
  243.             } catch (Throwable t) {
  244.                 if (config.isJitLogging()) {
  245.                     log(method, className + "." + methodName, "Could not compile; passes run: " + method.getIRMethod().getExecutedPasses(), t.getMessage());
  246.                     if (config.isJitLoggingVerbose()) {
  247.                         t.printStackTrace();
  248.                     }
  249.                 }

  250.                 counts.failCount.incrementAndGet();
  251.                 return;
  252.             }
  253.         }
  254.     }

  255.     public static String getHashForString(String str) {
  256.         return getHashForBytes(RubyEncoding.encodeUTF8(str));
  257.     }

  258.     public static String getHashForBytes(byte[] bytes) {
  259.         try {
  260.             MessageDigest sha1 = MessageDigest.getInstance("SHA1");
  261.             sha1.update(bytes);
  262.             byte[] digest = sha1.digest();
  263.             StringBuilder builder = new StringBuilder();
  264.             for (int i = 0; i < digest.length; i++) {
  265.                 builder.append(Integer.toString( ( digest[i] & 0xff ) + 0x100, 16).substring( 1 ));
  266.             }
  267.             return builder.toString().toUpperCase(Locale.ENGLISH);
  268.         } catch (NoSuchAlgorithmException nsae) {
  269.             throw new RuntimeException(nsae);
  270.         }
  271.     }
  272.    
  273.     public static class JITClassGenerator {
  274.         public JITClassGenerator(String className, String methodName, String key, Ruby ruby, InterpretedIRMethod method, JVMVisitor visitor) {
  275.             this.packageName = JITCompiler.RUBY_JIT_PREFIX;
  276.             if (RubyInstanceConfig.JAVA_VERSION == Opcodes.V1_7 || Options.COMPILE_INVOKEDYNAMIC.load() == true) {
  277.                 // Some versions of Java 7 seems to have a bug that leaks definitions across cousin classloaders
  278.                 // so we force the class name to be unique to this runtime.

  279.                 // Also, invokedynamic forces us to make jitted bytecode unique to each runtime, since the call sites cache
  280.                 // at class level rather than at our runtime level. This makes it impossible to share jitted bytecode
  281.                 // across runtimes.
  282.                
  283.                 digestString = key + Math.abs(ruby.hashCode());
  284.             } else {
  285.                 digestString = key;
  286.             }
  287.             this.className = packageName + "/" + className.replace('.', '/') + CLASS_METHOD_DELIMITER + JavaNameMangler.mangleMethodName(methodName) + "_" + digestString;
  288.             this.name = this.className.replaceAll("/", ".");
  289.             this.methodName = methodName;
  290.             this.ruby = ruby;
  291.             this.method = method;
  292.             this.visitor = visitor;
  293.         }
  294.        
  295.         @SuppressWarnings("unchecked")
  296.         protected void compile() {
  297.             if (bytecode != null) return;
  298.            
  299.             // Time the compilation
  300.             long start = System.nanoTime();

  301.             method.ensureInstrsReady();

  302.             // This may not be ok since we'll end up running passes specific to JIT
  303.             // CON FIXME: Really should clone scope before passes in any case
  304.             bytecode = visitor.compileToBytecode(method.getIRMethod());

  305.             compileTime = System.nanoTime() - start;
  306.         }

  307.         void updateCounters(JITCounts counts) {
  308.             counts.compiledCount.incrementAndGet();
  309.             counts.compileTime.addAndGet(compileTime);
  310.             counts.codeSize.addAndGet(bytecode.length);
  311.             counts.averageCompileTime.set(counts.compileTime.get() / counts.compiledCount.get());
  312.             counts.averageCodeSize.set(counts.codeSize.get() / counts.compiledCount.get());
  313.             synchronized (counts) {
  314.                 if (counts.largestCodeSize.get() < bytecode.length) {
  315.                     counts.largestCodeSize.set(bytecode.length);
  316.                 }
  317.             }
  318.         }

  319.         public void generate() {
  320.             compile();
  321.         }
  322.        
  323.         public byte[] bytecode() {
  324.             return bytecode;
  325.         }

  326.         public String name() {
  327.             return name;
  328.         }

  329.         @Override
  330.         public String toString() {
  331.             return methodName + "() at " + method.getFile() + ":" + method.getLine();
  332.         }

  333.         private final Ruby ruby;
  334.         private final String packageName;
  335.         private final String className;
  336.         private final String methodName;
  337.         private final String digestString;
  338.         private final InterpretedIRMethod method;
  339.         private final JVMVisitor visitor;

  340.         private byte[] bytecode;
  341.         private long compileTime;
  342.         private String name;
  343.     }

  344.     static void log(InterpretedIRMethod method, String name, String message, String... reason) {
  345.         String className = method.getImplementationClass().getBaseName();
  346.        
  347.         if (className == null) className = "<anon class>";

  348.         StringBuilder builder = new StringBuilder(message + ":" + className + "." + name + " at " + method.getIRMethod().getFileName() + ":" + method.getIRMethod().getLineNumber());
  349.        
  350.         if (reason.length > 0) {
  351.             builder.append(" because of: \"");
  352.             for (int i = 0; i < reason.length; i++) {
  353.                 builder.append(reason[i]);
  354.             }
  355.             builder.append('"');
  356.         }
  357.        
  358.         LOG.info(builder.toString());
  359.     }
  360. }