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:
@@ -170,7 +170,8 @@ pkh deb --incremental <id>
|
|||||||
# fork that exact record (see `pkh deb list`);
|
# fork that exact record (see `pkh deb list`);
|
||||||
# a failed record is fine if its staged tree is
|
# a failed record is fine if its staged tree is
|
||||||
pkh deb list # the records of this tree: id, outcome,
|
pkh deb list # the records of this tree: id, outcome,
|
||||||
# version, fork lineage, staged-tree state, age
|
# version, fork lineage, staged-tree state,
|
||||||
|
# dep-install and build times, age
|
||||||
pkh deb log [-f] [<id>] # print a record's build log; -f follows a
|
pkh deb log [-f] [<id>] # print a record's build log; -f follows a
|
||||||
# live build
|
# live build
|
||||||
pkh deb --no-ccache # build without the shared compile cache
|
pkh deb --no-ccache # build without the shared compile cache
|
||||||
|
|||||||
+20
-1
@@ -15,6 +15,7 @@ use crate::deb::cross;
|
|||||||
use crate::deb::session;
|
use crate::deb::session;
|
||||||
use crate::debian::control::ControlInfo;
|
use crate::debian::control::ControlInfo;
|
||||||
use crate::debian::deps::{Deps, Facts, ParseOpts, PkgRelation};
|
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
|
/// Attach the capture sink to a command when the live UI is active
|
||||||
fn cap<'a>(
|
fn cap<'a>(
|
||||||
@@ -106,6 +107,9 @@ pub async fn build(
|
|||||||
session: Option<&session::Session>,
|
session: Option<&session::Session>,
|
||||||
build_resume: bool,
|
build_resume: bool,
|
||||||
ccache: 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>> {
|
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
|
||||||
let sink: Option<Arc<dyn LineSink>> = view.sink();
|
let sink: Option<Arc<dyn LineSink>> = view.sink();
|
||||||
|
|
||||||
@@ -261,6 +265,7 @@ pub async fn build(
|
|||||||
// Update package lists
|
// Update package lists
|
||||||
log::debug!("Updating package lists for local build...");
|
log::debug!("Updating package lists for local build...");
|
||||||
enter_phase(view, Phase::UpdatingPackageLists);
|
enter_phase(view, Phase::UpdatingPackageLists);
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(
|
let status = cap(
|
||||||
ctx.command("apt-get").envs(env.clone()).arg("update"),
|
ctx.command("apt-get").envs(env.clone()).arg("update"),
|
||||||
&sink,
|
&sink,
|
||||||
@@ -274,6 +279,7 @@ pub async fn build(
|
|||||||
e
|
e
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
timings.add_deps(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err("apt-get update failed inside the build context. \
|
return Err("apt-get update failed inside the build context. \
|
||||||
If this is a local build, try executing with sudo, \
|
If this is a local build, try executing with sudo, \
|
||||||
@@ -305,7 +311,9 @@ pub async fn build(
|
|||||||
cmd.arg(format!("libc6-dev:{arch}"));
|
cmd.arg(format!("libc6-dev:{arch}"));
|
||||||
}
|
}
|
||||||
enter_phase(view, Phase::InstallingEssentials);
|
enter_phase(view, Phase::InstallingEssentials);
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(&mut cmd, &sink).status()?;
|
let status = cap(&mut cmd, &sink).status()?;
|
||||||
|
timings.add_deps(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err("Could not install essential packages for the build".into());
|
return Err("Could not install essential packages for the build".into());
|
||||||
}
|
}
|
||||||
@@ -348,7 +356,7 @@ pub async fn build(
|
|||||||
|
|
||||||
// Install injected packages if specified
|
// Install injected packages if specified
|
||||||
if !inject_packages.is_empty() {
|
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
|
// Resolve and install the Build-* dependencies with dpkg's cross
|
||||||
@@ -367,6 +375,7 @@ pub async fn build(
|
|||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
view,
|
view,
|
||||||
&sink,
|
&sink,
|
||||||
|
timings,
|
||||||
)?;
|
)?;
|
||||||
// (The build-dependency phase reruns in every fresh chroot: nothing
|
// (The build-dependency phase reruns in every fresh chroot: nothing
|
||||||
// to journal.)
|
// to journal.)
|
||||||
@@ -400,6 +409,7 @@ pub async fn build(
|
|||||||
// Run the build step
|
// Run the build step
|
||||||
log::debug!("Building (debian/rules build) package...");
|
log::debug!("Building (debian/rules build) package...");
|
||||||
enter_phase(view, Phase::Building);
|
enter_phase(view, Phase::Building);
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(
|
let status = cap(
|
||||||
ctx.command("debian/rules")
|
ctx.command("debian/rules")
|
||||||
.current_dir(package_dir_str)
|
.current_dir(package_dir_str)
|
||||||
@@ -408,6 +418,7 @@ pub async fn build(
|
|||||||
&sink,
|
&sink,
|
||||||
)
|
)
|
||||||
.status()?;
|
.status()?;
|
||||||
|
timings.add_build(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err("Error while building the package".into());
|
return Err("Error while building the package".into());
|
||||||
}
|
}
|
||||||
@@ -428,6 +439,7 @@ pub async fn build(
|
|||||||
|
|
||||||
// Run the 'binary' step to produce deb
|
// Run the 'binary' step to produce deb
|
||||||
enter_phase(view, Phase::ProducingBinaries);
|
enter_phase(view, Phase::ProducingBinaries);
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(
|
let status = cap(
|
||||||
ctx.command("fakeroot")
|
ctx.command("fakeroot")
|
||||||
.current_dir(package_dir_str)
|
.current_dir(package_dir_str)
|
||||||
@@ -437,6 +449,7 @@ pub async fn build(
|
|||||||
&sink,
|
&sink,
|
||||||
)
|
)
|
||||||
.status()?;
|
.status()?;
|
||||||
|
timings.add_build(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(
|
return Err(
|
||||||
"Error while building the binary artifacts (.deb) from the built package".into(),
|
"Error while building the binary artifacts (.deb) from the built package".into(),
|
||||||
@@ -730,6 +743,7 @@ fn install_build_dependencies(
|
|||||||
ctx: Arc<Context>,
|
ctx: Arc<Context>,
|
||||||
view: &dyn BuildView,
|
view: &dyn BuildView,
|
||||||
sink: &Option<Arc<dyn LineSink>>,
|
sink: &Option<Arc<dyn LineSink>>,
|
||||||
|
timings: &mut session::BuildTimings,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
enter_phase(view, Phase::InstallingBuildDeps);
|
enter_phase(view, Phase::InstallingBuildDeps);
|
||||||
|
|
||||||
@@ -878,7 +892,9 @@ fn install_build_dependencies(
|
|||||||
for spec in &specs {
|
for spec in &specs {
|
||||||
cmd.arg(spec);
|
cmd.arg(spec);
|
||||||
}
|
}
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(&mut cmd, sink).status()?;
|
let status = cap(&mut cmd, sink).status()?;
|
||||||
|
timings.add_deps(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
view.suspend();
|
view.suspend();
|
||||||
// Diagnosing a dependency failure the user interrupted themselves
|
// Diagnosing a dependency failure the user interrupted themselves
|
||||||
@@ -1210,6 +1226,7 @@ pub(crate) fn install_injected_packages(
|
|||||||
ctx: Arc<Context>,
|
ctx: Arc<Context>,
|
||||||
view: &dyn BuildView,
|
view: &dyn BuildView,
|
||||||
sink: &Option<Arc<dyn LineSink>>,
|
sink: &Option<Arc<dyn LineSink>>,
|
||||||
|
timings: &mut session::BuildTimings,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
log::info!("Installing injected packages: {:?}", packages);
|
log::info!("Installing injected packages: {:?}", packages);
|
||||||
|
|
||||||
@@ -1252,7 +1269,9 @@ pub(crate) fn install_injected_packages(
|
|||||||
if !package_names.is_empty() {
|
if !package_names.is_empty() {
|
||||||
cmd.args(&package_names);
|
cmd.args(&package_names);
|
||||||
}
|
}
|
||||||
|
let started = Instant::now();
|
||||||
let status = cap(&mut cmd, sink).status()?;
|
let status = cap(&mut cmd, sink).status()?;
|
||||||
|
timings.add_deps(started.elapsed());
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
return Err(format!("Could not install injected packages: {:?}", deb_files).into());
|
return Err(format!("Could not install injected packages: {:?}", deb_files).into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,6 +458,10 @@ async fn build_binary_package_impl(
|
|||||||
None => base_ctx.clone(),
|
None => base_ctx.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The measured durations of the build's work phases, recorded on the
|
||||||
|
// record when the run ends — success or failure alike.
|
||||||
|
let mut timings = session::BuildTimings::default();
|
||||||
|
|
||||||
let result = async {
|
let result = async {
|
||||||
// Prepare build directory: the stable /tmp/pkh-build for session
|
// Prepare build directory: the stable /tmp/pkh-build for session
|
||||||
// builds (see EphemeralContextGuard::build_root — the overlay
|
// builds (see EphemeralContextGuard::build_root — the overlay
|
||||||
@@ -571,6 +575,7 @@ async fn build_binary_package_impl(
|
|||||||
session.as_ref(),
|
session.as_ref(),
|
||||||
build_resume,
|
build_resume,
|
||||||
ccache,
|
ccache,
|
||||||
|
&mut timings,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
@@ -617,6 +622,14 @@ async fn build_binary_package_impl(
|
|||||||
{
|
{
|
||||||
s.set_outcome(session::OUTCOME_FAILED);
|
s.set_outcome(session::OUTCOME_FAILED);
|
||||||
}
|
}
|
||||||
|
// The measured durations land in the record whatever the outcome: a
|
||||||
|
// failed build's partial timings are data too. Nothing measured (the
|
||||||
|
// run ended before any timed command) records nothing.
|
||||||
|
if let Some(s) = &session
|
||||||
|
&& !timings.is_empty()
|
||||||
|
{
|
||||||
|
s.set_timings(timings);
|
||||||
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-6
@@ -2,8 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! A record is one build's metadata: identity, lineage, outcome, the
|
//! A record is one build's metadata: identity, lineage, outcome, the
|
||||||
//! SHA-256 of the chroot tarball its environment was extracted from, a
|
//! SHA-256 of the chroot tarball its environment was extracted from, a
|
||||||
//! tree phase journal, the full build log, and — for `--incremental`
|
//! tree phase journal, the measured build durations, the full build
|
||||||
//! runs — the staged tree's overlay upperdir. Records live under
|
//! log, and — for `--incremental` runs — the staged tree's overlay
|
||||||
|
//! upperdir. Records live under
|
||||||
//! `/var/tmp/pkh/sessions/<slug>/<record-id>/`.
|
//! `/var/tmp/pkh/sessions/<slug>/<record-id>/`.
|
||||||
//!
|
//!
|
||||||
//! The chroot itself is per-run scratch: every build (plain and
|
//! The chroot itself is per-run scratch: every build (plain and
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
//! ```text
|
//! ```text
|
||||||
//! /var/tmp/pkh/sessions/<slug>/
|
//! /var/tmp/pkh/sessions/<slug>/
|
||||||
//! <record-id>/ # one record per build
|
//! <record-id>/ # one record per build
|
||||||
//! manifest.json # identity, lineage, outcome, tarball sha, journal
|
//! manifest.json # identity, lineage, outcome, tarball sha,
|
||||||
|
//! # journal, build durations
|
||||||
//! log/build.log # full tee of the build output (always)
|
//! log/build.log # full tee of the build output (always)
|
||||||
//! tree-upper/ # the staged tree's overlay upperdir
|
//! tree-upper/ # the staged tree's overlay upperdir
|
||||||
//! # (--incremental runs only, post-run)
|
//! # (--incremental runs only, post-run)
|
||||||
@@ -137,6 +139,38 @@ pub struct PhaseStamp {
|
|||||||
pub stamp: Option<String>,
|
pub stamp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wall-clock durations of one build's work, as recorded in a record's
|
||||||
|
/// manifest: the dependency installation (every apt phase before the
|
||||||
|
/// build — package list update, essential and injected packages,
|
||||||
|
/// build-dependencies) and the package build itself (`debian/rules
|
||||||
|
/// build` plus `binary`). The full build time is the sum of both.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct BuildTimings {
|
||||||
|
/// Seconds spent installing dependencies.
|
||||||
|
pub deps_secs: u64,
|
||||||
|
/// Seconds spent building the package.
|
||||||
|
pub build_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuildTimings {
|
||||||
|
/// Accumulate a measured duration into the dependency-installation
|
||||||
|
/// total.
|
||||||
|
pub fn add_deps(&mut self, elapsed: Duration) {
|
||||||
|
self.deps_secs += elapsed.as_secs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accumulate a measured duration into the package-build total.
|
||||||
|
pub fn add_build(&mut self, elapsed: Duration) {
|
||||||
|
self.build_secs += elapsed.as_secs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether nothing was measured: the build ended before any timed
|
||||||
|
/// command ran, and the record must not claim durations of zero.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.deps_secs == 0 && self.build_secs == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The record manifest: identity, lineage, outcome and tree state.
|
/// The record manifest: identity, lineage, outcome and tree state.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct SessionManifest {
|
pub struct SessionManifest {
|
||||||
@@ -184,6 +218,11 @@ pub struct SessionManifest {
|
|||||||
/// Whether this record's build staged the tree over a carried
|
/// Whether this record's build staged the tree over a carried
|
||||||
/// overlay upperdir (build-level resume was active).
|
/// overlay upperdir (build-level resume was active).
|
||||||
pub build_resume: bool,
|
pub build_resume: bool,
|
||||||
|
/// Wall-clock durations of this record's build (`None`: not
|
||||||
|
/// recorded — the build ended before any timed command ran, or the
|
||||||
|
/// record predates the field).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub timings: Option<BuildTimings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State shared through the [`Session`] handle: the manifest plus the
|
/// State shared through the [`Session`] handle: the manifest plus the
|
||||||
@@ -327,6 +366,14 @@ impl Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record the measured build durations (dependency installation,
|
||||||
|
/// package build) and persist them.
|
||||||
|
pub fn set_timings(&self, timings: BuildTimings) {
|
||||||
|
self.with_manifest(|manifest| {
|
||||||
|
manifest.timings = Some(timings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `f` over the manifest and persist the result; failures are
|
/// Run `f` over the manifest and persist the result; failures are
|
||||||
/// logged and never abort the build (the journal is a cache).
|
/// logged and never abort the build (the journal is a cache).
|
||||||
fn with_manifest(&self, f: impl FnOnce(&mut SessionManifest)) {
|
fn with_manifest(&self, f: impl FnOnce(&mut SessionManifest)) {
|
||||||
@@ -894,6 +941,7 @@ fn fresh_manifest(
|
|||||||
tree_version: tree_version.to_string(),
|
tree_version: tree_version.to_string(),
|
||||||
kept_upper: false,
|
kept_upper: false,
|
||||||
build_resume: false,
|
build_resume: false,
|
||||||
|
timings: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1053,6 +1101,7 @@ pub(crate) fn fork_record_in(
|
|||||||
tree_version: version.to_string(),
|
tree_version: version.to_string(),
|
||||||
kept_upper: false,
|
kept_upper: false,
|
||||||
build_resume: false,
|
build_resume: false,
|
||||||
|
timings: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if carry_upper {
|
if carry_upper {
|
||||||
@@ -1294,6 +1343,12 @@ pub struct SessionListRow {
|
|||||||
pub upper: bool,
|
pub upper: bool,
|
||||||
/// Outcome of this record's build.
|
/// Outcome of this record's build.
|
||||||
pub outcome: String,
|
pub outcome: String,
|
||||||
|
/// Duration of the record's dependency installation, formatted
|
||||||
|
/// ("-" when the record carries none).
|
||||||
|
pub deps: String,
|
||||||
|
/// Duration of the record's package build, formatted ("-" when the
|
||||||
|
/// record carries none).
|
||||||
|
pub build: String,
|
||||||
/// Human-readable age of this record's build.
|
/// Human-readable age of this record's build.
|
||||||
pub age: String,
|
pub age: String,
|
||||||
}
|
}
|
||||||
@@ -1305,6 +1360,13 @@ pub fn list_for_tree(tree: &Path) -> Vec<SessionListRow> {
|
|||||||
.map(|session| {
|
.map(|session| {
|
||||||
let manifest = session.manifest();
|
let manifest = session.manifest();
|
||||||
let cross = if manifest.identity.cross { "*" } else { "" };
|
let cross = if manifest.identity.cross { "*" } else { "" };
|
||||||
|
let (deps, build) = match manifest.timings {
|
||||||
|
Some(timings) => (
|
||||||
|
format_duration(timings.deps_secs),
|
||||||
|
format_duration(timings.build_secs),
|
||||||
|
),
|
||||||
|
None => ("-".to_string(), "-".to_string()),
|
||||||
|
};
|
||||||
SessionListRow {
|
SessionListRow {
|
||||||
id: manifest.id,
|
id: manifest.id,
|
||||||
package: manifest.identity.package,
|
package: manifest.identity.package,
|
||||||
@@ -1316,6 +1378,8 @@ pub fn list_for_tree(tree: &Path) -> Vec<SessionListRow> {
|
|||||||
base: manifest.base,
|
base: manifest.base,
|
||||||
upper: session.has_upper(),
|
upper: session.has_upper(),
|
||||||
outcome: manifest.last_outcome,
|
outcome: manifest.last_outcome,
|
||||||
|
deps,
|
||||||
|
build,
|
||||||
age: age_of(&manifest.last_used),
|
age: age_of(&manifest.last_used),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1343,6 +1407,17 @@ fn age_of(rfc3339: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Human-readable duration of a build phase ("45s", "2m41s", "1h02m",
|
||||||
|
/// "3d04h"): unlike [`age_of`] the minutes matter at every scale.
|
||||||
|
fn format_duration(secs: u64) -> String {
|
||||||
|
match secs {
|
||||||
|
0..=59 => format!("{secs}s"),
|
||||||
|
60..=3599 => format!("{}m{:02}s", secs / 60, secs % 60),
|
||||||
|
3600..=86399 => format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60),
|
||||||
|
_ => format!("{}d{:02}h", secs / 86400, (secs % 86400) / 3600),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Render the `pkh deb list` table.
|
/// Render the `pkh deb list` table.
|
||||||
pub fn render_session_list(tree: &Path, rows: &[SessionListRow]) -> String {
|
pub fn render_session_list(tree: &Path, rows: &[SessionListRow]) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
@@ -1355,7 +1430,7 @@ pub fn render_session_list(tree: &Path, rows: &[SessionListRow]) -> String {
|
|||||||
}
|
}
|
||||||
out.push_str(&format!("Records for {}:\n", tree.display()));
|
out.push_str(&format!("Records for {}:\n", tree.display()));
|
||||||
let headers = [
|
let headers = [
|
||||||
"ID", "PACKAGE", "VERSION", "TARGET", "BASE", "UPPER", "OUTCOME", "AGE",
|
"ID", "PACKAGE", "VERSION", "TARGET", "BASE", "UPPER", "OUTCOME", "DEPS", "BUILD", "AGE",
|
||||||
];
|
];
|
||||||
let table: Vec<Vec<String>> = rows
|
let table: Vec<Vec<String>> = rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1368,6 +1443,8 @@ pub fn render_session_list(tree: &Path, rows: &[SessionListRow]) -> String {
|
|||||||
row.base.clone().unwrap_or_else(|| "-".to_string()),
|
row.base.clone().unwrap_or_else(|| "-".to_string()),
|
||||||
if row.upper { "yes" } else { "-" }.to_string(),
|
if row.upper { "yes" } else { "-" }.to_string(),
|
||||||
row.outcome.clone(),
|
row.outcome.clone(),
|
||||||
|
row.deps.clone(),
|
||||||
|
row.build.clone(),
|
||||||
row.age.clone(),
|
row.age.clone(),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -2283,6 +2360,10 @@ mod tests {
|
|||||||
tree_version: "7.3.0-5.6~local2".to_string(),
|
tree_version: "7.3.0-5.6~local2".to_string(),
|
||||||
kept_upper: true,
|
kept_upper: true,
|
||||||
build_resume: true,
|
build_resume: true,
|
||||||
|
timings: Some(BuildTimings {
|
||||||
|
deps_secs: 41,
|
||||||
|
build_secs: 9020,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
let base = tempfile::tempdir().unwrap();
|
let base = tempfile::tempdir().unwrap();
|
||||||
save_manifest(base.path(), &manifest).unwrap();
|
save_manifest(base.path(), &manifest).unwrap();
|
||||||
@@ -2296,10 +2377,12 @@ mod tests {
|
|||||||
object.remove("base");
|
object.remove("base");
|
||||||
object.remove("tarball_sha256");
|
object.remove("tarball_sha256");
|
||||||
object.remove("kept_upper");
|
object.remove("kept_upper");
|
||||||
|
object.remove("timings");
|
||||||
let older: SessionManifest = serde_json::from_value(value).unwrap();
|
let older: SessionManifest = serde_json::from_value(value).unwrap();
|
||||||
assert_eq!(older.base, None);
|
assert_eq!(older.base, None);
|
||||||
assert_eq!(older.tarball_sha256, "");
|
assert_eq!(older.tarball_sha256, "");
|
||||||
assert!(!older.kept_upper);
|
assert!(!older.kept_upper);
|
||||||
|
assert_eq!(older.timings, None);
|
||||||
|
|
||||||
// A foreign schema is not discoverable.
|
// A foreign schema is not discoverable.
|
||||||
let mut foreign = manifest.clone();
|
let mut foreign = manifest.clone();
|
||||||
@@ -2308,8 +2391,9 @@ mod tests {
|
|||||||
assert!(open_from_dir(base.path().to_path_buf()).is_none());
|
assert!(open_from_dir(base.path().to_path_buf()).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `pkh deb list` renders a table with the fork lineage and the
|
/// `pkh deb list` renders a table with the fork lineage, the
|
||||||
/// upper-kept state; the empty case explains itself.
|
/// upper-kept state and the recorded build durations; the empty
|
||||||
|
/// case explains itself.
|
||||||
#[test]
|
#[test]
|
||||||
fn session_list_rendering() {
|
fn session_list_rendering() {
|
||||||
let rows = vec![
|
let rows = vec![
|
||||||
@@ -2321,6 +2405,8 @@ mod tests {
|
|||||||
base: Some("20260926T140000".to_string()),
|
base: Some("20260926T140000".to_string()),
|
||||||
upper: true,
|
upper: true,
|
||||||
outcome: OUTCOME_FAILED.to_string(),
|
outcome: OUTCOME_FAILED.to_string(),
|
||||||
|
deps: "2m05s".to_string(),
|
||||||
|
build: "2h30m".to_string(),
|
||||||
age: "2h".to_string(),
|
age: "2h".to_string(),
|
||||||
},
|
},
|
||||||
SessionListRow {
|
SessionListRow {
|
||||||
@@ -2331,6 +2417,8 @@ mod tests {
|
|||||||
base: None,
|
base: None,
|
||||||
upper: false,
|
upper: false,
|
||||||
outcome: OUTCOME_SUCCESS.to_string(),
|
outcome: OUTCOME_SUCCESS.to_string(),
|
||||||
|
deps: "-".to_string(),
|
||||||
|
build: "-".to_string(),
|
||||||
age: "1d".to_string(),
|
age: "1d".to_string(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -2345,6 +2433,11 @@ mod tests {
|
|||||||
// record's dash.
|
// record's dash.
|
||||||
assert!(rendered.contains("20260926T140000"), "{rendered}");
|
assert!(rendered.contains("20260926T140000"), "{rendered}");
|
||||||
assert!(rendered.contains("UPPER"), "{rendered}");
|
assert!(rendered.contains("UPPER"), "{rendered}");
|
||||||
|
// The timing columns: both durations of the timed record, the
|
||||||
|
// dashes of the record without any.
|
||||||
|
assert!(rendered.contains("DEPS"), "{rendered}");
|
||||||
|
assert!(rendered.contains("2m05s"), "{rendered}");
|
||||||
|
assert!(rendered.contains("2h30m"), "{rendered}");
|
||||||
assert!(
|
assert!(
|
||||||
rendered.contains("Fork with: pkh deb --incremental [<id>]"),
|
rendered.contains("Fork with: pkh deb --incremental [<id>]"),
|
||||||
"{rendered}"
|
"{rendered}"
|
||||||
@@ -2354,6 +2447,43 @@ mod tests {
|
|||||||
assert!(empty.contains("No build records recorded"), "{empty}");
|
assert!(empty.contains("No build records recorded"), "{empty}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The recorded durations round-trip through the manifest and the
|
||||||
|
/// list rows, and unmeasured builds record none: the accumulator
|
||||||
|
/// sums both totals, an empty one is not recorded, and
|
||||||
|
/// `pkh deb list` formats the seconds into stable units.
|
||||||
|
#[test]
|
||||||
|
fn timings_accumulate_render_and_persist() {
|
||||||
|
let mut timings = BuildTimings::default();
|
||||||
|
assert!(timings.is_empty());
|
||||||
|
timings.add_deps(Duration::from_millis(1500));
|
||||||
|
timings.add_deps(Duration::from_secs(3));
|
||||||
|
timings.add_build(Duration::from_secs(65));
|
||||||
|
assert_eq!(timings.deps_secs, 4);
|
||||||
|
assert_eq!(timings.build_secs, 65);
|
||||||
|
assert!(!timings.is_empty());
|
||||||
|
|
||||||
|
let (root, _tarball) = sessions_fixture();
|
||||||
|
let identity = test_identity();
|
||||||
|
let host_tree = root.path().join("tree");
|
||||||
|
let record = new_record_in(root.path(), &host_tree, &identity, "1.0").unwrap();
|
||||||
|
assert_eq!(record.manifest().timings, None);
|
||||||
|
|
||||||
|
record.set_timings(timings);
|
||||||
|
let stored = record
|
||||||
|
.manifest()
|
||||||
|
.timings
|
||||||
|
.expect("set_timings must record the durations");
|
||||||
|
assert_eq!(stored, timings);
|
||||||
|
let reloaded = open_from_dir(record.root().to_path_buf()).unwrap();
|
||||||
|
assert_eq!(reloaded.manifest().timings, Some(timings));
|
||||||
|
|
||||||
|
assert_eq!(format_duration(0), "0s");
|
||||||
|
assert_eq!(format_duration(45), "45s");
|
||||||
|
assert_eq!(format_duration(125), "2m05s");
|
||||||
|
assert_eq!(format_duration(5400), "1h30m");
|
||||||
|
assert_eq!(format_duration(277_200), "3d05h");
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Record layout, fork gate, retention, sweep and logs
|
// Record layout, fork gate, retention, sweep and logs
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
+1
-1
@@ -383,7 +383,7 @@ fn main() {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("list")
|
Command::new("list")
|
||||||
.about("List the build records recorded from this tree")
|
.about("List the build records recorded from this tree")
|
||||||
.long_about("List the build records recorded from this tree (plain runs too, as metadata records), with their outcome, version, fork lineage, whether their staged tree is kept, and the id to pass to 'pkh deb --incremental <id>'.\nRead-only: never builds or removes anything (at most five staged trees per target are kept, 'pkh prune' removes records older than a week)."),
|
.long_about("List the build records recorded from this tree (plain runs too, as metadata records), with their outcome, version, fork lineage, whether their staged tree is kept, how long the dependency installation and the package build took, and the id to pass to 'pkh deb --incremental <id>'.\nRead-only: never builds or removes anything (at most five staged trees per target are kept, 'pkh prune' removes records older than a week)."),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("log")
|
Command::new("log")
|
||||||
|
|||||||
+9
-1
@@ -561,7 +561,15 @@ async fn configure_testbed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !opts.inject.is_empty() {
|
if !opts.inject.is_empty() {
|
||||||
crate::deb::local::install_injected_packages(&opts.inject, env, ctx.clone(), view, &None)?;
|
// Testbed setup is not a timed build: the durations go nowhere.
|
||||||
|
crate::deb::local::install_injected_packages(
|
||||||
|
&opts.inject,
|
||||||
|
env,
|
||||||
|
ctx.clone(),
|
||||||
|
view,
|
||||||
|
&None,
|
||||||
|
&mut crate::deb::session::BuildTimings::default(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user