From e641b773b5fb5af6e4c586bce022ea14c35de251 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 5 Aug 2022 14:53:48 +0100 Subject: [PATCH] fix(gui): improve search dialog performance --- .../jadx/gui/jobs/BackgroundExecutor.java | 39 +++- .../main/java/jadx/gui/jobs/Cancelable.java | 8 + .../java/jadx/gui/jobs/IBackgroundTask.java | 2 +- .../main/java/jadx/gui/search/SearchTask.java | 51 +++-- .../gui/ui/dialog/CommonSearchDialog.java | 157 +++++-------- .../java/jadx/gui/ui/dialog/SearchDialog.java | 213 +++++++++--------- .../java/jadx/gui/ui/dialog/UsageDialog.java | 15 +- .../main/java/jadx/gui/utils/CacheObject.java | 13 ++ .../src/main/java/jadx/gui/utils/UiUtils.java | 8 + 9 files changed, 266 insertions(+), 240 deletions(-) diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index b1e9abfe3..8c1e0b779 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -145,8 +145,8 @@ public class BackgroundExecutor { task.onDone(this); // treat UI task operations as part of the task to not mix with others UiUtils.uiRunAndWait(() -> { - task.onFinish(this); progressPane.setVisible(false); + task.onFinish(this); }); } finally { taskComplete(id); @@ -190,13 +190,21 @@ public class BackgroundExecutor { performCancel(executor); return cancelStatus; } - updateProgress(executor); - k++; - Thread.sleep(k < 10 ? 200 : 1000); // faster update for short tasks - if (jobsCount == 1 && k == 3) { + if (k < 10) { + // faster update for short tasks + Thread.sleep(200); + if (k == 5) { + updateProgress(executor); + } + } else { + updateProgress(executor); + Thread.sleep(1000); + } + if (jobsCount == 1 && k == 5) { // small delay before show progress to reduce blinking on short tasks progressPane.changeVisibility(this, true); } + k++; } } catch (InterruptedException e) { LOG.debug("Task wait interrupted"); @@ -210,7 +218,7 @@ public class BackgroundExecutor { } private void updateProgress(ThreadPoolExecutor executor) { - Consumer onProgressListener = task.getOnProgressListener(); + Consumer onProgressListener = task.getProgressListener(); ITaskProgress taskProgress = task.getTaskProgress(); if (taskProgress == null) { setProgress(calcProgress(executor.getCompletedTaskCount(), jobsCount)); @@ -231,13 +239,16 @@ public class BackgroundExecutor { // force termination task.cancel(); executor.shutdown(); - if (executor.awaitTermination(2, TimeUnit.SECONDS)) { - LOG.debug("Task cancel complete"); - return; + int cancelTimeout = task.getCancelTimeoutMS(); + if (cancelTimeout != 0) { + if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) { + LOG.debug("Task cancel complete"); + return; + } } LOG.debug("Forcing tasks cancel"); executor.shutdownNow(); - boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS); + boolean complete = executor.awaitTermination(task.getShutdownTimeoutMS(), TimeUnit.MILLISECONDS); LOG.debug("Forced task cancel status: {}", complete ? "success" : "fail, still active: " + executor.getActiveCount()); } @@ -301,5 +312,13 @@ public class BackgroundExecutor { public long getTime() { return time; } + + @Override + public String toString() { + return "TaskWorker{status=" + status + + ", jobsCount=" + jobsCount + + ", jobsComplete=" + jobsComplete + + ", time=" + time + '}'; + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java b/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java index ec173b240..427ac6d7d 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/Cancelable.java @@ -4,4 +4,12 @@ public interface Cancelable { boolean isCanceled(); void cancel(); + + default int getCancelTimeoutMS() { + return 2000; + } + + default int getShutdownTimeoutMS() { + return 5000; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java index d1b70a0ad..bf26666b9 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IBackgroundTask.java @@ -54,7 +54,7 @@ public interface IBackgroundTask extends Cancelable { /** * Return progress notifications listener (use executor tick rate and thread) (Optional) */ - default @Nullable Consumer getOnProgressListener() { + default @Nullable Consumer getProgressListener() { return null; } } diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java index a7993a066..b8e517d07 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchTask.java @@ -4,8 +4,10 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; @@ -27,8 +29,8 @@ public class SearchTask extends CancelableBackgroundTask { private static final Logger LOG = LoggerFactory.getLogger(SearchTask.class); private final BackgroundExecutor backgroundExecutor; - private final Consumer onFinish; - private final Consumer results; + private final Consumer resultsListener; + private final BiConsumer onFinish; private final List jobs = new ArrayList<>(); private final TaskProgress taskProgress = new TaskProgress(); @@ -39,9 +41,9 @@ public class SearchTask extends CancelableBackgroundTask { private Consumer progressListener; - public SearchTask(MainWindow mainWindow, Consumer results, Consumer onFinish) { + public SearchTask(MainWindow mainWindow, Consumer results, BiConsumer onFinish) { this.backgroundExecutor = mainWindow.getBackgroundExecutor(); - this.results = results; + this.resultsListener = results; this.onFinish = onFinish; } @@ -55,8 +57,7 @@ public class SearchTask extends CancelableBackgroundTask { public synchronized void fetchResults() { if (future != null) { - cancel(); - waitTask(); + throw new IllegalStateException("Previous task not yet finished"); } resetCancel(); complete.set(false); @@ -70,9 +71,10 @@ public class SearchTask extends CancelableBackgroundTask { // ignore new results after cancel return true; } - this.results.accept(resultNode); + this.resultsListener.accept(resultNode); if (resultsLimit != 0 && resultsCount.incrementAndGet() >= resultsLimit) { cancel(); + complete.set(false); return true; } return false; @@ -81,14 +83,17 @@ public class SearchTask extends CancelableBackgroundTask { public synchronized void waitTask() { if (future != null) { try { - future.get(2, TimeUnit.SECONDS); + future.get(200, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + LOG.debug("Search task wait timeout"); } catch (Exception e) { - LOG.warn("Wait search task failed", e); + LOG.warn("Search task wait error", e); } finally { future.cancel(true); future = null; } } + } @Override @@ -101,18 +106,12 @@ public class SearchTask extends CancelableBackgroundTask { return jobs; } - public boolean isSearchComplete() { - return complete.get() && !isCanceled(); - } - @Override - public void onDone(ITaskInfo taskInfo) { - this.complete.set(true); - } - - @Override - public void onFinish(ITaskInfo status) { - this.onFinish.accept(status); + public void onFinish(ITaskInfo task) { + boolean complete = !isCanceled() + && task.getStatus() == TaskStatus.COMPLETE + && task.getJobsComplete() == task.getJobsCount(); + this.onFinish.accept(task, complete); } @Override @@ -131,7 +130,17 @@ public class SearchTask extends CancelableBackgroundTask { } @Override - public @Nullable Consumer getOnProgressListener() { + public @Nullable Consumer getProgressListener() { return this.progressListener; } + + @Override + public int getCancelTimeoutMS() { + return 0; + } + + @Override + public int getShutdownTimeoutMS() { + return 10; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java index 1d78ba716..a99535693 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommonSearchDialog.java @@ -14,11 +14,8 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; import java.util.List; -import java.util.Map; import javax.swing.AbstractAction; import javax.swing.BorderFactory; @@ -77,9 +74,7 @@ public abstract class CommonSearchDialog extends JFrame { protected JLabel warnLabel; protected ProgressPanel progressPane; - private String highlightText; - protected boolean highlightTextCaseInsensitive = false; - protected boolean highlightTextUseRegex = false; + private SearchContext highlightContext; public CommonSearchDialog(MainWindow mainWindow, String title) { this.mainWindow = mainWindow; @@ -88,7 +83,7 @@ public abstract class CommonSearchDialog extends JFrame { this.codeFont = mainWindow.getSettings().getFont(); this.windowTitle = title; UiUtils.setWindowIcons(this); - updateTitle(); + updateTitle(""); } protected abstract void openInit(); @@ -103,17 +98,24 @@ public abstract class CommonSearchDialog extends JFrame { } } - private void updateTitle() { - if (highlightText == null || highlightText.trim().isEmpty()) { + private void updateTitle(String searchText) { + if (searchText == null || searchText.isEmpty() || searchText.trim().isEmpty()) { setTitle(windowTitle); } else { - setTitle(windowTitle + ": " + highlightText); + setTitle(windowTitle + ": " + searchText); } } - public void setHighlightText(String highlightText) { - this.highlightText = highlightText; - updateTitle(); + public void updateHighlightContext(String text, boolean caseSensitive, boolean regexp) { + updateTitle(text); + highlightContext = new SearchContext(text); + highlightContext.setMatchCase(caseSensitive); + highlightContext.setRegularExpression(regexp); + highlightContext.setMarkAll(true); + } + + public void disableHighlight() { + highlightContext = null; } protected void registerInitOnOpen() { @@ -174,16 +176,16 @@ public abstract class CommonSearchDialog extends JFrame { openBtn.addActionListener(event -> openSelectedItem()); getRootPane().setDefaultButton(openBtn); - JPanel buttonPane = new JPanel(); - buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); - buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); - JCheckBox cbKeepOpen = new JCheckBox(NLS.str("search_dialog.keep_open")); cbKeepOpen.setSelected(mainWindow.getSettings().getKeepCommonDialogOpen()); cbKeepOpen.addActionListener(e -> { mainWindow.getSettings().setKeepCommonDialogOpen(cbKeepOpen.isSelected()); mainWindow.getSettings().sync(); }); + cbKeepOpen.setAlignmentY(Component.CENTER_ALIGNMENT); + + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); buttonPane.add(cbKeepOpen); buttonPane.add(Box.createRigidArea(new Dimension(15, 0))); buttonPane.add(progressPane); @@ -197,7 +199,7 @@ public abstract class CommonSearchDialog extends JFrame { protected JPanel initResultsTable() { ResultsTableCellRenderer renderer = new ResultsTableCellRenderer(); - resultsModel = new ResultsModel(renderer); + resultsModel = new ResultsModel(); resultsModel.addTableModelListener(e -> updateProgressLabel(false)); resultsTable = new ResultsTable(resultsModel, renderer); @@ -247,7 +249,6 @@ public abstract class CommonSearchDialog extends JFrame { warnLabel.setVisible(false); JScrollPane scroll = new JScrollPane(resultsTable, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED); - // scroll.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0)); JPanel resultsActionsPanel = new JPanel(); resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS)); @@ -261,7 +262,6 @@ public abstract class CommonSearchDialog extends JFrame { JPanel resultsPanel = new JPanel(); resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS)); - resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); resultsPanel.add(warnLabel, BorderLayout.PAGE_START); resultsPanel.add(scroll, BorderLayout.CENTER); resultsPanel.add(resultsActionsPanel, BorderLayout.PAGE_END); @@ -288,13 +288,12 @@ public abstract class CommonSearchDialog extends JFrame { protected static final class ResultsTable extends JTable { private static final long serialVersionUID = 3901184054736618969L; - private final transient ResultsTableCellRenderer renderer; private final transient ResultsModel model; public ResultsTable(ResultsModel resultsModel, ResultsTableCellRenderer renderer) { super(resultsModel); this.model = resultsModel; - this.renderer = renderer; + setRowHeight(renderer.getMaxRowHeight()); } public void initColumnWidth() { @@ -309,6 +308,11 @@ public abstract class CommonSearchDialog extends JFrame { public void updateTable() { UiUtils.uiThreadGuard(); + int rowCount = getRowCount(); + if (rowCount == 0) { + updateUI(); + return; + } long start = System.currentTimeMillis(); int width = getParent().getWidth(); TableColumn firstColumn = columnModel.getColumn(0); @@ -325,30 +329,6 @@ public abstract class CommonSearchDialog extends JFrame { } else { firstColumn.setPreferredWidth(width); } - int rowCount = getRowCount(); - int columnCount = getColumnCount(); - Map, Integer> heightByType = new HashMap<>(); - for (int row = 0; row < rowCount; row++) { - Object value = model.getValueAt(row, 0); - Class valueType = value.getClass(); - Integer cachedHeight = heightByType.get(valueType); - if (cachedHeight != null) { - setRowHeight(row, cachedHeight); - } else { - int height = 0; - for (int col = 0; col < columnCount; col++) { - Component comp = prepareRenderer(renderer, row, col); - if (comp == null) { - continue; - } - Dimension preferredSize = comp.getPreferredSize(); - int h = Math.max(comp.getHeight(), preferredSize.height); - height = Math.max(height, h); - } - heightByType.put(valueType, height); - setRowHeight(row, height); - } - } updateUI(); if (LOG.isDebugEnabled()) { LOG.debug("Update results table in {}ms, count: {}", System.currentTimeMillis() - start, rowCount); @@ -365,14 +345,9 @@ public abstract class CommonSearchDialog extends JFrame { private static final long serialVersionUID = -7821286846923903208L; private static final String[] COLUMN_NAMES = { NLS.str("search_dialog.col_node"), NLS.str("search_dialog.col_code") }; - private final transient List rows = Collections.synchronizedList(new ArrayList<>()); - private final transient ResultsTableCellRenderer renderer; + private final transient List rows = new ArrayList<>(); private transient boolean addDescColumn; - public ResultsModel(ResultsTableCellRenderer renderer) { - this.renderer = renderer; - } - public void addAll(Collection nodes) { rows.addAll(nodes); if (!addDescColumn) { @@ -388,7 +363,6 @@ public abstract class CommonSearchDialog extends JFrame { public void clear() { addDescColumn = false; rows.clear(); - renderer.clear(); } public boolean isAddDescColumn() { @@ -417,38 +391,36 @@ public abstract class CommonSearchDialog extends JFrame { } protected final class ResultsTableCellRenderer implements TableCellRenderer { - private final JLabel emptyLabel = new JLabel(); - private final Font font; + private final JLabel label; + private final RSyntaxTextArea codeArea; + private final JLabel emptyLabel; private final Color codeSelectedColor; private final Color codeBackground; - private final Map componentCache = new HashMap<>(); public ResultsTableCellRenderer() { - RSyntaxTextArea area = AbstractCodeArea.getDefaultArea(mainWindow); - this.font = area.getFont(); - this.codeSelectedColor = area.getSelectionColor(); - this.codeBackground = area.getBackground(); + codeArea = AbstractCodeArea.getDefaultArea(mainWindow); + codeArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); + codeArea.setRows(1); + codeBackground = codeArea.getBackground(); + codeSelectedColor = codeArea.getSelectionColor(); + label = new JLabel(); + label.setOpaque(true); + label.setFont(codeArea.getFont()); + label.setHorizontalAlignment(SwingConstants.LEFT); + emptyLabel = new JLabel(); + emptyLabel.setOpaque(true); } @Override public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, boolean hasFocus, int row, int column) { - Component comp = componentCache.computeIfAbsent(makeID(row, column), id -> { - if (obj instanceof JNode) { - return makeCell((JNode) obj, column); - } - return emptyLabel; - }); - updateSelection(table, comp, isSelected); + Component comp = makeCell((JNode) obj, column); + updateSelection(table, comp, column, isSelected); return comp; } - private int makeID(int row, int col) { - return row << 2 | (col & 0b11); - } - - private void updateSelection(JTable table, Component comp, boolean isSelected) { - if (comp instanceof RSyntaxTextArea) { + private void updateSelection(JTable table, Component comp, int column, boolean isSelected) { + if (column == 1) { if (isSelected) { comp.setBackground(codeSelectedColor); } else { @@ -467,39 +439,32 @@ public abstract class CommonSearchDialog extends JFrame { private Component makeCell(JNode node, int column) { if (column == 0) { - JLabel label = new JLabel(node.makeLongStringHtml(), node.getIcon(), SwingConstants.LEFT); - label.setFont(font); - label.setOpaque(true); + label.setText(node.makeLongStringHtml()); label.setToolTipText(label.getText()); + label.setIcon(node.getIcon()); return label; } if (!node.hasDescString()) { return emptyLabel; } - - RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(mainWindow); - textArea.setSyntaxEditingStyle(node.getSyntaxName()); + codeArea.setSyntaxEditingStyle(node.getSyntaxName()); String descStr = node.makeDescString(); - textArea.setText(descStr); - if (descStr.contains("\n")) { - textArea.setRows(textArea.getLineCount()); - } else { - textArea.setRows(1); - textArea.setColumns(descStr.length() + 1); + codeArea.setText(descStr); + codeArea.setColumns(descStr.length() + 1); + if (highlightContext != null) { + SearchEngine.markAll(codeArea, highlightContext); } - if (highlightText != null) { - SearchContext searchContext = new SearchContext(highlightText); - searchContext.setMatchCase(!highlightTextCaseInsensitive); - searchContext.setRegularExpression(highlightTextUseRegex); - searchContext.setMarkAll(true); - SearchEngine.markAll(textArea, searchContext); - } - textArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); - return textArea; + return codeArea; } - public void clear() { - componentCache.clear(); + public int getMaxRowHeight() { + label.setText("Text"); + codeArea.setText("Text"); + return Math.max(getCompHeight(label), getCompHeight(codeArea)); + } + + private int getCompHeight(Component comp) { + return Math.max(comp.getHeight(), comp.getPreferredSize().height); } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java index ac11a3265..377c95f48 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java @@ -2,7 +2,6 @@ package jadx.gui.ui.dialog; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.KeyAdapter; @@ -13,6 +12,8 @@ import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.swing.BorderFactory; @@ -25,21 +26,20 @@ import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.event.ChangeListener; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import hu.akarnokd.rxjava2.swing.SwingSchedulers; import io.reactivex.BackpressureStrategy; import io.reactivex.Emitter; import io.reactivex.Flowable; import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import jadx.api.JavaClass; import jadx.core.utils.ListUtils; +import jadx.gui.jobs.ITaskInfo; import jadx.gui.jobs.ITaskProgress; import jadx.gui.search.SearchSettings; import jadx.gui.search.SearchTask; @@ -58,6 +58,7 @@ import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; import jadx.gui.utils.layout.WrapLayout; +import jadx.gui.utils.ui.DocumentUpdateListener; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB; import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS; @@ -128,6 +129,11 @@ public class SearchDialog extends CommonSearchDialog { // temporal list for pending results private final List pendingResults = new ArrayList<>(); + /** + * Use single thread to do all background work, so additional synchronisation not needed + */ + private final Executor searchBackgroundExecutor = Executors.newSingleThreadExecutor(); + private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set additionalOptions) { super(mainWindow, NLS.str("menu.text_search")); this.searchPreset = preset; @@ -148,13 +154,10 @@ public class SearchDialog extends CommonSearchDialog { } resultsModel.clear(); removeActiveTabListener(); - if (searchTask != null) { - searchTask.cancel(); - mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), () -> { - stopSearchTask(); - unloadTempData(); - }); - } + searchBackgroundExecutor.execute(() -> { + stopSearchTask(); + unloadTempData(); + }); super.dispose(); } @@ -167,7 +170,7 @@ public class SearchDialog extends CommonSearchDialog { case TEXT: if (searchOptions.isEmpty()) { searchOptions.add(SearchOptions.CODE); - searchOptions.add(SearchOptions.IGNORE_CASE); + searchOptions.add(IGNORE_CASE); } break; @@ -205,16 +208,20 @@ public class SearchDialog extends CommonSearchDialog { searchField.setAlignmentX(LEFT_ALIGNMENT); TextStandardActions.attach(searchField); + JPanel searchLinePanel = new JPanel(); + searchLinePanel.setLayout(new BoxLayout(searchLinePanel, BoxLayout.LINE_AXIS)); + searchLinePanel.add(searchField); + searchLinePanel.setAlignmentX(LEFT_ALIGNMENT); + JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name")); findLabel.setAlignmentX(LEFT_ALIGNMENT); JPanel searchFieldPanel = new JPanel(); searchFieldPanel.setLayout(new BoxLayout(searchFieldPanel, BoxLayout.PAGE_AXIS)); - searchFieldPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); searchFieldPanel.setAlignmentX(LEFT_ALIGNMENT); searchFieldPanel.add(findLabel); searchFieldPanel.add(Box.createRigidArea(new Dimension(0, 5))); - searchFieldPanel.add(searchField); + searchFieldPanel.add(searchLinePanel); JPanel searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in"))); @@ -227,18 +234,17 @@ public class SearchDialog extends CommonSearchDialog { JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.options"))); - searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE)); - searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX)); + searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), IGNORE_CASE)); + searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), USE_REGEX)); searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.active_tab"), SearchOptions.ACTIVE_TAB)); - JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT)); + JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT, 0, 0)); optionsPanel.setAlignmentX(LEFT_ALIGNMENT); optionsPanel.add(searchInPanel); optionsPanel.add(searchOptions); JPanel searchPane = new JPanel(); searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS)); - searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); searchPane.add(searchFieldPanel); searchPane.add(Box.createRigidArea(new Dimension(0, 5))); searchPane.add(optionsPanel); @@ -247,10 +253,13 @@ public class SearchDialog extends CommonSearchDialog { JPanel resultsPanel = initResultsTable(); JPanel buttonPane = initButtonsPanel(); - Container contentPane = getContentPane(); - contentPane.add(searchPane, BorderLayout.PAGE_START); - contentPane.add(resultsPanel, BorderLayout.CENTER); - contentPane.add(buttonPane, BorderLayout.PAGE_END); + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BorderLayout(5, 5)); + contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + contentPanel.add(searchPane, BorderLayout.PAGE_START); + contentPanel.add(resultsPanel, BorderLayout.CENTER); + contentPanel.add(buttonPane, BorderLayout.PAGE_END); + getContentPane().add(contentPanel); searchField.addKeyListener(new KeyAdapter() { @Override @@ -269,11 +278,11 @@ public class SearchDialog extends CommonSearchDialog { protected void addCustomResultsActions(JPanel resultsActionsPanel) { loadAllButton = new JButton(NLS.str("search_dialog.load_all")); - loadAllButton.addActionListener(e -> loadAll()); + loadAllButton.addActionListener(e -> loadMoreResults(true)); loadAllButton.setEnabled(false); loadMoreButton = new JButton(NLS.str("search_dialog.load_more")); - loadMoreButton.addActionListener(e -> loadMore()); + loadMoreButton.addActionListener(e -> loadMoreResults(false)); loadMoreButton.setEnabled(false); resultsActionsPanel.add(loadAllButton); @@ -307,21 +316,36 @@ public class SearchDialog extends CommonSearchDialog { Flowable textChanges = onTextFieldChanges(searchField); Flowable searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable()); searchDisposable = searchEvents - .debounce(500, TimeUnit.MILLISECONDS) - .observeOn(SwingSchedulers.edt()) + .debounce(50, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.from(searchBackgroundExecutor)) .subscribe(this::search); } - @Nullable - private synchronized void search(String text) { - UiUtils.uiThreadGuard(); - resetSearch(); - if (text == null || options.isEmpty()) { + private void search(String text) { + UiUtils.notUiThreadGuard(); + stopSearchTask(); + UiUtils.uiRun(this::resetSearch); + searchTask = prepareSearch(text); + if (searchTask == null) { return; } + UiUtils.uiRunAndWait(() -> { + updateTableHighlight(); + prepareForSearch(); + }); + this.searchTask.setResultsLimit(50); + this.searchTask.setProgressListener(this::updateProgress); + this.searchTask.fetchResults(); + LOG.debug("Total search items count estimation: {}", this.searchTask.getTaskProgress().total()); + } + + private SearchTask prepareSearch(String text) { + if (text == null || options.isEmpty()) { + return null; + } // allow empty text for comments search if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) { - return; + return null; } LOG.debug("Building search for '{}', options: {}", text, options); boolean ignoreCase = options.contains(IGNORE_CASE); @@ -335,24 +359,16 @@ public class SearchDialog extends CommonSearchDialog { } else { searchField.setBackground(SEARCH_FIELD_ERROR_COLOR); resultsInfoLabel.setText(error); - return; + return null; } - searchTask = new SearchTask(mainWindow, this::addSearchResult, s -> searchComplete()); - if (!buildSearch(text, searchSettings)) { - return; + SearchTask newSearchTask = new SearchTask(mainWindow, this::addSearchResult, this::searchFinished); + if (!buildSearch(newSearchTask, text, searchSettings)) { + return null; } - - updateTableHighlight(); - startSearch(); - searchTask.setResultsLimit(100); - searchTask.setProgressListener(this::updateProgress); - searchTask.fetchResults(); - LOG.debug("Total search items count estimation: {}", searchTask.getTaskProgress().total()); + return newSearchTask; } - private boolean buildSearch(String text, SearchSettings searchSettings) { - Objects.requireNonNull(searchTask); - + private boolean buildSearch(SearchTask newSearchTask, String text, SearchSettings searchSettings) { List allClasses; if (options.contains(ACTIVE_TAB)) { JumpPosition currentPos = mainWindow.getTabbedPane().getCurrentPosition(); @@ -368,7 +384,7 @@ public class SearchDialog extends CommonSearchDialog { } // allow empty text for comments search if (text.isEmpty() && options.contains(SearchOptions.COMMENT)) { - searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); + newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); return true; } // using ordered execution for fast tasks @@ -384,84 +400,92 @@ public class SearchDialog extends CommonSearchDialog { } if (options.contains(CODE)) { if (allClasses.size() == 1) { - searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses)); + newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses)); } else { - List topClasses = ListUtils.filter(allClasses, c -> !c.isInner()); - for (List batch : mainWindow.getWrapper().buildDecompileBatches(topClasses)) { - searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch)); + List> batches = mainWindow.getCacheObject().getDecompileBatches(); + if (batches == null) { + List topClasses = ListUtils.filter(allClasses, c -> !c.isInner()); + batches = mainWindow.getWrapper().buildDecompileBatches(topClasses); + mainWindow.getCacheObject().setDecompileBatches(batches); + } + for (List batch : batches) { + newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch)); } } } if (options.contains(RESOURCE)) { - searchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings)); + newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings)); } if (options.contains(COMMENT)) { - searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); + newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings)); } merged.prepare(); - searchTask.addProviderJob(merged); + newSearchTask.addProviderJob(merged); return true; } - private synchronized void stopSearchTask() { + private void stopSearchTask() { + UiUtils.notUiThreadGuard(); if (searchTask != null) { searchTask.cancel(); + searchTask.waitTask(); searchTask = null; } } - private synchronized void loadMore() { - if (searchTask == null) { - return; - } - startSearch(); - searchTask.fetchResults(); + private void loadMoreResults(boolean all) { + searchBackgroundExecutor.execute(() -> { + if (searchTask == null) { + return; + } + searchTask.cancel(); + searchTask.waitTask(); + UiUtils.uiRunAndWait(this::prepareForSearch); + if (all) { + searchTask.setResultsLimit(0); + } + searchTask.fetchResults(); + }); } - private synchronized void loadAll() { - if (searchTask == null) { - return; - } - startSearch(); - searchTask.setResultsLimit(0); - searchTask.fetchResults(); - } - - private synchronized void resetSearch() { + private void resetSearch() { + UiUtils.uiThreadGuard(); resultsModel.clear(); - updateTable(); + resultsTable.updateTable(); + synchronized (pendingResults) { + pendingResults.clear(); + } progressPane.setVisible(false); warnLabel.setVisible(false); loadAllButton.setEnabled(false); loadMoreButton.setEnabled(false); - stopSearchTask(); } - private void startSearch() { + private void prepareForSearch() { showSearchState(); progressStartCommon(); } private void addSearchResult(JNode node) { synchronized (pendingResults) { + UiUtils.notUiThreadGuard(); pendingResults.add(node); } } private void updateTable() { synchronized (pendingResults) { + UiUtils.uiThreadGuard(); Collections.sort(pendingResults); resultsModel.addAll(pendingResults); pendingResults.clear(); + resultsTable.updateTable(); } - resultsTable.updateTable(); } private void updateTableHighlight() { String text = searchField.getText(); - setHighlightText(text); - highlightTextCaseInsensitive = options.contains(SearchOptions.IGNORE_CASE); - highlightTextUseRegex = options.contains(SearchOptions.USE_REGEX); + updateHighlightContext(text, !options.contains(IGNORE_CASE), options.contains(USE_REGEX)); cache.setLastSearch(text); cache.getLastSearchOptions().put(searchPreset, options); } @@ -473,17 +497,14 @@ public class SearchDialog extends CommonSearchDialog { }); } - private synchronized void searchComplete() { + private void searchFinished(ITaskInfo status, Boolean complete) { UiUtils.uiThreadGuard(); - LOG.debug("Search complete"); - updateTable(); - - boolean complete = searchTask == null || searchTask.isSearchComplete(); + LOG.debug("Search complete: {}, complete: {}", status, complete); loadAllButton.setEnabled(!complete); loadMoreButton.setEnabled(!complete); - updateProgressLabel(complete); - unloadTempData(); progressFinishedCommon(); + updateTable(); + updateProgressLabel(complete); } private void unloadTempData() { @@ -493,26 +514,8 @@ public class SearchDialog extends CommonSearchDialog { private static Flowable onTextFieldChanges(final JTextField textField) { return Flowable.create(emitter -> { - DocumentListener listener = new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - change(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - change(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - change(); - } - - public void change() { - emitter.onNext(textField.getText()); - } - }; + DocumentUpdateListener listener = new DocumentUpdateListener( + ev -> emitter.onNext(textField.getText())); textField.getDocument().addDocumentListener(listener); emitter.setDisposable(new Disposable() { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java index c4f51a99c..4febed33a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java @@ -1,7 +1,6 @@ package jadx.gui.ui.dialog; import java.awt.BorderLayout; -import java.awt.Container; import java.awt.FlowLayout; import java.awt.Font; import java.util.ArrayList; @@ -132,8 +131,7 @@ public class UsageDialog extends CommonSearchDialog { Collections.sort(usageList); resultsModel.addAll(usageList); - // TODO: highlight only needed node usage - setHighlightText(null); + updateHighlightContext(node.getName(), true, false); resultsTable.initColumnWidth(); resultsTable.updateTable(); updateProgressLabel(true); @@ -163,10 +161,13 @@ public class UsageDialog extends CommonSearchDialog { JPanel resultsPanel = initResultsTable(); JPanel buttonPane = initButtonsPanel(); - Container contentPane = getContentPane(); - contentPane.add(searchPane, BorderLayout.PAGE_START); - contentPane.add(resultsPanel, BorderLayout.CENTER); - contentPane.add(buttonPane, BorderLayout.PAGE_END); + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BorderLayout(5, 5)); + contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + contentPanel.add(searchPane, BorderLayout.PAGE_START); + contentPanel.add(resultsPanel, BorderLayout.CENTER); + contentPanel.add(buttonPane, BorderLayout.PAGE_END); + getContentPane().add(contentPanel); pack(); setSize(800, 500); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index ce31eab18..ba67bad74 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -1,11 +1,13 @@ package jadx.gui.utils; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; +import jadx.api.JavaClass; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JRoot; import jadx.gui.ui.dialog.SearchDialog; @@ -19,6 +21,8 @@ public class CacheObject { private JRoot jRoot; private JadxSettings settings; + private List> decompileBatches; + public CacheObject() { reset(); } @@ -29,6 +33,7 @@ public class CacheObject { lastSearch = null; jNodeCache = new JNodeCache(); lastSearchOptions = new HashMap<>(); + decompileBatches = null; } @Nullable @@ -63,4 +68,12 @@ public class CacheObject { public void setJRoot(JRoot jRoot) { this.jRoot = jRoot; } + + public @Nullable List> getDecompileBatches() { + return decompileBatches; + } + + public void setDecompileBatches(List> decompileBatches) { + this.decompileBatches = decompileBatches; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 5a727f055..6fbdef8bb 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -374,6 +374,8 @@ public class UiUtils { } try { SwingUtilities.invokeAndWait(runnable); + } catch (InterruptedException e) { + LOG.warn("UI thread interrupted", e); } catch (Exception e) { throw new RuntimeException(e); } @@ -385,6 +387,12 @@ public class UiUtils { } } + public static void notUiThreadGuard() { + if (SwingUtilities.isEventDispatchThread()) { + LOG.warn("Expect background thread, got: {}", Thread.currentThread(), new JadxRuntimeException()); + } + } + @TestOnly public static void debugTimer(int periodInSeconds, Runnable action) { if (!LOG.isDebugEnabled()) {