Skip to content

Log points to easily dump content of local variables - #9732

Open
jtulach wants to merge 1 commit into
apache:masterfrom
jtulach:jtulach/LogPoints
Open

jtulach wants to merge 1 commit into
apache:masterfrom
jtulach:jtulach/LogPoints

Conversation

@jtulach

@jtulach jtulach commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor
  • NetBeans IDE already supports log points
    • yet configuring them isn't easy (especially for debugging newbies)
  • this PR proposes an extra action "Add Logpoint"
log point action
  • the log point is just properly configured breakpoint
    • doesn't stop any thread when hit
    • asks to dump all local variables into "debugger console" everytime it is hit
  • e.g. prints multiple times when put into a for loop
log points
  • to make the configuratin easy a new {locals} directive in setPrintText message is recognized

    • to simplify the most common case
    • e.g. when one dumps all local variables of a method
  • new Variable.toFixed() API added to support "snapshotting the variable"

  • remove all {locals} occurrences and dump variables once

  • add a tooltip for breakpoint message field

@jtulach jtulach self-assigned this Oct 19, 2026
@jtulach jtulach added Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) debugger labels Oct 19, 2026
@jtulach jtulach added this to the NB33 milestone Oct 19, 2026
io.printWithAction(" = ", null, false);
if (refValue instanceof ObjectVariable obj && !isNull(obj)) {
var sessionRef = new WeakReference<>(session);
io.printWithAction(obj.getValue(), () -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hyperlink captures an ObjectLocalVariable wrapper that NetBeans reuses and updates. When a loop assigns another object to the same local, earlier links retain their original text but target the newer object. A temporary native test reproduced this: the first hyperlink’s object ID changed from 89 to 97. Capture a stable object reference when creating the link.

@jtulach jtulach Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

..temporary native test reporduced this ...

I don't understand. What is the test doing? True tests were missing, but I've just added one in 15eb8c4. It passes. What's the problem with the ObjectLocalVariable? Following code seems to be logging value of fac array properly:

fac array

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for editing your comment, newly using GH PR extension in VS Code and it has some editing specialities....
LogpointReviewTests.zip this is quick and dirty review regression test.

cp LogpointReviewTest.java to java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/

cp LogpointReviewMain.java to java/debugger.jpda.ui/test/unit/src/test/
Set: review_jvm_flags="$(cat nbbuild/jms-config/{base,desktop,compiler}.flags | tr '\n' ' ')"
Run then:

ANT_OPTS=-Djava.awt.headless=true \
ant -f java/debugger.jpda.ui/build.xml test-unit \
  -Dtest.includes='**/LogpointReviewTest.class' \
  -Dtest.qa-functional.src.dir=/tmp/pr9732-no-tests \
  "-Dtest.run.args=-ea -Djava.awt.headless=true -XX:+IgnoreUnrecognizedVMOptions $review_jvm_flags"

Should fail with something like: Earlier hyperlink should still target the first object expected:<89> but was:<97>
ID/numbers can differ for your run. Hope this helps

@jtulach jtulach Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the test, @MartinBalin(e)! It is valid and demontrates the problem quite clearly. It has opened my eyes. Added in e644433 - the test really fails the CI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The e644433 test is fixed by Variable.toFixed() introduced in 4db2f0a

@MartinBalin MartinBalin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left 2 comments

)
@ActionReferences({
@ActionReference(path = "Editors/text/x-java/Popup", position = 1900),
@ActionReference(path = "Editors/AnnotationTypes/BreakpointActionsWithToggleNew", position = 400)

@jtulach jtulach Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this Editors/AnnotationTypes/BreakpointActionsWithToggleNew registration we now see the action in the editor gutter of the Java files:

Image

Non-Java editors like PHP don't show this action. It wouldn't work as it is using JPDABreakpoint functionality.

@jtulach
jtulach requested a review from MartinBalin October 19, 2026 18:50
@jtulach
jtulach requested a review from MartinBalin October 26, 2026 06:30
* @return an <em>immutable snapshot</em> of the variable
* @since 3.47
*/
default Variable toFixed() {

@jtulach jtulach Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toFixed functionality was there, but it wasn't easily accessible via the API. Making it real, official API.

public void addFixedWatch (String name, Variable variable) {
public void addFixedWatch (String name, Variable orig) {
// Clone the variable to assure that it's unique and sticks to the JDI value.
if (variable instanceof Cloneable) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • previous obscure alternative of Variable.toFixed()
  • the AbstractVariable implemented Cloneable, but as Object.clone() method is protected...
  • reflection had to be used to call it
  • now it becomes simple toFixed() call

@eppleton eppleton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature, and toFixed() fixes the hyperlink identity problem cleanly. I checked a few things that turned out fine: SUSPEND_NONE breakpoints still pause the event thread while the listeners run (BreakpointImpl line 258), so reading the frame in printLocals is safe. The FixedWatchesManager change keeps the old behaviour, because every cloneable Variable (including WatchesModel.JPDAWatchEvaluating) extends AbstractVariable. The gutter popup moves the caret before it opens, so getCurrentLineNumber() returns the right line.

A few comments inline. Small nits:

  • debugger.jpda now overrides Variable.toFixed() from API 3.47, but its dependency on api.debugger.jpda is still 3.18. It does no harm at runtime, but bumping it would keep the dependency honest.
  • The apichanges date is 28 Oct.
  • FixedWatchesManager.addFixedWatch has a double space in = orig.toFixed().
  • The only red CI job is a MoveMethodTest timeout in refactoring.java, which looks unrelated.

Object createMirrorObject();


/** Creates an<em>immutable snapshot</em> of the variable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadoc promises an immutable snapshot of the current state, but for objects the copy only keeps pointing at the same object. Its state is still live.

I checked this locally on the PR head. A local held an object whose int field the program changed from 100 to 101 between two hits of the logpoint. At the second hit, the variable held by the first hit's hyperlink reported 101 (same unique ID in both hits). That fits the code: AbstractObjectVariable.clone() reuses the same JDI ObjectReference, and DebuggetStateListener marks the fields for re-reading as soon as the debuggee resumes. Garbage collection isn't disabled either, so the object can also disappear later.

So for a logpoint, clicking the link from iteration 1 shows the object as it is now, not as it was when the line was printed. Since this becomes public API, I'd describe what it actually guarantees: the copy keeps referring to the current value/object, regardless of what the original variable is reassigned to later. Two minor things: "animmutable" is missing a space, and toFixed might remind people of JavaScript's Number.toFixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting comment, Toni! On one side, it is obvious, how the debugger behaves when it works with live heap. E.g. it only snapshots the reference, not the whole state of the object.

On the other hand, this kind of snapshotting of the whole reference graph is exactly what I had done for GraalVM Insight heap dumping. E.g. the request to really create immutable snapshot isn't off, it could be done. Just probably not in this PR.

toFixed might remind people of JavaScript's Number.toFixed

Let's try to call the method asValue() then! Done in 8075fdc

debugger.jpda now overrides Variable.toFixed() from API 3.47, but its dependency on api.debugger.jpda is still 3.18.

How do you catch these omissions? Fixed in e7a606d

The apichanges date is 28 Oct.

I hope I'll get (your) approvals to merge on Oct 28. Is that too optimistic?

io.print(" ", null, false);
io.print(refValue.getName(), null, false);
io.print(" = ", null, false);
if (refValue instanceof ObjectVariable ov && !isNull(ov) && ov.toFixed() instanceof ObjectVariable obj) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to keep in mind about memory (I haven't measured it): every hit creates a copy of each object local, and the console hyperlink keeps it alive. Each copy also registers a (weak) state listener on the debugger in the AbstractObjectVariable constructor. {=expr} already works this way, but {locals} multiplies it by the number of locals, and logpoints are meant to sit in loops. A logpoint in a hot loop would keep adding up for the whole session. Maybe this is acceptable, but it might be worth limiting how many linked hits are kept.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the potential issue is already present in {=expr} we don't have to address it in this PR in my opinion. If it becomes a problem, then we'll need to address it in the future and we'd need to do it regardless of this PR.

Btw. the references do not keep the debuggee objects alive. Those objects can still be GCed and then the link becomes dysfunctional as its JDI value is gone.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this predates the PR with {=expr}, so it's fine to leave it for later. To clarify: I meant the IDE-side variable copies and their listeners, not the debuggee objects. I agree those can still be collected, which I also mentioned on the Variable.java comment.

)
@ActionReferences({
@ActionReference(path = "Editors/text/x-java/Popup", position = 1900),
@ActionReference(path = "Editors/AnnotationTypes/BreakpointActionsWithToggleNew", position = 400)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BreakpointActionsWithToggleNew is the popup of the existing Breakpoint annotation (Breakpoint.xml in spi.debugger.ui), so the entry only appears on a line that already has a breakpoint. "Add Logpoint" there adds a second breakpoint on the same line, so the line both stops and logs. Is that intended? On an existing breakpoint, I think users would expect "turn this into a logpoint" (set suspend to none and fill in the print text). The same applies to the editor popup on a line that already has a breakpoint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if everything you write is true, Toni. Exact steps to reproduce the problems might make it clear to me.

However yes, it is true, that adding a log point on a line that has a line breakpoint currently adds new breakpoint configured with SUSPEND_NONE - e.g. breakpoints and logpoints get accumulated on the line.

Is it a problem to prevent integration of this PR?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right to doubt it. I was wrong about where the entry shows up. Annotations.initMenu also adds the enabled actions of every visible annotation type that isn't on the line, so "Add Logpoint" is offered in the gutter on every Java line, not only on lines that already have a breakpoint. Sorry for the noise.

What remains is what you confirmed: on a line that already has a breakpoint, a second, non-suspending one is added. To reproduce: toggle a line breakpoint, then choose Add Logpoint on the same line, and the Breakpoints view lists two entries for that line. I don't think that should block the integration. Turning an existing line breakpoint into a logpoint could be a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying the breakpoint message of an existing breakpoint is a UX related change that may deserve its own discussion. Let's leave it for some other PR.

}
}

int printLocals = printText.indexOf(localsPattern);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor points about {locals}:

  • It is removed from wherever it appears, and the locals are always printed after the message.
  • Only the first occurrence is removed; a second {locals} is printed literally.

Both are fine if documented, but I couldn't find {locals} mentioned anywhere a user would see it, e.g. near the print text field in the breakpoint customizer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locals are always printed after the message.

Right. That's the intention. I considered it, but dumping all the local variables (inherently multi line) in the middle of the (single line) message seem inappropriate.

a second {locals} is printed literally.

Should it be also removed? I guess we don't need to dump the variables twice, right?

mentioned anywhere a user would see it

Where do you want me to add a reference to {locals}? If there is some suitable place I happily add some text there. However I think the whole BreakpointOutput infrastructure {=expr}, {className}, etc. is mostly undocumented. Which my be the reason why not many people found they can configure log points themselves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Printing the locals after the message makes sense to me. Yes, I'd remove every occurrence and print the locals once.

For documentation, the print text field in ActionsPanel (tfPrintText) has no tooltip. A tooltip listing the placeholders ({className}, {methodName}, {lineNumber}, {threadName}, {=expression}, {locals}) would be a small, natural place to put it, and it would cover the undocumented ones you mentioned too. It doesn't need to be part of this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltip expanded in 87121f3 now it looks like

tooltip

import org.netbeans.junit.NbTestCase;
import org.openide.util.Utilities;

public class LogpointReviewTest extends NbTestCase {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to have a regression test for the identity problem. Some suggestions for keeping it maintainable:

  • The names LogpointReviewTest/LogpointReviewMain describe how the test came about rather than what it checks.
  • It reads private console internals by reflection (output, ioManager, buffer, text, action) plus the lambda's captured fields, so any refactoring of the console IO will break it.
  • It sets the global test.dir.src property.

Could it go into BreakpointOutputTest, driving printLocals through the new BreakpointOutputWriter? That would at least avoid depending on the console internals.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a test provided by @MartinBalin's agent. The amount of reflection doesn't please me, but looks like agents are unstoppable when reaching their goals. I am not happy about the amount of the reflection either, but I don't want to waste (my) human power on polishing AI generated test code (I don't think it'd be easy as BreakpointOutputTest doesn't have the machinery to check the same without reflection either). The test has its value - it revealed a real problem and now it ensures the behavior is correct. Given how different and unnatural the test is, it is kept in a separate file however.

@eppleton eppleton left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The updated asValue() javadoc now describes it accurately: for objects it keeps referring to the same object regardless of later re-assignment, rather than taking an immutable snapshot of its state (see the comment on Variable.java). The other points are fine as follow-ups.

@lahodaj

lahodaj commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

I like the concept (and the functionality), I like the UI much less. For me the behavior is that if:

  • there's nothing on the line, I can either do Toggle Line Breakpoint or Add Logpoint. Either of these will visually add a breakpoint to the line (this by itself would not be a problem, but see below);
  • if there's a line breakpoint on a line, the Add Logpoint is disabled for me. I.e. I can't add a log point
  • if there's a log point, then Toggle Breakpoint removes it

I.e. the fact that the log points are just line breakpoints leaks into the behavior and UI of the editor and gutter. It does not make the feature/approach unusable, but the sharp edges are really sharp, IMO.

@jtulach

jtulach commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Looks like we are all (me, @MartinBalin and @eppleton) OK with b8ec084. Let's squash the changes and integrate on Oct 28, 2026 as announced in apichanges.xml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

debugger Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants