Merge remote-tracking branch

'origin/GP-6748_ghidragon_decompiler_slow_workarounds' (#5730)
This commit is contained in:
Ryan Kurtz
2026-04-29 06:55:31 -04:00
11 changed files with 306 additions and 78 deletions
@@ -41,6 +41,7 @@ icon.decompiler.action.provider.clone = icon.provider.clone
icon.decompiler.action.provider.unreachable = eliminateUnreachable.png
icon.decompiler.action.provider.readonly = readOnly.png
icon.decompiler.action.export = page_edit.png
icon.decompiler.action.display.lock = lock.gif
font.decompiler = font.monospaced
font.decompiler.pcode.dfg = font.graphdisplay.default
@@ -4009,7 +4009,7 @@
tokens (see <xref linkend="MouseActions"/>).
</para>
<sect2 id="CrossHighlighting">
<sect2 id="CrossHighlighting">
<title>Cross-Highlighting</title>
<para>
The main window maintains a map between the individual variable and operator tokens displayed in
@@ -4045,6 +4045,25 @@
</sect2>
</section>
<section id="LockDisplay">
<title>Disabling Auto Refresh</title>
<para>
For very large functions, the decompile window can take a significant amount of time to re-decompile
and update the display as changes are made to the program. This makes some actions frustrating,
such as renaming or retyping variables.
</para>
<para>
As a work around, you may use the lock toggle action
<imageobject>
<imagedata condition="noscaling" fileref="images/lock.gif" contentwidth="16px" contentdepth="16px"/>
<imagedata condition="withscaling" fileref="images/lock.gif" contentwidth="0.15in" contentdepth="0.15in"/>
</imageobject>&nbsp;.
When selected, this action will prevent the decompiler from auto refreshing for each change.
This may make the display appear to be broken since any changes will not appear to
take effect until the display is manually refreshed.
</para>
</section>
<section id="Snapshot">
<title>Snapshot Windows</title>
<para>
@@ -4085,6 +4104,18 @@
Double-clicking on specific tokens within the Snapshot window may also cause it to navigate
to a new location (see <xref linkend="MouseDouble"/>).
</para>
<anchor id="EventsOut"/>
<para>
Normally, snapshot windows are completely disconnected from the location and selection
events that synchronize the main components in the tool. To allow the snapshot window
to export its location and selection, select the
<imageobject>
<imagedata condition="noscaling" fileref="images/locationOut.gif" contentwidth="16px" contentdepth="16px"/>
<imagedata condition="withscaling" fileref="images/locationOut.gif" contentwidth="0.15in" contentdepth="0.15in"/>
</imageobject>&nbsp;
toolbar action.
</para>
</section>
<section id="UndefinedFunction">
@@ -101,7 +101,7 @@
tokens (see <a class="xref" href="DecompilerWindow.html#MouseActions" title="Mouse Actions">Mouse Actions</a>).
</p>
<div class="sect2">
<div class="sect2">
<div class="titlepage"><div><div><h3 class="title">
<a name="CrossHighlighting"></a>Cross-Highlighting</h3></div></div></div>
@@ -145,6 +145,23 @@
</div>
</div>
<div class="section">
<div class="titlepage"><div><div><h2 class="title" style="clear: both">
<a name="LockDisplay"></a>Disabling Auto Refresh</h2></div></div></div>
<p>
For very large functions, the decompile window can take a significant amount of time to re-decompile
and update the display as changes are made to the program. This makes using some actions
frustrating, such as renaming or retyping variables.
</p>
<p>
As a work around, you may use the lock toggle action
<img src="images/lock.gif" width="16" height="16">. When selected, this action will prevent
the decompiler from auto refreshing for each change. This may make the display appear to be
broken since any changes will not appear to take effect until the display is manually refreshed.
</p>
</div>
<div class="section">
<div class="titlepage"><div><div><h2 class="title" style="clear: both">
<a name="Snapshot"></a>Snapshot Windows</h2></div></div></div>
@@ -187,6 +204,14 @@
Double-clicking on specific tokens within the Snapshot window may also cause it to navigate
to a new location (see <a class="xref" href="DecompilerWindow.html#MouseDouble" title="Double-Click">Double-Click</a>).
</p>
<a name="EventsOut"></a>
<p>
Normally, snapshot windows are completely disconnected from the location and selection
events that synchronize the main components in the tool. To allow the snapshot window
to export its location and selection, select the
<img src="images/locationOut.gif" width="16" height="16"> toolbar action.
</p>
</div>
<div class="section">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 50 KiB

@@ -1388,6 +1388,23 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
fieldPanel.removeFocusListener(l);
}
/**
* {@return the bounds of the content area of this decompiler panel. This includes the main
* decompiler content panel and the line numbers panel}
*/
public Rectangle getViewContentBounds() {
// The bounds we want includes both the extent size of the main decompiler view + the
// area that displays the line numbers which is not inside the IndexedScrollPane. The width
// of the line numbers panel can be found by looking at the x position of the scroller as
// it is offset by the line number panel's width. We are also assuming there are no borders
// internal to the DecompilerPanel. If that changes, we would also need to factor in the
// insets.
Rectangle bounds = scroller.getBounds();
Dimension scrollerSize = scroller.getViewExtentSize();
int lineNumberWidth = bounds.x;
return new Rectangle(0, 0, scrollerSize.width + lineNumberWidth, scrollerSize.height);
}
private void buildPanels() {
removeAll();
add(buildLeftComponent(), BorderLayout.WEST);
@@ -207,13 +207,13 @@ public class DecompilePlugin extends Plugin {
}
void locationChanged(DecompilerProvider provider, ProgramLocation location) {
if (provider == connectedProvider) {
if (provider.shouldSendEvents()) {
firePluginEvent(new ProgramLocationPluginEvent(name, location, location.getProgram()));
}
}
void selectionChanged(DecompilerProvider provider, ProgramSelection selection) {
if (provider == connectedProvider) {
if (provider.shouldSendEvents()) {
firePluginEvent(new ProgramSelectionPluginEvent(name, selection, currentProgram));
}
}
@@ -15,17 +15,20 @@
*/
package ghidra.app.plugin.core.decompile;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.*;
import docking.*;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import docking.actions.KeyBindingUtils;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.ViewerPosition;
import generic.theme.GIcon;
@@ -76,6 +79,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
private static final Icon TOGGLE_READ_ONLY_DISABLED_ICON =
new MultiIconBuilder(TOGGLE_READ_ONLY_ICON).addCenteredIcon(SLASH_ICON).build();
private static final Icon LOCK_DISPLAY_ICON = new GIcon("icon.decompiler.action.display.lock");
private DockingAction pcodeGraphAction;
private DockingAction astGraphAction;
@@ -100,11 +104,16 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
private SwingUpdateManager redecompileUpdater;
private DecompilerProgramListener programListener;
private boolean lockDisplay;
// Follow-up work can be items that need to happen after a pending decompile is finished, such
// as updating highlights after a variable rename
private SwingUpdateManager followUpWorkUpdater;
private Queue<Callback> followUpWork = new ConcurrentLinkedQueue<>();
private OverlayMessagePainter overlayPainter = new OverlayMessagePainter();
private DockingAction refreshAction;
// only used by disconnected providers
private boolean allowOutgoingEvents = false;
private ServiceListener serviceListener = new ServiceListener() {
@@ -140,7 +149,14 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
// TODO move the hl controller into the panel
highlightController = new LocationClangHighlightController();
decompilerPanel.setHighlightController(highlightController);
decorationPanel = new DecoratorPanel(decompilerPanel, isConnected);
decorationPanel = new DecoratorPanel(decompilerPanel, isConnected) {
@Override
public void paint(Graphics g) {
super.paint(g);
overlayPainter.paintOverlay(g, decompilerPanel.getViewContentBounds());
}
};
if (!isConnected) {
setTransient();
@@ -333,7 +349,28 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
controller.setOptions(decompilerOptions);
if (currentLocation != null) {
controller.refreshDisplay(program, currentLocation, null);
if (lockDisplay) {
overlayPainter.setMessage(getOverlayRefreshMessage());
}
else {
controller.refreshDisplay(program, currentLocation, null);
overlayPainter.setMessage("");
}
}
}
private String getOverlayRefreshMessage() {
KeyStroke keyStroke = refreshAction.getKeyBinding();
if (keyStroke != null) {
String name = KeyBindingUtils.parseKeyStroke(keyStroke);
return name + " to refresh";
}
return "Refresh needed";
}
private void updateOverlayMessage() {
if (overlayPainter.isActive()) {
overlayPainter.setMessage(getOverlayRefreshMessage());
}
}
@@ -371,6 +408,8 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
options.getName().equals(GhidraOptions.CATEGORY_BROWSER_FIELDS)) {
doRefresh(true);
}
updateOverlayMessage();
}
//==================================================================================================
@@ -489,6 +528,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
*/
void refresh() {
controller.refreshDisplay(program, currentLocation, null);
overlayPainter.setMessage("");
}
/**
@@ -790,28 +830,35 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
private void createActions(boolean isConnected) {
String owner = plugin.getName();
new ToggleActionBuilder("Lock Display", owner)
.toolBarIcon(LOCK_DISPLAY_ICON)
.description("Lock display for auto-updates, only update on manual refresh")
.helpLocation(new HelpLocation(HelpTopics.DECOMPILER, "LockDisplay"))
.selected(false)
.onAction(c -> toggleDisplayLock())
.buildAndInstallLocal(this);
if (!isConnected) {
new ToggleActionBuilder("Decompiler Outgoing Events", owner)
.toolBarIcon(Icons.NAVIGATE_ON_OUTGOING_EVENT_ICON)
.description("Send location and selection events")
.helpLocation(new HelpLocation(HelpTopics.DECOMPILER, "EventsOut"))
.selected(false)
.onAction(c -> toggleOutgoingEvents())
.buildAndInstallLocal(this);
}
SelectAllAction selectAllAction =
new SelectAllAction(owner, controller.getDecompilerPanel());
DockingAction refreshAction = new DockingAction("Refresh", owner) {
@Override
public void actionPerformed(ActionContext context) {
refresh();
}
@Override
public boolean isEnabledForContext(ActionContext context) {
DecompileData decompileData = controller.getDecompileData();
if (decompileData == null) {
return false;
}
return decompileData.hasDecompileResults();
}
};
refreshAction.setToolBarData(new ToolBarData(REFRESH_ICON, "A" /* first on toolbar */));
refreshAction.setDescription("Push at any time to trigger a re-decompile");
refreshAction
.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ToolBarRedecompile")); // just use the default
refreshAction = new ActionBuilder("Refresh", owner)
.popupMenuPath("Refresh")
.popupMenuIcon(REFRESH_ICON)
.keyBinding("F5")
.helpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ToolBarRedecompile"))
.description("Re-decompile and update the display")
.onAction(c -> refresh())
.buildAndInstallLocal(this);
displayUnreachableCodeToggle = new ToggleDockingAction("Toggle Unreachable Code", owner) {
@Override
@@ -1120,6 +1167,8 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
findReferencesToAddressAction.getPopupMenuData().setParentMenuGroup(referencesParentGroup);
addLocalAction(findReferencesToAddressAction);
setGroupInfo(refreshAction, "comment6", subGroupPosition++);
//
// Options
//
@@ -1200,6 +1249,24 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
graphServiceAdded();
}
private void toggleOutgoingEvents() {
allowOutgoingEvents = !allowOutgoingEvents;
}
boolean shouldSendEvents() {
if (isConnected()) {
return true;
}
return allowOutgoingEvents;
}
private void toggleDisplayLock() {
lockDisplay = !lockDisplay;
if (!lockDisplay) {
refresh();
}
}
/**
* Sets the group and subgroup information for the given action.
*/
@@ -0,0 +1,82 @@
/* ###
* 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.decompile;
import java.awt.*;
import org.apache.commons.lang3.StringUtils;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.theme.Gui;
/**
* Class to overlay a message on the decompiler panel to indicate the display is stale and
* needs to be refreshed manually.
*/
class OverlayMessagePainter {
private static final int MARGIN = 10;
private static final String FONT_ID = "font.graph.component.message";
private final Color gradientColor = new GColor("color.bg.visualgraph.message");
private String message;
void setMessage(String message) {
this.message = message;
}
boolean isActive() {
return !StringUtils.isBlank(message);
}
void paintOverlay(Graphics g, Rectangle bounds) {
if (!isActive()) {
return;
}
Graphics2D g2 = (Graphics2D) g;
// this composite softens the text and color of the message
Composite originalComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SrcOver.getRule(), .60f));
// set up font
Font font = Gui.getFont(FONT_ID);
g.setFont(font);
Rectangle textBounds = font.getStringBounds(message, g2.getFontRenderContext()).getBounds();
int gh = textBounds.height * 3;
int gy = bounds.height - gh;
paintGradient(g2, 0, gy, bounds.width, gh);
// paint message
g2.setPaint(Palette.BLACK);
int textX = bounds.width - textBounds.width - MARGIN;
int textY = bounds.height - textBounds.height / 2; //text at bottom; account for baseline
g2.drawString(message, textX, textY);
g2.setComposite(originalComposite);
}
private void paintGradient(Graphics2D g2, int x, int y, int w, int h) {
Color[] colors = new Color[] { Color.WHITE, gradientColor };
float[] fractions = new float[] { 0.0f, .95f };
LinearGradientPaint gradiantPaint =
new LinearGradientPaint(new Point(x, y), new Point(x, y + h), fractions, colors);
g2.setPaint(gradiantPaint);
g2.fillRect(x, y, w, h);
}
}
@@ -1300,6 +1300,9 @@ public abstract class AbstractDockingTest extends AbstractGuiTest {
assertNotNull("Action cannot be null", action);
assertNotNull("Action context cannot be null", context);
boolean isValid = runSwing(() -> action.isValidContext(context));
assertTrue("Attempted to invoke action with invalid context", isValid);
runSwing(() -> {
action.isAddToPopup(context);
@@ -119,6 +119,10 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
return new Dimension(comp.getPreferredSize().width, indexMapper.getViewHeight());
}
public Dimension getViewExtentSize() {
return viewport.getExtentSize();
}
public void viewportStateChanged() {
Dimension extentSize = viewport.getExtentSize();
if (!extentSize.equals(visibleSize)) {
@@ -241,7 +245,24 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
@Override
public boolean getScrollableTracksViewportWidth() {
return false;
int prefWidth = comp.getPreferredSize().width;
int scrollPaneWidth = getScrollPaneWidth();
return scrollPaneWidth > prefWidth;
}
private int getScrollPaneWidth() {
Container myParent = getParent();
if (myParent == null) {
return 0;
}
if (myParent instanceof JViewport vp) {
return vp.getExtentSize().width;
}
Container grandParent = myParent.getParent();
if (grandParent == null) {
return 0;
}
return grandParent.getSize().width;
}
@Override
@@ -4,9 +4,9 @@
* 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.
@@ -24,9 +24,13 @@ import org.junit.Test;
import docking.ComponentProvider;
import docking.DockableComponent;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.support.FieldLocation;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.plugin.core.codebrowser.CodeViewerProvider;
import ghidra.app.plugin.core.datamgr.DataTypesProvider;
import ghidra.app.plugin.core.decompile.DecompilerProvider;
import ghidra.app.plugin.core.programtree.ViewManagerComponentProvider;
public class DecompilePluginScreenShots extends GhidraScreenShotGenerator {
@@ -115,52 +119,6 @@ public class DecompilePluginScreenShots extends GhidraScreenShotGenerator {
image = tf.getImage();
}
@Test
public void testBackwardSlice() {
TextFormatter tf = new TextFormatter(16, 500, 4, 5, 0);
TextFormatterContext hl = new TextFormatterContext(Palette.BLACK, Palette.YELLOW);
TextFormatterContext red = new TextFormatterContext(Palette.RED, Palette.WHITE);
TextFormatterContext blue = new TextFormatterContext(Palette.BLUE, Palette.WHITE);
TextFormatterContext green = new TextFormatterContext(Palette.GREEN, Palette.WHITE);
TextFormatterContext greenhl = new TextFormatterContext(Palette.GREEN, Palette.YELLOW);
TextFormatterContext cursorhl =
new TextFormatterContext(Palette.BLACK, Palette.YELLOW, Palette.RED);
tf.writeln(" |a| = |psParm2|->id;", hl, hl);
tf.writeln(" b = |max_alpha|(|psParm1|->next,|psParm1|->id);", red, hl, hl);
tf.writeln(" c = |max_beta|(psParm1->prev, |a|);", red, hl);
tf.writeln(" c = c + b;");
tf.writeln(" dStack8 = |0|;", green);
tf.writeln(" |while| (psParm1->count != dStack8 && (sdword)dStack8) {", blue);
tf.writeln(" |if| (c < (sdword)(dStack8 + b)) {", blue);
tf.writeln(" c = c + |a|;", hl);
tf.writeln(" }");
tf.writeln(" |else| {", blue);
tf.writeln(" |a| = |a| + |10|;", hl, hl, greenhl);
tf.writeln(" }");
tf.writeln(" dStack8 = dStack8 + |1|;", green);
tf.writeln(" }");
tf.writeln(" psParm1->count = |a| + c;", cursorhl);
tf.writeln(" |return|;", blue);
image = tf.getImage();
}
@Test
public void testStructnotapplied() {
Image listingImage = getListingImage();
Image decompImage = getDecompilerNoStructImage();
int listingWidth = listingImage.getWidth(null);
int decompWidth = decompImage.getWidth(null);
int height = Math.max(listingImage.getHeight(null), decompImage.getHeight(null));
BufferedImage combined = createEmptyImage(listingWidth + decompWidth, height);
Graphics2D g = combined.createGraphics();
g.drawImage(listingImage, 0, 0, null);
g.drawImage(decompImage, listingWidth, 0, null);
g.dispose();
image = combined;
}
public void testStructApplied() {
Image listingImage = getListingImage();
Image decompImage = getDecompilerStructAppliedImage();
@@ -177,14 +135,37 @@ public class DecompilePluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testEditFunctionSignature() {
DecompilerProvider provider = (DecompilerProvider) getProvider("Decompiler");
showProvider(provider.getClass());
goToListing(0x401040);
ComponentProvider provider = getProvider("Decompiler");
int line = 2; // function signature line
int charPos = 15; // function name
setDecompilerLocation(provider, line, charPos);
showProvider(provider.getClass());
waitForSwing();
performAction("Edit Function Signature", "DecompilePlugin", provider, false);
captureDialog();
}
private void setDecompilerLocation(DecompilerProvider provider, int line, int charPosition) {
DecompilerPanel panel = provider.getDecompilerPanel();
FieldPanel fp = panel.getFieldPanel();
FieldLocation loc = loc(line, charPosition);
// scroll to the field to make sure it has been built so that we can get its point
fp.scrollTo(loc);
Point p = fp.getPointForLocation(loc);
click(fp, p, 1, true);
waitForSwing();
}
private FieldLocation loc(int lineNumber, int col) {
FieldLocation loc = new FieldLocation(lineNumber - 1, 0, 0, col);
return loc;
}
private Image getListingImage() {
Font font = new Font("Monospaced", Font.PLAIN, 12);