Repository navigation
Conversation
| io.printWithAction(" = ", null, false); | ||
| if (refValue instanceof ObjectVariable obj && !isNull(obj)) { | ||
| var sessionRef = new WeakReference<>(session); | ||
| io.printWithAction(obj.getValue(), () -> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
..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:
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| ) | ||
| @ActionReferences({ | ||
| @ActionReference(path = "Editors/text/x-java/Popup", position = 1900), | ||
| @ActionReference(path = "Editors/AnnotationTypes/BreakpointActionsWithToggleNew", position = 400) |
| * @return an <em>immutable snapshot</em> of the variable | ||
| * @since 3.47 | ||
| */ | ||
| default Variable toFixed() { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
- previous obscure alternative of Variable.toFixed()
- the
AbstractVariableimplementedCloneable, but asObject.clone()method is protected... - reflection had to be used to call it
- now it becomes simple
toFixed()call
eppleton
left a comment
There was a problem hiding this comment.
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.jpdanow overridesVariable.toFixed()from API 3.47, but its dependency onapi.debugger.jpdais still 3.18. It does no harm at runtime, but bumping it would keep the dependency honest.- The apichanges date is 28 Oct.
FixedWatchesManager.addFixedWatchhas a double space in= orig.toFixed().- The only red CI job is a
MoveMethodTesttimeout in refactoring.java, which looks unrelated.
| Object createMirrorObject(); | ||
|
|
||
|
|
||
| /** Creates an<em>immutable snapshot</em> of the variable. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Tooltip expanded in 87121f3 now it looks like
| import org.netbeans.junit.NbTestCase; | ||
| import org.openide.util.Utilities; | ||
|
|
||
| public class LogpointReviewTest extends NbTestCase { |
There was a problem hiding this comment.
Good to have a regression test for the identity problem. Some suggestions for keeping it maintainable:
- The names
LogpointReviewTest/LogpointReviewMaindescribe 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.srcproperty.
Could it go into BreakpointOutputTest, driving printLocals through the new BreakpointOutputWriter? That would at least avoid depending on the console internals.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
I like the concept (and the functionality), I like the UI much less. For me the behavior is that if:
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. |
|
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 |
b8ec084 to
96e1c30
Compare

to make the configuratin easy a new
{locals}directive insetPrintTextmessage is recognizednew Variable.toFixed() API added to support "snapshotting the variable"
remove all
{locals}occurrences and dump variables onceadd a tooltip for breakpoint message field