未验证 提交 dd1be303 编写于 作者: S Skylot

fix(gui): split decompile and index tasks for correct time counting (#1361)

上级 8b30b770
......@@ -310,7 +310,13 @@ public final class JadxDecompiler implements Closeable {
}
processQueue.add(cls);
}
for (List<JavaClass> decompileBatch : decompileScheduler.buildBatches(processQueue)) {
List<List<JavaClass>> batches;
try {
batches = decompileScheduler.buildBatches(processQueue);
} catch (Exception e) {
throw new JadxRuntimeException("Decompilation batches build failed", e);
}
for (List<JavaClass> decompileBatch : batches) {
tasks.add(() -> {
for (JavaClass cls : decompileBatch) {
try {
......
......@@ -69,6 +69,10 @@ public final class JavaClass implements JavaNode {
cls.unloadCode();
}
public boolean isNoCode() {
return cls.contains(AFlag.DONT_GENERATE);
}
public synchronized String getSmali() {
return cls.getDisassembledCode();
}
......
......@@ -358,6 +358,17 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return codeInfo;
}
@Nullable
public ICodeInfo getCodeFromCache() {
ICodeCache codeCache = root().getCodeCache();
String clsRawName = getRawName();
ICodeInfo codeInfo = codeCache.get(clsRawName);
if (codeInfo == ICodeInfo.EMPTY) {
return null;
}
return codeInfo;
}
@Override
public void load() {
for (MethodNode mth : getMethods()) {
......
......@@ -15,8 +15,10 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
/**
......@@ -46,6 +48,14 @@ public class BackgroundExecutor {
return taskWorker;
}
public TaskStatus executeAndWait(IBackgroundTask task) {
try {
return execute(task).get();
} catch (Exception e) {
throw new JadxRuntimeException("Task execution error", e);
}
}
public void cancelAll() {
try {
taskQueueExecutor.shutdownNow();
......@@ -74,11 +84,12 @@ public class BackgroundExecutor {
return (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
}
private final class TaskWorker extends SwingWorker<TaskStatus, Void> {
private final class TaskWorker extends SwingWorker<TaskStatus, Void> implements ITaskInfo {
private final IBackgroundTask task;
private TaskStatus status = TaskStatus.WAIT;
private long jobsCount;
private long jobsComplete;
private long time;
public TaskWorker(IBackgroundTask task) {
this.task = task;
......@@ -111,13 +122,15 @@ public class BackgroundExecutor {
executor.execute(job);
}
executor.shutdown();
status = waitTermination(executor);
long startTime = System.currentTimeMillis();
status = waitTermination(executor, buildCancelCheck(startTime));
time = System.currentTimeMillis() - startTime;
jobsComplete = executor.getCompletedTaskCount();
task.onDone(this);
}
@SuppressWarnings("BusyWait")
private TaskStatus waitTermination(ThreadPoolExecutor executor) throws InterruptedException {
Supplier<TaskStatus> cancelCheck = buildCancelCheck();
private TaskStatus waitTermination(ThreadPoolExecutor executor, Supplier<TaskStatus> cancelCheck) throws InterruptedException {
try {
int k = 0;
while (true) {
......@@ -145,7 +158,7 @@ public class BackgroundExecutor {
}
private void performCancel(ThreadPoolExecutor executor) throws InterruptedException {
progressPane.changeLabel(this, task.getTitle() + " (Canceling)… ");
progressPane.changeLabel(this, task.getTitle() + " (" + NLS.str("progress.canceling") + ")… ");
progressPane.changeIndeterminate(this, true);
// force termination
executor.shutdownNow();
......@@ -153,8 +166,8 @@ public class BackgroundExecutor {
LOG.debug("Task cancel complete: {}", complete);
}
private Supplier<TaskStatus> buildCancelCheck() {
long waitUntilTime = task.timeLimit() == 0 ? 0 : System.currentTimeMillis() + task.timeLimit();
private Supplier<TaskStatus> buildCancelCheck(long startTime) {
long waitUntilTime = task.timeLimit() == 0 ? 0 : startTime + task.timeLimit();
boolean checkMemoryUsage = task.checkMemoryUsage();
return () -> {
if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) {
......@@ -180,7 +193,32 @@ public class BackgroundExecutor {
@Override
protected void done() {
progressPane.setVisible(false);
task.onFinish(status, jobsCount - jobsComplete);
task.onFinish(this);
}
@Override
public TaskStatus getStatus() {
return status;
}
@Override
public long getJobsCount() {
return jobsCount;
}
@Override
public long getJobsComplete() {
return jobsComplete;
}
@Override
public long getJobsSkipped() {
return jobsCount - jobsComplete;
}
@Override
public long getTime() {
return time;
}
}
......@@ -206,9 +244,9 @@ public class BackgroundExecutor {
}
@Override
public void onFinish(TaskStatus status, long l) {
public void onFinish(ITaskInfo taskInfo) {
if (onFinish != null) {
onFinish.accept(status);
onFinish.accept(taskInfo.getStatus());
}
}
......
package jadx.gui.jobs;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -29,7 +27,8 @@ public class DecompileTask implements IBackgroundTask {
private final JadxWrapper wrapper;
private final AtomicInteger complete = new AtomicInteger(0);
private int expectedCompleteCount;
private long startTime;
private ProcessResult result;
public DecompileTask(MainWindow mainWindow, JadxWrapper wrapper) {
this.mainWindow = mainWindow;
......@@ -44,18 +43,21 @@ public class DecompileTask implements IBackgroundTask {
@Override
public List<Runnable> scheduleJobs() {
IndexService indexService = mainWindow.getCacheObject().getIndexService();
List<JavaClass> classesForIndex = wrapper.getIncludedClasses()
.stream()
.filter(indexService::isIndexNeeded)
.collect(Collectors.toList());
expectedCompleteCount = classesForIndex.size();
List<JavaClass> classes = wrapper.getIncludedClasses();
expectedCompleteCount = classes.size();
indexService.setComplete(false);
complete.set(0);
List<Runnable> jobs = new ArrayList<>(expectedCompleteCount + 1);
jobs.add(indexService::indexResources);
for (List<JavaClass> batch : wrapper.buildDecompileBatches(classesForIndex)) {
List<List<JavaClass>> batches;
try {
batches = wrapper.buildDecompileBatches(classes);
} catch (Exception e) {
LOG.error("Decompile batches build error", e);
return Collections.emptyList();
}
List<Runnable> jobs = new ArrayList<>(batches.size());
for (List<JavaClass> batch : batches) {
jobs.add(() -> {
for (JavaClass cls : batch) {
try {
......@@ -68,58 +70,23 @@ public class DecompileTask implements IBackgroundTask {
}
});
}
jobs.add(() -> {
for (JavaClass cls : classesForIndex) {
try {
// TODO: a lot of synchronizations to index object, not effective for parallel usage
indexService.indexCls(cls);
} catch (Throwable e) {
LOG.error("Failed to index class: {}", cls, e);
}
}
});
startTime = System.currentTimeMillis();
return jobs;
}
@Override
public void onFinish(TaskStatus status, long skippedJobs) {
long taskTime = System.currentTimeMillis() - startTime;
public void onDone(ITaskInfo taskInfo) {
long taskTime = taskInfo.getTime();
long avgPerCls = taskTime / Math.max(expectedCompleteCount, 1);
int timeLimit = timeLimit();
int skippedCls = expectedCompleteCount - complete.get();
if (LOG.isInfoEnabled()) {
LOG.info("Decompile task complete in " + taskTime + " ms (avg " + avgPerCls + " ms per class)"
+ ", classes: " + expectedCompleteCount
+ ", time limit:{ total: " + timeLimit() + "ms, per cls: " + CLS_LIMIT + "ms }"
+ ", status: " + status);
}
IndexService indexService = mainWindow.getCacheObject().getIndexService();
indexService.setComplete(true);
if (skippedJobs == 0) {
return;
}
int skippedCls = expectedCompleteCount - complete.get();
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", timeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_MEMORY: {
mainWindow.showHeapUsageBar();
JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
+ ", skipped: " + skippedCls
+ ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }"
+ ", status: " + taskInfo.getStatus());
}
this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
}
@Override
......@@ -136,4 +103,8 @@ public class DecompileTask implements IBackgroundTask {
public boolean checkMemoryUsage() {
return true;
}
public ProcessResult getResult() {
return result;
}
}
......@@ -50,16 +50,16 @@ public class ExportTask implements IBackgroundTask {
}
@Override
public void onFinish(TaskStatus status, long skipped) {
public void onFinish(ITaskInfo taskInfo) {
// restore initial code cache
wrapper.getArgs().setCodeCache(uiCodeCache);
if (skipped == 0) {
if (taskInfo.getJobsSkipped() == 0) {
return;
}
String reason = getIncompleteReason(status);
String reason = getIncompleteReason(taskInfo.getStatus());
if (reason != null) {
JOptionPane.showMessageDialog(mainWindow,
NLS.str("message.saveIncomplete", reason, skipped),
NLS.str("message.saveIncomplete", reason, taskInfo.getJobsSkipped()),
NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE);
}
}
......
......@@ -8,7 +8,17 @@ public interface IBackgroundTask {
List<Runnable> scheduleJobs();
void onFinish(TaskStatus status, long skipped);
/**
* Called on executor thread after the all jobs finished.
*/
default void onDone(ITaskInfo taskInfo) {
}
/**
* Executed on the Event Dispatch Thread after the all jobs finished.
*/
default void onFinish(ITaskInfo taskInfo) {
}
default boolean canBeCanceled() {
return false;
......
package jadx.gui.jobs;
public interface ITaskInfo {
TaskStatus getStatus();
long getJobsCount();
long getJobsComplete();
long getJobsSkipped();
long getTime();
}
......@@ -5,9 +5,11 @@ import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.JavaClass;
import jadx.gui.utils.CacheObject;
......@@ -30,22 +32,36 @@ public class IndexService {
/**
* Warning! Not ready for parallel execution. Use only in a single thread.
*/
public void indexCls(JavaClass cls) {
public boolean indexCls(JavaClass cls) {
try {
TextSearchIndex index = cache.getTextIndex();
if (index == null) {
return;
return false;
}
List<StringRef> lines = splitLines(cls);
// get code from cache to avoid decompilation here
String code = getCodeFromCache(cls);
if (code == null) {
return cls.isNoCode();
}
List<StringRef> lines = splitLines(code);
CodeLinesInfo linesInfo = new CodeLinesInfo(cls);
index.indexCode(cls, linesInfo, lines);
index.indexNames(cls);
indexSet.add(cls);
return true;
} catch (Exception e) {
LOG.error("Index error in class: {}", cls.getFullName(), e);
return false;
}
}
// TODO: add to API
@Nullable
private String getCodeFromCache(JavaClass cls) {
ICodeInfo codeInfo = cls.getClassNode().getCodeFromCache();
return codeInfo != null ? codeInfo.getCodeStr() : null;
}
public void indexResources() {
TextSearchIndex index = cache.getTextIndex();
index.indexResource();
......@@ -75,8 +91,8 @@ public class IndexService {
}
@NotNull
protected static List<StringRef> splitLines(JavaClass cls) {
List<StringRef> lines = StringRef.split(cls.getCode(), ICodeWriter.NL);
protected static List<StringRef> splitLines(String code) {
List<StringRef> lines = StringRef.split(code, ICodeWriter.NL);
int size = lines.size();
for (int i = 0; i < size; i++) {
lines.set(i, lines.get(i).trim());
......
package jadx.gui.jobs;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JavaClass;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
public class IndexTask implements IBackgroundTask {
private static final Logger LOG = LoggerFactory.getLogger(IndexTask.class);
private final MainWindow mainWindow;
private final JadxWrapper wrapper;
private final AtomicInteger complete = new AtomicInteger(0);
private int expectedCompleteCount;
private ProcessResult result;
public IndexTask(MainWindow mainWindow, JadxWrapper wrapper) {
this.mainWindow = mainWindow;
this.wrapper = wrapper;
}
@Override
public String getTitle() {
return NLS.str("progress.index");
}
@Override
public List<Runnable> scheduleJobs() {
IndexService indexService = mainWindow.getCacheObject().getIndexService();
List<JavaClass> classesForIndex = wrapper.getIncludedClasses()
.stream()
.filter(indexService::isIndexNeeded)
.collect(Collectors.toList());
expectedCompleteCount = classesForIndex.size();
indexService.setComplete(false);
complete.set(0);
List<Runnable> jobs = new ArrayList<>(2);
jobs.add(indexService::indexResources);
jobs.add(() -> {
for (JavaClass cls : classesForIndex) {
try {
// TODO: a lot of synchronizations to index object, not efficient for parallel usage
if (indexService.indexCls(cls)) {
complete.incrementAndGet();
} else {
LOG.debug("Index skipped for {}", cls);
}
} catch (Throwable e) {
LOG.error("Failed to index class: {}", cls, e);
}
}
});
return jobs;
}
@Override
public void onDone(ITaskInfo taskInfo) {
int skippedCls = expectedCompleteCount - complete.get();
if (LOG.isInfoEnabled()) {
LOG.info("Index task complete in " + taskInfo.getTime() + " ms"
+ ", classes: " + expectedCompleteCount
+ ", skipped: " + skippedCls
+ ", status: " + taskInfo.getStatus());
}
IndexService indexService = mainWindow.getCacheObject().getIndexService();
indexService.setComplete(true);
this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), 0);
}
@Override
public boolean canBeCanceled() {
return true;
}
@Override
public boolean checkMemoryUsage() {
return true;
}
public ProcessResult getResult() {
return result;
}
}
package jadx.gui.jobs;
public class ProcessResult {
private final int skipped;
private final TaskStatus status;
private final int timeLimit;
public ProcessResult(int skipped, TaskStatus status, int timeLimit) {
this.skipped = skipped;
this.status = status;
this.timeLimit = timeLimit;
}
public int getSkipped() {
return skipped;
}
public TaskStatus getStatus() {
return status;
}
public int getTimeLimit() {
return timeLimit;
}
}
......@@ -30,6 +30,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
......@@ -38,7 +39,6 @@ import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import javax.swing.AbstractAction;
......@@ -94,6 +94,8 @@ import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.jobs.DecompileTask;
import jadx.gui.jobs.ExportTask;
import jadx.gui.jobs.IndexService;
import jadx.gui.jobs.IndexTask;
import jadx.gui.jobs.ProcessResult;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.JadxProject;
......@@ -555,14 +557,62 @@ public class MainWindow extends JFrame {
}
try {
DecompileTask decompileTask = new DecompileTask(this, wrapper);
Future<TaskStatus> task = backgroundExecutor.execute(decompileTask);
task.get();
backgroundExecutor.executeAndWait(decompileTask);
IndexTask indexTask = new IndexTask(this, wrapper);
backgroundExecutor.executeAndWait(indexTask);
processDecompilationResults(decompileTask.getResult(), indexTask.getResult());
} catch (Exception e) {
LOG.error("Decompile task execution failed", e);
}
}
}
private void processDecompilationResults(ProcessResult decompile, ProcessResult index) {
int skippedCls = Math.max(decompile.getSkipped(), index.getSkipped());
if (skippedCls == 0) {
return;
}
TaskStatus status = mergeStatus(EnumSet.of(decompile.getStatus(), index.getStatus()));
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", decompile.getTimeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_MEMORY: {
showHeapUsageBar();
JOptionPane.showMessageDialog(this, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
}
}
private TaskStatus mergeStatus(Set<TaskStatus> statuses) {
if (statuses.size() == 1) {
return statuses.iterator().next();
}
if (statuses.contains(TaskStatus.CANCEL_BY_MEMORY)) {
return TaskStatus.CANCEL_BY_MEMORY;
}
if (statuses.contains(TaskStatus.CANCEL_BY_TIMEOUT)) {
return TaskStatus.CANCEL_BY_TIMEOUT;
}
if (statuses.contains(TaskStatus.CANCEL_BY_USER)) {
return TaskStatus.CANCEL_BY_USER;
}
return TaskStatus.COMPLETE;
}
public void cancelBackgroundJobs() {
ExecutorService worker = Executors.newSingleThreadExecutor();
worker.execute(backgroundExecutor::cancelAll);
......
......@@ -38,6 +38,7 @@ tree.loading=Laden…
progress.load=Laden
progress.decompile=Dekompilieren
progress.index=Indizieren
#progress.canceling=Canceling
error_dialog.title=Fehler
......
......@@ -37,7 +37,8 @@ tree.loading=Loading...
progress.load=Loading
progress.decompile=Decompiling
#progress.index=Indexing
progress.index=Indexing
progress.canceling=Canceling
error_dialog.title=Error
......
......@@ -38,6 +38,7 @@ tree.loading=Cargando...
progress.load=Cargando
progress.decompile=Decompiling
#progress.index=Indexing
#progress.canceling=Canceling
#error_dialog.title=
......
......@@ -38,6 +38,7 @@ tree.loading=로딩중...
progress.load=로딩중
progress.decompile=디컴파일 중
progress.index=인덱싱 중
#progress.canceling=Canceling
error_dialog.title=오류
......@@ -193,7 +194,7 @@ popup.copy=붙여넣기
popup.paste=복사
popup.delete=삭제
popup.select_all=모두 선택
popup.frida=frida 스니펫으로 복사
popup.frida=frida 스니펫으로 복사
popup.find_usage=사용 찾기
popup.go_to_declaration=선언문으로 이동
popup.exclude=제외
......
......@@ -37,7 +37,8 @@ tree.loading=加载中...
progress.load=正在加载
progress.decompile=反编译中
#progress.index=索引中
progress.index=索引中
#progress.canceling=Canceling
error_dialog.title=错误
......
......@@ -38,6 +38,7 @@ tree.loading=載入中...
progress.load=載入中
progress.decompile=正在反編譯
#progress.index=Indexing
#progress.canceling=Canceling
error_dialog.title=錯誤
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册