Skip to content

perf(moq-pub-mmtp): parallelize publisher pipeline + add gated pprof …#46

Merged
kkroo merged 1 commit into
mainfrom
omar/pub45-parallelize
Jul 19, 2026
Merged

perf(moq-pub-mmtp): parallelize publisher pipeline + add gated pprof …#46
kkroo merged 1 commit into
mainfrom
omar/pub45-parallelize

Conversation

@kkroo

@kkroo kkroo commented Jul 19, 2026

Copy link
Copy Markdown

…endpoint (#45)

The publisher ran its three long-lived halves -- session.run(), publish_namespace() (egress/encode), and run_publisher() (multicast ingest) -- as three branches of a single tokio::select!. That is one future = one task, and tokio never parallelizes a single task across workers, so recv -> route -> per-shred open_uni -> encode -> quinn-write serialize on ONE core (~0.97 core, ~90% userspace) regardless of the runtime's worker count. Raising the pod CPU limit only removed CFS throttling; it did not lift this single-task ceiling.

Split the three halves onto separate tokio::spawn tasks and race their JoinHandles, so the multi-thread runtime can place ingest and egress on different workers. Producer (SubgroupsWriter) and consumer (SubgroupsReader) already communicate through an Arc-backed watch::State, so this is a scheduling change only: the wire output (objects, groups, subgroups, framing, ordering) is byte-identical and the relay is unaffected. run_publisher stays a single task, so the monotonic group_id assignment (datagram.rs) remains a single ordered point. Teardown is preserved -- the first task to finish (Ok or Err) is propagated exactly as the old select! did, so the external watchdog still respawns on error.

Also add an optional on-demand CPU profiler behind the profiling cargo feature (off by default) AND the MOQ_PUB_PROFILE_ADDR env var (unset by default): a pprof-rs flamegraph/protobuf endpoint served by tiny_http on its own std thread. With the feature off, the default release build never compiles pprof/tiny_http, so the shipped binary is byte-for-byte unchanged. pprof-rs samples via SIGPROF/ITIMER_PROF (userspace, no CAP_SYS_ADMIN), so it runs under baseline PodSecurity. --build-arg PROFILING=1 produces a profiling-enabled moq-pub-mmtp image, used to confirm which userspace frames dominate the pub's single-thread core.

…endpoint (#45)

The publisher ran its three long-lived halves -- session.run(),
publish_namespace() (egress/encode), and run_publisher() (multicast
ingest) -- as three branches of a single tokio::select!. That is one
future = one task, and tokio never parallelizes a single task across
workers, so recv -> route -> per-shred open_uni -> encode -> quinn-write
serialize on ONE core (~0.97 core, ~90% userspace) regardless of the
runtime's worker count. Raising the pod CPU limit only removed CFS
throttling; it did not lift this single-task ceiling.

Split the three halves onto separate tokio::spawn tasks and race their
JoinHandles, so the multi-thread runtime can place ingest and egress on
different workers. Producer (SubgroupsWriter) and consumer
(SubgroupsReader) already communicate through an Arc-backed watch::State,
so this is a scheduling change only: the wire output (objects, groups,
subgroups, framing, ordering) is byte-identical and the relay is
unaffected. run_publisher stays a single task, so the monotonic group_id
assignment (datagram.rs) remains a single ordered point. Teardown is
preserved -- the first task to finish (Ok or Err) is propagated exactly
as the old select! did, so the external watchdog still respawns on error.

Also add an optional on-demand CPU profiler behind the `profiling` cargo
feature (off by default) AND the MOQ_PUB_PROFILE_ADDR env var (unset by
default): a pprof-rs flamegraph/protobuf endpoint served by tiny_http on
its own std thread. With the feature off, the default release build never
compiles pprof/tiny_http, so the shipped binary is byte-for-byte
unchanged. pprof-rs samples via SIGPROF/ITIMER_PROF (userspace, no
CAP_SYS_ADMIN), so it runs under baseline PodSecurity. `--build-arg
PROFILING=1` produces a profiling-enabled moq-pub-mmtp image, used to
confirm which userspace frames dominate the pub's single-thread core.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@allyblockcast allyblockcast left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.

Looks good

Reviewed the task-spawn refactor in main.rs and the new gated pprof endpoint in profiling.rs. No Critical or Important issues found.

  • main.rs: the switch from a single select! over three inline futures to three tokio::spawn'd tasks raced via &mut JoinHandle is correct — #[tokio::main] here uses the default multi-thread flavor (tokio = { features = ["full"] }, no flavor/worker_threads override), so this actually gets the parallel scheduling the PR claims. Teardown semantics are preserved: .abort() on all three handles after the race (including the winner, a harmless no-op) mirrors the old select!'s implicit drop of the losing branches, and a JoinError on the winning task is converted to a bail! rather than silently swallowed. CI's release build (build check) confirms the Send + 'static bounds hold for the moved session/publisher/tracks_reader/router/tracks_writer.
  • profiling.rs/Cargo.toml/Dockerfile: the feature-gate (profiling, off by default) plus runtime env-var gate (MOQ_PUB_PROFILE_ADDR) is layered correctly — #[cfg(feature = "profiling")] on the mod profiling; declaration means the default release build never even parses this file, matching the "byte-for-byte unchanged" claim. The endpoint runs on its own std::thread, so a 30–120s blocking sample can't stall the tokio hot path.

Suggestions (1)

  • [gstack/review] moq-pub-mmtp/src/profiling.rs:38spawn_if_enabled() binds to whatever MOQ_PUB_PROFILE_ADDR is set to with no validation that it's loopback. The surrounding comments (both here and in Cargo.toml) describe the endpoint as "bind loopback only," but nothing in code enforces that — an operator setting e.g. 0.0.0.0:6060 on a profiling-enabled image would expose unauthenticated CPU-profile capture (and up to 120s-blocking DoS via repeated requests) on a routable interface. Consider a startup check that warns loudly (or refuses to bind) when the parsed host isn't 127.0.0.1/::1/localhost, so the documented intent is actually guaranteed rather than operator-trusted.

Strengths

  • Comments throughout explain non-obvious why (single-task-can't-parallelize-across-workers, why abort() after already racing is safe, why the profiler thread can't perturb the hot path) rather than restating code.
  • The Dockerfile's optional second build stage correctly overwrites /usr/local/cargo/bin/moq-pub-mmtp before the final COPY --from=builder, so --build-arg PROFILING=1 cleanly swaps the binary without touching the default build path.

Recommended Action

  1. Consider the loopback-binding suggestion above before shipping PROFILING=1 images to any environment where the debug port could be reachable off-host.

Reviewed head: d2eb1eb

@kkroo

kkroo commented Jul 19, 2026

Copy link
Copy Markdown
Author

Superseded for the CPU lever by #47. #47 fixes the root cause (unbounded waker-Vec growth in watch/state.rs::register via the unreliable will_wake retain), which live-measures as ~half the pub's ~1-core burn; cloudflare#141 covers the sibling ~20% in web-transport-quinn. With both landed, pub CPU drops well under one core, so the task-split parallelization here isn't needed for the throttle.

If the gated pprof endpoint in this PR is still wanted for profiling cloudflare#141, suggest splitting it out to a pprof-only PR and closing the parallelization half. Either way, don't roll this image for CPU — see the relay req-id drift deploy note I left on #47 (applies to any pub image roll).

@kkroo
kkroo merged commit 0843968 into main Jul 19, 2026
2 checks passed
@kkroo
kkroo deleted the omar/pub45-parallelize branch July 19, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants