Skip to content

Commit 9c70e06

Browse files
authored
Merge pull request #84 from fwcd/master
Generate object graph visualization in a fully language-agnostic way in GenericEvaluator
2 parents 061ccdc + b47efb7 commit 9c70e06

File tree

3 files changed

+159
-22
lines changed

3 files changed

+159
-22
lines changed

demos/java/.project

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1+
<?xml version="1.0" encoding="UTF-8"?>
22
<projectDescription>
3-
<name>java</name>
4-
<comment/>
5-
<projects>&#xD;
6-
</projects>
7-
<buildSpec>
8-
<buildCommand>
9-
<name>org.eclipse.jdt.core.javabuilder</name>
10-
<arguments>&#xD;
11-
</arguments>
12-
</buildCommand>
13-
</buildSpec>
14-
<natures>
15-
<nature>org.eclipse.jdt.core.javanature</nature>
16-
</natures>
17-
</projectDescription>
3+
<name>java</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
</buildSpec>
14+
<natures>
15+
<nature>org.eclipse.jdt.core.javanature</nature>
16+
</natures>
17+
<filteredResources>
18+
<filter>
19+
<id>1599063072264</id>
20+
<name></name>
21+
<type>30</type>
22+
<matcher>
23+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
24+
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25+
</matcher>
26+
</filter>
27+
</filteredResources>
28+
</projectDescription>

extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
DataExtractionResult,
33
DataExtractorId,
4+
GraphNode,
5+
GraphVisualizationData,
46
} from "@hediet/debug-visualizer-data-extraction";
57
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
68
import {
@@ -38,13 +40,36 @@ export class GenericEvaluator implements Evaluator {
3840
expression,
3941
preferredExtractorId,
4042
});
41-
let reply;
43+
let reply: { result: string, variablesReference: number };
4244
try {
4345
reply = await this.session.evaluate({
4446
expression: finalExpression,
4547
frameId,
4648
context: this.getContext(),
4749
});
50+
51+
// Use structural information about variables
52+
// from the evaluation response if present.
53+
if (reply.variablesReference) {
54+
const graph = await this.constructGraphFromVariablesReference(reply.result, reply.variablesReference);
55+
56+
return {
57+
kind: "data",
58+
result: {
59+
availableExtractors: [],
60+
usedExtractor: {
61+
id: "generic" as any,
62+
name: "Generic",
63+
priority: 1,
64+
},
65+
data: graph,
66+
},
67+
}
68+
} else {
69+
return parseEvaluationResultFromGenericDebugAdapter(reply.result, {
70+
debugAdapterType: this.session.session.configuration.type,
71+
});
72+
}
4873
} catch (error) {
4974
return {
5075
kind: "error",
@@ -65,10 +90,87 @@ export class GenericEvaluator implements Evaluator {
6590
},
6691
};
6792
}
93+
}
6894

69-
return parseEvaluationResultFromGenericDebugAdapter(reply.result, {
70-
debugAdapterType: this.session.session.configuration.type,
71-
});
95+
/**
96+
* Constructs GraphVisualizationData from a DAP variables
97+
* reference by successively querying the debug adapter for
98+
* variables. Objects are considered to be equivalent if
99+
* they share the same variables reference (this is important
100+
* for representing cyclic relationships).
101+
*
102+
* @param rootLabel - The root object's label
103+
* @param rootVariablesReference - The root object's DAP variables reference
104+
* @param maxDepth - The maximum depth to search at
105+
* @param maxKnownNodes - The maximum number of nodes
106+
*/
107+
private async constructGraphFromVariablesReference(
108+
rootLabel: string,
109+
rootVariablesReference: number,
110+
maxDepth: number = 30,
111+
maxKnownNodes: number = 50,
112+
): Promise<GraphVisualizationData> {
113+
// Perform a breadth-first search on the object to construct the graph
114+
115+
const graph: GraphVisualizationData = {
116+
kind: { graph: true },
117+
nodes: [],
118+
edges: []
119+
};
120+
const knownNodeIds: { [ref: number]: string; } = {};
121+
const bfsQueue: { source: { id: string, name: string } | undefined, label: string, variablesReference: number, depth: number }[] = [{
122+
source: undefined,
123+
label: rootLabel,
124+
variablesReference: rootVariablesReference,
125+
depth: 0,
126+
}];
127+
128+
let knownCount: number = 0;
129+
130+
do {
131+
const variable = bfsQueue.shift()!;
132+
const hasChilds = variable.variablesReference > 0;
133+
134+
if (variable.depth > maxDepth) {
135+
break;
136+
}
137+
138+
let nodeId: string;
139+
140+
if (!hasChilds || !(variable.variablesReference in knownNodeIds)) {
141+
// The variable is a leaf or an unvisited object: create the node.
142+
143+
const node: GraphNode = {
144+
id: hasChilds ? `${variable.variablesReference}` : `__${variable.label}@${knownCount}__`,
145+
label: variable.label,
146+
color: variable.depth == 0 ? "lightblue" : undefined,
147+
shape: "box",
148+
};
149+
150+
graph.nodes.push(node);
151+
knownCount++;
152+
153+
if (hasChilds) {
154+
knownNodeIds[variable.variablesReference] = node.id;
155+
156+
for (const child of await this.session.getVariables({ variablesReference: variable.variablesReference })) {
157+
bfsQueue.push({ source: { id: node.id, name: child.name }, label: child.value, variablesReference: child.variablesReference, depth: variable.depth + 1 });
158+
}
159+
}
160+
161+
nodeId = node.id;
162+
} else {
163+
// The variable is a visited object (e.g. due to a cyclic reference)
164+
165+
nodeId = knownNodeIds[variable.variablesReference];
166+
}
167+
168+
if (variable.source) {
169+
graph.edges.push({ from: variable.source.id, to: nodeId, label: variable.source.name });
170+
}
171+
} while (bfsQueue.length > 0 && knownCount <= maxKnownNodes);
172+
173+
return graph;
72174
}
73175

74176
protected getFinalExpression(args: {

extension/src/debugger/EnhancedDebugSession.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ export class EnhancedDebugSession {
4646
}
4747
}
4848

49+
public async getVariables(args: {
50+
variablesReference: number
51+
}): Promise<Variable[]> {
52+
try {
53+
const reply = await this.session.customRequest("variables", {
54+
variablesReference: args.variablesReference
55+
});
56+
if (!reply) {
57+
return [];
58+
}
59+
return reply.variables;
60+
} catch (error) {
61+
console.error(error);
62+
return [];
63+
}
64+
}
65+
4966
/**
5067
* Evaluates the given expression.
5168
* If context is "watch", long results are usually shortened.
@@ -55,15 +72,22 @@ export class EnhancedDebugSession {
5572
expression: string;
5673
frameId: number | undefined;
5774
context: "watch" | "repl" | "copy";
58-
}): Promise<{ result: string }> {
75+
}): Promise<{ result: string, variablesReference: number }> {
5976
const reply = await this.session.customRequest("evaluate", {
6077
expression: args.expression,
6178
frameId: args.frameId,
6279
context: args.context,
6380
});
64-
return { result: reply.result };
81+
return { result: reply.result, variablesReference: reply.variablesReference };
6582
}
6683
}
84+
85+
interface Variable {
86+
name: string;
87+
value: string;
88+
variablesReference: number;
89+
}
90+
6791
interface StackFrame {
6892
id: number;
6993
name: string;

0 commit comments

Comments
 (0)