fix(gui): prevent UI stuck on class load (#2259)

This commit is contained in:
Skylot 2024-08-31 22:30:18 +01:00
parent f5307636ef
commit 2df69bbfb4
No known key found for this signature in database
GPG Key ID: 47A4975761262B6A
9 changed files with 233 additions and 29 deletions

View File

@ -1,5 +1,6 @@
package jadx.gui.jobs;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@ -14,7 +15,15 @@ import jadx.core.utils.tasks.TaskExecutor;
public class SimpleTask implements IBackgroundTask {
private final String title;
private final List<Runnable> jobs;
private final Consumer<TaskStatus> onFinish;
private final @Nullable Consumer<TaskStatus> onFinish;
public SimpleTask(String title, Runnable run) {
this(title, Collections.singletonList(run), null);
}
public SimpleTask(String title, Runnable run, Runnable onFinish) {
this(title, Collections.singletonList(run), s -> onFinish.run());
}
public SimpleTask(String title, List<Runnable> jobs) {
this(title, jobs, null);
@ -31,6 +40,14 @@ public class SimpleTask implements IBackgroundTask {
return title;
}
public List<Runnable> getJobs() {
return jobs;
}
public @Nullable Consumer<TaskStatus> getOnFinish() {
return onFinish;
}
@Override
public ITaskExecutor scheduleTasks() {
TaskExecutor executor = new TaskExecutor();

View File

@ -22,6 +22,7 @@ import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ICodeNode;
import jadx.gui.jobs.SimpleTask;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
@ -57,14 +58,22 @@ public class JClass extends JLoadableNode implements JRenameNode {
return cls;
}
@Override
public boolean canRename() {
return !cls.getClassNode().contains(AFlag.DONT_RENAME);
}
@Override
public void loadNode() {
getRootClass().load();
}
@Override
public boolean canRename() {
return !cls.getClassNode().contains(AFlag.DONT_RENAME);
public SimpleTask getLoadTask() {
JClass rootClass = getRootClass();
return new SimpleTask(NLS.str("progress.decompile"),
() -> rootClass.getCls().decompile(),
rootClass::load);
}
private synchronized void load() {

View File

@ -1,7 +1,11 @@
package jadx.gui.treemodel;
import jadx.gui.jobs.IBackgroundTask;
public abstract class JLoadableNode extends JNode {
private static final long serialVersionUID = 5543590584166374958L;
public abstract void loadNode();
public abstract IBackgroundTask getLoadTask();
}

View File

@ -20,6 +20,8 @@ import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.jobs.IBackgroundTask;
import jadx.gui.jobs.SimpleTask;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.BinaryContentPanel;
import jadx.gui.ui.codearea.CodeContentPanel;
@ -98,6 +100,11 @@ public class JResource extends JLoadableNode {
update();
}
@Override
public synchronized IBackgroundTask getLoadTask() {
return new SimpleTask(NLS.str("progress.load"), this::getCodeInfo, this::update);
}
@Override
public String getName() {
return name;

View File

@ -155,13 +155,12 @@ import jadx.gui.utils.LafManager;
import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.dbg.UIWatchDog;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
import jadx.gui.utils.shortcut.ShortcutsController;
import jadx.gui.utils.ui.ActionHandler;
import jadx.gui.utils.ui.NodeLabel;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
public class MainWindow extends JFrame {
private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class);
@ -455,11 +454,11 @@ public class MainWindow extends JFrame {
}
public void open(Path path) {
open(Collections.singletonList(path), EMPTY_RUNNABLE);
open(Collections.singletonList(path), UiUtils.EMPTY_RUNNABLE);
}
public void open(List<Path> paths) {
open(paths, EMPTY_RUNNABLE);
open(paths, UiUtils.EMPTY_RUNNABLE);
}
private void open(List<Path> paths, Runnable onFinish) {
@ -501,7 +500,7 @@ public class MainWindow extends JFrame {
synchronized (ReloadProject.EVENT) {
saveAll();
closeAll();
loadFiles(EMPTY_RUNNABLE);
loadFiles(UiUtils.EMPTY_RUNNABLE);
menuBar.reloadShortcuts();
}
@ -1166,6 +1165,8 @@ public class MainWindow extends JFrame {
ExceptionDialog.throwTestException();
}
});
help.add(new JCheckBoxMenuItem(new ActionHandler("UI WatchDog", UIWatchDog::toggle)));
UIWatchDog.onStart();
}
help.add(aboutAction);
@ -1320,11 +1321,21 @@ public class MainWindow extends JFrame {
TreePath path = event.getPath();
Object node = path.getLastPathComponent();
if (node instanceof JLoadableNode) {
((JLoadableNode) node).loadNode();
}
if (!treeReloading) {
project.addTreeExpansion(getPathExpansion(event.getPath()));
update();
JLoadableNode treeNode = (JLoadableNode) node;
backgroundExecutor.execute(treeNode.getLoadTask());
// schedule update for expanded nodes in a tree
backgroundExecutor.execute(NLS.str("progress.load"),
UiUtils.EMPTY_RUNNABLE,
status -> {
if (!treeReloading) {
treeModel.nodeStructureChanged(treeNode);
project.addTreeExpansion(getPathExpansion(event.getPath()));
}
});
} else {
if (!treeReloading) {
project.addTreeExpansion(getPathExpansion(event.getPath()));
}
}
}

View File

@ -5,6 +5,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
@ -15,6 +16,8 @@ import jadx.api.JavaClass;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.gui.jobs.SimpleTask;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
@ -90,22 +93,7 @@ public class TabsController {
JavaClass codeParent = cls.getTopParentClass();
if (!Objects.equals(codeParent, origTopCls)) {
JClass jumpCls = mainWindow.getCacheObject().getNodeCache().makeFrom(codeParent);
mainWindow.getBackgroundExecutor().execute(
NLS.str("progress.load"),
jumpCls::loadNode, // load code in background
status -> {
// search original node in jump class
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode();
if (declNode.equals(node.getJavaNode().getCodeNodeRef())) {
codeJump(new JumpPosition(jumpCls, pos));
return true;
}
}
return null;
});
});
loadCodeWithUIAction(jumpCls, () -> jumpToInnerClass(node, codeParent, jumpCls));
return;
}
}
@ -120,6 +108,38 @@ public class TabsController {
NLS.str("progress.load"),
() -> node.getRootClass().getCodeInfo(), // run heavy loading in background
status -> codeJump(new JumpPosition(node)));
loadCodeWithUIAction(node.getRootClass(), () -> codeJump(new JumpPosition(node)));
}
private void loadCodeWithUIAction(JClass cls, Runnable action) {
SimpleTask loadTask = cls.getLoadTask();
mainWindow.getBackgroundExecutor().execute(
new SimpleTask(loadTask.getTitle(),
loadTask.getJobs(),
status -> {
Consumer<TaskStatus> onFinish = loadTask.getOnFinish();
if (onFinish != null) {
onFinish.accept(status);
}
action.run();
}));
}
/**
* Search and jump to original node in jumpCls
*/
private void jumpToInnerClass(JNode node, JavaClass codeParent, JClass jumpCls) {
codeParent.getCodeInfo().getCodeMetadata().searchDown(0, (pos, ann) -> {
if (ann.getAnnType() == ICodeAnnotation.AnnType.DECLARATION) {
ICodeNodeRef declNode = ((NodeDeclareRef) ann).getNode();
if (declNode.equals(node.getJavaNode().getCodeNodeRef())) {
codeJump(new JumpPosition(jumpCls, pos));
return true;
}
}
return null;
});
}
/**

View File

@ -63,6 +63,17 @@ public class UiUtils {
*/
public static final long MIN_FREE_MEMORY = calculateMinFreeMemory();
public static final Runnable EMPTY_RUNNABLE = new Runnable() {
@Override
public void run() {
}
@Override
public String toString() {
return "EMPTY_RUNNABLE";
}
};
private UiUtils() {
}

View File

@ -0,0 +1,120 @@
package jadx.gui.utils.dbg;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.utils.UiUtils;
/**
* Watch for UI thread state, if it stuck log a warning with stacktrace
*/
public class UIWatchDog {
private static final Logger LOG = LoggerFactory.getLogger(UIWatchDog.class);
private static final boolean RUN_ON_START = false;
private static final int UI_MAX_DELAY_MS = 1000;
private static final int CHECK_INTERVAL_MS = 100;
public static void onStart() {
if (RUN_ON_START) {
UiUtils.uiRun(UIWatchDog::toggle);
}
}
public static synchronized void toggle() {
if (SwingUtilities.isEventDispatchThread()) {
INSTANCE.toggleState(Thread.currentThread());
} else {
throw new JadxRuntimeException("This method should be called in UI thread");
}
}
private static final UIWatchDog INSTANCE = new UIWatchDog();
private final AtomicBoolean enabled = new AtomicBoolean(false);
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private Future<?> taskFuture;
private UIWatchDog() {
// singleton
}
private void toggleState(Thread uiThread) {
if (enabled.get()) {
// stop
enabled.set(false);
if (taskFuture != null) {
try {
taskFuture.get(CHECK_INTERVAL_MS * 5, TimeUnit.MILLISECONDS);
} catch (Throwable e) {
LOG.warn("Stopping UI watchdog error", e);
}
}
} else {
// start
enabled.set(true);
taskFuture = executor.submit(() -> start(uiThread));
}
}
@SuppressWarnings("BusyWait")
private void start(Thread uiThread) {
LOG.debug("UI watchdog started");
try {
Exception e = new JadxRuntimeException("at");
TimeMeasure tm = new TimeMeasure();
boolean stuck = false;
long reportTime = 0;
while (enabled.get()) {
if (uiThread.getState() == Thread.State.TIMED_WAITING) {
if (!stuck) {
tm.start();
stuck = true;
reportTime = UI_MAX_DELAY_MS;
} else {
tm.end();
long time = tm.getTime();
if (time > reportTime) {
e.setStackTrace(uiThread.getStackTrace());
LOG.warn("UI events thread stuck for {}ms", time, e);
reportTime += UI_MAX_DELAY_MS;
}
}
} else {
stuck = false;
}
Thread.sleep(CHECK_INTERVAL_MS);
}
} catch (Throwable e) {
LOG.error("UI watchdog fail", e);
}
LOG.debug("UI watchdog stopped");
}
private static final class TimeMeasure {
private long start;
private long end;
public void start() {
start = System.currentTimeMillis();
}
public void end() {
end = System.currentTimeMillis();
}
public long getTime() {
return end - start;
}
}
}

View File

@ -24,6 +24,11 @@ public class ActionHandler extends AbstractAction {
this.consumer = consumer;
}
public ActionHandler(String name, Runnable action) {
this(action);
setName(name);
}
public ActionHandler() {
this.consumer = ev -> {
};