From 3339c45dda86ab00f6c1927a8086755b7ca3cbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 16:37:18 +0100 Subject: [PATCH] render jobs take their world snapshots one per tick (a burst or max-concurrent > 1 no longer stacks several snapshots into one tick), trace threads numbered (all were named -0) --- .../pixelpics/render/RenderManager.java | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java index cb8dedd..09e94e2 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; @@ -24,6 +25,7 @@ import java.util.function.Supplier; import org.bukkit.Bukkit; import org.bukkit.plugin.IllegalPluginAccessException; import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; /** * Server-friendly scheduling for photo renders. Bounds CPU use and protects the server tick by: @@ -33,6 +35,8 @@ import org.bukkit.plugin.Plugin; * {@code queueSize} more waiting; further requests are rejected. *
  • CPU: the ray tracing runs on a dedicated {@link ForkJoinPool} of {@code threads} * {@link Thread#MIN_PRIORITY} workers instead of fanning out over the common pool.
  • + *
  • Main thread: at most one world snapshot ({@link #onMainThread}) per tick, also when several + * jobs start at once.
  • * * *

    Reserve a slot with {@link #tryReserve(UUID)}; if accepted, run the heavy work via @@ -55,6 +59,10 @@ public final class RenderManager { private final ScheduledExecutorService watchdog; private final Set activeUsers = ConcurrentHashMap.newKeySet(); private final Set> jobs = ConcurrentHashMap.newKeySet(); + /** Main-thread tasks of the jobs ({@link #onMainThread}), run one per tick by {@link #runNextMainTask}. */ + private final ConcurrentLinkedQueue mainTasks = new ConcurrentLinkedQueue<>(); + + private final BukkitTask mainTaskPump; private volatile boolean shuttingDown = false; private final AtomicInteger inFlight = new AtomicInteger(); private final int capacity; @@ -78,6 +86,7 @@ public final class RenderManager { thread.setDaemon(true); return thread; }); + this.mainTaskPump = Bukkit.getScheduler().runTaskTimer(plugin, this::runNextMainTask, 1, 1); } /** The bounded, low-priority pool the ray tracer must run its parallel work on. */ @@ -148,28 +157,24 @@ public final class RenderManager { } /** - * From a dispatched job's {@code work}: runs {@code task} on the main thread (next tick) and waits for its result. - * Lets a job capture its world snapshot only once it is its turn, so queued photos hold no snapshot. Returns null - * without running {@code task} once {@code cancelled} is set (timeout, shutdown), the plugin is disabled or the - * worker is interrupted. + * From a dispatched job's {@code work}: runs {@code task} on the main thread and waits for its result. Lets a job + * capture its world snapshot only once it is its turn, so queued photos hold no snapshot. The tasks of all jobs run + * one per tick in request order, so jobs starting together (a burst, or {@code max-concurrent} > 1) never stack + * their snapshots into one long tick. Returns null without running {@code task} once {@code cancelled} is set + * (timeout, shutdown), the plugin is disabled or the worker is interrupted. */ public T onMainThread(Supplier task, AtomicBoolean cancelled) { + if (shuttingDown) return null; CompletableFuture result = new CompletableFuture<>(); - try { - Bukkit.getScheduler().runTask(plugin, () -> { - if (cancelled.get()) { - result.complete(null); - return; - } - try { - result.complete(task.get()); - } catch (Throwable t) { - result.completeExceptionally(t); - } - }); - } catch (IllegalPluginAccessException e) { - return null; // disabled meanwhile - } + mainTasks.add(new MainTask( + () -> { + try { + result.complete(task.get()); + } catch (Throwable t) { + result.completeExceptionally(t); + } + }, + cancelled)); while (!cancelled.get()) { try { return result.get(50, TimeUnit.MILLISECONDS); @@ -187,6 +192,18 @@ public final class RenderManager { return null; } + /** Every tick: runs the oldest main-thread task whose job is still alive (cancelled ones are dropped). */ + private void runNextMainTask() { + MainTask next; + while ((next = mainTasks.poll()) != null) { + if (next.cancelled.get()) continue; // its worker returns null on its own + next.run.run(); + return; + } + } + + private record MainTask(Runnable run, AtomicBoolean cancelled) {} + /** * Stops all rendering without leaving a job undelivered: running and queued jobs are cancelled, the workers get * a few seconds to wind down, then every job's callback runs here (on the main thread) — {@code onSuccess} for @@ -194,7 +211,9 @@ public final class RenderManager { */ public void shutdown() { shuttingDown = true; + mainTaskPump.cancel(); for (Job job : jobs) job.cancelled.set(true); + mainTasks.clear(); watchdog.shutdownNow(); dispatcher.shutdownNow(); try { @@ -237,9 +256,11 @@ public final class RenderManager { } private static ForkJoinPool.ForkJoinWorkerThreadFactory lowPriorityForkJoinFactory() { + AtomicInteger counter = new AtomicInteger(); return pool -> { ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - thread.setName("PixelPics-trace-" + thread.getPoolIndex()); + // not getPoolIndex(): it is only assigned once the worker registers, here every name would end in -0 + thread.setName("PixelPics-trace-" + counter.incrementAndGet()); thread.setPriority(Thread.MIN_PRIORITY); thread.setDaemon(true); return thread;