JSR 292 Goodness: Fast code coverage tool in less than 10k
Feb 12, 2011
JSR 292 introduces a new bytecode instruction invokedynamic but also several new kind of constant pool constants. Which means that most of the tools that parse bytecodes like ASM, BCEL, findbugs or EMMA will need to be updated to be java 7 compatible. How a code coverage tool works ? A code coverage tool records all paths taken when running the application and checks at the end if all lines of codes was recorded. To record if an instruction was executed or not, code coverage tools add a probe which is a small amount of bytecodes that will call the runtime of the tool to say: "I have visited this instruction". So a code coverge tool is a tool that find basic blocks and add probes at the begining of each one. Using JSR 292 API to implement a code coverage tool. Finding basic block is easy with bytecode that come from 1.6 or 1.7 compiler because the compiler is required to add stack maps All existing code coverage tools have an impact on the performance of the application because the code of the probe is executed each time you call a basic block even if it should be executed once. A no-op, is a method handle that takes nothing and return void. This method handle can be retrieved with Methodhandles.identity(void.class). The code of the prototype is freely available (as attachment of this blog) and works like an agent. Side note: This prototype doesn’t handle exception correctly. If an exception is thrown, it will escape from the basic block without ending it. Running the code with one argument "foo" will print line 2 is the declaration of the class, it’s because javac adds a default constructor which is not used. If you want to play with it don’t forget to compile your sources with the debug flag on. Otherwise, the generated bytecodes will not contain mapping information between opcodes and line numbers. Cheers,
EMMA is a code coverage tool, a tool that helps developers to know if their tests cover all the code of the application. While it’s not the only code coverage tool available in Java, it’s the most popular from my personal experience.
In this blog entry, I would like to show how to write a simple code coverage tool indycov that use JSR 292 API to have a runtime overhead close to zero.
By example, if I run the code below with no argument, it will print "foo" and "bar" and the code coverage tool will say that the else branch that prints "baz" will be not covered.
public static void main(String[] args) {
System.out.println("foo");
if (args.length == 0) {
System.out.println("bar");
} else {
System.out.println("baz");
}
}
In fact, tools, only add probes where necessary, at the begining of each basic block of the control flow. A basic block is a collection of instructions without any jump (return, thow, if, break etc).
By example, the code above has 4 basic blocks: the once printing "foo", the one printing "bar", the one printing "baz" and the one containing the return at the end of the method.
in the bytecode flow. Stack maps are used to verify the bytecode in linear time and are inserted at the join points of the control flow.
So finding basic block in a 1.7 compatible bytecode can be done in one pass thanks to the stack maps info inserted by the compiler.
If you are a regular reader of this blog, you already know how to create a probe that will be executed once. The trick is use use invokedynamic, to record the visit in the bootstrap method and
to use a target method handle that is equivalent to no-op. So subsequent call will not execute any code.
main([Ljava/lang/String;)V
INVOKEDYNAMIC probe ()V [fr/umlv/indycov/RT#bsm, 1]
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "foo"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
ALOAD 0
ARRAYLENGTH
IFNE L0
INVOKEDYNAMIC probe ()V [fr/umlv/indycov/RT#bsm, 2]
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "bar"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GOTO L1
L0
FRAME SAME
INVOKEDYNAMIC probe ()V [fr/umlv/indycov/RT#bsm, 3]
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "baz"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
FRAME SAME
INVOKEDYNAMIC probe ()V [fr/umlv/indycov/RT#bsm, 4]
RETURN
So the bootstrap method is the following. The first line records that the basic block with number ‘index’ is visited.
public static CallSite bsm(Lookup lookup, String name, MethodType type, Object index) {
classValue.get(lookup.lookupClass()).cover((Integer)index);
return new ConstantCallSite(MethodHandles.identity(void.class));
}
It relies on ASM 4 (still in beta) to do the bytecode transformation.
How to modify the prototype to take care of exception is let to interrested readers.
java -XX:+UnlockExperimentalVMOptions -XX:+EnableInvokeDynamic -javaagent:lib/indycov.jar -cp test-classes/ TestCoverage foo
foo
baz
TestCoverage: no coverage for line(s) 2 to 2
TestCoverage: no coverage for line(s) 5 to 6
lines 5 to 6 are the ones that print "bar".
Rémi
| Attachment | Size |
|---|---|
| indycov.zip | 681.84 KB |



