Server Admin

Scuffed Diagnostics

Requirements, every staff subcommand, the permission node, config keys, detection triggers, capture and retention layout, the report viewer, API and troubleshooting.

Last updated

Scuffed Diagnostics (ScuffedDiag) is an operator tool. It measures tick lag, memory, garbage collection, per-player network traffic and entity populations, writes evidence to disk when something goes wrong, and turns each capture into a shareable report page. It has no player-facing features, so there is no gameplay page. Everything below is for staff.

  • Plugin version: 3.13.12 (plugin name ScuffedDiag)

What it does

ScuffedDiag is read-only. It never changes game state. It has three layers:

LayerWhat runsWhen
Always-on collectorsPer-world tick intervals (TPS), JVM pause detector, allocation-rate sampler, entity census and density hotspots, per-world load level, per-player network watch and per-stream network accounting, network thread gauge, container memory guard, lag watchdog, TPS guard (auto report), 5-minute heartbeatFrom boot
RecordingSpike records, freeze stack sampler, incident snapshots, particle / packet / entity-update taps, disconnect records, ping spikesOnly while a capture window or /scuffeddiag on is active (off by default)
Capture windowsA time-boxed bundle: 30-second time series plus Refixes per-system timing, written to a session folder and turned into a report page/scuffeddiag report, /scuffeddiag capture, automatic baseline and stall windows, automatic low-TPS reports, or a remote call through the API

Every /scuffeddiag subcommand also runs from the server console.


Requirements

ItemVersionType
Hytale server>=0.7.0-pre.0 <0.8.0Required
Hytale:AssetModuleanyRequired (the plugin ships a small asset pack for the entity census popup)
Refixes (Endless)any recent versionOptional. When present, captures include deterministic per-system and per-resource tick timing, which names the system that owned a freeze. Without it the freeze charts still work but nothing is named
sparkanyOptional. /scuffeddiag report and /scuffeddiag capture also run spark profiler start ... for the full call tree; if spark is absent that command prints its own unknown-command line and the rest of the capture is unaffected

Notes:

  • Several tools read Linux files (/proc/self/status, cgroup memory files). On a host without them the container memory guard stays idle and the native-memory dump omits those sections.
  • ZGC-specific tuning (soft max heap) only applies when the JVM exposes that flag. The boot log states whether the JVM accepted it.
  • The heap, JFR and native-memory commands use the JVM's own diagnostic interface inside the process. They need no shell or jcmd, but a host that blocks JFR or the diagnostic MBean will refuse them.

Admin & Staff Commands

Root command: /scuffeddiag. All subcommands are staff-only (see Permissions).

World-scoped commands (entities, entitieschat, interactions) take an optional world name: an exact name, or a substring that matches exactly one world. With no name, a player gets their own world; the console gets every world that has players (busiest first, at most 3), or the first 3 worlds if nobody is online.

Status and live readouts

CommandDescription
/scuffeddiag statusPer-world TPS, slow-tick percentage and tick interval (mean, p95, max) over the last 60 seconds, plus the busiest network thread. Worlds with no players show "no ticks observed"
/scuffeddiag lagreportOne-screen readout from the always-on collectors: server line, worlds, entities by class, top NPC roles, hotspots, network, load transitions, last stall. Also written to <day>/lagreport-<UTC>.txt. Names players and their coordinates
/scuffeddiag spikesRecent over-budget ticks with the context captured at the time (needs recording on)
/scuffeddiag disconnectsThe last 20 disconnects with reason, ping, queue and packet context (needs recording on)
/scuffeddiag netServer-wide outbound and inbound bandwidth, top packet types, per-player ping, queue and rate
/scuffeddiag net <player>One player's live readout: Default, Chunks and WorldMap streams, QUIC connection stats, pings, spawns and page opens
/scuffeddiag packetsFull outbound packet-type breakdown, ranked by bytes per second, with the particle share
/scuffeddiag particlesPer-effect particle send rates and the top effect per recipient (needs the particle tap)
/scuffeddiag entityupdatesEntity-update traffic split by component type and recipient (needs recording on)
/scuffeddiag entities [world]Entity census by NPC role. Opens a popup for a player in their own world, text everywhere else
/scuffeddiag entitieschat [world]The same census as plain chat or console text
/scuffeddiag interactions [world]Which blocks and fluids drive per-tick collision interaction chains. Written to interactions-*.jsonl in the session folder. A single named world gets a full sweep; several worlds at once are time-capped to 15 ms each
/scuffeddiag config <show|reload>Show every diag-config.json key (changed values are marked with their default) or re-read the file live
/scuffeddiag atlas [top N | model N | lint | dupes | <pack>] [fresh]Which packs load the client texture atlas, from PNG headers only. top lists the heaviest textures, model the heaviest model textures, lint model textures that break the multiple-of-32 rule plus textures referenced but not shipped, dupes byte-identical copies, <pack> one pack. N is 1 to 200 (default 20). The scan is cached for 5 minutes; fresh rescans. Full report: atlas-report.txt in the plugin folder

Recording and captures

CommandDescription
/scuffeddiag onStart recording into a new session folder. There is no time limit
/scuffeddiag offStop all recording paths at once (taps, samplers, session writes, per-system timer). The watchdog and the TPS guard keep running
/scuffeddiag report <minutes>Time-boxed full bundle, 1 to 30 minutes. Records, writes SUMMARY.txt and README.txt, stops and disables recording by itself. Also starts spark. Preempts a running automatic window
/scuffeddiag capture <minutes>Arms a capture window, 1 to 120 minutes. Samples every 30 seconds. Restores the previous recording state when it ends
/scuffeddiag capture stop (or off)End a window early
/scuffeddiag threshold <ms>Tick-spike threshold in milliseconds (minimum 1, default 50). Not saved across restarts
/scuffeddiag sampler <on|off>Freeze stack sampler switch. Turned on and off with recording
/scuffeddiag particletap <on|off>Particle inspector tap switch
/scuffeddiag allocprofile <seconds>JFR allocation profile, 10 to 300 seconds, written as alloc-<date>.jfr plus ALLOC_SUMMARY.txt into the session folder. Shows which classes and call sites generate garbage
/scuffeddiag web <status|link|build|list|reload>Report viewer controls, see Report viewer

report and capture do not run at the same time as another report. A second report while one is in progress is refused.

Unattended capture

CommandDescription
/scuffeddiag watch <on|off|status>The always-on lag watchdog (on by default)
/scuffeddiag watch <attribute-on|attribute-off>Retroactive attribution: a stall of 4 s or more arms Refixes' meters for 2 minutes (at most once per 10 minutes)
/scuffeddiag auto (or status)Show baseline and stall window settings
/scuffeddiag auto <on|off>Enable or disable both automatic window types
/scuffeddiag auto baseline <durationMin> <periodMin>Set baseline length (1 to 6 min) and period (10 to 720 min)
/scuffeddiag auto baseline offDisable baseline windows only
/scuffeddiag auto stall <on|off|minutes>Enable, disable or set the length of stall windows (1 to 10 min)
/scuffeddiag autoreport (or status)Show the TPS guard state, trigger, last reading and cooldown
/scuffeddiag autoreport <on|off>Enable or disable the automatic 1-minute low-TPS report
/scuffeddiag autoreport <TPS>Set the trigger TPS (clamped to 1 to 29, default 15)
/scuffeddiag autoreport testRecord a test bundle now. Does not start the cooldown

Memory and JVM tools

These talk to the JVM directly. Some pause the server, so run them on purpose.

CommandDescription
/scuffeddiag softmax <GB>Set the ZGC SoftMaxHeapSize floor live. See Heap soft-max guard
/scuffeddiag heaphisto <rows>Live-object class histogram (1 to 200 rows printed to the console). Full table in heaphisto-<ms>.txt in the day folder. Collects first and pauses the server for a few seconds on a large heap
/scuffeddiag nmt <label>Native memory dump to nmt-<label>-<ms>.txt: NMT summary, buffer pools, non-heap pools, RSS lines and cgroup memory.stat. NMT needs -XX:NativeMemoryTracking=summary on the JVM command line; the other sections work without it
/scuffeddiag nmt baselineRecord an NMT baseline. Later dumps add a summary.diff against it
/scuffeddiag trimRun glibc malloc_trim (no GC) and print RssAnon before and after
/scuffeddiag leakhunt <minutes>In-process JFR old-object sample with path-to-GC-root, 1 to 60 minutes. Writes scuffeddiag-leak-<ms>.jfr to the day folder when done
/scuffeddiag leakworldsDiff the engine store registries against the loaded worlds and name any removed world still pinned in memory. A result of zero orphans does not prove there is no leak
/scuffeddiag heapdumpGzipped live-object HPROF dump in the day folder. Stalls the server for the whole dump. Test servers only

Permissions

NodeEffect
scuffeddiag.commandAllows every /scuffeddiag subcommand. Server operators (the OP group) always pass without it

Who may run commands:

  • The server console is always allowed.
  • A player needs OP or scuffeddiag.command, granted to the user or to one of their groups.
  • Any other sender (for example a chat bridge) is judged by its own hasPermission("scuffeddiag.command") answer. If a bridge reports every permission as granted, anyone who can reach the bridge's command relay can run these commands, so keep that relay limited to an admin role.

The lag watchdog sends its in-game warning only to online players who have OP or scuffeddiag.command.


Configuration

ScuffedDiag has two files in mods/ScuffedDiag/. Both are created with defaults on first boot.

diag-config.json

One flat JSON object of dotted keys. Keys added by a later version are merged in on load without touching values you changed. /scuffeddiag config reload re-reads the file with no restart. A value of the wrong type falls back to its default, and an unreadable file leaves the file untouched and runs on defaults.

Population census and hotspots

KeyDefaultEffect
census.enabledtrueRun the entity census
census.intervalSec60Seconds between census cycles per world (minimum 15)
census.writeIntervalSec300Seconds between census rows written to census-*.jsonl per world (minimum 60)
census.budgetUs750World-thread time per slice in microseconds (minimum 100). A large world is read over several ticks
census.minEntitiesWithoutPlayers500A world with no players is scanned only if it holds at least this many entities
census.maxWorlds16Most worlds scanned per cycle
hotspots.topN5Chunks listed per world (1 to 20)
hotspots.alertNpcs60A chunk with at least this many NPCs writes a hotspots-*.jsonl row and a log warning, once per chunk per 10 minutes. 0 disables
hotspots.afkMinutes5A player who stays within afkMoveBlocks for this long is marked AFK in hotspot rows
hotspots.afkMoveBlocks2.0Movement radius in blocks that resets the AFK timer

Per-player network watch

KeyDefaultEffect
netwatch.enabledtrueNetwork watch
netwatch.intervalSec5Sampling interval while recording is off
netwatch.outBytesPerSecFlag1000000A player sending more than this per second is flagged. 0 disables this check
netwatch.interactionPacketsPerSecFlag200Flag for interaction-chain packets per second. 0 disables
netwatch.interactionQueueFlag500Flag for an interaction backlog. 0 disables. The engine starts dropping at 1000
netwatch.flagCooldownSec60Minimum time between flags for one player (minimum 5)
netwatch.rollupIntervalSec60Interval of the top-5 consumer rollup in net-*.jsonl (minimum 10)

Allocation profile on an unattended low-TPS report

KeyDefaultEffect
allocAuto.enabledtrueAttach a JFR allocation profile to an automatic report
allocAuto.seconds60Profile length (10 to 300)
allocAuto.cooldownMinutes60Minimum time between automatic profiles (minimum 5)
allocAuto.maxFileMB32Size cap of the .jfr file (4 to 256)
allocAuto.minFreeDiskMB2048Skip the profile when less disk than this is free

Stall evidence retention (mods/ScuffedDiag/stalls/)

KeyDefaultEffect
stalls.keep30Newest folders kept per kind (stall windows and lock dumps counted separately)
stalls.maxAgeDays14Folders older than this are deleted. 0 turns the age rule off
stalls.maxTotalMB1024Oldest folders are deleted until the total is under this. 0 turns the size rule off

Lock dump

KeyDefaultEffect
lockdump.enabledtrueTake a full thread and lock-owner dump on a tick freeze or world hang
lockdump.cooldownSec300Minimum time between dumps, shared by all triggers (minimum 30)

Load level

KeyDefaultEffect
load.enabledtrueCompute the OK / SOFT / HARD level per world
load.softTps24.0Smoothed TPS at or below this means SOFT
load.hardTps12.0Smoothed TPS at or below this means HARD (a hung world also counts as HARD)
load.softMemFrac0.88Memory pressure at or above this fraction means SOFT
load.hardMemFrac0.95Memory pressure at or above this fraction means HARD
load.clearSec30A lower level must hold this long before the level drops (minimum 5)

Per-stream network accounting

KeyDefaultEffect
net.enabledtruePer-player, per-stream accounting
net.quicStatstrueRead each player's QUIC stats (bytes, loss, retransmits, RTT, congestion window) every second
net.spawnTrackingtrueCount newly visible entities per player
net.spawnBytestrueInclude the bytes of those spawns
net.histIntervalSec10Interval of the entity distance-band histogram
net.histBudgetUs500World-thread budget for that pass in microseconds (minimum 50)
net.pageEventstrueLog custom page opens and large page updates
net.pageEventMinBytes32768Page updates of at least this size are logged
net.senderSampleSec10Interval at which the sending plugin's stack is sampled per page class or packet type
net.burstRawBytes300000A player second at or above this many raw bytes per second writes a burst event. 0 disables
net.burstCooldownSec30Minimum time between burst events for one player
net.burstsPerMinute30Server-wide burst events per minute
net.rollupSec60Interval of the netstream rollup (minimum 10)
net.rollupTopPlayers10Players listed per rollup
net.dumpTopPlayers20Players whose 10-minute timeline goes into a lag report or capture
net.retainDepartedSec600How long a departed player's timeline is kept
net.eventMaxMBPerDay32Daily cap for netev events
net.rollupMaxMBPerDay32Daily cap for netstream rollups

The two daily caps are counted in memory and start over at every boot, so a day with restarts can write more than one cap's worth. Events and rollups have separate caps so a flood of events cannot stop the rollups.

Network thread gauge

KeyDefaultEffect
netThread.enabledtrueSample the ServerWorkerGroup thread that runs every player's connection
netThread.warnBusyFraction0.70Log a warning when the busiest network thread stays above this share of wall time for 10 seconds (at most once per minute). It also raises the load level: SOFT at this fraction, HARD at 90%

web-config.json

Settings for the report viewer. Written once when absent, so later boots never overwrite your edits.

KeyDefaultEffect
enabledtrueServe reports over HTTP. When false, reports are still written to disk
bindAddress0.0.0.0Interface to bind. Use 127.0.0.1 when a proxy or tunnel fronts the viewer
port0Listen port. 0 derives it as the server's game port plus portOffset
portOffset2Offset from the game port when port is 0 (0 to 4)
publicHostemptyHostname or IP that outsiders use to reach this server. The port is appended for you
publicBaseUrlemptyFull base URL used as written, with no port added. Overrides publicHost. For a reverse proxy or tunnel
linkTtlDays7How long a link works (1 to 7). It cannot exceed the 7-day folder retention
autoGeneratetrueBuild report.html automatically when a window closes
allowBundleDownloadtrueAllow the "Download bundle" zip of the whole session folder

Applied by /scuffeddiag web reload without a restart: publicHost, publicBaseUrl, autoGenerate, allowBundleDownload. A change to enabled, bindAddress, port, portOffset or linkTtlDays needs a restart; the reload message lists any such keys that differ on disk.

Fixed values (no key)

SettingValue
Tick budget33.3 ms (30 TPS)
Spike threshold50 ms, changed with /scuffeddiag threshold
Incident threshold150 ms. A freeze this long, while recording, takes an incident snapshot of that world (at most one per world per 3 seconds)
Freeze samplerStarts once a tick passes about 40 ms, samples the world thread every 20 ms, keeps up to 6000 samples
Session size cap120 MB. A session that hits it stops writing and drops a CAPPED.txt

Detection and capture behavior

Lag watchdog

Polls every 2 seconds, does only cheap reads while the server is healthy, and writes one compact line to <day>/watch-<date>.jsonl when a trigger fires. At most one record per 2 minutes (30 seconds for hangs). Online staff get an in-game warning at most once per 10 minutes. It never starts a capture itself. It stands down while an operator's own recording is on, but keeps running during automatic windows.

TriggerFires when
heap-pressureThe lowest heap use over 3 minutes stays at 92% of -Xmx or more for 3 polls. Measured on the floor, not on current use, because ZGC rides high on dead objects
tick-freezeA tick interval of 1000 ms or more on a world with players
jvm-pauseA JVM-wide pause of 1000 ms or more in the last 5 seconds (GC, safepoint or host)
low-tpsWorst world TPS below 20 for 3 polls (about 6 seconds)
mass-disconnect3 or more disconnects within 20 seconds
rate-limitA rate-limit disconnect reason within the last 20 seconds
backpressureA player's outbound queue at 2000 packets or more
ping-storm2 or more players with a tick ping of 1000 ms or more
world-hangA world's in-progress tick (or an empty world's tick counter) is 5 seconds old or more. The record includes the world thread's live stack. One record per hang episode

A tick-freeze or world-hang also takes a thread and lock-owner dump into stalls/lockdump-<UTC>-<trigger>/threads.txt (plus a locks-*.jsonl line naming deadlock patterns such as a read-to-write lock self-deadlock). The dump skips lock synchronizers on purpose, because collecting them forces a heap-wide safepoint.

Automatic capture windows

WindowDefaultOpened by
Baseline3 minutes every 30 minutes, first one 10 minutes after boot (about 10% duty)A timer. Its value is a healthy comparison window from the same server and day
Stall2 minutes, at most one per 10 minutesA tick-freeze of 4000 ms or more, a low-tps trigger, or a world-hang

Automatic windows never take over from an operator: if recording is on or a capture is running, they do nothing. If a stall window cannot open, the watchdog arms Refixes' meters alone. Refixes' meters record forward, so a stall window explains the next stall in the episode, not the one that opened it.

Baseline folders are <day>/auto-baseline-<UTC>/ (newest 24 kept). Stall windows are stalls/auto-stall-<UTC>/, outside the day folders so the 7-day sweep cannot remove them.

Automatic low-TPS report (TPS guard)

On by default. Every 5 seconds it reads the mean tick interval over the newest 90 ticks of each world that has players (at least 30 recorded ticks). A world whose last tick is 10 seconds old counts as 0 TPS. When the worst world stays at or below the trigger (default 15 TPS) for 15 seconds, it records a 1-minute report into <day>/autoreport-<UTC>/, attaches an allocation profile (see allocAuto.*), and writes a LAGREPORT.txt into the folder. Cooldown between automatic reports is 20 minutes, and the newest 24 autoreport-* folders are kept. It does not run while anything else is recording. Each trigger also adds a line to autoreport-<date>.jsonl.

Heartbeat

Every 5 minutes, whether or not anything is wrong, one line goes to <day>/heartbeat-<date>.jsonl: worlds, instances, idle instances, entity and chunk counts, heap used, heap floor over 10 minutes, post-GC live set (or -1 when untrusted), non-heap, direct buffers, GC count and time, container memory, census summary, top network consumers, load levels and the worst TPS. It keeps writing while the auto report is off and stands down only while a capture is running (the capture's own 30-second cap_worlds series covers it). Use it to see whether instance count, entity count or heap climbs over a day.

Load level

Per world with players, every 5 seconds: smoothed TPS (newest 150 ticks, smoothed) and memory pressure (the larger of the container resident fraction and the 5-minute heap floor fraction). The level is the worse of the two: OK, SOFT or HARD, using the load.* keys. Escalation needs two consecutive polls. Every change is written to load-*.jsonl. ScuffedDiag takes no action on the level; other mods read it through the API.

Container memory guard

If a container memory limit is readable, it checks resident memory (RssAnon plus RssShmem) every 5 seconds against that limit, since the host kills the process on resident memory, not heap. It always publishes a container-aware heap ceiling for the soft-max guard. At 85% it clamps the soft max and writes a container-memory watch row; at 90% it forces a GC through the diagnostic interface and a native heap trim (at most every 2 minutes); at 95% after that it logs that a kill is imminent. With no readable limit it logs that it is idle.

Heap soft-max guard

At boot ScuffedDiag sets the ZGC SoftMaxHeapSize flag to 6 GB (clamped to -Xmx) so the collector reclaims garbage instead of riding to -Xmx. That value is a floor. Every 60 seconds a guard raises the target to the larger of the floor and 1.6 times the live set, never above 75% of -Xmx or the container-aware ceiling, and lowers it again when the retained set falls. There is no config key: use /scuffeddiag softmax <GB> to change the floor until the next restart, or 0 to stop adjusting. The boot log shows whether the JVM accepted the flag.


Data & storage

Everything is under mods/ScuffedDiag/, written as JSON Lines (one object per line, t in epoch milliseconds, times in UTC) by a single background writer thread, so the tick never waits on disk.

mods/ScuffedDiag/
  diag-config.json  web-config.json  reports.jsonl  LATEST.txt  atlas-report.txt
  2026-09-03/                          one folder per UTC day
    session-<UTC>/                     recording started by /scuffeddiag on, report or capture
    autoreport-<UTC>/                  automatic low-TPS report
    auto-baseline-<UTC>/               automatic baseline window
    <reason>-<UTC>/                    window started through the API (for example a remote caller)
    watch-<date>.jsonl  heartbeat-<date>.jsonl  autoreport-<date>.jsonl
    census-<date>.jsonl  hotspots-<date>.jsonl  load-<date>.jsonl  locks-<date>.jsonl
    net-<date>.jsonl  netstream-<date>.jsonl  netev-<date>.jsonl  atlas-<date>.jsonl
    systemtimings_live-<date>.jsonl    attribution armed by the watchdog
    lagreport-<UTC>.txt  heaphisto-*.txt  nmt-*.txt  heapdump-*.hprof.gz  scuffeddiag-leak-*.jfr
  stalls/
    auto-stall-<UTC>/  lockdump-<UTC>-<trigger>/
ItemRetention
Day folders and everything inside themDeleted when older than 7 days. Checked at boot and on every UTC date roll
stalls/stalls.keep, stalls.maxAgeDays, stalls.maxTotalMB
auto-baseline-*Newest 24
autoreport-*Newest 24
API-started windows (<reason>-*)Newest 12 per reason label. Never swept by the automatic prunes
session-* made by handNot pruned by count. They go with their day folder
Root files (LATEST.txt, configs, reports.jsonl, atlas-report.txt)Kept

LATEST.txt holds the path of the newest session folder relative to the plugin folder, so pulling "the newest data" is one read. A flat mods/ScuffedDiag/ from an older version is migrated into day folders at boot.

Reading a session folder

Open SUMMARY.txt first. It lists findings (a warning sign marks anything needing attention), the report link when the viewer is running, and the file list. README.txt repeats the basics. Main files:

FileContents
system-*JVM flags, GC configuration, heap, threads, CPU, JDK
worlds-*, worlds_summary-*, cap_worlds-*World, instance, entity and chunk counts. A climbing instance count is the leaked-world signal
alloc-*, cap_mem-*Allocation MB/s, heap, non-heap, GC totals and JVM pauses every 30 seconds
spikes-*, freezestacks-*Over-budget ticks, and the call stacks that owned each freeze of 150 ms or more
incidents-*, incident_interactions-*Roles, counts and network state of a world at the moment of a long freeze
jvmpauses-*Stalls that froze every thread, including network I/O
cap_threads-*Per-world-thread CPU percent of one core plus host load
cap_particles-*, cap_net-*, cap_entityupdates-*, cap_interactions-*Particle, packet, entity-update and collision-source series
systemtimings_live-*, resourcetimings_live-*Deterministic per-system and per-resource tick time from Refixes. totalMs covers the window, maxMs is the worst single tick
nettimeline-*Per-player 1-second network timelines for the last 10 minutes
disconnects-*, pingspikes-*Disconnect reasons with context, and per-player ping spikes
log_findings-*Server log lines that matched known incident signatures
refixes_config-*Which Refixes optimizations were on
report.htmlThe generated report page

Reading the tick numbers: ScuffedDiag measures the interval between ticks, not work time. A healthy world paces at about 33 ms per tick, so a mean near 33 ms is good. TPS and the slow percentage (intervals of 50 ms or more) are the signals; use spark for steady work-time MSPT.


Report viewer

Each capture window mints a link to a self-contained report.html for its folder, served by a small built-in HTTP server (two worker threads, GET and HEAD only, 120 requests per minute). The link is announced when the window opens, so you can watch the page while it records; until the window closes the page shows "Still recording" and refreshes every 20 seconds.

Security model: the link is the access control. Each token is 128 random bits, there is no login and no index page, and a request path is never used to find a file. Anyone holding a link can read that bundle and download its zip, so treat links like the data inside (the bundle can name players). Bind to 127.0.0.1 behind a proxy if that trade is wrong for you, or set allowBundleDownload to false. Links expire after linkTtlDays and stop working once their folder is pruned. They survive restarts through reports.jsonl.

Setup:

  • The port defaults to game port plus 2, which usually falls inside the range a hosting panel already opens. Set port to override.
  • Set publicHost to the address players use to reach the server. If it is blank, the host is detected when the first report is made (a public address on the server, else the hosting panel's address variable). When only a private address is found, links will not open from outside and /scuffeddiag web says so. Edit publicHost and run /scuffeddiag web reload.
  • Use publicBaseUrl instead when a reverse proxy or tunnel serves the viewer and the link must not carry a port.

/scuffeddiag web actions:

ActionEffect
statusListener, port origin and link format, with a warning if the address is private
linkLink for the newest bundle (the live session while a window runs, else LATEST.txt, else the newest folder)
buildRebuild report.html for the newest bundle now. Use after a window was cut short by a restart, or to give an old bundle a viewer
listThe 10 most recent live links
reloadRe-read web-config.json

The report page

SectionWhat it shows
Header and chipsJDK, GC, cores, -Xmx, threads, uptime. Theme toggle and "Download bundle"
TilesWorst tick, peak heap, peak allocation, JVM pauses, freezes recorded
FindingsThe same flagged lines as SUMMARY.txt
TimelineHeap, allocation rate, tick freezes (by world), GC pause time, JVM-wide pauses, world-thread CPU, host CPU, entities (with the share in idle instance worlds), worlds and chunks, outbound bandwidth by packet type, particle packets. All on one time axis, downsampled to 600 points. Heap and entity counts use the bucket mean; allocation, tick time and packet rate use the bucket maximum so a burst is not averaged away
Where the time wentRanked tables: per-system tick time, per-resource save time, outbound packet types, particle effects by id, entity-update components, collision sources, and players (worst ping first)
FreezesThe worst freezes, each with a split of which code owned the samples and the call paths that repeated
BundleMatching server log lines and the file list

How to read it: world-thread CPU near 100% means expensive work (optimize the owner); a stalled thread near 0% means it was blocked or starved of a core (a lock, I/O, a ZGC allocation stall, or too many threads for the box). A high queue with normal ping points to server-side backpressure, not the player's connection. A JVM-wide pause with no world freeze explains a ping spike for every player at once. A report with no named systems means Refixes was absent or too old.


Integrations

Refixes (optional)

Reached by reflection, so ScuffedDiag runs without it. A capture window enables Refixes' per-system, per-resource and diagnostic meters (queue latency, page events, path-finding, archetype churn), snapshots them every 60 seconds so a cut-short window still has data, and switches them off at the end. Do not run /systemtimings capture alongside a window; the two clear each other's counters.

Discord and other bridges

ScuffedDiag has no Discord settings and no webhook keys of its own. It exposes an API and an event feed (below). The EndlessLink chat bridge uses them for a Discord slash command and for server-log alerts, and its configuration lives with that mod. Commands arriving through a bridge are judged by the sender's own permission answer, see Permissions.


API for other mods

com.zbeve.scuffeddiag.api.DiagApi is the supported entry point (API version 3). Every parameter and return value is a plain JDK type, so a caller can reach it by reflection with no compile dependency, and it degrades to an error map when ScuffedDiag is absent. Methods are safe from any thread and never throw.

MethodReturns
apiVersion() / isReady()API version number; whether setup has finished
status()Live health map: per-world TPS lines, worst freeze, heap, allocation, JVM pauses, players, watchdog and auto-capture state, capture state
startCapture(minutes, reason, onDone)Starts a window (1 to 30 minutes) and returns the folder and report URL. onDone receives the summary map when the window closes. An automatic baseline or stall window yields to it; another human's window does not
stopCapture()Ends a running window early (the onDone sink still fires)
recentIncidents(max)Up to 20 recent watchdog records, newest first (trigger, detail, atMs)
setAuto(what, value)on, off, baseline ("<durationMin> <periodMin>" or off), stall (on or off)
addListener(id, sink) / removeListener(id)Subscribe to events. Re-registering an id replaces the old sink
loadLevel(world), loadLevelCode(world), serverLoadLevel(), loadLevels()OK / SOFT / HARD (0, 1, 2; -1 unknown) for one world, the whole server, or every tracked world
lagReport()The /scuffeddiag lagreport text without writing a file. Contains player names and coordinates

Events delivered to a listener, on a dedicated thread through a bounded queue that drops when full:

EventWhenMain keys
watchdogA lag record was savedtrigger, detail, atMs
capture-startA window openedreason, minutes, folder, systemTiming, diagMeters, reportUrl
capture-endA window closedreason, minutes, folder, findings, worlds, worstFreezeMs, heap peak and GC figures, reportUrl

Load levels are observation only: a mod can read them to shed optional work, and ScuffedDiag itself takes no gameplay action.


Overhead

  • Always-on cost is small and bounded: one-second off-thread samplers, a 5-second load poll, and the census, which uses at most about census.budgetUs of world-thread time per tick and reads a large world over several ticks. The census cost appears at the bottom of lagreport.
  • With recording off, the freeze stack sampler, taps and spike records are skipped. The tick monitor still records the cheap tick interval so the watchdog and TPS guard can work.
  • Recording turns on the stack sampler (only walks stacks while a tick is already over budget) and four packet taps, and runs a collision probe on each populated world every 30 seconds, time-capped at 15 ms. At the default baseline schedule that is a 10% duty cycle.
  • Per-system timing in Refixes adds a small allocation per system tick while armed, so it is armed only for windows and for 2 minutes after a severe stall.
  • Heavy tools are on demand only: heaphisto and heapdump pause the server, leakhunt and allocprofile add roughly 1 to 2% while recording, and the lock dump costs one safepoint and a stack walk per thread.
  • Report building runs on a low-priority thread, and streaming the zip does not buffer the bundle in memory.

Troubleshooting

SymptomCause and fix
spikes, disconnects or particles is emptyRecording is off by default. Run /scuffeddiag on, or open a window with capture or report
Command answers "You need operator status"Grant scuffeddiag.command or OP to the player, or run it from the console
status shows "no ticks observed in the last minute"That world has no players. Tick monitoring only counts worlds with players
Report link does not open from outsideThe link host is a private guess. Set publicHost (or publicBaseUrl) in web-config.json and run /scuffeddiag web reload
Log says the viewer could not bindThe port is in use or blocked. Set port to another port inside your allocated range, or change portOffset, then restart
Link shows "Still recording" foreverThe window was cut short by a restart. Run /scuffeddiag web build
Link says "Report not found"The link expired or the day folder was pruned (7 days)
No named systems in the reportRefixes is missing or too old. Install a current Refixes
CAPPED.txt in a session folderThe session reached 120 MB and stopped writing. Run a shorter window
nmt says NMT is not enabledAdd -XX:NativeMemoryTracking=summary to the JVM arguments. The other sections of the dump still work
leakhunt or heaphisto fails to startThe host blocks JFR or the diagnostic MBean
No automatic bundles after a freezeCheck watch status, auto status and autoreport status. Windows are skipped during the cooldown, while an operator is recording, or when disk is low for the allocation profile
diag-config.json changes ignoredA syntax error leaves the file untouched and runs on defaults. Fix it and run /scuffeddiag config reload; check with config show
Heap flag warning at bootThe JVM does not expose SoftMaxHeapSize (not ZGC). The rest of the plugin is unaffected