Class GuiScheduler
It provides:
- a check for whether the current thread is the main thread
- scheduling a task to run on the main thread
- frame scheduling: coalescing multiple update requests within a short window
- prevention of duplicate scheduling
Thread-safety contract: State.setState(...) and Element.markNeedsBuild()
are safe to call from any thread -- a call made off the main thread is marshalled onto it, and
multiple calls that land within the same frame window are coalesced via this scheduler's
AtomicBoolean compare-and-set guard, at a nominal 16ms frame
interval (floored to one Bukkit tick when scheduling has to wait for one). flush(), by
contrast, throws IllegalStateException when called off the main thread -- it runs
pending work synchronously rather than marshalling it.
Per-frame task budget: one frame runs only the tasks that were already
queued when it started. A task queued while the frame is running -- for example the follow-up
rebuild a failing build replays from its finally -- runs on a later frame, which
scheduleFrame(java.lang.Runnable) schedules on the Bukkit scheduler (never inline inside the running
frame). This bounds the main-thread time one frame can take: a widget whose
State.initState requests a rebuild and then throws is retried once per frame while its
page is open, instead of keeping the server thread inside a single frame forever. No queued
task is dropped; it only waits for the next frame.
Frame scheduling:
Timeline:
|----16ms----|----16ms----|----16ms----|
↑ ↑ ↑
setState rebuild actually runs
↑
setState (coalesced into the same frame)
- Since:
- 6.2.0
- Version:
- 1.0.0
- Author:
- UltiTools Team
-
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static final longThe default frame interval, in milliseconds.private final longprivate booleanTrue whileexecuteFrame()is draining on the main thread.private final AtomicBooleanprivate longprivate final ConcurrentLinkedQueue<Runnable> private final org.bukkit.plugin.Plugin -
Constructor Summary
ConstructorsConstructorDescriptionCreates a GuiScheduler using the default plugin instance.GuiScheduler(@NotNull org.bukkit.plugin.Plugin plugin) Creates a GuiScheduler.GuiScheduler(@NotNull org.bukkit.plugin.Plugin plugin, long frameIntervalMs) Creates a GuiScheduler with the given frame interval. -
Method Summary
Modifier and TypeMethodDescriptionvoidCancels every pending task.private voiddrainFrameBudget(int budget) Runs at mostbudgetqueued tasks, catching and reporting each task's failure.private voidRuns the frame tasks that were pending when this frame started.voidflush()Runs every pending task immediately (blocking until complete).booleanChecks whether the current thread is the main thread.private voidThe console-visible half of a GUI frame task's failure -- a warning naming the task plus a full stack trace.private voidReports aRenderDepthExceededExceptiontoErrorReportCollector.voidrunOnMainThread(@NotNull Runnable task) Ensures the task runs on the main thread.voidscheduleFrame(@NotNull Runnable frameTask) Schedules a frame task.
-
Field Details
-
DEFAULT_FRAME_INTERVAL_MS
private static final long DEFAULT_FRAME_INTERVAL_MSThe default frame interval, in milliseconds. Minecraft's default tick is 50ms, but GUI updates can run more frequently.- See Also:
-
plugin
private final org.bukkit.plugin.Plugin plugin -
frameIntervalMs
private final long frameIntervalMs -
isScheduled
-
pendingTasks
-
lastFrameTime
private long lastFrameTime -
frameRunning
private volatile boolean frameRunningTrue whileexecuteFrame()is draining on the main thread. A frame requested during that drain is always handed to the Bukkit scheduler rather than run inline, so a slow frame (one that outlasts the frame interval) cannot start a nested frame from inside itself.
-
-
Constructor Details
-
GuiScheduler
public GuiScheduler()Creates a GuiScheduler using the default plugin instance. -
GuiScheduler
public GuiScheduler(@NotNull @NotNull org.bukkit.plugin.Plugin plugin) Creates a GuiScheduler.- Parameters:
plugin- the plugin instance
-
GuiScheduler
public GuiScheduler(@NotNull @NotNull org.bukkit.plugin.Plugin plugin, long frameIntervalMs) Creates a GuiScheduler with the given frame interval.- Parameters:
plugin- the plugin instanceframeIntervalMs- the frame interval, in milliseconds
-
-
Method Details
-
isOnMainThread
public boolean isOnMainThread()Checks whether the current thread is the main thread.- Returns:
- true if on the main thread
-
runOnMainThread
Ensures the task runs on the main thread.If already on the main thread, runs immediately. Otherwise, schedules it to run on the main thread.
Gate-2 Codex finding, PR #478 round 1: this is the path
GuiRenderer.initialize(java.util.function.Supplier<com.ultikits.ultitools.abstracts.gui.declarative.core.Widget>, com.ultikits.ultitools.abstracts.gui.declarative.core.BuildContext)drives the FIRST build through, andGuiRenderer.performBuild()'s own off-main-thread re-entrant guard -- neither passes throughexecuteFrame()'s try/catch, which is whereRenderDepthExceededExceptionreporting (WR-01) lived until now. Since mounting is where the depth guard (CR-01) actually fires, and mounting happens on the very first build, that build's own trip is the MORE common case to miss, not an edge case. Both branches below now report it; only the already-on-main-thread branch re-throws afterward (preserving the existing "the caller ofrunOnMainThread()sees the exception synchronously" contractGuiRenderer.initialize(java.util.function.Supplier<com.ultikits.ultitools.abstracts.gui.declarative.core.Widget>, com.ultikits.ultitools.abstracts.gui.declarative.core.BuildContext)and every CR-01 test depend on) -- the off-thread branch's caller could never observe the deferred task's exception anyway (Bukkit's own scheduler runs it later), so there is nothing to preserve there beyond adding the report.- Parameters:
task- the task to run
-
scheduleFrame
Schedules a frame task.This method implements the frame-coalescing mechanism: multiple calls within the same frame window trigger only one actual execution. A call made while a frame is running queues its task for a later frame (see the class javadoc's per-frame task budget).
- Parameters:
frameTask- the frame task
-
executeFrame
private void executeFrame()Runs the frame tasks that were pending when this frame started.The budget is the queue size read before the first task runs. A task queued during the drain stays queued for a later frame:
isScheduledis cleared before the drain, so the firstscheduleFrame(java.lang.Runnable)call made during it schedules that later frame, andframeRunningmakes sure it is scheduled on the Bukkit scheduler rather than run inline from inside this one. -
drainFrameBudget
private void drainFrameBudget(int budget) Runs at mostbudgetqueued tasks, catching and reporting each task's failure.- Parameters:
budget- the number of tasks this frame may run
-
logFrameTaskError
The console-visible half of a GUI frame task's failure -- a warning naming the task plus a full stack trace. Shared by every catch clause indrainFrameBudget(int)so the two log lines cannot drift between the exception types that also do additional reporting (RenderDepthExceededException) and those that do not.- Parameters:
e- the failure to log
-
reportRenderDepthExceeded
Reports aRenderDepthExceededExceptiontoErrorReportCollector.No new per-GUI "failed/closed" bookkeeping is added here: the exception is always thrown from the same guard site with a stable top stack frame (the guard name and depth vary in the message, not in the class/method/line the stack trace records), so
ErrorReportCollector's own fingerprint-based dedup already coalesces repeated occurrences -- e.g. the same over-deep tree being rebuilt on everysetState()call -- into ONE report with an incrementing count, rather than one report per frame.- Parameters:
e- the depth-guard trip to report
-
flush
public void flush()Runs every pending task immediately (blocking until complete).It drains the whole queue, ignores the per-frame task budget, and is intended for tests only.
Note: this method may only be called on the main thread.
Gate-2 Codex finding, PR #478 round 3: a
RenderDepthExceededExceptionfrom a queued task now reachesErrorReportCollectorhere too, matchingexecuteFrame()andrunOnMainThread(java.lang.Runnable)'s on-main-thread branch -- neither of those catches ran for a task drained through this method. Still re-thrown immediately afterward, preserving this method's own documented synchronous-drain contract (and, unchanged from before this fix, still skipping theisScheduled.set(false)below on that path -- not a new gap this fix introduces or is scoped to close).- Throws:
IllegalStateException- if not on the main thread
-
cancelAll
public void cancelAll()Cancels every pending task.
-