Merge remote-tracking branch 'origin/GP-5895_ghidra_red_BreakpointsInTimeMargin--SQUASHED'

This commit is contained in:
Ryan Kurtz
2026-02-27 13:29:30 -05:00
8 changed files with 1454 additions and 1 deletions
@@ -8,3 +8,4 @@ DebuggerRegisterColumnFactory
DisassemblyInject
EmulatorFactory
LocationTrackingSpecFactory
TimeOverviewColorService
@@ -49,6 +49,14 @@ color.debugger.plugin.timeoverview.box.type.snap.removed = color.palette.lightgr
color.debugger.plugin.timeoverview.box.type.snap.changed = color.palette.lightgray
color.debugger.plugin.timeoverview.box.type.undefined = color.palette.black
color.debugger.plugin.breakpoint.timeline.grid = color.fg
color.debugger.plugin.breakpoint.timeline.selection = color.palette.lime
color.debugger.plugin.breakpoint.timeline.hover = color.palette.lime
color.debugger.plugin.breakpoint.timeline.current = color.palette.lime
color.debugger.plugin.breakpoint.timeline.type.read.memory = color.palette.lightcornflowerblue
color.debugger.plugin.breakpoint.timeline.type.write.memory = color.palette.lemonchiffon
color.debugger.plugin.breakpoint.timeline.type.instructions = color.palette.red
color.bg.debugger.plugin.objects.default = color.bg
color.fg.debugger.plugin.objects.default = color.fg
color.fg.debugger.plugin.objects.invisible = color.palette.lightgray
@@ -238,6 +246,13 @@ icon.debugger.select.registers = select-registers.png
icon.debugger.enable.edits = editbytes.gif
icon.debugger.disassemble = editbytes.gif // TODO this icon was missing 'disassemble.png'
icon.debugger.breakpoint.timeline.outline = tick.png
icon.debugger.breakpoint.timeline.no_outline = edit-delete.png
icon.debugger.breakpoint.timeline.grid = color_swatch.png
icon.debugger.breakpoint.timeline.single_column = StackFrame_Red.png
icon.debugger.breakpoint.timeline.zoom_in = zoom_in.png
icon.debugger.breakpoint.timeline.zoom_out = zoom_out.png
icon.debugger.breakpoint.timeline.zoom_out_max = zoom.png
icon.debugger.breakpoint.timeline.close_all_zoom_windows = delete.png
[Dark Defaults]
@@ -0,0 +1,473 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.breakpoint.timeline;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import javax.swing.JPanel;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.debug.gui.breakpoint.timeline.BreakpointTimelineProvider.BreakpointHitEvent;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.trace.model.breakpoint.TraceBreakpointKind;
import ghidra.util.ColorUtils;
import ghidra.util.Swing;
class BreakpointTimelinePanel extends JPanel {
private static class CachedIndex {
private final long startSnap;
private final long stopSnap;
private final List<BreakpointHitEvent> breakpointEvents;
Rectangle rect;
long index;
CachedIndex(long startSnap, long stopSnap, Rectangle rect, long index) {
this.startSnap = startSnap;
this.stopSnap = stopSnap;
this.rect = rect;
this.index = index;
breakpointEvents = new ArrayList<>();
}
void addEvent(BreakpointHitEvent event) {
breakpointEvents.add(event);
}
Color getColor() {
Color retColor = null;
final List<TraceBreakpointKind> uniqueBreakTypes =
breakpointEvents.stream().map(BreakpointHitEvent::breakType).distinct().toList();
final double blendRatio = 1.0d / uniqueBreakTypes.size();
for (final TraceBreakpointKind bt : uniqueBreakTypes) {
if (retColor == null) {
retColor = BreakpointTimelinePanel.BREAKTYPE_TO_COLOR.get(bt);
continue;
}
retColor = ColorUtils.blend(BreakpointTimelinePanel.BREAKTYPE_TO_COLOR.get(bt),
retColor, blendRatio);
}
return (retColor == null) ? BreakpointTimelinePanel.BG_COLOR : retColor;
}
long getIndex() {
return index;
}
BreakpointHitEvent getMainEvent() {
if (breakpointEvents.isEmpty()) {
return null;
}
return breakpointEvents.getFirst();
}
long getMainSnap() {
final BreakpointHitEvent mainEvent = getMainEvent();
return (mainEvent != null) ? mainEvent.snap() : startSnap;
}
Rectangle getRect() {
return rect;
}
long getStartSnap() {
return startSnap;
}
long getStopSnap() {
return stopSnap;
}
}
private static boolean singleColumn = false;
private static boolean showGridOutline = true;
private static GColor BG_COLOR = Colors.BACKGROUND;
private static GColor GRID_COLOR = Colors.FOREGROUND_DISABLED;
private static GColor SELECTION_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.selection");
private static GColor HOVER_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.hover");
private static GColor CURRENT_SNAP_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.current");
private static GColor INSTRUCTION_HIT_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.type.instructions");
private static GColor MEMORY_READ_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.type.read.memory");
private static GColor MEMORY_WRITE_COLOR =
new GColor("color.debugger.plugin.breakpoint.timeline.type.write.memory");
private static Map<TraceBreakpointKind, GColor> BREAKTYPE_TO_COLOR = Map.ofEntries(
Map.entry(TraceBreakpointKind.HW_EXECUTE, BreakpointTimelinePanel.INSTRUCTION_HIT_COLOR),
Map.entry(TraceBreakpointKind.SW_EXECUTE, BreakpointTimelinePanel.INSTRUCTION_HIT_COLOR),
Map.entry(TraceBreakpointKind.READ, BreakpointTimelinePanel.MEMORY_READ_COLOR),
Map.entry(TraceBreakpointKind.WRITE, BreakpointTimelinePanel.MEMORY_WRITE_COLOR));
private long defaultCellSize = 10;
private List<BreakpointHitEvent> events;
private final BreakpointTimelineProvider provider;
private long cellWidth = defaultCellSize;
private long cellHeight = defaultCellSize;
private long visibleStart;
private long visibleEnd;
private Point dragStart;
private Point dragEnd;
private Point mousePos;
private long gridWidth;
private long gridHeight;
private CachedIndex lastHighlightedIndex;
private CachedIndex startDragIndex;
private CachedIndex endDragIndex;
private final Map<Long, CachedIndex> cells = new HashMap<>();
BreakpointTimelinePanel(BreakpointTimelineProvider provider) {
events = null;
this.provider = provider;
setup();
}
private void calculateGridAndBuildCache() {
Swing.runIfSwingOrRunLater(this::doCalculateGridAndBuildCache);
}
private void click(Point p) {
final Optional<BreakpointTimelinePanel.CachedIndex> first =
cells.values().stream().filter(s -> s.getRect().contains(p)).findFirst();
if (first.isPresent()) {
getTraceManagerService().activateSnap(first.get().getMainSnap());
}
}
void decreaseDefaultCellSize() {
defaultCellSize = Math.max(cellHeight - 1, 1);
calculateGridAndBuildCache();
}
private void doCalculateGridAndBuildCache() {
if ((visibleStart == 0) && (visibleEnd == 0)) {
cells.clear();
repaint();
return;
}
final int width = getWidth();
final int height = getHeight();
if ((width <= 0) && (height <= 0)) {
return;
}
if (!BreakpointTimelinePanel.singleColumn) {
final long numCells = visibleEnd - visibleStart;
double xSideLength;
double ySideLength;
final double xPixelsPerCell = Math.ceil(Math.sqrt((numCells * width) / height));
if ((Math.floor((xPixelsPerCell * height) / width) * xPixelsPerCell) < numCells) {
xSideLength = (height / Math.ceil((height * xPixelsPerCell) / width));
}
else {
xSideLength = width / xPixelsPerCell;
}
final double yPixelsPerCell = Math.ceil(Math.sqrt((numCells * height) / width));
if ((Math.floor((yPixelsPerCell * width) / height) * yPixelsPerCell) < numCells) {
ySideLength = (width / Math.ceil((width * yPixelsPerCell) / height));
}
else {
ySideLength = height / yPixelsPerCell;
}
final long potentialSideLength = (long) Math.max(xSideLength, ySideLength);
cellWidth = Math.max(defaultCellSize, potentialSideLength);
cellHeight = cellWidth;
gridWidth = Math.max(width / cellWidth, 1);
}
else {
final long numCells = visibleEnd - visibleStart;
final long cellHeightOption = height / numCells;
cellHeight = Math.max(defaultCellSize, cellHeightOption);
cellWidth = width;
gridWidth = 1;
}
gridHeight = Math.max(height / cellHeight, 1);
cells.clear();
final long totalCells = gridWidth * gridHeight;
final long range = visibleEnd - visibleStart;
final long span = Math.max(((range + totalCells) - 1) / totalCells, 1);
long index = 0;
for (long i = 0; i < range; i += span) {
final long row = index / gridWidth;
final long col = index % gridWidth;
final long x = col * cellWidth;
final long y = row * cellHeight;
final long startSnap = i + visibleStart;
final long stopSnap = i + visibleStart + Math.min(span, range - i);
cells.put(index, new CachedIndex(startSnap, stopSnap,
new Rectangle((int) x, (int) y, (int) cellWidth, (int) cellHeight), index));
index++;
}
setPreferredSize(
new Dimension((int) (gridWidth * cellWidth), (int) (gridHeight * cellHeight)));
for (final BreakpointHitEvent event : events) {
if ((event.snap() >= visibleStart) && (event.snap() < visibleEnd)) {
final long cellIndex = (event.snap() - visibleStart) / span;
final CachedIndex curSpan = cells.get(cellIndex);
curSpan.addEvent(event);
}
}
repaint();
}
@Override
public String getToolTipText(MouseEvent event) {
final Optional<BreakpointTimelinePanel.CachedIndex> first =
cells.values().stream().filter(s -> s.getRect().contains(event.getPoint())).findFirst();
if (first.isPresent()) {
final CachedIndex index = first.get();
final BreakpointHitEvent mainEvent = index.getMainEvent();
if (mainEvent != null) {
return """
Snapshots %d - %d
Jumps to snapshot %d
Event type %s
From %s
""".formatted(index.getStartSnap(), index.getStopSnap() - 1,
index.getMainSnap(), mainEvent.breakType(), mainEvent.breakpointName());
}
return """
Snapshots %d - %d
Jumps to snapshot %d
""".formatted(index.getStartSnap(), index.getStopSnap() - 1,
index.getMainSnap());
}
return "";
}
private DebuggerTraceManagerService getTraceManagerService() {
return provider.getTool().getService(DebuggerTraceManagerService.class);
}
void increaseDefaultCellSize() {
defaultCellSize = cellHeight + 1;
calculateGridAndBuildCache();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if ((visibleStart == 0) && (visibleEnd == 0)) {
return;
}
if ((gridWidth == 0) || (gridHeight == 0)) {
calculateGridAndBuildCache();
}
final Graphics2D g2d = (Graphics2D) g;
for (final Long cellIndex : cells.keySet()) {
final CachedIndex curSpan = cells.get(cellIndex);
if ((startDragIndex != null) && (endDragIndex != null) &&
(cellIndex >= startDragIndex.getIndex()) &&
(cellIndex <= endDragIndex.getIndex())) {
g2d.setColor(BreakpointTimelinePanel.SELECTION_COLOR);
}
else if ((getTraceManagerService().getCurrentSnap() >= curSpan.getStartSnap()) &&
(getTraceManagerService().getCurrentSnap() < curSpan.getStopSnap())) {
g2d.setColor(BreakpointTimelinePanel.CURRENT_SNAP_COLOR);
}
else if ((mousePos != null) && curSpan.getRect().contains(mousePos)) {
g2d.setColor(BreakpointTimelinePanel.HOVER_COLOR);
}
else {
g2d.setColor(curSpan.getColor());
}
g2d.fillRect(curSpan.getRect().x, curSpan.getRect().y, curSpan.getRect().width,
curSpan.getRect().height);
if (BreakpointTimelinePanel.showGridOutline) {
g2d.setColor(BreakpointTimelinePanel.GRID_COLOR);
g2d.drawRect(curSpan.getRect().x, curSpan.getRect().y, curSpan.getRect().width,
curSpan.getRect().height);
}
}
}
void refresh() {
calculateGridAndBuildCache();
}
void setEventsAndVisibleRange(List<BreakpointHitEvent> events, long start, long stop) {
this.events = events;
visibleStart = start;
visibleEnd = stop;
calculateGridAndBuildCache();
}
void setMinimumDefaultCellSize() {
defaultCellSize = 1;
calculateGridAndBuildCache();
}
private void setup() {
setToolTipText("");
setBackground(BreakpointTimelinePanel.BG_COLOR);
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
calculateGridAndBuildCache();
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mouseExited(MouseEvent e) {
mousePos = null;
repaint();
}
@Override
public void mousePressed(MouseEvent e) {
dragStart = e.getPoint();
}
@Override
public void mouseReleased(MouseEvent e) {
if (dragEnd != null) {
zoom();
}
else {
click(dragStart);
}
dragStart = null;
dragEnd = null;
startDragIndex = null;
endDragIndex = null;
repaint();
}
});
addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
final int rotation = e.getWheelRotation();
if (rotation > 0) {
getTraceManagerService()
.activateSnap(getTraceManagerService().getCurrentSnap() + 1);
}
else {
getTraceManagerService()
.activateSnap(getTraceManagerService().getCurrentSnap() - 1);
}
repaint();
}
});
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
dragEnd = e.getPoint();
final Optional<BreakpointTimelinePanel.CachedIndex> start = cells.values()
.stream()
.filter(s -> s.getRect().contains(dragStart))
.findFirst();
if (start.isPresent()) {
final Optional<BreakpointTimelinePanel.CachedIndex> end = cells.values()
.stream()
.filter(s -> s.getRect().contains(dragEnd))
.findFirst();
if (end.isPresent()) {
if (start.get().getIndex() < end.get().getIndex()) {
startDragIndex = start.get();
endDragIndex = end.get();
}
else {
startDragIndex = end.get();
endDragIndex = start.get();
}
}
}
repaint();
}
@Override
public void mouseMoved(MouseEvent e) {
mousePos = e.getPoint();
final Optional<BreakpointTimelinePanel.CachedIndex> indexSpan =
cells.values().stream().filter(s -> s.getRect().contains(mousePos)).findFirst();
if (indexSpan.isPresent() && (lastHighlightedIndex != indexSpan.get())) {
lastHighlightedIndex = indexSpan.get();
repaint();
}
}
});
}
void toggleGridOrColumn() {
BreakpointTimelinePanel.singleColumn = !BreakpointTimelinePanel.singleColumn;
calculateGridAndBuildCache();
}
void toggleGridOutline() {
BreakpointTimelinePanel.showGridOutline = !BreakpointTimelinePanel.showGridOutline;
repaint();
}
private void zoom() {
final Optional<BreakpointTimelinePanel.CachedIndex> start =
cells.values().stream().filter(s -> s.getRect().contains(dragStart)).findFirst();
if (start.isPresent()) {
long startSnap = start.get().getStartSnap();
final Optional<BreakpointTimelinePanel.CachedIndex> stop =
cells.values().stream().filter(s -> s.getRect().contains(dragEnd)).findFirst();
if (stop.isPresent()) {
long endSnap = stop.get().getStopSnap();
if (startSnap > endSnap) {
final long temp = startSnap;
startSnap = endSnap;
endSnap = temp;
}
final String zoomName = "Timeline Zoom: %d - %d".formatted(startSnap, endSnap - 1);
provider.createZoomProvider(zoomName, startSnap, endSnap);
}
}
}
}
@@ -0,0 +1,147 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.breakpoint.timeline;
import java.util.*;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.core.debug.AbstractDebuggerPlugin;
import ghidra.app.plugin.core.debug.DebuggerPluginPackage;
import ghidra.app.plugin.core.debug.event.TraceActivatedPluginEvent;
import ghidra.app.plugin.core.debug.event.TraceClosedPluginEvent;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.trace.model.Trace;
@PluginInfo(
shortDescription = "Debugger breakpoint hit timeline",
description = "Timeline of all snapshots showing breakpoint hits",
category = PluginCategoryNames.DEBUGGER,
packageName = DebuggerPluginPackage.NAME,
status = PluginStatus.UNSTABLE,
servicesRequired = { DebuggerTraceManagerService.class, },
eventsConsumed = { TraceClosedPluginEvent.class, TraceActivatedPluginEvent.class, }
)
public class BreakpointTimelinePlugin extends AbstractDebuggerPlugin {
BreakpointTimelineProvider provider;
private Trace currentTrace;
private final Map<Trace, List<BreakpointTimelineProvider>> traceSpecificZoomProviders =
new HashMap<>();
public BreakpointTimelinePlugin(PluginTool tool) {
super(tool);
}
void createZoomProvider(String title, long start, long stop) {
traceSpecificZoomProviders.computeIfAbsent(currentTrace, k -> new ArrayList<>())
.add(new BreakpointTimelineProvider(provider, title, start, stop));
}
@Override
protected void dispose() {
for (final var providers : traceSpecificZoomProviders.values()) {
for (final var provider : providers) {
tool.removeComponentProvider(provider);
}
}
tool.removeComponentProvider(provider);
super.dispose();
}
private void hideZoomProviders(Trace t) {
final List<BreakpointTimelineProvider> zoomProviders = traceSpecificZoomProviders.get(t);
if (zoomProviders != null) {
for (final BreakpointTimelineProvider p : zoomProviders) {
tool.showComponentProvider(p, false);
}
}
}
@Override
protected void init() {
super.init();
provider = new BreakpointTimelineProvider(this);
}
@Override
public void processEvent(PluginEvent event) {
super.processEvent(event);
switch (event) {
case final TraceClosedPluginEvent evt -> {
final Trace t = evt.getTrace();
if (currentTrace == t) {
currentTrace = null;
provider.setTrace(null);
}
removeZoomProviders(t);
}
case final TraceActivatedPluginEvent evt -> {
final Trace t = evt.getActiveCoordinates().getTrace();
if (t == null) {
provider.setTrace(null);
}
else if (currentTrace != t) {
hideZoomProviders(currentTrace);
currentTrace = t;
provider.setTrace(currentTrace);
showZoomProviders(currentTrace);
}
else {
refreshAllProviders(null);
}
}
default -> {
}
}
}
void refreshAllProviders(BreakpointTimelineProvider currentProvider) {
final List<BreakpointTimelineProvider> zoomProviders =
traceSpecificZoomProviders.get(currentTrace);
if (zoomProviders != null) {
for (final BreakpointTimelineProvider p : zoomProviders) {
if (p != currentProvider) {
p.refresh();
}
}
}
if (provider != currentProvider) {
provider.refresh();
}
}
void removeZoomProviders(Trace t) {
final List<BreakpointTimelineProvider> zoomProviders = traceSpecificZoomProviders.remove(t);
if (zoomProviders != null) {
for (final BreakpointTimelineProvider p : zoomProviders) {
tool.removeComponentProvider(p);
}
}
}
private void showZoomProviders(Trace t) {
final List<BreakpointTimelineProvider> zoomProviders = traceSpecificZoomProviders.get(t);
if (zoomProviders != null) {
for (final BreakpointTimelineProvider p : zoomProviders) {
tool.showComponentProvider(p, true);
}
}
}
}
@@ -0,0 +1,316 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.breakpoint.timeline;
import java.awt.BorderLayout;
import java.util.*;
import javax.swing.JComponent;
import javax.swing.JPanel;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import generic.theme.GIcon;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.symbol.RefType;
import ghidra.trace.model.*;
import ghidra.trace.model.breakpoint.*;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.symbol.TraceReference;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.util.TraceEvents;
class BreakpointTimelineProvider extends ComponentProvider {
record BreakpointHitEvent(long snap, TraceBreakpointKind breakType, String breakpointName) {}
private class BreakpointTimeOverviewEventListener extends TraceDomainObjectListener {
public BreakpointTimeOverviewEventListener() {
listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointDeleted);
}
void breakpointChanged(TraceBreakpointLocation tb) {
refreshBreakpointHits();
breakpointTimelinePlugin.refreshAllProviders(null);
}
void breakpointDeleted(TraceBreakpointLocation tb) {
refreshBreakpointHits();
breakpointTimelinePlugin.refreshAllProviders(null);
}
}
private class CloseAllZoomWindowsAction extends DockingAction {
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.close_all_zoom_windows");
CloseAllZoomWindowsAction(ComponentProvider provider) {
super("Close all zoom windows", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(ICON, "1"));
}
@Override
public void actionPerformed(ActionContext context) {
breakpointTimelinePlugin.removeZoomProviders(currentTrace);
}
}
private class SmallestCellSizeAction extends DockingAction {
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.zoom_out_max");
SmallestCellSizeAction(ComponentProvider provider) {
super("Set default cell size to the smallest", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(ICON, "zoom"));
}
@Override
public void actionPerformed(ActionContext context) {
breakpointTimelinePanel.setMinimumDefaultCellSize();
}
}
private class ToggleGridAction extends DockingAction {
private final GIcon OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.outline");
private final GIcon NO_OUTLINE_ICON = new GIcon("icon.debugger.breakpoint.timeline.no_outline");
private boolean grid = true;
ToggleGridAction(ComponentProvider provider) {
super("Toggle Grid Outline", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(OUTLINE_ICON, "2"));
}
@Override
public void actionPerformed(ActionContext context) {
grid = !grid;
getToolBarData().setIcon(grid ? OUTLINE_ICON : NO_OUTLINE_ICON);
breakpointTimelinePanel.toggleGridOutline();
breakpointTimelinePlugin.refreshAllProviders(BreakpointTimelineProvider.this);
}
}
private class ToggleGridOrColumnAction extends DockingAction {
private final GIcon GRID_ICON = new GIcon("icon.debugger.breakpoint.timeline.grid");
private final GIcon SINGLE_COLUMN_ICON =
new GIcon("icon.debugger.breakpoint.timeline.single_column");
private boolean grid = true;
ToggleGridOrColumnAction(ComponentProvider provider) {
super("Toggle between grid and single column", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(GRID_ICON, "2"));
}
@Override
public void actionPerformed(ActionContext context) {
grid = !grid;
getToolBarData().setIcon(grid ? GRID_ICON : SINGLE_COLUMN_ICON);
breakpointTimelinePanel.toggleGridOrColumn();
breakpointTimelinePlugin.refreshAllProviders(BreakpointTimelineProvider.this);
}
}
private class ZoomInAction extends DockingAction {
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.zoom_in");
ZoomInAction(ComponentProvider provider) {
super("Increase cell size", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(ICON, "zoom"));
}
@Override
public void actionPerformed(ActionContext context) {
breakpointTimelinePanel.increaseDefaultCellSize();
}
}
private class ZoomOutAction extends DockingAction {
private final GIcon ICON = new GIcon("icon.debugger.breakpoint.timeline.zoom_out");
ZoomOutAction(ComponentProvider provider) {
super("Decrease cell size", provider.getOwner());
setEnabled(true);
setToolBarData(new ToolBarData(ICON, "zoom"));
}
@Override
public void actionPerformed(ActionContext context) {
breakpointTimelinePanel.decreaseDefaultCellSize();
}
}
private Trace currentTrace;
private final BreakpointTimelinePanel breakpointTimelinePanel;
private final JPanel wrapperPanel;
private final BreakpointTimelinePlugin breakpointTimelinePlugin;
private final BreakpointTimeOverviewEventListener listener =
new BreakpointTimeOverviewEventListener();
private List<BreakpointHitEvent> breakpointHits;
BreakpointTimelineProvider(BreakpointTimelinePlugin breakpointTimelinePlugin) {
this(breakpointTimelinePlugin, false);
}
BreakpointTimelineProvider(BreakpointTimelinePlugin breakpointTimelinePlugin,
boolean makeTransient) {
super(breakpointTimelinePlugin.getTool(), "Breakpoint Timeline",
breakpointTimelinePlugin.getName());
this.breakpointTimelinePlugin = breakpointTimelinePlugin;
wrapperPanel = new JPanel(new BorderLayout());
breakpointTimelinePanel = new BreakpointTimelinePanel(this);
breakpointTimelinePanel.setFocusable(true);
wrapperPanel.add(breakpointTimelinePanel, BorderLayout.CENTER);
breakpointHits = new ArrayList<>();
if (makeTransient) {
setTransient();
}
dockingTool.addComponentProvider(this, true);
createActions();
}
BreakpointTimelineProvider(BreakpointTimelineProvider provider, String title, long start,
long end) {
this(provider.breakpointTimelinePlugin, true);
currentTrace = provider.currentTrace;
setTitle(title);
breakpointHits = provider.breakpointHits;
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, start, end);
}
private void createActions() {
dockingTool.addLocalAction(this, new ToggleGridOrColumnAction(this));
dockingTool.addLocalAction(this, new ToggleGridAction(this));
dockingTool.addLocalAction(this, new ZoomInAction(this));
dockingTool.addLocalAction(this, new ZoomOutAction(this));
dockingTool.addLocalAction(this, new CloseAllZoomWindowsAction(this));
dockingTool.addLocalAction(this, new SmallestCellSizeAction(this));
}
void createZoomProvider(String title, long start, long stop) {
breakpointTimelinePlugin.createZoomProvider(title, start, stop);
}
private void findAndAddAllBreakpointHitsAtLocation(TraceBreakpointLocation breakpointLocation,
AddressRange range, String kind) {
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind, false)) {
switch (breakpointKind) {
case HW_EXECUTE, SW_EXECUTE -> findAndAddExecuteBreakpointHits(breakpointLocation,
range);
case READ, WRITE -> findAndAddMemoryBreakpointHits(breakpointLocation, range,
breakpointKind);
}
}
}
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range) {
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
for (final TraceObjectValue tov : intersecting) {
breakpointHits.add(new BreakpointHitEvent(tov.getMinSnap(),
TraceBreakpointKind.SW_EXECUTE, breakpointLocation.getName(tov.getMinSnap())));
}
}
private void findAndAddMemoryBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range, TraceBreakpointKind kind) {
for (final TraceReference reference : currentTrace.getReferenceManager()
.getReferencesToRange(Lifespan.ALL, range)) {
if ((reference.getReferenceType() == RefType.READ) &&
(kind == TraceBreakpointKind.READ)) {
breakpointHits.add(
new BreakpointHitEvent(reference.getStartSnap(), TraceBreakpointKind.READ,
breakpointLocation.getName(reference.getStartSnap())));
}
if ((reference.getReferenceType() == RefType.WRITE) &&
(kind == TraceBreakpointKind.WRITE)) {
breakpointHits.add(
new BreakpointHitEvent(reference.getStartSnap(), TraceBreakpointKind.WRITE,
breakpointLocation.getName(reference.getStartSnap())));
}
}
}
@Override
public JComponent getComponent() {
return wrapperPanel;
}
void refresh() {
breakpointTimelinePanel.refresh();
}
private void refreshBreakpointHits() {
breakpointHits.clear();
if (currentTrace == null) {
return;
}
// LATER: Check if breakpoint is enabled after GP-6441 is done
for (final TraceBreakpointLocation breakpointLocation : currentTrace.getBreakpointManager()
.getAllBreakpointLocations()) {
for (final TraceObjectValue traceVal : breakpointLocation.getObject()
.getValues(Lifespan.ALL, TraceBreakpointLocation.KEY_RANGE)) {
if (!(traceVal.getValue() instanceof final AddressRange range)) {
continue;
}
for (final TraceObjectValue specValue : breakpointLocation.getSpecification()
.getObject()
.getValues(Lifespan.ALL, TraceBreakpointSpec.KEY_KINDS)) {
if (!(specValue.getValue() instanceof final String kind)) {
continue;
}
findAndAddAllBreakpointHitsAtLocation(breakpointLocation, range, kind);
}
}
}
}
void setTrace(Trace trace) {
if (currentTrace != null) {
currentTrace.removeListener(listener);
}
currentTrace = trace;
refreshBreakpointHits();
if (currentTrace != null) {
breakpointTimelinePanel.setEventsAndVisibleRange(breakpointHits, 0,
currentTrace.getTimeManager().getMaxSnap());
currentTrace.addListener(listener);
}
else {
breakpointTimelinePanel.setEventsAndVisibleRange(null, 0, 0);
}
}
}
@@ -0,0 +1,79 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.timeoverview.breakpoint;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import docking.widgets.label.GLabel;
import ghidra.util.layout.PairLayout;
public class BreakTypeOverviewLegendPanel extends JPanel {
private static Dimension COLOR_SIZE = new Dimension(15, 15);
private BreakpointTimeOverviewColorService colorService;
public BreakTypeOverviewLegendPanel(BreakpointTimeOverviewColorService colorService) {
this.colorService = colorService;
setLayout(new PairLayout(4, 10));
setBorder(BorderFactory.createEmptyBorder(4, 20, 4, 30));
buildLegend();
}
/**
* Kick to repaint when the colors have changed.
*/
public void updateColors() {
repaint();
}
private void buildLegend() {
removeAll();
CellType[] values = CellType.values();
for (CellType breakType : values) {
JPanel panel = new ColorPanel(breakType);
add(panel);
add(new GLabel(breakType.getDescription()));
}
}
private class ColorPanel extends JPanel {
private CellType type;
ColorPanel(CellType type) {
this.type = type;
setPreferredSize(COLOR_SIZE);
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
Color newColor =
JColorChooser.showDialog(ColorPanel.this, "Select Color", getBackground());
colorService.setColor(type, newColor);
}
});
}
@Override
protected void paintComponent(Graphics g) {
setBackground(colorService.getColor(type));
super.paintComponent(g);
}
}
}
@@ -0,0 +1,361 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.timeoverview.breakpoint;
import java.awt.Color;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.math.BigInteger;
import java.util.*;
import javax.swing.SwingUtilities;
import docking.DialogComponentProvider;
import docking.action.DockingActionIf;
import docking.action.builder.ActionBuilder;
import generic.ULongSpan;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.debug.gui.timeoverview.*;
import ghidra.app.plugin.core.overview.OverviewColorLegendDialog;
import ghidra.app.services.DebuggerTraceManagerService;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.symbol.RefType;
import ghidra.trace.model.*;
import ghidra.trace.model.breakpoint.*;
import ghidra.trace.model.breakpoint.TraceBreakpointKind.TraceBreakpointKindSet;
import ghidra.trace.model.stack.TraceStackFrame;
import ghidra.trace.model.symbol.TraceReference;
import ghidra.trace.model.target.TraceObjectValue;
import ghidra.trace.util.TraceEvents;
import ghidra.util.ColorUtils;
import ghidra.util.HelpLocation;
record BreakpointEvent(long snap, CellType breakType) {}
public class BreakpointTimeOverviewColorService implements TimeOverviewColorService {
private class BreakpointTimeOverviewEventListener extends TraceDomainObjectListener {
public BreakpointTimeOverviewEventListener() {
listenFor(TraceEvents.BREAKPOINT_ADDED, this::breakpointAdded);
listenFor(TraceEvents.BREAKPOINT_CHANGED, this::breakpointChanged);
listenFor(TraceEvents.BREAKPOINT_DELETED, this::breakpointDeleted);
listenFor(TraceEvents.BREAKPOINT_LIFESPAN_CHANGED, this::breakpointLifespanChanged);
}
void breakpointAdded(TraceBreakpointLocation tb) {
// Empty because all new breakpoints fire a breakpoint change
// event
}
void breakpointChanged(TraceBreakpointLocation tb) {
SwingUtilities.invokeLater(() -> {
calculateBreakpointHits();
calculateHelperMaps();
});
}
void breakpointDeleted(TraceBreakpointLocation tb) {
SwingUtilities.invokeLater(() -> {
calculateBreakpointHits();
calculateHelperMaps();
});
}
void breakpointLifespanChanged(TraceBreakpointLocation tb) {
SwingUtilities.invokeLater(() -> {
calculateBreakpointHits();
calculateHelperMaps();
});
}
}
private static String OPTIONS_NAME = "Breakpoint Hit Timeline";
private PluginTool tool;
Trace currentTrace;
TimeOverviewColorComponent overviewComponent;
DialogComponentProvider legendDialog;
BreakTypeOverviewLegendPanel legendPanel;
TimeOverviewColorPlugin plugin;
private final BreakpointTimeOverviewEventListener eventListener =
new BreakpointTimeOverviewEventListener();
private final Map<CellType, Color> colorMap = new HashMap<>();
private final Map<Integer, Long> indexToSnap = new HashMap<>();
private final Map<Long, Color> snapToColor = new HashMap<>();
private final Map<Long, String> snapToTooltip = new HashMap<>();
private final Map<Long, ULongSpan> snapToRange = new HashMap<>();
List<BreakpointEvent> snapsWithBreakpointsHit = new ArrayList<>();
Lifespan bounds;
private DebuggerTraceManagerService debuggerTraceManagerService;
protected void calculateBreakpointHits() {
snapsWithBreakpointsHit.clear();
// LATER: Check if breakpoint is enabled after GP-6441 is done
for (final TraceBreakpointLocation breakpointLocation : getTrace().getBreakpointManager()
.getAllBreakpointLocations()) {
for (final TraceObjectValue traceVal : breakpointLocation.getObject()
.getValues(Lifespan.ALL, TraceBreakpointLocation.KEY_RANGE)) {
if (!(traceVal.getValue() instanceof final AddressRange range)) {
continue;
}
for (final TraceObjectValue specValue : breakpointLocation.getSpecification()
.getObject()
.getValues(Lifespan.ALL, TraceBreakpointSpec.KEY_KINDS)) {
if (!(specValue.getValue() instanceof final String kind)) {
continue;
}
findAndAddAllBreakpointHitsAtLocation(breakpointLocation, range, kind);
}
}
}
}
protected void calculateHelperMaps() {
indexToSnap.clear();
snapToColor.clear();
if (bounds == null) {
return;
}
final long splits = overviewComponent.getOverviewPixelCount();
final long snapSpanPerCell = Math.max((bounds.lmax() - bounds.lmin()) / splits, 1);
int cellIndex = 0;
for (long i = 0; i < bounds.lmax(); i += snapSpanPerCell) {
boolean hasBreakpointHit = false;
Color cellColor = Colors.BACKGROUND;
for (final BreakpointEvent event : snapsWithBreakpointsHit) {
if ((i <= event.snap()) && (event.snap() <= (i + snapSpanPerCell))) {
hasBreakpointHit = true;
indexToSnap.put(cellIndex, event.snap());
snapToTooltip.put(event.snap(), """
Snapshot %d
Break type %s""".formatted(event.snap(), event.breakType()));
snapToRange.put(event.snap(), ULongSpan.span(i, i + snapSpanPerCell));
cellColor =
ColorUtils.addColors(cellColor, event.breakType().getDefaultColor());
}
}
if (!hasBreakpointHit) {
// Just use the first snap of this span
indexToSnap.put(cellIndex, i);
snapToRange.put(i, ULongSpan.span(i, i + snapSpanPerCell));
snapToTooltip.put(i, "Snapshot %d".formatted(i));
}
snapToColor.put(indexToSnap.get(cellIndex), cellColor);
cellIndex++;
}
}
private void findAndAddAllBreakpointHitsAtLocation(TraceBreakpointLocation breakpointLocation,
AddressRange range, String kind) {
for (final TraceBreakpointKind breakpointKind : TraceBreakpointKindSet.decode(kind,
false)) {
switch (breakpointKind) {
case HW_EXECUTE, SW_EXECUTE -> findAndAddExecuteBreakpointHits(breakpointLocation,
range);
case READ, WRITE -> findAndAddMemoryBreakpointHits(breakpointLocation, range,
breakpointKind);
}
}
}
private void findAndAddExecuteBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range) {
final Collection<? extends TraceObjectValue> intersecting = currentTrace.getObjectManager()
.getValuesIntersecting(Lifespan.ALL, range, TraceStackFrame.KEY_PC);
for (final TraceObjectValue tov : intersecting) {
snapsWithBreakpointsHit
.add(new BreakpointEvent(tov.getMinSnap(), CellType.INSTRUCTION_EXECUTED));
}
}
private void findAndAddMemoryBreakpointHits(TraceBreakpointLocation breakpointLocation,
AddressRange range, TraceBreakpointKind kind) {
for (final TraceReference reference : currentTrace.getReferenceManager()
.getReferencesToRange(Lifespan.ALL, range)) {
if ((reference.getReferenceType() == RefType.READ) &&
(kind == TraceBreakpointKind.READ)) {
snapsWithBreakpointsHit
.add(new BreakpointEvent(reference.getStartSnap(), CellType.MEMORY_READ));
}
if ((reference.getReferenceType() == RefType.WRITE) &&
(kind == TraceBreakpointKind.WRITE)) {
snapsWithBreakpointsHit.add(
new BreakpointEvent(reference.getStartSnap(), CellType.MEMORY_WRITTEN));
}
}
}
@Override
public List<DockingActionIf> getActions() {
final List<DockingActionIf> actions = new ArrayList<>();
actions.add(new ActionBuilder("Show Legend", getName()).popupMenuPath("Show Legend")
.description("Show types and associated colors")
.helpLocation(getHelpLocation())
.enabledWhen(c -> c.getContextObject() == overviewComponent)
.onAction(c -> tool.showDialog(getLegendDialog()))
.build());
return actions;
}
@Override
public Lifespan getBounds() {
return bounds;
}
/**
* Returns the color associated with the given {@link CellType}
*
* @param breakType the span type for which to get a color.
* @return the color associated with the given {@link CellType}
*/
public Color getColor(CellType breakType) {
final Color color = colorMap.get(breakType);
if (color == null) {
colorMap.put(breakType, breakType.getDefaultColor());
}
return color;
}
@Override
public Color getColor(Long snap) {
final Color c = Colors.BACKGROUND;
if (snap != null) {
final ULongSpan range = snapToRange.get(snap);
final long currentSnap = debuggerTraceManagerService.getCurrentSnap();
if ((range != null) && (currentSnap > range.min()) && (currentSnap < range.max())) {
return Color.GREEN;
}
return snapToColor.get(snap);
}
return c;
}
@Override
public HelpLocation getHelpLocation() {
return null;
}
private DialogComponentProvider getLegendDialog() {
if (legendDialog == null) {
legendPanel = new BreakTypeOverviewLegendPanel(this);
legendDialog =
new OverviewColorLegendDialog("Overview Legend", legendPanel, getHelpLocation());
}
return legendDialog;
}
@Override
public String getName() {
return OPTIONS_NAME;
}
@Override
public Long getSnap(int pixelIndex) {
final BigInteger bigHeight = BigInteger.valueOf(overviewComponent.getOverviewPixelCount());
final BigInteger bigPixelIndex = BigInteger.valueOf(pixelIndex);
final BigInteger span = BigInteger.valueOf(indexToSnap.size());
final BigInteger offset = span.multiply(bigPixelIndex).divide(bigHeight);
return indexToSnap.get(offset.intValue());
}
@Override
public String getToolTipText(Long snap) {
return snapToTooltip.getOrDefault(snap, "Snapshot %d".formatted(snap));
}
@Override
public Trace getTrace() {
return currentTrace;
}
@Override
public void initialize(PluginTool pluginTool) {
tool = pluginTool;
debuggerTraceManagerService = tool.getService(DebuggerTraceManagerService.class);
}
@Override
public void setBounds(Lifespan bounds) {
if (currentTrace != null) {
this.bounds = Lifespan.span(0, getTrace().getTimeManager().getMaxSnap());
}
}
public void setColor(CellType type, Color newColor) {
final ToolOptions options = tool.getOptions(OPTIONS_NAME);
options.setColor(type.getDescription(), newColor);
}
@Override
public void setIndices(TreeSet<Long> set) {
// Empty because we do not change indices once a trace is set
}
@Override
public void setOverviewComponent(TimeOverviewColorComponent component) {
overviewComponent = component;
overviewComponent.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
SwingUtilities.invokeLater(() -> calculateHelperMaps());
}
});
}
@Override
public void setPlugin(TimeOverviewColorPlugin plugin) {
this.plugin = plugin;
}
@Override
public void setTrace(Trace trace) {
if ((trace != null) && (trace != currentTrace)) {
if (currentTrace != null) {
currentTrace.removeListener(eventListener);
}
currentTrace = trace;
currentTrace.addListener(eventListener);
SwingUtilities.invokeLater(this::calculateHelperMaps);
bounds = Lifespan.span(0, getTrace().getTimeManager().getMaxSnap());
}
}
}
@@ -0,0 +1,61 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.debug.gui.timeoverview.breakpoint;
import java.awt.Color;
import generic.theme.GColor;
/**
* An enum for the different types that are represented by unique colors by the
* {@link BreakpointTimeOverviewColorService}
*/
public enum CellType {
INSTRUCTION_EXECUTED("Instruction Executed", new GColor(
"color.debugger.plugin.timeoverview.box.type.instructions")),
MEMORY_READ("Memory Read", new GColor(
"color.debugger.plugin.timeoverview.box.type.read.memory")),
MEMORY_WRITTEN("Memory Written", new GColor(
"color.debugger.plugin.timeoverview.box.type.write.memory")),
CURRENT_LOCATION("Current Location", new GColor("color.palette.green"));
final private String description;
final private Color color;
CellType(String description, Color color) {
this.description = description;
this.color = color;
}
/**
* Returns a description of this enum value.
*
* @return a description of this enum value.
*/
public String getDescription() {
return description;
}
/**
* Returns a color of this enum value.
*
* @return a color of this enum value.
*/
public Color getDefaultColor() {
return color;
}
}