deb,cli,test,docs: record and show dep-install and build durations

Every binary build measures its dependency installation (apt-get
update, the essential and injected packages, each build-dependency
pass) and its package build (debian/rules build plus binary) into a
timings accumulator; the record manifest stores both as an optional
`timings` field when the run ends, whatever the outcome — a failed
build keeps its partial durations, and a run that died before any
timed command records nothing.

`pkh deb list` shows the two durations as DEPS and BUILD columns,
formatted like the age ("45s", "2m41s", "1h02m"); records predating
the field show dashes. The field is serde-optional, so the manifest
schema stays at 1 and existing records remain discoverable and
forkable.
This commit is contained in:
2026-09-30 12:45:13 +02:00
parent 6385cf6dc1
commit d946af8bb6
6 changed files with 181 additions and 10 deletions
+20 -1
View File
@@ -15,6 +15,7 @@ use crate::deb::cross;
use crate::deb::session;
use crate::debian::control::ControlInfo;
use crate::debian::deps::{Deps, Facts, ParseOpts, PkgRelation};
use std::time::Instant;
/// Attach the capture sink to a command when the live UI is active
fn cap<'a>(
@@ -106,6 +107,9 @@ pub async fn build(
session: Option<&session::Session>,
build_resume: bool,
ccache: bool,
// Accumulator of the measured dependency-installation and build
// durations; the caller records it on the build record.
timings: &mut session::BuildTimings,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let sink: Option<Arc<dyn LineSink>> = view.sink();
@@ -261,6 +265,7 @@ pub async fn build(
// Update package lists
log::debug!("Updating package lists for local build...");
enter_phase(view, Phase::UpdatingPackageLists);
let started = Instant::now();
let status = cap(
ctx.command("apt-get").envs(env.clone()).arg("update"),
&sink,
@@ -274,6 +279,7 @@ pub async fn build(
e
)
})?;
timings.add_deps(started.elapsed());
if !status.success() {
return Err("apt-get update failed inside the build context. \
If this is a local build, try executing with sudo, \
@@ -305,7 +311,9 @@ pub async fn build(
cmd.arg(format!("libc6-dev:{arch}"));
}
enter_phase(view, Phase::InstallingEssentials);
let started = Instant::now();
let status = cap(&mut cmd, &sink).status()?;
timings.add_deps(started.elapsed());
if !status.success() {
return Err("Could not install essential packages for the build".into());
}
@@ -348,7 +356,7 @@ pub async fn build(
// Install injected packages if specified
if !inject_packages.is_empty() {
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink, timings)?;
}
// Resolve and install the Build-* dependencies with dpkg's cross
@@ -367,6 +375,7 @@ pub async fn build(
ctx.clone(),
view,
&sink,
timings,
)?;
// (The build-dependency phase reruns in every fresh chroot: nothing
// to journal.)
@@ -400,6 +409,7 @@ pub async fn build(
// Run the build step
log::debug!("Building (debian/rules build) package...");
enter_phase(view, Phase::Building);
let started = Instant::now();
let status = cap(
ctx.command("debian/rules")
.current_dir(package_dir_str)
@@ -408,6 +418,7 @@ pub async fn build(
&sink,
)
.status()?;
timings.add_build(started.elapsed());
if !status.success() {
return Err("Error while building the package".into());
}
@@ -428,6 +439,7 @@ pub async fn build(
// Run the 'binary' step to produce deb
enter_phase(view, Phase::ProducingBinaries);
let started = Instant::now();
let status = cap(
ctx.command("fakeroot")
.current_dir(package_dir_str)
@@ -437,6 +449,7 @@ pub async fn build(
&sink,
)
.status()?;
timings.add_build(started.elapsed());
if !status.success() {
return Err(
"Error while building the binary artifacts (.deb) from the built package".into(),
@@ -730,6 +743,7 @@ fn install_build_dependencies(
ctx: Arc<Context>,
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
timings: &mut session::BuildTimings,
) -> Result<(), Box<dyn Error>> {
enter_phase(view, Phase::InstallingBuildDeps);
@@ -878,7 +892,9 @@ fn install_build_dependencies(
for spec in &specs {
cmd.arg(spec);
}
let started = Instant::now();
let status = cap(&mut cmd, sink).status()?;
timings.add_deps(started.elapsed());
if !status.success() {
view.suspend();
// Diagnosing a dependency failure the user interrupted themselves
@@ -1210,6 +1226,7 @@ pub(crate) fn install_injected_packages(
ctx: Arc<Context>,
view: &dyn BuildView,
sink: &Option<Arc<dyn LineSink>>,
timings: &mut session::BuildTimings,
) -> Result<(), Box<dyn Error>> {
log::info!("Installing injected packages: {:?}", packages);
@@ -1252,7 +1269,9 @@ pub(crate) fn install_injected_packages(
if !package_names.is_empty() {
cmd.args(&package_names);
}
let started = Instant::now();
let status = cap(&mut cmd, sink).status()?;
timings.add_deps(started.elapsed());
if !status.success() {
return Err(format!("Could not install injected packages: {:?}", deb_files).into());
}