Permalink
Browse files

Multiple changes and enhancements to ClassCache:

- Move "one shot" classloading logic into ClassCache
- Remove all references to org.jruby code by defining a new OneShotClassLoader and adding a T param for Class<?>
- Add a "max" value to limit size of the cache
- Add a reference queue to allow efficient cleanup of dead class references
- ClassGenerator must implement bytecode() and name() methods

Also changes in JRuby to use the new cache
- Modified JITCompiler to use the new queue correctly
- JITCompiler gives up on JIT if cache.isFull() or cacheClassByKey returns null (failed to cache or load class)


git-svn-id: http://svn.codehaus.org/jruby/trunk/jruby@5937 961051c9-f516-0410-bf72-c9f7e237a7b7
  • Loading branch information...
1 parent 4c2f70d commit 114cc4121cdcb4dba145f7c20735b08517865a9b @headius headius committed Feb 15, 2008
@@ -37,6 +37,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import org.jruby.ast.executable.Script;
import org.jruby.exceptions.MainExitException;
import org.jruby.runtime.Constants;
import org.jruby.util.ClassCache;
@@ -92,7 +93,7 @@ public boolean shouldPrecompileAll() {
private ClassLoader loader = Thread.currentThread().getContextClassLoader();
- private ClassCache classCache = new ClassCache(loader);
+ private ClassCache<Script> classCache;
// from CommandlineParser
private List<String> loadPaths = new ArrayList<String>();
@@ -209,6 +210,9 @@ public RubyInstanceConfig() {
jitMax = max == null ? 2048 : Integer.parseInt(max);
}
+ // default ClassCache using jitMax as a soft upper bound
+ classCache = new ClassCache<Script>(loader, jitMax);
+
if (FORK_ENABLED) {
error.print("WARNING: fork is highly unlikely to be safe or stable on the JVM. Have fun!\n");
}
@@ -41,98 +41,41 @@
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ThreadContext;
import org.jruby.util.ClassCache;
-import org.jruby.util.JRubyClassLoader;
+import org.jruby.util.CodegenUtils;
import org.jruby.util.JavaNameMangler;
public class JITCompiler {
public static final boolean USE_CACHE = true;
- public static void runJIT(final DefaultMethod method, final Ruby runtime, ThreadContext context, final String name) {
+ public static void runJIT(final DefaultMethod method, final Ruby runtime, final ThreadContext context, final String name) {
Set<Script> jittedMethods = runtime.getJittedMethods();
final RubyInstanceConfig instanceConfig = runtime.getInstanceConfig();
- int jitMax = instanceConfig.getJitMax();
+ ClassCache classCache = instanceConfig.getClassCache();
- // We either are not JIT'ing, or we have already JIT'd too much. Go no further.
- if (method.getCallCount() < 0 || jitMax == 0 || (jitMax != -1 && jittedMethods.size() >= jitMax)) return;
+ // This method has JITed already or has been abandoned. Bail out.
+ if (method.getCallCount() < 0) return;
try {
method.setCallCount(method.getCallCount() + 1);
if (method.getCallCount() >= instanceConfig.getJitThreshold()) {
- String cleanName = JavaNameMangler.mangleStringForCleanJavaIdentifier(name);
- Node bodyNode = method.getBodyNode();
- final ArgsNode argsNode = method.getArgsNode();
- String filename = calculateFilename(argsNode, bodyNode);
- StaticScope staticScope = method.getStaticScope();
-
- final StandardASMCompiler asmCompiler = new StandardASMCompiler(cleanName +
- method.hashCode() + "_" + context.hashCode(), filename);
- asmCompiler.startScript(staticScope);
- final ASTCompiler compiler = new ASTCompiler();
-
- CompilerCallback args = new CompilerCallback() {
- public void call(MethodCompiler context) {
- compiler.compileArgs(argsNode, context);
- }
- };
-
- ASTInspector inspector = new ASTInspector();
- inspector.inspect(bodyNode);
- inspector.inspect(argsNode);
-
- MethodCompiler methodCompiler;
- CallConfiguration jitCallConfig = null;
- if (bodyNode != null) {
- // we have a body, do a full-on method
- methodCompiler = asmCompiler.startMethod("__file__", args, staticScope, inspector);
- compiler.compile(bodyNode, methodCompiler);
- } else {
- // If we don't have a body, check for required or opt args
- // if opt args, they could have side effects
- // if required args, need to raise errors if too few args passed
- // otherwise, method does nothing, make it a nop
- if (argsNode != null && (argsNode.getRequiredArgsCount() > 0 || argsNode.getOptionalArgsCount() > 0)) {
- methodCompiler = asmCompiler.startMethod("__file__", args, staticScope, inspector);
- methodCompiler.loadNil();
- } else {
- methodCompiler = asmCompiler.startMethod("__file__", null, staticScope, inspector);
- methodCompiler.loadNil();
- jitCallConfig = CallConfiguration.NO_FRAME_NO_SCOPE;
- }
+
+ // The cache is full. Abandon JIT for this method and bail out.
+ if (classCache.isFull()) {
+ method.setCallCount(-1);
+ return;
}
- methodCompiler.endMethod();
- asmCompiler.endScript(false, false, false);
-
- ClassCache.ClassGenerator classGenerator = new ClassCache.ClassGenerator() {
- @SuppressWarnings("unchecked")
- public Class<Script> generate(ClassLoader classLoader) throws ClassNotFoundException {
- Class<?> result = asmCompiler.loadClass(new JRubyClassLoader(classLoader));
-
- if (instanceConfig.isJitLogging()) log(method, name, "compiled anew");
- return (Class<Script>) result;
- }
- };
+ JITClassGenerator generator = new JITClassGenerator(name, method, context);
- Class<Script> sourceClass;
- if (USE_CACHE) {
- String key = SexpMaker.create(name, method.getArgsNode(), method.getBodyNode());
+ String key = SexpMaker.create(name, method.getArgsNode(), method.getBodyNode());
- sourceClass = instanceConfig.getClassCache().cacheClassByKey(key, classGenerator);
- } else {
- sourceClass = classGenerator.generate(new JRubyClassLoader(runtime.getJRubyClassLoader()));
- }
-
- // if we haven't already decided on a do-nothing call
- if (jitCallConfig == null) {
- // if we're not doing any of the operations that still need
- // a scope, use the scopeless config
- if (inspector.hasClosure() || inspector.hasScopeAwareMethods()) {
- jitCallConfig = CallConfiguration.FRAME_AND_SCOPE;
- } else {
- // switch to a slightly faster call config
- jitCallConfig = CallConfiguration.FRAME_ONLY;
- }
+ Class<Script> sourceClass = instanceConfig.getClassCache().cacheClassByKey(key, generator);
+
+ if (sourceClass == null) {
+ // class could not be found nor generated; give up on JIT and bail out
+ method.setCallCount(-1);
+ return;
}
// finally, grab the script
@@ -145,13 +88,13 @@ public void call(MethodCompiler context) {
if (instanceConfig.getJitLogEvery() > 0) {
int methodCount = jittedMethods.size();
if (methodCount % instanceConfig.getJitLogEvery() == 0) {
- System.err.println("live compiled methods: " + methodCount);
+ log(method, name, "live compiled methods: " + methodCount);
}
}
if (instanceConfig.isJitLogging()) log(method, name, "done jitting");
- method.setJITCallConfig(jitCallConfig);
+ method.setJITCallConfig(generator.callConfig());
method.setJITCompiledScript(jitCompiledScript);
method.setCallCount(-1);
}
@@ -162,6 +105,99 @@ public void call(MethodCompiler context) {
}
}
+ public static class JITClassGenerator implements ClassCache.ClassGenerator {
+ private StandardASMCompiler asmCompiler;
+ private DefaultMethod method;
+ private StaticScope staticScope;
+ private Node bodyNode;
+ private ArgsNode argsNode;
+ private CallConfiguration jitCallConfig;
+
+ private byte[] bytecode;
+ private String name;
+
+ public JITClassGenerator(String name, DefaultMethod method, ThreadContext context) {
+ this.method = method;
+ String cleanName = JavaNameMangler.mangleStringForCleanJavaIdentifier(name);
+ this.bodyNode = method.getBodyNode();
+ this.argsNode = method.getArgsNode();
+ final String filename = calculateFilename(argsNode, bodyNode);
+ staticScope = method.getStaticScope();
+ asmCompiler = new StandardASMCompiler(cleanName +
+ method.hashCode() + "_" + context.hashCode(), filename);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void compile() {
+ if (bytecode != null) return;
+
+ asmCompiler.startScript(staticScope);
+ final ASTCompiler compiler = new ASTCompiler();
+
+ CompilerCallback args = new CompilerCallback() {
+ public void call(MethodCompiler context) {
+ compiler.compileArgs(argsNode, context);
+ }
+ };
+
+ ASTInspector inspector = new ASTInspector();
+ inspector.inspect(bodyNode);
+ inspector.inspect(argsNode);
+
+ MethodCompiler methodCompiler;
+ if (bodyNode != null) {
+ // we have a body, do a full-on method
+ methodCompiler = asmCompiler.startMethod("__file__", args, staticScope, inspector);
+ compiler.compile(bodyNode, methodCompiler);
+ } else {
+ // If we don't have a body, check for required or opt args
+ // if opt args, they could have side effects
+ // if required args, need to raise errors if too few args passed
+ // otherwise, method does nothing, make it a nop
+ if (argsNode != null && (argsNode.getRequiredArgsCount() > 0 || argsNode.getOptionalArgsCount() > 0)) {
+ methodCompiler = asmCompiler.startMethod("__file__", args, staticScope, inspector);
+ methodCompiler.loadNil();
+ } else {
+ methodCompiler = asmCompiler.startMethod("__file__", null, staticScope, inspector);
+ methodCompiler.loadNil();
+ jitCallConfig = CallConfiguration.NO_FRAME_NO_SCOPE;
+ }
+ }
+ methodCompiler.endMethod();
+ asmCompiler.endScript(false, false, false);
+
+ // if we haven't already decided on a do-nothing call
+ if (jitCallConfig == null) {
+ // if we're not doing any of the operations that still need
+ // a scope, use the scopeless config
+ if (inspector.hasClosure() || inspector.hasScopeAwareMethods()) {
+ jitCallConfig = CallConfiguration.FRAME_AND_SCOPE;
+ } else {
+ // switch to a slightly faster call config
+ jitCallConfig = CallConfiguration.FRAME_ONLY;
+ }
+ }
+
+ bytecode = asmCompiler.getClassByteArray();
+ name = CodegenUtils.c(asmCompiler.getClassname());
+ }
+
+ public byte[] bytecode() {
+ compile();
+ return bytecode;
+ }
+
+ public String name() {
+ compile();
+ return name;
+ }
+
+ public CallConfiguration callConfig() {
+ compile();
+ return jitCallConfig;
+ }
+ }
+
private static String calculateFilename(ArgsNode argsNode, Node bodyNode) {
if (bodyNode != null) return bodyNode.getPosition().getFile();
if (argsNode != null) return argsNode.getPosition().getFile();
@@ -1,48 +1,95 @@
package org.jruby.util;
+import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
-import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import org.jruby.ast.executable.Script;
/**
* A Simple cache which maintains a collection of classes that can potentially be shared among
* multiple runtimes (or whole JVM).
*/
-public class ClassCache {
- public interface ClassGenerator {
- Class<Script> generate(ClassLoader classLoader) throws ClassNotFoundException;
- }
-
+public class ClassCache<T> {
/**
* The ClassLoader this class cache will use for any classes generated through it. It is
* assumed that the classloader provided will be a parent loader of any runtime using it.
* @param classLoader to use to generate shared classes
*/
- public ClassCache(ClassLoader classLoader) {
+ public ClassCache(ClassLoader classLoader, int max) {
this.classLoader = classLoader;
+ this.max = max;
+ }
+
+ public ClassCache(ClassLoader classLoader) {
+ this(classLoader, -1);
+ }
+
+ public interface ClassGenerator {
+ byte[] bytecode();
+ String name();
+ }
+
+ private static class KeyedClassReference<T> extends WeakReference<Class<T>> {
+ private Object key;
+
+ public KeyedClassReference(Object key, Class<T> referent, ReferenceQueue<Class<T>> referenceQueue) {
+ super(referent, referenceQueue);
+
+ this.key = key;
+ }
+
+ public Object getKey() {
+ return key;
+ }
}
- private Map<Object, WeakReference<Class<Script>>> cache =
- new ConcurrentHashMap<Object, WeakReference<Class<Script>>>();
+ private static class OneShotClassLoader extends ClassLoader {
+ public OneShotClassLoader(ClassLoader parent) {
+ super(parent);
+ }
+
+ public Class<?> defineClass(String name, byte[] bytecode) {
+ return super.defineClass(name, bytecode, 0, bytecode.length);
+ }
+ }
+
+ private ReferenceQueue referenceQueue = new ReferenceQueue();
+ private Map<Object, KeyedClassReference> cache =
+ new ConcurrentHashMap<Object, KeyedClassReference>();
private ClassLoader classLoader;
+ private int max;
public ClassLoader getClassLoader() {
return classLoader;
}
- public Class<Script> cacheClassByKey(Object key, ClassGenerator classGenerator)
+ public Class<T> cacheClassByKey(Object key, ClassGenerator classGenerator)
throws ClassNotFoundException {
- WeakReference<Class<Script>> weakRef = cache.get(key);
- Class<Script> contents = null;
+ WeakReference<Class<T>> weakRef = cache.get(key);
+ Class<T> contents = null;
if (weakRef != null) contents = weakRef.get();
if (weakRef == null || contents == null) {
- contents = classGenerator.generate(getClassLoader());
- cache.put(key, new WeakReference<Class<Script>>(contents));
+ if (isFull()) return null;
+
+ OneShotClassLoader oneShotCL = new OneShotClassLoader(getClassLoader());
+ contents = (Class<T>)oneShotCL.defineClass(classGenerator.name(), classGenerator.bytecode());
+
+ cache.put(key, new KeyedClassReference(key, contents, referenceQueue));
}
return contents;
}
+
+ public boolean isFull() {
+ cleanup();
+ return max > 0 && cache.size() >= max;
+ }
+
+ private void cleanup() {
+ KeyedClassReference reference;
+ while ((reference = (KeyedClassReference)referenceQueue.poll()) != null) {
+ cache.remove(reference.getKey());
+ }
+ }
}
@@ -26,6 +26,7 @@ protected void setUp() throws Exception {
System.setProperty("jruby.compile.mode", "JIT");
System.setProperty("jruby.jit.threshold", "0");
+ // construct a new cache with thread's classloader and no limit
ClassCache classCache = new ClassCache(Thread.currentThread().getContextClassLoader());
runtime1 = JavaEmbedUtils.initialize(new ArrayList<Object>(), classCache);

0 comments on commit 114cc41

Please sign in to comment.