You are viewing a potentially older version of this package. View all versions.
DeathMonger-DiagnoseServerLag-0.2.2 icon

DiagnoseServerLag

Names the cause of lag instead of guessing. Measures your client and the server once a second and tells you which it is: starved server, saturated link, packet loss, object churn, world loading, or your own machine.

Date uploaded 2 weeks ago
Version 0.2.2
Download link DeathMonger-DiagnoseServerLag-0.2.2.zip
Downloads 159
Dependency string DeathMonger-DiagnoseServerLag-0.2.2

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2350 icon
denikson-BepInExPack_Valheim

BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.

Preferred version: 5.4.2350

README

Diagnose Server Lag

Says which kind of lag you actually have, instead of guessing.

"The server is lagging" covers six unrelated failures. They feel identical while playing — the game goes sticky — and they have nothing in common but that. A starved server, a saturated link, a lossy connection, an overloaded base, a world still loading and a computer that cannot keep up each need a different fix, and picking the wrong one costs an evening. Worse, the measurement people reach for first actively misleads: a frame-rate counter reads perfectly fine while the server burns, because your machine has nothing to struggle with when the updates never arrive.

This mod measures both ends once a second, and names the cause.

What it tells you

Press F8 and the report opens with a verdict in plain language, the evidence it was decided from, and what is actually worth doing about it. For example:

The server is badly starved: 84 ms per tick sustained (12 per second). Nothing on your machine will change this, and it is happening to everyone online at the same time.

  • tick 91.2 ms now, 84.4 ms median over the server's last 60s
  • worst single tick in that window 340 ms
  • 47 server stalls over the window
  • 18,402 networked objects in the world, 612/s sent to 3 players

or

Packets are being lost: connection quality 91.4%. This is the network path, not the server and not your computer, so neither restarting the server nor turning settings down will touch it.

The six causes it separates:

Cause What proves it
Server starved The server's own tick times, which a client cannot see at all
Link saturated Bytes handed to the socket that have not left, and whether that number is climbing
Connection quality Steam's measured packet-loss fraction, and ping jitter judged apart from latency
Object churn Object updates per second against this server's own recent normal
World loading Objects being built into the scene at the moment the frame stalled
Your machine Frame times, but only claimed once the other five have been measured and ruled out

More than one can be true at once and often is — a saturated link and heavy churn are usually one event seen from two ends — so the report ranks them and shows the rest under the headline.

Why it needs the server

The server half is the point of the mod. A client watching itself can tell you the game feels bad; only the server's own tick times can tell you whether the server was keeping up at the time, and that is the single measurement that decides whether anything on your machine is worth changing.

Without it the mod still works and still finds link saturation, packet loss, churn and loading — it simply cannot rule the server in or out, and it says so rather than quietly guessing:

The server is not running this mod, so it cannot be measured. Every verdict below is made from this machine's side alone, and "the server was fine" is the one thing it cannot tell you.

Installation

Install on the server and on every client (Gale, r2modman or Thunderstore Mod Manager, or drop DiagnoseServerLag.dll into BepInEx/plugins). Keep the same version everywhere.

  • Server without the mod: clients diagnose their own end and say the server half is missing. Nothing breaks.
  • Client without the mod: that player gets no report. Everyone else is unaffected.
  • Hosting the world yourself: both halves are the same process, and the server numbers are read directly with no asking involved.

Works on Windows and Linux dedicated servers. Requires BepInExPack for Valheim.

Keys (configurable)

Key Does
F8 Open and close the report. Escape closes it too.
Shift+F8 Turn the small corner readout on and off.

There is also a pause toggle in the report's top-right corner, the same mark used on the large map and GrabMaterials' inventory panel: gray when off, Valheim orange while the game really is paused, red with a slash when the pause was asked for and refused. It is off by default.

Pausing matters more here than on those other panels, because this one reports on the very thing a pause changes. While the game is paused the mod stops recording. A paused world simulates nothing, so frames get cheap and the link goes quiet — recording those seconds would let the report work its way round to "nothing wrong right now" while you sat reading it. Instead a pause freezes the evidence, which is what you want when you paused to read why the last minute was bad. The report says so while it is held.

The pause goes through vanilla's own calls, so it works by itself solo or hosting alone; on a dedicated server it takes Pause My Server, and the button tells you whether it actually took.

F8 is free in vanilla Valheim. It replaced F10 in 0.1.1, which clashed with AutomaticFuel's default; if you installed 0.1.0 the mod moves your config over for you, unless you had already chosen a key of your own.

The readout is four lines — your frame time, ping, queue and the server's tick time — with the worst one colored. It is off by default, because watching numbers is the habit this mod exists to replace.

Console commands

Command Does
dsl Open or close the report
dsl_why Print the verdict and its evidence as text
dsl_now The last second measured on this machine
dsl_server What the server last reported about itself, including the per-player table
dsl_dump Write every measured second to a CSV under BepInEx/config/DiagnoseServerLag/
dsl_reset Forget the history and rebuild the baseline

dsl_why and dsl_server work on the dedicated server's own console too.

Configuration

BepInEx/config/DeathMonger.DiagnoseServerLag.cfg. Every threshold is exposed and documented, because a verdict is only worth as much as the number it was decided with, and the right number depends on hardware nobody else can see.

The ones worth knowing about:

  • Stall Milliseconds (100) — a frame longer than this counts as a stall.
  • Window Seconds (10) — how many recent seconds the verdict is made from.
  • Baseline Seconds (60) — how much history counts as "normal for this server".
  • Server Tick Warn / Severe Ms (50 / 100) — how slow the server's tick has to get before it is called slow, and then badly starved.
  • Steady Tick Ratio (1.5) — how close the server's worst tick must be to its median before the tick rate is read as a deliberate frame cap rather than a struggle. Many dedicated servers run a limiter (30 ticks a second is 33.3 ms), and a capped server is not a slow one: a limiter holds every tick to nearly the same length, while a machine that cannot keep up produces variance. Below this ratio, with no stalls, the server is left alone however slow the number looks — up to the severe threshold, above which nothing is forgiven.
  • Queue Warn / Severe Bytes (16 KB / 64 KB) — queued bytes that have not reached the wire.
  • Pause While Open (off) — pause the game while the report is open. The corner toggle changes this.
  • Show Pause Button (on) — whether that toggle is drawn.
  • Churn Factor (3) — how many times this server's own normal rate of object updates counts as churn.

Server-side only, read by whichever machine runs the server:

  • Answer Clients (true) — turn off and the server becomes indistinguishable from one without the mod.
  • Share Peer Detail (true) — include the per-player table (each player's ping, quality, queued bytes and distance from the world center) in the answer to everyone. The aggregate numbers that diagnose the server always go to everyone; this is the part that names who is on a bad line. Admins receive it either way.

How it works

Once a second each end records its frame times, the game's own network counters, its socket's queue and send rate, and how many objects it knows about and is exchanging. Ten minutes of those seconds are kept in a ring buffer on each side.

Two kinds of test run against them. Measurements with a physical meaning — packet loss, ping jitter, server tick time — are judged against absolute thresholds, because a connection losing packets is losing packets whatever it did a minute ago. Everything whose normal value depends on the world and the hardware is judged against this server's own recent median instead, because there is no universal right answer for how many objects should be changing near a large base.

Clients ask the server for its numbers once a second while the report is open and every five seconds otherwise; a server with nobody looking sends nothing at all. Every reading is a counter read or a dictionary count — nothing walks the object graph — because a diagnostic that cost a millisecond a frame would become a cause of the thing it is trying to explain.

One deliberate omission: Valheim's ISocket.GetAndResetStats would give cleaner byte totals and is never called, because it zeroes the counters the game keeps for its own bandwidth display.

Reading the blind spots

Ping and connection quality come from Steam's own connection status. Valheim's plain TCP path reports both as zero, so a zero here means not measurable, not perfect — the report says "not measurable on this socket" rather than coloring it green.

Nothing inside the game can see the host machine's CPU being shared with other tenants. It can, however, see the consequence: a server tick time that is bad while the object count and traffic are normal is the signature of an oversubscribed host, and that is itself a finding you can act on.

Compatibility

Reads public game API only and patches nothing that affects play. It should coexist with anything.

BetterNetworking changes how the protocol is compressed and paced. The measurements stay meaningful — queue sizes and tick times are the same numbers — but the byte rates are of compressed traffic, so they read lower than the uncompressed equivalent.

Source

https://github.com/gbahns/ValheimMods

CHANGELOG

Changelog

0.10.3

  • The capture now says what each machine is. CPU and cores, RAM, GPU and its memory, and the screen resolution, in a MACHINES block at the top of the report. Static for the session, so it travels in the header rather than being repeated in every one of eighteen hundred samples.

    This came out of a comparison that looked damning and was not. Two players doing identical CPU work - 222% of a core against 219% - for 27 ms frames and 52 ms frames reads as one machine being broken, right up until you learn the faster one was rendering 5120x1440 with HD texture mods and near-maximum settings. Same work in, half the frames out, is a hardware finding or a settings finding depending entirely on facts the mod was not recording.

  • New: free physical memory, once a second. GlobalMemoryStatusEx on Windows, MemAvailable from /proc/meminfo on Linux. It shares the machine line and takes over its colour below 2 GB.

    This answers a question nothing here could: is a machine slow because it is paging? That has a distinctive shape - a long frame at low CPU, because the process is blocked on a disk rather than computing - but telling it from an ordinary stall needs to know whether memory was short at the time. Process.WorkingSet64 reads zero under this Mono, so even the game's own footprint was unavailable; free physical memory is both more reliable and the better question, since paging is a property of the machine rather than of one process.

  • And what each player has their graphics set to - every slider and toggle, on its own line under that machine, taken from the game's own GraphicsSettingInt and GraphicsSettingBool enums rather than written out here, so a setting Iron Gate adds appears by itself. Valheim stores each under PlatformPrefs keyed by the enum member's name, which is what makes walking the enums work. A key that cannot be read is left out rather than guessed at.

  • GPU detail beyond the name: video memory, vendor, driver version, shader level and graphics API. A laptop quietly running on its integrated chip reads as vendor Intel while the machine has a discrete card - the likeliest explanation for good hardware and bad frames - and a years-old driver string is the next.

  • Wire layout 11, and the client series header grew too. Update the server before the clients.

0.10.2

  • Stalls while the world is loading are counted separately and no longer set off the warning. A hitch as a zone streams in is the cost of going somewhere, not a fault, and colouring it red teaches people to ignore the one line that matters. The readout now reads stalls 0 in 10s +12 loading, with the colour judged only on the stalls you can act on.

    Nothing is hidden: the loading figure is shown, and loading is a column in the group capture, because a machine that takes fifteen seconds to load an area has a real problem worth seeing.

    Streaming is detected from the loaded-object count changing sharply between seconds - a measured teleport went from 280 objects to 4,600 in six, while standing still moves none and ordinary running a few dozen. The threshold is Loading Objects Per Second, default 300. Either direction counts, since unloading an area hitches as much as loading one.

    This came out of reading three captures in a row where the biggest frames were all portal arrivals, and having to say "ignore those" by hand every time.

  • Wire layout 10. Update the server before the clients.

0.10.1

  • New line: built - how many of the nearby objects are player-built pieces, rather than trees, rocks and scenery. The plain object count could not answer "how big is our base": around Greg's, 5,094 objects were loaded, but a quiet stretch of swamp loaded 4,233, so the count alone says almost nothing about what anybody built. nearby_pieces is in the group capture too.

    Worked out once per prefab type and remembered, never per object: the loop it lives in runs over every loaded thing once a second, and a GetComponent on each would have made the measurement a cost of its own.

  • Wire layout 9. Update the server before the clients.

  • A missing machine-CPU reading now says so, instead of reading as an idle machine. A client too old to send the field had it default to 0, and 0 is a perfectly plausible busy figure - so in the first capture that used it, one player showed 0.0% machine busy for all 1800 seconds while burning 254% of a core. The most misleading value it could have chosen, and indistinguishable from a real measurement without checking that every second held the same number. Anything absent now reads as unavailable.

0.10.0

  • New line: machine - how busy the whole computer is, and how much of that is not Valheim.

    Everything else here measures this process, which is the right thing for "is the game heavy" and the wrong thing entirely for "is something else eating my computer". The two produce different signatures and only a machine-wide figure separates them: a stall with Valheim's own CPU unchanged is the game pausing itself, while a stall with Valheim's CPU dropping against a pegged machine is something else taking the processor away.

    It was added because a player's 78 stall seconds could not be attributed. His CPU held steady through them, which ruled out the background process he suspected, but nothing on hand could say what the rest of the machine was doing.

    GetSystemTimes on Windows, /proc/stat on Linux, so it works on the clients and on the dedicated server. Both are cumulative counters, so the figure is the change between samples and the first reading after startup reports unavailable rather than guessing. system_cpu_pct is in the group capture, so the same question can be asked of everybody at once.

  • Wire layout 8. Update the server before the clients.

0.9.10

  • New setting: Show My Own Ownership, off by default. The owner labels stay silent about creatures you own yourself, which is right - the labels exist to show where an interaction is going, and one to your own machine goes nowhere - but that makes the feature impossible to test alone, because solo you own everything near you and the unowned ones are too far off for the game to draw a nameplate on at all. Turn this on to watch it work before there is anybody to watch it work against.

0.9.9

  • Players SpreadTheLoad is steering work away from are greyed. Their low counts are the mod working rather than a fault, and the grey overrides the latency colour on purpose: an amber round trip beside two dozen objects would invite exactly the wrong conclusion about why their row is small.

    Neither mod could do this alone - SpreadTheLoad is server-only and this readout is drawn on the client - so DSL's server half finds SpreadTheLoad.SpreadTheLoadApi by reflection and the yield list rides down with the server report. Reflection rather than a reference so this mod still loads on a server that has never heard of SpreadTheLoad, which is every server but the one it was written for. Absent, disabled or broken all mean "nobody is yielding".

  • Six owner rows instead of four. A five-player group is five other owners once the server is counted, and the list truncated by object count - cutting whoever held least, which is precisely the row worth seeing when somebody is being steered away from.

  • Server report layout 3. Older clients stop at the layout they know, so there is no ordering requirement this time.

0.9.8

  • The readout is an ownership table, and you are a row in it. Greg's design: your own holdings sit under your character name in the same column shape as everybody else's, instead of a "simulating" line and an "objects" line in a different format above the others. The question is never "how much do I have" but "how does my share compare to theirs", and two line formats made that a calculation rather than a glance.

    Greg           4183 obj  11 mob
    Brane           320 obj   7 mob   144 ms
    server            1 obj   0 mob    82 ms
    unowned        1445 obj  12 mob
    total          5629 obj  23 mob
    
  • unowned is a row rather than a suffix, because without it the arithmetic does not close - 4183 + 1 + 1445 = 5629 only reads as complete when the parts are stacked. unowned_ai joins the capture, so the CSV carries the same breakdown as the screen.

  • The bottom line is now server tick. With a server row in the table for what the server itself owns, two unrelated lines shared one label. It pairs with server feed.

  • Wire layout 7. Update the server before the clients.

0.9.7

  • Owner labels now work on tamed animals. Greg found they did not. Character.GetHoverName never names a tame itself - it hands straight off to Tameable.GetHoverName, so wolves, boars and lox were the one category the label could not reach. Both methods are patched now, with the outer one standing down when a Tameable is present so nothing is labelled twice. Worth having: a tame somebody else owns behaves exactly like any other object of theirs.
  • Ownerless creatures are labelled [unowned]. Nobody is running their AI at all, which is why distant ones stand inert until somebody gets close enough for the two-second pass to hand them over. One solo reading had 12 of 23 nearby creatures in that state; this shows which.

0.9.6

  • Unowned objects are counted. Greg asked whether the server holds nearby objects before handing them over. Mostly it does not - away from world origin they sit unowned until a player's two-second pass claims them, and an unowned creature runs no AI at all. The objects line now says how many are loaded but unsimulated, and unowned_objects is in the capture.

  • Corrected a wrong comment, and it matters. The code claimed "a dedicated server never instantiates prefabs". The captures disprove it: bahnsheim held 82 instances against 430,020 ZDOs. ZNet.m_referencePosition is only ever assigned from client respawn code, so on a dedicated server it stays at its Vector3.zero initialiser and the server keeps an active area around world origin, permanently. What falls in that disc it instantiates and owns - and an instantiated creature there runs BaseAI.UpdateAI on the server.

    So server CPU can be a factor after all, bounded to roughly 112 m of spawn. That is not a small exception: most groups' first base is near spawn. The server's own owned_ai and owned_objects are already in the capture next to its cpu_ms_per_sec, so the correlation is testable.

0.9.5

  • Each owner's row shows creatures as well as objects. 0.9.4 replaced one with the other; Greg pointed out they are different burdens and both matter. Any object somebody else owns costs you a round trip when you touch it, but a creature also costs them CPU every frame, because BaseAI.UpdateAI runs only on the owner. Ten thousand of their fence posts sitting idle is not the same as ten of their trolls thinking, and a row that showed only one of those numbers could not tell the two situations apart.

0.9.4

  • Objects are counted, not just creatures. Greg spotted the gap: the readout said "simulating 11 of 23" and meant creatures, but ownership routing is not special to AI. TreeBase.RPC_Damage opens with the same if (!m_nview.IsOwner()) return; that BaseAI.UpdateAI does, so a tree, a rock, a container or a workbench somebody else owns costs a round trip through their machine exactly as a greydwarf does. A zone could read zero creatures while every axe swing still travelled through another player - which is precisely the case people complain about, chopping wood. There is now an objects line, the per-player rows count objects rather than creatures, and owned_objects/nearby_objects are in the group capture.
  • Owner names on creature health bars, F6. The readout can say Brane is simulating seven creatures; it cannot say that this greydwarf, the one ignoring your axe, is one of them. Off by default. It works through Character.GetHoverName, which EnemyHud reads its label from, so it needs no UI code - and because Player overrides that method, the label never appears on players.
  • Wire layout 5. Update the server before the clients, as with 0.9.3.

0.9.3

  • New line: server feed. How often the server actually reaches this client, measured, next to what the send cycle predicts. The server does not broadcast - SendZDOToPeers2 serves one peer per frame behind a 50 ms gate, so a full cycle is 50 ms plus one server frame per connected player: 150 ms at three players, 217 ms at five, 317 ms at eight, at the dedicated server's 30 Hz cap.

    This is the measurement CPU and tick time cannot make. A server can sit at 14% of one core, hold a perfect 33.3 ms tick, and still only reach you five times a second, because the cost is in the scheduling rather than the load. It is the one candidate for lag that grows with the number of people playing - which is the complaint this mod was built for, and the one thing a healthy server reading has never been able to rule out.

    Judged against the prediction rather than a constant, because 200 ms is expected at eight players and a fault at two. feed_ms is in the group capture CSV as well, so a session with five people either confirms the formula or kills it.

  • Wire layout 4. Update the server before the clients: an older server reading a newer client's series would stop short and misread the samples after it. Older clients against this server are fine.

0.9.2

  • A row per player, instead of a label nobody could read. The readout used to put who owns what on one line and the worst latency on another, under the heading "their stuff" - two rows from two sources to learn one thing about one person. Now each nearby owner gets a row carrying both: how many creatures they are simulating and what it costs you to touch them. The name is the label, so no heading is needed at all.
  • The readout lines up. Values sit on a column stop rather than being padded out to a fixed character count, which only ever worked in a monospaced run - forcing this font to monospace set every glyph on the same advance, so "frames" read as "f rames" and an eleven-character label ran straight into its value. The block is also left-aligned in every corner now; right-aligning it lined up the ends of the values and left the labels ragged.
  • "round trip" is now "server trip". It never said what it was a round trip to, and the new per-player rows are round trips as well - to somebody else entirely.
  • The server is named as the server. It owns a real share of the world - 82 objects of 395,778 in one capture - and is not in the player list, so it was appearing as "another player" and pointing suspicion at a teammate.
  • "Brane has 7 of 2 others" is gone. It read as a fraction, but counted creatures in the numerator and players in the denominator, so it meant nothing. Owners are now listed with their own tallies, sorted busiest first and then by name - the tally comes from a Dictionary, so equal counts would otherwise swap places every second and make the line flicker.
  • "(+1 more)" is gone too, for naming no unit. Where a summary is still needed it says what it is a summary of, and it counts only owners that actually answered rather than promising latencies that were never measured.

0.9.1

  • The bottom corners now sit above the game's key hints. Greg spotted the readout landing on top of the control reference along the bottom of the screen. The clearance is measured from the hints themselves each second rather than set to a constant, because the hints are contextual - building, fighting and fishing each show a different block at a different height - so any fixed offset would be wrong most of the time. Turn the hints off in Settings -> Gameplay and the readout takes the space back.

0.9.0

  • Latency to the people whose objects you are using. Greg's idea. Your ping to the server does not decide how an interaction feels: Valheim routes it to the object's owner, so hitting a tree somebody else loaded travels you → server → them → server → you, with their connection and their frame rate in the middle. That number exists nowhere in the game, and it is the one that governs whether the world answers you.
  • Per owner, not per object — every object the same player owns travels the identical path, so the latency belongs to the person.
  • Measured, not estimated. Adding your round trip to theirs would be a defensible guess; an echo over the real path costs a few bytes and includes what a guess leaves out — the server's forwarding and the owner's own frame time, which is exactly the cost when the owner is the one struggling.
  • The report lists every owner with their creature count and latency; the readout shows the slowest one, since that is the one you feel. An owner who does not run the mod is listed as not replying rather than being silently dropped.
  • Echoes go to at most four owners every four seconds, and the list forgets anyone whose objects are no longer loaded near you, so it follows you around the world instead of accumulating everyone you have ever stood beside.

0.8.0

  • The readout moved to the bottom right, and Readout Position now offers all four corners plus Custom, with Readout Custom X / Y as percentage sliders. Position is re-read every refresh, so you can drag the values while watching it move.
  • Toggle key is now F7, on its own rather than a chord — it is pressed far more often than a two-key combination deserves. Configs on Shift+F8 or Shift+F10 are migrated automatically.
  • Fixed a false alarm: cpu was colored red at normal load. The thresholds judged the client against one core, but a game client is heavily multi-threaded — Valheim's measured 331% of a core while running perfectly. It is still shown per-core, because that is the number people recognise, but judged against the whole machine, and it now shows both.
  • A socket ping of exactly zero is not a measurement, it is a socket declining to answer — the same trap as connection quality and working set. The mod's own round trip is preferred whenever it has one, and a genuinely sub-millisecond link says so rather than reporting 0 ms.

0.7.1

  • simulating no longer cries wolf when you are alone. On your own you own everything near you by definition — that is the design working, not a warning — so the line only colors when the game reports more than one player online. Caught before it shipped to anyone: solo on a test server, every session would have shown a warning-colored 12/12 that meant nothing, and a readout that warns about normal is a readout people stop reading.
  • The count is still shown when alone. It is the coloring and the "who holds the rest" note that wait for company.

0.7.0

  • The corner readout now shows what a player can act on, and is on by default (Shift+F8 toggles; an existing config keeps whatever it was set to). Six lines:

    frames      10 ms  103/s
    stalls      0 in 10s
    cpu         45% of a core
    round trip  18 ms
    simulating  41/44  (Marco has 3)
    server      33.3 ms  412669 obj
    
  • simulating is the line that justifies the readout. Valheim runs a creature's AI only on the machine that owns its ZDO; ownership falls to whoever was in range first and is never rebalanced. Nothing else in the game will ever tell you that you are carrying a zone for four other people — and it is the only number here you can act on, by spreading out or letting the strongest machine enter first. It colors on share, not count: ten creatures all yours is worth noticing, forty split across five players is not.

  • It names who holds the rest where it can. A client's peer list contains only the server, so an owner id cannot be looked up directly; the synced player list is matched instead, via the id carried in each character's ZDOID. That match can fail for a character made in an earlier session, in which case the count is still shown and only the name is missing — an unnamed number is honest, a wrong name would not be.

  • Deliberately absent: connection quality, queue bytes, heap, collections. They matter when a rule fires, and the rule is one key away.

0.6.1

  • Measures who is simulating the creatures. Valheim runs a creature's AI only on the machine that owns its ZDO (if (!m_nview.IsOwner()) in BaseAI.UpdateAI); every other client just renders what the owner reports. Ownership goes to whoever was in range when the object had none, sticks until they walk away, and is never rebalanced — so a group that piles into one zone leaves one person simulating all of it, on a machine nobody chose.
  • Each machine now reports creatures owned and creatures loaded nearby, counted from the game's own BaseAI.Instances list once a second. It appears in the report, in dsl_bench, in both CSVs, and — where it matters most — in the group capture, which now names who is carrying the group's simulation and what share of it.
  • The concentration finding is only printed when somebody actually holds a disproportionate share, so a spread-out group is not nagged about a design working exactly as intended.
  • This is why the server measuring clean and the game feeling bad are not a contradiction: the server does not take this load. On the real server it owned 82 objects out of 395,778.

0.6.0

  • The group capture now brings back every column from every machine, not the five the correlation needed. Greg's point, and the first real capture had already proved it: it answered "were the stalls shared" (they were not) and then could not say what the machine had been doing during them, which took a second command on a second machine to find out.
  • That second command does not scale to other people. Every machine records continuously and dsl_bench reads backwards, so a teammate asked later can still cover the same minute — but it costs four people's attention and four files, and a client that has logged off has taken its history with it, since the ring lives in memory. One admin command, while everyone is still connected, now ends with everything.
  • The series travels compressed (ZPackage.WriteCompressed). The payload objection that justified trimming it does not hold anyway: the window is fully recorded before any of it is sent, so the transfer cannot contaminate the measurement it is carrying.
  • group-<timestamp>.csv carries all 29 columns, one row per machine per second.
  • Clients older than 0.6.0 still answer, in the reduced format. Their extra columns are written empty rather than zero, so a gap is never read as a measurement, and the summary says how many sent the reduced form.

0.5.1

  • The group capture in 0.5.0 never ran. All of its code shipped, and none of it was reachable: BeginGather was defined but never called from either place that should have called it, so dsl_bench_server quietly did exactly what it did in 0.4.2 and wrote no group report at all. Caught by a real capture on the server producing no group-*.csv, and the server log showing the old code path word for word.
  • The cause was two scripted edits that silently failed to match and were not checked. The build succeeded because the code they should have replaced was still valid. Both call sites are now wired and verified.

0.5.0

  • dsl_bench_server now captures the whole group. The server takes its own window and asks every connected client for the same one, then answers with all of them together. Greg's idea, and it turns the mod from "diagnose this machine" into "diagnose this session".
  • The point is the correlation, not the collection. Stalls happening on two or more machines in the same wall-clock second are one shared event — the server, or the path everyone crosses. The same stalls scattered across different seconds are separate local problems. The numbers are identical either way and only the alignment tells them apart, which is exactly what no single capture can do. The report says which, and names the seconds.
  • Each sample now carries a UTC timestamp. Time.unscaledTime counts from process start, so two machines agree on nothing; without wall clock a group capture would be a pile of unrelated summaries.
  • Clients send a compact per-second series rather than a summary, because a summary cannot be aligned. Only the columns correlation needs travel; the full record stays in each machine's own CSV.
  • A group-<timestamp>.csv is written on the server, one row per machine per second, in long format so a spreadsheet pivots it in a click.
  • Share My Performance (client-side, default on) decides whether this machine answers. It sends frame times, stalls, CPU share and collection counts for the asked-about window — performance numbers only, nothing about what you were doing. Turn it off and you are simply absent from the group view.
  • Clients that do not answer are counted and named as such, so a partial picture is never mistaken for a complete one. Clients older than 0.5.0 cannot answer and will show up that way.

0.4.2

  • Stopped inventing a distinction between GC generations. Unity's Mono — which every copy of Valheim ships, on Windows and Linux alike — returns the same number from GC.CollectionCount for all three generations. A real 300-second capture showed gc0 == gc1 == gc2 in all 300 rows. The mod now detects that from accumulated counts and reports one honest "collections per minute" figure instead of three identical ones dressed up as generations.
  • The garbage-collection verdict adapts with it: where generations are real it still ignores gen0, which is cheap and constant, and where they are not it tests whether a collection happened at all. The base-rate comparison — collections during stalled seconds against quiet ones — carries the rule either way, which is why it still works without the generation filter.
  • "working set 0 MB" was never a measurement. Mono leaves Process.WorkingSet64 at zero, and the report was printing that as though the process used no memory. It now says "not measurable" wherever it appears — panel, dsl_bench, dsl_cpu and the compare script. The managed heap is unaffected and still reported.
  • Captures record what the runtime could actually answer, as gc_generations_distinct and working_set_readable in the CSV header, so a file stays readable long after the session.

0.4.1

0.4.1

  • Captures can cover an hour instead of ten minutes. The history was a fixed 600 samples, which capped dsl_bench at ten minutes. It is now a History Minutes setting, default 60 and adjustable up to 180. A sample is about 110 bytes, so an hour costs roughly 400 KB and three hours about 1.2 MB — the ceiling is set by what is useful to capture, not by what it costs.
  • The setting's description says the thing that is easy to get wrong: longer is not automatically better. The summary reports medians over whatever is in the window, so a capture spanning ten minutes of work and ten of standing still describes neither. Match the capture to the activity.

0.4.0

  • "Your machine hitched" is no longer the end of the conversation. A new verdict attributes stalls to garbage collection when the evidence supports it. A collection pause is invisible to every other measurement here — network fine, server fine, frame average barely moving, one frame in a hundred taking a quarter of a second — which is exactly the shape people report as random stuttering. Coincidence alone is not treated as evidence. A large heap collects gen0 constantly, so "there was a collection during the stall" is nearly always true and proves nothing. The rule asks whether collections are disproportionately concentrated in the seconds that stalled compared with the seconds that did not, and stays silent when the rate is merely high throughout. Gen0 is ignored entirely; only gen1 and gen2 actually pause.
  • The report now shows game CPU, collections per minute, heap and working set for your own machine. They were recorded from 0.3.0 and only written to the CSV, so the panel could say the machine hitched while showing nothing about what the machine was doing.
  • When the verdict is still "this machine", it now carries that CPU and memory evidence instead of naming the machine and stopping.

Also in this release

  • A throwing method was discarding measurements that worked. The three per-socket figures were read inside one try, so when one threw the other two went with it. That cost real data on a real server: ZPlayFabSocket.GetCurrentSendRate() throws NotImplementedException outright, and it was taking the send queue size down with it — the one per-player number genuinely measurable there, and the one that detects saturation. Each figure is now read on its own.
  • The mod measures latency itself now. No socket Valheim gives a dedicated server can report a ping: ZPlayFabSocket inherits GetConnectionQuality from ZNetStats, the stub that hardcodes ping and quality to zero, and ZSteamSocket reaches for the client Steam interface, which a server process never initializes because SteamAPI.Init lives in the client-only SteamManager. So the request the mod already sends every second now carries a sequence number, the report echoes it back, and the gap is a real round trip.
  • Clients pass their measured round trip up with the next request, so the server's per-player table shows a real latency for everyone running the mod — something no socket on a dedicated server can provide. Shown as ms rt to keep it distinct from a socket ping.
  • A round trip is labeled as a round trip, not passed off as a ping. It includes a frame of server processing, which arguably makes it the more useful figure — it is how long an action takes to be acknowledged — but it is not the same number.
  • Connection quality no longer claims to be measured just because a latency exists. A round trip says nothing about packet loss, and the two had shared one "is this measurable" flag.
  • Report layout 2 adds both fields after the peer block, so an older client reads every field it knows and never notices the trailing bytes.

0.3.1

  • dsl_bench could not be run on a dedicated server — the machine it was written for. A Valheim dedicated server has no console to type into: commands registered with Terminal.ConsoleCommand only ever reach the in-game console, which needs a client, and the server process reads nothing from stdin. Verified against the real server by sending it a plain save and watching it do nothing.
  • dsl_bench_server [seconds] fixes it. An admin runs it from a connected client; the server captures itself, writes its CSV, and sends the summary back to be printed in that client's console. Admin only, because it makes the server do work and write a file.
  • Corrected the README, which had claimed since 0.1.0 that dsl_why and dsl_server work on the dedicated server's own console. They never could.

0.3.0

  • Measures what the server costs the machine, not just how long its ticks are. Tick time cannot measure a capped server, and most dedicated servers are capped: bahnsheim holds 33.3 ms with almost no variance, and that single number is equally consistent with 3 ms of work plus 30 of sleep or with 33 ms of work and nothing left over. Those are the same measurement and opposite situations. The mod now records CPU time consumed per wall second, which survives the cap, says how much room is left before the server is in trouble, and is the one figure that compares honestly between two different machines.
  • Also records garbage collections by generation — a gen2 pause is a stall the tick average hides — along with managed heap and working set.
  • dsl_bench [seconds] prints a summary built for comparison (role, cores, world, tick, CPU share, headroom, GC rate, memory, traffic) and writes the same window to a CSV. It reads the history already in the ring, so it returns immediately rather than starting a timer.
  • dsl_cpu prints the current CPU share and headroom on its own.
  • The CSV now carries a # metadata header — mod version, role, cores, CPU model, OS, world size, player count — so a capture can be identified later without anyone remembering which machine it came from, and gained columns for CPU, GC and memory.
  • compare-servers.ps1 puts two captures side by side. -A dathost fetches the newest capture off the DatHost server over its REST API; -List shows what captures are on it. It refuses to draw a conclusion from captures of different worlds — it says so loudly and falls back to per-object normalization, because comparing two servers carrying different amounts of world is comparing worlds, not servers.
  • Headroom is reported against one core, not the machine. Valheim's simulation is effectively single threaded, so idle cores next to it do not raise the ceiling.

0.2.3

  • Stopped flooding the server console. On the real server, 452 of the last 600 console lines were one warning from this mod — "Could not read a peer socket: Steamworks is not initialized" — repeated once per peer per second, with nobody connected. A diagnostic mod was making the server harder to diagnose. The cause: GetConnectedPeers() returns everything in the peer list, including sockets still being set up or torn down, and calling into a socket like that throws. Vanilla's own GetNetStats walks the same list but touches a socket only when IsReady() — "has a uid yet" — and that guard is the entire reason vanilla never hits this. The peer walk now has it too.
  • The per-player table was empty on every real server. The row was built after the socket call, so any peer that threw was dropped before it was ever added. That silently emptied one of the things the mod exists to show, and left the server's worst-queue figure sitting at a reassuring 0 B that nothing had measured. The row is now built from the peer's identity first and kept whatever the socket does; only the socket figures are left as not measurable.
  • "2 connected" over an empty list — the player count included peers that had not finished handshaking. It now counts the same peers the table shows.
  • Any socket that still cannot be read is reported once per session instead of once a second. The reading itself is not disabled: the failure is per-peer and transient, and switching it off for everyone because one stale peer threw would trade a noisy bug for a silent one.

0.2.2

  • Stopped accusing a healthy server. A real server reported 33.3 ms per tick now, 33.3 ms median and a 34 ms worst tick, with no stalls — and the verdict called it starved. That is not a server in trouble; it is a server pinned to exactly 30 ticks a second by a frame limiter, and the old 33 ms threshold sat precisely on top of that cap. The rule now looks at the spread rather than the level: a frame limiter holds every tick to nearly the same length, while a machine that genuinely cannot keep up produces variance, because the work that overruns is not the same work every tick. A worst tick close to the median, with no stalls, is read as a cap and left alone. This is not a list of known cap values — a server capped at 20 or 60 is recognized the same way, with nobody having to enumerate them.
  • Above the severe threshold a steady tick no longer earns the benefit of the doubt: a server holding a metronomic 200 ms is still far too slow to run the game, however even it is.
  • Thresholds raised accordingly — warn 33 → 50 ms, severe 66 → 100 ms — with a new Steady Tick Ratio setting controlling how close the worst tick must be to the median to read as a cap.
  • The server section now shows a steadiness row explaining which of the two it is, so the number that caused the confusion carries its own explanation.
  • The value column was unreadable. It used the shared row helper, whose right-hand column is a fixed 96 pixels — right for a scoreboard's short numbers, hopeless for these, so every value ellipsized to a few characters: "9.7 ms (103…", "42 ms in th…". Measurement rows now use a layout built for them, with a narrow fixed label column and the value taking all the remaining width.
  • Negative queue sizes are clamped. A real session reported "-21294 B queued". The game's own GetSendQueueSize only ever sums non-negative values, so whatever Steam is reporting through that struct, a negative backlog is not a measurement — and it was being fed to the saturation rule as well as printed.

0.2.1

  • The report now takes the mouse. It had no Harmony patches at all, so Valheim never treated it as a UI screen: the cursor stayed locked to the camera, mouse movement kept turning the character behind the panel, and nothing in it could be clicked — including 0.2.0's new pause button. The panel now reports itself through TextInput.IsVisible, which is the flag GameCamera.UpdateMouseCapture consults to release the cursor, and the same one vanilla already uses to hold back movement, the hotbar, the map key, the ESC menu and chat. Escape still closes the report without also opening the game menu, because Menu.Update gates on that same flag.
  • The mouse wheel no longer zooms the camera behind the panel while you scroll the evidence. GameCamera's zoom check looks at chat, the console, the inventory and several other screens, but not at text prompts.
  • Leaving a world now closes the report, drops any pause and clears the history from ZNet.Shutdown, rather than a frame later from the plugin's own Update. A pause is a global flag, so letting a shutdown race it could leave the game frozen behind a panel that no longer exists.

0.2.0

  • Fixed a crash. The report threw NullReferenceException out of TextMeshPro on every layout pass whenever it showed a wrapped paragraph — including the "the server is not running this mod" notice, so it hit anyone on an unmodded server almost as soon as they opened it. The paragraphs were built by hand and never got a font: adding a TextMeshProUGUI to a live GameObject makes TMP look up Unity's default font, which Valheim does not ship. They now go through the same helper as every other label, which assigns the game's font while the object is still inactive. The corner readout built its label the same way and no longer does.
  • Pause button, top-right of the report, matching the ones on the large map and GrabMaterials' inventory panel: gray when off, Valheim orange while the game really is paused, red with a slash when the pause was asked for and refused. Off by default — a diagnostic should not change how the game runs the first time you open it.
  • Nothing is recorded while the game is paused. A paused world simulates nothing, so frames get cheap and the link goes quiet; recording those seconds would let the report talk itself round to "nothing wrong right now" while you sat reading it, which is the exact opposite of what pausing to read a diagnosis is for. Pausing now freezes the evidence, and the report says so.
  • The pause survives another mod dropping it. Vanilla keeps a single pause flag rather than a count, so any mod calling Game.Unpause() releases everyone's — the sibling mods only act on a transition and quietly believe they still hold a pause that is gone. This re-asserts when a pause it actually had stops being in effect, and stays silent when the request was simply refused.

0.1.1

  • Default key moved from F10 to F8, and the readout toggle from Shift+F10 to Shift+F8. F10 is AutomaticFuel's default and F9 cycles the controller layout, so 0.1.0 shipped into a conflict with a neighbor that was no escape either. F8 is free in vanilla and unused by every other mod in this repo.
  • A config still holding 0.1.0's F10 is moved to F8 automatically. BepInEx writes every default into the .cfg on first run, so without this the new default would have reached nobody who already had the mod. A key you chose yourself is never touched.

0.1.0

Early alpha, first release. Not yet tested against a real lag event on the DatHost server.

  • Measures both ends of the connection once a second and names which of six unrelated causes is behind the lag: a starved server, a saturated link, packet loss or jitter, object churn, the world still loading, or the local machine.
  • Server half reports its own tick times, stall count, world object count and per-player socket state over a routed RPC. That is the one measurement a client cannot make for itself, and without it "the server was fine" is unprovable.
  • A server without the mod never answers; clients say so plainly and fall back to diagnosing their own end rather than guessing at the half they cannot see.
  • Report on a key press (default F10): verdict, the evidence behind it, what is worth doing, then the live numbers for this machine, the server and every connected player.
  • Optional four-line corner readout (Shift+F10), off by default, with the worst measurement colored.
  • Thresholds with a physical meaning - packet loss, ping jitter, server tick time - are judged absolutely. Everything whose normal value depends on the world and the hardware is judged against this server's own recent median instead. Every threshold is configurable and documented.
  • Per-player detail (ping, quality, queued bytes, distance from the world center) goes to admins always and to everyone when the server's Share Peer Detail is on.
  • Console commands dsl, dsl_why, dsl_now, dsl_server, dsl_dump and dsl_reset. dsl_dump writes every measured second to a CSV.
  • Reads public game API only and patches nothing that affects play. ISocket.GetAndResetStats is deliberately never called: it would zero the counters Valheim keeps for its own bandwidth display.