mardi 4 juin 2013

Project Tyrion

Recently, thanks to Kirk Pepperdine for the idea and a brainstorming session, I started working on a new open-source lock-profiler (Tyrion). Now that I have a first version working, it's time to write an article to explain the concepts for those who would want to join.

Why Tyrion ?

Because it is interresting !

Consider the following questions :
  • In my application, which locks are never used ?
  • In my application, which locks are used only by one thread ?
  • In my application, which locks are contended ?
To answer them, you have to dig inside the JVM and understand the inner-workings, you have to be curious, you eventually have to manipulate code that you did not write. These are challenges that sound fun !

How (high level view) ?

I wanted to instrument code to have a clear view on the critical sections of the application. Basically, I wanted to transform this kind of code (assuming bar is an instance variable) :
public void foo() {
  synchronized(bar) {
    //...
  }
}

My goal is to have this result :
public void foo() {
  synchronized(bar) {
    LocksInterceptor.enteredSynchronizedBlock(bar);
    //...
    LocksInterceptor.leavingSynchronizedBlock(bar);
  }
}

How (tricky technical details) ?

The bytecode of method foo looks like this.
GETFIELD #3
MONITORENTER
...
GETFIELD #3
MONITOREXIT

I want to get a reference to the target of the lock. Fortunately, the bytecode DUP is just what I need, since it duplicated the last element on the stack. I just have to transform the code so that it looks like this.
GETFIELD #3
DUP
MONITORENTER
INVOKESTATIC   #4    // fr/pingtimeout/tyrion/LockInterceptor.enteredSynchronizedBlock:(Ljava/lang/Object;)V
...
GETFIELD #3
DUP
INVOKESTATIC   #5    // fr/pingtimeout/tyrion/LockInterceptor.leavingSynchronizedBlock:(Ljava/lang/Object;)V
MONITOREXIT

And guess what ? Using ASM, obtaining this result is really simple, as you only have to find MONITORENTER and MONITOREXIT, and add intructions before/after them. See the class LockTransformer.java for more details

Pitfall 1 - Synchronized methods

This approach is nice, but does not cover all possible cases. For instance, there is not MONITORENTER nor MONITOREXIT for synchronized methods. The JVM handles synchronization internally. The solution is to visit every synchronzied methods and insert calls to LockInterceptor at method entry and method exit.

Now the question is : what is the lock ? Simple : it is "this", and a reference to "this" in instance methods can be pushed on the stack using ALOAD_0.

So the following bytecodes should be added as the first statements of every synchronized method.
ALOAD_0
INVOKESTATIC   #6    // fr/pingtimeout/tyrion/LockInterceptor.enteredSynchronizedMethod:(Ljava/lang/Object;)V

And the following bytecodes should be added as the last statements of every synchronized method.
ALOAD_0
INVOKESTATIC   #7    // fr/pingtimeout/tyrion/LockInterceptor.leavingSynchronizedMethod:(Ljava/lang/Object;)V

Pitfall 2 - Static methods

So far, things are cool. However, static methods are not. In a static synchronized method, the lock is the class itself, and it cannot be retrieved by ALOAD_0.

To get a reference to the class, I had to get the class FQN and insert a call to java/lang/Class.forName() instead of the ALOAD_0 instruction. So the following bytecodes should be added as the first statements of every static-synchronized method.
LDC            "class FQN"
INVOKESTATIC   #2    // java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
INVOKESTATIC   #6    // fr/pingtimeout/tyrion/LockInterceptor.enteredSynchronizedMethod:(Ljava/lang/Object;)V

And the following bytecodes should be added as the last statements of every static-synchronized method.
LDC            "class FQN"
INVOKESTATIC   #2    // java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
INVOKESTATIC   #7    // fr/pingtimeout/tyrion/LockInterceptor.leavingSynchronizedMethod:(Ljava/lang/Object;)V

Conclusion & Special Thanks

That's it, synchronized blocks and methods are intercepted, and it did not required lots of code (3 bytecodes inserted at most, 5 classes to do that with ASM). The source is available on GitHub, if you want to have a look.

There are lots of subtle things going on, such as reentrant locks, but this is another topic.

Special thanks to Julien Ponge and Frédéric Le Mouël who did a great talk at Devoxx France 2012 about bytecode manipulation. This really helped me a lot. Also, thanks to ASM guys, this framework rox.

Any experience with ASM or bytecode manipulation ? Already wrote an open-source lock profiler ? Let's discuss it in the comments !

Aucun commentaire:

Enregistrer un commentaire