fix(gui): rework background executor to reduce delay for short tasks
This commit is contained in:
@@ -15,9 +15,6 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.SwingWorker;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -30,49 +27,49 @@ import jadx.gui.ui.panel.ProgressPanel;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
import static jadx.gui.utils.UiUtils.calcProgress;
|
||||
|
||||
/**
|
||||
* Class for run tasks in background with progress bar indication.
|
||||
* Run tasks in the background with progress bar indication.
|
||||
* Use instance created in {@link MainWindow}.
|
||||
*/
|
||||
public class BackgroundExecutor {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class);
|
||||
|
||||
private final JadxSettings settings;
|
||||
private final ProgressPanel progressPane;
|
||||
private final ProgressUpdater progressUpdater;
|
||||
|
||||
private ThreadPoolExecutor taskQueueExecutor;
|
||||
private final Map<Long, IBackgroundTask> taskRunning = new ConcurrentHashMap<>();
|
||||
private final Map<Long, InternalTask> taskRunning = new ConcurrentHashMap<>();
|
||||
private final AtomicLong idSupplier = new AtomicLong(0);
|
||||
|
||||
public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) {
|
||||
this.settings = Objects.requireNonNull(settings);
|
||||
this.progressPane = Objects.requireNonNull(progressPane);
|
||||
this.progressUpdater = new ProgressUpdater(progressPane, this::taskCanceled);
|
||||
reset();
|
||||
}
|
||||
|
||||
public synchronized Future<TaskStatus> execute(IBackgroundTask task) {
|
||||
long id = idSupplier.incrementAndGet();
|
||||
TaskWorker taskWorker = new TaskWorker(id, task);
|
||||
taskRunning.put(id, task);
|
||||
taskQueueExecutor.execute(() -> {
|
||||
taskWorker.init();
|
||||
taskWorker.run();
|
||||
public synchronized void execute(IBackgroundTask task) {
|
||||
InternalTask internalTask = buildTask(task);
|
||||
taskQueueExecutor.execute(() -> runTask(internalTask));
|
||||
}
|
||||
|
||||
public synchronized Future<TaskStatus> executeWithFuture(IBackgroundTask task) {
|
||||
InternalTask internalTask = buildTask(task);
|
||||
return taskQueueExecutor.submit(() -> {
|
||||
runTask(internalTask);
|
||||
return internalTask.getStatus();
|
||||
});
|
||||
return taskWorker;
|
||||
}
|
||||
|
||||
public synchronized void cancelAll() {
|
||||
try {
|
||||
taskRunning.values().forEach(Cancelable::cancel);
|
||||
taskRunning.values().forEach(this::cancelTask);
|
||||
taskQueueExecutor.shutdownNow();
|
||||
boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS);
|
||||
if (complete) {
|
||||
LOG.debug("Background task executor canceled successfully");
|
||||
} else {
|
||||
String taskNames = taskRunning.values().stream()
|
||||
.map(IBackgroundTask::getTitle)
|
||||
.map(t -> t.getBgTask().getTitle())
|
||||
.collect(Collectors.joining(", "));
|
||||
LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames);
|
||||
}
|
||||
@@ -100,8 +97,8 @@ public class BackgroundExecutor {
|
||||
execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable), onFinishUiRunnable));
|
||||
}
|
||||
|
||||
public Future<TaskStatus> execute(String title, Runnable backgroundRunnable) {
|
||||
return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable)));
|
||||
public void execute(String title, Runnable backgroundRunnable) {
|
||||
execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable)));
|
||||
}
|
||||
|
||||
public void startLoading(Runnable backgroundRunnable, Runnable onFinishUiRunnable) {
|
||||
@@ -118,152 +115,87 @@ public class BackgroundExecutor {
|
||||
idSupplier.set(0);
|
||||
}
|
||||
|
||||
private void taskComplete(long id) {
|
||||
taskRunning.remove(id);
|
||||
private InternalTask buildTask(IBackgroundTask task) {
|
||||
long id = idSupplier.incrementAndGet();
|
||||
InternalTask internalTask = new InternalTask(id, task);
|
||||
taskRunning.put(id, internalTask);
|
||||
return internalTask;
|
||||
}
|
||||
|
||||
private final class TaskWorker extends SwingWorker<TaskStatus, Void> implements ITaskInfo {
|
||||
private final long id;
|
||||
private final IBackgroundTask task;
|
||||
private ITaskExecutor taskExecutor;
|
||||
private TaskStatus status = TaskStatus.WAIT;
|
||||
private long jobsCount;
|
||||
private long jobsComplete;
|
||||
private long time;
|
||||
|
||||
public TaskWorker(long id, IBackgroundTask task) {
|
||||
this.id = id;
|
||||
this.task = task;
|
||||
private void runTask(InternalTask internalTask) {
|
||||
try {
|
||||
IBackgroundTask task = internalTask.getBgTask();
|
||||
ITaskExecutor taskExecutor = task.scheduleTasks();
|
||||
taskExecutor.setThreadsCount(settings.getThreadsCount());
|
||||
int tasksCount = taskExecutor.getTasksCount();
|
||||
internalTask.setTaskExecutor(taskExecutor);
|
||||
internalTask.setJobsCount(tasksCount);
|
||||
if (UiUtils.JADX_GUI_DEBUG) {
|
||||
LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}",
|
||||
task.getTitle(), tasksCount, task.timeLimit(), task.checkMemoryUsage());
|
||||
}
|
||||
long startTime = System.currentTimeMillis();
|
||||
Supplier<TaskStatus> cancelCheck = buildCancelCheck(internalTask, startTime);
|
||||
internalTask.taskStart(startTime, cancelCheck);
|
||||
progressUpdater.addTask(internalTask);
|
||||
taskExecutor.execute();
|
||||
taskExecutor.awaitTermination();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Task failed", e);
|
||||
internalTask.setStatus(TaskStatus.ERROR);
|
||||
} finally {
|
||||
taskComplete(internalTask);
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
addPropertyChangeListener(progressPane);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
progressPane.reset();
|
||||
if (task.getTaskProgress() != null) {
|
||||
progressPane.setIndeterminate(false);
|
||||
private void taskComplete(InternalTask internalTask) {
|
||||
try {
|
||||
IBackgroundTask task = internalTask.getBgTask();
|
||||
internalTask.setJobsComplete(internalTask.getTaskExecutor().getProgress());
|
||||
internalTask.setStatus(TaskStatus.COMPLETE);
|
||||
internalTask.updateExecTime();
|
||||
task.onDone(internalTask);
|
||||
// treat UI task operations as part of the task to not mix with others
|
||||
UiUtils.uiRunAndWait(() -> {
|
||||
try {
|
||||
task.onFinish(internalTask);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Task onFinish failed", e);
|
||||
internalTask.setStatus(TaskStatus.ERROR);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOG.error("Task complete failed", e);
|
||||
internalTask.setStatus(TaskStatus.ERROR);
|
||||
} finally {
|
||||
internalTask.taskComplete();
|
||||
progressUpdater.taskComplete(internalTask);
|
||||
removeTask(internalTask);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TaskStatus doInBackground() throws Exception {
|
||||
progressPane.changeLabel(this, task.getTitle() + "… ");
|
||||
progressPane.changeCancelBtnVisible(this, task.canBeCanceled());
|
||||
try {
|
||||
runJobs();
|
||||
} finally {
|
||||
try {
|
||||
task.onDone(this);
|
||||
// treat UI task operations as part of the task to not mix with others
|
||||
UiUtils.uiRunAndWait(() -> {
|
||||
try {
|
||||
progressPane.setVisible(false);
|
||||
task.onFinish(this);
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Task onFinish failed", e);
|
||||
status = TaskStatus.ERROR;
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
LOG.error("Task onDone failed", e);
|
||||
status = TaskStatus.ERROR;
|
||||
} finally {
|
||||
taskComplete(id);
|
||||
progressPane.changeVisibility(this, false);
|
||||
removePropertyChangeListener(progressPane);
|
||||
}
|
||||
private void removeTask(InternalTask internalTask) {
|
||||
taskRunning.remove(internalTask.getId());
|
||||
}
|
||||
|
||||
private void cancelTask(InternalTask internalTask) {
|
||||
try {
|
||||
IBackgroundTask task = internalTask.getBgTask();
|
||||
if (!internalTask.isRunning()) {
|
||||
// task complete or not yet started
|
||||
task.cancel();
|
||||
removeTask(internalTask);
|
||||
return;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private void runJobs() throws InterruptedException {
|
||||
taskExecutor = task.scheduleTasks();
|
||||
jobsCount = taskExecutor.getTasksCount();
|
||||
LOG.debug("Starting background task '{}', jobs count: {}, time limit: {} ms, memory check: {}",
|
||||
task.getTitle(), jobsCount, task.timeLimit(), task.checkMemoryUsage());
|
||||
if (jobsCount != 1 && !task.isSilent()) {
|
||||
progressPane.changeVisibility(this, true);
|
||||
}
|
||||
status = TaskStatus.STARTED;
|
||||
taskExecutor.setThreadsCount(settings.getThreadsCount());
|
||||
taskExecutor.execute();
|
||||
long startTime = System.currentTimeMillis();
|
||||
status = waitTermination(buildCancelCheck(startTime));
|
||||
time = System.currentTimeMillis() - startTime;
|
||||
jobsComplete = taskExecutor.getProgress();
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
private TaskStatus waitTermination(Supplier<TaskStatus> cancelCheck) throws InterruptedException {
|
||||
try {
|
||||
int k = 0;
|
||||
while (true) {
|
||||
if (!taskExecutor.isRunning()) {
|
||||
return TaskStatus.COMPLETE;
|
||||
}
|
||||
if (task.isSilent()) {
|
||||
Thread.sleep(50);
|
||||
continue;
|
||||
}
|
||||
TaskStatus cancelStatus = cancelCheck.get();
|
||||
if (cancelStatus != null) {
|
||||
performCancel();
|
||||
return cancelStatus;
|
||||
}
|
||||
if (k < 10) {
|
||||
// faster update for short tasks
|
||||
Thread.sleep(200);
|
||||
if (k == 5) {
|
||||
updateProgress();
|
||||
}
|
||||
} else {
|
||||
updateProgress();
|
||||
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");
|
||||
performCancel();
|
||||
return TaskStatus.CANCEL_BY_USER;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Task wait aborted by exception", e);
|
||||
performCancel();
|
||||
return TaskStatus.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
Consumer<ITaskProgress> onProgressListener = task.getProgressListener();
|
||||
ITaskProgress taskProgress = task.getTaskProgress();
|
||||
if (taskProgress == null) {
|
||||
setProgress(calcProgress(taskExecutor.getProgress(), jobsCount));
|
||||
if (onProgressListener != null) {
|
||||
onProgressListener.accept(new TaskProgress(taskExecutor.getProgress(), jobsCount));
|
||||
}
|
||||
} else {
|
||||
setProgress(calcProgress(taskProgress));
|
||||
if (onProgressListener != null) {
|
||||
onProgressListener.accept(taskProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performCancel() throws InterruptedException {
|
||||
progressPane.changeLabel(this, task.getTitle() + " (" + NLS.str("progress.canceling") + ")… ");
|
||||
progressPane.changeIndeterminate(this, true);
|
||||
ITaskExecutor taskExecutor = internalTask.getTaskExecutor();
|
||||
// force termination
|
||||
task.cancel();
|
||||
taskExecutor.terminate();
|
||||
|
||||
ExecutorService executor = taskExecutor.getInternalExecutor();
|
||||
if (executor == null) {
|
||||
return;
|
||||
}
|
||||
taskExecutor.terminate();
|
||||
task.cancel();
|
||||
int cancelTimeout = task.getCancelTimeoutMS();
|
||||
if (cancelTimeout != 0) {
|
||||
if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) {
|
||||
@@ -277,74 +209,48 @@ public class BackgroundExecutor {
|
||||
LOG.debug("Forced task cancel status: {}", complete
|
||||
? "success"
|
||||
: "fail, still active: " + (taskExecutor.getTasksCount() - taskExecutor.getProgress()));
|
||||
}
|
||||
|
||||
private Supplier<TaskStatus> buildCancelCheck(long startTime) {
|
||||
long waitUntilTime = task.timeLimit() == 0 ? 0 : startTime + task.timeLimit();
|
||||
boolean checkMemoryUsage = task.checkMemoryUsage();
|
||||
return () -> {
|
||||
if (task.isCanceled()) {
|
||||
return TaskStatus.CANCEL_BY_USER;
|
||||
}
|
||||
if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) {
|
||||
LOG.error("Task '{}' execution timeout, force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_TIMEOUT;
|
||||
}
|
||||
if (isCancelled() || Thread.currentThread().isInterrupted()) {
|
||||
LOG.warn("Task '{}' canceled", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_USER;
|
||||
}
|
||||
if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.info("Memory usage: {}", UiUtils.memoryInfo());
|
||||
if (taskExecutor.getThreadsCount() == 1) {
|
||||
LOG.error("Task '{}' memory limit reached, force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_MEMORY;
|
||||
}
|
||||
LOG.warn("Low memory, reduce processing threads count to 1");
|
||||
// reduce threads count and continue
|
||||
taskExecutor.setThreadsCount(1);
|
||||
System.gc();
|
||||
UiUtils.sleep(1000); // wait GC
|
||||
if (!UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.error("Task '{}' memory limit reached (after GC), force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_MEMORY;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TaskWorker{status=" + status
|
||||
+ ", jobsCount=" + jobsCount
|
||||
+ ", jobsComplete=" + jobsComplete
|
||||
+ ", time=" + time + "ms}";
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to cancel task: {}", internalTask, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task cancel notification from progress updater
|
||||
*/
|
||||
private void taskCanceled(InternalTask task) {
|
||||
cancelTask(task);
|
||||
}
|
||||
|
||||
private Supplier<TaskStatus> buildCancelCheck(InternalTask internalTask, long startTime) {
|
||||
IBackgroundTask task = internalTask.getBgTask();
|
||||
int timeLimit = task.timeLimit();
|
||||
long waitUntilTime = timeLimit == 0 ? 0 : startTime + timeLimit;
|
||||
boolean checkMemoryUsage = task.checkMemoryUsage();
|
||||
return () -> {
|
||||
if (task.isCanceled() || Thread.currentThread().isInterrupted()) {
|
||||
return TaskStatus.CANCEL_BY_USER;
|
||||
}
|
||||
if (waitUntilTime != 0 && waitUntilTime < System.currentTimeMillis()) {
|
||||
LOG.warn("Task '{}' execution timeout, force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_TIMEOUT;
|
||||
}
|
||||
if (checkMemoryUsage && !UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.warn("High memory usage: {}", UiUtils.memoryInfo());
|
||||
if (internalTask.getTaskExecutor().getThreadsCount() == 1) {
|
||||
LOG.warn("Task '{}' memory limit reached, force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_MEMORY;
|
||||
}
|
||||
LOG.warn("Low free memory, reduce processing threads count to 1");
|
||||
// reduce threads count and continue
|
||||
internalTask.getTaskExecutor().setThreadsCount(1);
|
||||
System.gc();
|
||||
UiUtils.sleep(1000); // wait GC
|
||||
if (!UiUtils.isFreeMemoryAvailable()) {
|
||||
LOG.error("Task '{}' memory limit reached (after GC), force cancel", task.getTitle());
|
||||
return TaskStatus.CANCEL_BY_MEMORY;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import jadx.api.utils.tasks.ITaskExecutor;
|
||||
|
||||
public class InternalTask implements Delayed, ITaskInfo {
|
||||
private final long id;
|
||||
private final IBackgroundTask bgTask;
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private final AtomicLong nextUpdate = new AtomicLong(0);
|
||||
private final AtomicBoolean firstUpdate = new AtomicBoolean(true);
|
||||
|
||||
private long startTime;
|
||||
private long execTime;
|
||||
private Supplier<TaskStatus> cancelCheck;
|
||||
private TaskStatus status = TaskStatus.WAIT;
|
||||
private ITaskExecutor taskExecutor;
|
||||
private long jobsCount;
|
||||
private long jobsComplete;
|
||||
|
||||
public InternalTask(long id, IBackgroundTask task) {
|
||||
this.id = id;
|
||||
this.bgTask = task;
|
||||
}
|
||||
|
||||
public void taskStart(long startTime, Supplier<TaskStatus> cancelCheck) {
|
||||
this.startTime = startTime;
|
||||
this.cancelCheck = cancelCheck;
|
||||
this.status = TaskStatus.STARTED;
|
||||
this.running.set(true);
|
||||
}
|
||||
|
||||
public void taskComplete() {
|
||||
this.running.set(false);
|
||||
if (status == TaskStatus.STARTED) {
|
||||
// might be already set to error or cancel
|
||||
this.status = TaskStatus.COMPLETE;
|
||||
}
|
||||
updateExecTime();
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public IBackgroundTask getBgTask() {
|
||||
return bgTask;
|
||||
}
|
||||
|
||||
public void setNextUpdate(long nextUpdate) {
|
||||
this.nextUpdate.set(nextUpdate);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
public boolean checkForFirstUpdate() {
|
||||
return firstUpdate.compareAndExchange(true, false);
|
||||
}
|
||||
|
||||
public Supplier<TaskStatus> getCancelCheck() {
|
||||
return cancelCheck;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(TaskStatus taskStatus) {
|
||||
this.status = taskStatus;
|
||||
}
|
||||
|
||||
public ITaskExecutor getTaskExecutor() {
|
||||
return taskExecutor;
|
||||
}
|
||||
|
||||
public void setTaskExecutor(ITaskExecutor taskExecutor) {
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getJobsComplete() {
|
||||
return jobsComplete;
|
||||
}
|
||||
|
||||
public void setJobsComplete(long jobsComplete) {
|
||||
this.jobsComplete = jobsComplete;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getJobsCount() {
|
||||
return jobsCount;
|
||||
}
|
||||
|
||||
public void setJobsCount(long jobsCount) {
|
||||
this.jobsCount = jobsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getJobsSkipped() {
|
||||
return jobsCount - jobsComplete;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTime() {
|
||||
return execTime;
|
||||
}
|
||||
|
||||
public void updateExecTime() {
|
||||
this.execTime = System.currentTimeMillis() - startTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(@NotNull TimeUnit unit) {
|
||||
return unit.convert(nextUpdate.get() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Delayed o) {
|
||||
return Long.compare(nextUpdate.get(), ((InternalTask) o).nextUpdate.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InternalTask{" + bgTask.getTitle() + ", status=" + status
|
||||
+ ", progress=" + jobsComplete + " of " + jobsCount + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package jadx.gui.jobs;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.gui.ui.panel.ProgressPanel;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
@SuppressWarnings({ "FieldCanBeLocal", "InfiniteLoopStatement" })
|
||||
public class ProgressUpdater {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ProgressUpdater.class);
|
||||
|
||||
private static final int UPDATE_INTERVAL_MS = 1000;
|
||||
|
||||
private final ProgressPanel progressPane;
|
||||
private final Consumer<InternalTask> cancelCallback;
|
||||
private final ExecutorService bgExecutor = Executors.newSingleThreadExecutor(Utils.simpleThreadFactory("jadx-progress"));
|
||||
private final BlockingQueue<InternalTask> tasks = new DelayQueue<>();
|
||||
|
||||
public ProgressUpdater(ProgressPanel progressPane, Consumer<InternalTask> cancelCallback) {
|
||||
this.progressPane = progressPane;
|
||||
this.cancelCallback = cancelCallback;
|
||||
this.bgExecutor.execute(this::updateLoop);
|
||||
}
|
||||
|
||||
public void addTask(InternalTask task) {
|
||||
if (task.getBgTask().isSilent()) {
|
||||
return;
|
||||
}
|
||||
scheduleNextUpdate(task);
|
||||
}
|
||||
|
||||
public void taskComplete(InternalTask task) {
|
||||
task.setNextUpdate(0);
|
||||
updateProgress(task);
|
||||
}
|
||||
|
||||
private void scheduleNextUpdate(InternalTask task) {
|
||||
task.setNextUpdate(System.currentTimeMillis() + UPDATE_INTERVAL_MS);
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
private void updateLoop() {
|
||||
while (true) {
|
||||
try {
|
||||
InternalTask task = tasks.take();
|
||||
if (task.isRunning()) {
|
||||
updateProgress(task);
|
||||
cancelCheck(task);
|
||||
scheduleNextUpdate(task);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error in ProgressUpdater loop", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(InternalTask internalTask) {
|
||||
UiUtils.uiRun(() -> {
|
||||
IBackgroundTask bgTask = internalTask.getBgTask();
|
||||
if (internalTask.isRunning()) {
|
||||
if (internalTask.checkForFirstUpdate()) {
|
||||
progressPane.setLabel(bgTask.getTitle() + "… ");
|
||||
progressPane.setCancelButtonVisible(bgTask.canBeCanceled());
|
||||
progressPane.setVisible(true);
|
||||
}
|
||||
ITaskProgress taskProgress = bgTask.getTaskProgress();
|
||||
if (taskProgress == null) {
|
||||
int progress = internalTask.getTaskExecutor().getProgress();
|
||||
taskProgress = new TaskProgress(progress, internalTask.getJobsCount());
|
||||
}
|
||||
progressPane.setProgress(taskProgress);
|
||||
Consumer<ITaskProgress> onProgressListener = bgTask.getProgressListener();
|
||||
if (onProgressListener != null) {
|
||||
onProgressListener.accept(taskProgress);
|
||||
}
|
||||
} else {
|
||||
progressPane.reset();
|
||||
progressPane.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void cancelCheck(InternalTask task) {
|
||||
TaskStatus taskStatus = task.getCancelCheck().get();
|
||||
if (taskStatus == null) {
|
||||
return;
|
||||
}
|
||||
task.setStatus(taskStatus);
|
||||
UiUtils.uiRun(() -> {
|
||||
IBackgroundTask bgTask = task.getBgTask();
|
||||
progressPane.setLabel(bgTask.getTitle() + " (" + NLS.str("progress.canceling") + ")… ");
|
||||
progressPane.setCancelButtonVisible(false);
|
||||
progressPane.setIndeterminate(true);
|
||||
});
|
||||
cancelCallback.accept(task);
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,9 @@ public class TaskProgress implements ITaskProgress {
|
||||
public void updateTotal(int total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TaskProgress{" + progress + " of " + total + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
public class SearchJob implements Runnable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SearchJob.class);
|
||||
|
||||
private final SearchTask searchTask;
|
||||
private final ISearchProvider provider;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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.AtomicInteger;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
@@ -62,7 +61,7 @@ public class SearchTask extends CancelableBackgroundTask {
|
||||
resetCancel();
|
||||
resultsCount.set(0);
|
||||
taskProgress.updateTotal(jobs.stream().mapToInt(s -> s.getProvider().total()).sum());
|
||||
future = backgroundExecutor.execute(this);
|
||||
future = backgroundExecutor.executeWithFuture(this);
|
||||
}
|
||||
|
||||
public synchronized boolean addResult(JNode resultNode) {
|
||||
@@ -79,19 +78,17 @@ public class SearchTask extends CancelableBackgroundTask {
|
||||
}
|
||||
|
||||
public synchronized void waitTask() {
|
||||
if (future != null) {
|
||||
try {
|
||||
future.get(200, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
LOG.debug("Search task wait timeout");
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Search task wait error", e);
|
||||
} finally {
|
||||
future.cancel(true);
|
||||
future = null;
|
||||
}
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
future.get(200, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Search task wait error", e);
|
||||
future.cancel(true);
|
||||
} finally {
|
||||
future = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package jadx.gui.ui.panel;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.BoxLayout;
|
||||
@@ -11,18 +9,15 @@ import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingWorker;
|
||||
|
||||
import jadx.gui.jobs.ITaskProgress;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.Icons;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
|
||||
public class ProgressPanel extends JPanel implements PropertyChangeListener {
|
||||
|
||||
public class ProgressPanel extends JPanel {
|
||||
private static final long serialVersionUID = -3238438119672015733L;
|
||||
|
||||
private static final Icon ICON_CANCEL = UiUtils.openSvgIcon("ui/close");
|
||||
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel progressLabel;
|
||||
private final JButton cancelButton;
|
||||
@@ -43,8 +38,9 @@ public class ProgressPanel extends JPanel implements PropertyChangeListener {
|
||||
add(progressLabel);
|
||||
add(progressBar);
|
||||
|
||||
cancelButton = new JButton(ICON_CANCEL);
|
||||
cancelButton.setPreferredSize(new Dimension(ICON_CANCEL.getIconWidth(), ICON_CANCEL.getIconHeight()));
|
||||
Icon cancelIcon = Icons.ICON_CLOSE;
|
||||
cancelButton = new JButton(cancelIcon);
|
||||
cancelButton.setPreferredSize(new Dimension(cancelIcon.getIconWidth(), cancelIcon.getIconHeight()));
|
||||
cancelButton.setToolTipText("Cancel background jobs");
|
||||
cancelButton.setBorderPainted(false);
|
||||
cancelButton.setFocusPainted(false);
|
||||
@@ -82,31 +78,6 @@ public class ProgressPanel extends JPanel implements PropertyChangeListener {
|
||||
progressBar.setStringPainted(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
switch (evt.getPropertyName()) {
|
||||
case "progress":
|
||||
setProgress((Integer) evt.getNewValue());
|
||||
break;
|
||||
|
||||
case "label":
|
||||
setLabel((String) evt.getNewValue());
|
||||
break;
|
||||
|
||||
case "visible":
|
||||
setVisible(((Boolean) evt.getNewValue()));
|
||||
break;
|
||||
|
||||
case "indeterminate":
|
||||
setIndeterminate(((Boolean) evt.getNewValue()));
|
||||
break;
|
||||
|
||||
case "cancel-visible":
|
||||
cancelButton.setVisible(((Boolean) evt.getNewValue()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
progressLabel.setText(label);
|
||||
}
|
||||
@@ -115,19 +86,7 @@ public class ProgressPanel extends JPanel implements PropertyChangeListener {
|
||||
progressBar.setIndeterminate(newValue);
|
||||
}
|
||||
|
||||
public void changeLabel(SwingWorker<?, ?> task, String label) {
|
||||
task.firePropertyChange("label", null, label);
|
||||
}
|
||||
|
||||
public void changeIndeterminate(SwingWorker<?, ?> task, boolean indeterminate) {
|
||||
task.firePropertyChange("indeterminate", null, indeterminate);
|
||||
}
|
||||
|
||||
public void changeVisibility(SwingWorker<?, ?> task, boolean visible) {
|
||||
task.firePropertyChange("visible", null, visible);
|
||||
}
|
||||
|
||||
public void changeCancelBtnVisible(SwingWorker<?, ?> task, boolean visible) {
|
||||
task.firePropertyChange("cancel-visible", null, visible);
|
||||
public void setCancelButtonVisible(boolean visible) {
|
||||
cancelButton.setVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user