JProfiler Help

Injected Probes


Similarly to script probes, injected probes define interception handlers for selected methods. However, injected probes are developed outside the JProfiler GUI in your IDE and rely on the injected probe API that is provided by JProfiler. The API is licensed under the permissive Apache License, version 2.0, making it easy to distribute the associated artifacts.

The best way to get started with injected probes is to study the example in the api/samples/simple-injected-probe directory of your JProfiler installation. Execute ../gradlew in that directory to compile and run it. The gradle build file build.gradle contains further information about the sample. The example in api/samples/advanced-injected-probe shows more features of the probe system, including control objects.

Development environment

The probe API that you need for developing an injected probe is contained in the single artifact with maven coordinates

group: com.jprofiler
artifact: jprofiler-probe-injected
version: <JProfiler version>

where the JProfiler version corresponding to this manual is 14.0.2.

Jar, source and javadoc artifacts are published to the repository at

https://maven.ej-technologies.com/repository

You can either add the probe API to your development class path with a build tool like Gradle or Maven, or use the JAR file

api/jprofiler-probe-injected.jar

in the JProfiler installation.

To browse the Javadoc, go to

api/javadoc/index.html

That javadoc combines the javadoc for all APIs that are published by JProfiler. The overview for the com.jprofiler.api.probe.injected package is a good starting point for exploring the API.

Probe structure

An injected probe is a class annotated with com.jprofiler.api.probe.injected.Probe. The attributes of that annotation expose configuration options for the entire probe. For example, if you create a lot of probe events that are not interesting for individual inspection, the events attribute allows you to disable the probe events view and reduce overhead.

@Probe(name = "Foo", description = "Shows foo server requests", events = "false")
public class FooProbe {
    ...
}

To the probe class, you add specially annotated static methods in order to define interception handlers. The PayloadInterception annotation creates payloads while the SplitInterception annotation splits the call tree. The return value of the handler is used as the payload or the split string, depending on the annotation. Like for script probes, if you return null, the interception has no effect. Timing information is automatically calculated for the intercepted method.

@Probe(name = "FooBar")
public class FooProbe {
    @PayloadInterception(
        invokeOn = InvocationType.ENTER,
        method = @MethodSpec(className = "com.bar.Database",
                             methodName = "processQuery",
                             parameterTypes = {"com.bar.Query"},
                             returnType = "void"))
    public static String fooRequest(@Parameter(0) Query query) {
        return query.getVerbose();
    }

    @SplitInterception(
        method = @MethodSpec(className = "com.foo.Server",
                             methodName = "handleRequest",
                             parameterTypes = {"com.foo.Request"},
                             returnType = "void"))
    public static String barQuery(@Parameter(0) Request request) {
        return request.getPath();
    }
}

As you can see in the above example, both annotations have a method attribute for defining the intercepted methods with a MethodSpec. In contrast to script probes, the MethodSpec can have an empty class name, so all methods with a particular signature are intercepted, regardless of the class name. Alternatively, you can use the subtypes attribute of the MethodSpec to intercept entire class hierarchies.

Unlike for script probes where all parameters are available automatically, the handler methods declare parameters to request values of interest. Each parameter is annotated with an annotation from the com.jprofiler.api.probe.injected.parameter package, so the profiling agent knows which object or primitive value has to be passed to the method. For example, annotating a parameter of the handler method with @Parameter(0) injects the first parameter of the intercepted method.

Method parameters of the intercepted method are available for all interception types. Payload interceptions can access the return value with @ReturnValue or a thrown exception with @ExceptionValue if you tell the profiling agent to intercept the exit rather than the entry of the method. This is done with the invokeOn attribute of the PayloadInterception annotation.

In contrast to script probes, injected probes handlers can be called for recursive invocations of the intercepted method if you set the reentrant attribute of the interception annotation to true. With a parameter of type ProbeContext in your handler method, you can control the probe's behavior for nested invocations by calling ProbeContext.getOuterPayload() or ProbeContext.restartTiming().

Advanced interceptions

Sometimes a single interception is not sufficient to collect all necessary information for building the probe string. For that purpose, your probe can contain an arbitrary number of interception handlers annotated with Interception that do not create payloads or splits. Information can be stored in static fields of your probe class. For thread safety in a multi-threaded environment, you should use ThreadLocal instances for storing reference types and the atomic numeric types from the java.util.concurrent.atomic package for maintaining counters.

Under some circumstances, you need interceptions for both method entry and method exit. A common case is if you maintain state variables like inMethodCall that modify the behavior of another interception. You can set inMethodCall to true in the entry interception, which is the default interception type. Now you define another static method directly below that interception and annotate it with @AdditionalInterception(invokeOn = InvocationType.EXIT). The intercepted method is taken from the interception handler above, so you do not have to specify it again. In the method body, you can set your inMethodCall variable to false.

...

private static final ThreadLocal<Boolean> inMethodCall =
    ThreadLocal.withInitial(() -> Boolean.FALSE);

@Interception(
    invokeOn = InvocationType.ENTER,
    method = @MethodSpec(className = "com.foo.Server",
                         methodName = "internalCall",
                         parameterTypes = {"com.foo.Request"},
                         returnType = "void"))
public static void guardEnter() {
    inMethodCall.set(Boolean.TRUE);
}

@AdditionalInterception(InvocationType.EXIT)
public static void guardExit() {
    inMethodCall.set(Boolean.FALSE);
}

@SplitInterception(
      method = @MethodSpec(className = "com.foo.Server",
                           methodName = "handleRequest",
                           parameterTypes = {"com.foo.Request"},
                           returnType = "void"),
      reentrant = true)
public static String splitRequest(@Parameter(0) Request request) {
    if (!inMethodCall.get()) {
        return request.getPath();
    } else {
        return null;
    }
}

...

You can see a working example of this use case in api/samples/advanced-injected-probe/src/main/java/AdvancedAwtEventProbe.java.

Control objects

The control objects view is not visible unless the controlObjects attribute of the Probe annotation is set to true. For working with control objects, you have to obtain a ProbeContext by declaring a parameter of that type in your handler method. The sample code below shows how to open a control object and associate it with a probe event.

@Probe(name = "Foo", controlObjects = true, customTypes = MyEventTypes.class)
public class FooProbe {
    @Interception(
            invokeOn = InvocationType.EXIT,
            method = @MethodSpec(className = "com.foo.ConnectionPool",
                         methodName = "createConnection",
                         parameterTypes = {},
                         returnType = "com.foo.Connection"))
    public static void openConnection(ProbeContext pc, @ReturnValue Connection c) {
        pc.openControlObject(c, c.getId());
    }

    @PayloadInterception(
            invokeOn = InvocationType.EXIT,
            method = @MethodSpec(className = "com.foo.ConnectionPool",
                         methodName = "createConnection",
                         parameterTypes = {"com.foo.Query", "com.foo.Connection"},
                         returnType = "com.foo.Connection"))
    public static Payload handleQuery(
        ProbeContext pc, @Parameter(0) Query query, @Parameter(1) Connection c) {
        return pc.createPayload(query.getVerbose(), c, MyEventTypes.QUERY);
    }

    ...

}

Control objects have a defined lifetime, and the probe view records their open and close times in the timeline and the control objects view. If possible, you should open and close control objects explicitly by calling ProbeContext.openControlObject() and ProbeContext.closeControlObject(). Otherwise you have to declare a static method annotated with @ControlObjectName that resolves the display names of control objects.

Probe events can be associated with control objects if your handler method returns instances of Payload instead of String. The ProbeContext.createPayload() method takes a control object and a probe event type. The enum with the allowed event types has to be registered with the customTypes attribute of the Probe annotation.

Control objects have to be specified at the start of the time measurement which corresponds to the method entry. In some cases, the name of payload string will only be available at method exit because it depends on the return value or other interceptions. In that case, you can use ProbeContext.createPayloadWithDeferredName() to create a payload object without a name. Define an interception handler annotated with @AdditionalInterception(invokeOn = InvocationType.EXIT) right below and return a String from that method, it will then automatically be used as the payload string.

Overriding the thread state

When measuring execution times for database drivers or native connectors to external resources, it sometimes becomes necessary to tell JProfiler to put some methods into a different thread state. For example, it is useful to have database calls in the "Net I/O" thread state. If the communication mechanism does not use the standard Java I/O facilities, but some native mechanism, this will not automatically be the case.

With a pair of ThreadState.NETIO.enter() and ThreadState.exit() calls, the profiling agent adjusts the thread state accordingly.

...

@Interception(invokeOn = InvocationType.ENTER, method = ...)
public static void enterMethod(ProbeContext probeContext, @ThisValue JComponent component) {
    ThreadState.NETIO.enter();
}

@AdditionalInterception(InvocationType.EXIT)
public static void exitMethod() {
    ThreadState.exit();
}

...

Deployment

There are two ways to deploy injected probes, depending on whether you want to put them on the classpath or not. With the VM parameter

-Djprofiler.probeClassPath=...

a separate probe class path is set up by the profiling agent. The probe classpath can contain directories and class files, separated with ';' on Windows and ':' on other platforms. The profiling agent will scan the probe classpath and find all probe definitions.

If it's easier for you to place the probe classes on the classpath, you can set the VM parameter

-Djprofiler.customProbes=...
to a comma-separated list of fully qualified class names. For each of these class names, the profiling agent will try to load an injected probe.