Files
casino/pkg/room/bench_test.go
drjones 038550b6ff perf: single-statement postings, marshal-once broadcast, client interpolation
Measured, then fixed, the three things that made a crowd impossible.

Ledger: Post issued three round trips per posting, so settlement scaled
in network latency rather than work. It is now two statements regardless
of leg count — settling 1000 winners went 844ms to 220ms. The lock and
the balance read must stay separate statements: a single statement, even
one whose CTE does FOR UPDATE, evaluates against a snapshot taken before
the locks are held, so concurrent transactions read stale balances and
money disappears. The conservation tests caught exactly that.

Broadcast: every connection marshalled its own copy, ~355us each. At any
real crowd that exceeds the tick interval by orders of magnitude. Frames
are now serialised once per broadcast and shared.

Feed: the player list is capped at 24 and carries no public keys, and
running rounds broadcast at 5Hz instead of 60Hz. Clients compute the
multiplier locally from the round start time, which the deterministic
curve makes exact. Frame size fell from 3.6KB to 1.8KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:36:30 +00:00

70 lines
1.6 KiB
Go

package room
import (
"encoding/json"
"testing"
)
// Broadcast cost decides whether a crowd can watch the same round. At 60Hz
// with N subscribers the server does N marshals per tick unless the payload is
// serialised once and shared.
func BenchmarkSnapshotMarshal(b *testing.B) {
r := New("rocket", nil, nil)
for i := 0; i < 200; i++ {
r.bets[int64(i)] = &Bet{
AccountID: int64(i),
Pubkey: []byte("0123456789abcdef0123456789abcdef"),
Nickname: "player",
StakeMsat: 10000,
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
snap := r.Snapshot()
if _, err := json.Marshal(snap); err != nil {
b.Fatal(err)
}
}
}
// Snapshot alone, without serialisation: this is the lock-held portion, which
// blocks every other operation on the room.
func BenchmarkSnapshotOnly(b *testing.B) {
r := New("rocket", nil, nil)
for i := 0; i < 200; i++ {
r.bets[int64(i)] = &Bet{
AccountID: int64(i),
Pubkey: []byte("0123456789abcdef0123456789abcdef"),
Nickname: "player",
StakeMsat: 10000,
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = r.Snapshot()
}
}
// Fan-out to many subscriber channels.
func BenchmarkBroadcast1000Subscribers(b *testing.B) {
r := New("rocket", nil, nil)
for i := 0; i < 50; i++ {
r.bets[int64(i)] = &Bet{
AccountID: int64(i), Pubkey: []byte("key"),
Nickname: "p", StakeMsat: 1000,
}
}
// Drain subscribers so the buffered channels do not simply fill.
for i := 0; i < 1000; i++ {
ch, _ := r.Subscribe()
go func(c <-chan []byte) {
for range c {
}
}(ch)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.broadcast()
}
}