Cookbook

Live leaderboard

A ranking of 100,000 players, a 1.4 MB snapshot and a stream of score changes. The big structure stays off the core lock and out of every signal: it is ingested on the blocking pool, and the screen gets a window of 60 rows and four numbers in one small transaction.

The problem#

A game, a contest or a fitness app shows a ranked list of 100,000 players. A snapshot (JSON, [[id, score], ..], 1,377,798 bytes) arrives when the app opens and now and then after that, and score changes stream in between. The screen shows three things: the top 50, a few rows around the current player, and four numbers (how many players, the player's rank, the top score, the median). It never shows 100,000 rows, so the core should never ask a platform to hold them.

The core has one lock. Whoever holds it is the core: a call, the poll of a task, the commit of a change-set all wait for it (SPEC 5.1). So the question is not whether the snapshot gets parsed, but where, and for how long the lock is held while it is.

Two designs that look right#

DesignCore lock held per snapshot, p50p99What reaches the platform
This recipe: pool, then publish a window375 ns458 ns780 bytes
Parse inside a core call2.56 ms2.94 ms780 bytes
Parse inside a call, a signal of every player2.94 ms3.26 ms1,200,033 bytes

The recipe#

Show the Rust
core/src/leaderboard.rs
/// The big structure and who is followed, behind the app's own lock. Only blocking-pool threads
/// take it; the core never does.
#[derive(Default)]
struct Inner {
    standings: Standings,
    me: Option<u32>,
    version: u64, // bumped by every change, under this lock
}

impl Inner {
    /// The window, and the version it shows: read under the lock, right after a change.
    fn window(&mut self) -> (u64, View) {
        self.version += 1;
        (self.version, self.standings.view(self.me, TOP, AROUND))
    }
}

/// The leaderboard's store: three small signals, and a handle on the structure that is not one.
#[undra::store(restore = "Self::assemble")]
pub struct Leaderboard {
    ctx: WeakCtx,
    shared: Arc<Shared>, // holds the Mutex<Inner>: the 100,000 players
    top: Signal<Vec<Row>>,
    around: Signal<Vec<Row>>,
    summary: Signal<Summary>,
}
core/src/leaderboard.rs
/// Parses a snapshot on the blocking pool, swaps it in and publishes the window.
async fn load(&self, bytes: Vec<u8>) -> Result<u32, LeaderboardError> {
    let ctx = self.ctx.upgrade().map_err(|_| LeaderboardError::Closed)?;
    let shared = self.shared.clone();
    let work = ctx.spawn_blocking(move || -> Result<(u64, View), SnapshotError> {
        // Milliseconds of parsing and sorting, with no lock held at all.
        let fresh = Standings::from_snapshot(&bytes)?;
        // The lock is held for the swap and the window, not for the parse.
        let mut inner = lock(&shared.inner);
        inner.standings = fresh;
        Ok(inner.window())
    });
    drop(ctx);
    let (version, view) = work.await?; // back on the core, with 50 rows and four numbers
    self.publish(version, &view);
    Ok(view.players)
}
core/src/leaderboard.rs
/// Writes the window: three signals, one transaction, and nothing that grows with the players.
/// This is the only part of an update that runs on the core.
fn publish(&self, version: u64, view: &View) {
    // Two updates can come back from the pool in either order: never write an older window.
    if self.shared.published.fetch_max(version, Ordering::SeqCst) >= version {
        return;
    }
    let top: Vec<Row> = view.top.iter().map(Row::from).collect();
    let around: Vec<Row> = view.around.iter().map(Row::from).collect();
    let summary = Summary {
        players: view.players,
        top_score: view.top_score,
        median_score: view.median_score,
        me: view.me.as_ref().map(Row::from),
    };
    txn(|| {
        // A write is a change-set whether or not the value changed: skip what did not.
        if self.top.with(|have| *have != top) {
            self.top.set(top);
        }
        if self.around.with(|have| *have != around) {
            self.around.set(around);
        }
        if self.summary.with(|have| *have != summary) {
            self.summary.set(summary);
        }
    });
}

The structure#

Standings (standings.rs) is plain Rust that knows nothing about Undra. It keeps every player twice: sorted by id, to find one, and in rank order, to answer for the top, for a neighbourhood and for the middle. A delta batch changes the scores in place, takes the players it touched out of the ranking and merges them back in one pass (O(players + touched × log touched), never a sort of everyone). view is O(top + around + log n). Ranks are competition ranks: players with the same score share a rank and the next one skips them (1, 2, 2, 4). Because it is plain Rust, the benchmark harness compiles the very same file, so the numbers on this page are the numbers of this code.

On the platform#

The screen observes three small signals: the best fifty, a few rows around the player and the summary. The platform neither knows nor cares how many players there are.

Show the Swift, Kotlin and TypeScript
swift
struct LeaderboardView: View {
    let board: Leaderboard
    let myId: UInt32

    var body: some View {
        List {
            // Four numbers about 100,000 players, and the best fifty of them.
            Section("\(board.summary.players) players, median score \(board.summary.medianScore)") {
                ForEach(board.top, id: \.id) { row in PlayerRow(row: row, mine: row.id == myId) }
            }
            // Where I stand: a few rows around me, which the core moves when I ask or when scores change.
            if let me = board.summary.me {
                Section("You are #\(me.rank)") {
                    ForEach(board.around, id: \.id) { row in PlayerRow(row: row, mine: row.id == myId) }
                }
            } else {
                Button("Find me") { Task { try? await board.follow(id: myId) } }
            }
        }
        .task { _ = try? await board.loadSnapshot(path: "/standings") }
        .refreshable { _ = try? await board.loadSnapshot(path: "/standings") }
    }
}

struct PlayerRow: View {
    let row: Row
    let mine: Bool

    var body: some View {
        HStack {
            Text("#\(row.rank)").monospacedDigit()
            Text("Player \(row.id)").fontWeight(mine ? .bold : .regular)
            Spacer()
            Text("\(row.score)").monospacedDigit()
        }
    }
}
kotlin
class LeaderboardScreen(private val board: Leaderboard, private val myId: UInt) {
    // Fifty rows, up to seven rows around me and four numbers: the 100,000 players stay in the core.
    val top: StateFlow<List<Row>> = board.top
    val around: StateFlow<List<Row>> = board.around
    val summary: StateFlow<Summary> = board.summary

    suspend fun refresh() { board.loadSnapshot("/standings") }

    suspend fun findMe() = board.follow(myId)

    fun isMine(row: Row): Boolean = row.id == myId
}
tsx
function PlayerRow({ row, mine }: { row: Row; mine: boolean }) {
  return (
    <li style={{ fontWeight: mine ? 700 : 400 }}>
      #{row.rank} Player {row.id}: {row.score}
    </li>
  );
}

export function LeaderboardView({ board, myId }: { board: Leaderboard; myId: number }) {
  const top = useSignal(board.top);         // the best fifty
  const around = useSignal(board.around);   // a few rows around the player being followed
  const summary = useSignal(board.summary); // players, top score, median score, my rank
  useEffect(() => void board.loadSnapshot("/standings"), [board]);
  return (
    <>
      <h2>{summary.players} players, median score {summary.medianScore}</h2>
      <ol>{top.map((row) => <PlayerRow key={row.id} row={row} mine={row.id === myId} />)}</ol>
      {summary.me ? (
        <>
          <h3>You are #{summary.me.rank}</h3>
          <ol>{around.map((row) => <PlayerRow key={row.id} row={row} mine={row.id === myId} />)}</ol>
        </>
      ) : (
        <button onClick={() => void board.follow(myId)}>Find me</button>
      )}
    </>
  );
}

The numbers#

The benchmark rows time one core call on the host. A call holds the core lock for as long as it runs, so its time is how long one snapshot of 100,000 players holds the lock. The host copies every change-set under the lock, as each platform's callback does. Best of several runs on an Apple M5 Pro while other builds ran (load average 13), rustc 1.99.0, 2026-10-05:

Row (leaderboard/..)What the call does under the core lockp50p99
lock_hold/recommendedpublishes the window the pool prepared: three signals, one transaction375 ns458 ns
lock_hold/in_core_callparses, sorts and swaps the snapshot inside the call, then publishes the same window2.56 ms2.94 ms
lock_hold/signal_of_rowsparses and sorts inside the call and sets a signal of every player2.94 ms3.26 ms
ingest/off_core(no core lock) what the pool does for this recipe: parse, sort, swap, window2.56 ms2.98 ms

The lock is held about 6,800 times shorter at the median and 6,400 times at the 99th percentile. The cost of the snapshot did not go away: it moved to a thread that does not hold the lock, and the snapshot reaches the screen about as soon either way. The second wrong design is only 15% worse under the lock on a host, because encoding and copying 1.2 MB is cheap next to parsing 1.4 MB; its price is paid on each platform, which has to decode and apply those 1.2 MB, and that no host-side row shows.

What a caller sees: a second thread made a call that does nothing but need the core lock, in a loop, while a snapshot was applied about every 9 ms for 8 seconds per design (UNDRA_STRESS_SECONDS=8; the test runs 5 without it; best of four runs; a busy machine deschedules the probe itself, so the maximum is the noisiest figure):

DesignWait p50p99p99.9Max
This recipe125 ns2.0 us7.8 us132 us
Snapshot applied inside a call125 ns2.9 us3.47 ms7.04 ms

A call that arrives during an in-call apply waits out the parse. That is about one call in 200 here, so it is the p99.9 and the maximum that show it, and not the median or the p99. Both tables come from the same code: UNDRA_BENCH_FILTER=leaderboard cargo test -p undra-bench --test budgets --release -- --nocapture --exact budgets prints the leaderboard/ rows (without the variable it times every row of the benchmark) and UNDRA_STRESS_SECONDS=8 cargo test -p undra-bench --test leaderboard --release -- --ignored --nocapture stall_report the caller's wait. CI holds the first one to a budget (the recommended row's p50 and p99, in bench/budgets.toml) and to a ratio that does not depend on the machine: the publish must stay under 5% of the in-call apply, and it is 0.015%. A publish that grew with the players, the ranking read or sorted on the core or every row in a signal, would put that ratio near 1. These are host numbers; finding 9 of the benchmark results has the method and the limits.

What the tests prove#

What to watch#

Source: examples/cookbook/core/src/leaderboard.rs and standings.rs.

Edit this page on GitHub