Merge pull request #84 from fwcd/master
Generate object graph visualization in a fully language-agnostic way in GenericEvaluator
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>java</name>
|
||||
<comment/>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
<name>java</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1599063072264</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
DataExtractorId,
|
||||
GraphNode,
|
||||
GraphVisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
import {
|
||||
@@ -38,13 +40,36 @@ export class GenericEvaluator implements Evaluator {
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
});
|
||||
let reply;
|
||||
let reply: { result: string, variablesReference: number };
|
||||
try {
|
||||
reply = await this.session.evaluate({
|
||||
expression: finalExpression,
|
||||
frameId,
|
||||
context: this.getContext(),
|
||||
});
|
||||
|
||||
// Use structural information about variables
|
||||
// from the evaluation response if present.
|
||||
if (reply.variablesReference) {
|
||||
const graph = await this.constructGraphFromVariablesReference(reply.result, reply.variablesReference);
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
result: {
|
||||
availableExtractors: [],
|
||||
usedExtractor: {
|
||||
id: "generic" as any,
|
||||
name: "Generic",
|
||||
priority: 1,
|
||||
},
|
||||
data: graph,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return parseEvaluationResultFromGenericDebugAdapter(reply.result, {
|
||||
debugAdapterType: this.session.session.configuration.type,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "error",
|
||||
@@ -65,10 +90,87 @@ export class GenericEvaluator implements Evaluator {
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return parseEvaluationResultFromGenericDebugAdapter(reply.result, {
|
||||
debugAdapterType: this.session.session.configuration.type,
|
||||
});
|
||||
/**
|
||||
* Constructs GraphVisualizationData from a DAP variables
|
||||
* reference by successively querying the debug adapter for
|
||||
* variables. Objects are considered to be equivalent if
|
||||
* they share the same variables reference (this is important
|
||||
* for representing cyclic relationships).
|
||||
*
|
||||
* @param rootLabel - The root object's label
|
||||
* @param rootVariablesReference - The root object's DAP variables reference
|
||||
* @param maxDepth - The maximum depth to search at
|
||||
* @param maxKnownNodes - The maximum number of nodes
|
||||
*/
|
||||
private async constructGraphFromVariablesReference(
|
||||
rootLabel: string,
|
||||
rootVariablesReference: number,
|
||||
maxDepth: number = 30,
|
||||
maxKnownNodes: number = 50,
|
||||
): Promise<GraphVisualizationData> {
|
||||
// Perform a breadth-first search on the object to construct the graph
|
||||
|
||||
const graph: GraphVisualizationData = {
|
||||
kind: { graph: true },
|
||||
nodes: [],
|
||||
edges: []
|
||||
};
|
||||
const knownNodeIds: { [ref: number]: string; } = {};
|
||||
const bfsQueue: { source: { id: string, name: string } | undefined, label: string, variablesReference: number, depth: number }[] = [{
|
||||
source: undefined,
|
||||
label: rootLabel,
|
||||
variablesReference: rootVariablesReference,
|
||||
depth: 0,
|
||||
}];
|
||||
|
||||
let knownCount: number = 0;
|
||||
|
||||
do {
|
||||
const variable = bfsQueue.shift()!;
|
||||
const hasChilds = variable.variablesReference > 0;
|
||||
|
||||
if (variable.depth > maxDepth) {
|
||||
break;
|
||||
}
|
||||
|
||||
let nodeId: string;
|
||||
|
||||
if (!hasChilds || !(variable.variablesReference in knownNodeIds)) {
|
||||
// The variable is a leaf or an unvisited object: create the node.
|
||||
|
||||
const node: GraphNode = {
|
||||
id: hasChilds ? `${variable.variablesReference}` : `__${variable.label}@${knownCount}__`,
|
||||
label: variable.label,
|
||||
color: variable.depth == 0 ? "lightblue" : undefined,
|
||||
shape: "box",
|
||||
};
|
||||
|
||||
graph.nodes.push(node);
|
||||
knownCount++;
|
||||
|
||||
if (hasChilds) {
|
||||
knownNodeIds[variable.variablesReference] = node.id;
|
||||
|
||||
for (const child of await this.session.getVariables({ variablesReference: variable.variablesReference })) {
|
||||
bfsQueue.push({ source: { id: node.id, name: child.name }, label: child.value, variablesReference: child.variablesReference, depth: variable.depth + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
nodeId = node.id;
|
||||
} else {
|
||||
// The variable is a visited object (e.g. due to a cyclic reference)
|
||||
|
||||
nodeId = knownNodeIds[variable.variablesReference];
|
||||
}
|
||||
|
||||
if (variable.source) {
|
||||
graph.edges.push({ from: variable.source.id, to: nodeId, label: variable.source.name });
|
||||
}
|
||||
} while (bfsQueue.length > 0 && knownCount <= maxKnownNodes);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
protected getFinalExpression(args: {
|
||||
|
||||
@@ -46,6 +46,23 @@ export class EnhancedDebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
public async getVariables(args: {
|
||||
variablesReference: number
|
||||
}): Promise<Variable[]> {
|
||||
try {
|
||||
const reply = await this.session.customRequest("variables", {
|
||||
variablesReference: args.variablesReference
|
||||
});
|
||||
if (!reply) {
|
||||
return [];
|
||||
}
|
||||
return reply.variables;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given expression.
|
||||
* If context is "watch", long results are usually shortened.
|
||||
@@ -55,15 +72,22 @@ export class EnhancedDebugSession {
|
||||
expression: string;
|
||||
frameId: number | undefined;
|
||||
context: "watch" | "repl" | "copy";
|
||||
}): Promise<{ result: string }> {
|
||||
}): Promise<{ result: string, variablesReference: number }> {
|
||||
const reply = await this.session.customRequest("evaluate", {
|
||||
expression: args.expression,
|
||||
frameId: args.frameId,
|
||||
context: args.context,
|
||||
});
|
||||
return { result: reply.result };
|
||||
return { result: reply.result, variablesReference: reply.variablesReference };
|
||||
}
|
||||
}
|
||||
|
||||
interface Variable {
|
||||
name: string;
|
||||
value: string;
|
||||
variablesReference: number;
|
||||
}
|
||||
|
||||
interface StackFrame {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user