The debug agent is suppose to co-locate some events into the same composite event (which on the JDI side equates to being in the same EventSet). See https://docs.oracle.com/en/java/javase/17/docs/specs/jdwp/jdwp-protocol.html#JDWP_Event_Composite, which says:
The events that are grouped in a composite event are restricted in the following ways:
...
Only with other members of this group, at the same location and in the same thread:
Breakpoint Event
Step Event
Method Entry Event
Method Exit Event
As and aside, and not relevant to this CR, it appears that MethodExitEvents are not co-located with the others, and the JDI spec says the following in the EventSet description, which is in slight disagreement with the JDWP spec, but seems to match the implementation:
Only with other members of this group, at the same location and in the same thread:
BreakpointEvent
StepEvent
MethodEntryEvent
There is a bug in the debug agent support for this that is triggered when MethodEntryEvents are enabled, and then single stepping over an instruction that results in class loading (or lookup) triggered by constant pool resolution. There is a bit of a hack in JVMTI that disables single stepping during this time (actually more like hides it - see JvmtiExport::hide_single_stepping), but the debug agent thinks that single stepping is still enabled, which leads to some confusion in its event co-location logic.
When the debug agent gets a MethodEntryEvent triggered by the class lookup, it needs to figure out if there might also be a single step or breakpoint at this same location. If there is, it defers handling of the event so it can be combined with the upcoming single step or breakpoint events (JVMTI always delivers these events in the following order: MethodEntry, Step, Breakpoint). The debug agent can see that single stepping is currently enabled for the thread, so it defers the MethodEntry and returns back to JVMTI (from the event callback). It assumes that the next thing JVMTI will do is send along the StepEvent. However, JVMTI sees that it is in the "hide single step events" mode, so it doesn't post the StepEvent, and simply continues execution of the thread. Eventually another MethodEntryEvent comes along and is also deferred, and then another, and another. These deferred MethodEntryEvents accumulate in the event bag hanging off the ThreadNode. Eventually some other event comes alone that is not deferred. It is also added to the bag, and triggers the sending of all the events in the bag, including the deferred MethodEntryEvents. So basically the debugger ends up getting the MethodEntryEvents long after the method entries actually happened, and all of them combined in an EventSet with a bunch of other MethodEntryEvents and some other event that triggered them all being sent.
It's unclear how to fix this properly. It seems that the debug agent may need knowledge of this JVMTI "hide single step events" mode in order to make correct decisions about co-locating events. Fortunately this problem is rare because it requires doing a single step over an instruction that references an unresolved ClassRef constant pool entry, and doing so when MethodEntryEvents are enabled. It also requires that the MethodEntryRequest does not filter library classes, which most IDEs do by default. With the filtering enabled, there are no MethodEntryEvents during the constant pool resolution.
There is a workaround that fixes this issue when doing a STEP_OVER or STEP_OUT. Rather than the debug agent checking if it has enabled JVMTI single stepping, instead checks some fields in the ThreadNode that say if single stepping is pending, and it is for a STEP_INTO. If it is not STEP_INTO, then it can assume that no StepEvent will occur at the same location and therefor the MethodEntryEvent should not be deferred. So this limits the bug to only happening when doing a STEP_INTO.
The events that are grouped in a composite event are restricted in the following ways:
...
Only with other members of this group, at the same location and in the same thread:
Breakpoint Event
Step Event
Method Entry Event
Method Exit Event
As and aside, and not relevant to this CR, it appears that MethodExitEvents are not co-located with the others, and the JDI spec says the following in the EventSet description, which is in slight disagreement with the JDWP spec, but seems to match the implementation:
Only with other members of this group, at the same location and in the same thread:
BreakpointEvent
StepEvent
MethodEntryEvent
There is a bug in the debug agent support for this that is triggered when MethodEntryEvents are enabled, and then single stepping over an instruction that results in class loading (or lookup) triggered by constant pool resolution. There is a bit of a hack in JVMTI that disables single stepping during this time (actually more like hides it - see JvmtiExport::hide_single_stepping), but the debug agent thinks that single stepping is still enabled, which leads to some confusion in its event co-location logic.
When the debug agent gets a MethodEntryEvent triggered by the class lookup, it needs to figure out if there might also be a single step or breakpoint at this same location. If there is, it defers handling of the event so it can be combined with the upcoming single step or breakpoint events (JVMTI always delivers these events in the following order: MethodEntry, Step, Breakpoint). The debug agent can see that single stepping is currently enabled for the thread, so it defers the MethodEntry and returns back to JVMTI (from the event callback). It assumes that the next thing JVMTI will do is send along the StepEvent. However, JVMTI sees that it is in the "hide single step events" mode, so it doesn't post the StepEvent, and simply continues execution of the thread. Eventually another MethodEntryEvent comes along and is also deferred, and then another, and another. These deferred MethodEntryEvents accumulate in the event bag hanging off the ThreadNode. Eventually some other event comes alone that is not deferred. It is also added to the bag, and triggers the sending of all the events in the bag, including the deferred MethodEntryEvents. So basically the debugger ends up getting the MethodEntryEvents long after the method entries actually happened, and all of them combined in an EventSet with a bunch of other MethodEntryEvents and some other event that triggered them all being sent.
It's unclear how to fix this properly. It seems that the debug agent may need knowledge of this JVMTI "hide single step events" mode in order to make correct decisions about co-locating events. Fortunately this problem is rare because it requires doing a single step over an instruction that references an unresolved ClassRef constant pool entry, and doing so when MethodEntryEvents are enabled. It also requires that the MethodEntryRequest does not filter library classes, which most IDEs do by default. With the filtering enabled, there are no MethodEntryEvents during the constant pool resolution.
There is a workaround that fixes this issue when doing a STEP_OVER or STEP_OUT. Rather than the debug agent checking if it has enabled JVMTI single stepping, instead checks some fields in the ThreadNode that say if single stepping is pending, and it is for a STEP_INTO. If it is not STEP_INTO, then it can assume that no StepEvent will occur at the same location and therefor the MethodEntryEvent should not be deferred. So this limits the bug to only happening when doing a STEP_INTO.