diff --git a/src/con_duct/_formatter.py b/src/con_duct/_formatter.py index c804dc2b..8bf2713e 100644 --- a/src/con_duct/_formatter.py +++ b/src/con_duct/_formatter.py @@ -9,11 +9,9 @@ lgr = logging.getLogger("con-duct") -# Decimal (SI) byte units, single source of truth for byte humanization in -# duct. Used by SummaryFormatter.naturalsize for run-summary output and by -# the plot axis formatter for tick labels, so a "kB" means the same thing -# in both places. A future opt-in could add a 1024-base + IEC-suffix -# variant (KiB/MiB/GiB/...) and let callers pick. +# Decimal (SI) byte units shared by SummaryFormatter.naturalsize (run +# summary) and the plot axis formatter, so "kB" means the same thing in +# both places. FILESIZE_UNITS: list[tuple[str, int]] = [ ("B", 1), ("kB", 1000**1), diff --git a/src/con_duct/_utils.py b/src/con_duct/_utils.py index 80c57773..90693cf1 100644 --- a/src/con_duct/_utils.py +++ b/src/con_duct/_utils.py @@ -79,34 +79,27 @@ def pdcpu_from_pcpu( """Delta-corrected %CPU between two ps samples of the same pid. Inverts the procps identity ``pcpu = cputime / etime * 100`` to - recover cputime at each sample, takes the cputime delta, and - divides by the elapsed interval. The ``/100`` and ``*100`` - cancel, so the result is in the same units as ``pcpu``. + recover cputime at each sample, deltas it, and divides by the + elapsed interval. Result is in the same units as ``pcpu``. Linux-only: assumes ``pcpu`` is the cumulative ``cputime/etime`` ratio. Invalid on Darwin (decayed EWMA). - Identity is the caller's responsibility: invoke ``is_same_pid`` - first. If the inputs satisfy that precondition, ``Δetime`` is - positive and the math is well-defined. + Identity is the caller's responsibility -- invoke ``is_same_pid`` + first. :param prev_pcpu: %CPU from the earlier sample. :param prev_etimes: elapsed seconds at the earlier sample. :param curr_pcpu: %CPU from the later sample. :param curr_etimes: elapsed seconds at the later sample. :returns: delta-corrected %CPU over the interval, or ``None`` - in two "no measurement" cases: - - - ``Δetime <= 0`` -- defensive guard for callers that - skipped ``is_same_pid``; covers sub-quantum and obvious - pid-reuse. - - Computed ``pdcpu < 0`` -- aggregation-timing artifact. - When duct's per-pid ``pcpu`` is max-across-samples while - ``etime`` is from the last sample, a spike-then-idle - pattern earlier in the run inflates ``prev_pcpu * - prev_etimes`` enough that the cputime "delta" goes - negative. The pid is the same; the math is just noisy. - A small minus dip is honestly null, not zero. + when no measurement is recoverable: + + - ``Δetime <= 0`` -- defensive guard; covers sub-quantum and + obvious pid-reuse for callers that skipped ``is_same_pid``. + - Computed ``pdcpu < 0`` -- aggregation-timing artifact from + duct's max-pcpu / end-etime mismatch on spike-then-idle + patterns. Honestly null, not zero. """ interval = curr_etimes - prev_etimes if interval <= 0: diff --git a/src/con_duct/plot.py b/src/con_duct/plot.py index b3ebf756..b13fa58d 100644 --- a/src/con_duct/plot.py +++ b/src/con_duct/plot.py @@ -174,10 +174,6 @@ def _envelopes( omitted entirely rather than reported as zero -- a missing measurement is not the same as zero load. - The grid is the union of (kept pids') elapsed values; the alternative - of "every entry timestamp regardless of who appeared" would only add - grid points that are missing measurements anyway. - :returns: ``(grid_xs, max_ys, sum_ys)`` aligned, sorted by ``grid_xs``. """ grid: Dict[float, List[float]] = {} @@ -337,22 +333,12 @@ def matplotlib_plot(args: argparse.Namespace) -> int: alpha=0.4, ) - # Envelopes: max-across-pids (lower bound) solid, upper bound dashed. - # If some pid was at 50%, the total was at least 50% -- max-of-pids is - # a true lower bound on the concurrent total in both metrics. - # - # Upper bounds: for both rss and (in ps-pcpu mode) cpu, we use duct's - # per-record ``totals[field]`` -- the peak concurrent value observed - # at any single sub-sample within the report interval. This is a - # tight upper bound under "observed samples only" framing, and avoids - # the phantom-coexistence inflation of summing per-pid peaks (pids - # whose peaks fell in different sub-samples within the interval would - # otherwise both contribute their peak). - # - # In ps-cpu-timepoint mode there is no per-record ``totals.pdcpu`` -- - # pdcpu is computed at plot time -- so we fall back to summing per-pid - # pdcpu. The negative-pdcpu clamp filters the worst aggregation-timing - # artifacts; remaining looseness is a known, accepted caveat. + # Solid = max-across-pids (lower bound on the concurrent total). + # Dashed = upper bound: per-record ``totals[field]`` for rss and + # ps-pcpu cpu; in ps-cpu-timepoint mode there is no + # ``totals.pdcpu`` (computed at plot time), so fall back to + # sum-across-pids. See docs/resource-statistics.md for the + # phantom-coexistence rationale. pcpu_xs, pcpu_max, pcpu_sum = _envelopes(pid_series, "cpu") if pcpu_xs: ax.plot( # type: ignore[call-arg] @@ -389,10 +375,7 @@ def matplotlib_plot(args: argparse.Namespace) -> int: ax.set_ylabel(f"{args.cpu} (%)") ax2.set_ylabel("rss") if pid_series: - # Two legends, color-agnostic linestyle key on the right and metric - # color key on the left. Linestyle entries are listed in the order - # a viewer's eye scans the chart: upper bound (the high dashed line), - # lower bound (the solid line below it), per-pid (dotted cloud). + # Two legends: linestyle key on the right, metric color key on the left. style_handles = [ Line2D( [0],