Class ServerMonitorManager
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classThe result of one main-thread sample. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate FileThe file whose filesystem backs thediskUsagemetric -- the server root (the working directory the Paper process was started in), matchingFileOperationManager's ownserverRootconvention.private ToLongFunction<File> Seam overFile.getFreeSpace()(WR-03) -- includes space reserved for the filesystem's own root/superuser allocation, matching whatdf's ownUse%column treats as "used" (total minus this value).private intprivate booleanprivate doubleprivate final AtomicLongWall-clock timestamp of the last time queued logs were actually drained into abatch_updatemessage, in external-drain mode (Gate-2 finding: with monitoring enabled -- the shipped default --sendBatchUpdate()runs on its own hardcoded 5-second tick, completely independent ofUltiPanelLogTransmitter's own configured batch-send interval.private longprivate static final longPolling granularity formaybeSendLogsOnly()'s scheduled check, in milliseconds (Gate-2 finding, round 11).private final ObjectGuards the ACTUAL drain-and-send operation (drainAndSendLogsOnly(com.ultikits.ultitools.manager.UltiPanelLogTransmitter)) across all THREE paths that can trigger one:sendBatchUpdate()'s 5-second tick,maybeSendLogsOnly()'s 1-second tick, anddrainLogsNow()'s size-triggered, event-driven call (Gate-2 finding, round 7).private ScheduledExecutorServiceSend thread pool.private static final longSampling period for world/player/plugin status, in ticks.private org.bukkit.scheduler.BukkitTaskA server-state snapshot sampled on the main thread; the async send thread only ever reads it.private intprivate ToLongFunction<File> Seam overFile.getTotalSpace(), so "the filesystem reports zero total space" (D-14) is deterministic in tests without needing an actual zero-capacity filesystem.private final long[]private final long[]private final long[]private org.bukkit.scheduler.BukkitTaskThe two main-thread scheduled tasks;stopMonitoring()needs to be able to cancel them.private ToLongFunction<File> Seam overFile.getUsableSpace(), same rationale astotalSpaceReader.private UltiPanelWebSocketClient -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate doublecalculateAverageTPS(long[] history, int count) Calculates the average of a history-of-TPS array.private doubleCalculates the realtime TPS (based on the most recent history entry).private double[]Calculates TPS -- returns the [1-minute, 5-minute, 15-minute] averages.private static voidcancelTask(org.bukkit.scheduler.BukkitTask task) private booleanclaimLogFlushWindow(long now, int intervalMs) Atomically claims the current log-flush window if (and only if)intervalMshas elapsed since the last successful claim, advancinglastLogFlushMstonowas part of the same compare-and-set.private doubleThe used percentage of the filesystem holdingdiskUsageRoot, matchingdf's ownUse%convention (WR-03) rather than a straight(total - usable) / total:used = total - free(viafreeSpaceReader,File.getFreeSpace()-- INCLUDES space reserved for the filesystem's own root/superuser allocation), and the percentage isused / (used + avail)withavailfromusableSpaceReader(File.getUsableSpace()-- EXCLUDES that reservation).(package private) static intcountEnabledPlugins(com.google.gson.JsonArray plugins) Counts entries inpluginswhoseenabledfield istrue-- the per-plugin flagsampleServerState()already captures for every installed plugin (D-14, #437:enabledPluginsmust count plugins that are enabled, not every plugin installed).Gets the current snapshot for the send thread to use.private voiddrainAndSendLogsOnly(UltiPanelLogTransmitter transmitter) Drains up totransmitter's configuredbatchSizeand, if non-empty, sends abatch_updatemessage carrying ONLYdata.logs-- shared bymaybeSendLogsOnly()anddrainLogsNow()(Gate-2 finding, round 6 extraction).(package private) voidImmediately drains and sends whatever is currently queued, without waiting for eithersendBatchUpdate()'s 5-second tick ormaybeSendLogsOnly()'s 1-second tick (Gate-2 finding, round 6).private StringextractVersionNumber(String fullVersion) Extract the version number.private doubleGet the CPU usage (returns the cached value, updated by periodic sampling).private com.google.gson.JsonObjectGets the current performance metrics data.private com.google.gson.JsonArrayGets the current plugin list array.private com.google.gson.JsonObjectGets the current server status data.doubleGet current 1-minute average TPS.(package private) ObjectExposeslogDrainLocksoLogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient)can wireUltiPanelLogTransmitter.setExternalDrainCoordinationLock(java.lang.Object)to the SAME lock object this class's own drain paths already synchronize on (Gate-2 finding, round 9).(package private) booleanLets tests assert whether sampling has actually happened yet -- more direct than observing the emitted JSON externally.booleanprivate booleanThe single check point for the thread contract: logs SEVERE and returns false when not on the main thread.private voidChecks the log-drain interval gate at aLOG_FLUSH_POLL_INTERVAL_MSgranularity (narrowed from 1 second in round 11), independent ofsendBatchUpdate()'s own fixed 5-second tick (Gate-2 finding, round 3).(package private) voidEntry point for the main-thread scheduled task: samples and publishes the snapshot.private voidSample CPU usage and update the cached value.Samples a server-state snapshot on the main thread.private voidSends the batch_update message, merging status, metrics, plugins and logs into a single WebSocket frame.voidSends performance metrics data (standalone message, for on-demand requests).voidsendMetricsDataWithRequestId(String requestId) Sends performance metrics data with a request ID (in response to a backend request).voidsendPlayerEvent(String eventType, org.bukkit.entity.Player player, com.google.gson.JsonObject additionalData) Sends a player event.voidSend server status information.voidsendServerStatusWithRequestId(String requestId) Send server status information with a request ID (in response to a backend request).(package private) voidsetDiskSpaceReaders(ToLongFunction<File> totalSpaceReader, ToLongFunction<File> freeSpaceReader, ToLongFunction<File> usableSpaceReader) Test seam: inject fake total/free/usable-space readers (e.g.(package private) voidsetDiskUsageRoot(File diskUsageRoot) Test seam: point the disk-usage reading at a different root (e.g.voidSet the WebSocket client.voidStart monitoring server status.voidStop monitoring.private voidUpdates the TPS calculation + CPU sampling -- runs once per second (20 ticks).
-
Field Details
-
webSocketClient
-
scheduler
Send thread pool. Must not be final:stopMonitoring()shuts it down, and once aScheduledExecutorServiceis shut down it is permanently dead. Logging out and back in is a perfectly normal path, and at that pointstartMonitoring()must obtain a usable pool -- otherwisescheduleAtFixedRatethrowsRejectedExecutionExceptiondirectly. -
isMonitoring
private boolean isMonitoring -
tickCount
private int tickCount -
lastTick
private long lastTick -
tpsHistory1m
private final long[] tpsHistory1m -
tpsHistory5m
private final long[] tpsHistory5m -
tpsHistory15m
private final long[] tpsHistory15m -
historyIndex
private int historyIndex -
lastCpuUsage
private volatile double lastCpuUsage -
SNAPSHOT_INTERVAL_TICKS
private static final long SNAPSHOT_INTERVAL_TICKSSampling period for world/player/plugin status, in ticks. 100 ticks = 5 seconds, matching batch_update's send cadence.- See Also:
-
LOG_FLUSH_POLL_INTERVAL_MS
private static final long LOG_FLUSH_POLL_INTERVAL_MSPolling granularity formaybeSendLogsOnly()'s scheduled check, in milliseconds (Gate-2 finding, round 11). Narrowed from 1 second to bound the worst-case overshoot past a configuredbatchConfig.intervalto roughly this value, rather than up to a full second -- see the call site's own comment for the numeric evidence and the reasoning for not instead dynamically rescheduling this task to track the transmitter's own live interval.- See Also:
-
stateSnapshot
A server-state snapshot sampled on the main thread; the async send thread only ever reads it.This is the entire point of issue #179. Before this field existed,
sendBatchUpdateran on a plainScheduledThreadPoolbut calledBukkit.getWorlds(),world.getLoadedChunks(),Bukkit.getOnlinePlayers(),player.getLocation()directly from there -- all mutable world state Paper explicitly does not support touching from an async thread, surfacing as intermittent concurrent-modification exceptions or torn reads.TPS/CPU sampling in this same class already correctly hopped over to
runTaskTimer, showing the contract was recognized at the time -- just only half applied. This field fills in the other half, following the exact same pattern.The cost is that data can be stale by up to one sampling period (5 seconds). This is a deliberate choice: the alternative would have the async thread synchronously wait on the main thread via
callSyncMethod().get(), which would make monitoring's liveness depend on the main thread's health -- precisely when the server is lagging is exactly when monitoring most needs to still be able to speak. -
tpsTask
private org.bukkit.scheduler.BukkitTask tpsTaskThe two main-thread scheduled tasks;stopMonitoring()needs to be able to cancel them. -
snapshotTask
private org.bukkit.scheduler.BukkitTask snapshotTask -
lastLogFlushMs
Wall-clock timestamp of the last time queued logs were actually drained into abatch_updatemessage, in external-drain mode (Gate-2 finding: with monitoring enabled -- the shipped default --sendBatchUpdate()runs on its own hardcoded 5-second tick, completely independent ofUltiPanelLogTransmitter's own configured batch-send interval. Before this field existed, that meant the panel's livebatchConfig.intervalaction (#432) had NO observable effect whenever monitoring was on, because the monitor drained the queue every 5 seconds regardless of what interval was configured. Gating the drain itself by this timestamp makes the configured interval govern how often logs actually go out, even thoughsendBatchUpdate()itself still ticks every 5 seconds for status/metrics). Starts at0so the very first tick afterstartMonitoring()always drains.AtomicLong, not a plainvolatile long(Gate-2 finding, round 4): two independently-scheduled tasks (sendBatchUpdate()'s 5-second tick andmaybeSendLogsOnly()'s 1-second tick) can become due in the SAME window (e.g. a 1000ms configured interval), andvolatileonly guarantees visibility, not that a "read, compare, then write" sequence is atomic -- both threads could pass the elapsed-time check before either updates the field, both callUltiPanelLogTransmitter.drainQueue(int)concurrently against the same queue, and split one batch of records across two separatelog_batch/batch_updateframes with interleavedpoll()calls, scrambling delivery order across the two frames.claimLogFlushWindow(long, int)usesAtomicLong.compareAndSet(long, long)so only ONE of the two competing callers ever wins a given window; the loser skips draining entirely rather than draining a partial, racing subset. -
logDrainLock
Guards the ACTUAL drain-and-send operation (drainAndSendLogsOnly(com.ultikits.ultitools.manager.UltiPanelLogTransmitter)) across all THREE paths that can trigger one:sendBatchUpdate()'s 5-second tick,maybeSendLogsOnly()'s 1-second tick, anddrainLogsNow()'s size-triggered, event-driven call (Gate-2 finding, round 7).claimLogFlushWindow(long, int)'s CAS only arbitrates between the two TIME-based paths against each other;drainLogsNow()deliberately does not participate in that CAS at all (it is not time-gated), so without a separate mutual-exclusion lock it could run concurrently with whichever time-based path just won the CAS, and the two would poll() the same queue at once, splitting one logical batch across two frames with no ordering guarantee between them. This lock is orthogonal to the CAS: the CAS decides WHETHER a time-based path may proceed at all; this lock decides that only ONE drain (whichever path triggered it) is ever in flight at a time. -
diskUsageRoot
The file whose filesystem backs thediskUsagemetric -- the server root (the working directory the Paper process was started in), matchingFileOperationManager's ownserverRootconvention. Package-private and mutable only so tests can point it at a controlled location; production code never reassigns it (D-14, #436). -
totalSpaceReader
Seam overFile.getTotalSpace(), so "the filesystem reports zero total space" (D-14) is deterministic in tests without needing an actual zero-capacity filesystem. Defaults to the real method reference. -
freeSpaceReader
Seam overFile.getFreeSpace()(WR-03) -- includes space reserved for the filesystem's own root/superuser allocation, matching whatdf's ownUse%column treats as "used" (total minus this value). Distinct fromusableSpaceReader(File.getUsableSpace()), which EXCLUDES that reservation -- the two diverge on a real ext4 volume with a nonzero reserved-block percentage. -
usableSpaceReader
Seam overFile.getUsableSpace(), same rationale astotalSpaceReader.
-
-
Constructor Details
-
ServerMonitorManager
public ServerMonitorManager()
-
-
Method Details
-
getLogDrainLock
Object getLogDrainLock()ExposeslogDrainLocksoLogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient)can wireUltiPanelLogTransmitter.setExternalDrainCoordinationLock(java.lang.Object)to the SAME lock object this class's own drain paths already synchronize on (Gate-2 finding, round 9). Package-private -- this is internal cross-class coordination, not a public API. -
claimLogFlushWindow
private boolean claimLogFlushWindow(long now, int intervalMs) Atomically claims the current log-flush window if (and only if)intervalMshas elapsed since the last successful claim, advancinglastLogFlushMstonowas part of the same compare-and-set. Returnsfalsewithout side effects if the interval has not elapsed, OR if a concurrently-running competing task already claimed this exact window first (Gate-2 finding, round 4 -- seelastLogFlushMs's own javadoc).- Parameters:
now- the caller's ownSystem.currentTimeMillis()snapshotintervalMs- the currently configured batch-send interval- Returns:
trueonly for the caller that should proceed to drain and send
-
setDiskUsageRoot
Test seam: point the disk-usage reading at a different root (e.g. a real temp directory). Production code never calls this. -
setDiskSpaceReaders
void setDiskSpaceReaders(ToLongFunction<File> totalSpaceReader, ToLongFunction<File> freeSpaceReader, ToLongFunction<File> usableSpaceReader) Test seam: inject fake total/free/usable-space readers (e.g. to force the zero-total-space outcome deterministically, or to pin a known used-percentage without depending on the real filesystem's actual free space). Production code never calls this. -
setWebSocketClient
Set the WebSocket client.- Parameters:
client- the WebSocket client
-
startMonitoring
public void startMonitoring()Start monitoring server status. -
stopMonitoring
public void stopMonitoring()Stop monitoring.Besides shutting down the send thread, the two main-thread scheduled tasks must also be cancelled. This used to be just
scheduler.shutdown(), which was enough before sampling moved to the main thread -- the cost of iterating every world lived on the send thread, so shutting down sending removed it too. After the move, not cancelling turns "stop monitoring" into "stop sending, but still iterate every world and chunk every 5 seconds" -- and now on the main thread. See the PR #265 review. -
cancelTask
private static void cancelTask(org.bukkit.scheduler.BukkitTask task) -
sendServerStatus
public void sendServerStatus()Send server status information. -
extractVersionNumber
Extract the version number. -
getCPUUsage
private double getCPUUsage()Get the CPU usage (returns the cached value, updated by periodic sampling). -
sampleCpuUsage
private void sampleCpuUsage()Sample CPU usage and update the cached value. -
sendServerStatusWithRequestId
Send server status information with a request ID (in response to a backend request). -
sampleServerState
Samples a server-state snapshot on the main thread.May only be called on the Bukkit main thread. A call from a non-main thread is refused and logged as SEVERE -- this is a second, defensive gate: scheduling already guarantees the thread, but this class's own history is exactly "the contract was recognized, only half applied", and writing the contract into the code is more reliable than writing it into a comment.
- Returns:
- the new snapshot;
nullif not on the main thread
-
isOnPrimaryThreadOrComplain
private boolean isOnPrimaryThreadOrComplain()The single check point for the thread contract: logs SEVERE and returns false when not on the main thread.This class's own history is exactly "the contract was recognized, only half applied", so the contract is written as a runtime-observable signal rather than trusted to a comment -- on a real server, this line appearing in the log is proof that scheduling was broken.
-
refreshStateSnapshot
void refreshStateSnapshot()Entry point for the main-thread scheduled task: samples and publishes the snapshot.Package-visible so tests can drive it directly.
-
hasSampledState
boolean hasSampledState()Lets tests assert whether sampling has actually happened yet -- more direct than observing the emitted JSON externally. -
currentSnapshot
Gets the current snapshot for the send thread to use.If sampling has never happened even once and the caller happens to be on the main thread, sample once right here -- otherwise the first frame after the connection is established would be all zeroes, and on-demand requests like
sendServerStatusWithRequestIdwould return empty data before monitoring has started. -
getCurrentServerStatusData
private com.google.gson.JsonObject getCurrentServerStatusData()Gets the current server status data.Every Bukkit-derived field comes from the main-thread-sampled snapshot (see
stateSnapshot); this method itself may be called from any thread. Runtime memory, JVM uptime, TPS and CPU are not Bukkit state and are read in place -- TPS and CPU are already maintained by the main thread's 1Hz task. -
sendBatchUpdate
private void sendBatchUpdate()Sends the batch_update message, merging status, metrics, plugins and logs into a single WebSocket frame. Called every 5 seconds; plugins is included once every 12th tick (60 seconds). -
maybeSendLogsOnly
private void maybeSendLogsOnly()Checks the log-drain interval gate at aLOG_FLUSH_POLL_INTERVAL_MSgranularity (narrowed from 1 second in round 11), independent ofsendBatchUpdate()'s own fixed 5-second tick (Gate-2 finding, round 3).Both this method and
sendBatchUpdate()callclaimLogFlushWindow(long, int)against the SAMElastLogFlushMsfield, so only one of the two competing scheduled tasks ever drains for a given window (Gate-2 finding, round 4 -- seelastLogFlushMs's own javadoc for why a plainvolatile longcheck-then-write was not race-safe here).Sends a
batch_updatemessage carrying ONLYdata.logswhen logs are both due and non-empty -- not a new message type: the Worker's own handler (`websocket-server.ts` `case 'batch_update'`) already guards every field withif (batch.X), so a partial batch_update is an already-supported shape, not a protocol change. -
drainLogsNow
void drainLogsNow()Immediately drains and sends whatever is currently queued, without waiting for eithersendBatchUpdate()'s 5-second tick ormaybeSendLogsOnly()'s 1-second tick (Gate-2 finding, round 6). Registered asUltiPanelLogTransmitter'sexternalSizeThresholdCallbackfromLogStreamManager.initialize(com.ultikits.ultitools.websocket.UltiPanelWebSocketClient): under external drain mode,addToBatch()'s own size-threshold trigger is otherwise silently swallowed bysendBatch()'s own early return, so a configuredbatchSizenever shortened delivery latency while monitoring was active, and a sustained burst could fill the transmitter's queue cap and start discarding old entries despite repeatedly crossing the threshold.Deliberately does NOT go through
claimLogFlushWindow(long, int)'s interval gate: that gate rate-limits the two TIME-based tasks against the configured interval, but this trigger is queue-DEPTH-based, a different reason to drain, and the whole point is to bypass waiting for the interval when the queue is already at capacity risk.drainQueueis safe to call concurrently with the other two tasks (ConcurrentLinkedQueue#pollis atomic per call) -- a near-simultaneous interval-triggered drain and this size-triggered one would, at worst, split one burst across two frames rather than lose or duplicate any entry.lastLogFlushMsis still advanced afterwards so the interval-based tasks do not immediately re-fire for the records this call already sent. -
drainAndSendLogsOnly
Drains up totransmitter's configuredbatchSizeand, if non-empty, sends abatch_updatemessage carrying ONLYdata.logs-- shared bymaybeSendLogsOnly()anddrainLogsNow()(Gate-2 finding, round 6 extraction). Not a new message type: the Worker's own handler (`websocket-server.ts` `case 'batch_update'`) already guards every field withif (batch.X), so a partial batch_update is an already-supported shape, not a protocol change. Callers are responsible for their own connectivity/capability checks and for advancinglastLogFlushMs.Holds
logDrainLockfor its whole body (Gate-2 finding, round 7): the poll-and-send sequence itself must be mutually exclusive across all three callers, not just gated byclaimLogFlushWindow(long, int)'s CAS (which only arbitrates the two TIME-based callers against each other and is not even consulted bydrainLogsNow()). -
getCurrentPluginArray
private com.google.gson.JsonArray getCurrentPluginArray()Gets the current plugin list array.Taken from the main-thread-sampled snapshot.
Bukkit.getPluginManager().getPlugins()likewise should not be iterated from an async thread. -
getCurrentMetricsData
private com.google.gson.JsonObject getCurrentMetricsData()Gets the current performance metrics data.Bukkit-derived fields come from the snapshot; Runtime memory and TPS are computed in place.
-
computeDiskUsage
private double computeDiskUsage()The used percentage of the filesystem holdingdiskUsageRoot, matchingdf's ownUse%convention (WR-03) rather than a straight(total - usable) / total:used = total - free(viafreeSpaceReader,File.getFreeSpace()-- INCLUDES space reserved for the filesystem's own root/superuser allocation), and the percentage isused / (used + avail)withavailfromusableSpaceReader(File.getUsableSpace()-- EXCLUDES that reservation). The earlier(total - usable) / totalformula measured roughly one percentage point higher thandfon a real ext4 volume with its default ~5% reserved-block allocation, at or past this metric's own documented one-point UAT tolerance -- see this plan's gate record for the real-machine measurement. Rounded to two decimals exactly likememoryUsageabove (D-14, #436). A filesystem reporting zero total space, or a used+avail denominator of zero, is a stated outcome --0.0-- not a division error. -
countEnabledPlugins
static int countEnabledPlugins(com.google.gson.JsonArray plugins) Counts entries inpluginswhoseenabledfield istrue-- the per-plugin flagsampleServerState()already captures for every installed plugin (D-14, #437:enabledPluginsmust count plugins that are enabled, not every plugin installed). Package-private and pure so it is unit-testable directly against a syntheticJsonArraywithout needing a multi-plugin MockBukkit fixture.- Parameters:
plugins- the snapshot's plugin array; each element is expected to be aJsonObjectcarrying a booleanenabledfield- Returns:
- the number of entries whose
enabledfield istrue;0for an empty array
-
sendMetricsData
public void sendMetricsData()Sends performance metrics data (standalone message, for on-demand requests). -
sendMetricsDataWithRequestId
Sends performance metrics data with a request ID (in response to a backend request). -
updateTpsAndCpu
private void updateTpsAndCpu()Updates the TPS calculation + CPU sampling -- runs once per second (20 ticks). -
getCurrentTPS
public double getCurrentTPS()Get current 1-minute average TPS.- Returns:
- the 1-minute average TPS
- Since:
- 6.2.3
-
calculateTPS
private double[] calculateTPS()Calculates TPS -- returns the [1-minute, 5-minute, 15-minute] averages. -
calculateAverageTPS
private double calculateAverageTPS(long[] history, int count) Calculates the average of a history-of-TPS array. -
calculateRealtimeTPS
private double calculateRealtimeTPS()Calculates the realtime TPS (based on the most recent history entry). -
sendPlayerEvent
public void sendPlayerEvent(String eventType, org.bukkit.entity.Player player, com.google.gson.JsonObject additionalData) Sends a player event. -
isMonitoring
public boolean isMonitoring()
-