ludic.net: a message's record, bytes and lists kept - queued ones back to a pool once sent or taken, the inbox's two drains on, a read text interned
np_queue made a NetMsg and a buffer per message, every flush a new keep list, and every message that came in a NetMessage and a buffer; nr_text a new string per text read. A party plays at dozens a second, so all of it is kept now: a pool of MTU-sized records for the queue (a peer's two lists swapped by the flush), the inbox's records in two halves swapped when a message finds the inbox empty, net_written's one record, a relay hello's and a STUN request's bytes in one scratch buffer, and the texts read out interned. Tests: 6000 messages with the heap flat (Os.heap_bytes), and an inbox record that holds still across a drain and comes back two drains on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e6729b3123
commit
ca69a3f9ae
11 changed files with 134 additions and 20 deletions
|
|
@ -27,6 +27,16 @@ The host is pid 1 and hands out 2 upwards (`net_accept`). Kind 1 is `NET_HELLO`,
|
|||
stranger may send a host; kind 0 is the heartbeat, never delivered. Until the game accepts a
|
||||
stranger, nothing else from it reaches the inbox.
|
||||
|
||||
## Nothing a message makes is new
|
||||
|
||||
A party sends and takes dozens of messages a second for as long as it plays, and Ludic frees
|
||||
nothing, so the package keeps what a message needs. A queued message's record and its bytes go back
|
||||
to a pool once it is sent (unreliable) or taken (reliable); a peer's queue is two lists swapped by a
|
||||
flush. An inbox message's record comes back two drains after it was handed out: read a drained list
|
||||
before the drain after next. `net_written()` hands out one kept record, good until the next call.
|
||||
`nr_s` / `nr_text` intern what they read (a name comes every frame), so equal texts are one string.
|
||||
The tests hold the heap flat over 6000 messages.
|
||||
|
||||
## Config and ports
|
||||
|
||||
```ludic
|
||||
|
|
|
|||
|
|
@ -72,9 +72,11 @@ export function nr_text(net_st: mut NetState, cap: int) -> string {
|
|||
net_st.net__rbad = true
|
||||
return ""
|
||||
}
|
||||
nb_copy(net_st.net__tb, 0, net_st.net__rb, net_st.net__rp, n)
|
||||
let sb = net_st.net__tsb
|
||||
sb_clear(sb)
|
||||
for i in 0 .. n { sb_byte(sb, net_st.net__rb[net_st.net__rp + i]) }
|
||||
net_st.net__rp += n
|
||||
return text_of(net_st.net__tb, n)
|
||||
return sb_intern(sb, net_st.net__ttb)
|
||||
}
|
||||
|
||||
export function nr_bad(net_st: NetState) -> bool { return net_st.net__rbad }
|
||||
|
|
@ -87,12 +89,16 @@ export function net_read(net_st: mut NetState, m: NetMessage) -> void {
|
|||
net_st.net__rbad = false
|
||||
}
|
||||
|
||||
# the message just written, as if it had come in (a loopback: a test, a host telling itself)
|
||||
export function net_written(net_st: NetState) -> NetMessage {
|
||||
let m = new NetMessage
|
||||
# the message just written, as if it had come in (a loopback: a test, a host telling itself); the
|
||||
# record is kept, so it is good until the next net_written
|
||||
export function net_written(net_st: mut NetState) -> NetMessage {
|
||||
if net_st.net__wr == null {
|
||||
net_st.net__wr = new NetMessage
|
||||
net_st.net__wr.data = buffer(NET_MTU)
|
||||
}
|
||||
let m = net_st.net__wr
|
||||
m.from = net_st.net__my_pid
|
||||
m.len = min(net_st.net__wl, NET_MTU)
|
||||
m.data = buffer(m.len)
|
||||
nb_copy(m.data, 0, net_st.net__wb, 0, m.len)
|
||||
return m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import "state.ludic"
|
|||
import "ports.ludic"
|
||||
import "codec.ludic"
|
||||
import "peers.ludic"
|
||||
import "pool.ludic"
|
||||
import "send.ludic"
|
||||
import "receive.ludic"
|
||||
import "session.ludic"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function net__open(net_st: mut NetState, slot: int, ip: int, port: int, pid: int
|
|||
p.recv_id = 0
|
||||
p.acked = 0
|
||||
p.ack_due = false
|
||||
p.queue = new []NetMsg
|
||||
net__queue_free(net_st, p)
|
||||
p.closing = false
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ export function np_close(net_st: mut NetState, slot: int) -> void {
|
|||
let p = net_st.net__peers[slot]
|
||||
p.on = false
|
||||
p.closing = false
|
||||
p.queue = new []NetMsg
|
||||
net__queue_free(net_st, p)
|
||||
}
|
||||
|
||||
# the written message, for one peer (a reliable one is kept until it is taken)
|
||||
|
|
@ -45,15 +45,15 @@ export function np_queue(net_st: mut NetState, slot: int, kind: int, rel: bool)
|
|||
if net_st.net__peers == null or slot < 0 or slot >= NET_PEERS { return }
|
||||
let p = net_st.net__peers[slot]
|
||||
if not p.on { return }
|
||||
let m = new NetMsg
|
||||
let m = net__msg_take(net_st)
|
||||
m.kind = kind
|
||||
m.rel = rel
|
||||
m.id = 0
|
||||
m.sent = -1.0
|
||||
m.made = net_st.net__time
|
||||
var n = net_st.net__wl
|
||||
if n > NET_MTU - 40 { n = NET_MTU - 40 }
|
||||
m.len = n
|
||||
m.data = buffer(n)
|
||||
nb_copy(m.data, 0, net_st.net__wb, 0, n)
|
||||
if rel {
|
||||
m.id = p.next_id
|
||||
|
|
|
|||
36
packages/ludic.net/pool.ludic
Normal file
36
packages/ludic.net/pool.ludic
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# pool.ludic - a message's record and its bytes are kept, never made again: a party sends and takes
|
||||
# dozens a second for as long as it plays, and Ludic frees nothing. A queued one goes back when it is
|
||||
# taken or goes stale; an inbox one comes back two drains after it was handed out
|
||||
function net__msg_take(net_st: mut NetState) -> NetMsg {
|
||||
if len(net_st.net__free) > 0 { return List.pop(net_st.net__free) }
|
||||
let m = new NetMsg
|
||||
m.data = buffer(NET_MTU)
|
||||
return m
|
||||
}
|
||||
|
||||
# everything a peer had queued, back to the pool (its slot opened again, or let go)
|
||||
function net__queue_free(net_st: mut NetState, p: NetPeer) -> void {
|
||||
for i in 0 .. len(p.queue) { push(net_st.net__free, p.queue[i]) }
|
||||
List.clear(p.queue)
|
||||
}
|
||||
|
||||
# the halves swap whenever a message finds the inbox empty (everything before it drained or
|
||||
# cleared), so a reader of the last drain's list never sees one change
|
||||
function net__in_take(net_st: mut NetState) -> NetMessage {
|
||||
if q_len(net_st.net__inbox) == 0 and net_st.net__inn > 0 {
|
||||
net_st.net__in_on_b = not net_st.net__in_on_b
|
||||
net_st.net__inn = 0
|
||||
}
|
||||
var pool = net_st.net__ina
|
||||
if net_st.net__in_on_b { pool = net_st.net__inb }
|
||||
if net_st.net__inn >= len(pool) {
|
||||
let m = new NetMessage
|
||||
m.data = buffer(NET_MTU)
|
||||
push(pool, m)
|
||||
}
|
||||
let m = pool[net_st.net__inn]
|
||||
net_st.net__inn += 1
|
||||
return m
|
||||
}
|
||||
|
||||
export function net_spare_count(net_st: NetState) -> int { return len(net_st.net__free) }
|
||||
|
|
@ -77,15 +77,14 @@ function net__messages(net_st: mut NetState, slot: int, n: int) -> void {
|
|||
}
|
||||
|
||||
# onto the inbox - except a heartbeat, and on a host anything but a hello before its welcome
|
||||
function net__deliver(net_st: NetState, slot: int, from: int, kind: int, o: int, ml: int) -> void {
|
||||
function net__deliver(net_st: mut NetState, slot: int, from: int, kind: int, o: int, ml: int) -> void {
|
||||
if kind == NET_BEAT { return }
|
||||
if net_st.net__state == NET_HOSTING and net_st.net__peers[slot].pid < 2 and kind != NET_HELLO { return }
|
||||
let m = new NetMessage
|
||||
let m = net__in_take(net_st)
|
||||
m.slot = slot
|
||||
m.from = from
|
||||
m.kind = kind
|
||||
m.len = ml
|
||||
m.data = buffer(ml)
|
||||
nb_copy(m.data, 0, net_st.net__in, o, ml)
|
||||
q_push(net_inbox(net_st), m)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function net__code_bytes(net_st: NetState, b: []byte, o: int) -> void {
|
|||
}
|
||||
|
||||
function net__relay_hello(net_st: NetState) -> void {
|
||||
let b = buffer(10)
|
||||
let b = net_st.net__sb
|
||||
nb_put8(b, 0, 'M')
|
||||
nb_put8(b, 1, 'L')
|
||||
nb_put8(b, 2, 'R')
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ export function np_flush(net_st: mut NetState, slot: int) -> void {
|
|||
function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
|
||||
var o = 20
|
||||
var count = 0
|
||||
let keep = new []NetMsg
|
||||
let keep = p.back
|
||||
List.clear(keep)
|
||||
for i in 0 .. len(p.queue) {
|
||||
let m = p.queue[i]
|
||||
if m.rel and m.id <= p.acked { continue } # taken: drop it
|
||||
if not m.rel and net_st.net__time - m.made > 0.5 { continue } # stale
|
||||
let taken = m.rel and m.id <= p.acked
|
||||
let stale = not m.rel and net_st.net__time - m.made > 0.5
|
||||
if taken or stale {
|
||||
push(net_st.net__free, m) # back to the pool
|
||||
continue
|
||||
}
|
||||
var due = true
|
||||
if m.rel and not (m.sent < 0.0) and net_st.net__time - m.sent < 0.25 { due = false }
|
||||
var size = 4 + m.len
|
||||
|
|
@ -28,11 +33,12 @@ function net__flush_one(net_st: mut NetState, p: NetPeer) -> bool {
|
|||
count += 1
|
||||
m.sent = net_st.net__time
|
||||
if net_st.net__kind_out != null and m.kind < 64 { net_st.net__kind_out[m.kind] += size }
|
||||
if m.rel { push(keep, m) } # kept until acknowledged
|
||||
if m.rel { push(keep, m) } else { push(net_st.net__free, m) } # a reliable one kept until acknowledged
|
||||
} else {
|
||||
push(keep, m) # not due, or waits for the next packet
|
||||
}
|
||||
}
|
||||
p.back = p.queue
|
||||
p.queue = keep
|
||||
if count == 0 and not p.ack_due { return false }
|
||||
nb_put8(net_st.net__out, 0, 'M')
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ property NetPeer {
|
|||
acked: int = 0, # the highest id it has taken from us
|
||||
ack_due: bool = false,
|
||||
queue: []NetMsg,
|
||||
back: []NetMsg, # the queue's other list, filled by a flush and swapped in
|
||||
closing: bool = false, # let go once it has taken close_id
|
||||
close_id: int = 0,
|
||||
close_t: float = 0.0 # ... or at this net time, whichever comes first
|
||||
|
|
@ -51,6 +52,15 @@ export state NetState {
|
|||
net__cfg: NetConfig = new NetConfig
|
||||
net__facts: Queue<NetFact> = net__facts__new()
|
||||
net__seen: []string = new []string # guests already punched for
|
||||
net__free: []NetMsg = new []NetMsg # sent and taken (or stale): made again, never new (pool.ludic)
|
||||
net__ina: []NetMessage = new []NetMessage # the inbox's records, two halves
|
||||
net__inb: []NetMessage = new []NetMessage
|
||||
net__inn: int = 0
|
||||
net__in_on_b: bool = false
|
||||
net__sb: []byte = buffer(32) # a relay hello's or a STUN request's bytes
|
||||
net__wr: NetMessage = null # net_written's, kept
|
||||
net__tsb: StrBuf = sb_new(NET_MTU) # a text read out of a message, interned (a name comes every frame)
|
||||
net__ttb: StrTable = strs_new(4096)
|
||||
net__punch_ip: words = null
|
||||
net__punch_port: words = null
|
||||
net__punch_left: words = null
|
||||
|
|
@ -88,7 +98,6 @@ export state NetState {
|
|||
net__rl: int = 0
|
||||
net__rp: int = 0
|
||||
net__rbad: bool = false
|
||||
net__tb: []byte = buffer(NET_MTU) # text read out of a message, through one scratch buffer
|
||||
net__hello_b: []byte = buffer(NET_MTU) # the game's hello, sent again on every dial
|
||||
net__hello_l: int = 0
|
||||
net__hello_t: float = 0.0 # seconds since it went; -1 not yet
|
||||
|
|
@ -119,6 +128,7 @@ function net__peers_new() -> []NetPeer {
|
|||
for i in 0 .. NET_PEERS {
|
||||
let p = new NetPeer
|
||||
p.queue = new []NetMsg
|
||||
p.back = new []NetMsg
|
||||
push(out, p)
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function net__stun_send(net_st: mut NetState) -> void {
|
|||
}
|
||||
if net_st.net__stun_ip == 0 { return }
|
||||
}
|
||||
let b = buffer(20)
|
||||
let b = net_st.net__sb
|
||||
nb_put8(b, 0, 0) # a binding request, no attributes
|
||||
nb_put8(b, 1, 1)
|
||||
nb_put8(b, 2, 0)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ program NetTest {
|
|||
inbound: []Pkt = null
|
||||
from_ip: int = 0
|
||||
from_port: int = 0
|
||||
quiet: bool = false # sends only counted, not kept: a test of what the package holds
|
||||
sends: int = 0
|
||||
}
|
||||
|
||||
function fake_open(port: int) -> int { return 5 }
|
||||
|
|
@ -19,6 +21,8 @@ program NetTest {
|
|||
function fake_from_ip(net_test_st: NetTestState, h: int) -> int { return net_test_st.from_ip }
|
||||
function fake_from_port(net_test_st: NetTestState, h: int) -> int { return net_test_st.from_port }
|
||||
function fake_send(net_test_st: mut NetTestState, h: int, ip: int, port: int, b: []byte, n: int) -> int {
|
||||
net_test_st.sends += 1
|
||||
if net_test_st.quiet { return n }
|
||||
let p = new Pkt
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
|
|
@ -274,4 +278,46 @@ program NetTest {
|
|||
expect(not net_room_join(net_st, "ABCDEF"))
|
||||
expect_eq(net_why(net_st), NET_WHY_NO_MATCHMAKER)
|
||||
}
|
||||
test "sending holds its bytes flat: a message's record and bytes come back once it is sent (6 s of a guest, 6000 messages)" (net_st: mut NetState, net_test_st: mut NetTestState) {
|
||||
let slot = np_of_pid(net_st, host_with_guest(net_st, net_test_st))
|
||||
net_test_st.quiet = true
|
||||
for r in 0 .. 3000 {
|
||||
if r == 1000 { net_test_st.from_ip = Os.heap_bytes() }
|
||||
nw_begin(net_st)
|
||||
nw_i(net_st, r)
|
||||
np_queue(net_st, slot, 9, false)
|
||||
nw_begin(net_st)
|
||||
nw_i(net_st, r + 1)
|
||||
np_queue(net_st, slot, 9, false)
|
||||
frame(net_st, 0.002)
|
||||
}
|
||||
let after = Os.heap_bytes()
|
||||
expect(net_test_st.sends >= 3000)
|
||||
if Os.heap_bytes() > 0 { expect(after <= net_test_st.from_ip) }
|
||||
expect(net_spare_count(net_st) >= 1)
|
||||
}
|
||||
|
||||
test "an inbox message's record is kept: a drained list holds still while more come, and comes back two drains on" (net_st: mut NetState, net_test_st: mut NetTestState) {
|
||||
host_with_guest(net_st, net_test_st)
|
||||
q_clear(net_inbox(net_st))
|
||||
arrive(net_test_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 111)
|
||||
net_begin(net_st, 0.01)
|
||||
let first = q_drain(net_inbox(net_st))
|
||||
expect_eq(len(first), 1)
|
||||
let r0: pointer = first[0]
|
||||
arrive(net_test_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 222)
|
||||
net_begin(net_st, 0.01)
|
||||
net_read(net_st, first[0])
|
||||
expect_eq(nr_i(net_st), 111)
|
||||
let second = q_drain(net_inbox(net_st))
|
||||
net_read(net_st, second[0])
|
||||
expect_eq(nr_i(net_st), 222)
|
||||
arrive(net_test_st, Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 4, 333)
|
||||
net_begin(net_st, 0.01)
|
||||
let third = q_drain(net_inbox(net_st))
|
||||
let r2: pointer = third[0]
|
||||
expect(r2 == r0)
|
||||
net_read(net_st, third[0])
|
||||
expect_eq(nr_i(net_st), 333)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue