Correction: The files you need to download to try out the profiler are:

ajweaver-agent.jar

ajweaver-agent-profiler.jar

The main body of the article contains incorrect links to these files. Sorry for the confusion caused.

In this post, I present a Java agent that calls back a custom listener class (which you provide) whenever methods in a set of classes (which you specify via configuration) are invoked. The only requirement on the listener class is that it contain a public static method with a specified signature:

public static void someMethod(int operation, String methodSignature, Object targetObject, Object…args), where:

operation “0” (start of method execution), “1” (normal end of method execution) or “2” (end of method execution with an uncaught exception)
methodSignature signature of the invoked method
targetObject object on which the method was invoked.
args variable number of arguments indicating the arguments the method was invoked with.

It is trivially simple writing listener classes. Here, in its entirety, is a “logging” listener:

public class SimpleListener{

public static void callme(int op, String signature, Object obj,
        Object... args) {

if ( op == 0 ){

System.out.println( signature );

}

}

}

Later, I provide a more involved (and useful) listener class in the code examples that allows the profiling of method executions.

What I really like about this project is that the agent is implemented using AspectJ, but AspectJ is fully embedded (“hidden” from view) from the user’s viewpoint. In other words, the user does not provide any resources, additional JAR’s or configuration that AspectJ requires, nor even “needs to know” that AspectJ is being used under the covers. Look at the “NotifierMain” and “PrepareAjWeaverInputs” and “Transformer” classes for details on how I did this. I do not go into this further in this post since this is irrelevant to the ideas I wish to discuss here.

Using the agent

To use the agent, you need the file “ajweaver-agent.jar” which can be downloaded from here. Next, you must provide some command line arguments to the command that launches your application, to wit “-javaagent:{absolute path to the ajweaver-agent.jar file}. Finally, you must provide a set of system properties that tell the agent what classes must have their method invocations reported, what your custom listener class is called, and what the name of the method is (in that class) that must be called back by the agent. For the example listener shown above (SimpleListener), these system properties would be defined as:

instrument-packages com.justthispackage.*,com.thispackageandallsubpackages..*,com.yet.another.package.*
(Note that the values are comma separated).
callback-class SimpleListener
callback-method callme

The command line looks like this:

java …… –javaagent:${home}/ajweaver-agent.jar –Dinstrument-packages=com.sun.example.*,com.sun.allexamples..* –Dcallback-class=SimpleListener –Dcallback-method=callme ……

With these instructions out of way, let us jump right in and try out the agent (and a cool profiler listener class).

Profiling method calls

To get started with the agent, let us look at a custom listener class (com.subhajit.ajprofiler.Profiler) that provides basic profiling of method invocations by tracking the number of times individual methods are called, and the minimum, average and maximum execution time of the invocations. These classes can be configured to optionally setup JMX functionality from which the profile information can be extracted remotely.

As with all agent callback classes, the Profiler class has a public, static method with the signature: public static void {methodName}(int,String,Object,Object..). In Profiler, the method is named “profilerCallback”:

public static void profilerCallback(int op, String methodSignature,
        Object obj, Object... args) {
    switch (op) {
    case 0: {
        final long timestamp = System.nanoTime();
        defaultInstance.begin(obj,methodSignature, timestamp);
        break;
    }

    case 1:
    case 2: {
        final long timestamp = System.nanoTime();
        defaultInstance.end(obj, methodSignature, timestamp);
        break;
    }

    default:
        return;
    }
}

The profilerCallback method treats the two cases when the first parameter is “0” (execution start) and “1” or “2” (execution ended normally or execution ended with uncaught exception) separately. The first case is executed as method bodies are entered, and the other case is executed when method bodies are exited. These cases invoke the “begin” and “end” methods of a default instance of Profiler, respectively.

The “begin” method creates a “CflowKey” object to represent the program flow at this point using the target object (whose instance method body has started executing), the current thread, the method signature and the current timestamp. This object is next appended to a list of such objects dedicated to this method instance. This effectively records the start of execution of this method’s body in the program flow:

public void begin(Object obj, String methodSignature, long timestamp) {
    List<CflowKey> flowList = flows.get(methodSignature);
    if (flowList == null) {
        flowList = new ArrayList<CflowKey>();
        flows.putIfAbsent(methodSignature, flowList);
        flowList = flows.get(methodSignature);
    }
    synchronized (flowList) {
        flowList.add(new CflowKey(obj, Thread.currentThread(),
                methodSignature, timestamp));
    }
}

The “end” method creates a new CflowKey instance using the target object, the current thread and the method signature, and then iterates through the list of stored CflowKey objects for this method signature looking for the last element that matches this CflowKey (with respect to the target object, method signature and current thread) and for which the stored timestamp is less than the timestamp of this CflowKey object. What we are effectively doing in this step is finding the last recorded “start of execution” of this method in this program flow. Given that one object can be executing only one method in the same thread at any time, the last time we recorded the start of this method for the same object and the same thread must correspond to the start of this selfsame flow of execution. Having found this CflowKey item, we record its timestamp, and then remove it from the list of stored CflowKey items.

public void endHelper(Object obj, String methodSignature, long timestamp) {
    List<CflowKey> flowList = flows.get(methodSignature);
    if (flowList == null) {
        return;
    }
    CflowKey searchFlowKey = new CflowKey(obj, Thread.currentThread(),
            methodSignature, timestamp);
    boolean executionTimeHasBeenSet = false;
    long executionTime = 0;
    synchronized (flowList) {
        int index = 0;
        int foundIndex = -1;
        for (CflowKey flowKey : flowList) {
            if (flowKey.getTimestamp() < searchFlowKey.getTimestamp()
                    && flowKey.equals(searchFlowKey)) {
                foundIndex = index;
            }
            index++;
        }
        if (foundIndex != -1) {
            executionTime = searchFlowKey.getTimestamp()
                    - flowList.get(foundIndex).getTimestamp();
            executionTimeHasBeenSet = true;
            flowList.remove(foundIndex);
        }
    }

    if (executionTimeHasBeenSet) {
        // Time to update methodInfoMap.
        MethodExecutionInfo methodExecutionInfo = methodInfoMap
                .get(methodSignature);
        if (methodExecutionInfo == null) {
            methodExecutionInfo = new MethodExecutionInfo();
            methodInfoMap.putIfAbsent(methodSignature, methodExecutionInfo);
            methodExecutionInfo = methodInfoMap.get(methodSignature);
        }
        synchronized (methodExecutionInfo) {
            methodExecutionInfo.update(executionTime);
        }
    }
}

The second part of this method (starting with the line “if (executionTimeHasBeenSet)”), updates a “MethodExecutionInfo” object with the time taken by this execution of the method. The “update” method of the MethodExecutionInfo class updates its internal state using the execution time:

public void update(long executionTime) {
    if (executionTime < minimumExecutionTime) {
        minimumExecutionTime = executionTime;
    }
    if (executionTime > maximumExecutionTime) {
        maximumExecutionTime = executionTime;
    }
    averageExecutionTime = (averageExecutionTime * invocationCount + executionTime)
            / (++invocationCount);
}

Trying it out

To try out this agent and the profiler, download the two files ajweaver-agent.jar and ajweaver-agent-profiler.jar (see the top of this article under "Correction" for the download links) and place them in a temporary directory (say “C:\tmp”). Next, modify the launch script for your application by introducing the following terms (prior to the specification of the class to be executed):

java….-Djavaagent:c:\tmp\ajweaver-agent.jar –Dinstrument-packages={list of packages} –Dcallback-class=com.subhajit.ajprofiler.Profiler –Dcallback-method=profilerCallback …

If this is a long running application (such as a server), also add the term: –Dprofiler-port={a port number}, and you will be able to access profiling information via JConsole. For example, if you entered port “1234”, start JConsole, choose “Remote Process”, enter “localhost:1234” and click on the “Connect” button. Once connected, go to the “MBeans” tab, locate the tree node for “com.subhajit.ajprofiler” and open it out fully. Selecting “Operations”, followed by clicking on the “getData” button, fetches a Map of accumulated profile information from the Profiler class.

To try this with Apache tomcat, make the following changes:

1. Make a copy of the “bin\catalina.bat” file and call it “bin\cataprofiler.bat”.

2. Add the following lines to “bin\cataprofiler.bat”:

set JAVA_OPTS=-Dinstrument-packages=org.apache.catalina..*,org.apache.coyote..*,org.apache.naming..*,org.apache.tomcat..* -javaagent:C:\var\tmp\ajweaver-agent.jar -Dcallback-method=profilerCallback -Dcallback-class=com.subhajit.ajprofiler.Profiler -Xms1024m -Xmx1256m -Dprofiler-port=1234

set CLASSPATH=C:\tmp\ajweaver-agent-profiler\dist\ajweaver-agent-profiler.jar;%CLASSPATH%

changing the package names in the first line to whatever packages you are interested in.

3. In the “bin\setclasspath.bat” file, locate the following stanza and modify it as shown:

rem Set standard CLASSPATH
rem Note that there are no quotes as we do not want to introduce random
rem quotes into the CLASSPATH
if not exist "%JAVA_HOME%\lib\tools.jar" goto noJavac
set CLASSPATH=%JAVA_HOME%\lib\tools.jar;C:\tmp\ajweaver-agent-profiler\dist\ajweaver-agent-profiler.jar

4. Start tomcat using the command: “bin\cataprofiler.bat run”.

5. After tomcat starts up, start JConsole using the command: “jconsole”, which assumes that you have the JDK’s “bin” directory in your path. You should see:

aj1

Select “Remote Process”, enter “localhost:1234” as shown and click on the “Connect” button. This takes you to:

aj2

Select the “MBeans” tab:

aj3

Clicking on the “getData” button pops up a dialog showing accumulated profile data:

aj4

To use a custom GUI program, download ajweaver-agent-profiler-gui.jar. To use it, run “java –jar ajweaver-agent-profiler-gui.jar”. This brings up:

aj5

Click the “Refresh” button to bring up a dialog:

aj6


Enter the hostname and port where the profiler is available. If the profiled application is running on the same host as the GUI, you could enter “localhost” for the host. For the port, enter the value of the “profiler-port” system property that was specified when the application was started. Clicking the “Ok” button brings up:

aj7

Clicking the “Refresh” button refreshes the display.

Other applications

Some applications of this facility that immediately come to mind are method tracing, method logging, heap tracing (dynamically tracing the amount of heap space used by an application as it runs), application health monitoring (trends of method invocations resulting in uncaught exceptions vs. “normal” execution), etc.. At development time, this framework can provide lightweight profiling capabilities.

Comments:

Hi Subhajit,
The agent-weaver and agent-weaver-profiler are pointing to the same file:
http://www.mediafire.com/download.php?xsjfitdrgje
Is this intended?

Thanks,
OS

Posted by Ocean Stinger on March 07, 2009 at 10:30 AM CST #

Hey Ocean Stinger,

I have re-uploaded the files and added a "Correction" to the beginning of the blog post...please download the files from the newly updated locations. Sorry for the mixup. By way of explanation, I recently started using mediafire to store builds that I want to share...do you know of a better (more friendly) site? Thanks,

Posted by Subhajit Dasgupta on March 07, 2009 at 07:02 PM CST #

Hi Subhajit,
Thank you for your quick reply and fixup. I would use for code some of the public repositories, let me name few:
http://code.google.com - quite a popular choice, svn based
http://github.com/ - public git repo
and there couple of others.
Again, very nice work,
Cheers,
OS

Posted by Ocean Stinger on March 08, 2009 at 06:14 AM CDT #

Hi Subhajit,
Could you include the source in the jar files as well?
Thanks in advance for this,
OS

Posted by Ocean Stinger on March 08, 2009 at 07:54 AM CDT #

I definitely plan to post the source code as well (just as with all my other posts). At this time, I am working on a mini-project that includes only that code that is necessary to have a blog project working: the alternative (which is what my previous blog posts are) is to have you wade through hundreds of classes (most of which are irrelevant to the task at hand). Give me a couple of days and I should be done with this as well. Thanks for your patience.

Posted by Subhajit Dasgupta on March 08, 2009 at 07:59 AM CDT #

Thanks. I tried this with Java CAPS 5.1.3 which runs on 1.5.0_10. It worked!

1.5 Jconsole didn't work so I used 1.6 Jconsole.

Posted by Katsumi INOUE on April 14, 2009 at 03:47 AM CDT #

I'm unable to use this profiler, I always get a Jdk14Trace error for AspectJ (Cannor register non aspect). Does someone has an issue for that ?

Posted by Loïc Juillerat on June 15, 2009 at 03:56 AM CDT #

Hey Subhajit,
Any chance we can see the code?
Cheers,
OS

Posted by OS on June 29, 2009 at 06:35 PM CDT #

Get a Jdk14Trace error for AspectJ (Can not register non aspect). TOO!
info register aspect com.subhajit.ajweaver.inotifier.MethodInterceptor
Nov 24, 2009 11:24:51 AM org.aspectj.weaver.tools.Jdk14Trace
error
SEVERE: register definition failed
java.lang.RuntimeException: Cannot register non aspect: com$subhajit$ajweaver$inotifier$MethodInterceptor , com.subhajit.ajweaver.inotifier.MethodInterceptor

Posted by 216.117.131.98 on November 23, 2009 at 09:41 PM CST #

Post a Comment:
  • HTML Syntax: NOT allowed

This blog copyright 2009 by Subhajit Dasgupta