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
6 changes: 3 additions & 3 deletions docs/runtime/pgo-bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,14 @@ reuse the project's proto types and TLS config.

## Validating your workload locally

Before pushing a `.hyperi-ci.yaml` opt-in, test the pipeline locally:
Before pushing a `.hyperi-ci.yaml` opt-in, test the pipeline locally. CI passes `--bin` for each binary it ships on every `cargo pgo` step, so a feature-gated driver in the same package never compiles against the profile. Do the same here:

```bash
cargo install cargo-pgo
rustup component add llvm-tools-preview

# 1. Instrument
cargo pgo build -- --features jemalloc
cargo pgo build -- --bin <your-binary> --features jemalloc

# 2. Run your workload against the instrumented binary
bash scripts/pgo-workload.sh ./target/x86_64-unknown-linux-gnu/release/<your-binary>
Expand All @@ -259,7 +259,7 @@ llvm-profdata show --topn=20 target/pgo-profiles/merged.profdata
# Red flag: your startup / config-loading functions at the top.

# 6. Build optimised
cargo pgo optimize build -- --features jemalloc
cargo pgo optimize build -- --bin <your-binary> --features jemalloc
```

If step 5 shows startup code at the top, your workload needs more
Expand Down
5 changes: 3 additions & 2 deletions src/hyperi_ci/languages/rust/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,14 +1266,15 @@ def _build_for_target(
"PGO requested but crate has no binaries -- falling back to plain build"
)
else:
# Use the first binary for PGO (projects with multiple bins can
# extend this later; the common case is one binary per crate).
# The workload profiles the first binary; every cargo-pgo step still
# builds all the binaries packaging ships, and only those.
from hyperi_ci.languages.rust.pgo import run_pgo_build

return run_pgo_build(
target=target,
profile=profile,
binary_name=binary_names[0],
shipped_binaries=binary_names,
cwd=Path.cwd(),
# RUST_FEATURES / RUST_ALL_FEATURES are where the PGO path
# reads the declared features for its own cargo lines.
Expand Down
43 changes: 34 additions & 9 deletions src/hyperi_ci/languages/rust/pgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def run_pgo_build(
cwd: Path,
extra_env: dict[str, str] | None = None,
outcome: OptimizationOutcome | None = None,
shipped_binaries: list[str] | None = None,
) -> int:
"""Run the PGO (and optionally BOLT) pipeline for one target.

Expand All @@ -88,6 +89,9 @@ def run_pgo_build(
outcome: Filled in with the stages that actually completed, so the
caller can report a skip that this function warns about
but does not fail on.
shipped_binaries: Every binary packaging ships. Each `cargo pgo`
step builds these and nothing else. Defaults to
``[binary_name]``.

Returns:
0 on success, non-zero on failure.
Expand All @@ -101,6 +105,10 @@ def run_pgo_build(
env_features.get("RUST_FEATURES", ""),
all_features=env_features.get("RUST_ALL_FEATURES") == "true",
)
cargo_args = [
*_bin_scope_args(shipped_binaries or [binary_name]),
*feature_args,
]

if not _ensure_cargo_pgo_installed():
warn(
Expand Down Expand Up @@ -147,7 +155,7 @@ def run_pgo_build(

# 1. Instrumented build
info(f"PGO: building instrumented binary for {target}")
instrument_args = ["build", "--", "--target", target, *feature_args]
instrument_args = ["build", "--", "--target", target, *cargo_args]
rc = _run_cargo_pgo(instrument_args, cwd=cwd, extra_env=build_env)
if rc != 0 and target.startswith("aarch64") and shutil.which("mold"):
# Profile counters push a large binary's text past the +/-128 MB
Expand Down Expand Up @@ -192,7 +200,7 @@ def run_pgo_build(
# 3. Optimised build using profile data
info(f"PGO: building optimised binary for {target}")
rc = _run_cargo_pgo(
["optimize", "--", "--target", target, *feature_args],
["optimize", "--", "--target", target, *cargo_args],
cwd=cwd,
extra_env={**(build_env or {}), **_PROFILE_USE_ENV},
)
Expand All @@ -205,7 +213,7 @@ def run_pgo_build(
# 4. BOLT (optional, Linux-only)
if bolt:
rc = _run_bolt(
target, feature_args, binary_name, profile, cwd, build_env, outcome
target, cargo_args, binary_name, profile, cwd, build_env, outcome
)
if rc != 0:
warn("BOLT step failed -- continuing with PGO-only optimised binary")
Expand All @@ -219,6 +227,20 @@ def run_pgo_build(
# ---------------------------------------------------------------------------


def _bin_scope_args(binaries: list[str]) -> list[str]:
"""Render one ``--bin`` per shipped binary for a `cargo pgo` step.

Without a target filter cargo builds every bin whose required features are
on, so `--all-features` compiles a feature-gated workload driver against a
profile that does not cover it (#526). ``--bin`` filters targets only, so
package selection and feature resolution are unchanged.
"""
args: list[str] = []
for name in binaries:
args.extend(["--bin", name])
return args


def _run_plain_release_build(
target: str,
feature_args: list[str],
Expand Down Expand Up @@ -965,7 +987,7 @@ def _bolt_optimize_args() -> list[str]:

def _attempt_bolt(
target: str,
feature_args: list[str],
cargo_args: list[str],
binary_name: str,
profile: OptimizationProfile,
cwd: Path,
Expand All @@ -976,6 +998,9 @@ def _attempt_bolt(
) -> int:
"""Run one BOLT pass: instrument -> workload -> optimise.

``cargo_args`` follows ``--target`` on both builds: the ``--bin`` scope
and the feature flags, the same as the PGO steps.

`bolt build` emits `<binary>-bolt-instrumented`; the workload must run
against THAT binary so BOLT collects its own branch profile. Skipping
the workload (the old behaviour) left `bolt optimize` with nothing to
Expand Down Expand Up @@ -1017,7 +1042,7 @@ def _attempt_bolt(
"--",
"--target",
target,
*feature_args,
*cargo_args,
],
cwd=cwd,
extra_env=bolt_env,
Expand Down Expand Up @@ -1060,7 +1085,7 @@ def _attempt_bolt(
"--",
"--target",
target,
*feature_args,
*cargo_args,
],
cwd=cwd,
extra_env=bolt_env,
Expand All @@ -1075,7 +1100,7 @@ def _attempt_bolt(

def _run_bolt(
target: str,
feature_args: list[str],
cargo_args: list[str],
binary_name: str,
profile: OptimizationProfile,
cwd: Path,
Expand All @@ -1099,7 +1124,7 @@ def _run_bolt(
"""
rc = _attempt_bolt(
target,
feature_args,
cargo_args,
binary_name,
profile,
cwd,
Expand All @@ -1120,7 +1145,7 @@ def _run_bolt(
)
return _attempt_bolt(
target,
feature_args,
cargo_args,
binary_name,
profile,
cwd,
Expand Down
128 changes: 128 additions & 0 deletions tests/unit/test_rust_pgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,7 @@ def _run_pgo_bolt_pipeline(
bolt_enabled: bool = True,
bolt_toolchain: bool = True,
extra_env: dict[str, str] | None = None,
shipped_binaries: list[str] | None = None,
):
"""Run PGO (+BOLT) with a project env naming sccache; return cargo calls.

Expand All @@ -1838,6 +1839,7 @@ def _run_pgo_bolt_pipeline(
binary_name="my-bin",
cwd=tmp_path,
extra_env=extra_env or {"RUSTC_WRAPPER": "sccache"},
shipped_binaries=shipped_binaries,
)
assert rc == 0
return [(c.args[0], c.kwargs["extra_env"]) for c in cargo.call_args_list]
Expand Down Expand Up @@ -1908,6 +1910,132 @@ def test_both_bolt_steps_build_on_the_pgo_layout(
assert "--with-pgo" in args[: args.index("--")], args


def _cargo_build_args(args: list[str]) -> list[str]:
"""What cargo-pgo forwards to `cargo build`: everything after `--`."""
return args[args.index("--") + 1 :]


class TestBuildsAreScopedToTheShippedBinaries:
"""Every cargo-pgo step builds the shipped binaries and nothing else (#526).

An app's feature-gated workload driver compiled under `--all-features` in
the profile-use step, logging thousands of no-profile-data warnings for a
binary that never ships.
"""

_ALL_FEATURES = {"RUSTC_WRAPPER": "sccache", "RUST_ALL_FEATURES": "true"}

@pytest.mark.parametrize(
("cargo_results", "steps"),
[
([0, 0, 0, 0], ["build", "optimize", "bolt build", "bolt optimize"]),
(
[0, 0, 1, 0, 0],
["build", "optimize", "bolt build", "bolt build", "bolt optimize"],
),
],
ids=["first", "no-split"],
)
def test_every_step_names_the_binary_before_the_features(
self, tmp_path, cargo_results, steps
) -> None:
calls = _run_pgo_bolt_pipeline(
tmp_path, cargo_results, extra_env=self._ALL_FEATURES
)
assert [_step(args) for args, _ in calls] == steps
for args, _ in calls:
assert _cargo_build_args(args) == [
"--target",
"x86_64-unknown-linux-gnu",
"--bin",
"my-bin",
"--all-features",
], args

def test_pgo_only_steps_are_scoped_too(self, tmp_path) -> None:
calls = _run_pgo_bolt_pipeline(tmp_path, [0, 0], bolt_enabled=False)
assert [_step(args) for args, _ in calls] == ["build", "optimize"]
for args, _ in calls:
assert _cargo_build_args(args)[2:4] == ["--bin", "my-bin"], args

def test_every_shipped_binary_is_built_and_no_other(self, tmp_path) -> None:
calls = _run_pgo_bolt_pipeline(
tmp_path,
[0, 0, 0, 0],
extra_env=self._ALL_FEATURES,
shipped_binaries=["my-bin", "my-bin-admin"],
)
assert len(calls) == 4
for args, _ in calls:
forwarded = _cargo_build_args(args)
bins = [
forwarded[i + 1] for i, arg in enumerate(forwarded) if arg == "--bin"
]
assert bins == ["my-bin", "my-bin-admin"], args
assert "--workspace" not in forwarded
assert "-p" not in forwarded

def test_an_empty_list_falls_back_to_the_profiled_binary(self, tmp_path) -> None:
calls = _run_pgo_bolt_pipeline(
tmp_path, [0, 0], bolt_enabled=False, shipped_binaries=[]
)
for args, _ in calls:
assert _cargo_build_args(args).count("--bin") == 1, args
assert "my-bin" in _cargo_build_args(args), args

def test_the_plain_fallback_is_unchanged(self, tmp_path) -> None:
with (
patch.object(pgo, "_ensure_cargo_pgo_installed", return_value=False),
patch.object(
pgo, "run_cmd", return_value=MagicMock(returncode=0)
) as run_cmd,
):
rc = run_pgo_build(
target="x86_64-unknown-linux-gnu",
profile=_make_profile(allocator="system"),
binary_name="my-bin",
cwd=tmp_path,
shipped_binaries=["my-bin"],
)
assert rc == 0
assert run_cmd.call_args.args[0] == [
"cargo",
"build",
"--release",
"--target",
"x86_64-unknown-linux-gnu",
]

def test_the_build_stage_hands_over_every_packaged_binary(
self, tmp_path, monkeypatch
) -> None:
from hyperi_ci.languages.rust import build

monkeypatch.chdir(tmp_path)
bin_dir = tmp_path / "target" / "x86_64-unknown-linux-gnu" / "release"
bin_dir.mkdir(parents=True)
(bin_dir / "my-bin").touch()
monkeypatch.setattr(build, "_ensure_target_installed", lambda _target: True)
monkeypatch.setattr(
build, "_detect_binary_names", lambda: ["my-bin", "my-bin-admin"]
)

with (
patch.object(pgo, "_ensure_cargo_pgo_installed", return_value=True),
patch.object(pgo, "_run_cargo_pgo", return_value=0) as cargo,
patch.object(pgo, "_run_workload", return_value=0) as workload,
):
rc = build._build_for_target(
"x86_64-unknown-linux-gnu", "", False, {}, profile=_make_profile()
)

assert rc == 0
assert str(workload.call_args.args[2]).endswith("/my-bin")
for call in cargo.call_args_list:
forwarded = _cargo_build_args(call.args[0])
assert forwarded[2:6] == ["--bin", "my-bin", "--bin", "my-bin-admin"]


def _profile_settings(env: dict[str, str]) -> dict[str, str]:
return {
key: value for key, value in env.items() if key.startswith("CARGO_PROFILE_")
Expand Down
17 changes: 5 additions & 12 deletions tests/unit/test_rust_subprocess_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,19 +327,12 @@ def test_every_pgo_and_bolt_compile(self, tmp_path, monkeypatch, launches) -> No
shared = _expect(**self._PROJECT_ENV, CARGO_PROFILE_RELEASE_STRIP="none")
profile_use = {**shared, "RUSTC_WRAPPER": ""}
bolt = {**profile_use, _RUSTFLAGS: "-C link-arg=-fuse-ld=lld"}
scope = ["--target", _NATIVE, "--bin", "app"]
expected = [
(["cargo", "pgo", "build", "--", "--target", _NATIVE], shared),
(["cargo", "pgo", "optimize", "--", "--target", _NATIVE], profile_use),
(
["cargo", "pgo", "bolt", "build", "--with-pgo", "--"]
+ ["--target", _NATIVE],
bolt,
),
(
["cargo", "pgo", "bolt", "optimize", "--with-pgo", "--"]
+ ["--target", _NATIVE],
bolt,
),
(["cargo", "pgo", "build", "--", *scope], shared),
(["cargo", "pgo", "optimize", "--", *scope], profile_use),
(["cargo", "pgo", "bolt", "build", "--with-pgo", "--", *scope], bolt),
(["cargo", "pgo", "bolt", "optimize", "--with-pgo", "--", *scope], bolt),
]
assert [(launch["args"], _watched_env(launch)) for launch in launches] == (
expected
Expand Down
Loading