From c1d0bb973a5a4ab5553951df91f3fb8b38aad023 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 1 Feb 2026 19:10:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E5=8C=96=20Task=20(#5365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/construct/TaskListPane.java | 8 +- .../hmcl/task/AsyncTaskExecutor.java | 21 +-- .../java/org/jackhuang/hmcl/task/Task.java | 169 +++++++++++------- .../org/jackhuang/hmcl/task/TaskExecutor.java | 15 +- 4 files changed, 115 insertions(+), 98 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 0eb62cbed..0f5fd1be4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -326,8 +326,7 @@ public final class TaskListPane extends StackPane { if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null) prevStageNode.status.removeListener(statusChangeListener); - if (item instanceof ProgressListNode) { - var progressListNode = (ProgressListNode) item; + if (item instanceof ProgressListNode progressListNode) { title.setText(progressListNode.title); message.textProperty().bind(progressListNode.message); bar.progressProperty().bind(progressListNode.progress); @@ -336,8 +335,7 @@ public final class TaskListPane extends StackPane { pane.setLeft(null); pane.setRight(message); pane.setBottom(bar); - } else if (item instanceof StageNode) { - var stageNode = (StageNode) item; + } else if (item instanceof StageNode stageNode) { title.textProperty().bind(stageNode.title); message.setText(""); bar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS); @@ -493,12 +491,10 @@ public final class TaskListPane extends StackPane { private ProgressListNode(Task task) { this.title = task.getName(); - message.bind(task.messageProperty()); progress.bind(task.progressProperty()); } public void unbind() { - message.unbind(); progress.unbind(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java index 71ae7cfd1..fe16a8a2a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java @@ -92,7 +92,7 @@ public final class AsyncTaskExecutor extends TaskExecutor { throw new IllegalStateException("Cannot cancel a not started TaskExecutor"); } - cancelled.set(true); + cancelled = true; } private CompletableFuture executeTasksExceptionally(Task parentTask, Collection> tasks) { @@ -101,8 +101,6 @@ public final class AsyncTaskExecutor extends TaskExecutor { return CompletableFuture.completedFuture(null) .thenComposeAsync(unused -> { - totTask.addAndGet(tasks.size()); - if (isCancelled()) { for (Task task : tasks) task.setException(new CancellationException()); return CompletableFuture.runAsync(this::checkCancellation); @@ -164,7 +162,7 @@ public final class AsyncTaskExecutor extends TaskExecutor { } task.setResult(result); - task.onDone().fireEvent(new TaskEvent(this, task, false)); + task.fireDoneEvent(this, false); taskListeners.forEach(it -> it.onFinished(task)); task.setState(Task.TaskState.SUCCEEDED); @@ -173,14 +171,13 @@ public final class AsyncTaskExecutor extends TaskExecutor { }) .exceptionally(throwable -> { Throwable resolved = resolveException(throwable); - if (resolved instanceof Exception) { - Exception e = (Exception) resolved; + if (resolved instanceof Exception e) { if (e instanceof InterruptedException || e instanceof CancellationException) { task.setException(null); if (task.getSignificance().shouldLog()) { LOG.trace("Task aborted: " + task.getName()); } - task.onDone().fireEvent(new TaskEvent(this, task, true)); + task.fireDoneEvent(this, true); taskListeners.forEach(it -> it.onFailed(task, e)); } else { task.setException(e); @@ -188,7 +185,7 @@ public final class AsyncTaskExecutor extends TaskExecutor { if (task.getSignificance().shouldLog()) { LOG.trace("Task failed: " + task.getName(), e); } - task.onDone().fireEvent(new TaskEvent(this, task, true)); + task.fireDoneEvent(this, true); taskListeners.forEach(it -> it.onFailed(task, e)); } @@ -278,7 +275,7 @@ public final class AsyncTaskExecutor extends TaskExecutor { LOG.trace("Task finished: " + task.getName()); } - task.onDone().fireEvent(new TaskEvent(this, task, false)); + task.fireDoneEvent(this, false); taskListeners.forEach(it -> it.onFinished(task)); task.setState(Task.TaskState.SUCCEEDED); @@ -300,7 +297,7 @@ public final class AsyncTaskExecutor extends TaskExecutor { LOG.trace("Task failed: " + task.getName(), e); } } - task.onDone().fireEvent(new TaskEvent(this, task, true)); + task.fireDoneEvent(this, true); taskListeners.forEach(it -> it.onFailed(task, e)); task.setState(Task.TaskState.FAILED); @@ -311,8 +308,8 @@ public final class AsyncTaskExecutor extends TaskExecutor { } private CompletableFuture executeTask(Task parentTask, Task task) { - if (task instanceof CompletableFutureTask) { - return executeCompletableFutureTask(parentTask, (CompletableFutureTask) task); + if (task instanceof CompletableFutureTask completableFutureTask) { + return executeCompletableFutureTask(parentTask, completableFutureTask); } else { return executeNormalTask(parentTask, task); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index 0b0799ea2..a0ee0dd20 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -18,18 +18,18 @@ package org.jackhuang.hmcl.task; import javafx.application.Platform; +import javafx.beans.property.DoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.SimpleDoubleProperty; import org.jackhuang.hmcl.event.EventManager; -import org.jackhuang.hmcl.util.InvocationDispatcher; import org.jackhuang.hmcl.util.function.ExceptionalConsumer; import org.jackhuang.hmcl.util.function.ExceptionalFunction; import org.jackhuang.hmcl.util.function.ExceptionalRunnable; import org.jackhuang.hmcl.util.function.ExceptionalSupplier; import org.jetbrains.annotations.Nullable; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; @@ -47,8 +47,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; */ public abstract class Task { - private final EventManager onDone = new EventManager<>(); - /** * True if not logging when executing this task. */ @@ -64,9 +62,9 @@ public abstract class Task { } // cancel - private Supplier cancelled; + private BooleanSupplier cancelled; - final void setCancelled(Supplier cancelled) { + final void setCancelled(BooleanSupplier cancelled) { this.cancelled = cancelled; } @@ -76,7 +74,7 @@ public abstract class Task { return true; } - return cancelled != null ? cancelled.get() : false; + return cancelled != null && cancelled.getAsBoolean(); } // stage @@ -92,6 +90,7 @@ public abstract class Task { /** * You must initialize stage in constructor. + * * @param stage the stage */ protected final void setStage(String stage) { @@ -211,10 +210,10 @@ public abstract class Task { } // name - private String name = getClass().getName(); + private String name; public String getName() { - return name; + return name != null ? name : getClass().getName(); } public Task setName(String name) { @@ -236,7 +235,7 @@ public abstract class Task { /** * Returns the result of this task. - * + *

* The result will be generated only if the execution is completed. */ public T getResult() { @@ -270,7 +269,8 @@ public abstract class Task { * @throws InterruptedException if current thread is interrupted * @see Thread#isInterrupted() */ - public void preExecute() throws Exception {} + public void preExecute() throws Exception { + } /** * @throws InterruptedException if current thread is interrupted @@ -284,7 +284,7 @@ public abstract class Task { /** * This method will be called after dependency tasks terminated all together. - * + *

* You can check whether dependencies succeed in this method by calling * {@link Task#isDependenciesSucceeded()} no matter when * {@link Task#isRelyingOnDependencies()} returns true or false. @@ -293,7 +293,8 @@ public abstract class Task { * @see Thread#isInterrupted() * @see Task#isDependenciesSucceeded() */ - public void postExecute() throws Exception {} + public void postExecute() throws Exception { + } /** * The collection of sub-tasks that should execute **before** this task running. @@ -310,44 +311,74 @@ public abstract class Task { return Collections.emptySet(); } + private volatile EventManager onDone; + public EventManager onDone() { + EventManager onDone = this.onDone; + if (onDone == null) { + synchronized (this) { + onDone = this.onDone; + if (onDone == null) { + this.onDone = onDone = new EventManager<>(); + } + } + } + return onDone; } - protected long getProgressInterval() { - return 1000L; + void fireDoneEvent(Object source, boolean failed) { + EventManager onDone = this.onDone; + if (onDone != null) + onDone.fireEvent(new TaskEvent(source, this, failed)); } - private long lastTime = Long.MIN_VALUE; - private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(this, "progress", -1); - private final InvocationDispatcher progressUpdate = InvocationDispatcher.runOn(Platform::runLater, progress::set); + private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1); public ReadOnlyDoubleProperty progressProperty() { - return progress.getReadOnlyProperty(); + return progress; } + private long lastUpdateProgressTime = 0L; + protected void updateProgress(long progress, long total) { updateProgress(1.0 * progress / total); } protected void updateProgress(double progress) { - if (progress < 0 || progress > 1.0) - throw new IllegalArgumentException("Progress is must between 0 and 1."); + if (progress < 0 || progress > 1.0 || Double.isNaN(progress)) + throw new IllegalArgumentException("Progress must be between 0 and 1."); + long now = System.currentTimeMillis(); - if (lastTime == Long.MIN_VALUE || now - lastTime >= getProgressInterval()) { + if (progress == 1.0 || now - lastUpdateProgressTime >= 1000L) { updateProgressImmediately(progress); - lastTime = now; + lastUpdateProgressTime = now; } } - protected void updateProgressImmediately(double progress) { - progressUpdate.accept(progress); + //region Helpers for updateProgressImmediately + + @SuppressWarnings("FieldMayBeFinal") + private volatile double pendingProgress = -1.0; + + /// @see Task#pendingProgress + private static final VarHandle PENDING_PROGRESS_HANDLE; + + static { + try { + PENDING_PROGRESS_HANDLE = MethodHandles.lookup() + .findVarHandle(Task.class, "pendingProgress", double.class); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new ExceptionInInitializerError(e); + } } + //endregion updateProgressImmediately - private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(this, "message", null); - - public final ReadOnlyStringProperty messageProperty() { - return message.getReadOnlyProperty(); + protected void updateProgressImmediately(double progress) { + // assert progress >= 0 && progress <= 1.0; + if ((double) PENDING_PROGRESS_HANDLE.getAndSet(this, progress) == -1.0) { + Platform.runLater(() -> this.progress.set((double) PENDING_PROGRESS_HANDLE.getAndSet(this, -1.0))); + } } public final T run() throws Exception { @@ -359,16 +390,14 @@ public abstract class Task { execute(); for (Task task : getDependencies()) doSubTask(task); - onDone.fireEvent(new TaskEvent(this, this, false)); + fireDoneEvent(this, false); return getResult(); } private void doSubTask(Task task) throws Exception { - message.bind(task.message); progress.bind(task.progress); task.run(); - message.unbind(); progress.unbind(); } @@ -402,7 +431,7 @@ public abstract class Task { * normally, is executed using the default Executor, with this * task's result as the argument to the supplied function. * - * @param fn the function to use to compute the value of the returned Task + * @param fn the function to use to compute the value of the returned Task * @param the function's return type * @return the new Task */ @@ -416,8 +445,8 @@ public abstract class Task { * task's result as the argument to the supplied function. * * @param executor the executor to use for asynchronous execution - * @param fn the function to use to compute the value of the returned Task - * @param the function's return type + * @param fn the function to use to compute the value of the returned Task + * @param the function's return type * @return the new Task */ public Task thenApplyAsync(Executor executor, ExceptionalFunction fn) { @@ -429,10 +458,10 @@ public abstract class Task { * normally, is executed using the supplied Executor, with this * task's result as the argument to the supplied function. * - * @param name the name of this new Task for displaying + * @param name the name of this new Task for displaying * @param executor the executor to use for asynchronous execution - * @param fn the function to use to compute the value of the returned Task - * @param the function's return type + * @param fn the function to use to compute the value of the returned Task + * @param the function's return type * @return the new Task */ public Task thenApplyAsync(String name, Executor executor, ExceptionalFunction fn) { @@ -445,7 +474,7 @@ public abstract class Task { * task's result as the argument to the supplied action. * * @param action the action to perform before completing the - * returned Task + * returned Task * @return the new Task */ public Task thenAcceptAsync(ExceptionalConsumer action) { @@ -457,7 +486,7 @@ public abstract class Task { * normally, is executed using the supplied Executor, with this * task's result as the argument to the supplied action. * - * @param action the action to perform before completing the returned Task + * @param action the action to perform before completing the returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -470,8 +499,8 @@ public abstract class Task { * normally, is executed using the supplied Executor, with this * task's result as the argument to the supplied action. * - * @param name the name of this new Task for displaying - * @param action the action to perform before completing the returned Task + * @param name the name of this new Task for displaying + * @param action the action to perform before completing the returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -487,7 +516,7 @@ public abstract class Task { * normally, executes the given action using the default Executor. * * @param action the action to perform before completing the - * returned Task + * returned Task * @return the new Task */ public Task thenRunAsync(ExceptionalRunnable action) { @@ -498,8 +527,8 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, executes the given action using the supplied Executor. * - * @param action the action to perform before completing the - * returned Task + * @param action the action to perform before completing the + * returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -511,9 +540,9 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, executes the given action using the supplied Executor. * - * @param name the name of this new Task for displaying - * @param action the action to perform before completing the - * returned Task + * @param name the name of this new Task for displaying + * @param action the action to perform before completing the + * returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -528,7 +557,7 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, is executed using the default Executor. * - * @param fn the function to use to compute the value of the returned Task + * @param fn the function to use to compute the value of the returned Task * @param the function's return type * @return the new Task */ @@ -541,8 +570,8 @@ public abstract class Task { * normally, is executed using the default Executor. * * @param name the name of this new Task for displaying - * @param fn the function to use to compute the value of the returned Task - * @param the function's return type + * @param fn the function to use to compute the value of the returned Task + * @param the function's return type * @return the new Task */ public final Task thenSupplyAsync(String name, Callable fn) { @@ -554,7 +583,7 @@ public abstract class Task { * normally, is executed. * * @param other the another Task - * @param the type of the returned Task's result + * @param the type of the returned Task's result * @return the Task */ public final Task thenComposeAsync(Task other) { @@ -565,7 +594,7 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, is executed. * - * @param fn the function returning a new Task + * @param fn the function returning a new Task * @param the type of the returned Task's result * @return the Task */ @@ -577,9 +606,9 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, is executed. * - * @param fn the function returning a new Task + * @param fn the function returning a new Task * @param executor the executor to use for asynchronous execution - * @param the type of the returned Task's result + * @param the type of the returned Task's result * @return the Task */ public final Task thenComposeAsync(Executor executor, ExceptionalSupplier, ?> fn) { @@ -591,7 +620,7 @@ public abstract class Task { * normally, is executed with result of this task as the argument * to the supplied function. * - * @param fn the function returning a new Task + * @param fn the function returning a new Task * @param the type of the returned Task's result * @return the Task */ @@ -604,9 +633,9 @@ public abstract class Task { * normally, is executed with result of this task as the argument * to the supplied function. * - * @param fn the function returning a new Task + * @param fn the function returning a new Task * @param executor the executor to use for asynchronous execution - * @param the type of the returned Task's result + * @param the type of the returned Task's result * @return the Task */ public Task thenComposeAsync(Executor executor, ExceptionalFunction, E> fn) { @@ -626,7 +655,7 @@ public abstract class Task { * normally, executes the given action using the default Executor. * * @param action the action to perform before completing the - * returned Task + * returned Task * @return the new Task */ public Task withRunAsync(ExceptionalRunnable action) { @@ -637,8 +666,8 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, executes the given action using the supplied Executor. * - * @param action the action to perform before completing the - * returned Task + * @param action the action to perform before completing the + * returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -650,9 +679,9 @@ public abstract class Task { * Returns a new Task that, when this task completes * normally, executes the given action using the supplied Executor. * - * @param name the name of this new Task for displaying - * @param action the action to perform before completing the - * returned Task + * @param name the name of this new Task for displaying + * @param action the action to perform before completing the + * returned Task * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -690,7 +719,7 @@ public abstract class Task { * encounters an exception, then the returned task exceptionally completes * with this exception unless this task also completed exceptionally. * - * @param action the action to perform + * @param action the action to perform * @param executor the executor to use for asynchronous execution * @return the new Task */ @@ -994,10 +1023,12 @@ public abstract class Task { FAILED } + @FunctionalInterface public interface FinalizedCallback { void execute(Exception exception) throws Exception; } + @FunctionalInterface public interface FinalizedCallbackWithResult { void execute(T result, Exception exception) throws Exception; } @@ -1063,7 +1094,7 @@ public abstract class Task { /** * A task that combines two tasks and make sure pred runs before succ. * - * @param fn a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred. + * @param fn a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred. * @param relyingOnDependents true if this task chain will be broken when task pred fails. */ UniCompose(ExceptionalSupplier, ?> fn, boolean relyingOnDependents) { @@ -1073,7 +1104,7 @@ public abstract class Task { /** * A task that combines two tasks and make sure pred runs before succ. * - * @param fn a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred. + * @param fn a callback that returns the task runs after pred, succ will be executed asynchronously. You can do something that relies on the result of pred. * @param relyingOnDependents true if this task chain will be broken when task pred fails. */ UniCompose(ExceptionalFunction, ?> fn, boolean relyingOnDependents) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index 43dac3e36..0655c342d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -20,20 +20,17 @@ package org.jackhuang.hmcl.task; import org.jetbrains.annotations.Nullable; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; public abstract class TaskExecutor { protected final Task firstTask; - protected final List taskListeners = new ArrayList<>(); - protected final AtomicInteger totTask = new AtomicInteger(0); - protected final AtomicBoolean cancelled = new AtomicBoolean(false); + protected final List taskListeners = new ArrayList<>(0); + protected volatile boolean cancelled = false; protected Exception exception; private final List stages; public TaskExecutor(Task task) { this.firstTask = task; - this.stages = task instanceof Task.StagesHintTask ? ((Task.StagesHintTask) task).getStages() : Collections.emptyList(); + this.stages = task instanceof Task.StagesHintTask hintTask ? hintTask.getStages() : Collections.emptyList(); } public void addTaskListener(TaskListener taskListener) { @@ -59,11 +56,7 @@ public abstract class TaskExecutor { public abstract void cancel(); public boolean isCancelled() { - return cancelled.get(); - } - - public int getTaskCount() { - return totTask.get(); + return cancelled; } public List getStages() {