diff --git a/packages/ludic.net/README.md b/packages/ludic.net/README.md index ae11a85e..fda23719 100644 --- a/packages/ludic.net/README.md +++ b/packages/ludic.net/README.md @@ -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 diff --git a/packages/ludic.net/codec.ludic b/packages/ludic.net/codec.ludic index 6be5b9fc..241029d1 100644 --- a/packages/ludic.net/codec.ludic +++ b/packages/ludic.net/codec.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 } diff --git a/packages/ludic.net/index.ludic b/packages/ludic.net/index.ludic index 419fb1b0..38d40f4a 100644 --- a/packages/ludic.net/index.ludic +++ b/packages/ludic.net/index.ludic @@ -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" diff --git a/packages/ludic.net/peers.ludic b/packages/ludic.net/peers.ludic index 96d61833..440304d7 100644 --- a/packages/ludic.net/peers.ludic +++ b/packages/ludic.net/peers.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 diff --git a/packages/ludic.net/pool.ludic b/packages/ludic.net/pool.ludic new file mode 100644 index 00000000..3b30b7cf --- /dev/null +++ b/packages/ludic.net/pool.ludic @@ -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) } diff --git a/packages/ludic.net/receive.ludic b/packages/ludic.net/receive.ludic index ce140469..f3d091cc 100644 --- a/packages/ludic.net/receive.ludic +++ b/packages/ludic.net/receive.ludic @@ -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) } diff --git a/packages/ludic.net/relay.ludic b/packages/ludic.net/relay.ludic index f05f7401..6fdb4a78 100644 --- a/packages/ludic.net/relay.ludic +++ b/packages/ludic.net/relay.ludic @@ -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') diff --git a/packages/ludic.net/send.ludic b/packages/ludic.net/send.ludic index 0be5d23d..4a414d8c 100644 --- a/packages/ludic.net/send.ludic +++ b/packages/ludic.net/send.ludic @@ -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') diff --git a/packages/ludic.net/state.ludic b/packages/ludic.net/state.ludic index 4a0cdeff..12fd4195 100644 --- a/packages/ludic.net/state.ludic +++ b/packages/ludic.net/state.ludic @@ -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 = 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 diff --git a/packages/ludic.net/stun.ludic b/packages/ludic.net/stun.ludic index df61c7fd..d0889f3a 100644 --- a/packages/ludic.net/stun.ludic +++ b/packages/ludic.net/stun.ludic @@ -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) diff --git a/packages/ludic.net/tests/net_test.ludic b/packages/ludic.net/tests/net_test.ludic index 592d7b38..4e2e345d 100644 --- a/packages/ludic.net/tests/net_test.ludic +++ b/packages/ludic.net/tests/net_test.ludic @@ -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) + } }