ktstr/ctprof_compare/report/
mod.rs

1//! `write_diff` — primary diff renderer for [`CtprofDiff`].
2//!
3//! Multi-section emitter for the `ctprof compare` output.
4//! Each section emits through a dedicated submodule so the
5//! orchestrator below stays a slim sequencer:
6//!
7//! - [`primary`] — `## Primary metrics` table (53 non-taskstats
8//!   rows + 34 taskstats rows). Three layouts based on
9//!   [`GroupBy`]: hierarchical cgroup + pcomm + comm tree
10//!   under [`GroupBy::All`], one-table-per-parent under
11//!   [`GroupBy::Cgroup`], flat under [`GroupBy::Pcomm`] /
12//!   [`GroupBy::Comm`] / [`GroupBy::CommExact`].
13//! - [`derived`] — `## Derived metrics` table; reuses the
14//!   primary tree builder for [`GroupBy::All`] so primary and
15//!   derived rows share heading order.
16//! - [`smaps`] — `## smaps_rollup` per-process memory
17//!   compare; suppressed when neither side carries data.
18//! - [`cgroup`] — five sub-tables that exist only under
19//!   [`GroupBy::Cgroup`]: cgroup-stats, limits/knobs,
20//!   memory.stat, memory.events, per-cgroup PSI.
21//! - [`host_psi`] — `## Host pressure / <resource>` tables;
22//!   independent of group_by because host pressure is the
23//!   primary scheduler-health signal.
24//! - [`sched_ext`] — `## sched_ext` global sysfs compare.
25//! - [`orphans`] — fudged-cgroup pairs (rendered first so
26//!   they don't get buried), only-baseline list,
27//!   only-candidate list.
28//!
29//! Each emitter is gated on
30//! [`DisplayOptions::is_section_enabled`] so `--sections`
31//! always wins over the data-availability suppression
32//! heuristic, and the column widths used by [`primary`],
33//! [`derived`], and [`smaps`] are measured ONCE across all
34//! data rows (primary + derived) so every table in every
35//! section shares column widths under [`GroupBy::All`].
36//!
37//! Cell rendering is delegated to [`super::render`]; metric
38//! lookup to [`super::metrics`]; column resolution to
39//! [`super::columns`]; the table builder lives on
40//! [`super::DisplayOptions`].
41
42mod cgroup;
43mod derived;
44mod host_psi;
45mod orphans;
46mod primary;
47mod sched_ext;
48mod smaps;
49
50use std::fmt;
51use std::path::Path;
52
53use super::columns::Column;
54use super::diff_types::CtprofDiff;
55use super::options::GroupBy;
56use super::render::{colored_header_with_sort, render_derived_row_cells, render_diff_row_cells};
57use super::runner::DisplayOptions;
58
59/// Render [`CtprofDiff`] into `w`. The formatter layer lives
60/// here so tests can inspect exactly what `print_diff` would
61/// emit without shelling through stdout capture. Write errors
62/// propagate as [`std::fmt::Error`] — callers that write into an
63/// infallible sink (`String`) can unwrap or ignore.
64///
65/// `display` controls per-row column layout, terminal-width
66/// wrapping, and per-section filtering: see `DisplayFormat` /
67/// [`Column`] / `Section` / [`DisplayOptions`] for the
68/// resolution rules. Each sub-emitter is gated on
69/// [`DisplayOptions::is_section_enabled`] before its
70/// data-availability check, so `--sections` always wins over
71/// the per-section zero-suppression heuristic.
72pub fn write_diff<W: fmt::Write>(
73    w: &mut W,
74    diff: &CtprofDiff,
75    baseline_path: &Path,
76    candidate_path: &Path,
77    group_by: GroupBy,
78    display: &DisplayOptions,
79) -> fmt::Result {
80    let group_header = match group_by {
81        GroupBy::Pcomm => "pcomm",
82        GroupBy::Cgroup => "cgroup",
83        GroupBy::Comm => "comm-pattern",
84        GroupBy::CommExact => "comm",
85        GroupBy::All => "comm",
86    };
87
88    let mut columns = display.resolved_compare_columns();
89    let has_sort_col = diff.rows.first().is_some_and(|r| r.sort_by_cell.is_some());
90    if has_sort_col {
91        columns.push(Column::SortBy);
92    }
93
94    // Compute column widths from ALL data rows (primary + derived)
95    // so every table in every section shares the same widths.
96    // Heading rows are constrained to these widths via
97    // new_constrained_table so they can't inflate columns.
98    let global_max_widths: Vec<u16> = if group_by == GroupBy::All {
99        compute_global_max_widths(diff, &columns, display)
100    } else {
101        Vec::new()
102    };
103
104    primary::write_primary_section(
105        w,
106        diff,
107        group_by,
108        group_header,
109        &columns,
110        display,
111        &global_max_widths,
112    )?;
113    derived::write_derived_section(
114        w,
115        diff,
116        group_by,
117        group_header,
118        &columns,
119        display,
120        &global_max_widths,
121    )?;
122    smaps::write_smaps_section(w, diff, group_by, &columns, display, &global_max_widths)?;
123    cgroup::write_cgroup_sections(w, diff, group_by, display)?;
124    host_psi::write_host_psi_section(w, diff, display)?;
125    sched_ext::write_sched_ext_section(w, diff, display)?;
126    orphans::write_orphans_section(w, diff, baseline_path, candidate_path, group_by)?;
127
128    Ok(())
129}
130
131/// Two-pass column-width measurement: build a throwaway table,
132/// add every primary + derived data row through the same cell
133/// path the real renderers use, and read back per-column max
134/// widths. Heading rows are constrained to these widths in the
135/// real tables so they can't inflate columns.
136fn compute_global_max_widths(
137    diff: &CtprofDiff,
138    columns: &[Column],
139    display: &DisplayOptions,
140) -> Vec<u16> {
141    let mut measure = display.new_table();
142    measure.set_header(colored_header_with_sort(
143        columns,
144        "comm",
145        diff.sort_metric_name,
146    ));
147    for row in &diff.rows {
148        let mut cells = render_diff_row_cells(row, columns);
149        if let Some(pos) = columns.iter().position(|c| *c == Column::Group) {
150            let comm = row.group_key.splitn(3, '\x00').nth(2).unwrap_or("");
151            cells[pos] = comm.to_string();
152        }
153        measure.add_row(cells);
154    }
155    for row in &diff.derived_rows {
156        let mut cells = render_derived_row_cells(row, columns);
157        if let Some(pos) = columns.iter().position(|c| *c == Column::Group) {
158            let comm = row.group_key.splitn(3, '\x00').nth(2).unwrap_or("");
159            cells[pos] = comm.to_string();
160        }
161        measure.add_row(cells);
162    }
163    measure.column_max_content_widths()
164}