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:
+183
-9
@@ -68,6 +68,46 @@ impl Phase {
|
||||
Phase::RetrievingArtifacts => "Retrieving artifacts",
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable machine name of the phase, as recorded in a build
|
||||
/// record's profile (`pkh deb profile`); unlike [`Self::label`]
|
||||
/// this is a storage key, never reworded.
|
||||
pub fn key(&self) -> &'static str {
|
||||
match self {
|
||||
Phase::PreparingChroot => "preparing_chroot",
|
||||
Phase::ExtractingChroot => "extracting_chroot",
|
||||
Phase::FinalizingChroot => "finalizing_chroot",
|
||||
Phase::UpdatingPackageLists => "updating_package_lists",
|
||||
Phase::InstallingEssentials => "installing_essentials",
|
||||
Phase::ApplyingPatches => "applying_patches",
|
||||
Phase::InjectingPackages => "injecting_packages",
|
||||
Phase::InstallingBuildDeps => "installing_build_deps",
|
||||
Phase::Building => "building",
|
||||
Phase::ProducingBinaries => "producing_binaries",
|
||||
Phase::RetrievingArtifacts => "retrieving_artifacts",
|
||||
}
|
||||
}
|
||||
|
||||
/// The phase `key` names, when it is one of the known machine
|
||||
/// names; a key written by an older pkh whose phase has since been
|
||||
/// renamed renders as its raw key.
|
||||
pub fn from_key(key: &str) -> Option<Phase> {
|
||||
let phase = match key {
|
||||
"preparing_chroot" => Phase::PreparingChroot,
|
||||
"extracting_chroot" => Phase::ExtractingChroot,
|
||||
"finalizing_chroot" => Phase::FinalizingChroot,
|
||||
"updating_package_lists" => Phase::UpdatingPackageLists,
|
||||
"installing_essentials" => Phase::InstallingEssentials,
|
||||
"applying_patches" => Phase::ApplyingPatches,
|
||||
"injecting_packages" => Phase::InjectingPackages,
|
||||
"installing_build_deps" => Phase::InstallingBuildDeps,
|
||||
"building" => Phase::Building,
|
||||
"producing_binaries" => Phase::ProducingBinaries,
|
||||
"retrieving_artifacts" => Phase::RetrievingArtifacts,
|
||||
_ => return None,
|
||||
};
|
||||
Some(phase)
|
||||
}
|
||||
}
|
||||
|
||||
/// Default line classifier rewriting a phase's subprocess output
|
||||
@@ -87,8 +127,57 @@ fn default_classifier(phase: Phase) -> Box<dyn Classifier> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter `phase` on the view with its default line classifier
|
||||
pub(crate) fn enter_phase(view: &dyn BuildView, phase: Phase) {
|
||||
/// Records the wall-clock duration of every [`Phase`] the build flow
|
||||
/// announces: one profile entry per executed phase, in execution
|
||||
/// order. A phase closes when the next one is entered or at
|
||||
/// [`BuildProfiler::finish`] — so a failed build's profile ends at the
|
||||
/// phase the error propagated out of, its entry covering the phase
|
||||
/// until the failure. Conditional phases that never ran (the chroot
|
||||
/// tarball was cached, the device nodes were ready) record nothing.
|
||||
pub(crate) struct BuildProfiler {
|
||||
/// The phase being timed, since when.
|
||||
open: Option<(Phase, std::time::Instant)>,
|
||||
/// Closed phases, in execution order.
|
||||
phases: Vec<session::PhaseTiming>,
|
||||
}
|
||||
|
||||
impl BuildProfiler {
|
||||
/// A profiler with nothing recorded yet.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
open: None,
|
||||
phases: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the phase being timed (if any) and open `phase`.
|
||||
pub(crate) fn enter(&mut self, phase: Phase) {
|
||||
self.close_open();
|
||||
self.open = Some((phase, std::time::Instant::now()));
|
||||
}
|
||||
|
||||
/// Close the open phase and return the recorded profile.
|
||||
pub(crate) fn finish(&mut self) -> session::BuildProfile {
|
||||
self.close_open();
|
||||
session::BuildProfile {
|
||||
phases: std::mem::take(&mut self.phases),
|
||||
}
|
||||
}
|
||||
|
||||
fn close_open(&mut self) {
|
||||
if let Some((phase, started)) = self.open.take() {
|
||||
self.phases.push(session::PhaseTiming {
|
||||
phase: phase.key().to_string(),
|
||||
millis: started.elapsed().as_millis() as u64,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter `phase` on the view with its default line classifier, timing
|
||||
/// it into `profiler`
|
||||
pub(crate) fn enter_phase(view: &dyn BuildView, phase: Phase, profiler: &mut BuildProfiler) {
|
||||
profiler.enter(phase);
|
||||
view.phase(phase.label(), default_classifier(phase));
|
||||
}
|
||||
|
||||
@@ -451,6 +540,10 @@ async fn build_binary_package_impl(
|
||||
// The shared ccache of this target — skipped with --no-ccache.
|
||||
let ccache_dir = (session_enabled && ccache).then(|| session::ccache_dir(&identity));
|
||||
let mirror_arg = mirror.as_deref();
|
||||
// The per-phase wall-clock profile of this run: fed by every
|
||||
// enter_phase call from the chroot bootstrap to the artifact
|
||||
// retrieval, recorded on the record at the end of the run.
|
||||
let mut profiler = BuildProfiler::new();
|
||||
let mut guard = if let Some(s) = session.clone() {
|
||||
Some(
|
||||
ephemeral::EphemeralContextGuard::new_for_session(
|
||||
@@ -462,6 +555,7 @@ async fn build_binary_package_impl(
|
||||
view,
|
||||
keep_upper,
|
||||
ccache_dir.as_deref(),
|
||||
&mut profiler,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -473,6 +567,7 @@ async fn build_binary_package_impl(
|
||||
mirror_arg,
|
||||
base_ctx.clone(),
|
||||
view,
|
||||
&mut profiler,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
@@ -611,6 +706,7 @@ async fn build_binary_package_impl(
|
||||
// ephemeral chroot) can carry the on-demand build user.
|
||||
guard.is_some().then_some(local::BUILD_USER),
|
||||
&mut timings,
|
||||
&mut profiler,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -618,7 +714,7 @@ async fn build_binary_package_impl(
|
||||
|
||||
// Retrieve the produced artifacts (binary packages plus the upload
|
||||
// metadata) to the parent directory.
|
||||
enter_phase(view, Phase::RetrievingArtifacts);
|
||||
enter_phase(view, Phase::RetrievingArtifacts, &mut profiler);
|
||||
let total_debs = remote_files.len();
|
||||
|
||||
let mut artifacts = Vec::with_capacity(total_debs);
|
||||
@@ -658,12 +754,17 @@ async fn build_binary_package_impl(
|
||||
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);
|
||||
// failed build's partial timings and phase profile are data too.
|
||||
// Nothing measured (the run ended before any timed command ran)
|
||||
// records nothing.
|
||||
if let Some(s) = &session {
|
||||
let profile = profiler.finish();
|
||||
if !profile.is_empty() {
|
||||
s.set_profile(profile);
|
||||
}
|
||||
if !timings.is_empty() {
|
||||
s.set_timings(timings);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
@@ -1032,6 +1133,59 @@ mod tests {
|
||||
assert!(!is_archive_source_pinned(mirror, None));
|
||||
}
|
||||
|
||||
/// Every phase's machine key round-trips through `Phase::from_key`:
|
||||
/// the profile renderer relies on it to show a recorded entry as
|
||||
/// the phase's human label, and a key/from_key drift would
|
||||
/// downgrade the row to the raw key.
|
||||
#[test]
|
||||
fn phase_keys_round_trip() {
|
||||
for phase in [
|
||||
Phase::PreparingChroot,
|
||||
Phase::ExtractingChroot,
|
||||
Phase::FinalizingChroot,
|
||||
Phase::UpdatingPackageLists,
|
||||
Phase::InstallingEssentials,
|
||||
Phase::ApplyingPatches,
|
||||
Phase::InjectingPackages,
|
||||
Phase::InstallingBuildDeps,
|
||||
Phase::Building,
|
||||
Phase::ProducingBinaries,
|
||||
Phase::RetrievingArtifacts,
|
||||
] {
|
||||
assert_eq!(Phase::from_key(phase.key()), Some(phase), "{}", phase.key());
|
||||
assert_ne!(phase.key(), "", "{}", phase.key());
|
||||
}
|
||||
assert_eq!(Phase::from_key("nonesuch"), None);
|
||||
}
|
||||
|
||||
/// The profiler closes each phase when the next opens and the last
|
||||
/// one at `finish`, recording entries in execution order; an empty
|
||||
/// run records nothing (the record must not claim an empty
|
||||
/// profile), and a phase never entered leaves no entry.
|
||||
#[test]
|
||||
fn profiler_records_phases_in_execution_order() {
|
||||
let mut profiler = BuildProfiler::new();
|
||||
assert!(profiler.finish().is_empty());
|
||||
|
||||
let mut profiler = BuildProfiler::new();
|
||||
profiler.enter(Phase::PreparingChroot);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
profiler.enter(Phase::UpdatingPackageLists);
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
profiler.enter(Phase::Building);
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
let profile = profiler.finish();
|
||||
|
||||
let phases: Vec<&str> = profile.phases.iter().map(|p| p.phase.as_str()).collect();
|
||||
assert_eq!(
|
||||
phases,
|
||||
vec!["preparing_chroot", "updating_package_lists", "building"]
|
||||
);
|
||||
assert!(profile.total_millis() >= 9, "{profile:?}");
|
||||
// finish() closes the last phase: nothing stays open afterwards.
|
||||
assert!(profiler.finish().is_empty());
|
||||
}
|
||||
|
||||
/// An unshare context mapped over `chroot_root`, parented on a local
|
||||
/// context like the ephemeral build contexts are: exists/list_files/
|
||||
/// is_dir answer through the path mapping, no namespace privileges
|
||||
@@ -1183,6 +1337,26 @@ mod tests {
|
||||
.expect("Cannot build binary package (deb)");
|
||||
log::info!("Successfully built binary package");
|
||||
|
||||
// The run's record carries the per-phase timing profile: the
|
||||
// metadata-only record of a plain run keeps the manifest too.
|
||||
// (The record's host tree is the package directory the build
|
||||
// was pointed at.)
|
||||
let tree = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
|
||||
let record =
|
||||
crate::deb::session::find_record(&crate::deb::session::sessions_root(), &tree, None)
|
||||
.expect("the build must leave a record for this tree");
|
||||
let profile = record
|
||||
.manifest()
|
||||
.profile
|
||||
.expect("the record must carry a phase profile");
|
||||
assert!(
|
||||
profile
|
||||
.phases
|
||||
.iter()
|
||||
.any(|p| p.phase == "building" && p.millis > 0),
|
||||
"{profile:?}"
|
||||
);
|
||||
|
||||
// Check that the .deb files are present
|
||||
let parent_dir = cwd.parent().expect("Cannot find parent directory");
|
||||
let deb_files = std::fs::read_dir(parent_dir)
|
||||
|
||||
Reference in New Issue
Block a user