From 30d6713a056707a264d71e9069e5963aa182c1c1 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 8 Oct 2026 10:12:10 +0200 Subject: [PATCH] The panel test boots an image without sshserver, and a CI step's cargo finds fresh what the job's cargo built `guest / suite` was measured again on three jobs of 2026-10-08 (113110642665, 113190683753, 113188345621): 821 to 1072 s, of which the suite's own `building` figure is 368 to 610 s. Two of its costs go here. tests/panelcase is a config of the compositor and logkeeper. `screen_fatal_halt_composited` booted tests/metalcase, the T14's image, and was the only guest test that did: its ROOT's BUILT line was 121 to 212 s on the runners. `cargo build --timings` of that ROOT on the development machine gave 369 unit-seconds over 279 units, 230 of them in crates only sshserver reaches, and no guest speaks SSH. Built after the same builds, panelcase's BUILT line read 8 s there against metalcase's 53 s. What the test asserts is unchanged: the compositor holds the panel, the fatal report takes it, and the black box carries the report. What it loses is sshserver, netstack, soundserver, the block and file servers and test-runner running when the kernel dies; it asserts nothing of them. `cargo run` hands the program it runs CARGO_PKG_* and CARGO_MANIFEST_*, and cargo judges a build script's `rerun-if-env-changed` by its own environment. ring's script names seven of them, so the suite step's `cargo test`, a child of the driver, found ring stale against the build the job's own cargo had just made and compiled ring, rustls, rustls-webpki, ureq and the build system again: 36 to 54 s between `=== [ci] the suite` and `Running tests/toyos.rs`. `dispatch` now takes both families out of the driver's environment before any step. Setting them by hand on `cargo test --test toyos-build --no-run` reproduced the five `Compiling` lines; under `cargo run -- --ci guest` at this commit the step prints `Finished` and none. issues/the-guest-job-builds-every-image-on-every-run.md is closed: its exit was the measurement, which the three logs' BUILT lines now are, two of them pull request runs. What they leave is filed with those numbers as issues/the-guest-suite-compiles-every-guest-crate-cold-on-every-run.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A --- ...est-job-builds-every-image-on-every-run.md | 27 ------- ...les-every-guest-crate-cold-on-every-run.md | 74 +++++++++++++++++++ src/build.rs | 1 + src/ci.rs | 60 +++++++++++++++ tests/metalcase/system.toml | 5 +- tests/panelcase/system.toml | 20 +++++ tests/toyos.rs | 2 +- 7 files changed, 157 insertions(+), 32 deletions(-) delete mode 100644 issues/the-guest-job-builds-every-image-on-every-run.md create mode 100644 issues/the-guest-suite-compiles-every-guest-crate-cold-on-every-run.md create mode 100644 tests/panelcase/system.toml diff --git a/issues/the-guest-job-builds-every-image-on-every-run.md b/issues/the-guest-job-builds-every-image-on-every-run.md deleted file mode 100644 index c11698030a3..00000000000 --- a/issues/the-guest-job-builds-every-image-on-every-run.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-10-03 ---- - -# The guest job builds every image on every run - -`guest.yml`'s `suite` restores one cache entry, the sysroot, and builds the -rest in the job: the build driver, the harness, and each kernel, loader, ROOT -and test binary a guest boots. Job 111020980149 of run 37061831501 (#649's -head `b6635359a`, a pull request run with the sysroot restored from cache) -ran 18 min 11 s (its log's lines: #682, comment 5968053849): - -- 2 min 46 s in `deps`, before the checkout; -- 2 min 15 s compiling the driver and the harness (`Finished` in 1m 26s and - in 47.62s); -- 767.5 s in the suite by its own line, of which its 26 `PASS` lines account - for 208 s. The other 559 s is the log's silence, up to 209 s at a time, - between a `cleaning` line or a verdict and the next guest's first line: - three kernel builds by the suite's summary, and the loaders, ROOTs and test - binaries beside them. - -Owner: the orchestrator. - -**Exit**: what of those builds an entry restored from `main`'s cache scope -would save is measured on a pull request run. diff --git a/issues/the-guest-suite-compiles-every-guest-crate-cold-on-every-run.md b/issues/the-guest-suite-compiles-every-guest-crate-cold-on-every-run.md new file mode 100644 index 00000000000..ba8fc3360b4 --- /dev/null +++ b/issues/the-guest-suite-compiles-every-guest-crate-cold-on-every-run.md @@ -0,0 +1,74 @@ +--- +status: open +kind: tooling +opened: 2026-10-08 +--- + +# The guest suite compiles every guest crate cold on every run + +`guest.yml`'s `suite` restores the sysroot and nothing compiled with it: every +kernel, loader, userland crate and test binary a guest boots is built in the +job, for both architectures. Three jobs of 2026-10-08, in seconds, by each +log's own timestamps and the suite's `building` and `testing` figures: A is +113110642665, a merge group; B is 113190683753, a pull request that moved the +kernel; C is 113188345621, one that moved a single userland crate. + +| | A | B | C | +|---|---|---|---| +| `deps`: apt and rustup | 103 | 171 | 141 | +| the driver's build | 146 | 102 | 148 | +| the harness's build | 54 | 36 | 53 | +| the suite, building | 610 | 368 | 542 | +| the suite, testing | 136 | 124 | 131 | +| the job | 1072 | 821 | 1036 | + +The `BUILT` lines behind `building`, A / B / C: + +- x86_64 kernel, loader, ROOT of tests/netcase: 138 / 84 / 123 +- x86_64 test kernel, ROOT of tests/testcases: 65 / 37 / 56 +- x86_64 ROOT of tests/metalcase: 212 / 121 / 186 +- aarch64 test kernel, loader, ROOT of tests/testcases: 110 / 70 / 97 +- aarch64 kernel: 33 / 20 / 31 +- aarch64 `mask-windows` kernel, ROOT of tests/virtsmpcase: 33 / 21 / 30 +- the test binaries and the small ROOTs together: 19 / 15 / 19 + +Two of those costs are gone since. The metalcase line is: the one guest test +that booted it boots tests/panelcase, whose `BUILT` line read 8 s on the +development machine where metalcase's read 53 s after the same builds. And +the harness's build no longer compiles ring, rustls, ureq and the build system +a second time. No runner has measured either. + +What an entry of main's targets could save a run is bounded by its `building` +figure. With every target fresh the whole suite's builds took 23 s on the +development machine, 8 s of them an AArch64 userland that was not. A run that +moved the kernel compiles it five times whatever is restored, once per feature +set and architecture: the two lines above that are a kernel and little else +are 20 to 33 s each. + +What an entry would hold, measured on the development machine after one whole +suite: the four target directories are 724,457,182 B in 2953 files without +cargo's incremental state and 2,259,625,434 B of incremental state beside it; +`tar | zstd -T0` of the first is 201,025,465 B. + +What stands in the way: + +- The repository's caches are past GitHub's 10 GB and no gate reads a stored + size (`issues/the-host-caches-limit-reaches-the-10-gb-only-through-one-measured-ratio.md`). +- An entry names target directories, and + `issues/the-tree-resolves-in-five-cargo-locks-not-one.md` folds `kernel/target`, + `bootloader/target` and `userland/target` into the root's. +- Only main's scope is read by every ref, so the writer is a run on main, and + no pull request can show the saving before the writer has landed. +- A restored target is trusted by content or not at all: `src/cicache.rs` is + the tree's one reader that does. + +Not taken: `CARGO_INCREMENTAL=0` in the job. A cold +x86-64 kernel took 43 s of CPU and left 35 MiB without it, 62 s and 339 MiB with +it, on the development machine under load. It also turns rustc's MIR inliner +on (`kernel/Cargo.toml`, `[profile.toyos]`), so the job would boot other bytes +than `cargo run` builds. + +Owner: the guest job (`src/ci.rs`'s `guest`, `.github/workflows/guest.yml`). + +**Exit**: `guest / suite` on a pull request that moves no guest source prints +a `building` figure under 60 s. diff --git a/src/build.rs b/src/build.rs index 20dea9a07bd..29f424effd6 100644 --- a/src/build.rs +++ b/src/build.rs @@ -3160,6 +3160,7 @@ mod tests { "tests/metalcase/system.toml", "tests/metaldevicecase/system.toml", "tests/netcase/system.toml", + "tests/panelcase/system.toml", "tests/proctreecase/system.toml", "tests/testcases/system.toml", "tests/virtjobcase/system.toml", diff --git a/src/ci.rs b/src/ci.rs index 1811de03598..74b96108f46 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -72,6 +72,7 @@ pub fn dispatch(root: &Path, args: &[String]) { eprintln!("Error: {refusal}\n{USAGE}"); std::process::exit(2); }); + forget_own_package(); let steps = match &job { Job::Host => host(root), Job::Seal => seal(root), @@ -94,6 +95,22 @@ pub fn dispatch(root: &Path, args: &[String]) { } } +/// Take what `cargo run` told the driver about its own package out of what +/// every step inherits, before any thread. +/// +/// Cargo judges a build script's `rerun-if-env-changed` by its own +/// environment, and ring's names these: left in, a step's cargo finds ring +/// stale against the build the job's cargo made without them, and builds it, +/// its dependents and this crate a second time. +fn forget_own_package() { + for (name, _) in std::env::vars_os() { + let own = name.to_str().is_some_and(|n| n.starts_with("CARGO_PKG_") || n.starts_with("CARGO_MANIFEST_")); + if own { + std::env::remove_var(name); + } + } +} + /// One step's verdict: a sentence on green, the refusal on red. struct Step { label: String, @@ -1073,6 +1090,49 @@ mod tests { assert!(!rustc.contains("-C incremental"), "{rustc}"); } + /// A build script naming what `cargo run` hands the driver is fresh for a + /// step's cargo once the job's cargo, which ran without them, has built + /// it. The driver is a process of its own, as above. + #[test] + fn a_steps_cargo_finds_fresh_what_the_jobs_cargo_built() { + const NAMED: [&str; 2] = ["CARGO_PKG_NAME", "CARGO_MANIFEST_DIR"]; + let fixture = toyos_tmpdir::TempDir::new("ci-own-package"); + std::fs::create_dir(fixture.join("src")).unwrap(); + let manifest = "[package]\nname = \"one\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\n"; + std::fs::write(fixture.join("Cargo.toml"), manifest).unwrap(); + std::fs::write(fixture.join("src/lib.rs"), "").unwrap(); + let script = NAMED.map(|name| format!(" println!(\"cargo:rerun-if-env-changed={name}\");\n")).concat(); + std::fs::write(fixture.join("build.rs"), format!("fn main() {{\n{script}}}\n")).unwrap(); + let mut jobs = Command::new("cargo"); + jobs.args(["build", "--offline"]).current_dir(&fixture); + for name in NAMED { + jobs.env_remove(name); + } + let out = jobs.output().expect("run cargo"); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let out = crate::buildlock::tests::rerun("ci::tests::a_steps_cargo") + .env(FIXTURE, fixture.path()) + .output() + .expect("run the driver"); + let said = String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr); + assert!(out.status.success() && said.contains("test result: ok. 1 passed"), "{said}"); + } + + #[test] + #[ignore = "the driver of the test above; never runs on its own"] + fn a_steps_cargo() { + let fixture = PathBuf::from(std::env::var_os(FIXTURE).unwrap_or_else(|| panic!("run without {FIXTURE}"))); + assert!(std::env::var_os("CARGO_PKG_NAME").is_some(), "cargo names no package to this process"); + forget_own_package(); + let out = Command::new("cargo") + .args(["build", "-v", "--offline"]) + .current_dir(&fixture) + .output() + .expect("run cargo"); + let said = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success() && said.contains("Fresh one v0.1.0"), "{said}"); + } + fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } diff --git a/tests/metalcase/system.toml b/tests/metalcase/system.toml index 1a5d2559a90..49bbffaaa84 100644 --- a/tests/metalcase/system.toml +++ b/tests/metalcase/system.toml @@ -1,13 +1,10 @@ -# The metal-sim boot: what `Profile::Metal` puts in front of userland. -# # soundserver, netstack and sshserver are here for exactly the reason they have no device: # their graceful exit is half of what this config certifies. # sshserver is the one whose absence *is* the device absence — it has no device of # its own and reaches the machine's shape only through netstack. # test-runner makes the boot announce itself, so the harness waits for a # marker instead of guessing from pixels — and it is what lets an in-guest -# binary run on the machine shape that gets flashed. Under `mute` it simply -# parks on a console that never produces a byte. +# binary run on the machine shape that gets flashed. assets = ["assets"] diff --git a/tests/panelcase/system.toml b/tests/panelcase/system.toml new file mode 100644 index 00000000000..d13bb762223 --- /dev/null +++ b/tests/panelcase/system.toml @@ -0,0 +1,20 @@ +# The boot `screen_fatal_halt_composited` reads the panel of: a compositor +# holding the scanout when the kernel dies, on `Profile::Metal`'s machine. +# +# Not tests/metalcase: that image's sshserver is most of what it compiles, and +# no guest speaks SSH. + +assets = ["assets"] + +[boot] +start = ["logkeeper", "compositor"] + +# `every_boot_config_runs_logkeeper` is what refuses a boot config without it. +[programs.logkeeper] +service = true +syscap = ["logread"] + +[programs.compositor] +service = true +serves = ["compositor"] +devices = ["framebuffer", "keyboard", "mouse"] diff --git a/tests/toyos.rs b/tests/toyos.rs index 996ed86de37..606a0252a53 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -2509,7 +2509,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re // Driven by `metal-panic-probe`, which is the same kernel the owner // flashes: a gate that staged this with SYS_DEBUG would certify a // path his image does not contain. - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/metalcase"); + let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/panelcase"); let options = BootOptions { profile, smp: 8,