Skip to main content

Method Breakpoints are Evil

Some IDEs expose an option to set "Method Breakpoints", it turns out that "Method Breakpoints" might tremendously decrease debugger's performance. In this article, I explain what are "Method Breakpoints" and the reasons they impact performance so badly.

To better understand this subject I will cover how Debuggers works under the hoods and how Breakpoints and Method Breakpoints are implemented internally.

Java Platform Debugger Architecture

JDPA is an architecture designed for enabling communication between debuggers and debugees. The architecture consists of three APIs:

  1. JVM Tool Interface (JVM TI) - A native interface which defines the services a VM must provide for debugging purposes
  2. Java Debug Wire Protocol (JWDP) - A textual API which defines the communication's protocol between debugee and debugger
  3. Java Debug Interface (JDI) - Defines a high-level Java API designed to facilitate the interaction between debugge and debuggers. Internally JDI implements the JDWP protocol


The VM expose JVMTI capabilities to its Agent (back-end layer). The Agent lives inside VM's process and communicates with the VM with callbacks (via JNI). Whenever something interesting happens the VM fires a callback to the Agent which in turn send the event back to the debugger (front-end layer), our IDE, in most of the cases. The debugger can communicate with the Agent via raw JDWP or via JDI.

Whenever you set a Breakpoint, step into your code, read and alter variables' values, you are triggering a chain of events directed to your VM.

What are Method Breakpoints

At first sight, Method Breakpoints may seem very similar to regular Breakpoints, instead to be registered on a specific bytecode's line, Method Breakpoint can be configured to break on a method entry and/or exit.

Therefore, Method Breakpoints can be useful when you want to debug a program which you don't have its original source code (you can't instruct the Debugee to break on a specific line) or when there are multiple exits flows and you don't want to set manually numerous breakpoint on each possible exit point.

Consider the following flow, setting all these Breakpoint ain't so cool...:

Look and Feel

Method Breakpoints seems to be very similar in their appearance to regular breakpoints expect for the fact that they are set directly on a method signature instead of on a specific instruction line. Intelliji look & feel:

JVMIT - Zoom In

To understand how Method Breakpoints work and why they decrease performance, we must understand how  Breakpoints are implemented.
Let's dig into JVMTI native API.

Breakpoints - Internal Implementation

When using JVMTI to make Breakpoints work, we need to perform three steps: Enabling the event, registering the Breakpoint and receiving a callback when the VM hits the correct instruction.

Enabling Breakpoint Events

The VM allows debuggers (via the Agent) to enable different events. For example, in order to enable Breakpoints events, the Debugger needs to enable the "can_generate_breakpoint_events" event via the SetEventNotificationMode() method. Once done, the VM will fire events when the Breakpoint is reached.

Registering a Breakpoint

The VM also expose an interface to register a Breakpoint at a specific bytecode instruction:
SetBreakpoint(jvmtiEnv* env,
              jmethodID method,
              jlocation location)
Now, when a thread reaches the specified line of code, the VM will fire an event and stop the execution of all active threads.

Breakpoint Event

The event fired by the VM (to the Agent and back to the Debugger) has the following signature:
Breakpoint(jvmtiEnv *jvmti_env,
           JNIEnv* jni_env,
           jthread thread,
           jmethodID method,
           jlocation location)

NOTE: Breakpoint event is fired only when the JVM hits the specified location

Method Breakpoints - Internal Implementation

Method Break is an IDE feature not included in JPDA. Internally Method Breakpoints are implemented by your IDE using two JVMTI events:
  1. can_generate_method_entry_events
  2. can_generate_method_exit_events
When using JVMTI to make Method Breakpoints work, we need to perform two steps: Enabling the event(s), and receiving a call back when the VM enters or/and exits any method.

Enabling Method Entry and Method Exit Events

Similar to Breakpoints, Method Breakpoints can be enabled via the SetEventNotificationMode() method by registering the following events "can_generate_method_entry_events" and/or "can_generate_method_exit_events".
Once done, the VM will fire events when any thread enters or exit any method.

Method Entry/Exit Event

The event fired by the VM (to the Agent and back to the Debugger) has the following signature:

Method Entry event

MethodEntry(jvmtiEnv *jvmti_env,
            jNIEnv* jni_env,
            jthread thread,
            jmethodID method)


Method Exit event

MethodExit(jvmtiEnv *jvmti_env,
           JNIEnv* jni_env,
           jthread thread,
           jmethodID method,
           jboolean was_popped_by_exception,
           jvalue return_value)

Whenever we set a Method Breakpoint in our IDE the following actions occur:
  1. IDE adds the Breakpoints to its internal list of Method Breakpoints
  2. IDE tells the front-end to enable Method Entry and Method Exit events
  3. front-end (the Debugger) communicates the request to the VM through the Agent
  4. On each Method Entry and Method Exit event a notification is forwarded through the entire chain to the IDE
  5. The IDE checks whether its Method Breakpoint's list contains the method id which just fired the Method Entry event
  6. If #5 is found to be true then the IDE will send a SetBreakpoint request to the VM, otherwise, VM's thread will be released and nothing will happen

REMEMBER:
When registered, events are fired on each method twice on entering and exiting. Method Breakpoint is an IDE feature not included in JPDA

Why Method Breakpoints are so evil?

Equipped with this information we can now list a number of reasons why Method Breakpoints are so evil. All of these reasons are related to MethodEntry & MethodExit high-frequency events.

#1 - jmethodid

The most popular answer is related to the lookup JVM needs to perform to fetch the jmethodid of a method (jmethodid is a unique method identifier used to perform actions on/with methods). As explained before, on MethodEntry event the VM will send the jmethoid of the just method been entered:
MethodEntry(jvmtiEnv *jvmti_env,
            jNIEnv* jni_env,
            jthread thread,
            jmethodID method)

It figures out that when the VM needs to retrieve a jmethodid it needs to perform an expensive lookup. jmethodid lookup is a VM's internal implementation, my guess is that the VM creates a MethodID list based on bytecode's constant_pool.

Anyway, the combination of fetching jmethodid and numerous MethodEntry & MethodExit events makes Method Breakpoint very evil.

#2 - Communication

MethodEntry & MethodExit cause a lot of communication round-trips between the back-end (Agent) and the Debugger (front-end).

Furthermore, in case of remote debugging, communication can turn to a severe I/O penalty.

#3 - VM's callbacks are synchronous

As already explained, once the Agent registers itself to VM's events (as specified by JVMTI specification), the VM will notify the Agent via callbacks.
These callbacks are fired from the same thread which triggered the event. VM's thread needs to wait for the following:
  • Context switch - Agent must now take control and send a notification to the Debugger
  • Method Breakpoint Validation - The Debugger needs to check whether the jmethodid related the MethodEntry or MethodExit events matches a jmethodid registered to be breaked.
During this period of time code execution is stopped.

Conclusion

  1. Method Breakpoints are an IDE feature not included in JPDA
  2. Method Breakpoints are really evil
  3. Method Breakpoints will impact tremendously heavily busy processes (Debugees)
  4. Use them only when really needed (no more than once in a lifetime)
  5. If you must use Method Breakpoint, consider turning off Method Exit events

Epilogue

After reflecting on it, I found out that On Entry Method Breakpoint could have been implemented differently.

Instead of using the expensive Method Entry feature, when the user sets a Breakpoint on a method's signature, the debugger can search, at runtime, for the correct method, and set a breakpoint in its first instruction's line.

I had the chance to dig into JDB's source code and I figure out that this is exactly how On Entry Method Breakpoint is implemented. On Exit Method Breakpoint, on the other hand, do require using Method Exit event feature, but the suggested implementation can significantly decrease the chances of slowness as Method Exit event feature can be enabled by the debugger only if explicitly requested by the user and not by default.

Comments

The Best

GetHashCode Hands-On Session

The following is a hands-on post meant to demonstrate how GetHashCode() and Equals() methods are used by .NET Framework under the hood. For the sake of simplicity I will refer to the popular hashed-base Dictionary type, although any other hash based structure will follow a similar behavior, if not the same one. After understanding this post you should be able to spot potential problematic behaviors and resolve them, prevent creation of unreachable items in dictionaries and improve CRUD actions performance on hash based structures. The TheoryGetHashCode() is used to create a unique integer identifier for objects/structs. The hashcode can be used for two purposes: Programmatically, by developers, to distinguish objects/structs form each other (NOTE: Not recommended when the default .NET implementation is used, as it's not guaranteed to preserve the same hash between .NET versions and platforms)Internally, by .NET Framework, when using the object/struct as a key in a hashed based l…

Closures in C# vs JavaScript -
Same But Different

Closure in a Nutshell Closures are a Software phenomenon which exist in several languages, in which methods declared inside other methods (nested methods), capture variables declared inside the outer methods. This behavior makes captured variables available even after the outer method's scope has vanished.

The following pseudo-code demonstrates the simplest sample:
Main() //* Program starts from here { Closures(); } AgeCalculator() { int myAge = 30; return() => { //* Returns the correct answer although AgeCalculator method Scope should have ordinarily disappear return myAge++; }; } Closures() { Func ageCalculator = AgeCalculator(); //* At this point AgeCalculator scopeid cleared, but the captured values keeps to live Log(ageCalculator()); //* Result: 30 Log(ageCalculator()); //* Result: 31 } JavaScript and C# are two languages that support…

Formatting Data with IFormatProvider & ICustomFormatter

This post provides an internal overview of IFormatProvider & ICustomFormatter interfaces, and they way they are handled by .NET.

IFormatProvider is a .NET Framework Interface that should be used, by implementing its single public object GetFormat(Type) method, when there is a need to implement custom formatting of data like String and DateTime.

The public object GetFormat(Type) method simply returns an object that in turns is available to supply all available information to continue the formatting process. The Type passed in by the Framework is meant to give the implementor a way to decide which type to return back. Its like a Factory Method Design Pattern where the "formatType" is the type expected to be returned.
class MyProvider : IFormatProvider { public object GetFormat(Type formatType) { object result = null; //* Factory Method if (formatType == typeof( ICustomFormatter)) //* Some object, will be disc…