Embedded Probes


If you control the source code of the software component that is the target of your probe, you should write an embedded probe instead of an injected probe.

Most of the initial effort when writing an injected probe goes into specifying the intercepted methods and selecting the injected objects as method parameters for the handler method. With embedded probes, this is not necessary, because you can call the embedded probe API directly from the monitored methods. Another advantage of embedded probes is that deployment is automatic. Probes ship together with your software and appear in the JProfiler UI when the application is profiled. The only dependency you have to ship is a small JAR file licensed under the Apache 2.0 License that mainly consists of empty method bodies serving as hooks for the profiling agent.

Development environment

The development environment is the same as for injected probes, with the difference that the artifact name is jprofiler-probe-embedded instead of jprofiler-probe-injected and that you ship the JAR file with your application instead of developing the probe in a separate project. The probe API that you need for adding an embedded probe to your software component is contained in the single JAR artifact. In the javadoc, start with the package overview for com.jprofiler.api.probe.embedded when you explore the API.

Just like for injected probes, there are two examples for embedded probes as well. In api/samples/simple-embedded-probe, there is an example that gets you started with writing an embedded probe. Execute ../gradlew in that directory to compile and run it and study the gradle build file build.gradle to understand the execution environment. For more features, including control objects, go to the example in api/samples/advanced-embedded-probe.

Payload probes

Similar to injected probes, you still need a probe class for configuration purposes. The probe class must extend com.jprofiler.api.probe.embedded.PayloadProbe or com.jprofiler.api.probe.embedded.SplitProbe, depending on whether your probe collects payloads or splits the call tree. With the injected probe API, you use different annotations on the handler methods for payload collection and splitting. The embedded probe API, on the other hand, has no handler methods and needs to shift this configuration to the probe class itself.

public class FooPayloadProbe extends PayloadProbe {
    @Override
    public String getName() {
        return "Foo queries";
    }

    @Override
    public String getDescription() {
        return "Records foo queries";
    }
}

Whereas injected probes use annotations for configuration, you configure embedded probes by overriding methods from the base class of the probe. For a payload probe, the only abstract method is getName(), all other methods have a default implementation that you can override if required. For example, if you want to disable the events view to reduce overhead, you can override isEvents() to return false.

For collecting payloads and measuring their associated timing you use a pair of Payload.enter() and Payload.exit() calls

public void measuredCall(String query) {
    Payload.enter(FooPayloadProbe.class);
    try {
        performWork();
    } finally {
        Payload.exit(query);
    }
}

The Payload.enter() call receives the probe class as an argument, so the profiling agent knows which probe is the target of the call, the Payload.exit() call is automatically associated with the same probe and receives the payload string as an argument. If you miss an exit call, the call tree would be broken, so this should always be done in a finally clause of a try block.

If the measured code block does not produce any value, you can call the Payload.execute method instead which takes the payload string and a Runnable. With Java 8+, lambdas or method references make this method invocation very concise.

public void measuredCall(String query) {
  Payload.execute(FooPayloadProbe.class, query, this::performWork);
}

The payload string must be known in advance in that case. There is also a version of execute that takes a Callable.

public QueryResult measuredCall(String query) throws Exception {
    return Payload.execute(PayloadProbe.class, query, () -> query.execute());
}

The problem with the signatures that take a Callable is that Callable.call() throws a checked Exception and so you have to either catch it or declare it on the containing method.

Control objects

Payload probes can open and close control objects by calling the appropriate methods in the Payload class. They are associated with probe events by passing them to a version of the Payload.enter() or Payload.execute() methods that take a control object and a custom event type.

public void measuredCall(String query, Connection connection) {
    Payload.enter(FooPayloadProbe.class, connection, MyEventTypes.QUERY);
    try {
        performWork();
    } finally {
        Payload.exit(query);
    }
}

The control object view must be explicitly enabled in the probe configuration and custom event types must be registered in the probe class as well.

public class FooPayloadProbe extends PayloadProbe {
    @Override
    public String getName() {
        return "Foo queries";
    }

    @Override
    public String getDescription() {
        return "Records foo queries";
    }

    @Override
    public boolean isControlObjects() {
        return true;
    }

    @Override
    public Class<? extends Enum> getCustomTypes() {
        return Connection.class;
    }
}

If you do not explicitly open and close your control objects, the probe class must override getControlObjectName in order to resolve display names for all control objects.

Split probes

The split probe base class has no abstract methods, because it can be used to just split the call tree without adding a probe view. In that case, the minimal probe definition is just

public class FooSplitProbe extends SplitProbe {}

One important configuration for split probes is whether they should be reentrant. By default, only the top-level call is split. If you would like to split recursive calls as well, override isReentrant() to return true. Split probes can also create a probe view and publish the split strings as payloads if you override isPayloads() to return true in the probe class.

To perform a split, make a pair of calls to Split.enter() and Split.exit().

public void splitMethod(String parameter) {
    Split.enter(FooSplitProbe.class, parameter);
    try {
        performWork(parameter);
    } finally {
        Split.exit();
    }
}

Contrary to to payload collection, the split string has to be passed to the Split.enter() method together with the probe class. Again, it is important that Split.exit() is called reliably, so it should be in a finally clause of a try block. Split also offers execute() methods with Runnable and Callable arguments that perform the split with a single call.

Telemetries

It is particularly convenient to publish telemetries for embedded probes, because being in the same classpath you can directly access all static methods in your application. Just like for injected probes, annotate static public methods in your probe configuration class with @Telemetry and return a numeric value. See the chapter on probe concepts for more information. The @Telemetry annotations of the embedded and the injected probe APIs are equivalent, they are just in different packages.

Another parallel functionality between embedded and injected probe API is the ability to modify the thread state with the ThreadState class. Again, the class is present in both APIs with different packages.

Deployment

There are no special steps necessary to enable embedded probes when profiling with the JProfiler UI. However, the probe will only be registered when the first call into Payload or Split is made. Only at that point will the associated probe view be created in JProfiler. If you prefer the probe view to be visible from the beginning, as is the case for built-in and injected probes, you can call

PayloadProbe.register(FooPayloadProbe.class);

for payload probes and

SplitProbe.register(FooSplitProbe.class);

for split probes.

You may be considering whether to call the methods of Payload and Split conditionally, maybe controlled by a command line switch in order to minimize overhead. However, this is generally not necessary, because the method bodies are empty. Without the profiling agent attached, no overhead is incurred apart from the construction of the payload string. Considering that probe events should not be generated on a microscopic scale, they will be created relatively rarely, so that building the payload string should be a comparatively insignificant effort.

Another concern for containers may be that you do not want to expose external dependencies on the class path. A user of your container could also use the embedded probe API which would lead to a conflict. In that case you can shade the embedded probe API into your own package. JProfiler will still recognize the shaded package and instrument the API classes correctly. If build-time shading is not practical, you can extract the source archive and make the classes part of your project.