deb: record per-phase build timings, shown by pkh deb profile

Time every Phase the build flow announces through enter_phase — the
chroot bootstrap (tarball download, extraction, finalization), the
apt phases, patch application, the build and binary steps and the
artifact retrieval — and land the ordered profile in the record's
manifest of every run, failed builds included: their profile ends
at the phase the error propagated out of.

Timing rides the enter_phase choke point instead of new measurement
sites, so a phase added later profiles itself. The manifest field
is additive (serde default, skipped when absent), so older manifests
keep loading without a schema bump. The coarse timings stay the
source of the deb list columns: they measure the apt transactions
themselves, where the profile measures whole phases.

pkh deb profile renders a record's profile through the same
read-only record discovery as pkh deb log — the newest record of
the tree, or an id prefix as shown by pkh deb list — so
record_for_log becomes the neutral find_record. Phase keys are
storage names, distinct from the display labels; Phase::from_key
turns them back and a test keeps both in sync.
This commit is contained in:
2026-10-02 14:17:15 +02:00
parent 7055d69152
commit 2ab2ff91e9
8 changed files with 552 additions and 38 deletions
+36 -10
View File
@@ -1,7 +1,7 @@
/// Local binary package building
/// Directly calling 'debian/rules' in current context
use crate::context::{Context, ContextCommand, LineSink};
use crate::deb::{Phase, enter_phase, find_dsc_file};
use crate::deb::{BuildProfiler, Phase, enter_phase, find_dsc_file};
use crate::logfmt::QuiltClassifier;
use crate::report::BuildView;
use log::warn;
@@ -275,6 +275,9 @@ pub async fn build(
// Accumulator of the measured dependency-installation and build
// durations; the caller records it on the build record.
timings: &mut session::BuildTimings,
// The per-phase profile the caller records on the build record:
// fed by every enter_phase call of the build flow.
profiler: &mut BuildProfiler,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let sink: Option<Arc<dyn LineSink>> = view.sink();
@@ -486,7 +489,7 @@ pub async fn build(
// Update package lists
log::debug!("Updating package lists for local build...");
enter_phase(view, Phase::UpdatingPackageLists);
enter_phase(view, Phase::UpdatingPackageLists, profiler);
let started = Instant::now();
let status = cap(
ctx.command("apt-get").envs(env.clone()).arg("update"),
@@ -532,7 +535,7 @@ pub async fn build(
cmd.arg(format!("libc6:{arch}"));
cmd.arg(format!("libc6-dev:{arch}"));
}
enter_phase(view, Phase::InstallingEssentials);
enter_phase(view, Phase::InstallingEssentials, profiler);
let started = Instant::now();
let status = cap(&mut cmd, &sink).status()?;
timings.add_deps(started.elapsed());
@@ -571,14 +574,30 @@ pub async fn build(
.unwrap_or(false);
applied && previous != patch_stamp
};
apply_quilt_patches(package_dir_str, &env, ctx.clone(), view, &sink, pop_patches)?;
apply_quilt_patches(
package_dir_str,
&env,
ctx.clone(),
view,
&sink,
pop_patches,
profiler,
)?;
if let Some(s) = session {
s.record_phase("patches", patch_stamp);
}
// Install injected packages if specified
if !inject_packages.is_empty() {
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink, timings)?;
install_injected_packages(
inject_packages,
&env,
ctx.clone(),
view,
&sink,
timings,
profiler,
)?;
}
// Resolve and install the Build-* dependencies with dpkg's cross
@@ -598,6 +617,7 @@ pub async fn build(
view,
&sink,
timings,
profiler,
)?;
// (The build-dependency phase reruns in every fresh chroot: nothing
// to journal.)
@@ -669,7 +689,7 @@ pub async fn build(
// Run the build step
log::debug!("Building (debian/rules build) package...");
enter_phase(view, Phase::Building);
enter_phase(view, Phase::Building, profiler);
let started = Instant::now();
let status = cap(
rules_command(&ctx, rule_ids.as_ref(), "debian/rules")
@@ -699,7 +719,7 @@ pub async fn build(
}
// Run the 'binary' step to produce deb
enter_phase(view, Phase::ProducingBinaries);
enter_phase(view, Phase::ProducingBinaries, profiler);
let started = Instant::now();
let status = cap(
rules_command(&ctx, rule_ids.as_ref(), "fakeroot")
@@ -1005,8 +1025,9 @@ fn install_build_dependencies(
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
timings: &mut session::BuildTimings,
profiler: &mut BuildProfiler,
) -> Result<(), Box<dyn Error>> {
enter_phase(view, Phase::InstallingBuildDeps);
enter_phase(view, Phase::InstallingBuildDeps, profiler);
let host_arch = arch.to_string();
let build_arch = context_build_arch(&ctx);
@@ -1309,6 +1330,7 @@ fn apply_quilt_patches(
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
pop_first: bool,
profiler: &mut BuildProfiler,
) -> Result<(), Box<dyn Error>> {
let series_path = Path::new(package_dir).join("debian/patches/series");
if !ctx.exists(&series_path)? {
@@ -1409,7 +1431,9 @@ fn apply_quilt_patches(
}
}
// Apply all patches listed in the series
// Apply all patches listed in the series (a custom classifier: the
// phase reports per-patch progress, not the default's raw output)
profiler.enter(Phase::ApplyingPatches);
view.phase(
Phase::ApplyingPatches.label(),
Box::new(QuiltClassifier::new(total_patches)),
@@ -1488,10 +1512,11 @@ pub(crate) fn install_injected_packages(
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
timings: &mut session::BuildTimings,
profiler: &mut BuildProfiler,
) -> Result<(), Box<dyn Error>> {
log::info!("Installing injected packages: {:?}", packages);
enter_phase(view, Phase::InjectingPackages);
enter_phase(view, Phase::InjectingPackages, profiler);
// Separate .deb files from package names
let mut deb_files: Vec<String> = Vec::new();
@@ -1955,6 +1980,7 @@ mod tests {
&crate::report::Quiet,
&None,
false,
&mut BuildProfiler::new(),
)
.unwrap();
}