Quick Start
Guide
to the Java DTrace API
Contents
"hello, world" Example
To demonstrate how to use the Java DTrace API, let's write a simple Java
program that runs a D script, in this case hello.d (prints
"hello, world" and exits). You will need root permission to use the
Java DTrace API (just as you do to use the dtrace(1M) command).
You may want to eliminate this inconvenience by adding the following
line to /etc/user_attr:
user-name::::defaultpriv=basic,dtrace_kernel,dtrace_proc
(Substitute your user name.) See the
Security chapter of the Solaris Dynamic Tracing Guide
for more information.
Writing a Simple Consumer
Creating a DTrace consumer
is easy:
Consumer consumer = new LocalConsumer();
Before you can do anything with the consumer, you must first open it.
Then you simply compile and enable one or more D programs and run it:
consumer.open();
consumer.compile(program);
consumer.enable();
consumer.go(); // runs in a background thread
To get the data generated by DTrace, you also need to add a listener:
consumer.addConsumerListener(new ConsumerAdapter() {
public void dataReceived(DataEvent e) {
System.out.println(e.getProbeData());
}
});
Here is a simple example that runs a given D script:
Java program (TestAPI.java)
import org.opensolaris.os.dtrace.*;
import java.io.File;
public class TestAPI {
public static void
main(String[] args)
{
if (args.length < 1) {
System.err.println("Usage: java TestAPI <script> [ macroargs... ]");
System.exit(2);
}
File file = new File(args[0]);
String[] macroArgs = new String[args.length - 1];
System.arraycopy(args, 1, macroArgs, 0, (args.length - 1));
Consumer consumer = new LocalConsumer();
consumer.addConsumerListener(new ConsumerAdapter() {
public void dataReceived(DataEvent e) {
System.out.println(e.getProbeData());
}
});
try {
consumer.open();
consumer.compile(file, macroArgs);
consumer.enable();
consumer.go();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
Compile the test program as follows:
javac -cp /usr/share/lib/java/dtrace.jar TestAPI.java
Running the hello.d Script
Now we need a D script for the program to run. The following is a
simple example that prints "hello, world" and exits:
D script (hello.d)
dtrace:::BEGIN
{
trace("hello, world");
exit(0);
}
Run as follows:
java -cp .:/usr/share/lib/java/dtrace.jar TestAPI hello.d
The output should look like this:
org.opensolaris.os.dtrace.ProbeData[epid = 1, cpu = 1,
enabledProbeDescription = dtrace:::BEGIN, flow = null, records =
["hello, world", 0]]
There is one record in the ProbeData
for each action in the D script. The first record is generated by the
trace() action. The second is generated by the exit()
action. For prettier output, you could change the ConsumerAdapter dataReceived()
implementation as follows:
consumer.addConsumerListener(new ConsumerAdapter() {
public void dataReceived(DataEvent e) {
// System.out.println(e.getProbeData());
ProbeData data = e.getProbeData();
java.util.List < Record > records = data.getRecords();
for (Record r : records) {
if (r instanceof ExitRecord) {
} else {
System.out.println(r);
}
}
}
});
Aggregations
The example Java program can just as easily run a more complex script,
such as an aggregation:
D script (syscall.d)
syscall:::entry
/ execname == $$1 /
{
@[probefunc] = count();
}
profile:::tick-1sec
{
printa(@);
clear(@);
}
The above script uses the $$1 macro variable as a placeholder
for whatever executable you'd like to trace. See the
Macro Arguments section of the Scripting chapter of the
Solaris Dynamic Tracing Guide. Using two dollar signs ($$1)
instead of one ($1) forces expansion of the macro variable to
type string.
To run the example Java program using the above D script, you need to
specify an argument to the execname placeholder, such as
"java":
java -cp .:/usr/share/lib/java/dtrace.jar TestAPI syscall.d java
A data record generated by the printa() action is printed to
the console once every second. It contains counts of system calls by
function name made by java. No record is generated by the
clear() action.
If you omit the argument to the execname placeholder, the
program fails to compile and the API throws the following exception:
org.opensolaris.os.dtrace.DTraceException: failed to compile script
syscall.d: line 2: macro argument $$1 is not defined
at org.opensolaris.os.dtrace.LocalConsumer._compileFile(Native Method)
at org.opensolaris.os.dtrace.LocalConsumer.compile(LocalConsumer.java:342)
at TestAPI.main(TestAPI.java:26)
A DTrace script may have more than one aggregation. In that case, each
aggregation needs a distinct name:
D script (intrstat.d)
sdt:::interrupt-start
{
self->ts = vtimestamp;
}
sdt:::interrupt-complete
/ self->ts && arg0 /
{
this->devi = (struct dev_info *)arg0;
@counts[stringof(`devnamesp[this->devi->devi_major].dn_name),
this->devi->devi_instance, cpu] = count();
@times[stringof(`devnamesp[this->devi->devi_major].dn_name),
this->devi->devi_instance, cpu] = sum(vtimestamp - self->ts);
self->ts = 0;
}
The @counts and @times aggregations both accumulate
values for each unique combination of device name, device instance, and
CPU (a three-element tuple). In this example we drop the tick
probe to demonstrate a more convenient way to get aggregation data
without the use of the printa() action. The
getAggregate() method allows us to get a read-consistent
snapshot of all aggregations at once on a programmatic interval.
Java program (TestAPI2.java)
...
try {
consumer.open();
consumer.compile(file, macroArgs);
consumer.enable();
consumer.go();
Aggregate a;
do {
Thread.sleep(1000); // 1 second
a = consumer.getAggregate();
if (!a.asMap().isEmpty()) {
System.out.println(a);
}
} while (consumer.isRunning());
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
...
Compile and run:
javac -cp /usr/share/lib/java/dtrace.jar TestAPI2.java
java -cp .:/usr/share/lib/java/dtrace.jar TestAPI2 intrstat.d
Try removing the tick probe from the syscall.d example
and running it again with the above modification (TestAPI2).
By default, the requested aggregate includes every aggregation and
accumulates running totals. To display values per time interval
(instead of running totals), clear the aggregations each time you call
getAggregate(). Clearing an aggregation resets all counts to
zero without removing any elements. The following modification to the
example above clears all aggregations:
// a = consumer.getAggregate();
a = consumer.getAggregate(null, null); // included, cleared
Each Set of aggregation names, included and
cleared, specifies all aggregations if null and
no aggregations if empty. Any subset is possible. However, if an
aggregation has ever been used in the printa() action, it is no
longer available to the getAggregate() method.
Be aware that you cannot call getAggregate() on an interval
faster that the aggrate setting. See the
Options and Tunables chapter of the Solaris Dynamic
Tracing Guide. See also the
Minimizing Drops section of the Aggregations chapter
for specific information about the aggrate option. The default
aggrate is once per second. Here's an example of how you might
double the aggrate to minimize drops:
consumer.setOption(Option.aggrate, Option.millis(500)); // every half second
Even a single drop terminates the consumer unless you override the
dataDropped() method of ConsumerAdapter to handle
drops differently. To avoid drops, it is probably better to increase
the aggsize option, since increasing the aggrate makes
the consumer work harder. In most cases, the aggrate should
only be increased when you need to update a display of aggregation data
more frequently than once per second. Many runtime options, including
aggrate, can be changed dynamically while the consumer is
running.
It's also worth mentioning that a D aggregation may omit square
brackets and aggregate only a single value:
@total = count();
The resulting singleton
Aggregation has one record that may be obtained as follows:
Aggregate a = consumer.getAggregate();
Aggregation total = a.getAggregation("total");
AggregationRecord totalRecord = total.getRecord(Tuple.EMPTY);
Target Process ID
In addition to supporting macro arguments (see the syscall.d
aggregation example above), the Java DTrace API also supports the
$target macro variable. (See the
Target Process ID section of the Scripting chapter of
the Solaris Dynamic Tracing Guide.) This allows you to trace a
process from the very beginning of its execution, rather than sometime
after you manually obtain its process ID. The API does this by creating
a process that is initially suspended and allowed to start only after
go() has initiated tracing. For example, you can aggregate
all the system calls from start to finish made by the date
command:
D script (target.d)
syscall:::entry
/ pid == $target /
{
@[probefunc] = count();
}
A modified version of the TestAPI.java program adds the
createProcess() call to execute the given command but
prevent it from starting until the consumer is running:
Java program (TestTarget.java)
...
consumer.open();
consumer.createProcess(command);
consumer.compile(file);
consumer.enable();
consumer.go();
...
It also overrides the
processStateChanged() method of the
ConsumerAdapter to print a notification when the process has
ended:
...
consumer.addConsumerListener(new ConsumerAdapter() {
public void dataReceived(DataEvent e) {
System.out.println(e.getProbeData());
}
public void consumerStopped(ConsumerEvent e) {
try {
Aggregate a = consumer.getAggregate();
for (Aggregation agg : a.asMap().values()) {
for (AggregationRecord rec : agg.asMap().values()) {
System.out.println(rec.getTuple() + " " +
rec.getValue());
}
}
} catch (Exception x) {
x.printStackTrace();
System.exit(1);
}
consumer.close();
}
public void processStateChanged(ProcessEvent e) {
System.out.println(e.getProcessState());
}
});
...
Compile and run:
javac -cp /usr/share/lib/java/dtrace.jar TestTarget.java
java -cp .:/usr/share/lib/java/dtrace.jar TestTarget target.d date
The consumer exits automatically when the target date process
completes.
Closing Consumers
An application using the Java DTrace API may run multiple consumers
simultaneously. When a consumer stops running, the programmer is
responsible for closing it in order to release the system resources it
holds. A consumer may stop running for any of the following reasons:
- It was stopped explicitly by a call to its
stop() method
- It encountered the exit() action
- Its $target process or processes (if any) all completed
- It encountered an exception
By default, an exception prints a stack trace to stderr before
notifying listeners that the consumer has stopped. You can define
different behavior by setting an
ExceptionHandler, but the consumer is still stopped.
The same listener that receives probe data generated by DTrace is also
notified when the consumer stops. This is a good place to close the
consumer:
consumer.addConsumerListener(new ConsumerAdapter() {
public void dataReceived(DataEvent e) {
System.out.println(e.getProbeData());
}
public void consumerStopped(ConsumerEvent e) {
Consumer consumer = (Consumer)e.getSource();
consumer.close();
}
}
});
This releases the resources held by the consumer in all cases, i.e.
after it exits for any of the reasons listed above.
You can request the last aggregate snapshot made by a stopped consumer,
as long as it has not yet been closed:
Aggregate a = consumer.getAggregate();
Note however that any aggregation that has already appeared in a
PrintaRecord as a result of the printa() action
action will not be included in the requested aggregate.
Learning More
The
OpenSolaris DTrace page has links to resources to help you learn
DTrace. In particular, you should read the Solaris Dynamic Tracing
Guide.
Try the example Java programs on this page with other D scripts. You
need not remove #!/usr/sbin/dtrace -s from the top of an
executable script. You may want to remove profile:::tick*
clauses if you plan to use the Consumer
getAggregate() method and control the data interval
programmatically. If the script uses the pre-compiler, you will need to
call the Consumer
setOption() method with the
Option.cpp argument.
To quickly familiarize yourself with the Java DTrace API, take a look at
the overview diagram.
Back to top