ByCounter Documentation

See:
          Description

Packages
de.uka.ipd.sdq.ByCounter.example Provides example usages of ByCounter.
de.uka.ipd.sdq.ByCounter.example.fibonacci Provides an example implementation of the Fibonacci algorithm as well as instrumentation code for it.
de.uka.ipd.sdq.ByCounter.execution Provides the main BytecodeCounter class and classes for the execution of instrumented classes.
de.uka.ipd.sdq.ByCounter.instrumentation Provides counting instrumentation for ByCounter.
de.uka.ipd.sdq.ByCounter.parsing Provides analysis of the class structure of instrumented applications.
de.uka.ipd.sdq.ByCounter.reporting Provides classes for reporting ByCounter results.
de.uka.ipd.sdq.ByCounter.utils Provides utility methods for ByCounter used in different packages.
de.uka.ipd.sdq.ByCounter.utils.wide Experimental code dealing with wide vs non-wide bytecode instructions.

 

ByCounter Introduction

This document provides an overview over the structure of ByCounter, what it is and how to use it. You can find the source code for the examples used here in the de.uka.ipd.sqd.ByCounter.example package. Please refer to the example classes in that package for source code reference as the extracts presented here may not be up to date.

Overview

ByCounter is a tool for collecting information about the execution of Java bytecode instructions in Java classes. It works by first instrumenting Java bytecode with counting instructions and then executing the instrumented code to gather the results of these instructions.

Using ByCounter involves at least two separate steps that are important for users to consider. First of all, the user needs to decide what specific code he wants to get counting information for and then tell ByCounter to instrument that code by calling ByCounters instrument method with the desired options. This step is explained in the instrumentation section of this document.

The second step is to execute the instrumented code so that ByCounter can collect runtime information about the code. ByCounter can execute instrumented code using reflection methods. This is, however, not appropriate in many cases as you may want to execute the code in it's normal program context. More information on that is supplied in the execution section of this document.

Depending on how you choose to instrument your code, your results might have been written to a log file that you can now evaluate. Or you may want to make use of ByCounters result aggregation facilities. More details on that is provided in the result retrieval section.

There are ways to control the instrumentation done by ByCounter in more detail. Information on that is discussed in the options section of this document.

Example1

Whenever you want to use ByCounter, you need to construct an instance of BytecodeCounter. Here is what example1 of the example class does:
   
        /**
         * Quickstart example of the most basic ByCounter usage.
         */
        public static void example1() {         
                //1. Set up a BytecodeCounter instance to use ByCounter, using a parameterless constructor.
                BytecodeCounter counter = new BytecodeCounter();
                [..]

Instrumentation

For ByCounter to instrument your code, you need to specify that code. This means describing one or more method(s) using the class MethodDescriptor. In short, a MethodDescriptor contains information about the package and class a specific method exists in, as well as information about its exact signature so that it can be distinguished from all other methods.

The example contains a simple method with the name dummyMethod.
        /**
         * This dummy method is here to get instrumented in the example.
         * @param str Some String that gets printed.
         * @param f Some float that contributes to the result.
         * @return An int value that is computed based on input (cf. source code).
         */
        public static int dummyMethod(String str, float f) {
                System.out.println(str);
                if(f > 0) {
                        return -1;
                }
                int result = (int)(2*f);
                return result;
        }
To specify a MethodDescriptor for this method, we supply MethodDescriptor's constructor with the fully qualified class name (i.e. containing the package name) and the method signature as in your Java code. Here is the continuation of example1:
                [..]
                //2. Specify the method to be instrumented (several methods are supported as well)
                String className = "de.uka.ipd.sdq.ByCounter.example.ByCounterExample";
                MethodDescriptor myMethod = new MethodDescriptor(
                                className,
                                "public static int dummyMethodToBeInstrumented(java.lang.String str, float f)");
                
                //3. now tell ByCounter to instrument the specified method
                counter.instrument(myMethod);
                [..]

Notice however a subtle, yet very important difference here. For the type String we did not simply write String, but java.lang.String. That is the fully qualified name for the String class. For MethodDescriptor signatures, you always need to use these fully qualified type names. Whenever ByCounter fails, this is a good place to start looking for an error.

To make ByCounter instrument the now specified method(s), call instrument(..). To perform instrumentation on more than one method, you can also supply a List of MethodDescriptors (List<MethodDescriptor>) instead.

Execution

Now that we have used ByCounter to instrument your methods, the code that uses these methods needs to be invoked so that the instrumented code is executed.

If the class you want to execute specifies a default constructor, i.e. a constructor without parameters, the execution setup is completed here. Likewise, static methods can be executed without any setup. If you do not want to run the executed method by itself, as we do for this example, construction is done in your normal code. However, if that is not the case, ByCounter also needs to know how to create an instance of the class that contains the method you want to execute.

In that case, the constructor to use is once again specified using a MethodDescriptor. For constructors, you need to use the static method MethodDescriptor.forConstructor(..) instead of new MethodDescriptor(..). Then, this construction information is supplied to the ByCounter instance using the setConstructionParameters(..) method that uses the method descriptor and an array of objects to use as the parameters of the constructor.

                //4. If the class which contains the method that we want to execute 
                // has no default constructor and the method is non-static, we need to 
                // provide construction parameters.
                // If a default constructor is available or the method you want to 
                // execute is static, you can skip this step.
                MethodDescriptor constructor = MethodDescriptor.forConstructor(
                                ByCounterExample.class.getCanonicalName(), 
                                "public ByCounterExample(int number)");
                counter.setConstructionParameters(constructor, new Object[]{8});
        
Now everything is set up to actually run the instrumented code, so that ByCounter can acquire the counting information. To run the instrumented method, call execute(..).
                [..]
                //5. let ByCounter execute the method (note that here, this class is reloaded internally)
                counter.execute(myMethod, new Object[] {"Hello world!", 0.0f});
                [..]
        

As before for instrument(), you need to specify the method to execute using the MethodDescriptor class. In this simple example, we reuse the MethodDescriptor myMethod, as all we want is to execute and count that method in no specific context. But this is not a limitation. You could specify any method in any class, i.e. the main method of your application, to start the execution. In that case, pay attention to the classpath.

In addition to the MethodDescriptor, we also need to supply the methods arguments since dummyMethod takes a String and a float. We do this in the form of a simple Object[].

Result retrieval

Results can either be retrieved through the class
CountingResultCollector or through log files optionally created by ByCounter. The usage of CountingResultCollector is demonstrated below.
                [..]
                //6. now that ByCounter has completed counting, we can fetch the results,
                //i.e. get the result list from CountingResultCollector
                //"nonRecursively" means that even if the instrumented methods called 
                //other instrumented methods, the CountingResults of the callees are 
                //not inlined into those of callers
                List results = 
                        CountingResultCollector.getInstance().getAllCountingResults_nonRecursively();
                
                //7. output the results to the console/log
                for(CountingResult r : results) {
                        CountingResultCollector.getInstance().logResult(r, false, true);
                }
                
                //8. clear the results as we do not need them anymore
                CountingResultCollector.getInstance().clearResults();
        

To access CountingResultCollector (which employs the singleton pattern) you have to use the static getInstance() method. From the instance you can retrieve the results by calling one of the getResults() method. Here, we use CountingResultCollector again to dump the results to the console or log (as specified in the log4j config file). You can, however, get more detailed information from the results. Please refer to the javadoc for CountingResult.

If you no longer need the result collector to hold the results, call clearResults().

Example2

ByCounter options

There are more options to ByCounter than shown above. Some of these options are presented here. The code to this example is from the example2() method. For reference, here is what it looks like:
        /**
         * Example of ByCounter usage with options.
         * Watch the console output to see where the result log file is written.
         */
        public static void example2() {
                //1. Set up a BytecodeCounter instance to use ByCounter. 
                BytecodeCounter counter = new BytecodeCounter();
                
                //2. In this example, we do not want to use the CountingResultCollector, so
                // we tell ByCounter to write to a log file instead.
                counter.getInstrumentationParams().setUseResultCollector(false);
                counter.getInstrumentationParams().setResultLogFileName(
                                "myResults" + File.separatorChar + "ByCounter_result_");
                
                //3. we want to keep the class files that are generated by ByCounter 
                // (to see what the instrumented file looks like), so we write the classes to disk
                counter.getInstrumentationParams().setWriteClassesToDisk(true);

                //4 If the class which contains the method that we want to execute 
                // has no default constructor, we need to provide construction parameters.
                // If a default constructor is available or the method you want to 
                // execute is static, you can skip this step.
                MethodDescriptor constructor = MethodDescriptor.forConstructor(
                                ByCounterExample.class.getCanonicalName(), 
                                "public ByCounterExample(int number)");
                counter.setConstructionParameters(constructor, new Object[]{8});
                
                //5. we want to know more about the usage of arrays in our code
                counter.getInstrumentationParams().setUseArrayParameterRecording(true);

                //6. as in the first example, we specify the method to instrument
                MethodDescriptor myMethod = new MethodDescriptor(
                                "de.uka.ipd.sdq.ByCounter.example.ByCounterExample",
                                "public static int dummyMethodToBeInstrumented(java.lang.String str, float f)");
                
                //7. ... we tell ByCounter to instrument the specified method
                counter.instrument(myMethod);
                
                //8. ... make ByCounter execute the method (note that this class must be reloaded! TODO check)
                counter.execute(myMethod, new Object[] {"Hello world!", 0.0f});
                
                //since we wrote the results to a log file, we are done with this example
        }
        

The basic usage of ByCounter in this example is similiar to that seen in Example1. Notice the block in which we set the instrumentation parameters. To access these, we call getInstrumentationParams() on the BytecodeCounter instance.

First we tell ByCounter not to use the CountingResultCollector mechanism. This means that a log file is written for each method instead. In the next line, we specify the file names for these log files. The specified string is a prefix to the generated name. The javadoc explains this in more detail.

Next we instruct ByCounter to write the instrumented .class files to disk. You will find these in the 'bin_instrumented' directory.

Finally we tell ByCounter to record array parameters such as the dimension and type of arrays. These are then found in the CountingResults.

Other than the here mentioned options, more are available. Please refer to the javadoc.

Note that ByCounter allows you to specify the exact class file that you want to instrument as a byte array. Use the setClassToInstrument(classToInstrument) method of the BytecodeCounter for that.