proj/portage:master commit in: lib/portage/tests/emerge/, lib/_emerge/
"Matt Turner" <[email protected]>
| Newsgroups | gmane.linux.gentoo.cvs |
|---|---|
| Message-ID | <1786149767.023f12350ae30f85fe274bf8394ab821cfcba276.mattst88@gentoo> |
commit: 023f12350ae30f85fe274bf8394ab821cfcba276
Author: Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Jun 14 18:41:17 2026 +0000
Commit: Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Sat Aug 8 00:42:47 2026 +0000
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=023f1235
Report cgroup resource usage per build
The Scheduler creates a CgroupManager when FEATURES="cgroup" is enabled
and reads each build's leaf by cpv. Live accounting (cpu time, current
and peak memory, I/O) is included per task in the live observability
snapshot. On build exit a one-line summary is written to the build log
and the leaf is removed; remaining leaves and the base cgroup are
cleaned up when emerge exits.
Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>
lib/_emerge/Scheduler.py | 59 +++++++++++++++++++++++---
lib/_emerge/_observability.py | 5 +++
lib/portage/tests/emerge/test_observability.py | 18 ++++++++
3 files changed, 75 insertions(+), 7 deletions(-)
diff --git a/lib/_emerge/Scheduler.py b/lib/_emerge/Scheduler.py
index e546caadc..82bee6857 100644
--- a/lib/_emerge/Scheduler.py
+++ b/lib/_emerge/Scheduler.py
@@ -24,15 +24,17 @@ from portage.output import colorize, create_color_func, red
bad = create_color_func("BAD")
from portage._sets import SETPREFIX
from portage._sets.base import InternalPackageSet
-from portage.package.ebuild.digestcheck import digestcheck
-from portage.package.ebuild.digestgen import digestgen
-from portage.package.ebuild.doebuild import _check_temp_dir, _prepare_self_update
-from portage.package.ebuild.prepare_build_dirs import prepare_build_dirs
from portage.util import ensure_dirs, writemsg, writemsg_level
-from portage.util._async.SchedulerInterface import SchedulerInterface
+from portage.util.cgroup import CgroupManager, DEFAULT_CGROUP_ROOT
from portage.util.futures import asyncio
+from portage.util.human_readable import bytes_to_human
from portage.util.path import first_existing
from portage.util.SlotObject import SlotObject
+from portage.util._async.SchedulerInterface import SchedulerInterface
+from portage.package.ebuild.digestcheck import digestcheck
+from portage.package.ebuild.digestgen import digestgen
+from portage.package.ebuild.doebuild import _check_temp_dir, _prepare_self_update
+from portage.package.ebuild.prepare_build_dirs import prepare_build_dirs
import _emerge
from _emerge._find_deep_system_runtime_deps import _find_deep_system_runtime_deps
@@ -229,6 +231,18 @@ class Scheduler(PollScheduler):
xterm_titles=("notitles" not in settings.features)
)
self._observability = ObservabilityMonitor(self)
+ self._cgroup = None
+ if "cgroup" in settings.features:
+ cg = CgroupManager(
+ settings.get("PORTAGE_CGROUP_ROOT", DEFAULT_CGROUP_ROOT),
+ verbose="--verbose" in myopts,
+ )
+ if cg.setup():
+ self._cgroup = cg
+ settings.unlock()
+ settings["PORTAGE_CGROUP_ROOT"] = cg.emerge_root
+ settings.backup_changes("PORTAGE_CGROUP_ROOT")
+ settings.lock()
self._max_load = myopts.get("--load-average")
max_jobs = myopts.get("--jobs", 1)
self._set_max_jobs(max_jobs)
@@ -405,6 +419,36 @@ class Scheduler(PollScheduler):
"""
self._observability.note_phase(cpv, phase)
+ def _cgroup_finish(self, build):
+ """Log the final cgroup resource summary for a build and remove it."""
+ if self._cgroup is None:
+ return
+ cpv = build.pkg.cpv
+ stats = self._cgroup.read_stats(cpv)
+ if stats:
+ parts = []
+ if "cpu_usec" in stats:
+ parts.append(f"cpu {stats['cpu_usec'] / 1e6:.1f}s")
+ if "mem_peak" in stats:
+ parts.append(f"peak-mem {bytes_to_human(stats['mem_peak'])}")
+ if "mem_swap_peak" in stats and stats["mem_swap_peak"]:
+ parts.append(f"peak-swap {bytes_to_human(stats['mem_swap_peak'])}")
+ if "mem_zswap_current" in stats and stats["mem_zswap_current"]:
+ parts.append(f"zswap {bytes_to_human(stats['mem_zswap_current'])}")
+ if "io_read_bytes" in stats:
+ parts.append(
+ "io {} / {} r/w".format(
+ bytes_to_human(stats["io_read_bytes"]),
+ bytes_to_human(stats.get("io_write_bytes", 0)),
+ )
+ )
+ if parts:
+ msg = f"=== Resource usage for {cpv}: {', '.join(parts)}"
+ log_path = build.settings.get("PORTAGE_LOG_FILE")
+ self._sched_iface.output(f"{msg}\n", log_path=log_path)
+ self._logger.log(f" {msg}")
+ self._cgroup.destroy(cpv)
+
def _init_graph(self, graph_config):
"""
Initialization structures used for dependency calculations
@@ -1576,6 +1620,7 @@ class Scheduler(PollScheduler):
def _build_exit(self, build):
self._running_tasks.pop(id(build), None)
self._observability.note_task_finished(build)
+ self._cgroup_finish(build)
self._release_job_token(id(build))
if build.returncode == os.EX_OK and self._terminated_tasks:
# We've been interrupted, so we won't
@@ -1737,6 +1782,8 @@ class Scheduler(PollScheduler):
finally:
self._main_loop_cleanup()
self._observability.close()
+ if self._cgroup is not None:
+ self._cgroup.close()
portage.locks._quiet = False
portage.elog.remove_listener(self._elog_listener)
if display_callback.handle is not None:
@@ -1955,8 +2002,6 @@ class Scheduler(PollScheduler):
if actual_free_bytes < required_free_bytes:
if not self._warned_tmpdir_free_space:
- from portage.util.human_readable import bytes_to_human
-
actual_free_bytes_hr = bytes_to_human(actual_free_bytes)
required_free_bytes_hr = bytes_to_human(
required_free_bytes
diff --git a/lib/_emerge/_observability.py b/lib/_emerge/_observability.py
index bf1c99aef..ff75405a0 100644
--- a/lib/_emerge/_observability.py
+++ b/lib/_emerge/_observability.py
@@ -70,6 +70,7 @@ def build_snapshot(monitor):
scheduler = monitor._scheduler
now = time.time()
+ cgroup = getattr(scheduler, "_cgroup", None)
merge_wait_ids = {id(t) for t in getattr(scheduler, "_merge_wait_queue", ())}
tasks = []
@@ -114,6 +115,10 @@ def build_snapshot(monitor):
"start_time": start,
"elapsed": elapsed,
}
+ if cgroup is not None:
+ res = cgroup.read_stats(str(pkg.cpv))
+ if res:
+ entry["resources"] = res
tasks.append(entry)
tasks.sort(key=lambda t: (t["start_time"] is None, t["start_time"] or 0))
diff --git a/lib/portage/tests/emerge/test_observability.py b/lib/portage/tests/emerge/test_observability.py
index a21f80730..152fbe7b5 100644
--- a/lib/portage/tests/emerge/test_observability.py
+++ b/lib/portage/tests/emerge/test_observability.py
@@ -62,6 +62,7 @@ def _make_scheduler(features=("observability",), eprefix="", tasks=None):
_task_queues=SimpleNamespace(merge=[]),
_status_display=SimpleNamespace(curval=1, maxval=5),
_event_loop=None,
+ _cgroup=None,
)
@@ -93,6 +94,23 @@ class ObservabilitySnapshotTestCase(TestCase):
self.assertEqual(by_cpv["dev-libs/foo-1.2"]["kind"], "build")
self.assertEqual(by_cpv["sys-apps/bar-3"]["kind"], "merge")
+ def test_snapshot_includes_cgroup_resources(self):
+ build = EbuildBuild(_Pkg("dev-libs/foo-1.2"), pid=7)
+ sched = _make_scheduler(tasks=[build])
+
+ class _Cg:
+ def read_stats(self, cpv):
+ return {"cpu_usec": 2_000_000, "mem_peak": 1234} if cpv else None
+
+ sched._cgroup = _Cg()
+ monitor = ObservabilityMonitor(sched)
+ monitor.note_task_started(build)
+
+ snap = build_snapshot(monitor)
+ res = snap["tasks"][0]["resources"]
+ self.assertEqual(res["cpu_usec"], 2_000_000)
+ self.assertEqual(res["mem_peak"], 1234)
+
def test_snapshot_marks_merge_wait(self):
# A merge sitting in the merge-wait queue is reported as waiting, with
# its phase surfaced as "merge-wait".