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

This commit is contained in:
MineTec committed 2026-11-05 18:56:39 +01:00
1 parent a082271f28
commit be94c33ea6
3 files changed
+157 -6

No files matched your search

@@ -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<T> 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);
}
@@ -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();
}
}
@@ -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);
}
}