Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions quest/m1/unbounded-groups/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Decided 2026-10-16 (maintainer interview):
separate `Slot.head` copy.
- Storage (decided 2026-10-24 by the maintainer, over the group storage
benchmark, [moq-dev/moq#6627](https://github.com/moq-dev/moq/pull/6627)):
the lock-free [append-only log](/quest/m1/unbounded-groups/log.md) is the
the lock-free append-only log (`kio::log`) is the
tail run, from a base that only advances, and refilled runs sit below it.
It moved here from m2, ahead of Front eviction, so the end-state storage
lands once instead of a mutex layout swapped later. Prototyped, the log
Expand Down Expand Up @@ -106,10 +106,10 @@ moq-transport, with memory held near the pool size.
## Required

- [Segmented frames](/quest/m1/unbounded-groups/segmented-frames.md) - large frames store as sealed segments, preallocate at most one, and release receive credit per segment
- [Append-only log](/quest/m1/unbounded-groups/log.md) - `GroupState`'s frames move onto a lock-free kio log with an advancing base
- [Front eviction](/quest/m1/unbounded-groups/front-eviction.md) - groups shed whole frames from their front under pressure and refill them in place; the group caps go
- [Byte holes](/quest/m1/unbounded-groups/byte-holes.md) - segments inside a frame evict and refill by `(frame, byte)` on the next lite version; the frame cap goes
- [JS unbounded groups](/quest/m1/unbounded-groups/js.md) - `@moq/net` drops its caps, streams huge frames, and matches front eviction
- [Log lock profile](/quest/m1/unbounded-groups/log-lock-profile.md) - a lock profile on a perf host confirms the epoch-based `kio::log` cut `GroupState` contention

## Related

Expand Down
3 changes: 1 addition & 2 deletions quest/m1/unbounded-groups/front-eviction.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ records".
See the [epic](/quest/m1/unbounded-groups/README.md) for the decisions.

- Storage is the lock-free
[append-only log](/quest/m1/unbounded-groups/log.md) (decided 2026-10-24
append-only log (`kio::log`) (decided 2026-10-24
by the maintainer over the group storage benchmark,
[moq-dev/moq#6627](https://github.com/moq-dev/moq/pull/6627): land the
end-state storage now rather than a mutex layout swapped later; numbers in
Expand Down Expand Up @@ -103,4 +103,3 @@ groups. A reader that used to see `GroupTooLarge` now refills or sees
## Required

- [Segmented frames](/quest/m1/unbounded-groups/segmented-frames.md) - frame storage the cache can account for without whole-frame preallocation
- [Append-only log](/quest/m1/unbounded-groups/log.md) - `GroupState`'s frames in a lock-free kio log with an advancing base, the tail run this builds on
21 changes: 21 additions & 0 deletions quest/m1/unbounded-groups/log-lock-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# [XS] Log lock profile

## Goal

`just bench-profile lock video-heavy` runs before and after the epoch-based
`kio::log` ([moq-dev/moq#6782](https://github.com/moq-dev/moq/pull/6782)) on a
host with `kernel.perf_event_paranoid=-1`, and shows no lock-contention
regression: `GroupState`'s share of mutex wait with multi-frame groups drops
or holds.

## Plan

- Blocked on the perf-host quest (`quest/m1/perf-host.md`, planned in
[moq-dev/moq#6796](https://github.com/moq-dev/moq/pull/6796)); add it under
Required once it lands.
- Compare the commit before #6782 with the one that merged it, at each worker
count, and post the wait-by-stack diff on the PR that finishes this.
- #6782's body holds the criterion numbers, taken on an oversubscribed host:
`group_contended` costs 13 to 61% less per reader across cores, and about
4 ns more per frame on one thread. A profile that contradicts them goes back
to the maintainer.
79 changes: 0 additions & 79 deletions quest/m1/unbounded-groups/log.md

This file was deleted.

10 changes: 4 additions & 6 deletions quest/m2/perf/kio-channel-contention/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,8 @@ Every hot site is a read or re-poll
of a per-track channel that all of a track's subscribers share, which is the
[version counter](/quest/m2/perf/kio-channel-contention/version.md)'s target.
`GroupState` is not contended here because both shapes send one-frame
groups; a multi-frame shape is unmeasured, so the
[append-only log](/quest/m1/unbounded-groups/log.md) measures one as its
before.
groups; `group_contended` in `rs/moq-net/benches/group.rs` measures a
multi-frame group instead.

Decided (maintainer, 2026-10-07):

Expand Down Expand Up @@ -108,7 +107,7 @@ Decided (maintainer, 2026-10-14):
- Candidates kept: a version counter with split waiter lists
([Version counter](/quest/m2/perf/kio-channel-contention/version.md)) and
an append-only log primitive with front eviction
([Append-only log](/quest/m1/unbounded-groups/log.md)), which
(Append-only log (`kio::log`)), which
much of moq-net could adopt
([Log adoption](/quest/m2/perf/kio-channel-contention/log-adoption.md)).
- Rejected, so later sessions don't re-propose them:
Expand Down Expand Up @@ -211,7 +210,7 @@ median): deque 30, runs 30, log 46, ring 42. Draining it: 14, 15, 12, 19.
`GroupState` wait with multi-frame groups is still unmeasured.

Decided (maintainer, 2026-10-24): Front eviction lands on the lock-free
[append-only log](/quest/m1/unbounded-groups/log.md) itself, a tail run from
append-only log (`kio::log`) itself, a tail run from
an advancing base with refilled runs below it, rather than the same layout
under today's mutex with the log swapped in later. The maintainer preferred
landing the end-state storage once. So the log moved to m1, ahead of Front
Expand All @@ -231,5 +230,4 @@ epic's targets but carries no lock-wait split.
- [Version counter](/quest/m2/perf/kio-channel-contention/version.md) - an opted-in re-poll skips the mutex when nothing changed, and parking no longer contends with readers
- [Replica spike](/quest/m2/perf/kio-channel-contention/spike-replica.md) - measures per-worker data-plane replicas linked over the in-memory transport, announcements shared
- [Cluster spike](/quest/m2/perf/kio-channel-contention/spike-cluster.md) - measures a relay per thread clustered over the in-memory transport, nothing shared
- [Append-only log](/quest/m1/unbounded-groups/log.md) - a kio log primitive with lock-free cursor reads and front eviction, holding `GroupState`'s frames
- [Log adoption](/quest/m2/perf/kio-channel-contention/log-adoption.md) - the rest of moq-net's log-shaped state moves onto the log where it measures as a win
3 changes: 1 addition & 2 deletions quest/m2/perf/kio-channel-contention/log-adoption.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Goal

Every moq-net channel whose state is an append log with front eviction moves
onto the [kio append-only log](/quest/m1/unbounded-groups/log.md),
onto the kio append-only log (`kio::log`),
where it measures as a win. Each migrated channel lands with the contended
bench or `just bench-profile lock` before and after; a channel that measures no win
stays on its mutex.
Expand All @@ -29,6 +29,5 @@ stays on its mutex.

## Required

- [Append-only log](/quest/m1/unbounded-groups/log.md) - the primitive this adopts
- [Replica spike](/quest/m2/perf/kio-channel-contention/spike-replica.md) - if per-worker replicas win, each channel's cross-core readers collapse to one flow-controlled stream per worker and adoption is likely abandoned
- [Cluster spike](/quest/m2/perf/kio-channel-contention/spike-cluster.md) - the same question with nothing shared
2 changes: 1 addition & 1 deletion quest/m2/perf/kio-channel-contention/version.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ longer contends with readers of the value. Lands with the contended bench (`rs/k
- `is_closed` and the closed check become atomic loads.
- This attacks spurious polls, not the wake-all herd: a woken consumer whose
data really changed still locks to read `T`. The
[append-only log](/quest/m1/unbounded-groups/log.md) covers that.
append-only log (`kio::log`) covers that.
- The skip is opt-in, never a silent change to `poll`: a predicate may read
state outside the channel. `Settle::poll` (`rs/moq-net/src/tail.rs`)
captures a deadline's `expired` in its closure, so skipping it on an
Expand Down
2 changes: 1 addition & 1 deletion quest/m3/middle-eviction.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ the decisions. Decided 2026-10-20 (maintainer): moved to m3, because group
storage is chosen for hot-path performance, not for evicting the middle.
Group storage was decided 2026-10-24 (maintainer, over
[moq-dev/moq#6627](https://github.com/moq-dev/moq/pull/6627)): the
[append-only log](/quest/m1/unbounded-groups/log.md) is a tail run that sheds
append-only log (`kio::log`) is a tail run that sheds
only its front, from a base that only advances, plus refilled runs below it.
So a kept head moves into a run below the base before the base advances past
the middle, rather than splitting the tail.
Expand Down
9 changes: 5 additions & 4 deletions rs/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,13 @@ doctest *args:
# `tests/loom.rs`, `LOOM_MAX_PREEMPTIONS`). kio's own unit tests are
# `cfg(not(loom))`.

# Permutation-test the concurrent handoffs with loom.
# Permutation-test the concurrent handoffs with loom. `crossbeam_loom` puts the
# epoch collector that frees `kio::log` segments under loom too.
[positional-arguments]
loom *args:
RUSTFLAGS="--cfg loom" cargo test --locked --release -p kio --lib loom:: "$@"
RUSTFLAGS="--cfg loom" cargo test --locked --release -p moq-net --test loom "$@"
RUSTFLAGS="--cfg loom" cargo test --locked --release -p moq-net --test expiry-loom "$@"
RUSTFLAGS="--cfg loom --cfg crossbeam_loom" cargo test --locked --release -p kio --lib loom:: "$@"
RUSTFLAGS="--cfg loom --cfg crossbeam_loom" cargo test --locked --release -p moq-net --test loom "$@"
RUSTFLAGS="--cfg loom --cfg crossbeam_loom" cargo test --locked --release -p moq-net --test expiry-loom "$@"

# Run the moq-quic simulation's random workloads from a fresh seed, printing the settings that replay it, then the bandwidth probe's acceptance suite.
quic-sim:
Expand Down
4 changes: 4 additions & 0 deletions rs/kio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ categories = ["asynchronous", "concurrency"]
_sim = []

[dependencies]
# Frees a log's evicted segments once no read that could reach them is running.
crossbeam-epoch = "0.9.21"
smallvec = { version = "1.15", features = ["union"] }

# Loom swaps in its own Arc/Mutex/atomics so it can permute every interleaving.
# It's a normal (not dev) dependency because `sync.rs` re-exports it from the
# library itself, but `cfg(loom)` is never set outside `just rs loom`.
[target.'cfg(loom)'.dependencies]
# Under `--cfg crossbeam_loom` too, so loom permutes the collector's own atomics.
crossbeam-epoch = { version = "0.9.21", features = ["loom"] }
loom = { workspace = true }

[dev-dependencies]
Expand Down
4 changes: 3 additions & 1 deletion rs/kio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ poll-native FIFO queue (bounded or unbounded) built in the same style. `Park` ho
waiter for as long as a poll stays pending, bridging a `std::task::Context` to kio's
waiter-based polls when implementing `Future` or `poll_*` on top of kio channels.
`coop::Budget` bounds the passes a loop makes per poll, so one that always finds
work ready still yields to the rest of its runtime.
work ready still yields to the rest of its runtime. `log::Writer` is an append-only
log whose `log::Reader`s read committed entries by index without a lock, paired with
a channel that parks them; evicted entries are freed by epoch-based reclamation.

It's used internally by [moq-net](https://github.com/moq-dev/moq/tree/main/rs/moq-net) and friends, but is generic enough to be useful on its own.

Expand Down
67 changes: 66 additions & 1 deletion rs/kio/benches/contended.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
//! - `contended_log`: a `GroupState`-shaped log. The producer appends a frame to a
//! `VecDeque`; each consumer reads its cursor's frame, cloning it out under the
//! lock, and parks again.
//! - `contended_log_lockfree`: the same log on [`kio::log`], read without the lock;
//! each consumer takes it only to park.
//!
//! The notify and log cycles include releasing and joining the workers each
//! iteration, a fixed cost that matters only at a few consumers per thread.
Expand Down Expand Up @@ -326,9 +328,72 @@ fn bench_log(c: &mut Criterion) {
group.finish();
}

/// `contended_log` on [`kio::log`]: the frames move out of the channel into the log,
/// which readers read without its lock. The channel stays for parking, so a reader
/// takes the lock only to park at the edge, once per round instead of twice.
fn bench_log_lockfree(c: &mut Criterion) {
let mut group = c.benchmark_group("contended_log_lockfree");
for (threads, consumers) in sweep() {
group.throughput(Throughput::Elements(consumers as u64));
let id = BenchmarkId::new(format!("threads_{threads}"), consumers);
group.bench_function(id, |b| {
let producer = Producer::new(kio::log::Writer::<Frame>::new(0));
let work = readers(&producer, consumers, threads)
.into_iter()
.map(|set| -> Work {
let mut set: Vec<_> = set
.into_iter()
.map(|reader| {
let log = reader.consumer.read().reader();
(reader, log)
})
.collect();
Box::new(move || {
for (reader, log) in &mut set {
{
let mut read = log.read();
let Ok(Some(frame)) = read.get(reader.seen) else {
panic!("an appended frame must be ready");
};
black_box(frame.clone());
reader.seen += 1;
assert_eq!(read.get(reader.seen), Ok(None));
}
let cursor = reader.seen;
let next = |log: &kio::Ref<'_, kio::log::Writer<Frame>>| match log.end() > cursor {
true => Poll::Ready(()),
false => Poll::Pending,
};
assert!(reader.consumer.poll(&reader.waiter, next).is_pending());
}
})
})
.collect();
let crew = Crew::new(work);

let mut index = 0;
b.iter_custom(|iters| {
let start = Instant::now();
for _ in 0..iters {
{
let mut log = producer.write().ok().expect("open");
log.push(Arc::new(index));
let end = log.end();
log.evict(end.saturating_sub(LOG_FRAMES as u64));
}
index += 1;
crew.run(1);
}
start.elapsed()
});
});
}
group.finish();
}

criterion_group! {
name = benches;
config = Criterion::default().warm_up_time(Duration::from_secs(1)).measurement_time(Duration::from_secs(3));
targets = bench_notify, bench_repoll, bench_log
targets = bench_notify, bench_repoll, bench_log, bench_log_lockfree
}
criterion_main!(benches);
4 changes: 4 additions & 0 deletions rs/kio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
//! clone-able handles, bounded or unbounded, with separate wake lists for the
//! push and pop sides.
//!
//! [`log`] is an append-only log whose readers take committed entries by index
//! without a lock, for fan-out state paired with a channel that parks them.
//!
//! [`coop`] bounds the passes a loop makes per poll, so one that always finds
//! work ready still yields.

Expand All @@ -30,6 +33,7 @@ mod sync;
mod waiter;

pub mod coop;
pub mod log;

mod consumer;
mod pending;
Expand Down
Loading
Loading