The Truffle Instrument API provides a powerful framework for building debugging tools for languages implemented on the Truffle platform. This comprehensive guide covers creating custom debuggers, profilers, and monitoring tools.
Understanding Truffle Instrument Architecture
Key Components:
- Instruments: Debugging and profiling tools
- Probes: Code instrumentation points
- Events: Execution events (breakpoints, stepping, etc.)
- Contexts: Language execution contexts
- Sources: Source code representation
Project Setup and Dependencies
<properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- GraalVM Truffle API --> <dependency> <groupId>org.graalvm.truffle</groupId> <artifactId>truffle-api</artifactId> <version>23.1.0</version> </dependency> <dependency> <groupId>org.graalvm.truffle</groupId> <artifactId>truffle-dsl-processor</artifactId> <version>23.1.0</version> <scope>provided</scope> </dependency> <!-- JSON for debug protocol --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> <!-- WebSocket for debug adapter protocol --> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.5.4</version> </dependency> </dependencies>
Basic Instrument Foundation
package com.example.truffle.instrument;
import com.oracle.truffle.api.instrumentation.*;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Base class for Truffle instruments
*/
public abstract class BaseDebugInstrument implements Instrumenter {
protected final Set<EventBinding<?>> bindings = ConcurrentHashMap.newKeySet();
protected final Map<Object, List<EventListener>> eventListeners = new ConcurrentHashMap<>();
@Override
public <T extends EventContext> EventBinding<T> attachListener(
SourceSectionFilter sourceSectionFilter,
EventBinding.ExecutionEventNodeFactory<T> factory) {
EventBinding<T> binding = Instrumenter.super.attachListener(sourceSectionFilter, factory);
bindings.add(binding);
return binding;
}
@Override
public <T extends EventContext> EventBinding<T> attachExecutionEventFactory(
SourceSectionFilter sourceSectionFilter,
EventBinding.ExecutionEventNodeFactory<T> factory) {
EventBinding<T> binding = Instrumenter.super.attachExecutionEventFactory(sourceSectionFilter, factory);
bindings.add(binding);
return binding;
}
public void dispose() {
// Clean up all bindings
for (EventBinding<?> binding : bindings) {
binding.dispose();
}
bindings.clear();
eventListeners.clear();
}
protected void addEventListener(Object key, EventListener listener) {
eventListeners.computeIfAbsent(key, k -> new ArrayList<>()).add(listener);
}
protected void removeEventListeners(Object key) {
eventListeners.remove(key);
}
protected void notifyEventListeners(Object key, DebugEvent event) {
List<EventListener> listeners = eventListeners.get(key);
if (listeners != null) {
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}
public interface EventListener {
void onEvent(DebugEvent event);
}
public static class DebugEvent {
public enum Type {
BREAKPOINT_HIT, STEP_COMPLETE, EXCEPTION, THREAD_START, THREAD_END,
VARIABLE_CHANGE, FRAME_ENTER, FRAME_EXIT, SOURCE_LOADED
}
private final Type type;
private final Object data;
private final long timestamp;
public DebugEvent(Type type, Object data) {
this.type = type;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
// Getters
public Type getType() { return type; }
public Object getData() { return data; }
public long getTimestamp() { return timestamp; }
}
}
Breakpoint Manager Instrument
package com.example.truffle.instrument;
import com.oracle.truffle.api.instrumentation.*;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Advanced breakpoint management instrument
*/
@TruffleInstrument.Registration(
id = "debug-breakpoints",
name = "Debug Breakpoint Manager",
version = "1.0",
services = BreakpointManager.class
)
public class BreakpointInstrument extends TruffleInstrument {
private Env env;
private BreakpointManager breakpointManager;
@Override
protected void onCreate(Env env) {
this.env = env;
this.breakpointManager = new BreakpointManager(env);
env.registerService(breakpointManager);
// Register as instrumenter
env.getInstrumenter().attachLoadSourceListener(
SourceSectionFilter.ANY,
new LoadSourceListener(),
true
);
}
public static class BreakpointManager {
private final Env env;
private final Map<String, Set<Breakpoint>> breakpointsBySource = new ConcurrentHashMap<>();
private final Map<Integer, Breakpoint> breakpointsById = new ConcurrentHashMap<>();
private final Set<BreakpointListener> listeners = new CopyOnWriteArraySet<>();
private int nextBreakpointId = 1;
public BreakpointManager(Env env) {
this.env = env;
}
public Breakpoint setBreakpoint(Source source, int line) {
return setBreakpoint(source, line, null);
}
public Breakpoint setBreakpoint(Source source, int line, String condition) {
Breakpoint breakpoint = new Breakpoint(nextBreakpointId++, source, line, condition);
breakpointsBySource.computeIfAbsent(source.getName(), k -> new HashSet<>()).add(breakpoint);
breakpointsById.put(breakpoint.getId(), breakpoint);
// Notify listeners
for (BreakpointListener listener : listeners) {
listener.onBreakpointSet(breakpoint);
}
return breakpoint;
}
public boolean removeBreakpoint(int breakpointId) {
Breakpoint breakpoint = breakpointsById.remove(breakpointId);
if (breakpoint != null) {
Set<Breakpoint> sourceBreakpoints = breakpointsBySource.get(breakpoint.getSource().getName());
if (sourceBreakpoints != null) {
sourceBreakpoints.remove(breakpoint);
if (sourceBreakpoints.isEmpty()) {
breakpointsBySource.remove(breakpoint.getSource().getName());
}
}
for (BreakpointListener listener : listeners) {
listener.onBreakpointRemoved(breakpoint);
}
return true;
}
return false;
}
public List<Breakpoint> getBreakpoints() {
return new ArrayList<>(breakpointsById.values());
}
public List<Breakpoint> getBreakpointsForSource(Source source) {
Set<Breakpoint> sourceBreakpoints = breakpointsBySource.get(source.getName());
return sourceBreakpoints != null ? new ArrayList<>(sourceBreakpoints) : new ArrayList<>();
}
public void addBreakpointListener(BreakpointListener listener) {
listeners.add(listener);
}
public void removeBreakpointListener(BreakpointListener listener) {
listeners.remove(listener);
}
public void clearAllBreakpoints() {
List<Breakpoint> allBreakpoints = new ArrayList<>(breakpointsById.values());
for (Breakpoint breakpoint : allBreakpoints) {
removeBreakpoint(breakpoint.getId());
}
}
public Breakpoint findBreakpointAt(Source source, int line) {
Set<Breakpoint> sourceBreakpoints = breakpointsBySource.get(source.getName());
if (sourceBreakpoints != null) {
for (Breakpoint breakpoint : sourceBreakpoints) {
if (breakpoint.getLine() == line) {
return breakpoint;
}
}
}
return null;
}
}
private class LoadSourceListener implements LoadSourceListener {
@Override
public void onLoad(LoadSourceEvent event) {
Source source = event.getSource();
Set<Breakpoint> breakpoints = breakpointManager.breakpointsBySource.get(source.getName());
if (breakpoints != null && !breakpoints.isEmpty()) {
// Instrument the source with breakpoints
for (Breakpoint breakpoint : breakpoints) {
instrumentBreakpoint(breakpoint);
}
}
}
private void instrumentBreakpoint(Breakpoint breakpoint) {
SourceSectionFilter filter = SourceSectionFilter.newBuilder()
.sourceIs(breakpoint.getSource())
.lineIs(breakpoint.getLine())
.build();
env.getInstrumenter().attachExecutionEventFactory(filter,
new BreakpointExecutionFactory(breakpoint));
}
}
private static class BreakpointExecutionFactory
implements EventBinding.ExecutionEventNodeFactory<BreakpointContext> {
private final Breakpoint breakpoint;
BreakpointExecutionFactory(Breakpoint breakpoint) {
this.breakpoint = breakpoint;
}
@Override
public BreakpointContext create(EventContext context) {
return new BreakpointContext(context, breakpoint);
}
}
private static class BreakpointContext extends EventContext {
private final Breakpoint breakpoint;
BreakpointContext(EventContext context, Breakpoint breakpoint) {
super(context.getInstrumentedNode(), context.getInstrumentedSourceSection());
this.breakpoint = breakpoint;
}
@TruffleInstrumentation.Insert(value = TruffleInstrumentation.Insert.Before.class)
public void onBreakpoint() {
if (breakpoint.isEnabled()) {
// Check condition if present
if (breakpoint.getCondition() != null) {
// Evaluate condition (simplified)
// In real implementation, you'd parse and evaluate the condition
if (!evaluateCondition(breakpoint.getCondition())) {
return;
}
}
// Trigger breakpoint
breakpoint.hit();
}
}
private boolean evaluateCondition(String condition) {
// Simplified condition evaluation
// In practice, you'd use the language's expression evaluator
try {
// This is a placeholder - real implementation would be language-specific
return true; // Assume condition is true for demo
} catch (Exception e) {
return false;
}
}
}
public static class Breakpoint {
private final int id;
private final Source source;
private final int line;
private final String condition;
private boolean enabled;
private int hitCount;
private String logMessage;
public Breakpoint(int id, Source source, int line, String condition) {
this.id = id;
this.source = source;
this.line = line;
this.condition = condition;
this.enabled = true;
this.hitCount = 0;
}
public void hit() {
hitCount++;
// Notify breakpoint listeners
// In practice, this would pause execution and notify debugger
}
// Getters and setters
public int getId() { return id; }
public Source getSource() { return source; }
public int getLine() { return line; }
public String getCondition() { return condition; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public int getHitCount() { return hitCount; }
public String getLogMessage() { return logMessage; }
public void setLogMessage(String logMessage) { this.logMessage = logMessage; }
}
public interface BreakpointListener {
void onBreakpointSet(Breakpoint breakpoint);
void onBreakpointRemoved(Breakpoint breakpoint);
void onBreakpointHit(Breakpoint breakpoint);
}
}
Execution Tracer Instrument
package com.example.truffle.instrument;
import com.oracle.truffle.api.instrumentation.*;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Execution tracing and profiling instrument
*/
@TruffleInstrument.Registration(
id = "execution-tracer",
name = "Execution Tracer",
version = "1.0",
services = ExecutionTracer.class
)
public class ExecutionTracerInstrument extends TruffleInstrument {
private Env env;
private ExecutionTracer tracer;
@Override
protected void onCreate(Env env) {
this.env = env;
this.tracer = new ExecutionTracer(env);
env.registerService(tracer);
// Attach to all execution events
attachExecutionTracer();
}
private void attachExecutionTracer() {
SourceSectionFilter filter = SourceSectionFilter.newBuilder()
.tagIs(StandardTags.StatementTag.class, StandardTags.ExpressionTag.class)
.build();
env.getInstrumenter().attachExecutionEventFactory(filter,
new ExecutionTraceFactory());
}
private class ExecutionTraceFactory
implements EventBinding.ExecutionEventNodeFactory<ExecutionTraceContext> {
@Override
public ExecutionTraceContext create(EventContext context) {
return new ExecutionTraceContext(context, tracer);
}
}
public static class ExecutionTracer {
private final Env env;
private final Map<Source, SourceExecutionStats> sourceStats = new ConcurrentHashMap<>();
private final AtomicLong totalExecutions = new AtomicLong();
private final Set<ExecutionListener> listeners = new HashSet<>();
private boolean enabled = true;
private OutputStream traceOutput;
public ExecutionTracer(Env env) {
this.env = env;
}
public void recordExecution(SourceSection sourceSection, long durationNanos) {
if (!enabled) return;
totalExecutions.incrementAndGet();
Source source = sourceSection.getSource();
SourceExecutionStats stats = sourceStats.computeIfAbsent(source,
k -> new SourceExecutionStats(source));
stats.recordExecution(sourceSection, durationNanos);
// Notify listeners
ExecutionEvent event = new ExecutionEvent(sourceSection, durationNanos);
for (ExecutionListener listener : listeners) {
listener.onExecution(event);
}
// Write to trace output if configured
if (traceOutput != null) {
writeTraceEvent(event);
}
}
public void setTraceOutput(OutputStream output) {
this.traceOutput = output;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void addListener(ExecutionListener listener) {
listeners.add(listener);
}
public void removeListener(ExecutionListener listener) {
listeners.remove(listener);
}
public ExecutionReport generateReport() {
return new ExecutionReport(new HashMap<>(sourceStats), totalExecutions.get());
}
public void clearStats() {
sourceStats.clear();
totalExecutions.set(0);
}
private void writeTraceEvent(ExecutionEvent event) {
if (traceOutput != null) {
try {
String traceLine = String.format("%d,%s,%d,%d%n",
System.currentTimeMillis(),
event.getSourceSection().getSource().getName(),
event.getSourceSection().getStartLine(),
event.getDurationNanos());
traceOutput.write(traceLine.getBytes());
} catch (IOException e) {
// Ignore write errors
}
}
}
}
private static class ExecutionTraceContext extends EventContext {
private final ExecutionTracer tracer;
private long startTime;
ExecutionTraceContext(EventContext context, ExecutionTracer tracer) {
super(context.getInstrumentedNode(), context.getInstrumentedSourceSection());
this.tracer = tracer;
}
@TruffleInstrumentation.Insert(value = TruffleInstrumentation.Insert.Before.class)
public void onEnter() {
startTime = System.nanoTime();
}
@TruffleInstrumentation.Insert(value = TruffleInstrumentation.Insert.After.class)
public void onExit() {
long duration = System.nanoTime() - startTime;
tracer.recordExecution(getInstrumentedSourceSection(), duration);
}
}
public static class SourceExecutionStats {
private final Source source;
private final Map<SourceSection, LineExecutionStats> lineStats = new ConcurrentHashMap<>();
private long totalExecutions;
private long totalTimeNanos;
public SourceExecutionStats(Source source) {
this.source = source;
}
public void recordExecution(SourceSection sourceSection, long durationNanos) {
LineExecutionStats stats = lineStats.computeIfAbsent(sourceSection,
k -> new LineExecutionStats(sourceSection.getStartLine()));
stats.recordExecution(durationNanos);
totalExecutions++;
totalTimeNanos += durationNanos;
}
// Getters
public Source getSource() { return source; }
public Map<SourceSection, LineExecutionStats> getLineStats() { return lineStats; }
public long getTotalExecutions() { return totalExecutions; }
public long getTotalTimeNanos() { return totalTimeNanos; }
}
public static class LineExecutionStats {
private final int lineNumber;
private long executionCount;
private long totalTimeNanos;
private long minTimeNanos = Long.MAX_VALUE;
private long maxTimeNanos = Long.MIN_VALUE;
public LineExecutionStats(int lineNumber) {
this.lineNumber = lineNumber;
}
public void recordExecution(long durationNanos) {
executionCount++;
totalTimeNanos += durationNanos;
minTimeNanos = Math.min(minTimeNanos, durationNanos);
maxTimeNanos = Math.max(maxTimeNanos, durationNanos);
}
public double getAverageTimeNanos() {
return executionCount > 0 ? (double) totalTimeNanos / executionCount : 0;
}
// Getters
public int getLineNumber() { return lineNumber; }
public long getExecutionCount() { return executionCount; }
public long getTotalTimeNanos() { return totalTimeNanos; }
public long getMinTimeNanos() { return minTimeNanos; }
public long getMaxTimeNanos() { return maxTimeNanos; }
}
public static class ExecutionReport {
private final Map<Source, SourceExecutionStats> sourceStats;
private final long totalExecutions;
public ExecutionReport(Map<Source, SourceExecutionStats> sourceStats, long totalExecutions) {
this.sourceStats = sourceStats;
this.totalExecutions = totalExecutions;
}
public void printReport(PrintWriter writer) {
writer.printf("Execution Report - Total Executions: %,d%n", totalExecutions);
writer.println("==============================================");
for (SourceExecutionStats sourceStats : sourceStats.values()) {
writer.printf("Source: %s%n", sourceStats.getSource().getName());
writer.printf(" Total Executions: %,d%n", sourceStats.getTotalExecutions());
writer.printf(" Total Time: %,d ns%n", sourceStats.getTotalTimeNanos());
for (LineExecutionStats lineStats : sourceStats.getLineStats().values()) {
writer.printf(" Line %d: %,d executions, avg: %.2f ns%n",
lineStats.getLineNumber(),
lineStats.getExecutionCount(),
lineStats.getAverageTimeNanos());
}
writer.println();
}
}
}
public interface ExecutionListener {
void onExecution(ExecutionEvent event);
}
public static class ExecutionEvent {
private final SourceSection sourceSection;
private final long durationNanos;
public ExecutionEvent(SourceSection sourceSection, long durationNanos) {
this.sourceSection = sourceSection;
this.durationNanos = durationNanos;
}
// Getters
public SourceSection getSourceSection() { return sourceSection; }
public long getDurationNanos() { return durationNanos; }
}
}
Variable Inspector Instrument
package com.example.truffle.instrument;
import com.oracle.truffle.api.instrumentation.*;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.frame.*;
import com.oracle.truffle.api.source.SourceSection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Variable inspection and watch expression instrument
*/
@TruffleInstrument.Registration(
id = "variable-inspector",
name = "Variable Inspector",
version = "1.0",
services = VariableInspector.class
)
public class VariableInspectorInstrument extends TruffleInstrument {
private Env env;
private VariableInspector inspector;
@Override
protected void onCreate(Env env) {
this.env = env;
this.inspector = new VariableInspector(env);
env.registerService(inspector);
// Attach frame instrumentation
attachFrameListener();
}
private void attachFrameListener() {
env.getInstrumenter().attachContextsListener(new ContextsListener() {
@Override
public void onContextCreated(TruffleContext context) {
inspector.onContextCreated(context);
}
@Override
public void onContextClosed(TruffleContext context) {
inspector.onContextClosed(context);
}
}, true);
}
public static class VariableInspector {
private final Env env;
private final Map<TruffleContext, ContextInspector> contextInspectors = new ConcurrentHashMap<>();
private final Set<VariableChangeListener> listeners = new HashSet<>();
private final Set<WatchExpression> watchExpressions = new HashSet<>();
public VariableInspector(Env env) {
this.env = env;
}
public void onContextCreated(TruffleContext context) {
ContextInspector contextInspector = new ContextInspector(context);
contextInspectors.put(context, contextInspector);
// Instrument frames for this context
instrumentFrames(context);
}
public void onContextClosed(TruffleContext context) {
contextInspectors.remove(context);
}
private void instrumentFrames(TruffleContext context) {
// This would attach to frame events in the context
// Implementation depends on language-specific frame access
}
public List<Variable> getVariables(TruffleContext context, Frame frame) {
ContextInspector inspector = contextInspectors.get(context);
return inspector != null ? inspector.getVariables(frame) : new ArrayList<>();
}
public void setVariableValue(TruffleContext context, Frame frame, String name, Object value) {
ContextInspector inspector = contextInspectors.get(context);
if (inspector != null) {
inspector.setVariableValue(frame, name, value);
}
}
public void addWatchExpression(String expression) {
WatchExpression watch = new WatchExpression(expression);
watchExpressions.add(watch);
// Evaluate watch expression on relevant events
}
public void removeWatchExpression(String expression) {
watchExpressions.removeIf(w -> w.getExpression().equals(expression));
}
public List<WatchResult> evaluateWatchExpressions(TruffleContext context, Frame frame) {
List<WatchResult> results = new ArrayList<>();
for (WatchExpression watch : watchExpressions) {
try {
Object value = evaluateExpression(context, frame, watch.getExpression());
results.add(new WatchResult(watch.getExpression(), value, null));
} catch (Exception e) {
results.add(new WatchResult(watch.getExpression(), null, e.getMessage()));
}
}
return results;
}
private Object evaluateExpression(TruffleContext context, Frame frame, String expression) {
// This would use the language's expression evaluator
// Simplified implementation
return "evaluated: " + expression;
}
public void addVariableChangeListener(VariableChangeListener listener) {
listeners.add(listener);
}
public void removeVariableChangeListener(VariableChangeListener listener) {
listeners.remove(listener);
}
private void notifyVariableChanged(TruffleContext context, Frame frame,
String name, Object oldValue, Object newValue) {
VariableChangeEvent event = new VariableChangeEvent(context, frame, name, oldValue, newValue);
for (VariableChangeListener listener : listeners) {
listener.onVariableChanged(event);
}
}
}
private static class ContextInspector {
private final TruffleContext context;
private final Map<Frame, FrameSnapshot> frameSnapshots = new ConcurrentHashMap<>();
public ContextInspector(TruffleContext context) {
this.context = context;
}
public List<Variable> getVariables(Frame frame) {
FrameSnapshot snapshot = frameSnapshots.computeIfAbsent(frame,
k -> new FrameSnapshot(frame));
return snapshot.getVariables();
}
public void setVariableValue(Frame frame, String name, Object value) {
FrameSnapshot snapshot = frameSnapshots.get(frame);
if (snapshot != null) {
snapshot.setVariableValue(name, value);
}
}
public void updateFrame(Frame frame) {
FrameSnapshot snapshot = frameSnapshots.get(frame);
if (snapshot != null) {
snapshot.update();
}
}
}
private static class FrameSnapshot {
private final Frame frame;
private final Map<String, Variable> variables = new ConcurrentHashMap<>();
private long lastUpdate;
public FrameSnapshot(Frame frame) {
this.frame = frame;
update();
}
public void update() {
// Capture current frame state
// This is language-specific and would need adaptation
try {
FrameDescriptor descriptor = frame.getFrameDescriptor();
for (FrameSlot slot : descriptor.getSlots()) {
Object value = frame.getValue(slot);
String name = slot.getIdentifier().toString();
Variable oldVar = variables.get(name);
Object oldValue = oldVar != null ? oldVar.getValue() : null;
if (!Objects.equals(oldValue, value)) {
variables.put(name, new Variable(name, value, slot.getKind()));
// Notify about change
}
}
lastUpdate = System.currentTimeMillis();
} catch (Exception e) {
// Frame might be invalid
}
}
public List<Variable> getVariables() {
return new ArrayList<>(variables.values());
}
public void setVariableValue(String name, Object value) {
Variable variable = variables.get(name);
if (variable != null) {
// This would need language-specific frame manipulation
// variable.setValue(value);
}
}
}
public static class Variable {
private final String name;
private final Object value;
private final FrameSlotKind kind;
public Variable(String name, Object value, FrameSlotKind kind) {
this.name = name;
this.value = value;
this.kind = kind;
}
// Getters
public String getName() { return name; }
public Object getValue() { return value; }
public FrameSlotKind getKind() { return kind; }
public String getType() { return value != null ? value.getClass().getSimpleName() : "null"; }
}
public static class WatchExpression {
private final String expression;
private final String id;
public WatchExpression(String expression) {
this.expression = expression;
this.id = UUID.randomUUID().toString();
}
// Getters
public String getExpression() { return expression; }
public String getId() { return id; }
}
public static class WatchResult {
private final String expression;
private final Object value;
private final String error;
public WatchResult(String expression, Object value, String error) {
this.expression = expression;
this.value = value;
this.error = error;
}
// Getters
public String getExpression() { return expression; }
public Object getValue() { return value; }
public String getError() { return error; }
public boolean hasError() { return error != null; }
}
public interface VariableChangeListener {
void onVariableChanged(VariableChangeEvent event);
}
public static class VariableChangeEvent {
private final TruffleContext context;
private final Frame frame;
private final String variableName;
private final Object oldValue;
private final Object newValue;
public VariableChangeEvent(TruffleContext context, Frame frame,
String variableName, Object oldValue, Object newValue) {
this.context = context;
this.frame = frame;
this.variableName = variableName;
this.oldValue = oldValue;
this.newValue = newValue;
}
// Getters
public TruffleContext getContext() { return context; }
public Frame getFrame() { return frame; }
public String getVariableName() { return variableName; }
public Object getOldValue() { return oldValue; }
public Object getNewValue() { return newValue; }
}
}
Debug Adapter Protocol Implementation
package com.example.truffle.instrument;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.*;
/**
* Debug Adapter Protocol server for Truffle languages
*/
public class DebugAdapterServer extends WebSocketServer {
private final ObjectMapper mapper = new ObjectMapper();
private final Map<Integer, WebSocket> sessions = new ConcurrentHashMap<>();
private final DebugSessionManager sessionManager;
private final ExecutorService messageProcessor = Executors.newCachedThreadPool();
public DebugAdapterServer(int port) {
super(new InetSocketAddress(port));
this.sessionManager = new DebugSessionManager();
}
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
int sessionId = conn.hashCode();
sessions.put(sessionId, conn);
sessionManager.createSession(sessionId);
System.out.println("New debug session connected: " + sessionId);
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
int sessionId = conn.hashCode();
sessions.remove(sessionId);
sessionManager.closeSession(sessionId);
System.out.println("Debug session closed: " + sessionId);
}
@Override
public void onMessage(WebSocket conn, String message) {
messageProcessor.submit(() -> processMessage(conn, message));
}
@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("WebSocket error: " + ex.getMessage());
ex.printStackTrace();
}
@Override
public void onStart() {
System.out.println("Debug Adapter Server started on port: " + getPort());
}
private void processMessage(WebSocket conn, String message) {
try {
DAPMessage request = mapper.readValue(message, DAPMessage.class);
int sessionId = conn.hashCode();
DAPResponse response = sessionManager.handleRequest(sessionId, request);
if (response != null) {
String responseJson = mapper.writeValueAsString(response);
conn.send(responseJson);
}
} catch (Exception e) {
System.err.println("Error processing message: " + e.getMessage());
e.printStackTrace();
}
}
public void sendEvent(int sessionId, DAPEvent event) {
WebSocket conn = sessions.get(sessionId);
if (conn != null && conn.isOpen()) {
try {
String eventJson = mapper.writeValueAsString(event);
conn.send(eventJson);
} catch (Exception e) {
System.err.println("Error sending event: " + e.getMessage());
}
}
}
// DAP Message classes
public static class DAPMessage {
private String type;
private int seq;
private String command;
private Map<String, Object> arguments;
// Getters and setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public int getSeq() { return seq; }
public void setSeq(int seq) { this.seq = seq; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public Map<String, Object> getArguments() { return arguments; }
public void setArguments(Map<String, Object> arguments) { this.arguments = arguments; }
}
public static class DAPResponse {
private String type = "response";
private int request_seq;
private boolean success;
private String command;
private String message;
private Map<String, Object> body;
public DAPResponse(int requestSeq, String command, boolean success) {
this.request_seq = requestSeq;
this.command = command;
this.success = success;
}
// Getters and setters
public String getType() { return type; }
public int getRequest_seq() { return request_seq; }
public void setRequest_seq(int request_seq) { this.request_seq = request_seq; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Map<String, Object> getBody() { return body; }
public void setBody(Map<String, Object> body) { this.body = body; }
}
public static class DAPEvent {
private String type = "event";
private String event;
private Map<String, Object> body;
public DAPEvent(String event) {
this.event = event;
}
// Getters and setters
public String getType() { return type; }
public String getEvent() { return event; }
public void setEvent(String event) { this.event = event; }
public Map<String, Object> getBody() { return body; }
public void setBody(Map<String, Object> body) { this.body = body; }
}
private static class DebugSessionManager {
private final Map<Integer, DebugSession> sessions = new ConcurrentHashMap<>();
public void createSession(int sessionId) {
sessions.put(sessionId, new DebugSession(sessionId));
}
public void closeSession(int sessionId) {
DebugSession session = sessions.remove(sessionId);
if (session != null) {
session.close();
}
}
public DAPResponse handleRequest(int sessionId, DAPMessage request) {
DebugSession session = sessions.get(sessionId);
if (session != null) {
return session.handleRequest(request);
}
return createErrorResponse(request, "Session not found");
}
private DAPResponse createErrorResponse(DAPMessage request, String message) {
DAPResponse response = new DAPResponse(request.getSeq(), request.getCommand(), false);
response.setMessage(message);
return response;
}
}
private static class DebugSession {
private final int sessionId;
private final Map<String, Object> capabilities;
public DebugSession(int sessionId) {
this.sessionId = sessionId;
this.capabilities = initializeCapabilities();
}
public DAPResponse handleRequest(DAPMessage request) {
return switch (request.getCommand()) {
case "initialize" -> handleInitialize(request);
case "launch" -> handleLaunch(request);
case "attach" -> handleAttach(request);
case "setBreakpoints" -> handleSetBreakpoints(request);
case "setExceptionBreakpoints" -> handleSetExceptionBreakpoints(request);
case "configurationDone" -> handleConfigurationDone(request);
case "threads" -> handleThreads(request);
case "stackTrace" -> handleStackTrace(request);
case "scopes" -> handleScopes(request);
case "variables" -> handleVariables(request);
case "continue" -> handleContinue(request);
case "next" -> handleNext(request);
case "stepIn" -> handleStepIn(request);
case "stepOut" -> handleStepOut(request);
case "pause" -> handlePause(request);
case "disconnect" -> handleDisconnect(request);
default -> createErrorResponse(request, "Unsupported command: " + request.getCommand());
};
}
private DAPResponse handleInitialize(DAPMessage request) {
DAPResponse response = new DAPResponse(request.getSeq(), request.getCommand(), true);
response.setBody(capabilities);
return response;
}
private DAPResponse handleSetBreakpoints(DAPMessage request) {
// Implementation for setting breakpoints
DAPResponse response = new DAPResponse(request.getSeq(), request.getCommand(), true);
response.setBody(Map.of("breakpoints", new ArrayList<>()));
return response;
}
// Other handler methods would be implemented similarly
public void close() {
// Clean up session resources
}
private Map<String, Object> initializeCapabilities() {
Map<String, Object> caps = new HashMap<>();
caps.put("supportsConfigurationDoneRequest", true);
caps.put("supportsEvaluateForHovers", true);
caps.put("supportsStepBack", false);
caps.put("supportsSetVariable", true);
caps.put("supportsRestartFrame", false);
caps.put("supportsGotoTargetsRequest", false);
caps.put("supportsStepInTargetsRequest", false);
caps.put("supportsCompletionsRequest", true);
caps.put("supportsModulesRequest", false);
caps.put("supportsRestartRequest", false);
caps.put("supportsExceptionOptions", false);
caps.put("supportsValueFormattingOptions", false);
caps.put("supportsExceptionInfoRequest", true);
caps.put("supportTerminateDebuggee", false);
caps.put("supportsDelayedStackTraceLoading", false);
caps.put("supportsLoadedSourcesRequest", false);
caps.put("supportsLogPoints", true);
caps.put("supportsTerminateThreadsRequest", false);
caps.put("supportsSetExpression", false);
caps.put("supportsTerminateRequest", false);
caps.put("supportsDataBreakpoints", false);
caps.put("supportsReadMemoryRequest", false);
caps.put("supportsDisassembleRequest", false);
caps.put("supportsCancelRequest", false);
caps.put("supportsBreakpointLocationsRequest", true);
caps.put("supportsClipboardContext", true);
caps.put("supportsSteppingGranularity", false);
caps.put("supportsInstructionBreakpoints", false);
caps.put("supportsExceptionFilterOptions", false);
return caps;
}
private DAPResponse createErrorResponse(DAPMessage request, String message) {
DAPResponse response = new DAPResponse(request.getSeq(), request.getCommand(), false);
response.setMessage(message);
return response;
}
}
}
Usage Examples and Integration
package com.example.truffle.instrument;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.nodes.RootNode;
/**
* Example usage of Truffle debugging instruments
*/
public class DebuggingExamples {
/**
* Example of using the breakpoint instrument
*/
public static class BreakpointExample {
public void demonstrateBreakpoints() {
// In a real implementation, you'd get the instrument from the environment
// BreakpointInstrument.BreakpointManager breakpointManager =
// env.getInstrumenter().lookup(BreakpointInstrument.BreakpointManager.class);
// Set breakpoints
// breakpointManager.setBreakpoint(source, 10);
// breakpointManager.setBreakpoint(source, 20, "x > 5");
// Add breakpoint listener
// breakpointManager.addBreakpointListener(new BreakpointInstrument.BreakpointListener() {
// @Override
// public void onBreakpointSet(BreakpointInstrument.Breakpoint breakpoint) {
// System.out.println("Breakpoint set at line " + breakpoint.getLine());
// }
//
// @Override
// public void onBreakpointHit(BreakpointInstrument.Breakpoint breakpoint) {
// System.out.println("Breakpoint hit at line " + breakpoint.getLine());
// // Pause execution and start debugging session
// }
// });
}
}
/**
* Example of using the execution tracer
*/
public static class TracingExample {
public void demonstrateTracing() {
// ExecutionTracerInstrument.ExecutionTracer tracer =
// env.getInstrumenter().lookup(ExecutionTracerInstrument.ExecutionTracer.class);
// Configure tracing
// tracer.setEnabled(true);
// tracer.setTraceOutput(System.out);
// Add execution listener
// tracer.addListener(new ExecutionTracerInstrument.ExecutionListener() {
// @Override
// public void onExecution(ExecutionTracerInstrument.ExecutionEvent event) {
// SourceSection section = event.getSourceSection();
// System.out.printf("Executed %s:%d in %d ns%n",
// section.getSource().getName(),
// section.getStartLine(),
// event.getDurationNanos());
// }
// });
// Generate report after execution
// ExecutionTracerInstrument.ExecutionReport report = tracer.generateReport();
// report.printReport(new PrintWriter(System.out));
}
}
/**
* Example of using the variable inspector
*/
public static class VariableInspectionExample {
public void demonstrateVariableInspection() {
// VariableInspectorInstrument.VariableInspector inspector =
// env.getInstrumenter().lookup(VariableInspectorInstrument.VariableInspector.class);
// Add watch expressions
// inspector.addWatchExpression("x + y");
// inspector.addWatchExpression("calculateTotal()");
// Add variable change listener
// inspector.addVariableChangeListener(
// new VariableInspectorInstrument.VariableChangeListener() {
// @Override
// public void onVariableChanged(
// VariableInspectorInstrument.VariableChangeEvent event) {
// System.out.printf("Variable %s changed from %s to %s%n",
// event.getVariableName(),
// event.getOldValue(),
// event.getNewValue());
// }
// });
}
}
/**
* Integration with a Truffle language
*/
@TruffleLanguage.Registration(
id = "example-lang",
name = "Example Language",
version = "1.0",
characterMimeTypes = "application/x-example"
)
public static class ExampleLanguage extends TruffleLanguage<ExampleContext> {
@Override
protected ExampleContext createContext(Env env) {
// Initialize debugging instruments
initializeInstruments(env);
return new ExampleContext();
}
private void initializeInstruments(Env env) {
// The instruments are automatically loaded by Truffle
// We can access them through the environment
// Example: Set up breakpoints for commonly used files
// BreakpointInstrument.BreakpointManager breakpointManager =
// env.getInstrumenter().lookup(BreakpointInstrument.BreakpointManager.class);
//
// if (breakpointManager != null) {
// // Set initial breakpoints based on configuration
// breakpointManager.addBreakpointListener(new DebugEventListener());
// }
}
@Override
protected CallTarget parse(ParsingRequest request) throws Exception {
// Parse the source code and return call target
Source source = request.getSource();
RootNode rootNode = new ExampleRootNode(this, source);
return Truffle.getRuntime().createCallTarget(rootNode);
}
@Override
protected Object getLanguageGlobal(ExampleContext context) {
return context.getGlobalScope();
}
@Override
protected boolean isObjectOfLanguage(Object object) {
return object instanceof ExampleObject;
}
// Debug event listener for the language
private static class DebugEventListener
implements BreakpointInstrument.BreakpointListener {
@Override
public void onBreakpointSet(BreakpointInstrument.Breakpoint breakpoint) {
System.out.println("Language: Breakpoint set at " +
breakpoint.getSource().getName() + ":" + breakpoint.getLine());
}
@Override
public void onBreakpointRemoved(BreakpointInstrument.Breakpoint breakpoint) {
System.out.println("Language: Breakpoint removed from " +
breakpoint.getSource().getName() + ":" + breakpoint.getLine());
}
@Override
public void onBreakpointHit(BreakpointInstrument.Breakpoint breakpoint) {
System.out.println("Language: Breakpoint hit at " +
breakpoint.getSource().getName() + ":" + breakpoint.getLine());
// Implement language-specific breakpoint handling
}
}
}
// Supporting classes for the example language
public static class ExampleContext {
private final Map<String, Object> globalScope = new HashMap<>();
public Map<String, Object> getGlobalScope() {
return globalScope;
}
}
public static class ExampleRootNode extends RootNode {
private final Source source;
protected ExampleRootNode(ExampleLanguage language, Source source) {
super(language);
this.source = source;
}
@Override
public Object execute(VirtualFrame frame) {
// Execute the program
return "Program result";
}
}
public static class ExampleObject {
// Example language object
}
}
Best Practices
- Performance Considerations:
- Use
SourceSectionFilterto limit instrumentation to relevant code - Cache event bindings and reuse them when possible
- Use asynchronous processing for expensive operations
- Memory Management:
- Always dispose of event bindings when no longer needed
- Use weak references for long-lived listeners
- Clean up resources in
onContextClosed
- Error Handling:
- Handle exceptions gracefully in event listeners
- Use appropriate logging for debugging the debugger itself
- Validate inputs in public API methods
- Integration:
- Follow the Debug Adapter Protocol for IDE compatibility
- Provide comprehensive capabilities reporting
- Support both launch and attach debugging modes
Conclusion
The Truffle Instrument API provides a powerful foundation for building sophisticated debugging tools for Truffle languages. Key advantages include:
- Language-Agnostic: Works with any language implemented on Truffle
- High Performance: Minimal overhead through selective instrumentation
- Comprehensive Access: Full access to execution state, frames, and variables
- Protocol Support: Easy integration with standard debugging protocols like DAP
- Extensible Architecture: Easy to add new debugging features and instruments
By leveraging these instruments, you can create professional-grade debugging experiences that integrate seamlessly with existing IDEs and debugging tools while providing language-specific insights and capabilities.