This is now solved.
I noticed that the problem became worse the bigger the classpath was, even
if the extra jars contained totally irrellevant classes.
Profiling revealed that the rule compilation generated an enomous amount of
IO traffic from ClassLoader.loadClass originating from
org.mvel.ParserContext.checkForDynamicImport and
org.drools.rule.PackageCompilationData$PackageClassLoader.
In both these places Drools generates a lot of loadClass calls for classes
that does not exist.
It seems like the Sun classloaders does not cache the result of failed class
lookups, but scans the whole classpath again and again looking for these
non-existing classes.
That is why the problem gets bigger the larger your classpath is. Our
classpath contains 100+ jars, so we got hit pretty bad.
I solved the problem by creating a caching classloader that also caches the
result of failed lookups. When I added this as the context classloader
scoped around all my compilation I saw a performance increase of a factor of
10 - 20 in our various environments.
Here are the stats from the caching classloader after compiling 850 rules:
Caching loader called 200608 times
119 actual successful loads
932 actual failed loads
73738 cache hits for successfull loads
125819 cache hits for failed loads
In other words: Drools generated 126751 loads for 932 non-existing classes.
We saved 125819 full classapth scans by caching the failed lookups.
As mentioned above the caching classloader can be hooked into the system by
setting it at the context classloader.
org.mvel.ParserContext.checkForDynamicImport uses the context classloader
directly.
org.drools.rule.PackageCompilationData$PackageClassLoader uses the
classloader configured in PackageBuilderConfiguration which is the context
classloader by default.
This means you get the full effect only when you scope the caching class
loader around all your PackageBuilderConfiguration and PackageBuilder usage.
Below is the code for our caching classloader for the reference of anybody
else who might encounter this problem.
Regards
Chr
PS Edson:
This problem is related to compilation. The RETE building is usually very
fast.
----------------------------------------------
package domain.rules;
import java.util.HashMap;
import java.util.Map;
/**
* Classloader that intercepts calls to the current context class loader and
caches the result of the calls to {@link #loadClass}
* on it.
* <p/>
* The interception will start automagically when this class is
instantiated. Make sure that you call {@link #stop()} from a finally
* block when you want the loader interception to stop, to avoid unnecessary
caching.
*
* @author Christian Nedregård
* @see Thread#getContextClassLoader()
*/
public class CachingContextClassLoader extends ClassLoader {
private Map classLoaderResultMap = new HashMap();
private ClassLoader originalClassLoader;
private int successfulCalls = 0;
private int failedCalls = 0;
private int cacheHits = 0;
private int successfulLoads = 0;
private int failedLoads = 0;
/**
* Create an instance. The instance will automagically start
intercepting {@link #loadClass(String)} calls to the current
* context class loader.
*
* @see Thread#getContextClassLoader()
*/
CachingContextClassLoader() {
super(Thread.currentThread().getContextClassLoader());
this.originalClassLoader =
Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this);
}
/**
* Try to load the class from the original context class loader and
chache the result (also <code>ClassNotFoundException</code>s).
* Subsequent calls for the same class name will return the cached
version, or throw a cached ClassNotFoundException if it was
* not found initially.
*
* @param name the name of the class to find.
* @return the cached result of a call to <code>loadClass</code> on the
original context class loader.
* @throws ClassNotFoundException when the class of the given name could
not be found.
*/
public Class loadClass(String name) throws ClassNotFoundException {
Object result;
if (classLoaderResultMap.containsKey(name)) {
++cacheHits;
result = classLoaderResultMap.get(name);
} else {
try {
result = super.loadClass(name);
++successfulLoads;
} catch (ClassNotFoundException e) {
++failedLoads;
result = e;
}
classLoaderResultMap.put(name, result);
}
if (result instanceof ClassNotFoundException) {
++failedCalls;
throw (ClassNotFoundException) result;
} else {
++successfulCalls;
return (Class) result;
}
}
/**
* Stop intercepting by resetting the original context classloader using
{@link Thread#setContextClassLoader(ClassLoader)}.
*/
public void stop() {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
/**
* Get a textual report of the caching that was performed.
*
* @return a textual report of the caching that was performed.
*/
public String getReport() {
return new StringBuffer("Loader called ").append(successfulCalls +
failedCalls).append(" times, ").append(successfulCalls)
.append(" successfull, ").append(failedCalls).append("
failed. Cache hits: ").append(cacheHits)
.append(", successful loads:
").append(successfulLoads).append(", Failed loads: ").append(failedLoads)
.append('.').toString();
}
}
----------------------------------------------
2009/4/17 Edson Tirelli <tirelli(a)post.com>
Christian,
Can you break down this performance into 2 steps?
1. COMPILE: this is the step where textual information is parsed and
compiled into binary data. This is done by the
PackageBuilder.addPackageFromDrl() method.
2. RETE BUILDING: this is the step where the binary data is assembled into
the rete network. This is done by RuleBase.addPackage() method.
These two processes are mostly independent and it would be good to know
what is the relative time spent in each.
After that, we will need to get a few rule samples from you with a test
case so that we can investigate.
Thanks,
Edson
2009/4/17 Christian Nedregård <christian_nedregaard(a)email.com>
Thank you for the quick reply and the tip Ingomar.
>
> Since our rules are template based I was able to merge them into 30 drls
> (850 before). This gave a 30% perfomance improvement, but we are still not
> able to compile more than 2 rules per second. I also tried to execute in
> with jdk 1.6.0_03 instead of 1.4.2_11. This only gave a small performance
> increase.
>
> We are also seing the same slow performance in our production environment
> wich is JDK 1.4.2_11 on Solaris, so this is not a OS spesific problem.
>
> I addition I tried to run the compilation in a dedicated process with 1G
> available memory with no performance increase, so it is not related to
> memory restrictions either.
>
> From our experimentation I see that the compile time, unsurpisingly, is
> directly connectet to the complexity of the rule. If i strip away he LHS and
> RHS of the rules and only keep the imports and global definitions I am able
> to get a compilation throughput at 24 rules per second.
>
> Are your rules much simpler than ours? Can you think of any other reason
> why you are seeing a much better compiler throughput?
>
> Regards
> Chr
>
>
>
> On Fri, Apr 17, 2009 at 10:34 AM, Ingomar Otter <iotter(a)mac.com> wrote:
>
>> Christian,
>> with our app, we see a compile performance which is at least an order of
>> magnitude higher.
>> I would assume that in your case the extreme fine granularity of your
>> DRLs may be slowing things down.
>> Have you tried to consolidate these in bigger but fewer DRls?
>>
>> If you need that granualarity, you still have the option of joining these
>> together before running it with the help of perl and the likes.
>>
>> --I
>> _______________________________________________
>> rules-users mailing list
>> rules-users(a)lists.jboss.org
>>
https://lists.jboss.org/mailman/listinfo/rules-users
>>
>>
>
> _______________________________________________
> rules-users mailing list
> rules-users(a)lists.jboss.org
>
https://lists.jboss.org/mailman/listinfo/rules-users
>
>
--
Edson Tirelli
JBoss Drools Core Development
JBoss, a division of Red Hat @
www.jboss.com
_______________________________________________
rules-users mailing list
rules-users(a)lists.jboss.org
https://lists.jboss.org/mailman/listinfo/rules-users