Byte Code Instrumentation
Byte code instrumentation requires the Java class file to be unpacked and additional timing code to be inserted. As a number of J2EE objects are used in a multi-threaded environment the timing code must be thread safe. The easiest way of insuring this is to create a Timing Object at the start of the point to be timed, this would record the current time. At the end of the timed code block an 'end' method will be called which will record the elapsed time.
The code is effectively:-
Timer t = new Timer(classname, methodname);
// block to be timed
...
t.end();
As a performance improvement it is possible to replace the classname, methodname parameters with an integer representing the method to be timed. This integer will correspond to the classname, methodname stored at the time the code was instrumented.
All instrumentation in x.Link will be at the method level. Code blocks inside methods will not be instrumented.
There are two alternatives to instrumenting the byte code.
Inserting Timer Routines in the Method
Add timer routines to the start and returns of each method to be instrumented. However there may be many returns in a single method which will bulk up the method size. In addition the Exception table pointers into the code will need to be adjusted with each bytecode insertion along with the Operand stack and Local count.
/*
* This method adds instrumentation code at the start and end of the method. We need to parse the
* method signature to determine the size of the paramters on the local variable stack. We add a
* reference to a JVMPI object.. e.g.
*
* parameter double 2 words
* parameter String 1 word <-- add new instrument object reference here
* local integer 1 word
* local object 1 word
*
* We need to add instructions to reference the instrument object at the correct place on the stack
* and we need to adjust references to all local objects!
> lang=NL>*/
lang=NL>void Code::instrument(u2 instrument, u2 methodname, u2 start_method, u2 end_method) {
lang="NL"> // need to parse signature to calculate size of parameters
> lang=NL>vector<u1> byte_codes;
lang="NL"> byte_codes.push_back(NEW);
byte_codes.push_back((u1) instrument >> 8);
byte_codes.push_back((u1) instrument);
byte_codes.push_back(DUP);
if (methodname < 256) {
byte_codes.push_back(LDC);
byte_codes.push_back(methodname);
} else {
// LDC_W
}
byte_codes.push_back(INVOKESPECIAL);
byte_codes.push_back((u1) start_method >> 8);
byte_codes.push_back((u1) start_method);
byte_codes.push_back(ASTORE);
u2 timer = max_locals;
byte_codes.push_back((u1) timer);
max_locals++;
>
>
attribute_length += byte_codes.size();
for (int i = 0; i < exception_table.size(); i++) {
exception_table[i]->adjust(byte_codes.size());
}// for
>
vector<u1>::iterator code_iterator = code.begin();
cout << "do insert" << endl;
code.insert(code_iterator, byte_codes.begin(), byte_codes.end());
cout << " code inserted " << endl;
byte_codes.clear();
// now push exit code
byte_codes.push_back(ALOAD);
byte_codes.push_back((u1) timer);
byte_codes.push_back(INVOKEVIRTUAL);
byte_codes.push_back((u1) end_method >> 8);
byte_codes.push_back((u1) end_method);
attribute_length += byte_codes.size();
code_iterator = code.end() - 1;
cout << "do insert" << endl;
code.insert(code_iterator, byte_codes.begin(), byte_codes.end());
if (max_stack < 3) {
// increase stack size to accomodate instructions, code is stack neutral
max_stack = 3;
}
cout << " code inserted " << endl;
Add Wrapper Method
The classfile::instrument routing will instrument all methods in a class. The routing loops over the methods in the class, it checks with the front end to see if a method should be instrumented or not. It then clones the method with the following code:
Method *m = new Method();
*m = *methods[i];
We now want to fool any calling program to call our wrapper method in place of the method to be timed. We do this by performing a switcharoo on the method names. The original method has its name changed by adding an underscore, we first create a Utf8 String in memory representing the new method name, then we set the method name reference of the original class to point to this string:
u2 method = addUtf8("_" + methods[i]->getName(constant_pool));
methods[i]->setNameIndex(method);
We add a ClassConstant to the constant pool, this is our timer method (note: we should make the timer class user defined), we add Method References to our init (constructor) and end methods to the constant pool. We create a String reference to the fully qualified method name of the method being timed which we changed above. Finally we add this new method to the constant pool.
u2 instrument = addClass("uk/co/kimble/xlink/RealTimer");
u2 start = addMethod("<init>", "(Ljava/lang/String;)V", instrument);
u2 end = addMethod("end", "()V", instrument);
u2 method_name = addString(thisClassName() + "." + methods[i]->getName(constant_pool));
u2 service = addMethod(method, m->getDescriptorIndex(), this_class);
We are now ready to create our service wrapper, this is the method that will get called in place of the method to be instrumented
m->addServiceWrapper(constant_pool, instrument, method_name, service, start, end, m->getDescriptor(constant_pool));
The service wrapper method is much simpler to implement compared to adding timer code directly into a method as discussed above. Trust me, I’ve tried both. Here we give a java code fragment for a standard jspService method:
protected void _jspService(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
RealTimer rt = new RealTimer("_jspService");
__jspService(req, res);
rt.end();
}
The target method must be renamed and called from the wrapper method. This is done by adding an additional underscore '_' character, as discussed previously. A new method string is added to the constant pool and its index is substituted for the current method index.
As the wrapper method duplicates the method to be wrapped in all but code it is simply 'cloned', including the original method name index. The code segment is then modified. The new code segment has an operand stack 3 deep. The number of local variables includes the variable passed to the constructor and the object reference and the parameters passed as part of the method call and any return value (note that long and double values occupy two positions).
For the signature below
protected (Ljavax/servlet/HttpServletRequest;Ljavax/servlet/HttpServletResponse;)V _jspService
This will be two, giving a local stack frame depth of 4.
The following byte code will be generated:
0: new uk/co/kimble/xlink/RealTimer
3: dup // duplicate the
top of the operand stack (the one with the reference)
4: ldc 0x4a // push item to operand stack from
constant pool (the string)
6: invokespecial uk/co/kimble/xlink/RealTimer
<init> (Ljava/lang/String;)V
9: astore_3 // store reference in offset 3 of
local variable frame
10: aload_0 // object reference
11: aload_1 // HttpServletRequest
12: aload_2 // HttpServletResponse
13: invokevirtual uk/co/kimble/xlink/test/TestServlet
Y_jspService (Luk/co/kimble/xlink/test/HttpServletRequest;Luk/co/kimble/xlink/test/HttpServletResponse;)V
16: aload_3
17: invokevirtual uk/co/kimble/xlink/RealTimer end ()V
20: return
The aload_<n> instructions are special 1 byte operations for frequently accessed variables on the stack frame. If there are more than 4 local variables the more generic aload <offset> instructions will be used, and if the local stack frame is greater than 256 the longer two byte offset instructions will be used. The generated code must take this into account and generate the appropriate instructions.
Algorithm to Calculate the Number of Locals
The number of locals can be calculated from the signature. The reference to the class object is always at position zero on the local stack and can be referenced with the instruction aload_0. The signature is a list of java types:
|
Signature |
Type |
Instructions |
|
B |
byte |
iload, ireturn |
|
C |
char |
iload, ireturn |
|
D |
double |
dload, dreturn |
|
F |
float |
fload, freturn |
|
I |
int |
iload, ireturn |
|
J |
long |
lload, lreturn |
|
L<classname>; |
Class instance |
aload, areturn |
|
[...<type> |
Array dimension |
aload, areturn |
|
V |
void |
return |
There are special instructions for the first 4 positions on the stack frame. These are of the type aload_0... aload_3.
The method with this signature
public (D[ILcom/machin/Bidule;)I _methodY
has the following call stack:
0
reference to class object
1
double parameter (occupied two positions)
3
integer array reference
4
Bidule object reference
5
RealTimer object
The algorithm to calculate the call stack is to search for an opening parentheses '(', match the characters shown in the table above, double and long occupying two slots. The return type should also be determined.
generating this wrapper code
0: new
3: dup
4: ldc 0x4a
6: invokespecial uk/co/kimble/xlink/RealTimer
<init> (Ljava/lang/String;)V
9: astore 0x5
11: aload_0 // object reference
12: dload_1 // Double
13: aload_3 // Array reference
14: aload 0x4
// Bidule object reference
16: invokevirtual wrapper method
19: aload 0x5
20 invokevirtual uk/co/kimble/xlink/RealTimer end
()V
23: ireturn
Calculating the Operand Stack Size
A number of the generated instructions have an affect on the operand stack. New and Dup will increase teh stack size by 1, the aloads will increase the stack size by 1 or 2 depending on the type. Some of the invoked methods will increase the stack size by the size of the return type, but they also consume variables on the stack. Taking the wrapper above:-
0: new
stack+1
3: dup
stack+1
4: ldc
stack+1
6: invokespecial
stack-2
9: astore 0x5
stack-1
11: aload_0
stack+1
12: dload_1
stack+2
13: aload_3
stack+1
14: aload 0x4
stack+1
16: invokevirtual
stack-5
19: aload 0x5
stack+1
20 invokevirtual
stack-1
23: ireturn
Calculating Code Attribute Length
Method Constant Structure
The Classfile::addMethod() function creates the following tree structure in the constant pool, representing a method that can be called with an invoke operand.
©1994-2005All text and images copyright: www.abcseo.com; last updated: Wed Apr 5 10:53:34 2006
