From be94c33ea6394fab32e98074cfa6336588088d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 5 Nov 2026 18:56:39 +0100 Subject: [PATCH] render stats and live limits: photo counters and phase timings (queue, snapshot, trace, dither), running/queued load, render limits applicable to the running pool (trace parallelism, concurrent renders, queue, timeout), debug log switch with stack traces of failed renders --- .../pixelpics/render/RenderManager.java | 67 ++++++++++++++++-- .../pixelpics/render/RenderStats.java | 69 +++++++++++++++++++ .../minecraft/pixelpics/utils/DebugLog.java | 27 ++++++++ 3 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderStats.java create mode 100644 src/main/java/eu/mhsl/minecraft/pixelpics/utils/DebugLog.java 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 29f9f57..b7801e4 100644 --- a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderManager.java @@ -1,5 +1,6 @@ package eu.mhsl.minecraft.pixelpics.render; +import eu.mhsl.minecraft.pixelpics.utils.DebugLog; import java.util.List; import java.util.Set; import java.util.UUID; @@ -22,6 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.plugin.IllegalPluginAccessException; import org.bukkit.plugin.Plugin; @@ -66,12 +68,16 @@ public final class RenderManager { private final ChunkHolds chunkHolds; private volatile boolean shuttingDown = false; private final AtomicInteger inFlight = new AtomicInteger(); - private final int capacity; - private final long timeoutMillis; + private final AtomicInteger running = new AtomicInteger(); + private final RenderStats stats = new RenderStats(); + private volatile int capacity; + private volatile int maxConcurrent; + private volatile long timeoutMillis; public RenderManager(Plugin plugin, int threads, int maxConcurrent, int queueSize, int timeoutSeconds) { this.plugin = plugin; this.capacity = maxConcurrent + queueSize; + this.maxConcurrent = maxConcurrent; this.timeoutMillis = Math.max(1L, timeoutSeconds) * 1000L; this.tracePool = new ForkJoinPool(threads, lowPriorityForkJoinFactory(), null, false); this.dispatcher = new ThreadPoolExecutor( @@ -91,6 +97,50 @@ public final class RenderManager { this.chunkHolds = new ChunkHolds(plugin); } + /** + * Applies new limits to the running manager ({@code /pixelpic reload}): trace threads, concurrent renders, queue + * size and timeout. Running and queued jobs are kept; a smaller capacity only rejects new photos until enough + * are done. + */ + public void configure(int threads, int maxConcurrent, int queueSize, int timeoutSeconds) { + tracePool.setParallelism(threads); + if (maxConcurrent >= dispatcher.getMaximumPoolSize()) { + dispatcher.setMaximumPoolSize(maxConcurrent); + dispatcher.setCorePoolSize(maxConcurrent); + } else { + dispatcher.setCorePoolSize(maxConcurrent); + dispatcher.setMaximumPoolSize(maxConcurrent); + } + this.maxConcurrent = maxConcurrent; + this.capacity = maxConcurrent + queueSize; + this.timeoutMillis = Math.max(1L, timeoutSeconds) * 1000L; + } + + /** Photos rendering now (incl. waiting for their snapshot), photos waiting for a worker, and the limits. */ + public record Load(int running, int queued, int maxConcurrent, int queueSize, int threads, long timeoutSeconds) {} + + public Load load() { + int total = inFlight.get(); + int run = Math.min(running.get(), total); + return new Load( + run, + Math.max(0, total - run), + maxConcurrent, + capacity - maxConcurrent, + tracePool.getParallelism(), + timeoutMillis / 1000); + } + + /** Counters and timings of the photos since the start. */ + public RenderStats stats() { + return stats; + } + + /** True while no job is running, queued or waiting for its callback. */ + public boolean isIdle() { + return jobs.isEmpty(); + } + /** Keeps the chunks of queued photos loaded until their snapshot (main thread). */ public ChunkHolds chunkHolds() { return chunkHolds; @@ -132,25 +182,30 @@ public final class RenderManager { Job job = new Job<>(onSuccess, onFailure); jobs.add(job); dispatcher.execute(() -> { + running.incrementAndGet(); + long timeout = timeoutMillis; AtomicBoolean timedOut = new AtomicBoolean(false); ScheduledFuture deadline = watchdog.schedule( () -> { timedOut.set(true); job.cancelled.set(true); }, - timeoutMillis, + timeout, TimeUnit.MILLISECONDS); try { T result = job.cancelled.get() ? null : work.apply(job.cancelled); if (result != null && !job.cancelled.get()) job.result = result; } catch (Throwable t) { - if (!shuttingDown) plugin.getLogger().warning("Render job failed: " + t); + if (!shuttingDown) { + plugin.getLogger().warning("Render job failed: " + t); + if (DebugLog.enabled()) plugin.getLogger().log(Level.WARNING, "Render job failure", t); + } } finally { deadline.cancel(false); + running.decrementAndGet(); if (timedOut.get()) { plugin.getLogger() - .warning( - "Render for " + user + " aborted after " + (timeoutMillis / 1000) + "s (timeout)."); + .warning("Render for " + user + " aborted after " + (timeout / 1000) + "s (timeout)."); } release(user); } diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderStats.java b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderStats.java new file mode 100644 index 0000000..cb545ea --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/render/RenderStats.java @@ -0,0 +1,69 @@ +package eu.mhsl.minecraft.pixelpics.render; + +import java.util.concurrent.atomic.AtomicLong; + +/** Photo counters and summed phase timings since the start (thread-safe), shown by {@code /pixelpic info}. */ +public final class RenderStats { + + /** + * Phase durations of one developed photo in ms: waiting for a worker and its snapshot turn, the world snapshot on + * the main thread, ray tracing, dithering to map colours, and the whole time from the shot to the result. + */ + public record Timings(long queued, long snapshot, long trace, long dither, long total) {} + + private final AtomicLong developed = new AtomicLong(); + private final AtomicLong failed = new AtomicLong(); + private final AtomicLong rejected = new AtomicLong(); + private final AtomicLong queued = new AtomicLong(); + private final AtomicLong snapshot = new AtomicLong(); + private final AtomicLong trace = new AtomicLong(); + private final AtomicLong dither = new AtomicLong(); + private final AtomicLong total = new AtomicLong(); + private final AtomicLong maxTotal = new AtomicLong(); + private final AtomicLong maxSnapshot = new AtomicLong(); + + public void developed(Timings t) { + developed.incrementAndGet(); + queued.addAndGet(t.queued()); + snapshot.addAndGet(t.snapshot()); + trace.addAndGet(t.trace()); + dither.addAndGet(t.dither()); + total.addAndGet(t.total()); + maxTotal.accumulateAndGet(t.total(), Math::max); + maxSnapshot.accumulateAndGet(t.snapshot(), Math::max); + } + + public void failed() { + failed.incrementAndGet(); + } + + public void rejected() { + rejected.incrementAndGet(); + } + + public long developedCount() { + return developed.get(); + } + + public long failedCount() { + return failed.get(); + } + + public long rejectedCount() { + return rejected.get(); + } + + /** Average timings of the developed photos (all zero before the first one). */ + public Timings average() { + long n = Math.max(1, developed.get()); + return new Timings(queued.get() / n, snapshot.get() / n, trace.get() / n, dither.get() / n, total.get() / n); + } + + public long maxTotal() { + return maxTotal.get(); + } + + public long maxSnapshot() { + return maxSnapshot.get(); + } +} diff --git a/src/main/java/eu/mhsl/minecraft/pixelpics/utils/DebugLog.java b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/DebugLog.java new file mode 100644 index 0000000..bc38791 --- /dev/null +++ b/src/main/java/eu/mhsl/minecraft/pixelpics/utils/DebugLog.java @@ -0,0 +1,27 @@ +package eu.mhsl.minecraft.pixelpics.utils; + +import java.util.logging.Logger; + +/** Optional detailed log output, switched by {@code debug} in config.yml (applied on start and reload). */ +public final class DebugLog { + + private static volatile boolean enabled; + private static volatile Logger logger; + + private DebugLog() {} + + public static void configure(Logger log, boolean on) { + logger = log; + enabled = on; + } + + public static boolean enabled() { + return enabled; + } + + /** Logs {@code message} at INFO with a {@code [debug]} prefix when debug logging is on. */ + public static void log(String message) { + Logger log = logger; + if (enabled && log != null) log.info("[debug] " + message); + } +}