
com.oracle.truffle.api.debug.LineBreakpointFactory Maven / Gradle / Ivy
/*
* Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.truffle.api.debug;
import static com.oracle.truffle.api.debug.Breakpoint.State.DISABLED;
import static com.oracle.truffle.api.debug.Breakpoint.State.DISABLED_UNRESOLVED;
import static com.oracle.truffle.api.debug.Breakpoint.State.DISPOSED;
import static com.oracle.truffle.api.debug.Breakpoint.State.ENABLED;
import static com.oracle.truffle.api.debug.Breakpoint.State.ENABLED_UNRESOLVED;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.oracle.truffle.api.Assumption;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.debug.Debugger.BreakpointCallback;
import com.oracle.truffle.api.debug.Debugger.WarningLog;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrument.EvalInstrumentListener;
import com.oracle.truffle.api.instrument.Instrumenter;
import com.oracle.truffle.api.instrument.Probe;
import com.oracle.truffle.api.instrument.ProbeInstrument;
import com.oracle.truffle.api.instrument.StandardSyntaxTag;
import com.oracle.truffle.api.instrument.SyntaxTag;
import com.oracle.truffle.api.instrument.impl.DefaultProbeListener;
import com.oracle.truffle.api.instrument.impl.DefaultStandardInstrumentListener;
import com.oracle.truffle.api.nodes.InvalidAssumptionException;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.LineLocation;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.api.utilities.CyclicAssumption;
//TODO (mlvdv) some common functionality could be factored out of this and TagBreakpointSupport
/**
* Support class for creating and managing all existing ordinary (user visible) line breakpoints.
*
* Notes:
*
* - Line breakpoints can only be set at nodes tagged as {@link StandardSyntaxTag#STATEMENT}.
* - A newly created breakpoint looks for probes matching the location, attaches to them if found
* by installing an {@link ProbeInstrument} that calls back to the breakpoint.
* - When Truffle "splits" or otherwise copies an AST, any attached {@link ProbeInstrument} will
* be copied along with the rest of the AST and will call back to the same breakpoint.
* - When notification is received of a new Node being tagged as a statement, and if a
* breakpoint's line location matches the Probe's line location, then the breakpoint will attach a
* new Instrument at the probe to activate the breakpoint at that location.
* - A breakpoint may have multiple Instruments deployed, one attached to each Probe that matches
* the breakpoint's line location; this might happen when a source is reloaded.
*
*
*/
final class LineBreakpointFactory {
private static final boolean TRACE = false;
private static final PrintStream OUT = System.out;
private static final String BREAKPOINT_NAME = "Line Breakpoints";
@TruffleBoundary
private static void trace(String format, Object... args) {
if (TRACE) {
OUT.println(String.format("%s: %s", BREAKPOINT_NAME, String.format(format, args)));
}
}
private static final Comparator> BREAKPOINT_COMPARATOR = new Comparator>() {
@Override
public int compare(Entry entry1, Entry entry2) {
final LineLocation line1 = entry1.getKey();
final LineLocation line2 = entry2.getKey();
final int nameOrder = line1.getSource().getShortName().compareTo(line2.getSource().getShortName());
if (nameOrder != 0) {
return nameOrder;
}
return Integer.compare(line1.getLineNumber(), line2.getLineNumber());
}
};
private final Debugger debugger;
private final BreakpointCallback breakpointCallback;
private final WarningLog warningLog;
/**
* Map: Source lines ==> attached breakpoints. There may be no more than one line breakpoint
* associated with a line.
*/
private final Map lineToBreakpoint = new HashMap<>();
/**
* A map of {@link LineLocation} to a collection of {@link Probe}s. This list must be
* initialized and filled prior to being used by this class.
*/
private final LineToProbesMap lineToProbesMap;
/**
* Globally suspends all line breakpoint activity when {@code false}, ignoring whether
* individual breakpoints are enabled.
*/
@CompilationFinal private boolean breakpointsActive = true;
private final CyclicAssumption breakpointsActiveUnchanged = new CyclicAssumption(BREAKPOINT_NAME + " globally active");
LineBreakpointFactory(Debugger debugger, BreakpointCallback breakpointCallback, final WarningLog warningLog) {
this.debugger = debugger;
this.breakpointCallback = breakpointCallback;
this.warningLog = warningLog;
final Instrumenter instrumenter = debugger.getInstrumenter();
this.lineToProbesMap = new LineToProbesMap();
instrumenter.install(lineToProbesMap);
instrumenter.addProbeListener(new DefaultProbeListener() {
@Override
public void probeTaggedAs(Probe probe, SyntaxTag tag, Object tagValue) {
if (tag == StandardSyntaxTag.STATEMENT) {
final SourceSection sourceSection = probe.getProbedSourceSection();
if (sourceSection != null) {
final LineLocation lineLocation = sourceSection.getLineLocation();
if (lineLocation != null) {
// A Probe with line location tagged STATEMENT we haven't seen before.
final LineBreakpointImpl breakpoint = lineToBreakpoint.get(lineLocation);
if (breakpoint != null) {
breakpoint.attach(probe);
}
}
}
}
}
});
}
/**
* Globally enables line breakpoint activity; all breakpoints are ignored when set to
* {@code false}. When set to {@code true}, the enabled/disabled status of each breakpoint
* determines whether it will trigger when flow of execution reaches it.
*
* @param breakpointsActive
*/
void setActive(boolean breakpointsActive) {
if (this.breakpointsActive != breakpointsActive) {
breakpointsActiveUnchanged.invalidate();
this.breakpointsActive = breakpointsActive;
}
}
/**
* Gets all current line breakpoints,regardless of status; sorted and modification safe.
*/
List getAll() {
ArrayList> entries = new ArrayList<>(lineToBreakpoint.entrySet());
Collections.sort(entries, BREAKPOINT_COMPARATOR);
final ArrayList breakpoints = new ArrayList<>(entries.size());
for (Entry entry : entries) {
breakpoints.add(entry.getValue());
}
return breakpoints;
}
/**
* Creates a new line breakpoint if one doesn't already exist. If one does exist, then resets
* the ignore count.
*
* @param lineLocation where to set the breakpoint
* @param ignoreCount number of initial hits before the breakpoint starts causing breaks.
* @param oneShot whether the breakpoint should dispose itself after one hit
* @return a possibly new breakpoint
* @throws IOException if a breakpoint already exists at the location and the ignore count is
* the same
*/
LineBreakpoint create(int ignoreCount, LineLocation lineLocation, boolean oneShot) throws IOException {
LineBreakpointImpl breakpoint = lineToBreakpoint.get(lineLocation);
if (breakpoint == null) {
breakpoint = new LineBreakpointImpl(ignoreCount, lineLocation, oneShot);
if (TRACE) {
trace("NEW " + breakpoint.getShortDescription());
}
lineToBreakpoint.put(lineLocation, breakpoint);
for (Probe probe : lineToProbesMap.findProbes(lineLocation)) {
if (probe.isTaggedAs(StandardSyntaxTag.STATEMENT)) {
breakpoint.attach(probe);
break;
}
}
} else {
if (ignoreCount == breakpoint.getIgnoreCount()) {
throw new IOException(BREAKPOINT_NAME + " already set at line " + lineLocation);
}
breakpoint.setIgnoreCount(ignoreCount);
if (TRACE) {
trace("CHANGED ignoreCount %s", breakpoint.getShortDescription());
}
}
return breakpoint;
}
/**
* Returns the {@link LineBreakpoint} for a given line. There should only ever be one breakpoint
* per line.
*
* @param lineLocation The {@link LineLocation} to get the breakpoint for.
* @return The breakpoint for the given line.
*/
LineBreakpoint get(LineLocation lineLocation) {
return lineToBreakpoint.get(lineLocation);
}
/**
* Removes the associated instrumentation for all one-shot breakpoints only.
*/
void disposeOneShots() {
List breakpoints = new ArrayList<>(lineToBreakpoint.values());
for (LineBreakpointImpl breakpoint : breakpoints) {
if (breakpoint.isOneShot()) {
breakpoint.dispose();
}
}
}
/**
* Removes all knowledge of a breakpoint, presumed disposed.
*/
private void forget(LineBreakpointImpl breakpoint) {
lineToBreakpoint.remove(breakpoint.getLineLocation());
}
/**
* Concrete representation of a line breakpoint, implemented by attaching an instrument to a
* probe at the designated source location.
*/
private final class LineBreakpointImpl extends LineBreakpoint implements EvalInstrumentListener {
private static final String SHOULD_NOT_HAPPEN = "LineBreakpointImpl: should not happen";
private final LineLocation lineLocation;
// Cached assumption that the global status of line breakpoint activity has not changed.
private Assumption breakpointsActiveAssumption;
// Whether this breakpoint is enable/disabled
@CompilationFinal private boolean isEnabled;
private Assumption enabledUnchangedAssumption;
private Source conditionSource;
/**
* The instrument(s) that this breakpoint currently has attached to a {@link Probe}:
* {@code null} if not attached.
*/
private List instruments = new ArrayList<>();
public LineBreakpointImpl(int ignoreCount, LineLocation lineLocation, boolean oneShot) {
super(ENABLED_UNRESOLVED, ignoreCount, oneShot);
this.lineLocation = lineLocation;
this.breakpointsActiveAssumption = LineBreakpointFactory.this.breakpointsActiveUnchanged.getAssumption();
this.isEnabled = true;
this.enabledUnchangedAssumption = Truffle.getRuntime().createAssumption(BREAKPOINT_NAME + " enabled state unchanged");
}
@Override
public boolean isEnabled() {
return isEnabled;
}
@Override
public void setEnabled(boolean enabled) {
if (enabled != isEnabled) {
switch (getState()) {
case ENABLED:
assert !enabled : SHOULD_NOT_HAPPEN;
doSetEnabled(false);
changeState(DISABLED);
break;
case ENABLED_UNRESOLVED:
assert !enabled : SHOULD_NOT_HAPPEN;
doSetEnabled(false);
changeState(DISABLED_UNRESOLVED);
break;
case DISABLED:
assert enabled : SHOULD_NOT_HAPPEN;
doSetEnabled(true);
changeState(ENABLED);
break;
case DISABLED_UNRESOLVED:
assert enabled : SHOULD_NOT_HAPPEN;
doSetEnabled(true);
changeState(DISABLED_UNRESOLVED);
break;
case DISPOSED:
assert false : "breakpoint disposed";
break;
default:
assert false : SHOULD_NOT_HAPPEN;
break;
}
}
}
@Override
public void setCondition(String expr) throws IOException {
if (this.conditionSource != null || expr != null) {
// De-instrument the Probes instrumented by this breakpoint
final ArrayList probes = new ArrayList<>();
for (ProbeInstrument instrument : instruments) {
probes.add(instrument.getProbe());
instrument.dispose();
}
instruments.clear();
this.conditionSource = Source.fromText(expr, "breakpoint condition from text: " + expr);
// Re-instrument the probes previously instrumented
for (Probe probe : probes) {
attach(probe);
}
}
}
@Override
public Source getCondition() {
return conditionSource;
}
@TruffleBoundary
@Override
public void dispose() {
if (getState() != DISPOSED) {
for (ProbeInstrument instrument : instruments) {
instrument.dispose();
}
changeState(DISPOSED);
LineBreakpointFactory.this.forget(this);
}
}
private void attach(Probe newProbe) {
if (getState() == DISPOSED) {
throw new IllegalStateException("Attempt to attach a disposed " + BREAKPOINT_NAME);
}
ProbeInstrument newInstrument = null;
final Instrumenter instrumenter = debugger.getInstrumenter();
if (conditionSource == null) {
newInstrument = instrumenter.attach(newProbe, new UnconditionalLineBreakInstrumentListener(), BREAKPOINT_NAME);
} else {
newInstrument = instrumenter.attach(newProbe, conditionSource, this, BREAKPOINT_NAME, null);
}
instruments.add(newInstrument);
changeState(isEnabled ? ENABLED : DISABLED);
}
private void doSetEnabled(boolean enabled) {
if (this.isEnabled != enabled) {
enabledUnchangedAssumption.invalidate();
this.isEnabled = enabled;
}
}
@TruffleBoundary
private String getShortDescription() {
return BREAKPOINT_NAME + "@" + getLineLocation().getShortDescription();
}
private void changeState(State after) {
if (TRACE) {
trace("STATE %s-->%s %s", getState().getName(), after.getName(), getShortDescription());
}
setState(after);
}
private void doBreak(Node node, VirtualFrame vFrame) {
if (incrHitCountCheckIgnore()) {
breakpointCallback.haltedAt(node, vFrame.materialize(), BREAKPOINT_NAME);
}
}
/**
* Receives notification from the attached instrument that execution is about to enter node
* where the breakpoint is set. Designed so that when in the fast path, there is either an
* unconditional "halt" call to the debugger or nothing.
*/
private void nodeEnter(Node astNode, VirtualFrame vFrame) {
// Deopt if the global active/inactive flag has changed
try {
this.breakpointsActiveAssumption.check();
} catch (InvalidAssumptionException ex) {
this.breakpointsActiveAssumption = LineBreakpointFactory.this.breakpointsActiveUnchanged.getAssumption();
}
// Deopt if the enabled/disabled state of this breakpoint has changed
try {
this.enabledUnchangedAssumption.check();
} catch (InvalidAssumptionException ex) {
this.enabledUnchangedAssumption = Truffle.getRuntime().createAssumption("LineBreakpoint enabled state unchanged");
}
if (LineBreakpointFactory.this.breakpointsActive && this.isEnabled) {
if (isOneShot()) {
dispose();
}
LineBreakpointImpl.this.doBreak(astNode, vFrame);
}
}
public void onExecution(Node node, VirtualFrame vFrame, Object result) {
if (result instanceof Boolean) {
final boolean condition = (Boolean) result;
if (TRACE) {
trace("breakpoint condition = %b %s", condition, getShortDescription());
}
if (condition) {
nodeEnter(node, vFrame);
}
} else {
onFailure(node, vFrame, new RuntimeException("breakpint failure = non-boolean condition " + result.toString()));
}
}
public void onFailure(Node node, VirtualFrame vFrame, Exception ex) {
addExceptionWarning(ex);
if (TRACE) {
trace("breakpoint failure = %s %s", ex, getShortDescription());
}
// Take the breakpoint if evaluation fails.
nodeEnter(node, vFrame);
}
@TruffleBoundary
private void addExceptionWarning(Exception ex) {
warningLog.addWarning(String.format("Exception in %s: %s", getShortDescription(), ex.getMessage()));
}
@Override
public String getLocationDescription() {
return "Line: " + lineLocation.getShortDescription();
}
@Override
public LineLocation getLineLocation() {
return lineLocation;
}
private final class UnconditionalLineBreakInstrumentListener extends DefaultStandardInstrumentListener {
@Override
public void onEnter(Probe probe, Node node, VirtualFrame vFrame) {
LineBreakpointImpl.this.nodeEnter(node, vFrame);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy