fix(ludic.telemetry): nothing made per event - properties and the line written into kept buffers, the queue a ring of bytes made at the start

An event was a tree of values encoded into a new string, a dropped line was never given back, and
every batch and save joined the queue into another. Now: telemetry_props / telemetry_str / _int /
_bool write the properties as JSON into a buffer the state keeps; the line is written beside it,
its time worked out from the clock's seconds (TelemetryWorld.clock_s, was stamp); its bytes go
into one ring (ring_bytes, at most queue_max lines), the oldest overwritten past either; a batch
and the file are written into a third kept buffer and go as bytes (TelemetryTransport.send takes
the bytes and their length; Http.body_bytes, Fs.write_bytes). The facts are pooled.
tests/ring_test: ten thousand events past the cap in lines and in bytes, 0 bytes of heap; the line
exact JSON, escaped, 2000-02-29 right; the batch puts the id in; the file round-trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:15:12 +03:00
parent fc0f3d3790
commit d1bea7f10e
14 changed files with 454 additions and 97 deletions

View file

@ -12,7 +12,8 @@ export property TelemetryConfig {
flush_ms: int = 30000 # a batch this often
flush_at: int = 50 # or sooner, at this many events
batch: int = 200 # events in one request
queue_max: int = 5000 # the oldest go past this
queue_max: int = 5000 # the oldest go past this, or past ring_bytes of lines
ring_bytes: int = 1048576 # the lines' ring (ring.ludic), made once at the start
backoff_max: int = 6 # the wait doubles at most this many times
save_ms: int = 5000 # the queue on disk at most this far behind
}
@ -22,13 +23,13 @@ export port TelemetryWorld {
can_send: fn() -> bool = fn telemetry__yes # may this run send at all (a test, a dev build: no)
enabled: fn() -> bool = fn telemetry__yes # the player's switch; off forgets everything queued
now_ms: fn() -> int = fn telemetry__wall_ms # a clock in milliseconds
stamp: fn() -> string = fn telemetry__wall_stamp # the ISO time an event happened
clock_s: fn() -> int = fn telemetry__wall_s # the wall clock in seconds since 1970, for an event's time
}
# how a batch travels (unbound: Http). send returns a handle (< 0: could not start); poll
# answers -1 while pending, 0 on failure, 1 when the server took it; drop abandons one.
export port TelemetryTransport {
send: fn(string, string) -> int = fn telemetry__http_send
send: fn(string, []byte, int) -> int = fn telemetry__http_send # (url, body, its length)
poll: fn(int) -> int = fn telemetry__http_poll
drop: fn(int) -> void = fn telemetry__http_drop
}
@ -56,8 +57,21 @@ export state TelemetryState {
telemetry__ready: bool = false
telemetry__id: string = ""
telemetry__session: string = ""
telemetry__q: []string = null # the queued lines, oldest first
telemetry__dirty: bool = false # telemetry__q differs from the file
telemetry__ring: []byte = null # the queued lines (ring.ludic), oldest first from head
telemetry__off: []int = null
telemetry__len: []int = null
telemetry__mk: []int = null # where each line's player mark is, -1 none
telemetry__head: int = 0
telemetry__n: int = 0
telemetry__wr: int = 0
telemetry__line: StrBuf = sb_new(TELEMETRY_LINE) # an event's line, written here
telemetry__props: TelemetryProps = telemetry__props_new()
telemetry__out: StrBuf = null # a batch or the file, written here
telemetry__url: string = "" # host and path, joined once at the config
telemetry__fpool: []TelemetryFact = new []TelemetryFact # fact records, reused once no drain holds one
telemetry__ffree: []TelemetryFact = new []TelemetryFact
telemetry__fnext: []int = words(1)
telemetry__dirty: bool = false # the ring differs from the file
telemetry__h: int = -1 # the batch in flight
telemetry__sent_n: int = 0 # how many lines it carries
telemetry__next_ms: int = 0 # when the next send may go
@ -71,8 +85,8 @@ export function telemetry_facts(telemetry_st: TelemetryState) -> Queue<Telemetry
return telemetry_st.telemetry__facts
}
function telemetry__fact(telemetry_st: TelemetryState, what: int, count: int, status: int, retry: int) -> void {
let f = new TelemetryFact
function telemetry__fact(telemetry_st: mut TelemetryState, what: int, count: int, status: int, retry: int) -> void {
let f = telemetry__fact_record(telemetry_st)
f.what = what
f.count = count
f.status = status
@ -80,3 +94,8 @@ function telemetry__fact(telemetry_st: TelemetryState, what: int, count: int, st
q_push(telemetry_facts(telemetry_st), f)
}
function telemetry__facts__new() -> Queue<TelemetryFact> { return queue_new("telemetry.facts") }
function telemetry__props_new() -> TelemetryProps {
let p = new TelemetryProps
p.sb = sb_new(TELEMETRY_LINE)
return p
}