hafen.client: The Counters
memory(), net(), loader(), render(), surfaces(), entities(), session() and textcache() are pull-only. They read counters the client keeps anyway, so they answer with profiling off and cost nothing while you are not asking. The first four are the numbers the client’s own stats HUD formats, field by field.
local render = hafen.client():profiling():render()
if render.drawSlots then
local megabytes = math.floor(render.vram.textures.bytes / 1048576 * 10 + 0.5) / 10 -- one decimal by hand: %.1f prints the raw double
hafen.log():write(string.format("%d slots, %d batches, %s MB textures", render.drawSlots, render.batches, megabytes))
end
| Rule |
Detail |
| The value right now |
Nothing here is sampled over time, except the running tallies, each marked cumulative below. Those are surfaces()’s uploads and frames, entities()’s passes, and session()’s ground and click counters since the client started. textcache()’s hits, misses and evictions since the addon loaded. A cumulative counter means something as a delta between two reads. |
| An absent key means “not measured”, never zero |
Everywhere on this surface. |
| Read-only in the strict sense |
Each group takes no argument, and passing one raises: profiling:net(1) is a refusal. The one argument on the surface is profiling:history(n), a whole number. A negative one raises, and more frames than the ring holds answers all of them. |
memory()
Sizes are in bytes, the one place the surface is not in milliseconds. hafen.client():memory() answers this same table.
| Key |
Description |
heapUsed / heapFree / heapTotal / heapMax |
The JVM heap, as the runtime reports it. |
allocPerFrame |
The client’s smoothed per-frame allocation estimate, the figure behind the HUD’s memory line. Advanced only while that HUD is drawn, so absent until computed at least once. |
gcCount / gcMs |
Collections and time spent collecting, cumulative since client start: a delta between two reads. |
net()
Empty while there is no connection (the login screen). rtt and rttVar are milliseconds.
| Key |
Description |
packetsTx / packetsRx / bytesTx / bytesRx |
The traffic counters, cumulative for the session. |
resentTx / resentRx |
Packets re-sent or received twice. |
reorderedRx |
Packets that arrived out of order. |
rtt / rttVar |
Smoothed round-trip time and its deviation. |
| Rule |
Detail |
| The character on screen’s |
As loader() and render() are: a connection is per login, so a handler for a background character reads the drawn character’s traffic. Hand that character the screen with hafen.session():current(session) to read its own. |
| One packet behind |
Written on the connection worker. Exactness would lock a path nothing needs exact. |
loader()
| Key |
Description |
queued / loading / busy / poolSize |
The UI resource loader. Taken under one lock, so mutually consistent. |
defer |
The shared background pool: {queued=, busy=, poolSize=}. |
resQueue / resLoaded |
Resource fetch queue depth, and resources resolved so far. |
render()
The 3D scene: everything but stateSlots, gobsHeld and the two overlay counters is absent before the world is up. Written on the render side, so one frame stale.
| Key |
Description |
drawSlots |
Draw slots this frame, the render tree’s nearest thing to draw calls. |
uniqueInstances / batches / instances |
The batching split: un-instanced slots, instanced batches, instances in them. |
invalid / bypass |
Slots pending revalidation, and slots that cannot be instanced. |
culled / cullable |
Slots frustum culling leaves out of the draw this frame because the camera cannot see them, and how many it could test at all; 0 of both with the Performance page’s Frustum culling off. |
treeLeaves / treeNodes |
Scene-tree size. |
programs |
Shader programs the GL environment holds. Absent on any other backend. |
vram |
Per-pool VRAM, keyed indices/vertices/textures/vaos/fbos, each {objects=, bytes=}. Absent without a GL environment. |
stateSlots |
Render-state slots in use, process-wide. |
gobsHeld |
Game objects kept out of the scene until their GobAdded fired, cumulative since client start. |
overlayMeshes / overlayOutlines |
Ground-overlay pieces laid over the terrain, and the outlines over those, cumulative since client start. |
recallGridsHeld |
Grids of remembered ground held right now. |
recallGridsRead |
Grids of it read back off the record, cumulative since the world came up. |
recallCutsDrawn / recallCutsWanted |
Pieces of remembered ground in the scene right now, and how many the client wanted there. |
| Rule |
Detail |
gobsHeld |
The addon layer’s tally, not the scene’s, so it answers on the login screen. Climbs each time the client holds an object back so GobAdded runs before its first drawn frame. Zero while no addon subscribes. Read as a delta: take one, walk into ground unseen this session, take another. |
overlayMeshes, overlayOutlines |
Terrain work, counted outside the scene. The client cuts the ground into squares. Every ground overlay (a patch you lay, a claim, a province) is laid over each cut its shape reaches. It is laid once as the sheet, and again as the outline where it has one. |
| What a patch moves |
Laying one moves overlayMeshes by the cuts its pieces reach and leaves overlayOutlines alone, since a patch’s edge is carved into the sheet. Wearing, widening or taking one up moves neither. Read as a delta: lay one with fifty on the ground and it moves by what one costs. |
The recall keys |
Three gauges: held, drawn, wanted at this instant. One cumulative: recallGridsRead, climbing while the record is read back until it catches up with the camera. Cuts drawn reaching cuts wanted is the feature’s claim as a number. Absent until the world is up. Then 0 is a count: with the setting off or another camera, drawn and wanted fall to zero. Grids held falls only when the client’s budget drops a grid, since ground read back is kept for a camera that returns. |
surfaces()
The widgets standing in the 3D world, every addon’s, and what drawing them costs.
| Key |
Description |
live |
Surfaces that exist right now, across every addon. |
culled |
How many of those are skipped this instant, because nothing is looking at them. |
uploads |
Offscreen passes issued, cumulative since client start. |
frames |
Frames those passes were offered, cumulative since client start. |
| Rule |
Detail |
| Culled is not gone |
A surface is culled when the camera points elsewhere, the entity is hidden, or its game object left the scene. The collection still holds it, its Update fires, and it draws again when looked at. |
| The cost claim |
uploads and frames as a delta: a panel nothing changes holds uploads still while frames climbs. A panel painted by a widget:on("Draw", …) handler moves them together, since a Lua function of anything is known only by running it. |
| The leak check |
live returning to zero on :reload or disabling every addon: each standing entity ends through the body :remove uses, freeing the texture. |
local before = hafen.client():profiling():surfaces()
hafen.timer():after(2, function()
local after = hafen.client():profiling():surfaces()
hafen.log():write(string.format("%d standing (%d culled), %d uploads over %d frames",
after.live, after.culled, after.uploads - before.uploads, after.frames - before.frames))
end)
entities()
The client-only things standing at a point in the world (a ghost, a sprite, a model, a panel) and what keeping them there costs. One standing on a game object is not counted: its place is that object’s.
| Key |
Description |
placed |
How many stand at a point right now, across every addon. |
waiting |
How many of those hold a place this session cannot locate. |
passes |
Times the client has worked out where they all are, cumulative since client start. |
| Rule |
Detail |
| Waiting |
The place is real but has no coordinate here. Ground recorded in another part of the world. Every place above while underground. Every place off the streamed ground for the moment after a re-base, until this session’s base is proved. It answers every verb, reports the place it was given, and stands up when that ground resolves. |
passes |
Climbs by a few while you walk and holds still while you stand. Where those things are is worked out when the world moves under them, and at no other time. |
session()
How many accounts the client holds logged in, and what it answered for them. The client keeps several sessions open (:session add) and draws one. The rest tick and answer the server with no view of their own, their ground merged into the scene you see.
| Key |
Description |
live |
Sessions the client holds right now, the one on screen included: 1 with one account in the world, 0 on the login screen. |
states |
How many of those hold client-side state of their own, which is every one of them. |
groundAnswered |
Times another session supplied the ground height under the camera, cumulative since client start. |
groundMissed |
Times none of them had that ground, so the camera kept its last height, cumulative since client start. |
placedRebuiltOffTick |
Times a click was resolved before the client had said where the sessions stand, cumulative since client start. |
addonsLive |
How many addons are running right now. |
engineReloads |
Times the client rebuilt the addon layer without being asked. |
| Rule |
Detail |
states equals live |
The client keeps a set of caches per session. They hold the widgets your selectors matched, the objects seen, and the slots your addon holds on that character’s bar. They are made when first needed and dropped when the session ends. Above live is a gone session still remembered. Below it is one joined and not yet needing anything, true between logging in and the world coming up. |
| The ground pair |
Climbs only while a free camera looks at ground the drawn session never loaded (another character’s surroundings). 0 means the query has not run. With live == 1 only groundMissed can climb. Read as a delta: pan the camera between reads. |
addonsLive, engineReloads |
The addon layer’s pair. addonsLive does not move when you switch character: your addon is loaded once for the client. engineReloads counts rebuilds nobody asked for (a :reload you typed does not count), and its only meant value is zero. Anything above is an addon torn down and reloaded behind your back, every Lua value it held gone with no event. |
placedRebuiltOffTick |
Only meant to hold zero. Where each session stands is worked out once a frame. A ground click is resolved off the frame’s thread a frame or so after the button went down. This counts clicks that got there first. Each is a click on another session’s merged ground answered in the drawn session’s coordinates. That puts the destination as far off as the two characters are apart. |
local before = hafen.client():profiling():session()
hafen.timer():after(5, function()
local after = hafen.client():profiling():session()
hafen.log():write(string.format("%d sessions (%d holding state), %d addons",
after.live, after.states, after.addonsLive))
hafen.log():write(string.format("%d answers, %d misses over 5 s; %d early, %d unasked reloads",
after.groundAnswered - before.groundAnswered, after.groundMissed - before.groundMissed,
after.placedRebuiltOffTick, after.engineReloads))
end)
textcache()
The rendered-text cache graphics:text and graphics:atext draw through. Per addon: the top level is your own, total sums every Lua owner.
| Key |
Description |
entries / bytes |
Cached strings held right now, and the GL texture bytes they occupy. |
hits / misses / evictions |
Lookups served from the cache, rasterised, or dropped to stay within the caps. Cumulative since the addon loaded. |
hitRate |
hits / (hits + misses), 0.0..1.0. Absent until something has been looked up. |
maxEntries / maxBytes |
The two caps the cache is bounded by. |
maxEntryBytes |
The size past which one raster is drawn and dropped rather than kept (text is cached across frames). |
total |
The same figures summed over every Lua owner, plus owners, how many were summed. |
| Rule |
Detail |
| Cumulative since the addon loaded |
A :reload builds a fresh cache and count. reset() does not touch them. entries and bytes are live state. |
| A miss is not a fault |
A string never drawn in that font, costing what every text draw cost before the cache. A line whose text changes every frame misses every frame (the budgeting rule). A full, evicting cache with a high hitRate is the volatile strings ageing out and the static ones reused. |
| The leak check |
Disable every addon, or :reload, and total.bytes goes to nearly zero: teardown drops each cache and disposes its textures. |
local cache = hafen.client():profiling():textcache()
local mebibytes = math.floor(cache.bytes / 1048576 * 100 + 0.5) / 100
local hit_rate = math.floor((cache.hitRate or 0) * 1000 + 0.5) / 10
hafen.log():write(string.format("%d entries / %s MiB, %s%% hit rate (%d evictions)",
cache.entries, mebibytes, hit_rate, cache.evictions))
See Also