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:
parent
fc0f3d3790
commit
d1bea7f10e
14 changed files with 454 additions and 97 deletions
93
packages/ludic.telemetry/tests/ring_test.ludic
Normal file
93
packages/ludic.telemetry/tests/ring_test.ludic
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# ring_test.ludic - an event written once into a kept line and the queue a ring of bytes: ten thousand
|
||||
# events, most past the cap in lines and in bytes, make nothing; the line is exact JSON with its time
|
||||
# worked out from the seconds; the batch puts the id in; the file round-trips with its marks
|
||||
import "ludic.telemetry"
|
||||
import "ludic.base"
|
||||
program RingTest {
|
||||
numbers float
|
||||
state RingTestState {
|
||||
now: int = 1790337600
|
||||
body: string = ""
|
||||
}
|
||||
function fk_now() -> int { return 0 }
|
||||
function fk_on() -> bool { return true }
|
||||
function fk_clock(ring_test_st: RingTestState) -> int { return ring_test_st.now }
|
||||
function fk_send(url: string, body: []byte, n: int) -> int { return -1 }
|
||||
function fk_poll(h: int) -> int { return -1 }
|
||||
function fk_drop(h: int) -> void { }
|
||||
bind TelemetryWorld { can_send: fn fk_on, enabled: fn fk_on, now_ms: fn fk_now, clock_s: fn fk_clock }
|
||||
bind TelemetryTransport { send: fn fk_send, poll: fn fk_poll, drop: fn fk_drop }
|
||||
|
||||
function qfile() -> string { return Os.temp_dir() + "/ludic-telemetry-ring-queue.txt" }
|
||||
function start(telemetry_st: mut TelemetryState, max: int, bytes: int) -> void {
|
||||
telemetry_reset(telemetry_st)
|
||||
if Fs.exists(qfile()) { Fs.remove(qfile()) }
|
||||
Fs.write_text(Os.temp_dir() + "/ludic-telemetry-ring-id.json", "{\"id\":\"p-9\"}")
|
||||
let c = new TelemetryConfig
|
||||
c.host = "http://fake"
|
||||
c.key = "k-9"
|
||||
c.lib = "ring"
|
||||
c.queue_file = qfile()
|
||||
c.id_file = Os.temp_dir() + "/ludic-telemetry-ring-id.json"
|
||||
c.queue_max = max
|
||||
c.ring_bytes = bytes
|
||||
c.batch = 3
|
||||
telemetry_config(telemetry_st, c)
|
||||
telemetry_start(telemetry_st)
|
||||
}
|
||||
function one(telemetry_st: mut TelemetryState, k: int) -> void {
|
||||
let p = telemetry_props(telemetry_st)
|
||||
telemetry_int(p, "n", k)
|
||||
telemetry_str(p, "what", "a fish")
|
||||
telemetry_bool(p, "dev", true)
|
||||
telemetry_event(telemetry_st, "caught", p)
|
||||
}
|
||||
|
||||
test "ten thousand events, past the cap in lines and in bytes, make nothing" (telemetry_st: mut TelemetryState) {
|
||||
start(telemetry_st, 5000, 262144)
|
||||
for k in 0 .. 200 { one(telemetry_st, k) }
|
||||
let heap = Os.heap_bytes()
|
||||
for k in 0 .. 10000 { one(telemetry_st, k) }
|
||||
let grew = Os.heap_bytes() - heap
|
||||
print(`ring: 10000 events, {telemetry_queued(telemetry_st)} held, heap {grew} bytes`)
|
||||
expect(grew == long(0))
|
||||
expect(Text.contains(telemetry_line(telemetry_st, telemetry_queued(telemetry_st) - 1), "\"n\":9999"))
|
||||
start(telemetry_st, 40, 1048576)
|
||||
let heap2 = Os.heap_bytes()
|
||||
for k in 0 .. 10000 { one(telemetry_st, k) }
|
||||
expect(Os.heap_bytes() - heap2 == long(0))
|
||||
expect_eq(telemetry_queued(telemetry_st), 40)
|
||||
expect(Text.contains(telemetry_line(telemetry_st, 0), "\"n\":9960"))
|
||||
}
|
||||
|
||||
test "a line is exact JSON, escaped, its time from the seconds" (telemetry_st: mut TelemetryState, ring_test_st: mut RingTestState) {
|
||||
start(telemetry_st, 50, 65536)
|
||||
let p = telemetry_props(telemetry_st)
|
||||
telemetry_str(p, "say", "a \"quote\"\\ and\na line")
|
||||
telemetry_event(telemetry_st, "said", p)
|
||||
ring_test_st.now = 951868799
|
||||
telemetry_event(telemetry_st, "leap", null)
|
||||
let a = telemetry_line(telemetry_st, 0)
|
||||
expect(Text.contains(a, "\"properties\":{\"say\":\"a \\\"quote\\\"\\\\ and\\u000aa line\",\"$session_id\":"))
|
||||
expect(Text.contains(a, "\"timestamp\":\"2026-09-25T12:00:00Z\"}"))
|
||||
expect(Json.parse(a) != null)
|
||||
expect(Text.contains(telemetry_line(telemetry_st, 1), "\"timestamp\":\"2000-02-29T23:59:59Z\""))
|
||||
expect(Text.contains(telemetry_line(telemetry_st, 1), "\"properties\":{\"$session_id\""))
|
||||
}
|
||||
|
||||
test "the batch puts the id in, and the file round-trips with its marks" (telemetry_st: mut TelemetryState) {
|
||||
start(telemetry_st, 50, 65536)
|
||||
for k in 0 .. 4 { one(telemetry_st, k) }
|
||||
let body = telemetry_batch_body(telemetry_st, 3)
|
||||
expect(Text.starts_with(body, "{\"api_key\":\"k-9\",\"batch\":[{"))
|
||||
expect(Text.contains(body, "\"distinct_id\":\"p-9\""))
|
||||
expect(not Text.contains(body, "\"n\":3"))
|
||||
expect(Json.parse(body) != null)
|
||||
telemetry_save(telemetry_st)
|
||||
telemetry_reset(telemetry_st)
|
||||
telemetry_start(telemetry_st)
|
||||
expect_eq(telemetry_queued(telemetry_st), 4)
|
||||
expect(Text.contains(telemetry_batch_body(telemetry_st, 4), "\"n\":3,"))
|
||||
expect(not Text.contains(telemetry_batch_body(telemetry_st, 4), "TELEMETRY_PLAYER"))
|
||||
}
|
||||
}
|
||||
|
|
@ -18,18 +18,18 @@ program TelemetryTest {
|
|||
function fake_now(telemetry_test_st: TelemetryTestState) -> int { return telemetry_test_st.clock }
|
||||
function fake_enabled(telemetry_test_st: TelemetryTestState) -> bool { return telemetry_test_st.on }
|
||||
function fake_can(telemetry_test_st: TelemetryTestState) -> bool { return telemetry_test_st.allowed }
|
||||
function fake_stamp() -> string { return "2026-09-25T12:00:00Z" }
|
||||
function fake_send(telemetry_test_st: mut TelemetryTestState, url: string, body: string) -> int {
|
||||
function fake_clock() -> int { return 1790337600 }
|
||||
function fake_send(telemetry_test_st: mut TelemetryTestState, url: string, body: []byte, n: int) -> int {
|
||||
if telemetry_test_st.refuse { return -1 }
|
||||
telemetry_test_st.sends += 1
|
||||
telemetry_test_st.last_url = url
|
||||
telemetry_test_st.last_body = body
|
||||
telemetry_test_st.last_body = text_of(body, n)
|
||||
return 7
|
||||
}
|
||||
function fake_poll(telemetry_test_st: TelemetryTestState, h: int) -> int { return telemetry_test_st.answer }
|
||||
function fake_drop(h: int) -> void { }
|
||||
|
||||
bind TelemetryWorld { can_send: fn fake_can, enabled: fn fake_enabled, now_ms: fn fake_now, stamp: fn fake_stamp }
|
||||
bind TelemetryWorld { can_send: fn fake_can, enabled: fn fake_enabled, now_ms: fn fake_now, clock_s: fn fake_clock }
|
||||
bind TelemetryTransport { send: fn fake_send, poll: fn fake_poll, drop: fn fake_drop }
|
||||
|
||||
function qfile() -> string { return Os.temp_dir() + "/ludic-telemetry-test-queue.txt" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue