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
3 changes: 3 additions & 0 deletions .config/hakari.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ workspace-members = ["moq-mux-fuzz", "moq-net-fuzz", "moq-quic-fuzz", "moq-rtmp-

[final-excludes]
workspace-members = ["kio-macros"]
# Keep native media dependencies out of unrelated test builds. Final excludes
# preserve feature unification for their other dependencies.
third-party = [{ name = "pipewire" }, { name = "libspa" }, { name = "clang-sys" }]
1 change: 1 addition & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ test:
python3 sh/kt/gradle.test.py
python3 sh/rs/select.test.py
python3 sh/rs/config.test.py
python3 sh/rs/hakari.test.py
python3 sh/js/workspace.test.py
python3 sh/js/audit.test.py
python3 sh/rs/test-lints.test.py
Expand Down
3 changes: 0 additions & 3 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion quest/m0/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,6 @@ The historical samples locate candidates; they are not controlled benchmarks.
- [Drop main from the publish Environment](/quest/m0/ci/publish-release-only.md) - the maintainer narrows `publish` to `release` and release tags once no job naming it runs as `main`
- [Binary release publishers run on their tag](/quest/m0/ci/release-tag-publishers.md) - brew, Go and Swift releases stop running through `workflow_run` on main
- [The bench comparison builds moq-gst's catalog bench on its base](/quest/m0/ci/bench-base-build.md) - A paired bench comparison that selects `rust/gst/moq-gst/catalog` against `origin/main` builds the base side and reports a comparison, instead of marking it unavailable with "base build failed"
- [The workspace-hack refuses trybuild edges and spares non-media builds](/quest/m0/ci/hakari-followups.md) - `hakari.py` refuses trybuild dev-dependency edges, and non-media Linux test builds stop needing libpipewire and libclang
- [Measure the Linux cache effect of native media exclusions](/quest/m0/ci/hakari-followups.md) - scheduled Linux jobs measure cache reuse after unrelated tests stop compiling PipeWire and libclang
- [A release merge opens no stale release PR](/quest/m0/ci/release-pr-after-release.md) - `just rs release` publishes before it proposes, so a merged release isn't re-proposed
- [Build two queue entries at once if it helps](/quest/m0/ci/queue-concurrency.md) - a measured trial of `maximumEntriesToBuild: 2` against the serial lane's burst latency
91 changes: 62 additions & 29 deletions quest/m0/ci/hakari-followups.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,73 @@
# [XS] The workspace-hack refuses trybuild edges and spares non-media builds
# [S] Measure the Linux cache effect of native media exclusions

## Goal

`sh/rs/hakari.py` fails when a crate that dev-depends on trybuild also
dev-depends on `workspace-hack`, and a Linux test build of a crate with no
media dependency needs neither libpipewire nor libclang.
Scheduled Linux jobs record the cache hit rate and build-time effect of
removing native media requirements from unrelated test builds. Bring a
material cache regression to the maintainer before declaring the tradeoff
settled.

## Plan

Decided 2026-10-31 (user, /quest-plan of the follow-ups from the 2026-10-31 /quest-complete run), from [moq-dev/moq#6985](https://github.com/moq-dev/moq/pull/6985). Milestone: ✅ m0 (user: "use recommended for any CI/flakes").

- **trybuild edge**: trybuild copies a crate's dev-dependencies into the
project it compiles each UI case in, so `kio-macros`' hack edge made
`kio-macros::ui` compile the whole hack (88-120 s), time out, and starve
the tests beside it; #6985 (merged) was ejected from the land queue for it
([run 41074871604](https://github.com/moq-dev/moq/actions/runs/41074871604))
and fixed it by excluding `kio-macros` by hand (`.config/hakari.toml`'s
`[final-excludes]`). `sh/rs/hakari.py:37-53` (at be39188000) only checks
that excluded members lack the edge and the rest carry it as a path
dev-dependency. Make it derive the rule: a crate with `trybuild` in its
dev-dependencies must not carry the `workspace-hack` edge, with a test
that plants one.
- **libpipewire**: CodeRabbit flagged on #6985 that the hack's unified
features pull PipeWire (through cpal) and libclang into ordinary Linux
test builds of crates that use no media; `rs/workspace-hack/Cargo.toml`
still lists `pipewire` (`:353`) and `clang-sys` (`:372`). The dev shell
provides both, so only builds outside it notice. Recommended: exclude the
pipewire/cpal feature from the hack with hakari's final-excludes, then
measure that the cache hit rate across jobs barely moves. If it does
move, bring the numbers to the maintainer instead.

`cargo-hakari` is already in the dev shell (`flake.nix:162`, in `rustDeps`),
so that part of the original quest is done.
The implementation in [moq-dev/moq#7713](https://github.com/moq-dev/moq/pull/7713)
adds the derived trybuild-edge guard and excludes `pipewire`, `libspa`, and
`clang-sys` from the workspace-hack's final output. Other transitive features
remain unified. Linux x86_64 and ARM test graphs of `moq-json` no longer reach
PipeWire or libclang. An x86_64 nextest run with shared compiler caching
bypassed passes with native media pkg-config probes refused and an invalid
libclang path. The original manifests fail under the same restriction.

Local Cargo 1.98.1 unit graphs for `moq-json`, `moq-video --features pipewire`,
and `moq-audio --features pipewire`, each built with `--tests`, have 531 distinct
third-party recursive compilation identities both before and after. The JSON
build loses 30 units; video retains 501 total units. Four media identities
change. These are graph identities, not mbx hit rates or CPU measurements.

Additional Linux x86_64 unit graphs at the merged main baseline
`ac0f04e5dcca9a9a07d15135dc2e904215c494f3` compare `moq-video --features vaapi`
and `moq-video --features pipewire`, each with `--tests`. VAAPI-only drops from
478 to 456 total units; PipeWire stays at 491. Recursive third-party identities
shared by those builds drop from 453 to 428, and their union grows from 483 to
486. Excluding `clang-sys` lets VAAPI's bindgen 0.70 request `clang_6_0`, while
PipeWire's bindgen 0.72 requests `clang_11_0`; the library and its two build-script
units split into three extra compilation identities. The earlier three-build
sample does not cover this cost. Record this VAAPI/PipeWire split when interpreting
the scheduled cache results. These graphs measure compilation identities, not
CPU cost or cache hit rates; the post-merge measurements still decide whether
the tradeoff is material.

Use the same scheduled Linux job before and after this PR's merge, beginning
with `Nightly (Rust benchmark tests)`, and retain its mbx cache summaries and
available compiler-time fields. Compare the last scheduled `main` run before
the merge with the first two after it. Failed or cancelled builds and missing summaries
are incomplete evidence, not zero cache hits. Record source/build differences
that prevent attributing a change to these exclusions.

The 2026-11-02 scheduled run is a pre-merge candidate:
[run 41483654426](https://github.com/moq-dev/moq/actions/runs/41483654426),
[Rust benchmark tests](https://github.com/moq-dev/moq/actions/runs/41483654426/job/125408205554).
Its 12 mbx object-cache summaries total 986 hits, 721 misses, 323 not
looked up, 8 bypassed, and 11 remote failures. The conditional hit rate is
57.76% (`986 / (986 + 721)`); this excludes not-looked-up and bypassed units.
The summaries report estimated compiler time avoided, not measured CPU totals.
The 5 GiB download budget was exhausted during the first build; remote
failures and mixed build phases limit attribution. Replace this
candidate if a later scheduled run precedes this PR's merge.

Public API: none. Wire: none.

## Waiting

- moq-dev/moq: after the native-media exclusions merge, collect the last
scheduled Linux baseline and first two post-merge Nightlies' mbx hit rates
and available compiler-time fields. No post-merge evidence for these
exclusions exists yet. If the cache cost is material, present the measured tradeoff to the
maintainer; recommend keeping the narrow final exclusions unless the
measurements justify another design.

## Related

- [Unify dependency features with cargo-hakari](/quest/m0/ci/hakari.md) - records the hosted jobs' cache hit rate around the workspace-hack, the measurement the pipewire exclusion needs
- [Unify dependency features with cargo-hakari](/quest/m0/ci/hakari.md) - owns
the separate macOS/Windows evidence and FFI invocation alignment in
[moq-dev/moq#7666](https://github.com/moq-dev/moq/pull/7666); do not duplicate
those measurements or depend on its unmerged implementation
10 changes: 0 additions & 10 deletions rs/workspace-hack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,10 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc
hyper-util = { version = "0.1", default-features = false, features = ["client-legacy", "client-proxy"] }
itertools = { version = "0.13" }
libc = { version = "0.2", features = ["extra_traits"] }
libspa = { version = "0.10", default-features = false, features = ["v0_3_33"] }
linux-raw-sys = { version = "0.12", features = ["auxvec", "elf", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
log = { version = "0.4", default-features = false, features = ["release_max_level_debug"] }
nix = { version = "0.31", features = ["net", "uio"] }
nom-15128469a54ed75a = { package = "nom", version = "7" }
pipewire = { version = "0.10", default-features = false, features = ["v0_3_53"] }
proc-macro2 = { version = "1" }
quick-xml = { version = "0.41", features = ["overlapped-lists", "serialize"] }
quote = { version = "1" }
Expand All @@ -349,7 +347,6 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive", "s
[target.aarch64-unknown-linux-gnu.build-dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
bytemuck = { version = "1", default-features = false, features = ["aarch64_simd"] }
clang-sys = { version = "1", default-features = false, features = ["clang_11_0", "runtime"] }
crossbeam-epoch = { version = "0.9" }
cudarc = { version = "0.19", default-features = false, features = ["cuda-12020", "driver", "fallback-dynamic-loading", "nvrtc"] }
event-listener = { version = "5" }
Expand All @@ -362,12 +359,10 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc
hyper-util = { version = "0.1", default-features = false, features = ["client-legacy", "client-proxy"] }
itertools = { version = "0.13" }
libc = { version = "0.2", features = ["extra_traits"] }
libspa = { version = "0.10", default-features = false, features = ["v0_3_33"] }
linux-raw-sys = { version = "0.12", features = ["auxvec", "elf", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
log = { version = "0.4", default-features = false, features = ["release_max_level_debug"] }
nix = { version = "0.31", features = ["net", "uio"] }
nom-15128469a54ed75a = { package = "nom", version = "7" }
pipewire = { version = "0.10", default-features = false, features = ["v0_3_53"] }
quick-xml = { version = "0.41", features = ["overlapped-lists", "serialize"] }
regex = { version = "1" }
rustc-hash = { version = "1" }
Expand Down Expand Up @@ -430,12 +425,10 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc
hyper-util = { version = "0.1", default-features = false, features = ["client-legacy", "client-proxy"] }
itertools = { version = "0.13" }
libc = { version = "0.2", features = ["extra_traits"] }
libspa = { version = "0.10", default-features = false, features = ["v0_3_33"] }
linux-raw-sys = { version = "0.12", features = ["auxvec", "elf", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
log = { version = "0.4", default-features = false, features = ["release_max_level_debug"] }
nix = { version = "0.31", features = ["net", "uio"] }
nom-15128469a54ed75a = { package = "nom", version = "7" }
pipewire = { version = "0.10", default-features = false, features = ["v0_3_53"] }
proc-macro2 = { version = "1" }
quick-xml = { version = "0.41", features = ["overlapped-lists", "serialize"] }
quote = { version = "1" }
Expand All @@ -453,7 +446,6 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive", "s
[target.x86_64-unknown-linux-gnu.build-dependencies]
bitflags = { version = "2", default-features = false, features = ["std"] }
bytemuck = { version = "1", default-features = false, features = ["aarch64_simd"] }
clang-sys = { version = "1", default-features = false, features = ["clang_11_0", "runtime"] }
crossbeam-epoch = { version = "0.9" }
cudarc = { version = "0.19", default-features = false, features = ["cuda-12020", "driver", "fallback-dynamic-loading", "nvrtc"] }
event-listener = { version = "5" }
Expand All @@ -466,12 +458,10 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["aws-lc
hyper-util = { version = "0.1", default-features = false, features = ["client-legacy", "client-proxy"] }
itertools = { version = "0.13" }
libc = { version = "0.2", features = ["extra_traits"] }
libspa = { version = "0.10", default-features = false, features = ["v0_3_33"] }
linux-raw-sys = { version = "0.12", features = ["auxvec", "elf", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
log = { version = "0.4", default-features = false, features = ["release_max_level_debug"] }
nix = { version = "0.31", features = ["net", "uio"] }
nom-15128469a54ed75a = { package = "nom", version = "7" }
pipewire = { version = "0.10", default-features = false, features = ["v0_3_53"] }
quick-xml = { version = "0.41", features = ["overlapped-lists", "serialize"] }
regex = { version = "1" }
rustc-hash = { version = "1" }
Expand Down
8 changes: 7 additions & 1 deletion sh/rs/hakari.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
every third-party crate while `cargo publish` strips it. `cargo hakari
manage-deps` only writes normal dependencies, so this checks the edges instead:
every member dev-depends on the hack, except those `.config/hakari.toml`
excludes, which must not depend on it at all. Run from `just rs lint`.
excludes, and crates whose UI tests use trybuild. Neither may depend on it
at all: trybuild copies dev-dependencies into every UI test project. Run
from `just rs lint`.
"""

import json
Expand Down Expand Up @@ -38,6 +40,10 @@
name = package["name"]
edges = [dep for dep in package["dependencies"] if dep["name"] == hack]
manifest = Path(package["manifest_path"]).relative_to(ROOT)
if any(dep["name"] == "trybuild" and dep["kind"] == "dev" for dep in package["dependencies"]):
if edges:
errors.append(f"{manifest}: trybuild copies dev-dependencies into UI tests; remove the {hack} edge")
continue
if name in excluded:
if edges:
errors.append(f"{manifest}: {name} is excluded in .config/hakari.toml but depends on {hack}")
Expand Down
88 changes: 88 additions & 0 deletions sh/rs/hakari.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Keep UI test projects and non-media test builds free of native media dependencies."""

import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]


class EdgesTest(unittest.TestCase):
def test_trybuild_cannot_inherit_the_hack(self):
scratch = ROOT / ".tmp"
scratch.mkdir(exist_ok=True)
for excluded in (False, True):
for target in (False, True):
with (
self.subTest(excluded=excluded, target=target),
tempfile.TemporaryDirectory(dir=scratch) as directory,
):
root = Path(directory)
(root / "sh/rs").mkdir(parents=True)
shutil.copyfile(ROOT / "sh/rs/hakari.py", root / "sh/rs/hakari.py")
(root / ".config").mkdir()
(root / ".config/hakari.toml").write_text(
'hakari-package = "workspace-hack"\n[final-excludes]\nworkspace-members = '
+ ('["ui", "trybuild"]\n' if excluded else '["trybuild"]\n')
)
(root / "Cargo.toml").write_text(
'[workspace]\nmembers = ["ui", "trybuild", "workspace-hack"]\nresolver = "2"\n'
)
for name in ("ui", "trybuild", "workspace-hack"):
crate = root / name
(crate / "src").mkdir(parents=True)
(crate / "src/lib.rs").touch()
(crate / "Cargo.toml").write_text(f'[package]\nname = "{name}"\nversion = "0.0.0"\n')
manifest = root / "ui/Cargo.toml"
with manifest.open("a") as output:
output.write(
("[target.'cfg(unix)'.dev-dependencies]\n" if target else "[dev-dependencies]\n")
+ 'ui-runner = { package = "trybuild", path = "../trybuild" }\n'
)
safe = manifest.read_text()
with manifest.open("a") as output:
output.write('workspace-hack = { path = "../workspace-hack" }\n')
result = subprocess.run(["python3", "sh/rs/hakari.py"], cwd=root, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("trybuild", result.stderr)
self.assertIn("workspace-hack", result.stderr)
# A new UI crate is safe without having to copy kio-macros' exclusion.
manifest.write_text(safe)
result = subprocess.run(["python3", "sh/rs/hakari.py"], cwd=root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)


class NativeDependenciesTest(unittest.TestCase):
def test_non_media_linux_test_graph(self):
for target in ("x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"):
with self.subTest(target=target):
result = subprocess.run(
[
"cargo",
"tree",
"--locked",
"-p",
"moq-json",
"--target",
target,
"--edges",
"normal,build,dev",
"--prefix",
"none",
"--format",
"{p}",
],
cwd=ROOT,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
packages = {line.split()[0] for line in result.stdout.splitlines()}
self.assertIn("workspace-hack", packages)
self.assertTrue(packages.isdisjoint({"pipewire-sys", "libspa-sys", "clang-sys"}), packages)


if __name__ == "__main__":
unittest.main()
Loading