Adding user-defined DTrace probes to Java programs

Tracing Java programs with DTrace has always been a bit challenging. To such tracing easier, we propose a new Java-level tracing interface which will allow a Java programmer to instrument their code with probes which can be traced using DTrace at runtime.

Essentially, we want to provide the programmer with USDT-like probes from Java code -- the developer defining their probes and adds probe points into their application. The probes are low-cost and have no effect on the running program, but a DTrace script may attach to the process and monitor the probe points.

We propose to add this functionality as part of a generic tracing mechanism as the Java-level, in a new com.sun.tracing package, available in the JRE.

The com.sun.tracing package defines two key entities, an interface called Provider, and a factory class that creates instances of this interface called (creatively) ProviderFactory. There are a few other annotations and interfaces in there, but these two are the key players so I'll cover the others later.

To define a DTrace-provider (or any other Java-level tracing provider, but for now there's only DTrace), one must create a new interface which extends from com.sun.tracing.Provider.  Any methods declared by the new interface represent probes which are part of the provider.  The method parameters are the probe parameters that will be available to DTrace scripts.  The probe name, by default, is the name of the method.  For example, here is the definition of a simple provider:

public interface MyProvider extends com.sun.tracing.Provider {
    void startProbe();
    void workProbe(int iteration, int value);
    void endProbe();
}

This interface definition is roughly equivalent in functionality to a USDT provider specification in native code. However, activation of the provider and placing probes in the application are a bit different that USDT. To add native USDT probes to an application, you must involve DTrace in the build step, and as a result, providers are immediately activated at application load time, and probe placement is performed by adding source-level macros in your code. The Java solution for this doesn't require any build changes, but does require a bit more manual intervention at the source-code level to activate the provider and place the probes. This is where the ProviderFactory class comes in. To activate a provider, and thus make it's probes available via a dtrace -l, you must first create an instance of a ProviderFactory using the static method, getDefaultFactory. This step can be done only once for the entire application, a single factory can create many providers if needed. The point of the factory object is to create instances of each of the developer-defined providers. This is accomplished via the createProvider method. Example code shows this best:

import com.sun.tracing.ProviderFactory;

public class MyApplication {
    public static void main(String argv[]) {
        ProviderFactory factory = ProviderFactory.getDefaultFactory();
        MyProvider provider = factory.createProvider(MyProvider.class);

        doWork()   
    }
    static void doWork() {
        ...
    }
}

Now to add probe points in the application, the developer simply makes adds calls to the method that they defined using the instance returned from createProvider.

import com.sun.tracing.ProviderFactory;

public class MyApplication {
    public static MyProvider provider;
    public static void main(String argv[]) {
        ProviderFactory factory = ProviderFactory.getDefaultFactory();
        provider = factory.createProvider(MyProvider.class);   

        provider->startProbe();
        doWork();
        provider->endProbe();
    }
    static void doWork() {
        ...
        provider->workProbe(i, j);
    }
}

(As can be seen from the example, this may mean that the created provider instance must be stored in a global variable for access from multiple methods. It could be passed as a parameter if desired.)

The example code above would result in the following DTrace probes which could be traced from an attached DTrace script (assuming the process ID is stored in '$target'):

MyProvider$target:::startProbe {
    trace(probename);
}
MyProvider$target:::workProbe { 
    printf("%s: iteration = %d, value = %d\\n", probename, arg0, arg1); 
}
MyProvider$target:::endProbe {
   trace(probename);
}

As you can see, the provider class name becomes the provider name in DTrace, and the method name is the probe name. The module and function names are undefined in the simple case, but can be set using annotations as described below. The probes themselves are very lightweight and have no side effects other than the DTrace visibility.

In the rare case where setting up the arguments is a costly endeavor, one can check the status of a probe manually in order to determine if an attached DTrace script is monitoring it. In USDT, this is the "is-enabled" functionality. In Java, we use a method of the com.sun.tracing.Provider interface to get a reference to the probe, and can then query the probe to get the enabled-state:

    ...
    Method m = MyProvider.class.getMethod("startProbe");
    com.sun.tracing.Probe p = provider.getProbe(m);
    if ( p.isEnabled() ) {
        // do argument setup, etc.
    else {
        // probe is not enabled, skip argument setup
    }

The probe can also be triggered directly from a reference:

    ... 
    Method m2 = MyProvider.class.getMethod("workProbe");
    com.sun.tracing.Probe p = provider.getProbe(m2);
    p.trigger(instance, value);

This is equivalent to calling the instance method directly, though the compiler won't be able to check that you're passing the right parameters, and thus you're more likely to run into an IllegalArgumentException

One can override the provider and probe names that will be used in the tracing mechanism by adding an annotation to the provider specification or the probe specification which contains the requested name:

@ProviderName("OverriddenName") 
public interface MyProvider extends com.sun.tracing.Provider {
    @ProbeName("applicationBegin") void startProbe();
    ...
}

In this case, the resulting DTrace probe would look like this:

OverriddenName$target:::applicationBegin ()

Similarly, the module and function fields of the DTrace probes can be controlled via annotations that live in the com.sun.tracing.dtrace package. The @ModuleName annotation applied to a provider specification will apply to all probes in that provider, and a @FunctionName annotation applied to a method in that provider will specify the function field of the DTrace probe. There are also a number of stability attributes that can be applied to the provider to control the stability aspects of the generated probes.

Here is a full listing of the proposed interfaces:

package com.sun.tracing;
 
public interface Probe {
    boolean isEnabled();
    void trigger(Object ... args);
}
 
public interface Provider {
    Probe getProbe(java.lang.reflect.Method method);
    void dispose();
}
 
public abstract class ProviderFactory {
    public abstract  T createProvider(Class cls);
    public static ProviderFactory getDefaultFactory();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ProbeName {
    String value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProviderName {
    String value();
}
-------------------------------------------------------------------------------
package com.sun.tracing.dtrace;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FunctionName {
    String value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ModuleName {
    String value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Attributes {
  StabilityLevel name();
  StabilityLevel data();
  DependencyClass dependency();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface ProviderAttributes {
    Attributes value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface ModuleAttributes {
    Attributes value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface FunctionAttributes {
    Attributes value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface NameAttributes {
    Attributes value();
}
 
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface ArgsAttributes {
  Attributes value();
}
 
public enum StabilityLevel {
    INTERNAL,
    PRIVATE,
    OBSOLETE,
    EXTERNAL,
    UNSTABLE,
    EVOLVING,
    STABLE,
    STANDARD;
}
 
public enum DependencyClass {
    UNKNOWN,
    CPU,
    PLATFORM,
    GROUP,
    ISA,
    COMMON;
}
Comments:

Keith,

awesome and long awaited! Can you say something about the argument types allowed in probe methods? Also, will I be able to traverse the Java heap in D scripts?

I guess calls to Probe interfaces will be intrinsified by the Hotspot compilers with zero overhead, right?

Matthias

Posted by Matthias on October 31, 2007 at 04:30 AM EDT #

Any argument type is allowed in the probe methods, but only integer-types will make it to DTrace (floats and objects will end up a zero-valued placeholders for now).

We don't have a way of accessing Java fields yet, though this is something we're working on, so traversing the heap may be tricky.

Calls to the probe interfaces should be low cost, and will improve over time, but we're not quite a zero-cost just yet. This is a first draft that can/will be improved. :)

Posted by Keith on October 31, 2007 at 04:46 AM EDT #

Post a Comment:
Comments are closed for this entry.
About

kamg

Search

Top Tags
Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today