[Next] [Previous] [Top] [Contents] [Index]
RootCause
In addition to the built-in traces and actions available in the RootCause GUI, RootCause also supports inserting arbitrary "probes" into an application. This allows for custom statistics-gathering, or even modifying the flow of the program.
The custom probes are themselves written in Java, and are activated by specifying them in a deployment descriptor file which is an XML file with the suffix .xmj.
This chapter describes how one writes these files though a graduated series of a examples. The source text for these examples is found on-line in the directory:
APROBE\Demo\RootCause\Java\Custom.
Let's start with a simple example of how to do this, using the same Pi demo supplied with RootCause that was used in Chapter 5, "RootCause Demo".
Create a file called MyFirstProbe.java as shown below.
This probe will print messages whenever a method to which it is applied gets called.
import com.ocsystems.aprobe.*;
public class MyFirstProbe extends ProbeMethod
{
public boolean onEntry( Object[] parameters )
{
System.out.println( "Hello world (from my probe)!" );
for( int i=parameters.length-1; i>=0; i-- )
{
System.out.println(
"Parameter # " + i + " is " + parameters[i] );
}
return true;
}
public Object onExit( Object returnValue )
{
System.out.println( "Goodbye world (from my probe)!" );
System.out.println( "Return value was: " + returnValue );
return returnValue;
}
public void onExit() // no return value
{
System.out.println( "Goodbye world (from my probe)!" );
}
public void onExceptionExit( Throwable e )
{
System.out.println(
"Goodbye world (from my probe), due to exception:" );
System.out.println( e );
}
}
Note that although all this probe does is make some calls to print via System.out.println(), probes may contain arbitrary Java code, to do whatever you want.
You can create any of your own probes to be applied to methods simply by extending the class com.ocsystems.aprobe.ProbeMethod, and overriding the onEntry(), onExit() and onExceptionExit() methods, as shown above. As you can see, your probe has access to the parameters and return value of the method it is probing.
Then compile this MyFirstProbe.java. You must specify to the java compiler where it can find the classes in the package com.ocsystems.aprobe. Do this with the command
This will create MyFirstProbe.class.
Copy the .class file into the workspace directory of the workspace you wish to use. Your workspace from the Pi example from previous chapters would be an appropriate place.
Now that we have written a probe, how do we specify where this probe is to apply? This is done with an XMJ file, or deployment descriptor file, as follows.
In the workspace directory, create another file, called MyFirstProbe.xmj, with the following contents:
<probe_deployment>
<probe class="MyFirstProbe">
<target value="Pi::main"/>
</probe>
</probe_deployment>
This indicates that the probe contained in the class MyFirstProbe should be applied to the method main() in target class Pi. You could pick any method in any class that interested you.
The workspace directory is added the the classpath when you run with RootCause, so you're ready to run if you have copied your .class and .xmj files to the workspace directory.
NOTE: If you put your custom probes in a JAR file, you'll need to rebuild the workspace to cause that to be added to the classpath (or you can add it explicitly to your 'java' command if that's convenient. To pick up a JAR file in the workspace, you can rebuild the workspace from the command-line with:
rootcause build Pi.awsIf you have the RootCause console open, you can just click the Build button.
Run the Pi program under RootCause. You may do this either by pressing the Run button in the RootCause GUI, or by turning RootCause on and running the program from the command line as described in "Trace With RootCause".
You will see the output from MyFirstProbe as Pi.main() is executed.
You may wish to apply a probe to many different methods. There are several ways to do this with the .xmj deployment descriptor file, without changing the Java at all: wildcards, target lists, and using multiple .xmj files.
You can use a wildcard string as the target value in your .xmj file to accomplish this, as shown below:
<probe_deployment>
<probe class="MyFirstProbe">
<target value="*::*"/>
</probe>
</probe_deployment>
Using WildcardExample.xmj as the deployment descriptor file will cause every method to have MyFirstProbe applied to it.
The class, the method, or both can be a wildcard. All of the following are valid:
Probe all methods in all classes, as shown above:
<target value="*::*"/>Probe all methods in a given class:
<target value="Classname::*"/>Probe all methods named "foo" in any class:
<target value="*::foo"/>You can define a list of targets, and reference that list as the target of a probe, as shown in ListExample.xmj below:
<probe_deployment>
<target_list_definition name="ListExampleList">
<list_target value="Pi::calc_pi"/>
<list_target value="Pi::main"/>
</target_list_definition >
<probe class="MyFirstProbe">
<target_list name="ListExampleList"/>
</probe>
</probe_deployment>
This deployment descriptor file shows how to construct a list. This example would apply MyFirstProbe to two methods, main() and calc_pi().
All .xmj files in the workspace directory are automatically detected and applied. so you could simply make a copy of one file, change the target name, and you'll pick up both target methods on the next run. Conversely, any .xmj files you do not wish to use must be removed from the workspace.
If you ran the introductory example, and applied the single probe to a number of different target methods, you may have noticed that the output can be a bit hard to figure out, because all of the methods being probed are producing the same output. However, there is built-in support to identify different target methods. Each one has a unique method ID, and your ProbeMethod knows the ID of the target method upon which it is acting.
In addition, the class com.ocsystems.aprobe.SymbolTable contains a variety of utility functions for manipulating method IDs. The most immediately useful such is getPrintableMethodName(). It converts a method ID to a String containing the name of the method. The method ID itself is retrieved by the invoking the probe's own getMethodId() method, which is inherited from the class com.ocsystems.aprobe.ProbeMethod.
This next probe, Example2.java, uses com.ocsystems.aprobe.SymbolTable.getPrintableMethodName() and a probe's method ID to make very clear the program flow in the target:
import com.ocsystems.aprobe.*;
public class Example2 extends ProbeMethod
{
private String methodName = "(unknown)";
private void printEvent( String event )
{
System.out.println(
event + " (in method " + methodName + ")" );
}
public boolean onEntry( Object[] parameters )
{
methodName =
SymbolTable.getPrintableMethodName( getMethodId() );
printEvent( "Method Entry" );
return true;
}
public Object onExit( Object returnValue )
{
printEvent( "Method Exit" );
return returnValue;
}
public void onExit() // no return value
{
printEvent( "Method Exit" );
}
public void onExceptionExit( Throwable e )
{
printEvent( "Method Exit via exception" );
}
}
This example also shows that your probe class is just another class. You can define any of your own fields and methods in it, and use them as you would in any other class, just as we did here with methodName and printEvent().
So far, we have sent all output from our probes to System.out. This mixes up the probe's output with the application's output, and makes post-runtime analysis more difficult. As you've seen in earlier chapters, RootCause normally logs data at runtime to be examined later. You can use the same mechanism in your custom probes. This is used most easily through the classcom.ocsystems.aprobe.Logger.
The following probe uses the method log() in that class to record information which will then be displayed when the APD file is formatted and examined.
import com.ocsystems.aprobe.*;
public class Example3 extends ProbeMethod
{
public boolean onEntry( Object[] parameters )
{
Logger.log( "Hello world (from my probe)!" );
return true;
}
public Object onExit( Object returnValue )
{
Logger.log( "Goodbye world (from my probe)!" );
return returnValue;
}
public void onExit() // no return value
{
Logger.log( "Goodbye world (from my probe)!" );
}
public void onExceptionExit( Throwable e )
{
Logger.log(
"Goodbye world via exception (from my probe)!" );
}
}
The probes that we have seen so far perform actions only on entry to, and exit from, the target methods. It is also possible to probe individual source lines of a method if it has been compiled with debugging information (usually using "javac -g"). This is done using the method onLine(), as shown in Example4.java below.
import com.ocsystems.aprobe.*;
public class Example4 extends ProbeMethod
{
private String methodName = "(unknown)";
private void printEvent( String event )
{
System.out.println(
event + " (in method " + methodName + ")" );
}
public void onLine( int lineNumber )
{
printEvent( "Line # " + lineNumber );
}
public boolean onEntry( Object[] parameters )
{
methodName =
SymbolTable.getPrintableMethodName( getMethodId() );
printEvent( "Method Entry" );
return true;
}
public Object onExit( Object returnValue )
{
printEvent( "Method Exit" );
return returnValue;
}
public void onExit()
{
printEvent( "Method Exit" );
}
public void onExceptionExit( Throwable e )
{
printEvent( "Method Exit via exception" );
}
}
If not applied selectively, line probes can seriously impact performance. For this reason, line probing is turned off by default.
Thus, in order for the onLine() method to be called, you must specify that your probe needs to access lines. This is done in the deployment descriptor (.xmj) file, using the attribute "lines" in the <probe> tag, as shown in Example4.xmj below. Valid values for the "lines" attribute are "TRUE" or "FALSE". If you do not specify a value, "FALSE" is assumed.
<probe_deployment>
<probe class="Example4" lines="TRUE">
<target value="Pi::main"/>
</probe>
</probe_deployment>
So far, all the examples have been, in certain ways, pretty simple and straightforward. Each probe is independent. Each applies to certain methods, and a new probe instance is created every time one of those methods is invoked. You've simply specified the name of the probe, and its targets, in the deployment descriptor, and RootCause did the rest automatically.
Under the covers, however, what's going on is quite intricate. For each method being probed, there exists what we call a "trigger", which, essentially, works as a "factory" for probes. When the method is invoked, this trigger gets, well, triggered, and then, by default, creates an instance of the appropriate probe.
This structure allows enormously powerful customizations. Triggers can create probes only conditionally. Triggers can be enabled or disabled. Triggers can be removed entirely from association with a method, and new triggers can be created dynamically.
How does one use this power? With a "ProbeBean". What is a "ProbeBean"? It is a class, derived from com.ocsystems.aprobe.ProbeBean, that groups together related code, data, triggers, and probes. ProbeBeans are created and initialized when the Java application first starts (while RootCause is on), and they in turn create the triggers that create the probes. Every probe is created by a trigger, and every trigger belongs to a particular ProbeBean.
There are a couple of places where you would want to use a "ProbeBean" instead of the simpler method described earlier:
you can collect data on a per-thread basis; and
sophisticated probes can be structured more easily, e.g., the number of class files can be reduced for the probes and data can be more easily shared among probes.
In the examples we've seen so far, an automatically-created ProbeBean created triggers to invoke the probes you specified in your deployment descriptor file. The files MyFirstProbeBean.java and MyFirstProbeBean.xmj below show how you would manually create and deploy a ProbeBean for MyFirstProbe.java, shown at the start of this chapter.
<probe_deployment>
<bean class="MyFirstProbeBean">
<instrument_target>
<target value="Pi::main"/>
</instrument_target>
</bean>
</probe_deployment>
In MyFirstProbeBean.xmj we use the tag <bean>, and the attribute "class" to specify which class is the ProbeBean to load. The <target> tag looks familiar, but what is <instrument_target>? Since the deployment descriptor no longer directly causes the creation of probes, we don't know which classes we need to add "hooks" to. The <instrument_target> tag indicates that RootCause must add its hooks to the targets (or target lists) specified.
Note that <instrument_target> does not create any triggers or probes, the way a <probe> tag does. The creation of triggers and probes is left up to the ProbeBean itself, as shown in MyFirstProbeBean.java below.
The tag <instrument_target> also accepts the attribute "lines" just as "probe" does, so that line probes may be added.
import com.ocsystems.aprobe.*;
public class MyFirstProbeBean extends ProbeBean
{
public void onEntry()
{
int targetID = getTarget();
new ProbeTargetTrigger( targetID )
{
public Probe createProbe()
{
return new MyFirstProbe();
};
};
}
}
Let's look more closely at MyFirstProbeBean, which is as simple as it gets. When the deployment descriptor is read, RootCause will create an instance of any ProbeBean specified with the <bean> tag, and invoke its onEntry() method. The ProbeBean needs to know what methods to apply itself to, so it can create the appropriate triggers. This is done with the method getTarget(), which returns an ID corresponding the group of methods specified within an <instrument_target> tag in the .xmj file. The ProbeBean then creates a new trigger, derived from ProbeTargetTrigger and applied to that target, which in turn creates probes of the class MyFirstProbe. The fact that our new trigger is a ProbeTargetTrigger causes it to automatically apply itself to all the methods represented by the target ID, and its createProbe() method gets invoked whenever any of those methods does.
That's probably more than you wanted to know, but this allows you to explore the power of ProbeBeans further in the next example.
Once you've compiled your ProbeBean and Probe Java code, you can still greatly vary their behavior through the deployment descriptor. The obvious way we've already seen is by changing the target methods to which your probes apply. You can also, however, pass arbitrary other parameters to your Probe or ProbeBean via the deployment descriptor, using the <parameter> tag. This pair of examples will show how to do that, for a ProbeBean or for a stand-alone Probe.
<probe_deployment>
<bean class="Example6ABean">
<parameter name="ReallyHaveProbes" value="true"/>
<instrument_target>
<target value="Pi::main"/>
</instrument_target>
</bean>
</probe_deployment>
As you can see, the <parameter> tag has two attributes, "name" and "value"; both are required. In Example6ABean.java below, we'll see how the ProbeBean can reference and use the parameter we've defined.
import com.ocsystems.aprobe.*;
public class Example6ABean extends ProbeBean
{
public void onEntry()
{
String reallyHaveProbesString =
getParameter( "ReallyHaveProbes" );
boolean reallyHaveProbes =
( Boolean.valueOf( reallyHaveProbesString )
).booleanValue();
if( reallyHaveProbes )
{
int target = getTarget();
new ProbeTargetTrigger( target )
{
public Probe createProbe()
{
return new Example4();
};
};
}
else
{
System.out.println(
"Probes disabled by parameter in deployment!" );
}
}
}
The ProbeBean retrieves the value of a parameter, as a String, by name, also a String. This name passed to getParameter() must exactly match the attribute "name" in the <parameter> tag.
This method of parameterization via the deployment file may also be used without ProbeBeans, as shown on the following pages:
<probe_deployment> <probe class="Example6BProbe"> <parameter name="BeVerbose" value="true"/> <target value="Pi::main"/> </probe> </probe_deployment>
The parameter is specified the same way here, but nested within the "probe" tag rather than the <bean> tag. The mechanism by which a Probe retrieves the parameter is shown in Example6BProbe.java.
Every probe has a field which references the trigger that created it, and every trigger belongs to a bean. Here, even though we did not create a custom bean explicitly, a default bean was created for us by RootCause. It is through this bean that a probe accesses its parameters. Here, the probe uses the value of the parameter to decide how much information to display.
Note that although both of these examples used the parameter as a boolean value, you are not restricted to that. Parameters could represent names, numerical values, etc. You may have multiple parameters for a single bean or probe, as long as each parameter has a unique name.
import com.ocsystems.aprobe.*;
public class Example6BProbe extends ProbeMethod
{
boolean beVerbose;
public boolean onEntry( Object[] parameters )
{
System.out.println( "Entering a method." );
String beVerboseString =
trigger.bean.getParameter( "BeVerbose" );
beVerbose =
( Boolean.valueOf( beVerboseString )
).booleanValue();
if( beVerbose )
{
for( int i=parameters.length-1; i>=0; i-- )
{
System.out.println(
"Parameter # " + i + " is " + parameters[i] );
}
}
return true;
}
public Object onExit( Object returnValue )
{
System.out.println( "Exiting a method." );
if( beVerbose )
{
System.out.println(
"Method's return value is: " + returnValue );
}
return returnValue;
}
// exercise for reader:
// provide other onExit, onExceptionExit methods
}
In addition to methods, RootCause can also track threads. In fact, unless you specify otherwise, all your method probes are actually created within the context of a default thread probe! But you can also create your own thread probes to get more information about your multi-threaded applications. Thread probes are created by a thread trigger every time the JVM creates a new thread. The thread trigger is typically created via a ProbeBean.
You can use thread probes in order to keep track of data on a per-thread basis. Like method probes, thread probes have an onEntry() method. All thread probes derive from the class com.ocsystems.aprobe.ProbeThread.
A thread probe is created by a ProbeThreadTrigger. Our ProbeBean creates a new such ProbeThreadTrigger that will create instances of our ProbeThread class.
Example7Bean.java, on the next page, tracks the beginning of all threads, and counts calls to methods within each of those threads. The Example7.xmj deployment description file applies this thread tracking to all methods in the Pi class.
<probe_deployment>
<bean class="Example7Bean">
<instrument_target>
<target value="Pi::*"/>
</instrument_target>
</bean>
</probe_deployment>
import com.ocsystems.aprobe.*;
public class Example7Bean extends ProbeBean
{
int target;
static int numberOfThreads = 0;
class Example7ThreadProbe extends ProbeThread
{
int threadNumber;
int callsInThisThread;
class Example7MethodProbe extends ProbeMethod
{
public boolean onEntry( Object[] p )
{
callsInThisThread++;
System.out.println(
"Call # " + callsInThisThread +
" in thread # " + threadNumber );
return true;
}
}
public void onEntry() // thread entry
{
threadNumber = ++numberOfThreads;
System.out.println(
"Hello from start of thread # " + threadNumber );
new ProbeTargetTrigger( target, this )
{
public Probe createProbe()
{
return new Example7MethodProbe();
}
};
}
} // end ProbeThread
public void onEntry() // bean entry
{
target = getTarget();
new ProbeThreadTrigger()
{
public Probe createProbe()
{
return new Example7ThreadProbe();
}
};
}
}
Triggers include the ability to be activated and deactivated dynamically. Example8.java on the next page shows a probe that will only be called once per thread, because the first time it is invoked, it disables the trigger that created it.
Of course, you don't have to restrict your logic to turning the trigger off immediately. You could set whatever conditions you like, such as after 100 iterations, or when a buffer fills up.
import com.ocsystems.aprobe.*;
class MyMethodProbe extends ProbeMethod
{
public boolean onEntry( Object[] p )
{
System.out.println(
"You should only see this once per thread!" );
getTrigger().disableProbe();
return true;
}
}
public class Example8 extends ProbeBean
{
public void onEntry() // bean entry
{
int target = getTarget();
new ProbeTargetTrigger( target )
{
public Probe createProbe()
{
return new MyMethodProbe();
};
};
}
}