diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/FastPlanStorage.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/FastPlanStorage.java deleted file mode 100644 index 69ddcc029..000000000 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/FastPlanStorage.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.jozufozu.flywheel.impl.visualization.storage; - -import java.util.ArrayList; -import java.util.List; - -import com.jozufozu.flywheel.api.task.TaskExecutor; -import com.jozufozu.flywheel.lib.task.PlanUtil; -import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; -import com.jozufozu.flywheel.lib.task.functional.ConsumerWithContext; - -public class FastPlanStorage implements SimplyComposedPlan { - private final List objects = new ArrayList<>(); - private final ConsumerWithContext consumer; - - public FastPlanStorage(ConsumerWithContext consumer) { - this.consumer = consumer; - } - - public void add(T object) { - objects.add(object); - } - - public void remove(T object) { - objects.remove(object); - } - - public void clear() { - objects.clear(); - } - - @Override - public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { - PlanUtil.distribute(taskExecutor, context, onCompletion, objects, consumer); - } -} diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/LitVisualStorage.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/LitVisualStorage.java index 58bc4342d..166f7dadb 100644 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/LitVisualStorage.java +++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/LitVisualStorage.java @@ -11,7 +11,7 @@ import com.jozufozu.flywheel.api.task.Plan; import com.jozufozu.flywheel.api.task.TaskExecutor; import com.jozufozu.flywheel.api.visual.LitVisual; import com.jozufozu.flywheel.api.visual.VisualFrameContext; -import com.jozufozu.flywheel.lib.task.PlanUtil; +import com.jozufozu.flywheel.lib.task.Distribute; import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; import com.jozufozu.flywheel.lib.task.Synchronizer; @@ -56,7 +56,7 @@ public class LitVisualStorage { for (long section : sectionsUpdatedThisFrame) { var visuals = sections2Visuals.get(section); if (visuals != null && !visuals.isEmpty()) { - taskExecutor.execute(() -> PlanUtil.distribute(taskExecutor, updateId, sync, visuals, Updater::updateLight)); + taskExecutor.execute(() -> Distribute.tasks(taskExecutor, updateId, sync, visuals, Updater::updateLight)); } else { sync.decrementAndEventuallyRun(); } diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/PlanStorage.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/PlanStorage.java deleted file mode 100644 index ace588c1a..000000000 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/PlanStorage.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.jozufozu.flywheel.impl.visualization.storage; - -import java.util.ArrayList; -import java.util.List; - -import com.jozufozu.flywheel.api.task.Plan; -import com.jozufozu.flywheel.api.task.TaskExecutor; -import com.jozufozu.flywheel.lib.task.PlanUtil; -import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; -import com.jozufozu.flywheel.lib.task.Synchronizer; - -public class PlanStorage implements SimplyComposedPlan { - private final List objects = new ArrayList<>(); - private final List> plans = new ArrayList<>(); - - public void add(T object, Plan plan) { - objects.add(object); - plans.add(plan); - } - - public void remove(T object) { - int index = objects.indexOf(object); - - if (index != -1) { - objects.remove(index); - plans.remove(index); - } - } - - public void clear() { - objects.clear(); - plans.clear(); - } - - @Override - public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { - final int size = plans.size(); - - if (size == 0) { - onCompletion.run(); - return; - } - - var synchronizer = new Synchronizer(size, onCompletion); - final int sliceSize = PlanUtil.sliceSize(taskExecutor, size, 8); - - if (size <= sliceSize) { - for (var t : plans) { - t.execute(taskExecutor, context, synchronizer); - } - } else if (sliceSize == 1) { - for (var t : plans) { - taskExecutor.execute(() -> t.execute(taskExecutor, context, synchronizer)); - } - } else { - int remaining = size; - - while (remaining > 0) { - int end = remaining; - remaining -= sliceSize; - int start = Math.max(remaining, 0); - - var subList = plans.subList(start, end); - taskExecutor.execute(() -> { - for (var t : subList) { - t.execute(taskExecutor, context, synchronizer); - } - }); - } - } - } -} diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/Storage.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/Storage.java index c2d104724..69e31fc54 100644 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/Storage.java +++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/storage/Storage.java @@ -1,6 +1,8 @@ package com.jozufozu.flywheel.impl.visualization.storage; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -14,7 +16,9 @@ import com.jozufozu.flywheel.api.visual.Visual; import com.jozufozu.flywheel.api.visual.VisualFrameContext; import com.jozufozu.flywheel.api.visual.VisualTickContext; import com.jozufozu.flywheel.api.visualization.VisualizationContext; +import com.jozufozu.flywheel.lib.task.ForEachPlan; import com.jozufozu.flywheel.lib.task.NestedPlan; +import com.jozufozu.flywheel.lib.task.PlanMap; import com.jozufozu.flywheel.lib.visual.SimpleDynamicVisual; import com.jozufozu.flywheel.lib.visual.SimpleTickableVisual; @@ -23,10 +27,10 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; public abstract class Storage { protected final Supplier visualizationContextSupplier; - protected final PlanStorage dynamicVisuals = new PlanStorage<>(); - protected final FastPlanStorage fastDynamicVisuals = new FastPlanStorage<>(SimpleDynamicVisual::beginFrame); - protected final PlanStorage tickableVisuals = new PlanStorage<>(); - protected final FastPlanStorage fastTickableVisuals = new FastPlanStorage<>(SimpleTickableVisual::tick); + protected final PlanMap dynamicVisuals = new PlanMap<>(); + protected final PlanMap tickableVisuals = new PlanMap<>(); + protected final List simpleDynamicVisuals = new ArrayList<>(); + protected final List simpleTickableVisuals = new ArrayList<>(); protected final LitVisualStorage litVisuals = new LitVisualStorage(); private final Map visuals = new Reference2ObjectOpenHashMap<>(); @@ -56,14 +60,14 @@ public abstract class Storage { if (visual instanceof TickableVisual tickable) { if (visual instanceof SimpleTickableVisual simpleTickable) { - fastTickableVisuals.remove(simpleTickable); + simpleTickableVisuals.remove(simpleTickable); } else { tickableVisuals.remove(tickable); } } if (visual instanceof DynamicVisual dynamic) { if (visual instanceof SimpleDynamicVisual simpleDynamic) { - fastDynamicVisuals.remove(simpleDynamic); + simpleDynamicVisuals.remove(simpleDynamic); } else { dynamicVisuals.remove(dynamic); } @@ -94,9 +98,9 @@ public abstract class Storage { public void recreateAll(float partialTick) { tickableVisuals.clear(); - fastTickableVisuals.clear(); dynamicVisuals.clear(); - fastDynamicVisuals.clear(); + simpleTickableVisuals.clear(); + simpleDynamicVisuals.clear(); litVisuals.clear(); visuals.replaceAll((obj, visual) -> { visual.delete(); @@ -133,11 +137,11 @@ public abstract class Storage { protected abstract Visual createRaw(T obj); public Plan framePlan() { - return NestedPlan.of(dynamicVisuals, fastDynamicVisuals, litVisuals.plan()); + return NestedPlan.of(dynamicVisuals, litVisuals.plan(), ForEachPlan.of(() -> simpleDynamicVisuals, SimpleDynamicVisual::beginFrame)); } public Plan tickPlan() { - return NestedPlan.of(tickableVisuals, fastTickableVisuals); + return NestedPlan.of(tickableVisuals, ForEachPlan.of(() -> simpleTickableVisuals, SimpleTickableVisual::tick)); } public void enqueueLightUpdateSections(LongSet sections) { @@ -149,7 +153,7 @@ public abstract class Storage { if (visual instanceof TickableVisual tickable) { if (visual instanceof SimpleTickableVisual simpleTickable) { - fastTickableVisuals.add(simpleTickable); + simpleTickableVisuals.add(simpleTickable); } else { tickableVisuals.add(tickable, tickable.planTick()); } @@ -157,7 +161,7 @@ public abstract class Storage { if (visual instanceof DynamicVisual dynamic) { if (visual instanceof SimpleDynamicVisual simpleDynamic) { - fastDynamicVisuals.add(simpleDynamic); + simpleDynamicVisuals.add(simpleDynamic); } else { dynamicVisuals.add(dynamic, dynamic.planFrame()); } diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/Distribute.java b/src/main/java/com/jozufozu/flywheel/lib/task/Distribute.java new file mode 100644 index 000000000..74041a7dd --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/task/Distribute.java @@ -0,0 +1,190 @@ +package com.jozufozu.flywheel.lib.task; + +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +import com.jozufozu.flywheel.api.task.Plan; +import com.jozufozu.flywheel.api.task.TaskExecutor; +import com.jozufozu.flywheel.lib.math.MoreMath; + +public final class Distribute { + /** + * Distribute the given list of tasks across the threads of the task executor. + * + *

An effort is made to balance the load across threads while also ensuring each + * runnable passed to the executor is large enough to amortize the cost of scheduling it.

+ * + * @param taskExecutor The task executor to run on. + * @param context The context to pass to each task. + * @param onCompletion The action to run when all tasks are complete. + * @param list The list of objects to run tasks on. + * @param action The action to run on each object. + * @param The context type. + * @param The object type. + */ + public static void tasks(TaskExecutor taskExecutor, C context, Runnable onCompletion, List list, BiConsumer action) { + final int size = list.size(); + + if (size == 0) { + onCompletion.run(); + return; + } + + final int sliceSize = sliceSize(taskExecutor, size); + + if (size <= sliceSize) { + for (T t : list) { + action.accept(t, context); + } + onCompletion.run(); + } else if (sliceSize == 1) { + var synchronizer = new Synchronizer(size, onCompletion); + for (T t : list) { + taskExecutor.execute(() -> { + action.accept(t, context); + synchronizer.decrementAndEventuallyRun(); + }); + } + } else { + var synchronizer = new Synchronizer(MoreMath.ceilingDiv(size, sliceSize), onCompletion); + int remaining = size; + + while (remaining > 0) { + int end = remaining; + remaining -= sliceSize; + int start = Math.max(remaining, 0); + + var subList = list.subList(start, end); + taskExecutor.execute(() -> { + for (T t : subList) { + action.accept(t, context); + } + synchronizer.decrementAndEventuallyRun(); + }); + } + } + } + + /** + * Distribute the given list of tasks in chunks across the threads of the task executor. + * + *

Unlike {@link #tasks(TaskExecutor, Object, Runnable, List, BiConsumer)}, this method + * gives the action a list of objects to work on, rather than a single object. This may be handy + * for when you can share some thread local objects between individual elements of the list.

+ * + *

An effort is made to balance the load across threads while also ensuring each + * runnable passed to the executor is large enough to amortize the cost of scheduling it.

+ * + * @param taskExecutor The task executor to run on. + * @param context The context to pass to each task. + * @param onCompletion The action to run when all tasks are complete. + * @param list The list of objects to run tasks on. + * @param action The action to run on each slice. + * @param The context type. + * @param The object type. + */ + public static void slices(TaskExecutor taskExecutor, C context, Runnable onCompletion, List list, BiConsumer, C> action) { + final int size = list.size(); + + if (size == 0) { + onCompletion.run(); + return; + } + + final int sliceSize = sliceSize(taskExecutor, size); + + if (size <= sliceSize) { + action.accept(list, context); + onCompletion.run(); + } else if (sliceSize == 1) { + var synchronizer = new Synchronizer(size, onCompletion); + for (T t : list) { + taskExecutor.execute(() -> { + action.accept(Collections.singletonList(t), context); + synchronizer.decrementAndEventuallyRun(); + }); + } + } else { + var synchronizer = new Synchronizer(MoreMath.ceilingDiv(size, sliceSize), onCompletion); + int remaining = size; + + while (remaining > 0) { + int end = remaining; + remaining -= sliceSize; + int start = Math.max(remaining, 0); + + var subList = list.subList(start, end); + taskExecutor.execute(() -> { + action.accept(subList, context); + synchronizer.decrementAndEventuallyRun(); + }); + } + } + } + + /** + * Distribute the given list of plans across the threads of the task executor. + * + *

Plan scheduling is normally lightweight compared to the cost of execution, + * but when many hundreds or thousands of plans need to be scheduled it may be beneficial + * to parallelize. This method does exactly that, distributing larger chunks of plans to + * be scheduled in batches.

+ * + *

An effort is made to balance the load across threads while also ensuring each + * runnable passed to the executor is large enough to amortize the cost of scheduling it.

+ * + * @param taskExecutor The task executor to run on. + * @param context The context to pass to the plans. + * @param onCompletion The action to run when all plans are complete. + * @param plans The list of plans to execute. + * @param The context type. + */ + public static void plans(TaskExecutor taskExecutor, C context, Runnable onCompletion, List> plans) { + final int size = plans.size(); + + if (size == 0) { + onCompletion.run(); + return; + } + + var synchronizer = new Synchronizer(size, onCompletion); + final int sliceSize = sliceSize(taskExecutor, size, 8); + + if (size <= sliceSize) { + for (var t : plans) { + t.execute(taskExecutor, context, synchronizer); + } + } else if (sliceSize == 1) { + for (var t : plans) { + taskExecutor.execute(() -> t.execute(taskExecutor, context, synchronizer)); + } + } else { + int remaining = size; + + while (remaining > 0) { + int end = remaining; + remaining -= sliceSize; + int start = Math.max(remaining, 0); + + var subList = plans.subList(start, end); + taskExecutor.execute(() -> { + for (var t : subList) { + t.execute(taskExecutor, context, synchronizer); + } + }); + } + } + } + + public static int sliceSize(TaskExecutor taskExecutor, int totalSize) { + return sliceSize(taskExecutor, totalSize, 32); + } + + public static int sliceSize(TaskExecutor taskExecutor, int totalSize, int denominator) { + return MoreMath.ceilingDiv(totalSize, taskExecutor.getThreadCount() * denominator); + } + + private Distribute() { + } +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/ForEachPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/ForEachPlan.java index 55b6bef46..0c08bdc72 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/task/ForEachPlan.java +++ b/src/main/java/com/jozufozu/flywheel/lib/task/ForEachPlan.java @@ -37,6 +37,6 @@ public record ForEachPlan(SupplierWithContext> listSupplier, @Override public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { - taskExecutor.execute(() -> PlanUtil.distribute(taskExecutor, context, onCompletion, listSupplier.get(context), action)); + taskExecutor.execute(() -> Distribute.tasks(taskExecutor, context, onCompletion, listSupplier.get(context), action)); } } diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/ForEachSlicePlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/ForEachSlicePlan.java index 691aa080c..2f2588eb5 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/task/ForEachSlicePlan.java +++ b/src/main/java/com/jozufozu/flywheel/lib/task/ForEachSlicePlan.java @@ -37,6 +37,6 @@ public record ForEachSlicePlan(SupplierWithContext> listSupplie @Override public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { - taskExecutor.execute(() -> PlanUtil.distributeSlices(taskExecutor, context, onCompletion, listSupplier.get(context), action)); + taskExecutor.execute(() -> Distribute.slices(taskExecutor, context, onCompletion, listSupplier.get(context), action)); } } diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/PlanMap.java b/src/main/java/com/jozufozu/flywheel/lib/task/PlanMap.java new file mode 100644 index 000000000..822336ed8 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/task/PlanMap.java @@ -0,0 +1,44 @@ +package com.jozufozu.flywheel.lib.task; + +import java.util.ArrayList; +import java.util.List; + +import com.jozufozu.flywheel.api.task.Plan; +import com.jozufozu.flywheel.api.task.TaskExecutor; + +/** + * A plan that executes a dynamic list of plans in parallel. + * + *

The plans can be added/removed by association with a key object.

+ * + * @param The key type + * @param The context type + */ +public class PlanMap implements SimplyComposedPlan { + private final List keys = new ArrayList<>(); + private final List> values = new ArrayList<>(); + + public void add(K object, Plan plan) { + keys.add(object); + values.add(plan); + } + + public void remove(K object) { + int index = keys.indexOf(object); + + if (index != -1) { + keys.remove(index); + values.remove(index); + } + } + + public void clear() { + keys.clear(); + values.clear(); + } + + @Override + public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { + Distribute.plans(taskExecutor, context, onCompletion, values); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java b/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java deleted file mode 100644 index b3ff709e3..000000000 --- a/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.jozufozu.flywheel.lib.task; - -import java.util.Collections; -import java.util.List; -import java.util.function.BiConsumer; - -import com.jozufozu.flywheel.api.task.TaskExecutor; -import com.jozufozu.flywheel.lib.math.MoreMath; - -public final class PlanUtil { - public static void distribute(TaskExecutor taskExecutor, C context, Runnable onCompletion, List list, BiConsumer action) { - final int size = list.size(); - - if (size == 0) { - onCompletion.run(); - return; - } - - final int sliceSize = sliceSize(taskExecutor, size); - - if (size <= sliceSize) { - for (T t : list) { - action.accept(t, context); - } - onCompletion.run(); - } else if (sliceSize == 1) { - var synchronizer = new Synchronizer(size, onCompletion); - for (T t : list) { - taskExecutor.execute(() -> { - action.accept(t, context); - synchronizer.decrementAndEventuallyRun(); - }); - } - } else { - var synchronizer = new Synchronizer(MoreMath.ceilingDiv(size, sliceSize), onCompletion); - int remaining = size; - - while (remaining > 0) { - int end = remaining; - remaining -= sliceSize; - int start = Math.max(remaining, 0); - - var subList = list.subList(start, end); - taskExecutor.execute(() -> { - for (T t : subList) { - action.accept(t, context); - } - synchronizer.decrementAndEventuallyRun(); - }); - } - } - } - - public static void distributeSlices(TaskExecutor taskExecutor, C context, Runnable onCompletion, List list, BiConsumer, C> action) { - final int size = list.size(); - - if (size == 0) { - onCompletion.run(); - return; - } - - final int sliceSize = sliceSize(taskExecutor, size); - - if (size <= sliceSize) { - action.accept(list, context); - onCompletion.run(); - } else if (sliceSize == 1) { - var synchronizer = new Synchronizer(size, onCompletion); - for (T t : list) { - taskExecutor.execute(() -> { - action.accept(Collections.singletonList(t), context); - synchronizer.decrementAndEventuallyRun(); - }); - } - } else { - var synchronizer = new Synchronizer(MoreMath.ceilingDiv(size, sliceSize), onCompletion); - int remaining = size; - - while (remaining > 0) { - int end = remaining; - remaining -= sliceSize; - int start = Math.max(remaining, 0); - - var subList = list.subList(start, end); - taskExecutor.execute(() -> { - action.accept(subList, context); - synchronizer.decrementAndEventuallyRun(); - }); - } - } - } - - public static int sliceSize(TaskExecutor taskExecutor, int totalSize) { - return sliceSize(taskExecutor, totalSize, 32); - } - - public static int sliceSize(TaskExecutor taskExecutor, int totalSize, int denominator) { - return MoreMath.ceilingDiv(totalSize, taskExecutor.getThreadCount() * denominator); - } - - private PlanUtil() { - } -} diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java index eff105ec6..26abad7b4 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java +++ b/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java @@ -29,7 +29,7 @@ public record SimplePlan(List> parallelTasks) implemen return; } - taskExecutor.execute(() -> PlanUtil.distribute(taskExecutor, context, onCompletion, parallelTasks, RunnableWithContext::run)); + taskExecutor.execute(() -> Distribute.tasks(taskExecutor, context, onCompletion, parallelTasks, RunnableWithContext::run)); } @Override