1

Today I got confused by the results of Visual VM profiling I got.

I have the following simple Java method:

public class Encoder {
  ...
  private BitString encode(InputStream in, Map<Character, BitString> table) 
      throws IOException {

    BufferedReader reader = new BufferedReader(new InputStreamReader(in));

    BitString result = new BitString();
    int i;
    while ((i = reader.read()) != -1) {
      char ch = (char) i;
      BitString string = table.get(ch);
      result = result.append(string);
    }

    return result;
  }
}

This method reads characters from a stream, one at a time. For each character it looks up it's bit-string representation and it joins these bit-strings to represent the whole stream.

A BitString is a custom data structure that represents a sequence of bits using an underlying byte array.

The method performs very poorly. The problem lies with BitString#append - that method creates a new byte array, copies the bits from both input BitStrings and returns it as a new BitString instance.

public BitString append(BitString other) {

  BitString result = new BitString(size + other.size);
  int pos = 0;

  for (byte b : this) {
    result.set(pos, b);
    pos++;
  }
  for (byte b : other) {
    result.set(pos, b);
    pos++;
  }

  return result;
}

However, when I tried to use VisualVM to verify what's happening, here's what I got:

profiling

I have very little experience with Visual VM and profiling in general. To my understanding, this looks as if the problem lied somewhere in encode method itself, not in append.

To be sure, I surrounded the whole encode method and the append call with custom time measuring, like this:

public class Encoder {
  private BitString encode(InputStream in, Map<Character, BitString> table)
      throws IOException {

>>  long startTime = System.currentTimeMillis();
>>  long appendDuration = 0;

    BufferedReader reader = new BufferedReader(new InputStreamReader(in));

    BitString result = new BitString();
    int i;
>>  long count = 0;
    while ((i = reader.read()) != -1) {
      char ch = (char) i;
      BitString string = table.get(ch);

>>    long appendStartTime = System.currentTimeMillis();
      result = result.append(string);
>>    long appendEndTime = System.currentTimeMillis();
>>    appendDuration += appendEndTime - appendStartTime;

>>    count++;
>>    if (count % 1000 == 0) {
>>      log.info(">>> CHARACTERS PROCESSED: " + count);
>>      long endTime = System.currentTimeMillis();
>>      log.info(">>> TOTAL ENCODE DURATION: " + (endTime - startTime) + " ms");
>>      log.info(">>> TOTAL APPEND DURATION: " + appendDuration + " ms");
>>    }
    }

    return result;
  }
}

And I got the following results:

CHARACTERS PROCESSED: 102000
TOTAL ENCODE DURATION: 188276 ms
APPEND CALL DURATION: 188179 ms

This seems in contradiction with the results from Visual VM.

What am I missing?

Dušan Rychnovský
  • 9,629
  • 8
  • 38
  • 58
  • how did you measure the time, could you please add that code as well. – hunter Sep 24 '16 at 19:48
  • I added explanation of how time was measured and also the code of `append`. I don't think there's any IO or Network waits there. Maybe GC pauses? – Dušan Rychnovský Sep 24 '16 at 20:04
  • Have you tried [*this method*](http://stackoverflow.com/a/378024/23771)? It works fine with Java and will tell you exactly where the time goes. Note, it *does not measure*. Rather, it exploits the fact that the activity takes a large fraction of time to pinpoint it. You can use it to get very rough measurement, but it finds the problem precisely. – Mike Dunlavey Sep 26 '16 at 18:31

2 Answers2

3

You're seeing this behavior because VisualVM can only sample the call stack at safepoints, and the JVM is optimizing the safepoints out of your code. This causes the samples to be lumped together under "Self time" instead, which makes it artificially inflated and misleading. There are two possible fixes:

  • To make VisualVM work better, add JVM options to keep more safepoints, like -XX:-Inline and -XX:+UseCountedLoopSafepoints. These will slow your code down some, but will make the profiling results much more accurate. This solution is easy, and it's usually good enough. Just remember to remove these options when you're not profiling!
  • If you don't mind switching profilers, you can use Java Mission Control or honest-profiler. These have the ability to take samples at places other than safepoints, using a special API of the JVM. This solution is a little more accurate, since you're profiling the fully optimized code, but in my opinion those profilers are harder to use than VisualVM.

In your specific case, the method call to BitString.append() is probably being inlined by the JVM for performance. This causes the safepoint that's normally at the end of a method call to be removed, which means that method won't show up in the profiler anymore.

There's an excellent blog post here with more details about what safepoints are and how they work, and another one here that talks in more detail about the interaction between safepoints and sampling profilers.

Wade Walker
  • 436
  • 4
  • 7
0

[THIS ANSWER is INVALID. but i keep it until OP get some help from a member,because this post contains two comments which helps others to understand the problem].

in this case VisualVM has measured the actual CPU time, but time value you measured is "elapsed time".

if the execution threads had to wait for IO or Network, then that time will not be measured as CPU time.

hunter
  • 3,008
  • 13
  • 19
  • Please see my extended answer - I included the implementation of `append`. I don't think there's any IO done there. Maybe rather GC pauses? However, as you can see in the screenshot, Visual VM shows two values - `Time` and `Time (CPU)`. Isn't `Time` the `elapsed time` and `Time (CPU)` the `actual CPU time`? – Dušan Rychnovský Sep 24 '16 at 20:07
  • yes, you are right, something is strange here, my answer is not valid for your case. for 2 encode invocations, append has been called only 4 times.at a glance all invocations has not been recorded. someone who is familiar with VisuvalVM might be able to help you. i tried to help you based on my experience on JProfiler. thanks – hunter Sep 25 '16 at 04:52