From ff2e35c2432866e257ee60b6efde66068c2597ea Mon Sep 17 00:00:00 2001 From: tterrag Date: Fri, 18 Jan 2019 21:10:22 -0500 Subject: [PATCH] Greatly improve DeferredWorkQueue API (#5357) --- .../minecraftforge/fml/DeferredWorkQueue.java | 196 ++++++++++++++++-- .../java/net/minecraftforge/fml/ModList.java | 6 +- .../resources/assets/forge/lang/en_us.json | 1 + 3 files changed, 183 insertions(+), 20 deletions(-) diff --git a/src/main/java/net/minecraftforge/fml/DeferredWorkQueue.java b/src/main/java/net/minecraftforge/fml/DeferredWorkQueue.java index 9d1a4d827f..20705ca432 100644 --- a/src/main/java/net/minecraftforge/fml/DeferredWorkQueue.java +++ b/src/main/java/net/minecraftforge/fml/DeferredWorkQueue.java @@ -19,26 +19,190 @@ package net.minecraftforge.fml; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; +import static net.minecraftforge.fml.Logging.LOADING; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.apache.commons.lang3.time.StopWatch; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.base.Function; +import com.google.common.base.Stopwatch; + +import net.minecraft.util.IThreadListener; +import net.minecraftforge.forgespi.language.IModInfo; + +/** + * Utility for running code on the main launch thread at the next available + * opportunity. There is no guaranteed order that work from various mods will be + * run, but your own work will be run sequentially. + *

+ * Use of this class after startup is not possible. At that + * point, {@link IThreadListener} should be used instead. + *

+ * Exceptions from tasks will be handled gracefully, causing a mod loading + * error. Tasks that take egregiously long times to run will be logged. + */ public class DeferredWorkQueue { - public static ConcurrentLinkedDeque> deferredWorkQueue = new ConcurrentLinkedDeque<>(); - - public static Future enqueueWork(Callable workToEnqueue) { - final FutureWorkTask workTask = new FutureWorkTask<>(workToEnqueue); - DeferredWorkQueue.deferredWorkQueue.add(workTask); - return workTask; - } - - public static class FutureWorkTask extends FutureTask + private static class TaskInfo { - FutureWorkTask(Callable callable) - { - super(callable); + public final IModInfo owner; + public final Runnable task; + + TaskInfo(IModInfo owner, Runnable task) { + this.owner = owner; + this.task = task; } } + + /** + * {@link Runnable} except it allows throwing checked exceptions. + * + * Is to {@link Runnable} as {@link Callable} is to {@link Supplier}. + */ + @FunctionalInterface + public interface CheckedRunnable + { + void run() throws Exception; + } + + private static final Logger LOGGER = LogManager.getLogger(); + + private static ThreadLocal currentOwner = new ThreadLocal<>(); + private static List raisedExceptions = new ArrayList<>(); + + private static final ConcurrentLinkedDeque taskQueue = new ConcurrentLinkedDeque<>(); + private static final Executor deferredExecutor = r -> taskQueue.add(new TaskInfo(currentOwner.get().getModInfo(), r)); + + private static Function handleException() { + final ModContainer owner = currentOwner.get(); + return t -> { + LogManager.getLogger(DeferredWorkQueue.class).error("Encountered exception executing deferred work", t); + raisedExceptions.add(new ModLoadingException(owner.getModInfo(), owner.getCurrentState(), "fml.modloading.failedtoprocesswork", t)); + return null; + }; + } + + /** + * Run a task on the loading thread at the next available opportunity, i.e. + * after the current lifecycle event has completed. + *

+ * If the task must throw a checked exception, use + * {@link #runLaterChecked(CheckedRunnable)}. + *

+ * If the task has a result, use {@link #getLater(Supplier)} or + * {@link #getLaterChecked(Callable)}. + * + * @param workToEnqueue A {@link Runnable} to execute later, on the loading + * thread + * @return A {@link CompletableFuture} that completes at said time + */ + public static CompletableFuture runLater(Runnable workToEnqueue) { + currentOwner.set(ModThreadContext.get().getActiveContainer()); + return CompletableFuture.runAsync(workToEnqueue, deferredExecutor).exceptionally(DeferredWorkQueue.handleException()); + } + + /** + * Run a task on the loading thread at the next available opportunity, i.e. + * after the current lifecycle event has completed. This variant allows the task + * to throw a checked exception. + *

+ * If the task does not throw a checked exception, use + * {@link #runLater(Runnable)}. + *

+ * If the task has a result, use {@link #getLater(Supplier)} or + * {@link #getLaterChecked(Callable)}. + * + * @param workToEnqueue A {@link CheckedRunnable} to execute later, on the + * loading thread + * @return A {@link CompletableFuture} that completes at said time + */ + public static CompletableFuture runLaterChecked(CheckedRunnable workToEnqueue) { + return runLater(() -> { + try { + workToEnqueue.run(); + } catch (Throwable t) { + throw new CompletionException(t); + } + }); + } + + /** + * Run a task computing a result on the loading thread at the next available + * opportunity, i.e. after the current lifecycle event has completed. + *

+ * If the task throws a checked exception, use + * {@link #getLaterChecked(Callable)}. + *

+ * If the task does not have a result, use {@link #runLater(Runnable)} or + * {@link #runLaterChecked(CheckedRunnable)}. + * + * @param The result type of the task + * @param workToEnqueue A {@link Supplier} to execute later, on the loading + * thread + * @return A {@link CompletableFuture} that completes at said time + */ + public static CompletableFuture getLater(Supplier workToEnqueue) { + currentOwner.set(ModThreadContext.get().getActiveContainer()); + return CompletableFuture.supplyAsync(workToEnqueue, deferredExecutor).exceptionally(DeferredWorkQueue.handleException()); + } + + /** + * Run a task computing a result on the loading thread at the next available + * opportunity, i.e. after the current lifecycle event has completed. This + * variant allows the task to throw a checked exception. + *

+ * If the task does not throw a checked exception, use + * {@link #getLater(Callable)}. + *

+ * If the task does not have a result, use {@link #runLater(Runnable)} or + * {@link #runLaterChecked(CheckedRunnable)}. + * + * @param The result type of the task + * @param workToEnqueue A {@link Supplier} to execute later, on the loading + * thread + * @return A {@link CompletableFuture} that completes at said time + */ + public static CompletableFuture getLaterChecked(Callable workToEnqueue) { + return getLater(() -> { + try { + return workToEnqueue.call(); + } catch (Throwable t) { + throw new CompletionException(t); + } + }); + } + + static void clear() { + taskQueue.clear(); + } + + static void runTasks(ModLoadingStage fromStage, Consumer> errorHandler) { + raisedExceptions.clear(); + if (taskQueue.isEmpty()) return; // Don't log unnecessarily + LOGGER.info(LOADING, "Dispatching synchronous work after {}: {} jobs", fromStage, taskQueue.size()); + StopWatch globalTimer = StopWatch.createStarted(); + while (!taskQueue.isEmpty()) { + TaskInfo taskinfo = taskQueue.poll(); + Stopwatch timer = Stopwatch.createStarted(); + taskinfo.task.run(); + timer.stop(); + if (timer.elapsed(TimeUnit.SECONDS) >= 1) { + LOGGER.warn(LOADING, "Mod '{}' took {} to run a deferred task.", taskinfo.owner.getModId(), timer); + } + } + LOGGER.info(LOADING, "Synchronous work queue completed in {}", globalTimer); + errorHandler.accept(raisedExceptions); + } } diff --git a/src/main/java/net/minecraftforge/fml/ModList.java b/src/main/java/net/minecraftforge/fml/ModList.java index e5aa1e851d..6af85bc904 100644 --- a/src/main/java/net/minecraftforge/fml/ModList.java +++ b/src/main/java/net/minecraftforge/fml/ModList.java @@ -91,7 +91,7 @@ public class ModList public void dispatchLifeCycleEvent(LifecycleEventProvider.LifecycleEvent lifecycleEvent, final Consumer> errorHandler) { FMLLoader.getLanguageLoadingProvider().forEach(lp->lp.consumeLifecycleEvent(()->lifecycleEvent)); - DeferredWorkQueue.deferredWorkQueue.clear(); + DeferredWorkQueue.clear(); try { modLoadingThreadPool.submit(()->this.mods.parallelStream().forEach(m->m.transitionState(lifecycleEvent, errorHandler))).get(); @@ -100,9 +100,7 @@ public class ModList { LOGGER.error(LOADING, "Encountered an exception during parallel processing", e); } - LOGGER.debug(LOADING, "Dispatching synchronous work, {} jobs", DeferredWorkQueue.deferredWorkQueue.size()); - DeferredWorkQueue.deferredWorkQueue.forEach(FutureTask::run); - LOGGER.debug(LOADING, "Synchronous work queue complete"); + DeferredWorkQueue.runTasks(lifecycleEvent.fromStage(), errorHandler); FMLLoader.getLanguageLoadingProvider().forEach(lp->lp.consumeLifecycleEvent(()->lifecycleEvent)); } diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 1bac7548d1..6cf899a7d0 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -25,6 +25,7 @@ "fml.modloading.errorduringevent":"{0,modinfo,name} ({0,modinfo,id}) encountered an error during the {1,lower} event phase\n\u00a77{2,exc,msg}", "fml.modloading.failedtoloadforge": "Failed to load forge", "fml.modloading.missingdependency": "Mod \u00a7e{4}\u00a7r requires \u00a76{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a76{3}\u00a7r\u00a77 is \u00a7o{6,i18n,fml.messages.artifactversion.ornotinstalled}", + "fml.modloading.failedtoprocesswork":"{0,modinfo,name} ({0,modinfo,id}) encountered an error processing deferred work\n\u00a77{2,exc,msg}", "fml.messages.artifactversion.ornotinstalled":"{0,ornull,fml.messages.artifactversion.notinstalled}", "fml.messages.artifactversion":"{0,ornull,fml.messages.artifactversion.none}",