NetStats.kt
src/jvmMain/kotlin/afield/app/server/NetStats.kt · 108 lines
package afield.app.server
import afield.app.protocol.send
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.loop
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.runtime.lifecycle.then
import afield.ui.output.stats
import afield.audio.synth.*
import afield.data.table.get
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.tools.docs.format
import afield.app.diagnostics.logging.lprintln
import java.util.Locale
import java.util.concurrent.atomic.AtomicLong
import kotlin.concurrent.thread
Per-second counters of WebSocket traffic between the backend and the browser, shown on the pinned "net" console row.
object NetStats
Time-windowed WebSocket traffic stats for the front-end /ws channel, surfaced on the pinned "net"
console row: ↑ = backend → front end, ↓ = front end → backend. Counters are bumped at the send
choke point (sendWithOutputBackPressure) and the inbound frame loop; a 1s reporter emits per-second
packet + KB rates — and only when there's traffic, so the pinned row dims then expires when the front
end goes idle (see PinnedConsole's lifecycle).
object NetStats {
private val outFrames = AtomicLong()
private val outBytes = AtomicLong()
private val inFrames = AtomicLong()
private val inBytes = AtomicLong()
fun sent(bytes: Int)
backend → front end: one WebSocket text frame of bytes.
fun sent(bytes: Int) {
outFrames.incrementAndGet()
outBytes.addAndGet(bytes.toLong())
}
fun received(bytes: Int)
front end → backend: one WebSocket text frame of bytes.
fun received(bytes: Int) {
inFrames.incrementAndGet()
inBytes.addAndGet(bytes.toLong())
}
@Volatile private var started = false
fun start()
Start the 1s reporter (idempotent).
body — 48 lines, from line 60
fun start() {
synchronized(this) {
if (started) return
started = true
}
thread(isDaemon = true, name = "afield-net-stats") {
var pOf = 0L
var pOb = 0L
var pIf = 0L
var pIb = 0L
var last = System.nanoTime()
while (true) {
try {
Thread.sleep(WINDOW_MS)
} catch (_: InterruptedException) {
break
}
val now = System.nanoTime()
val dt = (now - last) / 1e9
last = now
if (dt <= 0.0) continue
val of = outFrames.get()
val ob = outBytes.get()
val nf = inFrames.get()
val nb = inBytes.get()
val dOf = of - pOf
val dOb = ob - pOb
val dIf = nf - pIf
val dIb = nb - pIb
pOf = of; pOb = ob; pIf = nf; pIb = nb
if (dOf == 0L && dIf == 0L) continue // idle: let the pinned "net" row dim then expire
lprintln(
"info,net",
String.format(
Locale.US,
"↑ %.0f pkt/s %.1f KB/s ↓ %.0f pkt/s %.1f KB/s",
dOf / dt, dOb / dt / 1024.0, dIf / dt, dIb / dt / 1024.0,
),
)
}
}
}
private const val WINDOW_MS = 1000L
}