Merge pull request #84 from fwcd/master

Generate object graph visualization in a fully language-agnostic way in GenericEvaluator
This commit is contained in:
Henning Dieterichs
2020-09-29 18:52:54 +02:00
committed by GitHub
3 changed files with 159 additions and 22 deletions

View File

@@ -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>&#xD;
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>&#xD;
</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>

View File

@@ -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: {

View File

@@ -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;