Skip to content

Improve variables view #308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 133 additions & 18 deletions src/main/java/org/javacs/debug/JavaDebugServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -440,6 +441,7 @@ public void terminate(TerminateArguments req) {

@Override
public void continue_(ContinueArguments req) {
valueIdTracker.clear();
vm.resume();
}

Expand All @@ -454,6 +456,7 @@ public void next(NextArguments req) {
var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER);
step.addCountFilter(1);
step.enable();
valueIdTracker.clear();
vm.resume();
}

Expand All @@ -468,6 +471,7 @@ public void stepIn(StepInArguments req) {
var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO);
step.addCountFilter(1);
step.enable();
valueIdTracker.clear();
vm.resume();
}

Expand All @@ -482,6 +486,7 @@ public void stepOut(StepOutArguments req) {
var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OUT);
step.addCountFilter(1);
step.enable();
valueIdTracker.clear();
vm.resume();
}

Expand Down Expand Up @@ -632,47 +637,157 @@ private com.sun.jdi.StackFrame findFrame(long id) {
@Override
public ScopesResponseBody scopes(ScopesArguments req) {
var resp = new ScopesResponseBody();
var fields = new Scope();
fields.name = "Fields";
fields.presentationHint = "locals";
fields.expensive = true; // do not expand by default
fields.variablesReference = req.frameId * 2;
var locals = new Scope();
locals.name = "Locals";
locals.presentationHint = "locals";
locals.variablesReference = req.frameId * 2;
var arguments = new Scope();
arguments.name = "Arguments";
arguments.presentationHint = "arguments";
arguments.variablesReference = req.frameId * 2 + 1;
resp.scopes = new Scope[] {locals, arguments};
locals.variablesReference = req.frameId * 2 + 1;
resp.scopes = new Scope[] {fields, locals};
return resp;
}

private static final long VALUE_ID_START = 1000000000;

private static class ValueIdTracker {
private final HashMap<Long, Value> values = new HashMap<>();
private long nextId = VALUE_ID_START;

public void clear() {
values.clear();
// Keep nextId to avoid accidentally accessing wrong Values.
}

public Value get(long id) {
return values.get(id);
}

public long put(Value value) {
long id = nextId++;
values.put(id, value);
return id;
}
}

private final ValueIdTracker valueIdTracker = new ValueIdTracker();

private static boolean hasInterestingChildren(Value value) {
return value instanceof ObjectReference && !(value instanceof StringReference);
}

@Override
public VariablesResponseBody variables(VariablesArguments req) {
var frameId = req.variablesReference / 2;
var scopeId = (int) (req.variablesReference % 2);
var argumentScope = scopeId == 1;
if (req.variablesReference < VALUE_ID_START) {
var frameId = req.variablesReference / 2;
var scopeId = (int)(req.variablesReference % 2);
return frameVariables(frameId, scopeId);
}
Value value = valueIdTracker.get(req.variablesReference);
return valueChildren(value);
}

private VariablesResponseBody frameVariables(long frameId, int scopeId) {
var frame = findFrame(frameId);
var thread = frame.thread();
var variables = new ArrayList<Variable>();

if (scopeId == 0) {
var thisValue = frame.thisObject();
if (thisValue != null) {
variables.addAll(objectFieldsAsVariables(thisValue, thread));
}
} else {
variables.addAll(frameLocalsAsVariables(frame, thread));
}

var resp = new VariablesResponseBody();
resp.variables = variables.toArray(Variable[]::new);
return resp;
}

private VariablesResponseBody valueChildren(Value parentValue) {
// TODO: Use an actual owner thread.
ThreadReference mainThread = vm.allThreads().get(0);
var variables = new ArrayList<Variable>();

if (parentValue instanceof ArrayReference array) {
variables.addAll(arrayElementsAsVariables(array, mainThread));
} else if (parentValue instanceof ObjectReference object) {
variables.addAll(objectFieldsAsVariables(object, mainThread));
}

var resp = new VariablesResponseBody();
resp.variables = variables.toArray(Variable[]::new);
return resp;
}

private List<Variable> frameLocalsAsVariables(com.sun.jdi.StackFrame frame, ThreadReference thread) {
List<LocalVariable> visible;
try {
visible = frame.visibleVariables();
} catch (AbsentInformationException __) {
LOG.warning(String.format("No visible variable information in %s", frame.location()));
return new VariablesResponseBody();
return List.of();
}
var values = frame.getValues(visible);
var thread = frame.thread();

var variables = new ArrayList<Variable>();
var values = frame.getValues(visible);
for (var v : visible) {
if (v.isArgument() != argumentScope) continue;
var value = values.get(v);
var w = new Variable();
w.name = v.name();
w.value = print(values.get(v), thread);
w.value = print(value, thread);
w.type = v.typeName();
// TODO set variablesReference and allow inspecting structure of collections and POJOs
if (hasInterestingChildren(value)) {
w.variablesReference = valueIdTracker.put(value);
}
if (value instanceof ArrayReference array) {
w.indexedVariables = array.length();
}
// TODO set variablePresentationHint
variables.add(w);
}
var resp = new VariablesResponseBody();
resp.variables = variables.toArray(Variable[]::new);
return resp;
return variables;
}

private List<Variable> arrayElementsAsVariables(ArrayReference array, ThreadReference thread) {
var variables = new ArrayList<Variable>();
var arrayType = (ArrayType) array.type();
var values = array.getValues();
var length = values.size();
for (int i = 0; i < length; i++) {
var value = values.get(i);
var w = new Variable();
w.name = Integer.toString(i, 10);
w.value = print(value, thread);
w.type = arrayType.componentTypeName();
if (hasInterestingChildren(value)) {
w.variablesReference = valueIdTracker.put(value);
}
variables.add(w);
}
return variables;
}

private List<Variable> objectFieldsAsVariables(ObjectReference object, ThreadReference thread) {
var variables = new ArrayList<Variable>();
var classType = (ClassType) object.type();
var values = object.getValues(classType.allFields());
for (var field : values.keySet()) {
var value = values.get(field);
var w = new Variable();
w.name = field.name();
w.value = print(value, thread);
w.type = field.typeName();
if (hasInterestingChildren(value)) {
w.variablesReference = valueIdTracker.put(value);
}
variables.add(w);
}
return variables;
}

private String print(Value value, ThreadReference t) {
Expand Down
Binary file added src/test/examples/debug/DeepVariables.class
Binary file not shown.
14 changes: 14 additions & 0 deletions src/test/examples/debug/DeepVariables.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public class DeepVariables {
public static void main(String[] args) {
new DeepVariables().run();
}

public void run() {
Inner object = new Inner();
System.out.println(object.value);
}

private static class Inner {
public final int value = 42;
}
}
66 changes: 65 additions & 1 deletion src/test/java/org/javacs/JavaDebugServerTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.javacs;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -207,7 +210,7 @@ public void printCollections() throws IOException, InterruptedException {
var scopes = server.scopes(requestScopes).scopes;
// Get locals
var requestLocals = new VariablesArguments();
requestLocals.variablesReference = scopes[0].variablesReference;
requestLocals.variablesReference = scopes[1].variablesReference;
var locals = server.variables(requestLocals).variables;
System.out.println("Locals:");
for (var v : locals) {
Expand All @@ -220,5 +223,66 @@ public void printCollections() throws IOException, InterruptedException {
process.waitFor();
}

@Test
public void deepVariables() throws IOException, InterruptedException {
launchProcess("DeepVariables");
attach(5005);
setBreakpoint("DeepVariables", 8);
server.configurationDone();
stoppedEvents.take();

// Find the main thread
org.javacs.debug.proto.Thread mainThread = null;
for (var t : server.threads().threads) {
if (t.name.equals("main")) {
mainThread = t;
}
}
assertThat(mainThread, notNullValue());

// Get the stack trace
var requestTrace = new StackTraceArguments();
requestTrace.threadId = mainThread.id;
var stack = server.stackTrace(requestTrace);

// Get variables
var requestScopes = new ScopesArguments();
requestScopes.frameId = stack.stackFrames[0].id;
var scopes = server.scopes(requestScopes).scopes;

// Get locals
var requestLocals = new VariablesArguments();
requestLocals.variablesReference = scopes[1].variablesReference;
var locals = server.variables(requestLocals).variables;

// Find an object value
Variable objectVariable = null;
for (var v : locals) {
if (v.name.equals("object")) {
objectVariable = v;
}
}
assertThat(objectVariable, notNullValue());

// Get an object field
var requestObject = new VariablesArguments();
requestObject.variablesReference = objectVariable.variablesReference;
var fields = server.variables(requestObject).variables;

// Inspect an object field
Variable fieldVariable = null;
for (var v : fields) {
if (v.name.equals("value")) {
fieldVariable = v;
}
}
assertThat(fieldVariable, notNullValue());
assertThat(fieldVariable.value, equalTo("42"));

// Wait for process to exit
server.continue_(new ContinueArguments());
process.waitFor();
}

private static final Logger LOG = Logger.getLogger("main");
}