From 8542ed072db3f29e7514810f2742dec54d7c5cd6 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Thu, 20 Aug 2026 03:17:27 -0400 Subject: [PATCH] import Oracle_OSS pcp-6.3.7-8.0.1.el9_8.4 --- ...ohith-kumar-thummaluru-rocestat-pmda.patch | 1664 +++++++++++++++++ ...-rocestat-of-https-github.com-mohith.patch | 1093 +++++++++++ ...tional-metrics-added-for-mem.util-to.patch | 56 + ...dded-kreclaimable-and-hugtlb-metrics.patch | 170 ++ ...x-mpstat-showing-inconsistent-values.patch | 267 +++ .../1006-fix-broken-pipe-error-iostat.patch | 29 + .../1007-add-missing-numastat-metrics.patch | 220 +++ ...-add-numastat-support-for-mn-options.patch | 495 +++++ ...gesize-metric-value-from-bytes-to-KB.patch | 45 + ...fault-compression-changed-to-level-3.patch | 33 + ...8724866-fix-nfsclient-per-op-parsing.patch | 38 + ...duce-PCP-implementation-of-nfsiostat.patch | 575 ++++++ ...-fix-not-to-terminate-unauthorized-p.patch | 217 +++ ...-sort-option-to-allow-sorting-by-cpu.patch | 1278 +++++++++++++ ...-disk-option-for-archive-space-limit.patch | 486 +++++ ...-restore-backward-compatibility-with.patch | 328 ++++ ...70-adds-interval-option-in-nfsiostat.patch | 364 ++++ ...ics-and-adds-numastat-process-option.patch | 1286 +++++++++++++ SOURCES/pcp-6.3.7-CVE-2026-16524.patch | 269 +++ SOURCES/pcp-6.3.7-CVE-2026-16526.patch | 219 +++ SOURCES/pcp-6.3.7-CVE-2026-16527.patch | 206 ++ SOURCES/pcp-6.3.7-CVE-2026-16529.patch | 137 ++ SPECS/pcp.spec | 117 +- 23 files changed, 9589 insertions(+), 3 deletions(-) create mode 100644 SOURCES/1001-Merge-branch-mohith-kumar-thummaluru-rocestat-pmda.patch create mode 100644 SOURCES/1002-Merge-branch-pcp-rocestat-of-https-github.com-mohith.patch create mode 100644 SOURCES/1003-pcp-meminfo-additional-metrics-added-for-mem.util-to.patch create mode 100644 SOURCES/1004-meminfo-added-kreclaimable-and-hugtlb-metrics.patch create mode 100644 SOURCES/1005-fix-mpstat-showing-inconsistent-values.patch create mode 100644 SOURCES/1006-fix-broken-pipe-error-iostat.patch create mode 100644 SOURCES/1007-add-missing-numastat-metrics.patch create mode 100644 SOURCES/1008-add-numastat-support-for-mn-options.patch create mode 100644 SOURCES/1009-Fixes-higepagesize-metric-value-from-bytes-to-KB.patch create mode 100644 SOURCES/1010-xz-default-compression-changed-to-level-3.patch create mode 100644 SOURCES/1011-orabug38724866-fix-nfsclient-per-op-parsing.patch create mode 100644 SOURCES/1012-orabug38817068-Introduce-PCP-implementation-of-nfsiostat.patch create mode 100644 SOURCES/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch create mode 100644 SOURCES/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch create mode 100644 SOURCES/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch create mode 100644 SOURCES/1016-pcp-system-tools-restore-backward-compatibility-with.patch create mode 100644 SOURCES/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch create mode 100644 SOURCES/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch create mode 100644 SOURCES/pcp-6.3.7-CVE-2026-16524.patch create mode 100644 SOURCES/pcp-6.3.7-CVE-2026-16526.patch create mode 100644 SOURCES/pcp-6.3.7-CVE-2026-16527.patch create mode 100644 SOURCES/pcp-6.3.7-CVE-2026-16529.patch diff --git a/SOURCES/1001-Merge-branch-mohith-kumar-thummaluru-rocestat-pmda.patch b/SOURCES/1001-Merge-branch-mohith-kumar-thummaluru-rocestat-pmda.patch new file mode 100644 index 0000000..9cadcee --- /dev/null +++ b/SOURCES/1001-Merge-branch-mohith-kumar-thummaluru-rocestat-pmda.patch @@ -0,0 +1,1664 @@ +From ed8474266464ab5ae182c347ce93e0d4efe181c9 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Fri, 18 Apr 2025 11:30:35 +1000 +Subject: [PATCH 1/2] Merge branch 'mohith-kumar-thummaluru-rocestat-pmda' + +Orabug: 38109263 + +(cherry picked from commit 316878cba09758c2640ed718fb7f398b557b6eb2) +Signed-off-by: Mohith Kumar Thummaluru +--- + build/rpm/pcp.spec.in | 22 +- + build/rpm/redhat.spec | 27 +- + qa/1993 | 87 ++++ + qa/1993.out | 21 + + qa/GNUmakefile | 2 +- + qa/GNUmakefile.install | 2 +- + qa/group | 2 + + qa/rocestat/GNUmakefile | 22 + + qa/rocestat/GNUmakefile.install | 20 + + qa/rocestat/README | 29 ++ + qa/rocestat/metrics.list | 49 +++ + qa/rocestat/rocestat.json | 218 ++++++++++ + qa/rocestat/rocestat_test.python | 123 ++++++ + src/pmdas/GNUmakefile | 2 +- + src/pmdas/rocestat/GNUmakefile | 40 ++ + src/pmdas/rocestat/Install | 40 ++ + src/pmdas/rocestat/Remove | 38 ++ + src/pmdas/rocestat/domain.h | 2 + + src/pmdas/rocestat/pmdarocestat.1 | 94 +++++ + src/pmdas/rocestat/pmdarocestat.python | 536 +++++++++++++++++++++++++ + src/pmdas/rocestat/pmns | 1 + + src/pmns/stdpmid.pcp | 2 +- + 22 files changed, 1373 insertions(+), 6 deletions(-) + create mode 100755 qa/1993 + create mode 100644 qa/1993.out + create mode 100644 qa/rocestat/GNUmakefile + create mode 100644 qa/rocestat/GNUmakefile.install + create mode 100644 qa/rocestat/README + create mode 100644 qa/rocestat/metrics.list + create mode 100644 qa/rocestat/rocestat.json + create mode 100755 qa/rocestat/rocestat_test.python + create mode 100644 src/pmdas/rocestat/GNUmakefile + create mode 100644 src/pmdas/rocestat/Install + create mode 100644 src/pmdas/rocestat/Remove + create mode 100644 src/pmdas/rocestat/domain.h + create mode 100644 src/pmdas/rocestat/pmdarocestat.1 + create mode 100644 src/pmdas/rocestat/pmdarocestat.python + create mode 100644 src/pmdas/rocestat/pmns + +diff --git a/build/rpm/pcp.spec.in b/build/rpm/pcp.spec.in +index efec2fbf06..823d006863 100755 +--- a/build/rpm/pcp.spec.in ++++ b/build/rpm/pcp.spec.in +@@ -1787,6 +1787,20 @@ This package contains the PCP Performance Metrics Domain Agent (PMDA) for + collecting metrics from simple network checks. + # end pcp-pmda-netcheck + ++# ++# pcp-pmda-rocestat ++# ++%package pmda-rocestat ++License: GPL-2.0-or-later ++Summary: Performance Co-Pilot (PCP) metrics for nVidia RoCE devices ++URL: https://pcp.io ++Requires: pcp = @package_version@ pcp-libs = @package_version@ ++Requires: python3-pcp ++%description pmda-rocestat ++This package contains the PCP Performance Metrics Domain Agent (PMDA) for ++collecting statistics for nVidia RDMA over Converged Ethernet (RoCE) devices. ++# end pcp-pmda-rocestat ++ + # + # pcp-pmda-openvswitch + # +@@ -2507,6 +2521,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-fil + basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files + basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files + basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files ++basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files + basic_manifest | keep '(etc/pcp|pmdas)/roomtemp(/|$)' >pcp-pmda-roomtemp-files + basic_manifest | keep '(etc/pcp|pmdas)/rsyslog(/|$)' >pcp-pmda-rsyslog-files + basic_manifest | keep '(etc/pcp|pmdas)/samba(/|$)' >pcp-pmda-samba-files +@@ -2544,7 +2559,7 @@ for pmda_package in \ + nutcracker nvidia \ + openmetrics openvswitch oracle \ + pdns perfevent podman postfix postgresql \ +- rabbitmq redis resctrl roomtemp rsyslog \ ++ rabbitmq redis resctrl rocestat roomtemp rsyslog \ + samba sendmail shping slurm smart snmp \ + sockets statsd summary systemd \ + unbound uwsgi \ +@@ -2960,6 +2975,9 @@ done + %preun pmda-rabbitmq + %{pmda_remove "$1" "rabbitmq"} + ++%preun pmda-rocestat ++%{pmda_remove "$1" "rocestat"} ++ + %preun pmda-uwsgi + %{pmda_remove "$1" "uwsgi"} + %endif +@@ -3284,6 +3302,8 @@ fi + + %files pmda-rabbitmq -f pcp-pmda-rabbitmq-files.rpm + ++%files pmda-rocestat -f pcp-pmda-rocestat-files.rpm ++ + %files pmda-uwsgi -f pcp-pmda-uwsgi-files.rpm + + %files export-pcp2graphite -f pcp-export-pcp2graphite-files.rpm +diff --git a/build/rpm/redhat.spec b/build/rpm/redhat.spec +index e1fa1fbcd3..a15f859c9c 100644 +--- a/build/rpm/redhat.spec ++++ b/build/rpm/redhat.spec +@@ -1942,6 +1942,25 @@ collecting metrics from simple network checks. + # end pcp-pmda-netcheck + %endif + ++# ++# pcp-pmda-rocestat ++# ++%package pmda-rocestat ++License: GPL-2.0-or-later ++Summary: Performance Co-Pilot (PCP) metrics for nVidia RoCE devices ++URL: https://pcp.io ++Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} ++%if !%{disable_python3} ++Requires: python3-pcp ++%else ++Requires: %{__python2}-pcp ++%endif ++%description pmda-rocestat ++This package contains the PCP Performance Metrics Domain Agent (PMDA) for ++collecting statistics for nVidia RDMA over Converged Ethernet (RoCE) devices. ++# end pcp-pmda-rocestat ++%endif ++ + %if !%{disable_mongodb} + # + # pcp-pmda-mongodb +@@ -2715,6 +2734,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-fil + basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files + basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files + basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files ++basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files + basic_manifest | keep '(etc/pcp|pmdas)/roomtemp(/|$)' >pcp-pmda-roomtemp-files + basic_manifest | keep '(etc/pcp|pmdas)/rpm(/|$)' >pcp-pmda-rpm-files + basic_manifest | keep '(etc/pcp|pmdas)/rsyslog(/|$)' >pcp-pmda-rsyslog-files +@@ -2753,7 +2773,7 @@ for pmda_package in \ + nutcracker nvidia \ + openmetrics openvswitch oracle \ + pdns perfevent podman postfix postgresql \ +- rabbitmq redis resctrl roomtemp rpm rsyslog \ ++ rabbitmq redis resctrl rocestat roomtemp rpm rsyslog \ + samba sendmail shping slurm smart snmp \ + sockets statsd summary systemd \ + unbound uwsgi \ +@@ -3131,6 +3151,9 @@ exit 0 + %preun pmda-netcheck + %{pmda_remove "$1" "netcheck"} + ++%preun pmda-rocestat ++%{pmda_remove "$1" "rocestat"} ++ + %endif + + %preun pmda-apache +@@ -3475,6 +3498,8 @@ fi + + %files pmda-rabbitmq -f pcp-pmda-rabbitmq-files.rpm + ++%files pmda-rocestat -f pcp-pmda-rocestat-files.rpm ++ + %files pmda-uwsgi -f pcp-pmda-uwsgi-files.rpm + + %files export-pcp2graphite -f pcp-export-pcp2graphite-files.rpm +diff --git a/qa/1993 b/qa/1993 +new file mode 100755 +index 0000000000..5d0af70a1d +--- /dev/null ++++ b/qa/1993 +@@ -0,0 +1,87 @@ ++#!/bin/sh ++# PCP QA Test No. 1993 ++# Testing PCP Rocestat PMDA - install, remove and values. ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++. ./common.python ++ ++pmda_path="$PCP_PMDAS_DIR/rocestat" ++pmda_script="$pmda_path/pmdarocestat.python" ++test_script="rocestat/rocestat_test.python" ++ ++[ -d $pmda_path ] || _notrun "ROCESTAT PMDA is not installed" ++ ++$python -c "from pcp import pmda" >/dev/null 2>&1 ++[ $? -eq 0 ] || _notrun "python pcp pmda module not installed" ++ ++if [ ! -d /sys/class/infiniband ]; then ++ # Check if Infiniband kernel module is loaded ++ lsmod | grep -q '^ib_core' || _notrun "IB kernel modules are not loaded" ++ _notrun "No RoCE devices detected" ++fi ++ ++status=1 # failure is the default! ++trap "_cleanup_pmda rocestat; exit \$status" 0 1 2 3 15 ++ ++pmdarocestat_filter() ++{ ++ sed \ ++ -e "s/Ran [0-9]* tests in [0-9]*\.[0-9][0-9]*s/Ran X tests in YYYs/" \ ++ -e "s/FAILED (failures=[0-9]*)/FAILED (failures=X)/" \ ++ -e "/^$/d" ++} ++ ++pmdarocestat_remove() ++{ ++ cd $pmda_path ++ echo ++ echo "=== Removing ROCESTAT agent ===" ++ $sudo ./Remove >$tmp.out 2>&1 ++} ++ ++pmdarocestat_install() ++{ ++ cd $pmda_path ++ $sudo ./Remove >/dev/null 2>&1 ++ ++ echo ++ echo "=== Installing ROCESTAT agent ===" ++ $sudo ./Install $tmp.out 2>&1 ++ cd $here ++} ++ ++run_rocestat_test() ++{ ++ echo ++ echo "=== Running ROCESTAT test script ===" ++ $sudo $python $test_script 2>&1 | LC_COLLATE=POSIX sort | pmdarocestat_filter ++} ++ ++# Real QA test starts here ++_prepare_pmda rocestat ++ ++pmdarocestat_install ++ ++run_rocestat_test ++ ++pmdarocestat_remove ++ ++# Success, all done ++status=0 ++exit +diff --git a/qa/1993.out b/qa/1993.out +new file mode 100644 +index 0000000000..70753bdb62 +--- /dev/null ++++ b/qa/1993.out +@@ -0,0 +1,21 @@ ++QA output created by 1993 ++ ++=== Installing ROCESTAT agent === ++ ++=== Running ROCESTAT test script === ++---------------------------------------------------------------------- ++Fetching Rocestat PMDA metrics ++OK ++Ran X tests in YYYs ++test_hw_link_metrics (__main__.RocestatTests) ... ok ++test_hw_mcast_metrics (__main__.RocestatTests) ... ok ++test_hw_metrics (__main__.RocestatTests) ... ok ++test_hw_rcv_metrics (__main__.RocestatTests) ... ok ++test_hw_req_metrics (__main__.RocestatTests) ... ok ++test_hw_resp_metrics (__main__.RocestatTests) ... ok ++test_hw_rnr_metrics (__main__.RocestatTests) ... ok ++test_hw_ucast_metrics (__main__.RocestatTests) ... ok ++test_hw_xmit_metrics (__main__.RocestatTests) ... ok ++test_nic_metrics (__main__.RocestatTests) ... ok ++ ++=== Removing ROCESTAT agent === +diff --git a/qa/GNUmakefile b/qa/GNUmakefile +index e8a055b6e1..a4d6e6a4ef 100644 +--- a/qa/GNUmakefile ++++ b/qa/GNUmakefile +@@ -17,7 +17,7 @@ TESTS = $(shell sed -n -e '/^[0-9][0-9]*:retired/d' -e '/^[0-9][0-9]*:reserved/d + SUBDIRS = src pmdas cisco gluster pconf sadist collectl nfsclient named \ + archives badarchives views qt linux unbound cifs gpfs lustre ganglia \ + java mmv postfix perl json slurm tmparch sheet smart admin hacluster \ +- sockets denki gfs2 farm pdudata ++ sockets denki gfs2 farm pdudata rocestat + + ifeq "$(PMDA_PERFEVENT)" "true" + SUBDIRS += perfevent +diff --git a/qa/GNUmakefile.install b/qa/GNUmakefile.install +index cf7c816562..f7b6b9d174 100644 +--- a/qa/GNUmakefile.install ++++ b/qa/GNUmakefile.install +@@ -21,7 +21,7 @@ ifeq "$(PMDA_PERFEVENT)" "true" + SUBDIRS += perfevent + endif + ifeq "$(HAVE_PYTHON)" "true" +-SUBDIRS += secure mic haproxy lio openmetrics ++SUBDIRS += secure mic haproxy lio openmetrics rocestat + endif + + # Before installing the pcpqa policy module we need a PCP build with +diff --git a/qa/group b/qa/group +index 6fd43299d8..137dba39f8 100644 +--- a/qa/group ++++ b/qa/group +@@ -260,6 +260,7 @@ pmda.postgresql + pmda.proc + pmda.redis + pmda.resctrl ++pmda.rocestat + pmda.root + #pmda.rpm # note this group has been retired + pmda.rsyslog +@@ -2215,8 +2216,9 @@ pmcd.pdu + 1990 pcp buddyinfo python local + 1991 pcp netstat python local + 1992 pmda.uwsgi local ++1993 pmda.rocestat local python + 2100 pmproxy local security + 2101 pmda.sockets local security + 2104 libpcp local security + 2105 libpcp pmcd local security pmcd.pdu + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/qa/rocestat/GNUmakefile b/qa/rocestat/GNUmakefile +new file mode 100644 +index 0000000000..9c20dc00bc +--- /dev/null ++++ b/qa/rocestat/GNUmakefile +@@ -0,0 +1,22 @@ ++#!gmake ++ ++TOPDIR = ../.. ++include $(TOPDIR)/src/include/builddefs ++ ++TESTDIR = $(PCP_VAR_DIR)/testsuite/rocestat ++MYFILES = rocestat.json ++PYMODULES = rocestat_test.python ++LDIRT = rocestat_test.py rocestat_test.pyc rocestat_test.pyo ++ ++default default_pcp setup: rocestat_test.py ++ ++install install_pcp: ++ $(INSTALL) -m 755 -d $(TESTDIR) ++ $(INSTALL) -m 644 -f $(MYFILES) $(TESTDIR)/$(MYFILES) ++ $(INSTALL) -m 755 -f $(PYMODULES) $(TESTDIR)/$(PYMODULES) ++ $(INSTALL) -m 644 -f GNUmakefile.install $(TESTDIR)/GNUmakefile ++ ++include $(BUILDRULES) ++ ++%.py : %.python ++ $(LN_S) $< $@ +diff --git a/qa/rocestat/GNUmakefile.install b/qa/rocestat/GNUmakefile.install +new file mode 100644 +index 0000000000..495ef4b1bc +--- /dev/null ++++ b/qa/rocestat/GNUmakefile.install +@@ -0,0 +1,20 @@ ++#!gmake ++ ++ifdef PCP_CONF ++include $(PCP_CONF) ++else ++include $(PCP_DIR)/etc/pcp.conf ++endif ++PATH = $(shell . $(PCP_DIR)/etc/pcp.env; echo $$PATH) ++include $(PCP_INC_DIR)/builddefs ++ ++TESTDIR = $(PCP_VAR_DIR)/testsuite/rocestat ++ ++default default_pcp setup: rocestat_test.py ++ ++install install_pcp: ++ ++include $(BUILDRULES) ++ ++%.py : %.python ++ $(LN_S) $< $@ +diff --git a/qa/rocestat/README b/qa/rocestat/README +new file mode 100644 +index 0000000000..5888671b90 +--- /dev/null ++++ b/qa/rocestat/README +@@ -0,0 +1,29 @@ ++Pre-Requisites ++-------------- ++In order to perform the tests, we'll make some assumptions: ++ ++- The `pcp` Python module is installed and accessible. ++- The `rocestat` PMDA has been installed successfully. ++- The system has RoCE devices available and the required kernel modules (`ib_core`) are loaded. ++- You are able to run the test as the root user (required for PMDA installation and removal). ++ ++What the Test Does ++------------------ ++The test validates the functionality of the `rocestat` PMDA by performing the following steps: ++ ++1. Installs the `rocestat` PMDA. ++2. Fetches metrics defined in `metrics.list` and validates their values against the expected values in `rocestat.json`. ++3. Runs unit tests for each metric cluster (e.g., `hw`, `hw_xmit`, `hw_rcv`) to ensure the PMDA is reporting correct values. ++4. Removes the `rocestat` PMDA after the tests are complete. ++ ++If any of the above checks fail, an exception will be raised, and the test will terminate. ++ ++How to Test ++----------- ++1. Make sure the pre-requisites are in place. ++2. Run the rocestat_test.python script ++ ++Issues ++------ ++- None known at this time ++ +diff --git a/qa/rocestat/metrics.list b/qa/rocestat/metrics.list +new file mode 100644 +index 0000000000..68619da7c4 +--- /dev/null ++++ b/qa/rocestat/metrics.list +@@ -0,0 +1,49 @@ ++rocestat.lane.rx_pause ++rocestat.lane.rx_bytes ++rocestat.lane.tx_bytes ++rocestat.hw.link.link_error_recovery ++rocestat.hw.link.link_downed ++rocestat.hw.link.local_link_integrity_errors ++rocestat.hw.rnr.rnr_nak_retry_err ++rocestat.hw.resp.resp_local_length_error ++rocestat.hw.resp.resp_cqe_error ++rocestat.hw.resp.resp_cqe_flush_error ++rocestat.hw.resp.resp_remote_access_errors ++rocestat.hw.req.req_remote_invalid_request ++rocestat.hw.req.req_cqe_error ++rocestat.hw.req.req_cqe_flush_error ++rocestat.hw.req.duplicate_request ++rocestat.hw.req.rx_read_requests ++rocestat.hw.req.rx_atomic_requests ++rocestat.hw.req.req_remote_access_errors ++rocestat.hw.req.rx_write_requests ++rocestat.hw.mcast.multicast_rcv_packets ++rocestat.hw.mcast.multicast_xmit_packets ++rocestat.hw.ucast.unicast_rcv_packets ++rocestat.hw.ucast.unicast_xmit_packets ++rocestat.hw.rcv.port_rcv_errors ++rocestat.hw.rcv.port_rcv_remote_physical_errors ++rocestat.hw.rcv.port_rcv_packets ++rocestat.hw.rcv.port_rcv_data ++rocestat.hw.rcv.port_rcv_constraint_errors ++rocestat.hw.rcv.port_rcv_switch_relay_errors ++rocestat.hw.xmit.port_xmit_data ++rocestat.hw.xmit.port_xmit_constraint_errors ++rocestat.hw.xmit.port_xmit_wait ++rocestat.hw.xmit.port_xmit_packets ++rocestat.hw.xmit.port_xmit_discards ++rocestat.hw.roce_slow_restart_trans ++rocestat.hw.roce_slow_restart_cnps ++rocestat.hw.roce_slow_restart ++rocestat.hw.roce_adp_retrans_to ++rocestat.hw.clear_counters ++rocestat.hw.local_ack_timeout_err ++rocestat.hw.lifespan ++rocestat.hw.implied_nak_seq_err ++rocestat.hw.packet_seq_err ++rocestat.hw.roce_adp_retrans ++rocestat.hw.out_of_buffer ++rocestat.hw.out_of_sequence ++rocestat.hw.VL15_dropped ++rocestat.hw.excessive_buffer_overrun_errors ++rocestat.hw.symbol_error +\ No newline at end of file +diff --git a/qa/rocestat/rocestat.json b/qa/rocestat/rocestat.json +new file mode 100644 +index 0000000000..e5c6da7a5b +--- /dev/null ++++ b/qa/rocestat/rocestat.json +@@ -0,0 +1,218 @@ ++{ ++ "lane": { ++ "rx_pause": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "rx_bytes": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "tx_bytes": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_link": { ++ "link_error_recovery": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "link_downed": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "local_link_integrity_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_rnr": { ++ "rnr_nak_retry_err": { ++ "mlx5_0_1": 3, ++ "mlx5_1_1": 1 ++ } ++ }, ++ "hw_resp": { ++ "resp_local_length_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "resp_cqe_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "resp_cqe_flush_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "resp_remote_access_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_req": { ++ "req_remote_invalid_request": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "req_cqe_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "req_cqe_flush_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "duplicate_request": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "rx_read_requests": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "rx_atomic_requests": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "req_remote_access_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "rx_write_requests": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_mcast": { ++ "multicast_rcv_packets": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "multicast_xmit_packets": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_ucast": { ++ "unicast_rcv_packets": { ++ "mlx5_0_1": 2454, ++ "mlx5_1_1": 2469 ++ }, ++ "unicast_xmit_packets": { ++ "mlx5_0_1": 2496, ++ "mlx5_1_1": 2478 ++ } ++ }, ++ "hw_rcv": { ++ "port_rcv_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "port_rcv_remote_physical_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "port_rcv_packets": { ++ "mlx5_0_1": 2454, ++ "mlx5_1_1": 2469 ++ }, ++ "port_rcv_data": { ++ "mlx5_0_1": 57781, ++ "mlx5_1_1": 53893 ++ }, ++ "port_rcv_constraint_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "port_rcv_switch_relay_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw_xmit": { ++ "port_xmit_data": { ++ "mlx5_0_1": 67939, ++ "mlx5_1_1": 56182 ++ }, ++ "port_xmit_constraint_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "port_xmit_wait": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "port_xmit_packets": { ++ "mlx5_0_1": 2496, ++ "mlx5_1_1": 2478 ++ }, ++ "port_xmit_discards": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ }, ++ "hw": { ++ "roce_slow_restart_trans": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "roce_slow_restart_cnps": { ++ "mlx5_0_1": 3, ++ "mlx5_1_1": 1 ++ }, ++ "roce_slow_restart": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "roce_adp_retrans_to": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "clear_counters": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "local_ack_timeout_err": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "lifespan": { ++ "mlx5_0_1": 10, ++ "mlx5_1_1": 10 ++ }, ++ "implied_nak_seq_err": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "packet_seq_err": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "roce_adp_retrans": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "out_of_buffer": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "out_of_sequence": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "VL15_dropped": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "excessive_buffer_overrun_errors": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ }, ++ "symbol_error": { ++ "mlx5_0_1": 0, ++ "mlx5_1_1": 0 ++ } ++ } ++} +\ No newline at end of file +diff --git a/qa/rocestat/rocestat_test.python b/qa/rocestat/rocestat_test.python +new file mode 100755 +index 0000000000..5645761efc +--- /dev/null ++++ b/qa/rocestat/rocestat_test.python +@@ -0,0 +1,123 @@ ++#!/usr/bin/env pmpython ++ ++import os ++import json ++from pcp import pmapi ++import cpmapi as c_api ++import unittest ++import ctypes ++ ++ ++def load_metrics_from_file(filepath): ++ """Load metrics from a file and organize them by cluster.""" ++ clusters = {} ++ with open(filepath, 'r') as f: ++ for line in f: ++ line = line.strip() ++ if not line or line.startswith('#'): ++ continue ++ cluster, metric = line.split('.', 1) ++ clusters.setdefault(cluster, []).append(metric) ++ return clusters ++ ++ ++def fetch_metric_data(ctx, cluster, metric): ++ """Fetch metric data for a given cluster and metric.""" ++ # print(f"Fetching data for metric: {cluster}.{metric}") ++ pmid = ctx.pmLookupName([f"{cluster}.{metric}"])[0] ++ desc = ctx.pmLookupDescs([pmid])[0] ++ ++ pmid_array = (ctypes.c_uint * 1)(pmid) ++ results = ctx.pmFetch(pmid_array) ++ ++ # Handle instance domains ++ if desc.contents.indom == c_api.PM_INDOM_NULL: ++ print(f"No instances for metric: {cluster}.{metric}") ++ return {} # No instances for this metric ++ ++ instances = ctx.pmGetInDom(desc)[1] ++ metric_data = {} ++ for i, instance in enumerate(instances): ++ atom = ctx.pmExtractValue( ++ results.contents.get_valfmt(0), ++ results.contents.get_vlist(0, i), ++ desc.contents.type, ++ c_api.PM_TYPE_U64 ++ ) ++ metric_data[instance] = atom.ul ++ # print(f"Metric: {cluster}.{metric}, Instance: {instance}, Value: {atom.ul}") ++ ++ return metric_data ++ ++ ++class RocestatTests(unittest.TestCase): ++ """Unit tests for Rocestat PMDA metrics.""" ++ ++ @classmethod ++ def setUpClass(cls): ++ """Set up the test environment and fetch all metrics.""" ++ print("\nFetching Rocestat PMDA metrics\n") ++ cls.ctx = pmapi.pmContext() ++ cls.cluster_data = {} ++ ++ for cluster, metrics in metric_clusters.items(): ++ cls.cluster_data[cluster] = { ++ metric: fetch_metric_data(cls.ctx, cluster, metric) ++ for metric in metrics ++ } ++ ++ def _test_cluster_metrics(self, cluster): ++ """Test all metrics in a given cluster.""" ++ metrics = self.cluster_data.get(cluster, {}) ++ ++ for metric, instances in metrics.items(): ++ self.assertTrue(instances, f"No instances found for metric: {metric} in cluster: {cluster}") ++ for instance, value in instances.items(): ++ with self.subTest(cluster=cluster, metric=metric, instance=instance): ++ expected_value = expected_values.get(cluster, {}).get(metric, {}).get(instance) ++ self.assertIsNotNone(expected_value, f"Expected value for {metric} (instance {instance}) in {cluster} is missing") ++ self.assertEqual(value, expected_value, f"{metric} (instance {instance}) in {cluster} does not match expected value") ++ ++ def test_hw_metrics(self): ++ self._test_cluster_metrics("hw") ++ ++ def test_hw_xmit_metrics(self): ++ self._test_cluster_metrics("hw_xmit") ++ ++ def test_hw_rcv_metrics(self): ++ self._test_cluster_metrics("hw_rcv") ++ ++ def test_hw_ucast_metrics(self): ++ self._test_cluster_metrics("hw_ucast") ++ ++ def test_hw_mcast_metrics(self): ++ self._test_cluster_metrics("hw_mcast") ++ ++ def test_hw_req_metrics(self): ++ self._test_cluster_metrics("hw_req") ++ ++ def test_hw_resp_metrics(self): ++ self._test_cluster_metrics("hw_resp") ++ ++ def test_hw_rnr_metrics(self): ++ self._test_cluster_metrics("hw_rnr") ++ ++ def test_hw_link_metrics(self): ++ self._test_cluster_metrics("hw_link") ++ ++ def test_nic_metrics(self): ++ self._test_cluster_metrics("nic") ++ ++ ++if __name__ == '__main__': ++ # Load metrics and expected values ++ script_dir = os.path.dirname(__file__) ++ metrics_file = os.path.join(script_dir, "metrics.list") ++ expected_values_file = os.path.join(script_dir, "rocestat.json") ++ ++ metric_clusters = load_metrics_from_file(metrics_file) ++ with open(expected_values_file, 'r') as f: ++ expected_values = json.load(f) ++ ++ # Run tests ++ unittest.TextTestRunner(verbosity=2).run(unittest.TestLoader().loadTestsFromTestCase(RocestatTests)) +diff --git a/src/pmdas/GNUmakefile b/src/pmdas/GNUmakefile +index b906d4193e..4b0bbba397 100644 +--- a/src/pmdas/GNUmakefile ++++ b/src/pmdas/GNUmakefile +@@ -39,7 +39,7 @@ PLPMDAS = bonding netfilter zimbra postgresql \ + PYPMDAS = bcc gluster zswap unbound mic haproxy \ + json libvirt lio openmetrics elasticsearch \ + bpftrace mssql netcheck rabbitmq openvswitch \ +- nfsclient mongodb uwsgi ++ nfsclient mongodb uwsgi rocestat + + SUBDIRS = $(CPMDAS) $(PLPMDAS) $(PYPMDAS) + LDIRT = pmcd.conf +diff --git a/src/pmdas/rocestat/GNUmakefile b/src/pmdas/rocestat/GNUmakefile +new file mode 100644 +index 0000000000..fd036e0531 +--- /dev/null ++++ b/src/pmdas/rocestat/GNUmakefile +@@ -0,0 +1,40 @@ ++TOPDIR = ../../.. ++include $(TOPDIR)/src/include/builddefs ++ ++IAM = rocestat ++PYSCRIPT = pmda$(IAM).python ++LDIRT = domain.h root pmns $(IAM).log ++DOMAIN = GLUSTER ++ ++PMDAADMDIR = $(PCP_PMDASADM_DIR)/$(IAM) ++PMDATMPDIR = $(PCP_PMDAS_DIR)/$(IAM) ++ ++MAN_SECTION = 1 ++MAN_PAGES = pmda$(IAM).$(MAN_SECTION) ++MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION) ++ ++default_pcp default: build-me ++ ++include $(BUILDRULES) ++ ++ifeq "$(HAVE_PYTHON)" "true" ++build-me: check_domain ++install_pcp install: default ++ $(INSTALL) -m 755 -d $(PMDAADMDIR) ++ $(INSTALL) -m 755 -d $(PMDATMPDIR) ++ $(INSTALL) -m 755 -t $(PMDATMPDIR) Install Remove $(PYSCRIPT) $(PMDAADMDIR) ++ @$(INSTALL_MAN) ++else ++build-me: ++install_pcp install: ++ @$(INSTALL_MAN) ++endif ++ ++check_domain: ../../pmns/stdpmid ++ $(DOMAIN_PYTHONRULE) ++ ++check:: $(PYSCRIPT) ++ $(PYLINT) $^ ++ ++check:: $(MAN_PAGES) ++ $(MANLINT) $^ +diff --git a/src/pmdas/rocestat/Install b/src/pmdas/rocestat/Install +new file mode 100644 +index 0000000000..cb66717036 +--- /dev/null ++++ b/src/pmdas/rocestat/Install +@@ -0,0 +1,40 @@ ++#!/bin/sh ++# __author__ = "Mohith Kumar Thummaluru" ++# __email__ = "mohith.k.kumar.thummaluru@oracle.com" ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++# Install the Rocestat PMDA ++# ++ ++. $PCP_DIR/etc/pcp.env ++. $PCP_SHARE_DIR/lib/pmdaproc.sh ++ ++iam=rocestat ++python_opt=true ++daemon_opt=false ++ ++if [[ ! -d /sys/class/infiniband ]]; then ++ if ! lsmod | grep -q '^ib_core'; then ++ echo "IB kernel modules are not loaded" >&2 ++ else ++ echo "No IB devices detected" >&2 ++ fi ++ status=1 ++ exit ++fi ++ ++pmdaSetup ++pmdaInstall ++exit +diff --git a/src/pmdas/rocestat/Remove b/src/pmdas/rocestat/Remove +new file mode 100644 +index 0000000000..00f773b5d7 +--- /dev/null ++++ b/src/pmdas/rocestat/Remove +@@ -0,0 +1,38 @@ ++#!/bin/sh ++# __author__ = "Mohith Kumar Thummaluru" ++# __email__ = "mohith.k.kumar.thummaluru@oracle.com" ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++# Remove the rocestat PMDA ++# ++ ++# source the PCP configuration environment variables ++. $PCP_DIR/etc/pcp.env ++ ++# Get the common procedures and variable assignments ++# ++. $PCP_SHARE_DIR/lib/pmdaproc.sh ++ ++# The name of the PMDA ++# ++iam=rocestat ++ ++# Do it ++# ++pmdaSetup ++pmdaRemove ++ ++ ++exit +diff --git a/src/pmdas/rocestat/domain.h b/src/pmdas/rocestat/domain.h +new file mode 100644 +index 0000000000..a2bcb16fda +--- /dev/null ++++ b/src/pmdas/rocestat/domain.h +@@ -0,0 +1,2 @@ ++#define ROCESTAT 254 ++ +diff --git a/src/pmdas/rocestat/pmdarocestat.1 b/src/pmdas/rocestat/pmdarocestat.1 +new file mode 100644 +index 0000000000..28a55f19c6 +--- /dev/null ++++ b/src/pmdas/rocestat/pmdarocestat.1 +@@ -0,0 +1,94 @@ ++'\"macro stdmacro ++.\" ++.\" Copyright (c) 2025 Oracle and/or its affiliates. ++.\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++.\" ++.\" This program is free software; you can redistribute it and/or modify it ++.\" under the terms of the GNU General Public License as published by the ++.\" Free Software Foundation; either version 2 of the License, or (at your ++.\" option) any later version. ++.\" ++.\" This program is distributed in the hope that it will be useful, but ++.\" WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++.\" or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++.\" for more details. ++.\" ++.\" ++.TH PMDAROCESTAT 1 "PCP" "Performance Co-Pilot" "General Commands Manual" ++.SH NAME ++.B pmdarocestat ++\- Performance Metrics Domain Agent (PMDA) for RoCE devices ++ ++.SH DESCRIPTION ++The ++.B Rocestat PMDA ++(Performance Metrics Domain Agent) is a Performance Co-Pilot (PCP) module that collects ++and exports performance statistics for RDMA over Converged Ethernet (RoCE) devices. It provides insights into ++network performance, error conditions, and congestion events, aiding in the diagnosis and monitoring of RoCE-based ++communication. ++ ++This PMDA reports software-aggregated InfiniBand port statistics, including received/transmitted bytes and ++packets, link errors, and congestion-related drops, helping to identify potential bottlenecks and failures. ++Additionally, it includes hardware-level counters, which track low-level transmission metrics, duplicate requests, ++NAKs, and physical/constraint errors, offering a deeper view into the underlying transport reliability and efficiency. ++ ++Furthermore, Rocestat PMDA collects priority-based lane metrics from ethtool -S , filtering ++statistics related to priority lanes in RoCE traffic. These metrics provide visibility into traffic distribution ++across lanes, helping diagnose congestion hotspots and optimize workload balancing across different lanes ++ ++By integrating Rocestat PMDA into a PCP monitoring environment, users can efficiently analyze RoCE ++network behavior, detect performance anomalies, and optimize high-speed RDMA workloads in data center and HPC ++environments. ++ ++.SH INSTALLATION ++To install the Rocestat PMDA, follow these steps: ++.RS ++.nf ++# cd $PCP_PMDAS_DIR/rocestat ++# ./Install ++.fi ++.RE ++ ++To verify that the PMDA is running: ++.RS ++.nf ++$ pminfo -t rocestat ++.fi ++.RE ++ ++.SH USAGE ++To query Rocestat metrics, use the following command: ++.RS ++.nf ++$ pminfo rocestat ++.fi ++.RE ++To retrieve specific metric values: ++.RS ++.nf ++$ pmval rocestat.hw.rcv.port_rcv_packets ++.fi ++.RE ++ ++.SH FILES ++.TP ++.I $PCP_PMDAS_DIR/rocestat/Install ++Installation script for Rocestat PMDA. ++.TP ++.I $PCP_PMDAS_DIR/rocestat/Remove ++Uninstallation script. ++.TP ++.I $PCP_LOG_DIR/pmcd/rocestat.log ++Log file for Rocestat PMDA events and errors. ++ ++.SH PCP ENVIRONMENT ++Environment variables with the prefix PCP_ are used to parameterize the file and ++directory names used by PCP. On each installation, the file /etc/pcp.conf contains ++the local values for these variables. The $PCP_CONF variable may be used to specify ++an alternative configuration file, as described in pcp.conf(5). ++ ++.SH SEE ALSO ++.BR PCPIntro (1), ++.BR pmcd (1) ++.BR PMDA (3), ++.BR pminfo (1), +diff --git a/src/pmdas/rocestat/pmdarocestat.python b/src/pmdas/rocestat/pmdarocestat.python +new file mode 100644 +index 0000000000..9b852d398f +--- /dev/null ++++ b/src/pmdas/rocestat/pmdarocestat.python +@@ -0,0 +1,536 @@ ++# __author__ = "Mohith Kumar Thummaluru" ++# __email__ = "mohith.k.kumar.thummaluru@oracle.com" ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++# pylint: disable=no-self-use, consider-using-f-string, too-many-instance-attributes ++# pylint: disable=c-extension-no-member, consider-iterating-dictionary ++# pylint: disable=too-many-locals, too-many-arguments, too-many-lines, unused-argument, line-too-long ++# pylint: disable=too-many-return-statements, too-many-branches, too-many-statements, bare-except ++""" ++Python implementation of the "rocestat" Performance Metrics Domain Agent. ++""" ++import subprocess ++import os ++import re ++import cpmapi as c_api ++from pcp.pmda import PMDA, pmdaMetric, pmdaIndom ++from pcp.pmapi import pmUnits ++from ctypes import c_int, c_ulonglong, POINTER, cast, Structure ++ ++lane_mapping = { ++ 0: 'Default(TCP) - 0', ++ 1: 'Normal Large - 1', ++ 2: 'VIP Small - 2', ++ 3: 'VIP Large - 3', ++ 4: 'Normal Small - 4', ++ 5: 'VIP Small - 5', ++ 6: 'Unused - 6', ++ 7: 'CNP - 7', ++} ++ ++RoceStatHW = [ ++ ("symbol_error", c_ulonglong), ++ ("excessive_buffer_overrun_errors", c_ulonglong), ++ ("VL15_dropped", c_ulonglong), ++ ("out_of_sequence", c_ulonglong), ++ ("out_of_buffer", c_ulonglong), ++ ("roce_adp_retrans", c_ulonglong), ++ ("packet_seq_err", c_ulonglong), ++ ("implied_nak_seq_err", c_ulonglong), ++ ("lifespan", c_ulonglong), ++ ("local_ack_timeout_err", c_ulonglong), ++ ("clear_counters", c_ulonglong), ++ ("roce_adp_retrans_to", c_ulonglong), ++ ("roce_slow_restart", c_ulonglong), ++ ("roce_slow_restart_cnps", c_ulonglong), ++ ("roce_slow_restart_trans", c_ulonglong) ++ ] ++ ++RoceStatXmit = [ ++ ("port_xmit_discards", c_ulonglong), ++ ("port_xmit_packets", c_ulonglong), ++ ("port_xmit_wait", c_ulonglong), ++ ("port_xmit_constraint_errors", c_ulonglong), ++ ("port_xmit_data", c_ulonglong) ++ ] ++ ++RoceStatRcv = [ ++ ("port_rcv_switch_relay_errors", c_ulonglong), ++ ("port_rcv_constraint_errors", c_ulonglong), ++ ("port_rcv_data", c_ulonglong), ++ ("port_rcv_packets", c_ulonglong), ++ ("port_rcv_remote_physical_errors", c_ulonglong), ++ ("port_rcv_errors", c_ulonglong) ++ ] ++ ++RoceStatUcast = [ ++ ("unicast_xmit_packets", c_ulonglong), ++ ("unicast_rcv_packets", c_ulonglong) ++ ] ++ ++RoceStatMcast = [ ++ ("multicast_xmit_packets", c_ulonglong), ++ ("multicast_rcv_packets", c_ulonglong) ++ ] ++ ++RoceStatReq = [ ++ ("rx_write_requests", c_ulonglong), ++ ("req_remote_access_errors", c_ulonglong), ++ ("rx_atomic_requests", c_ulonglong), ++ ("rx_read_requests", c_ulonglong), ++ ("duplicate_request", c_ulonglong), ++ ("req_cqe_flush_error", c_ulonglong), ++ ("req_cqe_error", c_ulonglong), ++ ("req_remote_invalid_request", c_ulonglong) ++ ] ++ ++RoceStatResp = [ ++ ("resp_remote_access_errors", c_ulonglong), ++ ("resp_cqe_flush_error", c_ulonglong), ++ ("resp_cqe_error", c_ulonglong), ++ ("resp_local_length_error", c_ulonglong) ++ ] ++ ++RoceStatRnr = [ ++ ("rnr_nak_retry_err", c_ulonglong) ++ ] ++ ++RoceStatLink = [ ++ ("local_link_integrity_errors", c_ulonglong), ++ ("link_downed", c_ulonglong), ++ ("link_error_recovery", c_ulonglong), ++ ] ++ ++class RoceDev(Structure): ++ ''' Statistic values per-mlnx-device (mlnx device indom cache lookup) ''' ++ _fields_ = RoceStatHW + RoceStatXmit + RoceStatRcv ++ _fields_ += RoceStatUcast + RoceStatMcast ++ _fields_ += RoceStatReq + RoceStatResp ++ _fields_ += RoceStatRnr + RoceStatLink ++ ++class NicStats(Structure): ++ _fields_ = [ ++ ("tx_bytes", c_ulonglong), ++ ("rx_bytes", c_ulonglong), ++ ("rx_pause", c_ulonglong), ++ ] ++ ++class NICSTATS: ++ def __init__(self): ++ self.up_interfaces = [] ++ self.ethtool_stats = {} ++ ++ def get_up_interfaces(self): ++ """Get the names of all interfaces that are in the Up state.""" ++ result = subprocess.Popen(['/usr/bin/ibdev2netdev'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) ++ output, _ = result.communicate() ++ ++ if result.returncode != 0: ++ return [] ++ ++ # Regular expression to find interfaces and their status ++ pattern = re.compile(r'(\S+) \(Up\)') ++ self.up_interfaces = pattern.findall(output) ++ return self.up_interfaces ++ ++ def get_ethtool_stats(self, interface): ++ """Get the ethtool -S output for the specified interface and filter stats containing 'prio'.""" ++ result = subprocess.Popen(['ethtool', '-S', interface], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) ++ output, _ = result.communicate() ++ ++ if result.returncode != 0: ++ return {} ++ ++ # Filter the stats to include only those with 'prio' in their names ++ prio_stats = {} ++ for line in output.splitlines(): ++ if 'prio' in line: ++ if 'bytes' in line or 'pause' in line: ++ key, value = line.strip().split(':', 1) ++ value = int(value) ++ if value > 0: ++ prio_stats[key] = value ++ ++ return prio_stats ++ ++ def process_prio_stats(self, prio_stats): ++ """Process the prio stats to group them by lanes.""" ++ all_lanes = [f"lane{i}" for i in range(0,8)] ++ ++ lanes = {lane:{} for lane in all_lanes} ++ ++ for key, value in prio_stats.items(): ++ # Extract the lane number from the key ++ match = re.match(r'(rx|tx)_prio(\d+)_(pause|bytes)', key) ++ if match: ++ direction, lane, key = match.groups() ++ ++ lane = "lane" + str(lane) ++ if (direction =='rx' and key.endswith("pause")) or key.endswith("bytes"): ++ lanes[lane][f'{direction}_{key}'] = value ++ ++ for lane, sub_dict in lanes.items(): ++ metrics = ['rx_bytes', 'tx_bytes', 'rx_pause'] ++ for metric in metrics: ++ if metric not in sub_dict.keys(): ++ lanes[lane][metric] = 0 ++ return lanes ++ ++ def collect_stats(self): ++ """Collect ethtool stats for all Up interfaces.""" ++ self.get_up_interfaces() ++ for interface in self.up_interfaces: ++ stats = self.get_ethtool_stats(interface) ++ lanes_stats = self.process_prio_stats(stats) ++ self.ethtool_stats[interface] = lanes_stats ++ return self.convert_dict(self.ethtool_stats) ++ ++ def convert_dict(self, input_dict): ++ output_dict = {} ++ for key1, sub_dict in input_dict.items(): ++ if isinstance(sub_dict, dict): ++ for key2, value in sub_dict.items(): ++ new_key = f"{key1}_{key2}" ++ output_dict[new_key] = value ++ return output_dict ++ ++class MLNXCOUNTERS(): ++ hw_counter_hash = {} ++ hw_counter_path = [] ++ hw_counter = [] ++ first_hw_qry = True ++ ++ def get_hw_counter_names(self, base_path='/sys/class/infiniband'): ++ counter_paths = [] ++ ++ # Check if base path exists ++ if not os.path.exists(base_path): ++ return counter_paths ++ # raise FileNotFoundError(f"Base path {base_path} does not exist.") ++ ++ # Traverse each device directory in the base path ++ for device in os.listdir(base_path): ++ device_path = os.path.join(base_path, device) ++ ports_path = os.path.join(device_path, 'ports') ++ ++ # Check if ports directory exists ++ if os.path.exists(ports_path): ++ # Traverse each port directory in the device's ports directory ++ for port in os.listdir(ports_path): ++ port_path = os.path.join(ports_path, port) ++ ++ # Check and collect counter file paths ++ counters_path = os.path.join(port_path, 'counters') ++ if os.path.exists(counters_path): ++ for counter_file in os.listdir(counters_path): ++ counter_paths.append(os.path.join(counters_path, counter_file)) ++ ++ # Check and collect hw_counters file paths ++ hw_counters_path = os.path.join(port_path, 'hw_counters') ++ if os.path.exists(hw_counters_path): ++ for hw_counter_file in os.listdir(hw_counters_path): ++ counter_paths.append(os.path.join(hw_counters_path, hw_counter_file)) ++ ++ return counter_paths ++ ++ def find_device_status(self, device): ++ infiniband_base = "/sys/class/infiniband" ++ ports_dir = os.path.join(infiniband_base, device, "ports") ++ ++ if not os.path.isdir(ports_dir): ++ return False ++ ++ try: ++ ports = os.listdir(ports_dir) ++ except FileNotFoundError: ++ return False ++ ++ for port in ports: ++ port_dir = os.path.join(infiniband_base, device, "ports", str(port)) ++ ++ if os.path.isfile(os.path.join(port_dir, "phys_state")): ++ with open(os.path.join(port_dir, "phys_state"), 'r') as f: ++ phys_state = f.read().strip() ++ if "LinkUp" in phys_state: ++ return True ++ ++ return False ++ ++ def get_active_devices(self): ++ ++ active_devices = [] ++ visit = set() ++ ++ for path in self.hw_counter: ++ token = path.split('/') ++ if len(token) > 8: ++ mlx = token[4] ++ if mlx not in visit: ++ status = self.find_device_status(mlx) ++ if status is True: ++ active_devices.append(mlx) ++ self.hw_counter_path.append(path.strip()) ++ visit.add(mlx) ++ return active_devices ++ ++ # initialize the hardware counters ++ # self.hw_counter_hash['mlnx5_0'][1]['np_cnp_sent'] = ++ # where mlnx5_0 is device, 1 is port, np_cnp_sent is the counter ++ def init_hw_counter(self, active_devices): ++ port_keys = {} ++ port_keys['1'] = 0 ++ port_keys['2'] = 0 ++ for i in self.hw_counter: ++ token = i.split('/') ++ if len(token) > 8: ++ mlx = token[4] ++ port = token[6] ++ if mlx not in active_devices: ++ continue ++ self.hw_counter_path.append(i.strip()) ++ counter = token[8].strip() ++ if mlx not in self.hw_counter_hash: ++ self.hw_counter_hash[mlx] = {} ++ if port not in self.hw_counter_hash[mlx]: ++ self.hw_counter_hash[mlx][port] = {} ++ self.hw_counter_hash[mlx][port][counter] = 0 ++ port_keys[port] += 1 ++ ++ # get_hw_counters probes the counter periodically and updates the counter stats ++ def get_hw_counters(self, counter_path, hw_counter_vals): ++ for path in counter_path: ++ with open(path, 'r') as fread: ++ ret = fread.read().strip() ++ ++ token = path.split('/') ++ mlx, port, counter = token[4], token[6], token[8] ++ hw_counter_vals[mlx][port][counter] = int(ret) ++ ++ return hw_counter_vals ++ ++ def convert_dict(self, input_dict): ++ output_dict = {} ++ for key1, sub_dict in input_dict.items(): ++ if isinstance(sub_dict, dict): ++ for key2, value in sub_dict.items(): ++ new_key = f"{key1}_{key2}" ++ output_dict[new_key] = value ++ return output_dict ++ ++ def fetch_mlnx_counters(self): ++ self.hw_counter = self.get_hw_counter_names() ++ active_device = self.get_active_devices() ++ ++ self.init_hw_counter(active_device) ++ ++ self.hw_counter_hash = self.get_hw_counters(self.hw_counter_path, self.hw_counter_hash) ++ return self.convert_dict(self.hw_counter_hash) ++ ++ ++class ROCESTATPMDA(PMDA): ++ """ ++ A ROCESTAT Performance Metrics Domain Agent. ++ """ ++ ++ # Cluster definitions ++ CLUSTER_HW = 1 ++ CLUSTER_HW_XMIT = 2 ++ CLUSTER_HW_RCV = 3 ++ CLUSTER_HW_UCAST = 4 ++ CLUSTER_HW_MCAST = 5 ++ CLUSTER_HW_REQ = 6 ++ CLUSTER_HW_RESP = 7 ++ CLUSTER_HW_RNR = 8 ++ CLUSTER_HW_LINK = 9 ++ ++ CLUSTER_NIC = 10 ++ ++ mlnx_counters = {} ++ nic_stats = {} ++ ++ def register_metrics(self, name): ++ """ ++ Register Metrics ++ """ ++ pm_type_32 = c_api.PM_TYPE_U32 ++ pm_type_64 = c_api.PM_TYPE_U64 ++ ++ inst_sem = c_api.PM_SEM_INSTANT ++ ++ pm_units = pmUnits(0,0,0,0,0,0) ++ byte_units = pmUnits(1,0,0,c_api.PM_SPACE_BYTE,0,0) ++ count_units = pmUnits(0,0,1,0,0,0) ++ ++ # Registering MLNX HW Counters ++ self.mlnx_indom = self.indom(0) ++ self.add_indom(pmdaIndom(self.mlnx_indom, self.mlnx_counters)) ++ ++ hw_metrics = [(f"{name}.hw.{dev_attr[0]}", self.CLUSTER_HW, dev_attr[1]) for dev_attr in RoceStatHW] ++ hw_metrics += [(f"{name}.hw.xmit.{dev_attr[0]}", self.CLUSTER_HW_XMIT, dev_attr[1]) for dev_attr in RoceStatXmit] ++ hw_metrics += [(f"{name}.hw.rcv.{dev_attr[0]}", self.CLUSTER_HW_RCV, dev_attr[1]) for dev_attr in RoceStatRcv] ++ hw_metrics += [(f"{name}.hw.ucast.{dev_attr[0]}", self.CLUSTER_HW_UCAST, dev_attr[1]) for dev_attr in RoceStatUcast] ++ hw_metrics += [(f"{name}.hw.mcast.{dev_attr[0]}", self.CLUSTER_HW_MCAST, dev_attr[1]) for dev_attr in RoceStatMcast] ++ hw_metrics += [(f"{name}.hw.req.{dev_attr[0]}", self.CLUSTER_HW_REQ, dev_attr[1]) for dev_attr in RoceStatReq] ++ hw_metrics += [(f"{name}.hw.resp.{dev_attr[0]}", self.CLUSTER_HW_RESP, dev_attr[1]) for dev_attr in RoceStatResp] ++ hw_metrics += [(f"{name}.hw.rnr.{dev_attr[0]}", self.CLUSTER_HW_RNR, dev_attr[1]) for dev_attr in RoceStatRnr] ++ hw_metrics += [(f"{name}.hw.link.{dev_attr[0]}", self.CLUSTER_HW_LINK, dev_attr[1]) for dev_attr in RoceStatLink] ++ ++ for idx, (metric_name, metric_cluster, metric_type) in enumerate(hw_metrics, 0): ++ if metric_type == c_int: ++ metric_type = pm_type_32 ++ elif metric_type == c_ulonglong: ++ metric_type = pm_type_64 ++ ++ if "data" in metric_name: ++ pm_units = byte_units ++ else: ++ pm_units = count_units ++ ++ self.add_metric(metric_name, ++ pmdaMetric(self.pmid(metric_cluster, idx), ++ metric_type, ++ self.mlnx_indom, inst_sem, pm_units)) ++ ++ # Register Per Lane NIC stats ++ self.nic_indom = self.indom(1) ++ self.add_indom(pmdaIndom(self.nic_indom, self.nic_stats)) ++ ++ nic_metric = [f"{name}.lane.{dev_attr[0]}" for dev_attr in NicStats._fields_] ++ ++ for idx, metric_name in enumerate(nic_metric, 0): ++ if "bytes" in metric_name: ++ pm_units = byte_units ++ else: ++ pm_units = count_units ++ ++ self.add_metric(metric_name, ++ pmdaMetric(self.pmid(self.CLUSTER_NIC, idx), ++ pm_type_64, ++ self.nic_indom, inst_sem, pm_units)) ++ ++ def rocestat_fetch_mlnx_counters(self, item, inst): ++ ++ voidp = self.inst_lookup(self.mlnx_indom, inst) ++ ++ if voidp is None: ++ return [c_api.PM_ERR_INST, 0] ++ ++ cache = cast(voidp, POINTER(RoceDev)) ++ item_lookup = [dev_attr[0] for dev_attr in RoceDev._fields_] ++ dev = cache.contents ++ ++ if 0 <= item < len(RoceDev._fields_): ++ return [getattr(dev, item_lookup[item]), 1] ++ else: ++ return [c_api.PM_ERR_INST, 0] ++ ++ def rocestat_fetch_nic_stats(self, item, inst): ++ ++ voidp = self.inst_lookup(self.nic_indom, inst) ++ ++ if voidp is None: ++ return [c_api.PM_ERR_INST, 0] ++ ++ cache = cast(voidp, POINTER(NicStats)) ++ item_lookup = [dev_attr[0] for dev_attr in NicStats._fields_] ++ nic = cache.contents ++ ++ if 0 <= item < len(NicStats._fields_): ++ return [getattr(nic, item_lookup[item]), 1] ++ else: ++ return [c_api.PM_ERR_INST, 0] ++ ++ def refresh_mlnx_counters(self): ++ ''' Refresh the values and instances for mlnx devices''' ++ try: ++ roce_stat = MLNXCOUNTERS() ++ hw_cnts = roce_stat.fetch_mlnx_counters() ++ except: ++ self.log("failed to fetch mlnx counters") ++ ++ for device, dict_counters in hw_cnts.items(): ++ dev = RoceDev() ++ for k,v in dict_counters.items(): ++ setattr(dev, k, v) ++ ++ self.mlnx_counters[device] = dev ++ ++ def refresh_nic_stats(self): ++ try: ++ collector = NICSTATS() ++ nic_stats = collector.collect_stats() ++ except: ++ self.log("failed to fetch nic stats") ++ ++ for nic_lane, dict_stats in nic_stats.items(): ++ nic = NicStats() ++ for k,v in dict_stats.items(): ++ setattr(nic, k, v) ++ ++ self.nic_stats[nic_lane] = nic ++ ++ def refresh(self): ++ try: ++ self.refresh_mlnx_counters() ++ try: ++ self.refresh_nic_stats() ++ except: ++ self.log("Nic stats refresh failed") ++ except: ++ self.log("mlnx counters refresh failed") ++ ++ def roce_stat_fetch(self): ++ ''' Called once per "fetch" PDU ''' ++ self.refresh() ++ self.replace_indom(self.mlnx_indom, self.mlnx_counters) ++ self.replace_indom(self.nic_indom, self.nic_stats) ++ ++ def roce_fetch_callback(self, cluster, item, inst): ++ """ ++ Main fetch callback ++ """ ++ ++ if self.CLUSTER_HW <= cluster <= self.CLUSTER_HW_LINK: ++ return self.rocestat_fetch_mlnx_counters(item, inst) ++ ++ if cluster == self.CLUSTER_NIC: ++ return self.rocestat_fetch_nic_stats(item, inst) ++ ++ return [c_api.PM_ERR_PMID, 0] ++ ++ def __init__(self, name, domain, user, logfile): ++ ++ super().__init__(name, domain, logfile) ++ if user is not None: ++ self.set_user(user) ++ ++ self.connect_pmcd() ++ ++ self.pmda_name = name ++ ++ self.register_metrics(name) ++ self.set_fetch(self.roce_stat_fetch) ++ self.set_fetch_callback(self.roce_fetch_callback) ++ ++ @classmethod ++ def in_pmda_setup(cls): ++ """Check if PMDA is in setup state""" ++ return os.environ.get('PCP_PYTHON_DOMAIN') or os.environ.get('PCP_PYTHON_PMNS') ++ ++if __name__ == "__main__": ++ pmda = ROCESTATPMDA(name='rocestat', domain=254, user="root", logfile='rocestat.log') ++ ++ # Now enter the main loop ++ pmda.run() +diff --git a/src/pmdas/rocestat/pmns b/src/pmdas/rocestat/pmns +new file mode 100644 +index 0000000000..6eb997b7e1 +--- /dev/null ++++ b/src/pmdas/rocestat/pmns +@@ -0,0 +1 @@ ++rocestat 254:*:* +diff --git a/src/pmns/stdpmid.pcp b/src/pmns/stdpmid.pcp +index 41b2dfe51d..9a8e4d5bfa 100644 +--- a/src/pmns/stdpmid.pcp ++++ b/src/pmns/stdpmid.pcp +@@ -156,7 +156,7 @@ TRIVIAL 250 + FORQA 251 + QA_TEST_PERL 252 + SIMPLE 253 +-### FREE SLOT 254 ### ++ROCESTAT 254 + MEMORY_PYTHON 255 + ### MORE FREE SLOTS 256..510 ### + # +-- +2.43.5 diff --git a/SOURCES/1002-Merge-branch-pcp-rocestat-of-https-github.com-mohith.patch b/SOURCES/1002-Merge-branch-pcp-rocestat-of-https-github.com-mohith.patch new file mode 100644 index 0000000..750c2ad --- /dev/null +++ b/SOURCES/1002-Merge-branch-pcp-rocestat-of-https-github.com-mohith.patch @@ -0,0 +1,1093 @@ +From 35303835eceaf98e7fc45b9d1c6f478881a0be6c Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Tue, 24 Jun 2025 07:41:45 +1000 +Subject: [PATCH 2/2] Merge branch 'pcp-rocestat' of + https://github.com/mohith-kumar-thummaluru/pcp into + mohith-kumar-thummaluru-pcp-rocestat + +Orabug: 38109331 + +(cherry picked from commit 858814e52563652591d3b7bd6db900f7465e3549) +Signed-off-by: Mohith Kumar Thummaluru +--- + qa/1994 | 48 ++++ + qa/1994.out | 415 +++++++++++++++++++++++++++++++ + qa/archives/GNUmakefile | 2 +- + qa/archives/mk.rocestat | 79 ++++++ + qa/group | 2 + + src/pcp/GNUmakefile | 1 + + src/pcp/rocestat/GNUmakefile | 44 ++++ + src/pcp/rocestat/pcp-rocestat.1 | 78 ++++++ + src/pcp/rocestat/pcp-rocestat.py | 315 +++++++++++++++++++++++ + 9 files changed, 983 insertions(+), 1 deletion(-) + create mode 100755 qa/1994 + create mode 100644 qa/1994.out + create mode 100755 qa/archives/mk.rocestat + create mode 100755 src/pcp/rocestat/GNUmakefile + create mode 100755 src/pcp/rocestat/pcp-rocestat.1 + create mode 100755 src/pcp/rocestat/pcp-rocestat.py + +diff --git a/qa/1994 b/qa/1994 +new file mode 100755 +index 0000000000..51bf85d5fa +--- /dev/null ++++ b/qa/1994 +@@ -0,0 +1,48 @@ ++ ++#!/bin/sh ++# PCP QA Test No. 1994 ++# Exercise various pcp-rocestat(1) command options. ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++. ./common.python ++ ++status=1 # failure is the default! ++trap "cd $here; rm -rf $tmp.*; exit \$status" 0 1 2 3 15 ++ ++pcp_rocestat="$PCP_BINADM_DIR/pcp-rocestat" ++test -x $pcp_rocestat || _notrun "No pcp-rocestat(1) installed" ++pcp_rocestat="$python $pcp_rocestat" ++ ++# real QA test starts here ++echo && echo pcp-rocestat output : Display default output ++PCP_ARCHIVE="archives/pcp-rocestat" PCP_HOSTZONE=1 PCP_ORIGIN=1 $pcp_rocestat -s 2 ++ ++archive_first="-a archives/pcp-rocestat -z -O +1" ++ ++echo && echo pcp-rocestat output : Display all network statistics for nVidia RoCE devices. ++pcp $archive_first rocestat -s 2 ++ ++echo && echo pcp-rocestat output : Display hardware counter statistics for nVidia RoCE devices. ++pcp $archive_first rocestat --hw_stats -s 2 ++ ++echo && echo pcp-rocestat output : Display per lane statistics for nVidia RoCE devices. ++pcp $archive_first rocestat --lane_stats -s 2 ++ ++status=0 ++exit +diff --git a/qa/1994.out b/qa/1994.out +new file mode 100644 +index 0000000000..449e066325 +--- /dev/null ++++ b/qa/1994.out +@@ -0,0 +1,415 @@ ++QA output created by 1994 ++ ++pcp-rocestat output : Display default output ++zzz <06/19/25 23:21:52> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ ++zzz <06/19/25 23:21:53> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ ++ ++pcp-rocestat output : Display all network statistics for nVidia RoCE devices. ++zzz <06/19/25 23:21:52> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ ++zzz <06/19/25 23:21:53> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ ++ ++pcp-rocestat output : Display hardware counter statistics for nVidia RoCE devices. ++zzz <06/19/25 23:21:52> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ ++zzz <06/19/25 23:21:53> ++ Counter mlx5_0_1 mlx5_0_2 ++ link_error_recovery 0(0) 0(0) ++ link_downed 0(0) 0(0) ++ local_link_integrity_errors 0(0) 0(0) ++ rnr_nak_retry_err 0(0) 0(0) ++ resp_local_length_error 0(0) 0(0) ++ resp_cqe_error 0(0) 0(0) ++ resp_cqe_flush_error 0(0) 0(0) ++ resp_remote_access_errors 0(0) 0(0) ++ req_remote_invalid_request 0(0) 0(0) ++ req_cqe_error 0(0) 0(0) ++ req_cqe_flush_error 0(0) 0(0) ++ duplicate_request 0(0) 0(0) ++ rx_read_requests 0(0) 0(0) ++ rx_atomic_requests 0(0) 0(0) ++ req_remote_access_errors 0(0) 0(0) ++ rx_write_requests 0(0) 0(0) ++ multicast_rcv_packets 0(0) 0(0) ++ multicast_xmit_packets 0(0) 0(0) ++ unicast_rcv_packets 12406304(0) 6484416(0) ++ unicast_xmit_packets 11679454(0) 6478121(0) ++ port_rcv_errors 0(0) 0(0) ++ port_rcv_remote_physical_errors 0(0) 0(0) ++ port_rcv_packets 12406304(0) 6484618(0) ++ port_rcv_data 267669766(0) 271326623(0) ++ port_rcv_constraint_errors 0(0) 0(0) ++ port_rcv_switch_relay_errors 0(0) 0(0) ++ port_xmit_data 247591758(0) 270392723(0) ++ port_xmit_constraint_errors 0(0) 0(0) ++ port_xmit_wait 0(0) 0(0) ++ port_xmit_packets 11679454(0) 6478253(0) ++ port_xmit_discards 0(0) 0(0) ++ roce_slow_restart_trans 0(0) 0(0) ++ roce_slow_restart_cnps 0(0) 0(0) ++ roce_slow_restart 0(0) 0(0) ++ roce_adp_retrans_to 0(0) 0(0) ++ clear_counters 0(0) 0(0) ++ local_ack_timeout_err 0(0) 0(0) ++ lifespan 10(0) 10(0) ++ implied_nak_seq_err 0(0) 0(0) ++ packet_seq_err 0(0) 0(0) ++ roce_adp_retrans 0(0) 0(0) ++ out_of_buffer 2412(0) 340(0) ++ out_of_sequence 0(0) 0(0) ++ VL15_dropped 0(0) 0(0) ++ excessive_buffer_overrun_errors 0(0) 0(0) ++ symbol_error 0(0) 0(0) ++ ++ ++ ++pcp-rocestat output : Display per lane statistics for nVidia RoCE devices. ++zzz <06/19/25 23:21:52> ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ ++zzz <06/19/25 23:21:53> ++ Netdev Queue tx_bytes rx_bytes tx/s(Gbps) rx/s(Gbps) pause pause_delta ++ re0 Default(TCP) - 0 0 0 0 0 0 0 ++ re0 Normal Large - 1 0 0 0 0 0 0 ++ re0 VIP Small - 2 0 0 0 0 0 0 ++ re0 VIP Large - 3 0 0 0 0 0 0 ++ re0 Normal Small - 4 0 0 0 0 0 0 ++ re0 VIP Small - 5 0 0 0 0 0 0 ++ re0 Unused - 6 0 0 0 0 0 0 ++ re0 CNP - 7 0 0 0 0 0 0 ++ re1 Default(TCP) - 0 0 0 0 0 0 0 ++ re1 Normal Large - 1 0 0 0 0 0 0 ++ re1 VIP Small - 2 0 0 0 0 0 0 ++ re1 VIP Large - 3 0 0 0 0 0 0 ++ re1 Normal Small - 4 0 0 0 0 0 0 ++ re1 VIP Small - 5 0 0 0 0 0 0 ++ re1 Unused - 6 0 0 0 0 0 0 ++ re1 CNP - 7 0 0 0 0 0 0 ++ +diff --git a/qa/archives/GNUmakefile b/qa/archives/GNUmakefile +index a26d1b77ba..523812a2e8 100644 +--- a/qa/archives/GNUmakefile ++++ b/qa/archives/GNUmakefile +@@ -77,7 +77,7 @@ SCRIPTS = mk.diff mk.gap mk.mysql mk.schizo mk.foo+ \ + mk.log-derived mk.vmstat mk.rep mk.procsched mk.netstat \ + mk.ipcs mk.value-test mk.rank-pred mk.cputime mk.meminfo \ + mk.sample-labels mk.pmproxy mk.mmv.help mk.slabinfo mk.zoneinfo \ +- mk.atop-sar mk.pmstat ++ mk.atop-sar mk.pmstat mk.rocestat + + CONFIGS = config.verify config.shping YWhcCi.toium.config \ + kenj-pc-2.config reduce.config uwrap.config wrap.config \ +diff --git a/qa/archives/mk.rocestat b/qa/archives/mk.rocestat +new file mode 100755 +index 0000000000..b074c7706f +--- /dev/null ++++ b/qa/archives/mk.rocestat +@@ -0,0 +1,79 @@ ++#!/bin/sh ++# ++# remake the pcp-netstat archive ... ++# this archive is intended to be checked in and not remade, this script is ++# simply a record of how it was created ++# ++ ++. /etc/pcp.env ++ ++tmp=/var/tmp/$$ ++rm -f $tmp.* ++trap "rm -f $tmp.*; exit 0" 0 1 2 3 15 ++ ++cat <>$tmp.config ++ ++log mandatory on 5 sec { ++ rocestat.hw.link.link_error_recovery ++ rocestat.hw.link.link_downed ++ rocestat.hw.link.local_link_integrity_errors ++ rocestat.hw.rnr.rnr_nak_retry_err ++ rocestat.hw.resp.resp_local_length_error ++ rocestat.hw.resp.resp_cqe_error ++ rocestat.hw.resp.resp_cqe_flush_error ++ rocestat.hw.resp.resp_remote_access_errors ++ rocestat.hw.req.req_remote_invalid_request ++ rocestat.hw.req.req_cqe_error ++ rocestat.hw.req.req_cqe_flush_error ++ rocestat.hw.req.duplicate_request ++ rocestat.hw.req.rx_read_requests ++ rocestat.hw.req.rx_atomic_requests ++ rocestat.hw.req.req_remote_access_errors ++ rocestat.hw.req.rx_write_requests ++ rocestat.hw.mcast.multicast_rcv_packets ++ rocestat.hw.mcast.multicast_xmit_packets ++ rocestat.hw.ucast.unicast_rcv_packets ++ rocestat.hw.ucast.unicast_xmit_packets ++ rocestat.hw.rcv.port_rcv_errors ++ rocestat.hw.rcv.port_rcv_remote_physical_errors ++ rocestat.hw.rcv.port_rcv_packets ++ rocestat.hw.rcv.port_rcv_data ++ rocestat.hw.rcv.port_rcv_constraint_errors ++ rocestat.hw.rcv.port_rcv_switch_relay_errors ++ rocestat.hw.xmit.port_xmit_data ++ rocestat.hw.xmit.port_xmit_constraint_errors ++ rocestat.hw.xmit.port_xmit_wait ++ rocestat.hw.xmit.port_xmit_packets ++ rocestat.hw.xmit.port_xmit_discards ++ rocestat.hw.roce_slow_restart_trans ++ rocestat.hw.roce_slow_restart_cnps ++ rocestat.hw.roce_slow_restart ++ rocestat.hw.roce_adp_retrans_to ++ rocestat.hw.clear_counters ++ rocestat.hw.local_ack_timeout_err ++ rocestat.hw.lifespan ++ rocestat.hw.implied_nak_seq_err ++ rocestat.hw.packet_seq_err ++ rocestat.hw.roce_adp_retrans ++ rocestat.hw.out_of_buffer ++ rocestat.hw.out_of_sequence ++ rocestat.hw.VL15_dropped ++ rocestat.hw.excessive_buffer_overrun_errors ++ rocestat.hw.symbol_error ++ rocestat.lane.tx_bytes ++ rocestat.lane.rx_bytes ++ rocestat.lane.rx_pause ++} ++End-of-File ++ ++rm -f pcp-rocestat.0.xz pcp-rocestat.meta.xz pcp-rocestat.index.xz ++ ++if pmlogger -s 10 -c $tmp.config pcp-rocestat ++then ++ xz pcp-rocestat.0 ++ xz pcp-rocestat.meta ++ xz pcp-rocestat.index ++else ++ echo "Argh: pmlogger failed ..." ++ cat pmlogger.log ++fi +diff --git a/qa/group b/qa/group +index 137dba39f8..b2b9ad1aee 100644 +--- a/qa/group ++++ b/qa/group +@@ -383,6 +383,7 @@ meminfo + mpstat + netstat + ps ++rocestat + slabinfo + + # full test search +@@ -2217,8 +2218,9 @@ pmcd.pdu + 1991 pcp netstat python local + 1992 pmda.uwsgi local + 1993 pmda.rocestat local python ++1994 pcp rocestat python local + 2100 pmproxy local security + 2101 pmda.sockets local security + 2104 libpcp local security + 2105 libpcp pmcd local security pmcd.pdu + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/src/pcp/GNUmakefile b/src/pcp/GNUmakefile +index 09361a4103..da292695d3 100644 +--- a/src/pcp/GNUmakefile ++++ b/src/pcp/GNUmakefile +@@ -33,6 +33,7 @@ SUBDIRS = \ + pidstat \ + ps \ + python \ ++ rocestat \ + shping \ + slabinfo \ + ss \ +diff --git a/src/pcp/rocestat/GNUmakefile b/src/pcp/rocestat/GNUmakefile +new file mode 100755 +index 0000000000..0ead2d8eb7 +--- /dev/null ++++ b/src/pcp/rocestat/GNUmakefile +@@ -0,0 +1,44 @@ ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++ ++TOPDIR = ../../.. ++include $(TOPDIR)/src/include/builddefs ++ ++TARGET = pcp-rocestat ++SCRIPT = $(TARGET).py ++MAN_SECTION = 1 ++MAN_PAGES = $(TARGET).$(MAN_SECTION) ++MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION) ++ ++default: $(SCRIPT) $(MAN_PAGES) ++ ++include $(BUILDRULES) ++ ++install: default ++ifeq "$(HAVE_PYTHON)" "true" ++ $(INSTALL) -m 755 $(SCRIPT) $(PCP_BINADM_DIR)/$(TARGET) ++ @$(INSTALL_MAN) ++endif ++ ++default_pcp : default ++ ++install_pcp : install ++ ++check:: $(SCRIPT) ++ $(PYLINT) $^ ++ ++check :: $(MAN_PAGES) ++ $(MANLINT) $^ ++ +diff --git a/src/pcp/rocestat/pcp-rocestat.1 b/src/pcp/rocestat/pcp-rocestat.1 +new file mode 100755 +index 0000000000..a4b8ede416 +--- /dev/null ++++ b/src/pcp/rocestat/pcp-rocestat.1 +@@ -0,0 +1,78 @@ ++'\"! tbl | mmdoc ++'\"macro stdmacro ++.\" ++.\" Copyright (c) 2025 Oracle and/or its affiliates. ++.\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++.\" ++.\" This program is free software; you can redistribute it and/or modify it ++.\" under the terms of the GNU General Public License as published by the ++.\" Free Software Foundation; either version 2 of the License, or (at your ++.\" option) any later version. ++.\" ++.\" This program is distributed in the hope that it will be useful, but ++.\" WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++.\" or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++.\" for more details. ++.\" ++.TH PCP-ROCESTAT 1 "PCP" "Performance Co-Pilot" ++.SH NAME ++\f3pcp-rocestat\f1 \- Reports statistics for nVidia RoCE devices. ++.SH SYNOPSIS ++\f3pcp\f1 ++[\f2pcp\ options\f1] ++\f3rocestat\f1 ++\f2[--hw_stats|--lane_stats]\f2 ++[\f3\-s\f1 \f2samples\f1] ++[\f3\-a\f1 \f2archive\f1] ++[\f3\-S\f1 \f2start_time\f1] ++[\f3\-T\f1 \f2end_time\f1] ++ ++.SH DESCRIPTION ++The ++.B pcp-rocestat ++tool for viewing nVidia RoCE device counters and monitoring real-time throughput on local interfaces. ++This tool utilizes ethtool counters for each device and reports throughput per priority queue. ++By default ++.B pcp-rocestat ++reports live data for the local host. It has the capabilities to analyze the data on the archives as well. ++.SH OPTIONS ++.TP ++\fB\-\-hw_stats\fP ++Display hardware counters ++.TP ++\fB\-\-lane_stats\fP ++Display lane stats ++.TP ++\fB\-a\fP, \fB\-\-archive\fP ++Specify the archive to examine ++.TP ++\fB\-s\fP, \fB\-\-samples\fP ++Specify number of samples ++.TP ++\fB\-S\fP, \fB\-\-START TIME\fP ++Filter the samples from the archive from the given time ++.TP ++\fB\-T\fP, \fB\-\-END TIME\fP ++Filter the samples from the archive till the given time ++.TP ++\fB\-V\fR, \fB\-\-version\fR ++Display version number and exit. ++.TP ++\fB\-?\fR, \fB\-\-help\fR ++Display usage message and exit. ++.SH PCP ENVIRONMENT ++Environment variables with the prefix \fBPCP_\fP are used to parameterize ++the file and directory names used by PCP. ++On each installation, the ++file \fI/etc/pcp.conf\fP contains the local values for these variables. ++The \fB$PCP_CONF\fP variable may be used to specify an alternative ++configuration file, as described in \fBpcp.conf\fP(5). ++.PP ++For environment variables affecting PCP tools, see \fBpmGetOptions\fP(3). ++.SH SEE ALSO ++.BR PCPIntro (1), ++.BR pcp (1), ++.BR pmParseInterval (3) ++and ++.BR environ (7). ++ +diff --git a/src/pcp/rocestat/pcp-rocestat.py b/src/pcp/rocestat/pcp-rocestat.py +new file mode 100755 +index 0000000000..79bf98a7f3 +--- /dev/null ++++ b/src/pcp/rocestat/pcp-rocestat.py +@@ -0,0 +1,315 @@ ++#!/usr/bin/pmpython ++# ++# Copyright (c) 2025 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++# pylint: disable=bad-whitespace,too-many-arguments,too-many-lines, bad-continuation, line-too-long ++# pylint: disable=redefined-outer-name,unnecessary-lambda, wildcard-import, unused-wildcard-import ++# ++from datetime import datetime ++from pcp import pmapi ++from pcp import pmcc ++import sys ++import time ++ ++ROCESTAT_HW_METRICS = [ ++ 'rocestat.hw.link.link_error_recovery', ++ 'rocestat.hw.link.link_downed', ++ 'rocestat.hw.link.local_link_integrity_errors', ++ 'rocestat.hw.rnr.rnr_nak_retry_err', ++ 'rocestat.hw.resp.resp_local_length_error', ++ 'rocestat.hw.resp.resp_cqe_error', ++ 'rocestat.hw.resp.resp_cqe_flush_error', ++ 'rocestat.hw.resp.resp_remote_access_errors', ++ 'rocestat.hw.req.req_remote_invalid_request', ++ 'rocestat.hw.req.req_cqe_error', ++ 'rocestat.hw.req.req_cqe_flush_error', ++ 'rocestat.hw.req.duplicate_request', ++ 'rocestat.hw.req.rx_read_requests', ++ 'rocestat.hw.req.rx_atomic_requests', ++ 'rocestat.hw.req.req_remote_access_errors', ++ 'rocestat.hw.req.rx_write_requests', ++ 'rocestat.hw.mcast.multicast_rcv_packets', ++ 'rocestat.hw.mcast.multicast_xmit_packets', ++ 'rocestat.hw.ucast.unicast_rcv_packets', ++ 'rocestat.hw.ucast.unicast_xmit_packets', ++ 'rocestat.hw.rcv.port_rcv_errors', ++ 'rocestat.hw.rcv.port_rcv_remote_physical_errors', ++ 'rocestat.hw.rcv.port_rcv_packets', ++ 'rocestat.hw.rcv.port_rcv_data', ++ 'rocestat.hw.rcv.port_rcv_constraint_errors', ++ 'rocestat.hw.rcv.port_rcv_switch_relay_errors', ++ 'rocestat.hw.xmit.port_xmit_data', ++ 'rocestat.hw.xmit.port_xmit_constraint_errors', ++ 'rocestat.hw.xmit.port_xmit_wait', ++ 'rocestat.hw.xmit.port_xmit_packets', ++ 'rocestat.hw.xmit.port_xmit_discards', ++ 'rocestat.hw.roce_slow_restart_trans', ++ 'rocestat.hw.roce_slow_restart_cnps', ++ 'rocestat.hw.roce_slow_restart', ++ 'rocestat.hw.roce_adp_retrans_to', ++ 'rocestat.hw.clear_counters', ++ 'rocestat.hw.local_ack_timeout_err', ++ 'rocestat.hw.lifespan', ++ 'rocestat.hw.implied_nak_seq_err', ++ 'rocestat.hw.packet_seq_err', ++ 'rocestat.hw.roce_adp_retrans', ++ 'rocestat.hw.out_of_buffer', ++ 'rocestat.hw.out_of_sequence', ++ 'rocestat.hw.VL15_dropped', ++ 'rocestat.hw.excessive_buffer_overrun_errors', ++ 'rocestat.hw.symbol_error', ++] ++ ++ROCESTAT_HW_METRICS_DESC = [ ++ 'link_error_recovery', ++ 'link_downed', ++ 'local_link_integrity_errors', ++ 'rnr_nak_retry_err', ++ 'resp_local_length_error', ++ 'resp_cqe_error', ++ 'resp_cqe_flush_error', ++ 'resp_remote_access_errors', ++ 'req_remote_invalid_request', ++ 'req_cqe_error', ++ 'req_cqe_flush_error', ++ 'duplicate_request', ++ 'rx_read_requests', ++ 'rx_atomic_requests', ++ 'req_remote_access_errors', ++ 'rx_write_requests', ++ 'multicast_rcv_packets', ++ 'multicast_xmit_packets', ++ 'unicast_rcv_packets', ++ 'unicast_xmit_packets', ++ 'port_rcv_errors', ++ 'port_rcv_remote_physical_errors', ++ 'port_rcv_packets', ++ 'port_rcv_data', ++ 'port_rcv_constraint_errors', ++ 'port_rcv_switch_relay_errors', ++ 'port_xmit_data', ++ 'port_xmit_constraint_errors', ++ 'port_xmit_wait', ++ 'port_xmit_packets', ++ 'port_xmit_discards', ++ 'roce_slow_restart_trans', ++ 'roce_slow_restart_cnps', ++ 'roce_slow_restart', ++ 'roce_adp_retrans_to', ++ 'clear_counters', ++ 'local_ack_timeout_err', ++ 'lifespan', ++ 'implied_nak_seq_err', ++ 'packet_seq_err', ++ 'roce_adp_retrans', ++ 'out_of_buffer', ++ 'out_of_sequence', ++ 'VL15_dropped', ++ 'excessive_buffer_overrun_errors', ++ 'symbol_error', ++] ++ ++ROCESTAT_PER_LANE_METRICS = ['rocestat.lane.tx_bytes', 'rocestat.lane.rx_bytes', 'rocestat.lane.rx_pause'] ++ ++ ++class RoceStatOptions(pmapi.pmOptions): ++ context = None ++ timefmt = "%H:%M:%S" ++ samples = 0 ++ hw_stats_flag = False ++ lane_stats_flag = False ++ ++ def extra_options(self, opt, optarg, index): ++ if opt == "hw_stats": ++ RoceStatOptions.hw_stats_flag = True ++ ++ elif opt == "lane_stats": ++ RoceStatOptions.lane_stats_flag = True ++ ++ def __init__(self): ++ pmapi.pmOptions.__init__(self, "a:s:S:T:t:") ++ self.pmSetLongOption("hw_stats", 0, "", "", "Display hw counters") ++ self.pmSetLongOption("lane_stats", 0, "", "", "Display lane stats") ++ self.pmSetLongOptionStart() ++ self.pmSetLongOptionFinish() ++ self.pmSetLongOption("samples", 1, "s", "COUNT", ++ "Number of samples to collect") ++ self.pmSetLongOptionArchive() ++ self.pmSetLongOptionHelp() ++ ++ self.pmSetOptionCallback(self.extra_options) ++ ++ ++class ROCeStatReport: ++ prev_counter_data = None ++ def __init__(self, samples): ++ self.samples = samples ++ ++ def report(self, manager): ++ group = manager["rocestat"] ++ ++ opts.pmGetOptionSamples() ++ ++ timestamp = manager.pmLocaltime(group.timestamp.tv_sec) ++ time_string = time.strftime("%x", timestamp.struct_time()) + " " ++ t_s = group.contextCache.pmLocaltime(int(group.timestamp)) ++ time_string += time.strftime(RoceStatOptions.timefmt, t_s.struct_time()) ++ print("zzz <%s>" % (time_string)) ++ hw_stats_flag = RoceStatOptions.hw_stats_flag ++ lane_stats_flag = RoceStatOptions.lane_stats_flag ++ ++ # HW Counters ++ if hw_stats_flag or (hw_stats_flag == lane_stats_flag): ++ header = ["Counter"] ++ for val in group[ROCESTAT_HW_METRICS[0]].netValues: ++ if not val[1].endswith("_delta"): ++ header.append(val[1]) ++ card_idxs = {} ++ for i in range(1, len(header)): ++ card_idxs[header[i]] = i ++ prev_counter_data = self.prev_counter_data ++ curr_counter_data = {} ++ curr_counter_data["ts"] = datetime.now() ++ for card in card_idxs: ++ curr_counter_data[card] = {} ++ ++ for metric_name in ROCESTAT_HW_METRICS_DESC: ++ metric_idx = ROCESTAT_HW_METRICS_DESC.index(metric_name) ++ metric = ROCESTAT_HW_METRICS[metric_idx] ++ for val in group[metric].netValues: ++ card_name, value = val[1], val[2] ++ curr_counter_data[card_name][metric_name] = value ++ ++ counters = [] ++ for counter in ROCESTAT_HW_METRICS_DESC: ++ row = [counter] + ["" for _ in range(len(card_idxs.keys()))] ++ for card, card_data in curr_counter_data.items(): ++ if card == "ts": ++ continue ++ card_idx = card_idxs[card] ++ count = card_data[counter] ++ if prev_counter_data is not None and card in prev_counter_data: ++ count_delta = count - prev_counter_data[card][counter] ++ time_diff = (curr_counter_data["ts"] - prev_counter_data["ts"]).total_seconds() ++ count_delta /= time_diff ++ if "data" in counter: ++ count_delta = self.convert_to_gbps(count_delta) ++ count_delta = round(count_delta, 2) ++ if int(count_delta) == count_delta: ++ count_delta = int(count_delta) ++ else: ++ count_delta = 0 ++ row[card_idx] = f"{count}({count_delta})" ++ counters.append(row) ++ self.prev_counter_data = curr_counter_data ++ ++ space = 5 + max(len(counter) ++ for counter in ROCESTAT_HW_METRICS_DESC) ++ print_format = f"%{space}s " + \ ++ " ".join(["%25s" for _ in range(len(header)-1)]) ++ if len(header) > 1: ++ print(print_format % tuple(header)) ++ for counter in counters: ++ print(print_format % tuple(counter)) ++ print() ++ ++ # Per lane stats ++ if lane_stats_flag or (lane_stats_flag == hw_stats_flag): ++ lane_stats = {} ++ for metric in ROCESTAT_PER_LANE_METRICS: ++ idx = 0 ++ try: ++ val = group[metric].netValues ++ except IndexError: ++ idx += 1 ++ continue ++ ++ for elem in val: ++ lane = elem[1] ++ value = elem[2] ++ ++ if lane not in lane_stats: ++ lane_stats[lane] = { ++ m: 0 for m in ROCESTAT_PER_LANE_METRICS} ++ lane_stats[lane][metric] = value ++ ++ if lane_stats: ++ print("%10s %20s %12s %12s %12s %12s %12s %12s" % ( ++ "Netdev", "Queue", "tx_bytes", "rx_bytes", "tx/s(Gbps)", "rx/s(Gbps)", "pause", "pause_delta")) ++ for lane, metrics in lane_stats.items(): ++ netdev, queue = self.split_lane(lane) ++ tx_bw_gbps = self.convert_to_gbps( ++ metrics.get('rocestat.lane.tx_bytes_bw', 0)) ++ rx_bw_gbps = self.convert_to_gbps( ++ metrics.get('rocestat.lane.rx_bytes_bw', 0)) ++ print("%10s %20s %12s %12s %12s %12s %12s %12s" % ( ++ netdev, queue, ++ metrics.get('tx_bytes', 0), ++ metrics.get('rx_bytes', 0), ++ tx_bw_gbps, ++ rx_bw_gbps, ++ metrics.get('rx_pause', 0), ++ metrics.get('rx_pause_delta', 0) ++ )) ++ print() ++ ++ def split_lane(self, lane): ++ parts = lane.split('_') ++ if len(parts) == 2: ++ netdev = parts[0] ++ queue_id = parts[1].replace("lane", "") ++ ++ queue_name = { ++ "0": "Default(TCP) - 0", ++ "1": "Normal Large - 1", ++ "2": "VIP Small - 2", ++ "3": "VIP Large - 3", ++ "4": "Normal Small - 4", ++ "5": "VIP Small - 5", ++ "6": "Unused - 6", ++ "7": "CNP - 7" ++ }.get(queue_id, f"Queue - {queue_id}") ++ ++ return netdev, queue_name ++ return lane, "Unknown Queue" ++ ++ def convert_to_gbps(self, value): ++ if value == 0: ++ return 0 ++ return value / 1000000000 ++ ++ ++if __name__ == '__main__': ++ try: ++ opts = RoceStatOptions() ++ mngr = pmcc.MetricGroupManager.builder(opts, sys.argv) ++ RoceStatOptions.context = mngr.type ++ missing = mngr.checkMissingMetrics( ++ ROCESTAT_HW_METRICS + ROCESTAT_PER_LANE_METRICS) ++ if missing is not None: ++ sys.stderr.write( ++ 'Error: not all required metrics are available\nMissing %s\n' % missing) ++ sys.exit(1) ++ mngr["rocestat"] = ROCESTAT_HW_METRICS + ROCESTAT_PER_LANE_METRICS ++ mngr.printer = ROCeStatReport(opts.samples) ++ sts = mngr.run() ++ sys.exit(sts) ++ ++ except pmapi.pmErr as error: ++ sys.stderr.write("%s %s\n" % (error.progname(), error.message())) ++ except pmapi.pmUsageErr as usage: ++ usage.message() ++ sys.exit(1) ++ except KeyboardInterrupt: ++ pass +-- +2.43.5 diff --git a/SOURCES/1003-pcp-meminfo-additional-metrics-added-for-mem.util-to.patch b/SOURCES/1003-pcp-meminfo-additional-metrics-added-for-mem.util-to.patch new file mode 100644 index 0000000..9c554e7 --- /dev/null +++ b/SOURCES/1003-pcp-meminfo-additional-metrics-added-for-mem.util-to.patch @@ -0,0 +1,56 @@ +From e1c81c356a176fa244b3b32a2b98362d75fd2842 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Thu, 9 Oct 2025 20:30:48 +0530 +Subject: [PATCH] pcp-meminfo: additional metrics added for mem.util to show + newly added kernel metrics by following commit + 1c559713ebc5c02759de59013066c3d29e4395e4 + +Signed-off-by: sagar sagar + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/pull/2180/commits/43025e54a97cb8aa2a3cc955a9fb1d0cb3f39cf4 + +Orabug: 38526351 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/meminfo/pcp-meminfo.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/src/pcp/meminfo/pcp-meminfo.py b/src/pcp/meminfo/pcp-meminfo.py +index 103aa52..bae8931 100755 +--- a/src/pcp/meminfo/pcp-meminfo.py ++++ b/src/pcp/meminfo/pcp-meminfo.py +@@ -57,10 +57,14 @@ METRICS = ["mem.physmem", + "mem.util.vmallocTotal", + "mem.util.vmallocUsed", + "mem.util.vmallocChunk", ++ "mem.util.percpu", + "mem.util.corrupthardware", + "mem.util.anonhugepages", + "mem.vmstat.nr_shmem_hugepages", + "mem.vmstat.nr_shmem_pmdmapped", ++ "mem.util.filehugepages", ++ "mem.util.filepmdmapped", ++ "mem.util.cmatotal", + "mem.zoneinfo.nr_free_cma", + "mem.util.hugepagesTotal", + "mem.util.hugepagesFree", +@@ -105,10 +109,14 @@ METRICS_DESC = ["MemTotal", + "VmallocTotal", + "VmallocUsed", + "VmallocChunk", ++ "Percpu", + "HardwareCorrupted", + "AnonHugePages", + "ShmemHugePages", + "ShmemPmdMapped", ++ "FileHugePages", ++ "FilePmdMapped", ++ "CmaTotal", + "CmaFree", + "HugePages_Total_NO_kb", + "HugePages_Free_NO_kb", +-- +2.43.7 + diff --git a/SOURCES/1004-meminfo-added-kreclaimable-and-hugtlb-metrics.patch b/SOURCES/1004-meminfo-added-kreclaimable-and-hugtlb-metrics.patch new file mode 100644 index 0000000..7ed4496 --- /dev/null +++ b/SOURCES/1004-meminfo-added-kreclaimable-and-hugtlb-metrics.patch @@ -0,0 +1,170 @@ +From 975ec9e98e8ccbfbb2bfe5652920b3b3cd5208fe Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Thu, 9 Oct 2025 20:44:30 +0530 +Subject: [PATCH] meminfo: Added mem.util.kreclaimable and mem.util.hugetlb + metrics for meminfo + +updated pcp-meminfo tool as well two show these metrics +data + +Signed-off-by: sagar sagar + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/pull/2180/commits/81393407ec8d31642f637ab25eb3f9c082dab821 + +Orabug: 38526351 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/meminfo/pcp-meminfo.py | 4 ++++ + src/pmdas/linux/help | 3 +++ + src/pmdas/linux/pmda.c | 20 ++++++++++++++++++++ + src/pmdas/linux/proc_meminfo.c | 2 ++ + src/pmdas/linux/proc_meminfo.h | 2 ++ + src/pmdas/linux/root_linux | 2 ++ + 6 files changed, 33 insertions(+) + +diff --git a/src/pcp/meminfo/pcp-meminfo.py b/src/pcp/meminfo/pcp-meminfo.py +index bae8931..8492859 100755 +--- a/src/pcp/meminfo/pcp-meminfo.py ++++ b/src/pcp/meminfo/pcp-meminfo.py +@@ -44,6 +44,7 @@ METRICS = ["mem.physmem", + "mem.util.anonpages", + "mem.util.mapped", + "mem.util.shared", ++ "mem.util.kreclaimable", + "mem.util.slab", + "mem.util.slabReclaimable", + "mem.util.slabUnreclaimable", +@@ -71,6 +72,7 @@ METRICS = ["mem.physmem", + "mem.util.hugepagesRsvd", + "mem.util.hugepagesSurp", + "hinv.hugepagesize", ++ "mem.util.hugetlb", + "mem.util.directMap4k", + "mem.util.directMap2M", + "mem.util.directMap1G"] +@@ -96,6 +98,7 @@ METRICS_DESC = ["MemTotal", + "AnonPages", + "Mapped", + "Shmem", ++ "KReclaimable", + "Slab", + "SReclaimable", + "SUnreclaim", +@@ -123,6 +126,7 @@ METRICS_DESC = ["MemTotal", + "HugePages_Rsvd_NO_kb", + "HugePages_Surp_NO_kb", + "Hugepagesize", ++ "Hugetlb", + "DirectMap4k", + "DirectMap2M", + "DirectMap1G"] +diff --git a/src/pmdas/linux/help b/src/pmdas/linux/help +index 0429eeb..7c4b99d 100644 +--- a/src/pmdas/linux/help ++++ b/src/pmdas/linux/help +@@ -993,6 +993,9 @@ corruption of the call stack, allowing the kernel to react to such an + attach in an appropriate fashion. Shadow stacks are often maintained + by the processor hardware and require additional stack memory. + @ mem.util.percpu amount of per CPU allocator memory ++@ mem.util.kreclaimable Kbytes in kernel reclaimable memory, from /proc/meminfo ++Kernel allocations that the kernel will attempt to reclaim under memory pressure. ++@ mem.util.hugetlb the total amount of memory (in kB), consumed by huge pages of all sizes. + + @ mem.numa.util.total per-node total memory + @ mem.numa.util.free per-node free memory +diff --git a/src/pmdas/linux/pmda.c b/src/pmdas/linux/pmda.c +index d7c05c2..399f7f6 100644 +--- a/src/pmdas/linux/pmda.c ++++ b/src/pmdas/linux/pmda.c +@@ -1196,6 +1196,16 @@ static pmdaMetric metrictab[] = { + { PMDA_PMID(CLUSTER_MEMINFO,74), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, + ++/* mem.util.kreclaimable */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_MEMINFO,75), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.util.hugetlb */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_MEMINFO,76), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ + /* mem.numa.util.total */ + { NULL, + { PMDA_PMID(CLUSTER_NUMA_MEMINFO,0), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, +@@ -8755,6 +8765,16 @@ linux_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) + return 0; /* no values available */ + atom->ull = proc_meminfo.Percpu; + break; ++ case 75: /* mem.util.kreclaimable (in kbytes) */ ++ if (!MEMINFO_VALID_VALUE(proc_meminfo.KReclaimable)) ++ return 0; /* no values available */ ++ atom->ull = proc_meminfo.KReclaimable; ++ break; ++ case 76: /* mem.util.hugetlb (in kbytes) */ ++ if (!MEMINFO_VALID_VALUE(proc_meminfo.Hugetlb)) ++ return 0; /* no values available */ ++ atom->ull = proc_meminfo.Hugetlb; ++ break; + default: + return PM_ERR_PMID; + } +diff --git a/src/pmdas/linux/proc_meminfo.c b/src/pmdas/linux/proc_meminfo.c +index 7d9f5ce..8ae2936 100644 +--- a/src/pmdas/linux/proc_meminfo.c ++++ b/src/pmdas/linux/proc_meminfo.c +@@ -54,6 +54,7 @@ static struct { + { "AnonPages", &moff.AnonPages }, + { "Mapped", &moff.Mapped }, + { "Shmem", &moff.Shmem }, ++ { "KReclaimable", &moff.KReclaimable }, + { "Slab", &moff.Slab }, + { "SReclaimable", &moff.SlabReclaimable }, + { "SUnreclaim", &moff.SlabUnreclaimable }, +@@ -87,6 +88,7 @@ static struct { + { "HugePages_Rsvd", &moff.HugepagesRsvd }, + { "HugePages_Surp", &moff.HugepagesSurp }, + { "Hugepagesize", &moff.Hugepagesize }, ++ { "Hugetlb", &moff.Hugetlb }, + { "DirectMap4k", &moff.directMap4k }, + { "DirectMap2M", &moff.directMap2M }, + { "DirectMap1G", &moff.directMap1G }, +diff --git a/src/pmdas/linux/proc_meminfo.h b/src/pmdas/linux/proc_meminfo.h +index 321594c..17e504f 100644 +--- a/src/pmdas/linux/proc_meminfo.h ++++ b/src/pmdas/linux/proc_meminfo.h +@@ -51,6 +51,7 @@ typedef struct { + int64_t Writeback; + int64_t Mapped; + int64_t Shmem; ++ int64_t KReclaimable; + int64_t Slab; + int64_t SlabReclaimable; + int64_t SlabUnreclaimable; +@@ -84,6 +85,7 @@ typedef struct { + int64_t HugepagesRsvd; + int64_t HugepagesSurp; + int64_t Hugepagesize; ++ int64_t Hugetlb; + int64_t directMap4k; + int64_t directMap2M; + int64_t directMap1G; +diff --git a/src/pmdas/linux/root_linux b/src/pmdas/linux/root_linux +index 38e63c3..ee5d7ad 100644 +--- a/src/pmdas/linux/root_linux ++++ b/src/pmdas/linux/root_linux +@@ -746,6 +746,8 @@ mem.util { + zswapped 60:1:72 + shadowcallstack 60:1:73 + percpu 60:1:74 ++ kreclaimable 60:1:75 ++ hugetlb 60:1:76 + } + + mem.numa { +-- +2.43.7 + diff --git a/SOURCES/1005-fix-mpstat-showing-inconsistent-values.patch b/SOURCES/1005-fix-mpstat-showing-inconsistent-values.patch new file mode 100644 index 0000000..8cc31c2 --- /dev/null +++ b/SOURCES/1005-fix-mpstat-showing-inconsistent-values.patch @@ -0,0 +1,267 @@ +From a02f695771e7534a90391fe243badb47b7b2371d Mon Sep 17 00:00:00 2001 +From: sagar sagar +Date: Thu, 28 Aug 2025 19:37:54 +0530 +Subject: [PATCH] pcp-mpstat: refactor to use vuser/vnice metrics and unify CPU + utilization calculation + +Switch all references in MPSTAT_METRICS from cpu.user/cpu.nice to the newer cpu.vuser/cpu.vnice metrics +for both kernel.all and kernel.percpu to be inline with mpstat tool. +Refactor all per-metric calculation methods in CoreCpuUtil to delegate to a new _compute_metric helper, +eliminating redundant code. +Optimize CPU count retrieval by caching the result of hinv.ncpu in the CoreCpuUtil class. +Remove unused or redundant calls to cpu_online, clarifying the core class interface. +Simplify and centralize the per-metric computation logic by using self._all_or_percpu() for proper metric +path construction and handling instance (per-core vs global) logic in a single location. + +Signed-off-by: sagar sagar + +Orabug: 38526328 + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/a02f695771e7534a90391fe243badb47b7b2371d + +Signed-off-by: Sourav Sharma + +--- + src/pcp/mpstat/pcp-mpstat.py | 184 ++++++++--------------------------- + 1 file changed, 42 insertions(+), 142 deletions(-) + +diff --git a/src/pcp/mpstat/pcp-mpstat.py b/src/pcp/mpstat/pcp-mpstat.py +index 89f308ac39..fed1b91ae1 100755 +--- a/src/pcp/mpstat/pcp-mpstat.py ++++ b/src/pcp/mpstat/pcp-mpstat.py +@@ -22,14 +22,14 @@ + import time + MPSTAT_METRICS = ['kernel.uname.nodename', 'kernel.uname.release', 'kernel.uname.sysname', + 'kernel.uname.machine', 'hinv.map.cpu_num', 'hinv.ncpu', 'hinv.cpu.online', +- 'kernel.all.cpu.user', +- 'kernel.all.cpu.nice', 'kernel.all.cpu.sys', 'kernel.all.cpu.wait.total', +- 'kernel.all.cpu.irq.hard', 'kernel.all.cpu.irq.soft', 'kernel.all.cpu.steal', +- 'kernel.all.cpu.guest', 'kernel.all.cpu.guest_nice', 'kernel.all.cpu.idle', +- 'kernel.percpu.cpu.user', 'kernel.percpu.cpu.nice', 'kernel.percpu.cpu.sys', +- 'kernel.percpu.cpu.wait.total', 'kernel.percpu.cpu.irq.hard', 'kernel.percpu.cpu.irq.soft', +- 'kernel.percpu.cpu.steal', 'kernel.percpu.cpu.guest','kernel.percpu.cpu.guest_nice', +- 'kernel.percpu.cpu.idle', 'kernel.all.intr', 'kernel.percpu.intr'] ++ 'kernel.all.cpu.vuser', 'kernel.all.cpu.vnice', 'kernel.all.cpu.sys', ++ 'kernel.all.cpu.wait.total', 'kernel.all.cpu.irq.hard', 'kernel.all.cpu.irq.soft', ++ 'kernel.all.cpu.steal', 'kernel.all.cpu.guest', 'kernel.all.cpu.guest_nice', ++ 'kernel.all.cpu.idle','kernel.percpu.cpu.vuser','kernel.percpu.cpu.vnice', ++ 'kernel.percpu.cpu.sys','kernel.percpu.cpu.wait.total', 'kernel.percpu.cpu.irq.hard', ++ 'kernel.percpu.cpu.irq.soft','kernel.percpu.cpu.steal', 'kernel.percpu.cpu.guest', ++ 'kernel.percpu.cpu.guest_nice','kernel.percpu.cpu.idle', 'kernel.all.intr', 'kernel.percpu.intr'] ++ + interrupts_list = [] + soft_interrupts_list = [] + +@@ -126,170 +126,70 @@ def __fetch_previous_values(self,metric,instance): + + class CoreCpuUtil: + def __init__(self, instance, delta_time, metric_repository): +- self.delta_time = delta_time + self.instance = instance ++ self.delta_time = delta_time + self.metric_repository = metric_repository ++ self._total_cpus = None # Cache for performance + + def total_cpus(self): +- return self.metric_repository.current_value('hinv.ncpu', None) ++ if self._total_cpus is None: ++ self._total_cpus = self.metric_repository.current_value('hinv.ncpu', None) ++ return self._total_cpus ++ + def cpu_number(self): + return self.instance + +- def cpu_online(self): +- return self.metric_repository.current_value('hinv.cpu.online', self.instance) +- + def user_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.user' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- +- else: +- return None ++ return self._compute_metric('cpu.vuser') + + def nice_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.nice' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.vnice') + + def sys_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.sys' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.sys') + + def iowait_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.wait.total' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.wait.total') + + def irq_hard(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.irq.hard' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.irq.hard') + + def irq_soft(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.irq.soft' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.irq.soft') + + def steal(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.steal' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.steal') + + def guest_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.guest' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.guest') + + def guest_nice(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.guest_nice' +- p_time = self.metric_repository.previous_value(metric, self.instance) +- c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: +- return None +- return float("%.2f"%(value)) +- else: +- return None ++ return self._compute_metric('cpu.guest_nice') + + def idle_time(self): +- metric = 'kernel.' + self.__all_or_percpu() + '.cpu.idle' ++ return self._compute_metric('cpu.idle') ++ ++ def _compute_metric(self, metric_suffix): ++ metric = f'kernel.{self._all_or_percpu()}.{metric_suffix}' + p_time = self.metric_repository.previous_value(metric, self.instance) + c_time = self.metric_repository.current_value(metric, self.instance) +- if p_time is not None and c_time is not None: +- value = (100*(c_time - p_time))/(1000*self.delta_time) +- if self.instance is None and self.total_cpus() is not None: +- return float("%.2f"%(value/self.total_cpus())) +- else: +- if self.total_cpus() is None: ++ ++ if p_time is None or c_time is None or self.delta_time == 0: ++ return None ++ ++ try: ++ value = (100 * (c_time - p_time)) / (1000 * self.delta_time) ++ if self.instance is None: ++ total = self.total_cpus() ++ if total: ++ value /= total ++ else: + return None +- return float("%.2f"%(value)) +- else: ++ return min (round(value, 2),100) ++ except (ZeroDivisionError, TypeError): + return None + +- def __all_or_percpu(self): ++ def _all_or_percpu(self): + return 'all' if self.instance is None else 'percpu' + + class CpuUtil: +@@ -652,7 +552,7 @@ def get_summary_metrics(self,group): + def report(self,manager): + try: + group = manager['mpstat'] +- if group['kernel.all.cpu.user'].netPrevValues is None: ++ if group['kernel.all.cpu.vuser'].netPrevValues is None: + # need two fetches to report rate converted counter metrics + self.get_summary_metrics(group) + return diff --git a/SOURCES/1006-fix-broken-pipe-error-iostat.patch b/SOURCES/1006-fix-broken-pipe-error-iostat.patch new file mode 100644 index 0000000..6aa5115 --- /dev/null +++ b/SOURCES/1006-fix-broken-pipe-error-iostat.patch @@ -0,0 +1,29 @@ +From 64cfffa8777d349d58aac07684abe0d834695e22 Mon Sep 17 00:00:00 2001 +From: sagar sagar +Date: Fri, 2 May 2025 14:54:08 +0530 +Subject: [PATCH] pcp-iostat:fixed broken pipe issue in pcp-iostat utility. + +Signed-off-by: sagar sagar + +Orabug: 38526381 + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/pull/2200/commits/64cfffa8777d349d58aac07684abe0d834695e22 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/iostat/pcp-iostat.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/src/pcp/iostat/pcp-iostat.py b/src/pcp/iostat/pcp-iostat.py +index 909f2ac70d..ae503ed83f 100755 +--- a/src/pcp/iostat/pcp-iostat.py ++++ b/src/pcp/iostat/pcp-iostat.py +@@ -461,5 +461,7 @@ def __init__(self): + except pmapi.pmUsageErr as usage: + usage.message() + sys.exit(1) ++ except IOError: ++ signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except KeyboardInterrupt: + pass diff --git a/SOURCES/1007-add-missing-numastat-metrics.patch b/SOURCES/1007-add-missing-numastat-metrics.patch new file mode 100644 index 0000000..e664200 --- /dev/null +++ b/SOURCES/1007-add-missing-numastat-metrics.patch @@ -0,0 +1,220 @@ +From 8e6a5ac18c06f813bad74494134ad5ac2202684e Mon Sep 17 00:00:00 2001 +From: Sagar Sagar +Date: Mon, 22 Sep 2025 15:39:18 +0000 +Subject: [PATCH] Added new metrics for numastat per node + +newly added metrics are follows:- +mem.numa.util.swapCached +mem.numa.util.kreclaimable +mem.numa.util.anonhugepages +mem.numa.util.shmemhugepages +mem.numa.util.shmempmdmapped +mem.numa.util.filehugepages +mem.numa.util.filepmdmapped + +Fixed issue with mem.numa.util.mapped metric where it was not giving correct values. + +Signed-off-by: Sagar Sagar +Co-authored-by: Sourav Sharma + +Orabug: 38526450 + +Cherry-pick-commit: https://github.com/orasagar/pcp/commit/8e6a5ac18c06f813bad74494134ad5ac2202684e + +Signed-off-by: Sourav Sharma + +--- + src/pmdas/linux/help | 7 ++++ + src/pmdas/linux/numa_meminfo.c | 50 +++++++++++++++++------------ + src/pmdas/linux/pmda.c | 58 +++++++++++++++++++++++++++++++++- + src/pmdas/linux/root_linux | 7 ++++ + 4 files changed, 100 insertions(+), 22 deletions(-) + +diff --git a/src/pmdas/linux/help b/src/pmdas/linux/help +index c608d10ba5..482e099d66 100644 +--- a/src/pmdas/linux/help ++++ b/src/pmdas/linux/help +@@ -1030,6 +1030,13 @@ pressure. + @ mem.numa.util.hugepagesTotal per-node total count of hugepages + @ mem.numa.util.hugepagesFree per-node count of free hugepages + @ mem.numa.util.hugepagesSurp per-node count of surplus hugepages ++@ mem.numa.util.swapCached per-node amount of memory in swap cache ++@ mem.numa.util.kreclaimable per-node kernel reclaimable memory ++@ mem.numa.util.anonhugepages per-node amount of memory in anonymous huge pages ++@ mem.numa.util.shmemhugepages per-node amount of shared memory allocated with huge ++@ mem.numa.util.shmempmdmapped per-node shared memory mapped into userspace with hugepages ++@ mem.numa.util.filehugepages per-node page cache (file) pages allocated with hugepages ++@ mem.numa.util.filepmdmapped per-node page cache mapped into userspace with hugepages + @ mem.numa.alloc.hit per-node count of times a task wanted alloc on local node and succeeded + @ mem.numa.alloc.miss per-node count of times a task wanted alloc on local node but got another node + @ mem.numa.alloc.foreign count of times a task on another node alloced on that node, but got this node +diff --git a/src/pmdas/linux/numa_meminfo.c b/src/pmdas/linux/numa_meminfo.c +index 9024962b2e..461bed0965 100644 +--- a/src/pmdas/linux/numa_meminfo.c ++++ b/src/pmdas/linux/numa_meminfo.c +@@ -24,42 +24,50 @@ + + /* sysfs file for numa meminfo */ + static struct linux_table numa_meminfo_table[] = { +- { field: "MemTotal:", maxval: 0x0 }, +- { field: "MemFree:", maxval: 0x0 }, +- { field: "MemUsed:", maxval: 0x0 }, +- { field: "Active:", maxval: 0x0 }, +- { field: "Inactive:", maxval: 0x0 }, ++ { field: "MemTotal:", maxval: 0x0 }, ++ { field: "MemFree:", maxval: 0x0 }, ++ { field: "MemUsed:", maxval: 0x0 }, ++ { field: "SwapCached:", maxval: 0x0 }, ++ { field: "Active:", maxval: 0x0 }, ++ { field: "Inactive:", maxval: 0x0 }, + { field: "Active(anon):", maxval: 0x0 }, + { field: "Inactive(anon):", maxval: 0x0 }, + { field: "Active(file):", maxval: 0x0 }, + { field: "Inactive(file):", maxval: 0x0 }, +- { field: "HighTotal:", maxval: 0x0 }, +- { field: "HighFree:", maxval: 0x0 }, +- { field: "LowTotal:", maxval: 0x0 }, +- { field: "LowFree:", maxval: 0x0 }, ++ { field: "HighTotal:", maxval: 0x0 }, ++ { field: "HighFree:", maxval: 0x0 }, ++ { field: "LowTotal:", maxval: 0x0 }, ++ { field: "LowFree:", maxval: 0x0 }, + { field: "Unevictable:", maxval: 0x0 }, +- { field: "Mlocked:", maxval: 0x0 }, +- { field: "Dirty:", maxval: 0x0 }, +- { field: "Writeback:", maxval: 0x0 }, +- { field: "FilePages:", maxval: 0x0 }, +- { field: "Mapped:", maxval: 0x0 }, +- { field: "AnonPages:", maxval: 0x0 }, +- { field: "Shmem:", maxval: 0x0 }, ++ { field: "Mlocked:", maxval: 0x0 }, ++ { field: "Dirty:", maxval: 0x0 }, ++ { field: "Writeback:", maxval: 0x0 }, ++ { field: "FilePages:", maxval: 0x0 }, ++ { field: "AnonPages:", maxval: 0x0 }, ++ { field: "Shmem:", maxval: 0x0 }, + { field: "KernelStack:", maxval: 0x0 }, +- { field: "PageTables:", maxval: 0x0 }, ++ { field: "PageTables:", maxval: 0x0 }, ++ { field: "SecPageTables:", maxval: 0x0 }, + { field: "NFS_Unstable:", maxval: 0x0 }, +- { field: "Bounce:", maxval: 0x0 }, ++ { field: "Bounce:", maxval: 0x0 }, + { field: "WritebackTmp:", maxval: 0x0 }, +- { field: "Slab:", maxval: 0x0 }, ++ { field: "KReclaimable:", maxval: 0x0 }, ++ { field: "Slab:", maxval: 0x0 }, + { field: "SReclaimable:", maxval: 0x0 }, +- { field: "SUnreclaim:", maxval: 0x0 }, ++ { field: "SUnreclaim:", maxval: 0x0 }, ++ { field: "AnonHugePages:", maxval: 0x0 }, ++ { field: "ShmemHugePages:", maxval: 0x0 }, ++ { field: "ShmemPmdMapped:", maxval: 0x0 }, ++ { field: "FileHugePages:", maxval: 0x0 }, ++ { field: "FilePmdMapped:", maxval: 0x0 }, ++ { field: "Mapped:", maxval: 0x0 }, + { field: "HugePages_Total:", maxval: 0x0 }, + { field: "HugePages_Free:", maxval: 0x0 }, + { field: "HugePages_Surp:", maxval: 0x0 }, + { field: NULL } + }; + +-/* sysfs file for numastat */ ++/* sysfs file for numastat */ + static struct linux_table numa_memstat_table[] = { + { field: "numa_hit", maxval: ULONGLONG_MAX }, + { field: "numa_miss", maxval: ULONGLONG_MAX }, +diff --git a/src/pmdas/linux/pmda.c b/src/pmdas/linux/pmda.c +index 1026caf860..b30496b4b8 100644 +--- a/src/pmdas/linux/pmda.c ++++ b/src/pmdas/linux/pmda.c +@@ -1416,6 +1416,42 @@ static pmdaMetric metrictab[] = { + { PMDA_PMID(CLUSTER_NUMA_MEMINFO,41), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, + PMDA_PMUNITS(1,0,0,PM_SPACE_BYTE,0,0) }, }, + ++/* mem.numa.util.swapCached */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,42), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.kreclaimable */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,43), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.anonhugepages */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,44), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.shmemhugepages */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,45), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.shemempmdmapped */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,46), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.filehugepages */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,47), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++/* mem.numa.util.filepmdmapped */ ++ { NULL, ++ { PMDA_PMID(CLUSTER_NUMA_MEMINFO,48), PM_TYPE_U64, NODE_INDOM, PM_SEM_INSTANT, ++ PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0) }, }, ++ ++ + /* swap.length */ + { NULL, + { PMDA_PMID(CLUSTER_MEMINFO,6), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_INSTANT, +@@ -9952,7 +9988,27 @@ linux_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) + sts = linux_table_lookup("HugePages_Surp:", np->meminfo, &atom->ull); + atom->ull *= (proc_meminfo.Hugepagesize << 10); + break; +- ++ case 42: /* mem.numa.util.swapCached */ ++ sts = linux_table_lookup("SwapCached:", np->meminfo, &atom->ull); ++ break; ++ case 43: /* mem.numa.util.kreclaimable */ ++ sts = linux_table_lookup("KReclaimable:", np->meminfo, &atom->ull); ++ break; ++ case 44: /* mem.numa.util.anonhugepages */ ++ sts = linux_table_lookup("AnonHugePages:", np->meminfo, &atom->ull); ++ break; ++ case 45: /* mem.numa.util.shmemhugepages */ ++ sts = linux_table_lookup("ShmemHugePages:", np->meminfo, &atom->ull); ++ break; ++ case 46: /* mem.numa.util.shmempmdmapped */ ++ sts = linux_table_lookup("ShmemPmdMapped:", np->meminfo, &atom->ull); ++ break; ++ case 47: /* mem.numa.util.filehugepages */ ++ sts = linux_table_lookup("FileHugePages:", np->meminfo, &atom->ull); ++ break; ++ case 48: /* mem.numa.util.filepmdmapped */ ++ sts = linux_table_lookup("FilePmdMapped:", np->meminfo, &atom->ull); ++ break; + default: + return PM_ERR_PMID; + } +diff --git a/src/pmdas/linux/root_linux b/src/pmdas/linux/root_linux +index 1add9a43cf..054b77b67d 100644 +--- a/src/pmdas/linux/root_linux ++++ b/src/pmdas/linux/root_linux +@@ -793,6 +793,13 @@ mem.numa.util { + hugepagesTotalBytes 60:36:39 + hugepagesFreeBytes 60:36:40 + hugepagesSurpBytes 60:36:41 ++ swapCached 60:36:42 ++ kreclaimable 60:36:43 ++ anonhugepages 60:36:44 ++ shmemhugepages 60:36:45 ++ shmempmdmapped 60:36:46 ++ filehugepages 60:36:47 ++ filepmdmapped 60:36:48 + } + + mem.numa.alloc { diff --git a/SOURCES/1008-add-numastat-support-for-mn-options.patch b/SOURCES/1008-add-numastat-support-for-mn-options.patch new file mode 100644 index 0000000..cb88be7 --- /dev/null +++ b/SOURCES/1008-add-numastat-support-for-mn-options.patch @@ -0,0 +1,495 @@ +From b09b0435b4fafd56a5930de2adeeea4c7f45ea57 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Wed, 8 Oct 2025 13:17:18 +0530 +Subject: [PATCH] Modifies pcp numastat tool with pmcc metric group printer Add + support for -m and -n options in pcp numastat tool like numastat Provides + meminfo data per node + +Orabug: 38526434 + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/e4d513fd503cd3f0e7a2ccab0e0f069acf872427 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/numastat/pcp-numastat.1 | 8 +- + src/pcp/numastat/pcp-numastat.py | 414 +++++++++++++++------ + 2 files changed, 422 insertions(+), 1 deletions(-) + +diff --git a/src/pcp/numastat/pcp-numastat.1 b/src/pcp/numastat/pcp-numastat.1 +index d9165ea86e..65910a4877 100644 +--- a/src/pcp/numastat/pcp-numastat.1 ++++ b/src/pcp/numastat/pcp-numastat.1 +@@ -18,7 +18,7 @@ + \f3pcp-numastat\f1 \- report on NUMA memory allocation + .SH SYNOPSIS + \f3pcp\f1 [\f2pcp\ options\f1] \f3numastat\f1 +-[\f3\-Vw?\f1] ++[\fB-V\fR] [\fB-w\fR \fIwidth\fR] [\fB-m\fR][-n\fR] [\fB-?\fR] + .SH DESCRIPTION + .B pcp-numastat + displays NUMA allocation statistics from the kernel memory +@@ -69,6 +69,12 @@ Display the current version of the command. + Limit display to + .IR width . + .TP ++\fB-m\fR, \fB--meminfo\fR ++Display meminfo-like system-wide memory usage. ++.TP ++\fB-n\fR, \fB--numastat\fR ++Display the numastat statistics info. ++.TP + \fB\-?\fR, \fB\-\-help\fR + Display usage message and exit. + .SH NOTES +diff --git a/src/pcp/numastat/pcp-numastat.py b/src/pcp/numastat/pcp-numastat.py +index 46c2fcb97d..2a044c9bbd 100755 +--- a/src/pcp/numastat/pcp-numastat.py ++++ b/src/pcp/numastat/pcp-numastat.py +@@ -17,150 +17,328 @@ + """ Display NUMA memory allocation statistucs """ + + import os ++import signal + import sys ++import time ++ + from pcp import pmapi +-from cpmapi import PM_TYPE_U64, PM_CONTEXT_ARCHIVE ++from pcp import pmcc ++from cpmapi import PM_CONTEXT_ARCHIVE + + if sys.version >= '3': + long = int # python2 to python3 portability (no long() in python3) + xrange = range # more back-compat (xrange() is range() in python3) + +-class NUMAStat(object): +- """ Gives a short summary of per-node NUMA memory information. ++NUMA_METRICS = [ ++ "mem.numa.alloc.hit", ++ "mem.numa.alloc.miss", ++ "mem.numa.alloc.foreign", ++ "mem.numa.alloc.interleave_hit", ++ "mem.numa.alloc.local_node", ++ "mem.numa.alloc.other_node", ++] + +- Knows about some of the default PCP arguments - can function +- using remote hosts or historical data, using the timezone of +- the metric source, at an offset within an archive, and so on. +- """ ++MEM_METRICS = [ ++ "mem.numa.util.total", ++ "mem.numa.util.free", ++ "mem.numa.util.used", ++ "mem.numa.util.active", ++ "mem.numa.util.inactive", ++ "mem.numa.util.active_anon", ++ "mem.numa.util.inactive_anon", ++ "mem.numa.util.active_file", ++ "mem.numa.util.inactive_file", ++ "mem.numa.util.unevictable", ++ "mem.numa.util.mlocked", ++ "mem.numa.util.dirty", ++ "mem.numa.util.writeback", ++ "mem.numa.util.filePages", ++ "mem.numa.util.mapped", ++ "mem.numa.util.anonpages", ++ "mem.numa.util.shmem", ++ "mem.numa.util.kernelStack", ++ "mem.numa.util.pageTables", ++ "mem.numa.util.NFS_Unstable", ++ "mem.numa.util.bounce", ++ "mem.numa.util.writebackTmp", ++ "mem.numa.util.filehugepages", ++ "mem.numa.util.filepmdmapped", ++ "mem.numa.util.slab", ++ "mem.numa.util.slabReclaimable", ++ "mem.numa.util.slabUnreclaimable", ++ "mem.numa.util.anonhugepages", ++ "mem.numa.util.shmemhugepages", ++ "mem.numa.util.shmempmdmapped", ++ "mem.numa.util.hugepagesTotal", ++ "mem.numa.util.hugepagesFree", ++ "mem.numa.util.hugepagesSurp", ++ "mem.numa.util.swapCached", ++ "mem.numa.util.kreclaimable", ++] + +- def __init__(self): +- """ Construct object - prepare for command line handling """ +- self.opts = self.options() +- self.context = None +- self.width = 0 ++SYS_METRICS = [ ++ 'kernel.uname.nodename', ++ 'kernel.uname.release', ++ 'kernel.uname.sysname', ++ 'kernel.uname.machine', ++ 'hinv.ncpu', ++] ++ ++ALL_METRICS = NUMA_METRICS + MEM_METRICS + +- def resize(self): ++def prefix(metric): ++ last_part = metric.split('.')[-1] ++ result = last_part[0].upper() + last_part[1:] ++ return result ++ ++class MetricRepository: ++ def __init__(self, group): ++ self.group = group ++ self.current_cached_values = {} ++ self.previous_cached_values = {} ++ ++ def _fetch_current_values(self, metric, instance): ++ if instance is not None: ++ return dict( ++ map(lambda x: (x[0].inst, x[2]), self.group[metric].netValues) ++ ) ++ else: ++ if self.group[metric].netValues == []: ++ return None ++ else: ++ return self.group[metric].netValues[0][2] ++ def current_values(self, metric_name): ++ if self.group.get(metric_name, None) is None: ++ return None ++ if self.current_cached_values.get(metric_name, None) is None: ++ self.current_cached_values[ ++ metric_name ++ ] = self._fetch_current_values(metric_name, True) ++ return self.current_cached_values.get(metric_name, None) ++ ++class NUMAStat: ++ ++ def __init__(self, group): ++ self.group = group ++ self.repo = MetricRepository(group) ++ ++ def resize(self, width): + """ Find a suitable display width limit """ +- if self.width == 0: ++ if width == 0: + if not sys.stdout.isatty(): +- self.width = 1000000000 # mimic numastat(1) here ++ width = 1000000000 # mimic numastat(1) here + else: +- # popen() is SAFE, command is a literal string ++ # popen() is SAFE, command is a literal string + (_, width) = os.popen('stty size', 'r').read().split() +- self.width = int(width) +- self.width = int(os.getenv('NUMASTAT_WIDTH', str(self.width))) +- self.width = max(self.width, 32) ++ width = int(width) ++ width = int(os.getenv('NUMASTAT_WIDTH', str(width))) ++ return max(width, 32) + +- def option(self, opt, optarg, index): +- """ Perform setup for an individual command line option """ +- if opt == 'w': +- self.width = int(optarg) ++ def __format_table(self, width, nodes, data): ++ null_output = False ++ if not nodes: ++ null_output = True ++ nodes = [(0, 'Node ')] + +- def options(self): +- """ Setup default command line argument option handling """ +- opts = pmapi.pmOptions() +- opts.pmSetOptionCallback(self.option) +- opts.pmSetShortOptions("w:V?") +- opts.pmSetLongOptionHeader("Options") +- opts.pmSetLongOption("width", 1, 'w', "N", "limit the display width") +- opts.pmSetLongOptionVersion() +- opts.pmSetLongOptionHelp() +- return opts +- +- def extract(self, descs, insts, result): +- """ Extract the set of metric values from a given pmResult """ +- values = [[]] +- for metrics in xrange(len(descs)): +- values.append([]) +- for nodes in xrange(len(insts)): +- if result.contents.get_numval(metrics) > 0: +- atom = self.context.pmExtractValue( +- result.contents.get_valfmt(metrics), +- result.contents.get_vlist(metrics, nodes), +- descs[metrics].contents.type, PM_TYPE_U64) +- values[metrics].append(long(atom.ull)) +- else: +- values[metrics].append(long(0)) +- return values +- +- def execute(self): +- """ Using a PMAPI context (could be either host or archive), +- fetch and report per-node values related to NUMA memory. +- """ +- metrics = ('mem.numa.alloc.hit', 'mem.numa.alloc.miss', +- 'mem.numa.alloc.foreign', 'mem.numa.alloc.interleave_hit', +- 'mem.numa.alloc.local_node', 'mem.numa.alloc.other_node') +- +- pmids = self.context.pmLookupName(metrics) +- descs = self.context.pmLookupDescs(pmids) +- if self.context.type == PM_CONTEXT_ARCHIVE: +- (insts, nodes) = self.context.pmGetInDomArchive(descs[0]) ++ if "numastat" in data: ++ metrics = NUMA_METRICS ++ title = "NUMA memory allocation statistics (pages)" + else: +- (insts, nodes) = self.context.pmGetInDom(descs[0]) +- result = self.context.pmFetch(pmids) +- values = self.extract(descs, insts, result) +- self.context.pmFreeResult(result) +- self.report(metrics, nodes, values) +- +- def report(self, metrics, nodes, values): +- """ Given per-node metric names and values, dump 'em like numastat(1) +- Nodes is a list of strings, values is a list of lists of values. +- """ +- columns = len(nodes) * 16 +- if columns == 0: +- print("No NUMA nodes found, exiting") +- sys.exit(1) +- self.resize() +- maxnodes = int((self.width - 16) / 16) ++ metrics = MEM_METRICS ++ title = "Per-node system memory usage (KB)" ++ ++ total_w = max(42, int(width)) ++ print(title[:total_w]) ++ ++ width = self.resize(width) ++ maxnodes = int((width - 16) / 16) + if maxnodes > len(nodes): # just an initial header suffices +- header = '%-16s' % '' +- for node in nodes: +- header += '%16s' % node ++ header = '%30s' % '' ++ for _, node in nodes: ++ header += '%-12s' % node + print(header) +- for index in xrange(len(metrics)): +- title = self.prefix(metrics[index]) +- self.metric(title, nodes, values[index], maxnodes) +- +- def metric(self, prefix, nodes, values, maxnodes): +- """ Given one metric and its per-node values, produce one or more +- lines of output with the values, each line node-name prefixed +- and with a new node header for each. +- """ +- done = 0 +- while done < len(nodes): +- header = '%-16s' % '' +- window = '%-16s' % prefix +- for index in xrange(maxnodes): +- current = done + index +- if current >= len(nodes): +- break +- header += '%16s' % (nodes[current]) +- window += '%16d' % (values[current]) +- if done > maxnodes or maxnodes <= len(nodes): +- print('%s\n%s' % (header, window)) +- else: +- print('%s' % window) +- done += maxnodes + +- def prefix(self, metric): +- """ Transform the PCP metric names into the reported sub-headings """ +- title = metric[15:] +- if '_' not in title: +- title = 'numa_' + title +- return title ++ for m in metrics: ++ if not null_output: ++ vals = self.repo.current_values(m) ++ done = 0 # reset for each metric ++ ++ # Loop through nodes in chunks of 'maxnodes' ++ while done < len(nodes): ++ header = '%-30s' % '' ++ window = '%-20s : ' % prefix(m) ++ ++ # Slice the range we'll print in this batch ++ chunk = nodes[done:done + maxnodes] ++ ++ for i, ( _, name) in enumerate(chunk): ++ header += '%-12s' % name ++ if not null_output: ++ window += '%12s' % vals[done + i] ++ else: ++ window += '%12s' % "NA" ++ ++ # Print header once per row group (not every metric) ++ if done > maxnodes or maxnodes <= len(nodes): ++ print('%s\n%s' % (header, window)) ++ else: ++ print('%s' % window) ++ done += maxnodes ++ print() ++ ++ def print_mem(self, width, nodes, data): ++ self.__format_table(width, nodes, data) ++ ++ def print_numa(self, width, nodes, data): ++ self.__format_table(width, nodes, data) ++ ++class NumaStatOption(pmapi.pmOptions): ++ context = None ++ timefmt = "%m/%d/%Y %H:%M:%S" ++ width = 0 ++ mem_out = False ++ numa_out = False ++ ++ def override(self,opt): ++ """ Override standard PCP options to match numastat(1) """ ++ if opt == 'n': ++ return True ++ return False ++ ++ def __init__(self): ++ pmapi.pmOptions.__init__(self) ++ self.pmSetShortOptions("w:mV?:n") ++ self.pmSetOptionCallback(self.extraOptions) ++ self.pmSetOverrideCallback(self.override) ++ self.pmSetLongOptionHeader("Numastat options") ++ self.pmSetLongOption("width", 1, 'w', "n", "limit the display width") ++ # Map long options to our non-conflicting short letters ++ self.pmSetLongOption("meminfo", 0, 'm', "", "show meminfo-like system-wide memory usage") ++ self.pmSetLongOption("numastat", 0, 'n', "", "show the numastat statistics info") ++ self.pmSetLongOptionVersion() ++ self.pmSetLongOptionHelp() ++ ++ def extraOptions(self, opt, optarg, index): ++ if opt == 'w': ++ self.width = int(optarg) ++ elif opt == "m": ++ self.mem_out = True ++ elif opt == "n": ++ self.numa_out = True ++ elif opt == "V": ++ pass ++ else: ++ raise pmapi.pmUsageErr() ++ return True ++ ++ def checkoptions(self): ++ if (not self.mem_out) and (not self.numa_out) and (self.width == 0): ++ self.numa_out = True ++ if self.width < 0: ++ return False ++ return True + +- def connect(self): +- """ Establish a PMAPI context to archive, host or local, via args """ +- self.context = pmapi.pmContext.fromOptions(self.opts, sys.argv) ++class NumaStatReport(pmcc.MetricGroupPrinter): ++ machine_info_count = 0 ++ ++ def __init__(self, options): ++ self.options = options ++ self.timestamp = None ++ ++ def __get_timestamp(self, group): ++ ts = group.contextCache.pmLocaltime(int(group.timestamp)) ++ self.timestamp = time.strftime(NumaStatOption.timefmt, ts.struct_time()) ++ return self.timestamp ++ ++ def __get_ncpu(self, group): ++ return group['hinv.ncpu'].netValues[0][2] ++ ++ def print_machine_info(self,group, context): ++ timestamp = context.pmLocaltime(group.timestamp.tv_sec) ++ # Please check strftime(3) for different formatting options. ++ # Also check TZ and LC_TIME environment variables for more ++ # information on how to override the default formatting of ++ # the date display in the header ++ time_string = time.strftime("%x", timestamp.struct_time()) ++ header_string = '' ++ header_string += group['kernel.uname.sysname'].netValues[0][2] + ' ' ++ header_string += group['kernel.uname.release'].netValues[0][2] + ' ' ++ header_string += '(' + group['kernel.uname.nodename'].netValues[0][2] + ') ' ++ header_string += time_string + ' ' ++ header_string += group['kernel.uname.machine'].netValues[0][2] + ' ' ++ print("%s (%s CPU)" % (header_string, self.__get_ncpu(group))) ++ ++ def __discover_nodes(self, group, name): ++ # Build list of online nodes (instance id, instance name) ++ nodes = [] ++ try: ++ for ent in group[name].netValues: ++ inst_id = ent[0].inst ++ inst_name = ent[1] # usually "node0", "node1", ... ++ online = int(ent[2]) != 0 ++ if online: ++ nodes.append((inst_id, inst_name)) ++ except Exception: ++ pass ++ # Sort by instance id (node number) ++ nodes.sort(key=lambda t: t[0]) ++ return nodes ++ ++ def report(self, manager): ++ # Print in a stable order ++ group = manager["sys_info"] ++ try: ++ if not self.machine_info_count: ++ self.print_machine_info(group, manager) ++ self.machine_info_count = 1 ++ except IndexError: ++ return ++ ++ output_numa = ( ++ self.options.numa_out ++ or (not self.options.mem_out and not self.options.numa_out) ++ ) ++ output_mem = self.options.mem_out ++ group = manager["numastat"] ++ nodes = self.__discover_nodes(group, "mem.numa.util.total") ++ timestamp = self.__get_timestamp(group) ++ print("%-20s : %s"%("Timestamp", timestamp)) ++ if output_mem: ++ NUMAStat(group).print_mem(self.options.width, nodes, "meminfo") ++ if output_numa: ++ NUMAStat(group).print_numa(self.options.width, nodes, "numastat") ++ ++ if ( ++ NumaStatOption.context is not PM_CONTEXT_ARCHIVE ++ and self.options.pmGetOptionSamples() is None ++ ): ++ sys.exit(0) + + if __name__ == '__main__': + try: +- NUMASTAT = NUMAStat() +- NUMASTAT.connect() +- NUMASTAT.execute() ++ opts = NumaStatOption() ++ mngr = pmcc.MetricGroupManager.builder(opts, sys.argv) ++ if not opts.checkoptions(): ++ print("Invalid options from command line") ++ raise pmapi.pmUsageErr() ++ NumaStatOption.context = mngr.type ++ ++ missing = mngr.checkMissingMetrics(ALL_METRICS) ++ if missing is not None: ++ sys.stderr.write('Error: not all required metrics are available\nMissing: %s\n' % (missing)) ++ sys.exit(1) ++ ++ mngr["numastat"] = ALL_METRICS ++ mngr["sys_info"] = SYS_METRICS ++ mngr.printer = NumaStatReport(opts) ++ sts = mngr.run() ++ sys.exit(sts) ++ except IOError: ++ signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except pmapi.pmErr as error: +- print("%s: %s" % (error.progname(), error.message())) ++ sys.stderr.write("%s %s\n" % (error.progname(), error.message())) + except pmapi.pmUsageErr as usage: + usage.message() ++ sys.exit(1) + except KeyboardInterrupt: + pass diff --git a/SOURCES/1009-Fixes-higepagesize-metric-value-from-bytes-to-KB.patch b/SOURCES/1009-Fixes-higepagesize-metric-value-from-bytes-to-KB.patch new file mode 100644 index 0000000..a81ebc1 --- /dev/null +++ b/SOURCES/1009-Fixes-higepagesize-metric-value-from-bytes-to-KB.patch @@ -0,0 +1,45 @@ +From d2e92db5d6ee2a07e52ff72da471976aafaeb7ec Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Fri, 10 Oct 2025 16:39:51 +0530 +Subject: [PATCH] Fixes higepagesize metric value from bytes to KB as per + /proc/meminfo + +[sagar@vbox ~]$ pcp meminfo | grep Hugepagesize ; cat /proc/meminfo | grep Hugepagesize +Hugepagesize : 2097152 kB +Hugepagesize: 2048 kB + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/pull/2371/commits/84bf35c442cedab82bdd5d71da6cca21cbf628db + +Orabug: 38527066 + +Signed-off-by: Sourav Sharma +--- + src/pcp/meminfo/pcp-meminfo.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/src/pcp/meminfo/pcp-meminfo.py b/src/pcp/meminfo/pcp-meminfo.py +index 8492859..f956910 100755 +--- a/src/pcp/meminfo/pcp-meminfo.py ++++ b/src/pcp/meminfo/pcp-meminfo.py +@@ -168,6 +168,9 @@ class MeminfoReport(pmcc.MetricGroupPrinter): + units = "" + if METRICS_DESC[idx][-6:] == "_NO_kb": + metric_name = METRICS_DESC[idx][:-6] ++ elif METRICS_DESC[idx] == "Hugepagesize": ++ metric_name = METRICS_DESC[idx] ++ units = "B" + else: + metric_name = METRICS_DESC[idx] + units = "kB" +@@ -203,6 +206,9 @@ class MeminfoReport(pmcc.MetricGroupPrinter): + continue + + metric_name, units = self.getMetricName(idx) ++ if units == "B": ++ val = int(val / 1024) ++ units = "kB" + print("%-17s : %s %s"%(metric_name, val, units)) + + idx += 1 +-- +2.43.7 diff --git a/SOURCES/1010-xz-default-compression-changed-to-level-3.patch b/SOURCES/1010-xz-default-compression-changed-to-level-3.patch new file mode 100644 index 0000000..73a9178 --- /dev/null +++ b/SOURCES/1010-xz-default-compression-changed-to-level-3.patch @@ -0,0 +1,33 @@ +From 9703b9c63e27b541e32de56e1d482f85e4e548f4 Mon Sep 17 00:00:00 2001 +From: sagar sagar +Date: Fri, 21 Nov 2025 12:35:08 +0530 +Subject: [PATCH OL9 1010/1010] xz default compression changed to level 3 + +to reduce the size of the compressed pcp archives +Orabug: 38674828 +Signed-off-by: sagar sagar +--- + src/pmlogger/pmlogger_daily.sh | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/pmlogger/pmlogger_daily.sh b/src/pmlogger/pmlogger_daily.sh +index 18fc691..6e73a1f 100755 +--- a/src/pmlogger/pmlogger_daily.sh ++++ b/src/pmlogger/pmlogger_daily.sh +@@ -264,10 +264,10 @@ COMPRESS="" + COMPRESS_CMDLINE="" + if which xz >/dev/null 2>&1 + then +- if xz -0 --block-size=10MiB /dev/null 2>&1 ++ if xz -3 --block-size=10MiB /dev/null 2>&1 + then +- # want minimal overheads, -0 is the same as --fast +- COMPRESS_DEFAULT="xz -0 --block-size=10MiB" ++ # want better compression with minimum overhead ++ COMPRESS_DEFAULT="xz -3 --block-size=10MiB" + else + COMPRESS_DEFAULT=xz + fi +-- +2.43.7 + diff --git a/SOURCES/1011-orabug38724866-fix-nfsclient-per-op-parsing.patch b/SOURCES/1011-orabug38724866-fix-nfsclient-per-op-parsing.patch new file mode 100644 index 0000000..3101e0f --- /dev/null +++ b/SOURCES/1011-orabug38724866-fix-nfsclient-per-op-parsing.patch @@ -0,0 +1,38 @@ +From c80dc758eb0eb27fafa0b594d6c2aa1f4c3803fa Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Wed, 3 Dec 2025 20:40:52 +0530 +Subject: [PATCH] pmdanfsclient: fix regex to correctly parse NFS op stats + +The regex used to match NFS operation statistics in /proc/self/mountstats was + missing a capture group, causing parsing failures when an additional field + was present in newer kernel formats. Updated the regex to include the extra + numeric field so that all opstats lines are parsed correctly. + +Signed-off-by: Sourav Sharma + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/85671f8874d7d4b4e57eb45bfe295de92b95415c + +Orabug: 38724866 + +Signed-off-by: Sourav Sharma + +--- + src/pmdas/nfsclient/pmdanfsclient.python | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/pmdas/nfsclient/pmdanfsclient.python b/src/pmdas/nfsclient/pmdanfsclient.python +index 786d958..f08b93a 100644 +--- a/src/pmdas/nfsclient/pmdanfsclient.python ++++ b/src/pmdas/nfsclient/pmdanfsclient.python +@@ -636,7 +636,7 @@ class NFSCLIENTPMDA(PMDA): + line = STATS.readline() + if line == '': + break +- m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line) ++ m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line) + if not m: + break + opname = m.group(1).lower() +-- +2.43.7 + diff --git a/SOURCES/1012-orabug38817068-Introduce-PCP-implementation-of-nfsiostat.patch b/SOURCES/1012-orabug38817068-Introduce-PCP-implementation-of-nfsiostat.patch new file mode 100644 index 0000000..dc38400 --- /dev/null +++ b/SOURCES/1012-orabug38817068-Introduce-PCP-implementation-of-nfsiostat.patch @@ -0,0 +1,575 @@ +From ca8fbd2a827e3e7caa72331e003f93523c9f5241 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Tue, 6 Jan 2026 05:53:08 +0000 +Subject: [PATCH] pcp-nfsiostat: Introduce PCP implementation of nfsiostat + parser + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/c0a0c53b57774dfac0b727e62472fb1ae6065540 + +Orabug: 38817068 + +Signed-off-by: Sourav Sharma +--- + src/pcp/GNUmakefile | 1 + + src/pcp/nfsiostat/GNUmakefile | 43 ++++ + src/pcp/nfsiostat/pcp-nfsiostat.1 | 132 +++++++++++ + src/pcp/nfsiostat/pcp-nfsiostat.py | 346 +++++++++++++++++++++++++++++ + 4 files changed, 522 insertions(+) + create mode 100644 src/pcp/nfsiostat/GNUmakefile + create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.1 + create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.py + +diff --git a/src/pcp/GNUmakefile b/src/pcp/GNUmakefile +index da29269..2b74fc0 100644 +--- a/src/pcp/GNUmakefile ++++ b/src/pcp/GNUmakefile +@@ -30,6 +30,7 @@ SUBDIRS = \ + mpstat \ + netstat \ + numastat \ ++ nfsiostat \ + pidstat \ + ps \ + python \ +diff --git a/src/pcp/nfsiostat/GNUmakefile b/src/pcp/nfsiostat/GNUmakefile +new file mode 100644 +index 0000000..d8c5356 +--- /dev/null ++++ b/src/pcp/nfsiostat/GNUmakefile +@@ -0,0 +1,43 @@ ++# ++# Copyright (c) 2023 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++ ++TOPDIR = ../../.. ++include $(TOPDIR)/src/include/builddefs ++ ++TARGET = pcp-nfsiostat ++SCRIPT = $(TARGET).py ++MAN_SECTION = 1 ++MAN_PAGES = $(TARGET).$(MAN_SECTION) ++MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION) ++ ++default: $(SCRIPT) $(MAN_PAGES) ++ ++include $(BUILDRULES) ++ ++install: default ++ifeq "$(HAVE_PYTHON)" "true" ++ $(INSTALL) -m 755 $(SCRIPT) $(PCP_BINADM_DIR)/$(TARGET) ++ @$(INSTALL_MAN) ++endif ++ ++default_pcp : default ++ ++install_pcp : install ++ ++check:: $(SCRIPT) ++ $(PYLINT) $^ ++ ++check :: $(MAN_PAGES) ++ $(MANLINT) $^ +diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.1 b/src/pcp/nfsiostat/pcp-nfsiostat.1 +new file mode 100644 +index 0000000..b297285 +--- /dev/null ++++ b/src/pcp/nfsiostat/pcp-nfsiostat.1 +@@ -0,0 +1,132 @@ ++'\"! tbl | mmdoc ++'\"macro stdmacro ++.\" ++.\" Man page for pcp-nfsiostat ++.\" Copyright (c) 2023 Oracle and/or its affiliates. ++.\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++.\" ++.\" This program is free software; you can redistribute it and/or modify it ++.\" under the terms of the GNU General Public License as published by the ++.\" Free Software Foundation; either version 2 of the License, or (at your ++.\" option) any later version. ++.\" ++.\" This program is distributed in the hope that it will be useful, but ++.\" WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++.\" or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++.\" for more details. ++.\" ++ ++.TH PCP-NFSIOSTAT 1 "PCP" "Performance Co-Pilot" ++ ++.SH NAME ++\f3pcp-nfsiostat\f1 \- Emulate iostat for NFS mount points using /proc/self/mountstats ++ ++.SH SYNOPSIS ++\fBpcp\fP [\fBpcp options\fP] \fBnfsiostat\fP [\fB-s\fP \fBsamples\fP] [\fB-a\fP \fBarchive\fP] [\fB-Z\fP \fB--timezone\fP] [\fB-z\fP \fB--hostzone\fP] [\fB-V\fP \fBVersion\fP] ++ ++.SH DESCRIPTION ++The\fB pcp-nfsiostat \fPcommand reports client-side\fB Network File System (NFS) \fPI/O statistics for each mounted NFS filesystem. For every NFS mountpoint, the output consists of a summary section followed by detailed read and write statistics blocks. These statistics include operation rates, data throughput, latency, retransmissions, queueing delays, and error counts. By default,\fB pcp-nfsiostat \fPmonitors NFS mountpoints on the local host and reports live metrics collected via the \fB PCP NFS PMDA\fP. When an archive is specified, historical metrics are reported instead. ++ ++.SH OUTPUT FORMAT ++Statistics are reported per mounted NFS filesystem using the following layout: ++ ++.nf ++server:/export mounted on /mount/point: ++ ++ ops/s rpc bklog ++ 0.017 0.000 ++ ++read: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors ++ ... ++ ++write: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors ++ ... ++.fi ++ ++.SH FIELD DESCRIPTIONS ++.TP ++.B ops/s ++Number of NFS operations performed per second. ++ ++.TP ++.B rpc bklog ++Average number of RPC requests waiting to be transmitted. ++ ++.TP ++.B kB/s ++Kilobytes transferred per second. ++ ++.TP ++.B kB/op ++Average number of kilobytes transferred per operation. ++ ++.TP ++.B retrans ++Number of RPC retransmissions and the retransmission percentage. ++ ++.TP ++.B avg RTT (ms) ++Average round-trip time in milliseconds for RPC requests. ++ ++.TP ++.B avg exe (ms) ++Average execution time in milliseconds spent servicing requests on the server. ++ ++.TP ++.B avg queue (ms) ++Average time in milliseconds spent waiting in the RPC transmission queue. ++ ++.TP ++.B errors ++Number of failed operations and failure percentage. ++ ++.SH OPTIONS ++.TP ++.BR \-a ", " \-\-archive " " I archive ++Fetch NFS I/O statistics from the specified PCP archive. ++ ++.TP ++.BR \-s ", " \-\-samples " " I samples ++Number of samples to collect before exiting. ++ ++.TP ++.BR \-z ", " \-\-hostzone ++Set the reporting timezone to the local timezone of the metrics source. ++ ++.TP ++.BR \-Z ", " \-\-timezone " " I tz ++Set the reporting timezone. ++ ++.TP ++.BR \-V ", " \-\-version ++Display version information and exit. ++ ++.TP ++.BR \-? ", " \-\-help ++Display usage information and exit. ++ ++.SH NOTES ++.B pcp-nfsiostat ++reports client-side NFS statistics collected from the kernel and exposed ++via the PCP NFS PMDA. The output format and metrics are similar to those ++reported by the ++.BR nfsiostat (1) ++tool from the nfs-utils package. ++ ++.SH PCP ENVIRONMENT ++Environment variables with the prefix \fBPCP_\fP are used to parameterize ++the file and directory names used by PCP. ++On each installation, the ++file \fI/etc/pcp.conf\fP contains the local values for these variables. ++The \fB$PCP_CONF\fP variable may be used to specify an alternative ++configuration file, as described in ++.BR pcp.conf (5). ++ ++For environment variables affecting PCP tools, see ++.BR pmGetOptions (3). ++ ++.SH SEE ALSO ++.BR PCPIntro (1), ++.BR pcp (1), ++.BR nfsiostat (1), ++.BR environ (7). +diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py +new file mode 100644 +index 0000000..0b0376d +--- /dev/null ++++ b/src/pcp/nfsiostat/pcp-nfsiostat.py +@@ -0,0 +1,346 @@ ++#!/usr/bin/pmpython ++# ++# Copyright (c) 2023 Oracle and/or its affiliates. ++# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. ++# ++# This program is free software; you can redistribute it and/or modify it ++# under the terms of the GNU General Public License as published by the ++# Free Software Foundation; either version 2 of the License, or (at your ++# option) any later version. ++# ++# This program is distributed in the hope that it will be useful, but ++# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ++# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License ++# for more details. ++# ++# pylint: disable=bad-whitespace,too-many-lines,bad-continuation ++# pylint: disable=too-many-arguments,too-many-positional-arguments ++# pylint: disable=redefined-outer-name,unnecessary-lambda ++# ++ ++import signal ++import sys ++import time ++from pcp import pmapi, pmcc ++from cpmapi import PM_CONTEXT_ARCHIVE ++ ++SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release", ++ "kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"] ++NFSIOSTAT_METRICS = ["nfsclient.mountpoint","nfsclient.export","nfsclient.age", ++ "nfsclient.xprt.sends","nfsclient.xprt.backlog_u","nfsclient.ops.read.ops", ++ "nfsclient.ops.read.errors","nfsclient.ops.read.execute","nfsclient.ops.read.rtt", ++ "nfsclient.ops.read.queue","nfsclient.ops.read.bytes_recv","nfsclient.ops.read.bytes_sent", ++ "nfsclient.ops.read.ntrans","nfsclient.ops.write.ops","nfsclient.ops.write.errors", ++ "nfsclient.ops.write.execute","nfsclient.ops.write.rtt","nfsclient.ops.write.queue", ++ "nfsclient.ops.write.bytes_recv","nfsclient.ops.write.bytes_sent","nfsclient.ops.write.ntrans"] ++ALL_METRICS = NFSIOSTAT_METRICS + SYS_METRICS ++ ++def adjust_length(name): ++ return name.ljust(25) ++class ReportingMetricRepository: ++ ++ def __init__(self,group): ++ self.group=group ++ self.current_cached_values = {} ++ ++ def __sorted(self,data): ++ return dict(sorted(data.items(), key=lambda item: item[0].lower())) ++ ++ def __fetch_current_value(self,metric): ++ val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues)) ++ val=self.__sorted(val) ++ return dict(val) ++ ++ def current_value(self,metric): ++ if not metric in self.group: ++ return None ++ if self.current_cached_values.get(metric) is None: ++ first_value=self.__fetch_current_value(metric) ++ self.current_cached_values[metric]=first_value ++ return self.current_cached_values[metric] ++ ++class NfsioStatUtil: ++ def __init__(self,metrics_repository): ++ self.__metric_repository=metrics_repository ++ self.report=ReportingMetricRepository(self.__metric_repository) ++ ++ def mount_point(self): ++ return self.report.current_value('nfsclient.mountpoint') ++ ++ def mount_share(self): ++ return self.report.current_value('nfsclient.export') ++ ++ def mount_share_keys(self): ++ data = self.report.current_value('nfsclient.export') ++ return data.keys() ++ ++ def sample_time(self): ++ return self.report.current_value('nfsclient.age') ++ ++ def xprt_sends(self): ++ return self.report.current_value('nfsclient.xprt.sends') ++ ++ def xprt_backlog(self): ++ return self.report.current_value('nfsclient.xprt.backlog_u') ++ ++ def readops(self): ++ return self.report.current_value('nfsclient.ops.read.ops') ++ ++ def readerrors(self): ++ return self.report.current_value('nfsclient.ops.read.errors') ++ ++ def readexecute(self): ++ return self.report.current_value('nfsclient.ops.read.execute') ++ ++ def readrtt(self): ++ return self.report.current_value('nfsclient.ops.read.rtt') ++ ++ def readqueue(self): ++ return self.report.current_value('nfsclient.ops.read.queue') ++ ++ def readbytesrecv(self): ++ return self.report.current_value('nfsclient.ops.read.bytes_recv') ++ ++ def readbytessent(self): ++ return self.report.current_value('nfsclient.ops.read.bytes_sent') ++ ++ def readntrans(self): ++ return self.report.current_value('nfsclient.ops.read.ntrans') ++ ++ def writeops(self): ++ return self.report.current_value('nfsclient.ops.write.ops') ++ ++ def writeerrors(self): ++ return self.report.current_value('nfsclient.ops.write.errors') ++ ++ def writeexecute(self): ++ return self.report.current_value('nfsclient.ops.write.execute') ++ ++ def writertt(self): ++ return self.report.current_value('nfsclient.ops.write.rtt') ++ ++ def writequeue(self): ++ return self.report.current_value('nfsclient.ops.write.queue') ++ ++ def writebytesrecv(self): ++ return self.report.current_value('nfsclient.ops.write.bytes_recv') ++ ++ def writebytessent(self): ++ return self.report.current_value('nfsclient.ops.write.bytes_sent') ++ ++ def writentrans(self): ++ return self.report.current_value('nfsclient.ops.write.ntrans') ++ ++class NfsiostatReport(pmcc.MetricGroupPrinter): ++ def __init__(self,opts,group): ++ self.opts = opts ++ self.group = group ++ self.samples = opts.samples ++ self.context = opts.context ++ ++ def __get_ncpu(self, group): ++ return group['hinv.ncpu'].netValues[0][2] ++ ++ def __print_machine_info(self, context): ++ timestamp = self.group.pmLocaltime(context.timestamp.tv_sec) ++ # Please check strftime(3) for different formatting options. ++ # Also check TZ and LC_TIME environment variables for more ++ # information on how to override the default formatting of ++ # the date display in the header ++ time_string = time.strftime("%m/%d/%Y %H:%M:%S", timestamp.struct_time()) ++ header_string = '' ++ header_string += context['kernel.uname.sysname'].netValues[0][2] + ' ' ++ header_string += context['kernel.uname.release'].netValues[0][2] + ' ' ++ header_string += '(' + context['kernel.uname.nodename'].netValues[0][2] + ') ' ++ header_string += time_string + ' ' ++ header_string += context['kernel.uname.machine'].netValues[0][2] + ' ' ++ print("%s (%s CPU)" % (header_string, self.__get_ncpu(context))) ++ ++ def __print_values(self,timestamp, nfsstatus): ++ n_shares = nfsstatus.mount_share_keys() ++ mountshare = nfsstatus.mount_share() ++ mountpoint = nfsstatus.mount_point() ++ sampletime = nfsstatus.sample_time() ++ sends = nfsstatus.xprt_sends() ++ backlog = nfsstatus.xprt_backlog() ++ readops = nfsstatus.readops() ++ readerrors = nfsstatus.readerrors() ++ readexecute = nfsstatus.readexecute() ++ readrtt = nfsstatus.readrtt() ++ readqueue = nfsstatus.readqueue() ++ readbytesrecv = nfsstatus.readbytesrecv() ++ readbytessent = nfsstatus.readbytessent() ++ readntrans = nfsstatus.readntrans() ++ writeops = nfsstatus.writeops() ++ writeerrors = nfsstatus.writeerrors() ++ writeexecute = nfsstatus.writeexecute() ++ writertt = nfsstatus.writertt() ++ writequeue = nfsstatus.writequeue() ++ writebytesrecv = nfsstatus.writebytesrecv() ++ writebytessent = nfsstatus.writebytessent() ++ writentrans = nfsstatus.writentrans() ++ ++ print("%-18s:%s"%("Timestamp", timestamp)) ++ print() ++ ++ for name in n_shares: ++ # read ++ r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024 ++ if sampletime[name] > 0: ++ ops_per_sample = sends[name] / sampletime[name] ++ ops_per_sample_read = readops[name] / sampletime[name] ++ r_kilobytes_per_sample = r_kilobytes / sampletime[name] ++ else: ++ ops_per_sample = 0.0 ++ ops_per_sample_read = 0.0 ++ r_kilobytes_per_sample = 0.0 ++ ++ r_retrans = readntrans[name] - readops[name] ++ if readops[name] > 0: ++ r_kilobytes_per_op = r_kilobytes / readops[name] ++ r_retrans_percent = (r_retrans * 100) / readops[name] ++ r_rtt_per_op = readrtt[name] / readops[name] ++ r_exe_per_op = readexecute[name] / readops[name] ++ r_queued_for_per_op = readqueue[name] / readops[name] ++ r_errs_percent = (readerrors[name] * 100) / readops[name] ++ else: ++ r_kilobytes_per_op = 0.0 ++ r_retrans_percent = 0.0 ++ r_rtt_per_op = 0.0 ++ r_exe_per_op = 0.0 ++ r_queued_for_per_op = 0.0 ++ r_errs_percent = 0.0 ++ ++ # write ++ w_kilobytes = (writebytessent[name] + writebytesrecv[name]) / 1024 ++ if sampletime[name] > 0: ++ ops_per_sample_write = writeops[name] / sampletime[name] ++ w_kilobytes_per_sample = w_kilobytes / sampletime[name] ++ else: ++ ops_per_sample_write = 0.0 ++ w_kilobytes_per_sample = 0.0 ++ ++ w_retrans = writentrans[name] - writeops[name] ++ if writeops[name] > 0: ++ w_kilobytes_per_op = w_kilobytes / writeops[name] ++ w_retrans_percent = (w_retrans * 100) / writeops[name] ++ w_rtt_per_op = writertt[name] / writeops[name] ++ w_exe_per_op = writeexecute[name] / writeops[name] ++ w_queued_for_per_op = writequeue[name] / writeops[name] ++ w_errs_percent = (writeerrors[name] * 100) / writeops[name] ++ else: ++ w_kilobytes_per_op = 0.0 ++ w_retrans_percent = 0.0 ++ w_rtt_per_op = 0.0 ++ w_exe_per_op = 0.0 ++ w_queued_for_per_op = 0.0 ++ w_errs_percent = 0.0 ++ ++ print(f"{mountshare[name]} mounted on {mountpoint[name]}:") ++ ++ print(f"{'':14}ops/s{'':7}rpc bklog") ++ print(f"{ops_per_sample:19.3f}{backlog[name]:16.3f}") ++ print() ++ print( ++ "read: " ++ "ops/s kB/s kB/op retrans " ++ "avg RTT (ms) avg exe (ms) avg queue (ms) errors" ++ ) ++ print( ++ f"{'':19}" ++ f"{ops_per_sample_read:5.3f}" ++ f"{r_kilobytes_per_sample:12.3f}" ++ f"{r_kilobytes_per_op:13.3f} " ++ f"{int(r_retrans):2d} ({r_retrans_percent:2.1f}%)" ++ f"{r_rtt_per_op:15.3f}" ++ f"{r_exe_per_op:15.3f}" ++ f"{r_queued_for_per_op:17.3f} " ++ f"{int(readerrors[name]):4d} ({r_errs_percent:2.1f}%)" ++ ) ++ ++ print( ++ "write: " ++ "ops/s kB/s kB/op retrans " ++ "avg RTT (ms) avg exe (ms) avg queue (ms) errors" ++ ) ++ ++ print( ++ f"{'':19}" ++ f"{ops_per_sample_write:5.3f}" ++ f"{w_kilobytes_per_sample:12.3f}" ++ f"{w_kilobytes_per_op:13.3f} " ++ f"{int(w_retrans):2d} ({w_retrans_percent:2.1f}%)" ++ f"{w_rtt_per_op:15.3f}" ++ f"{w_exe_per_op:15.3f}" ++ f"{w_queued_for_per_op:17.3f} " ++ f"{int(writeerrors[name]):4d} ({w_errs_percent:2.1f}%)" ++ ) ++ print() ++ ++ def print_report(self,group,timestamp, manager_nfsiostat): ++ def __print_nfs_status(): ++ nfsstatus = NfsioStatUtil(manager_nfsiostat) ++ if nfsstatus.mount_share(): ++ try: ++ self.__print_machine_info(group) ++ self.__print_values(timestamp, nfsstatus) ++ except IndexError: ++ print("Incorrect machine info due to some missing metrics") ++ return ++ else: ++ pass ++ ++ if self.context != PM_CONTEXT_ARCHIVE and self.samples is None: ++ __print_nfs_status() ++ sys.exit(0) ++ elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None: ++ __print_nfs_status() ++ elif self.samples >=1: ++ __print_nfs_status() ++ self.samples-=1 ++ else: ++ pass ++ ++ def report(self, manager): ++ group = manager["sysinfo"] ++ self.samples = self.opts.pmGetOptionSamples() ++ t_s = group.contextCache.pmLocaltime(int(group.timestamp)) ++ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time()) ++ self.print_report(group,timestamp,manager['nfsiostat']) ++ ++class NfsiostatOptions(pmapi.pmOptions): ++ timefmt = "%m/%d/%Y %H:%M:%S" ++ def __init__(self): ++ pmapi.pmOptions.__init__(self, "a:s:Z:zV?") ++ self.pmSetLongOptionHeader("General options") ++ self.pmSetLongOptionHostZone() ++ self.pmSetLongOptionTimeZone() ++ self.pmSetLongOptionHelp() ++ self.pmSetLongOptionSamples() ++ self.pmSetLongOptionVersion() ++ self.samples=None ++ self.context=None ++ ++if __name__ == '__main__': ++ try: ++ opts = NfsiostatOptions() ++ mngr = pmcc.MetricGroupManager.builder(opts,sys.argv) ++ opts.context=mngr.type ++ missing = mngr.checkMissingMetrics(ALL_METRICS) ++ if missing is not None: ++ sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing) ++ sys.exit(1) ++ mngr["nfsiostat"] = ALL_METRICS ++ mngr["sysinfo"] = SYS_METRICS ++ mngr.printer = NfsiostatReport(opts,mngr) ++ sts = mngr.run() ++ sys.exit(sts) ++ except pmapi.pmErr as error: ++ sys.stderr.write('%s\n' % (error.message())) ++ except pmapi.pmUsageErr as usage: ++ usage.message() ++ sys.exit(1) ++ except IOError: ++ signal.signal(signal.SIGPIPE, signal.SIG_DFL) ++ except KeyboardInterrupt: ++ pass +-- +2.43.7 + diff --git a/SOURCES/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch b/SOURCES/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch new file mode 100644 index 0000000..45b975d --- /dev/null +++ b/SOURCES/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch @@ -0,0 +1,217 @@ +From e138ce29c6220377728b263b8c8cd8b0794cfcc2 Mon Sep 17 00:00:00 2001 +From: Sagar Sagar +Date: Tue, 3 Mar 2026 10:10:25 +0000 +Subject: [PATCH OL9 1013/1016] pmlogger_janitor fix not to terminate + unauthorized process + +- Fix pmlogger_janitor.sh to verify pid for active pmlogger before kill/removing map files +- Update process type detection to support *BSD ps output syntax +- Refactor pmlogger_check.sh to load janitor env vars more robustly + +upstream :- 358b684619d786acc93092824bc24ba69c5d5dbc + f305a22730a614535098c4f85c05de37eadc790e + +[Orabug:38598244] +Signed-off-by: sagar sagar +--- + src/pmlogger/pmlogger_check.sh | 26 +++++++++--- + src/pmlogger/pmlogger_farm.defaults | 7 ---- + src/pmlogger/pmlogger_janitor.sh | 62 +++++++++++++++++++++++++---- + src/pmlogger/utilproc.sh | 14 +++++++ + 4 files changed, 89 insertions(+), 20 deletions(-) + +diff --git a/src/pmlogger/pmlogger_check.sh b/src/pmlogger/pmlogger_check.sh +index 3593cea..87a9e72 100755 +--- a/src/pmlogger/pmlogger_check.sh ++++ b/src/pmlogger/pmlogger_check.sh +@@ -23,9 +23,6 @@ + + PMLOGGER="$PCP_BINADM_DIR/pmlogger" + PMLOGCONF="$PCP_BINADM_DIR/pmlogconf" +-PMLOGGERENVS="$PCP_SYSCONFIG_DIR/pmlogger" +-PMLOGGERFARMENVS="$PCP_SYSCONFIG_DIR/pmlogger_farm" +-PMLOGGERZEROCONFENVS="$PCP_SHARE_DIR/zeroconf/pmlogger" + + # error messages should go to stderr, not the GUI notifiers + # +@@ -1004,7 +1001,7 @@ END { print m }'` + if [ "X$primary" = Xy ] + then + # User configuration takes precedence over pcp-zeroconf +- envs=`grep -h ^PMLOGGER "$PMLOGGERZEROCONFENVS" "$PMLOGGERENVS" 2>/dev/null` ++ envs=`grep -h ^PMLOGGER "$PCP_SHARE_DIR/zeroconf/pmlogger" "$PCP_SYSCONFIG_DIR/pmlogger" 2>/dev/null` + args="-P $args" + iam=" primary" + # clean up port-map, just in case +@@ -1023,7 +1020,7 @@ END { print m }'` + continue + fi + else +- envs=`grep -h ^PMLOGGER "$PMLOGGERFARMENVS" 2>/dev/null` ++ envs=`grep -h ^PMLOGGER "$PCP_SYSCONFIG_DIR/pmlogger_farm" 2>/dev/null` + args="-h $host $args" + iam="" + fi +@@ -1191,9 +1188,26 @@ fi + # because the legitimate pmloggers, like the primary pmlogger, may + # not be included in the "test" control file(s). + # ++if [ -z "${PMLOGGER_CHECK_SKIP_JANITOR+is_set}" ] ++then ++ check=`grep '^PMLOGGER_CHECK_SKIP_JANITOR=' $PCP_SYSCONFIG_DIR/pmlogger` ++ if [ -n "$check" ] ++ then ++ eval $check ++ fi ++fi ++if [ -z "${PMLOGGER_JANITOR_ARGS+is_set}" ] ++then ++ check=`grep '^PMLOGGER_JANITOR_ARGS=' $PCP_SYSCONFIG_DIR/pmlogger` ++ if [ -n "$check" ] ++ then ++ eval $check ++ fi ++fi + if [ "$CONTROL" = "$PCP_PMLOGGERCONTROL_PATH" -a "$PMLOGGER_CHECK_SKIP_JANITOR" != "yes" ] + then +- $PCP_BINADM_DIR/pmlogger_janitor $daily_args ++ args="$daily_args" ++ $PCP_BINADM_DIR/pmlogger_janitor $args + fi + + [ -f $tmp/err ] && status=1 +diff --git a/src/pmlogger/pmlogger_farm.defaults b/src/pmlogger/pmlogger_farm.defaults +index 10a0132..d752dda 100644 +--- a/src/pmlogger/pmlogger_farm.defaults ++++ b/src/pmlogger/pmlogger_farm.defaults +@@ -26,10 +26,3 @@ + # and checking. + # PMLOGGER_CHECK_SKIP_LOGCONF=yes + +-# By default pmlogger_check(1) will run pmlogger_janitor to check for +-# pmlogger(1) badness caused by processes and/or files that were once +-# managed from the control files but have become detached from those +-# control files. +-# Setting PMLOGGER_CHECK_SKIP_JANITOR to yes disables pmlogger_janitor +-# and maybe useful for QA or special testing +-# PMLOGGER_CHECK_SKIP_JANITOR=yes +diff --git a/src/pmlogger/pmlogger_janitor.sh b/src/pmlogger/pmlogger_janitor.sh +index e111031..d7fb8d0 100755 +--- a/src/pmlogger/pmlogger_janitor.sh ++++ b/src/pmlogger/pmlogger_janitor.sh +@@ -77,7 +77,7 @@ _cleanup() + lockfile=`cat $tmp/lock 2>/dev/null` + [ -n "$lockfile" ] && rm -f "$lockfile" + rm -rf $tmp +- $VERY_VERBOSE && echo "End: `date '+%F %T.%N'`" ++ $VERBOSE && echo "End [janitor]: `_datestamp` status=$status" + } + + trap "_cleanup; exit \$status" 0 1 2 3 15 +@@ -242,11 +242,8 @@ else + exec 3>&2 1>"$MYPROGLOG" 2>&1 + fi + +-if $VERY_VERBOSE +-then +- echo "Start: `date '+%F %T.%N'`" +- _pstree_all $$ +-fi ++$VERBOSE && echo "Start [janitor]: `_datestamp`" ++$VERY_VERBOSE && _pstree_all $$ + + # if SaveLogs exists in the $PCP_LOG_DIR/pmlogger directory and is writeable + # then save $MYPROGLOG there as well with a unique name that contains the date +@@ -660,7 +657,13 @@ END { print m }'` + if [ -n "$pid" ] + then + # found matching pmlogger ... cull this one from +- $VERY_VERBOSE && echo "[$controlfile:$line] match PID $pid, nothing to be done" ++ if $VERY_VERBOSE ++ then ++ echo "[$controlfile:$line] match PID $pid, nothing to be done" ++ elif $VERBOSE ++ then ++ echo "Pass 3: PID $pid matches control [$controlfile:$line], nothing to be done" ++ fi + sed <$tmp/loggers >$tmp/tmp -e "/^$pid /d" + mv $tmp/tmp $tmp/loggers + fi +@@ -679,6 +682,51 @@ then + | while read file + do + pid=`echo "$file" | sed -e "s@$PCP_TMP_DIR/pmlogger/@@"` ++ # sanity checks ++ # 1. does this process exist? ++ # 2. is it really pmlogger? ++ # if "no" to either case, remove this (stale) mapfile ++ # and move on ... ++ # ++ if $PCP_PS_PROG -p "$pid" >$tmp/tmp 2>&1 ++ then ++ # ps(1) -p output should be something like this ... ++ # PID TTY TIME CMD ++ # 14298 ? 00:00:00 pmlogger ++ # or this (for *BSD) ++ # PID TT STAT TIME COMMAND ++ # 22839 1 S 0:00.04 /usr/libexec/pcp/bin/pmlogger -N -P ... ++ # ++ if sed -n -e 2p <$tmp/tmp | grep -E -q '( pmlogger$)|(/bin/pmlogger )' ++ then ++ : OK ++ else ++ if $VERBOSE ++ then ++ cat $tmp/tmp ++ echo "Warning: PID $pid is not a pmlogger process, removing $file" ++ fi ++ if $SHOWME ++ then ++ echo "+ rm $file" ++ else ++ rm -f "$file" ++ fi ++ continue ++ fi ++ else ++ if $VERBOSE ++ then ++ echo "Warning: PID $pid has vanished, removing $file" ++ fi ++ if $SHOWME ++ then ++ echo "+ rm $file" ++ else ++ rm -f "$file" ++ fi ++ continue ++ fi + # timing window here, file may have gone away between + # find(1) and awk(1), so just ignore any errors ... + # +diff --git a/src/pmlogger/utilproc.sh b/src/pmlogger/utilproc.sh +index 318b4a1..a0b62c9 100644 +--- a/src/pmlogger/utilproc.sh ++++ b/src/pmlogger/utilproc.sh +@@ -254,3 +254,17 @@ END { exit sts }' + return $__sts + fi + } ++ ++# current time to the highest precision available from date(1) and ++# strftime(3) ++# ++_datestamp() ++{ ++ if [ `date +%N` = N ] ++ then ++ # no %N, %T is the best we can do ++ date +'%F %T' ++ else ++ date +'%F %T.%N' ++ fi ++} +\ No newline at end of file +-- +2.43.7 + diff --git a/SOURCES/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch b/SOURCES/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch new file mode 100644 index 0000000..d93a62e --- /dev/null +++ b/SOURCES/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch @@ -0,0 +1,1278 @@ +From b4dd24f2fe7c84044cf1c5c2495b95efe48fdecd Mon Sep 17 00:00:00 2001 +From: Sagar Sagar +Date: Tue, 3 Mar 2026 10:11:06 +0000 +Subject: [PATCH OL9 1014/1016] pcp-ps implement --sort option to allow sorting + by %cpu, %mem + +- Implements the '--sort' option in 'pcp ps' include sorting by '%cpu' and '%mem'. + +upstream ref:- https://github.com/performancecopilot/pcp/pull/2436/ + +[Orabug:38719615] +Signed-off-by: sagar sagar +--- + src/pcp/ps/pcp-ps.1 | 20 +- + src/pcp/ps/pcp-ps.py | 920 +++++++++++++++++++++++++------------------ + 2 files changed, 557 insertions(+), 383 deletions(-) + +diff --git a/src/pcp/ps/pcp-ps.1 b/src/pcp/ps/pcp-ps.1 +index 8c44e67..42d1d5f 100755 +--- a/src/pcp/ps/pcp-ps.1 ++++ b/src/pcp/ps/pcp-ps.1 +@@ -1,5 +1,4 @@ +-'\"! tbl | mmdoc +-'\"macro stdmacro ++'\" t + .\" + .\" Copyright (c) 2022 Oracle and/or its affiliates. + .\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +@@ -28,6 +27,8 @@ + [\f3\-P\f1 \f2pid1,pid2..\f1] + [\f3\-p\f1 \f2pid1,pid2..\f1] + [\f3\-o\f1 \f2col1,col2... or ALL\f1] ++[\f3\-O\f1 \f2%mem,%cpu\f1] ++[\f3\-d\f1] + [\f3\-Z\f1 \f2timezone\f1] + [\f3\-z\f1] + [\f3\-?\f1] +@@ -244,6 +245,21 @@ WCHAN and COMMAND. + T} + .TE + .TP ++.BR \-O\ \fI%mem,%cpu\fR ", " \fB\-\-sort=\fI%mem,%cpu\fR ++Sort the output processes by the specified column. ++This sorting can now be used in conjunction with the ++.BR \-o ++option to sort on any user-specified output column (e.g., -o pid,%mem -O %mem). ++The column given to -O/--sort must appear in the -o list. ++If the sorting column is missing from the output format, an error will be shown. ++(In case of an unsupported or missing field name, sorting will default to %cpu.) ++.TP ++.BR \-d\, ++debug mode ++.br ++Enable debug mode, which will cause debug messages to be written to ++stdout. ++.TP + .BR \-Z \ \fItimezone\fR ", "\fB\-\-timezone=\fItimezone\fR + By default, + .B pcp-ps +diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py +index d31fc9a..c8f2e9d 100755 +--- a/src/pcp/ps/pcp-ps.py ++++ b/src/pcp/ps/pcp-ps.py +@@ -60,107 +60,86 @@ class NoneHandlingPrinterDecorator: + class ReportingMetricRepository: + def __init__(self, group): + self.group = group +- self.current_cached_values = {} +- self.previous_cached_values = {} +- +- def __fetch_current_values(self, metric, instance): +- if instance: +- return dict(map(lambda x: (x[0].inst, x[2]), self.group[metric].netValues)) +- else: +- return self.group[metric].netValues[0][2] +- +- def __fetch_previous_values(self, metric, instance): +- if instance: +- return dict(map(lambda x: (x[0].inst, x[2]), self.group[metric].netPrevValues)) +- else: +- return self.group[metric].netPrevValues[0][2] +- +- def current_value(self, metric, instance): +- if not metric in self.group: +- return None +- if instance: +- if self.current_cached_values.get(metric, None) is None: +- lst = self.__fetch_current_values(metric, instance) +- self.current_cached_values[metric] = lst +- +- return self.current_cached_values[metric].get(instance, None) +- else: +- if self.current_cached_values.get(metric, None) is None: +- self.current_cached_values[metric] = self.__fetch_current_values(metric, instance) +- return self.current_cached_values.get(metric, None) +- +- def previous_value(self, metric, instance): +- if not metric in self.group: +- return None +- if instance: +- if self.previous_cached_values.get(metric, None) is None: +- lst = self.__fetch_previous_values(metric, instance) +- self.previous_cached_values[metric] = lst +- +- return self.previous_cached_values[metric].get(instance, None) +- else: +- if self.previous_cached_values.get(metric, None) is None: +- self.previous_cached_values[metric] = self.__fetch_previous_values(metric, instance) +- return self.previous_cached_values.get(metric, None) ++ self._current_cache = {} ++ self._previous_cache = {} ++ ++ def _fetch_values(self, metric, use_previous=False): ++ """Fetch values - always returns a dictionary.""" ++ if metric not in self.group: ++ return {} ++ attr = "netPrevValues" if use_previous else "netValues" ++ values = getattr(self.group[metric], attr, []) ++ return {x[0].inst: x[2] for x in values} if values else {} ++ ++ def _get_values_dict(self, metric, use_previous=False): ++ """Get cached dictionary of all values for a metric.""" ++ cache = self._previous_cache if use_previous else self._current_cache ++ if metric not in cache: ++ cache[metric] = self._fetch_values(metric, use_previous) ++ return cache[metric] ++ ++ def current_value(self, metric, instance=None): ++ """Get current value. Returns single value if instance given, else returns dict.""" ++ values_dict = self._get_values_dict(metric, use_previous=False) ++ if instance is not None: ++ return values_dict.get(instance) ++ return values_dict ++ ++ def previous_value(self, metric, instance=None): ++ """Get previous value. Returns single value if instance given, else returns dict.""" ++ values_dict = self._get_values_dict(metric, use_previous=True) ++ if instance is not None: ++ return values_dict.get(instance) ++ return values_dict + + def current_values(self, metric_name): +- if self.group.get(metric_name, None) is None: +- return None +- if self.current_cached_values.get(metric_name, None) is None: +- self.current_cached_values[metric_name] = self.__fetch_current_values(metric_name, True) +- return self.current_cached_values.get(metric_name, None) ++ """Get all current values for a metric (returns dict).""" ++ return self._get_values_dict(metric_name, use_previous=False) + + def previous_values(self, metric_name): +- if self.group.get(metric_name, None) is None: +- return None +- if self.previous_cached_values.get(metric_name, None) is None: +- self.previous_cached_values[metric_name] = self.__fetch_previous_values(metric_name, True) +- return self.previous_cached_values.get(metric_name, None) ++ """Get all previous values for a metric (returns dict).""" ++ return self._get_values_dict(metric_name, use_previous=True) + + + class ProcessFilter: ++ """ ++ Optimized filtering of processes based on user-provided options. ++ Precomputes filter predicates at instantiation to minimize overhead per process, ++ and leverages generator expressions with all() for efficiency. ++ """ + def __init__(self, options): + self.options = options ++ self._predicates = [] ++ flag = getattr(options, "universal_flag", None) ++ # Username filter ++ if flag == "username" and getattr(options, "filtered_process_user", None) is not None: ++ required_user = options.filtered_process_user.strip() ++ self._predicates.append(lambda proc: proc.user_name().strip() == required_user) ++ # PID filter ++ if flag == "pid" and getattr(options, "pid_list", None) is not None: ++ try: ++ pids = set(int(pid) for pid in options.pid_list) ++ self._predicates.append(lambda proc: int(proc.pid()) in pids) ++ except Exception: ++ self._predicates.append(lambda proc: False) ++ # PPID filter ++ if flag == "ppid" and getattr(options, "ppid_list", None) is not None: ++ try: ++ ppids = set(int(ppid) for ppid in options.ppid_list) ++ self._predicates.append(lambda proc: int(proc.ppid()) in ppids) ++ except Exception: ++ self._predicates.append(lambda proc: False) ++ # Command name filter ++ if flag == "command" and getattr(options, "command_list", None) is not None: ++ commands = set(cmd.strip() for cmd in options.command_list if cmd is not None) ++ self._predicates.append(lambda proc: (proc.process_name() or "").strip() in commands) ++ # If no filter flag: no filter ++ if not getattr(options, "filter_flag", False) or not self._predicates: ++ self._predicates.append(lambda proc: True) + + def filter_processes(self, processes): +- return filter(lambda p: self.__predicate(p), processes) +- +- def __predicate(self, process): +- if self.options.filter_flag: +- return bool(self.__matches_process_username(process) +- and self.__matches_process_pid(process) +- and self.__matches_process_name(process) +- and self.__matches_process_ppid(process)) +- else: +- return True +- +- def __matches_process_username(self, process): +- if self.options.username_filter_flag is True and self.options.filtered_process_user is not None: +- return process.user_name().strip() == self.options.filtered_process_user.strip() +- else: +- return True +- +- def __matches_process_pid(self, process): +- if self.options.pid_filter_flag: +- if self.options.pid_list is not None: +- pid = int(process.pid()) +- return bool(pid in self.options.pid_list) +- return True +- +- def __matches_process_ppid(self, process): +- if self.options.ppid_filter_flag: +- if self.options.ppid_list is not None: +- ppid = int(process.ppid()) +- return bool(ppid in self.options.ppid_list) +- return True +- +- def __matches_process_name(self, process): +- name = process.process_name() +- if self.options.command_filter_flag is True and self.options.command_list is not None and name is not None: +- return name.strip() in self.options.command_list +- else: +- return True +- ++ """Yield processes matching all active predicates.""" ++ return (proc for proc in processes if all(pred(proc) for pred in self._predicates)) + + class ProcessStatusUtil: + def __init__(self, instance, manager, delta_time, metrics_repository): +@@ -169,106 +148,88 @@ class ProcessStatusUtil: + self.__delta_time = delta_time + self.__metric_repository = metrics_repository + ++ def __get_value(self, metric, instance=None): ++ return self.__metric_repository.current_value(metric, instance) ++ def __get_previous_value(self, metric, instance=None): ++ return self.__metric_repository.previous_value(metric, instance) ++ + def pid(self): +- data = str(self.__metric_repository.current_value('proc.psinfo.pid', self.instance)) +- if len(str(data)) < 8: +- whitespace = 8 - len(str(data)) +- res = data.ljust(whitespace + len(str(data)), ' ') +- return res +- else: +- return data ++ data = str(self.__get_value('proc.psinfo.pid', self.instance)) ++ return data.ljust(8) if len(data) < 8 else data ++ + + def ppid(self): +- data = str(self.__metric_repository.current_value('proc.psinfo.ppid', self.instance)) +- if len(str(data)) < 8: +- whitespace = 8 - len(str(data)) +- res = data.ljust(whitespace + len(str(data)), ' ') +- return res +- else: +- return data ++ data = str(self.__get_value('proc.psinfo.ppid', self.instance)) ++ return data.ljust(8) if len(data) < 8 else data + + def user_name(self): +- data = self.__metric_repository.current_value('proc.id.uid_nm', self.instance)[:10] +- if len(data) < 10: +- whitespace = 10 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res +- else: ++ data = self.__get_value('proc.id.uid_nm', self.instance) ++ if data is None: + return data ++ return data.ljust(10) if len(data) < 10 else data[:10] + + def process_name(self): +- try: +- data = self.__metric_repository.current_value('proc.psinfo.cmd', self.instance)[:20] +- if len(data) < 20: +- whitespace = 20 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res +- else: +- return data +- except TypeError: +- data = '-' ++ data = self.__get_value('proc.psinfo.cmd', self.instance) ++ if data is None: ++ return '-' ++ return data.ljust(20) if len(data) < 20 else data[:20] ++ ++ def process_name_with_args(self, flag=False): ++ data = self.__get_value('proc.psinfo.psargs', self.instance) ++ if data is None: + return data +- +- def process_name_with_args(self,flag = False): +- if flag is True: +- data = self.__metric_repository.current_value('proc.psinfo.psargs', self.instance) +- else: +- data = self.__metric_repository.current_value('proc.psinfo.psargs', self.instance)[:30] +- if len(data) < 30: +- whitespace = 30 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res ++ max_length = 30 ++ if not flag: ++ return data[:max_length].ljust(max_length) + else: + return data + + def process_name_with_args_last(self): + return self.process_name_with_args(True) + def vsize(self): +- return self.__metric_repository.current_value('proc.psinfo.vsize', self.instance) ++ return self.__get_value('proc.psinfo.vsize', self.instance) + + def rss(self): +- return self.__metric_repository.current_value('proc.psinfo.rss', self.instance) +- ++ return self.__get_value('proc.psinfo.rss', self.instance) + def mem(self): +- total_mem = self.__metric_repository.current_value('mem.physmem', None) +- rss = self.__metric_repository.current_value('proc.psinfo.rss', self.instance) ++ total_mem = self.__get_value('mem.physmem', None) ++ if isinstance(total_mem, dict): ++ total_mem = next(iter(total_mem.values()), None) ++ rss = self.__get_value('proc.psinfo.rss', self.instance) + if total_mem is not None and rss is not None: + return float("%.2f" % (100 * float(rss) / total_mem)) + else: + return None + + def s_name(self): +- return self.__metric_repository.current_value('proc.psinfo.sname', self.instance) ++ return self.__get_value('proc.psinfo.sname', self.instance) + + def cpu_number(self): +- return self.__metric_repository.current_value('proc.psinfo.processor', self.instance) ++ return self.__get_value('proc.psinfo.processor', self.instance) + + def system_percent(self): +- c_systemtime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_systemtime = self.__metric_repository.previous_value('proc.psinfo.stime', self.instance) ++ c_systemtime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_systemtime = self.__get_previous_value('proc.psinfo.stime', self.instance) + if c_systemtime is not None and p_systemtime is not None: +- percent_of_time = 100 * float(c_systemtime - p_systemtime) / float(1000 * self.__delta_time) ++ system_time_diff = float(c_systemtime - p_systemtime) ++ delta_time_in_ms = float(1000 * self.__delta_time) ++ percent_of_time = 100 * system_time_diff / delta_time_in_ms + return float("%.2f" % percent_of_time) + else: +- return None ++ return '-' + + def wchan_s(self): +- process = self.__metric_repository.current_value('proc.psinfo.wchan_s', self.instance) +- if process is None: +- process = '-' +- return process +- elif len(process) < 30: +- whitespace = 30 - len(process) +- res = process.ljust(whitespace + len(process), ' ') +- return res +- return process[:30] ++ process = self.__get_value('proc.psinfo.wchan_s', self.instance) ++ if process is None or process == "0" or process == '': ++ return '-' + ' ' * 29 ++ return process.ljust(30) if len(process) < 30 else process[:30] + + def priority(self): +- return self.__metric_repository.current_value('proc.psinfo.priority', self.instance) ++ return self.__get_value('proc.psinfo.priority', self.instance) + + def user_percent(self): +- c_usertime = self.__metric_repository.current_value('proc.psinfo.utime', self.instance) +- p_usertime = self.__metric_repository.previous_value('proc.psinfo.utime', self.instance) ++ c_usertime = self.__get_value('proc.psinfo.utime', self.instance) ++ p_usertime = self.__get_previous_value('proc.psinfo.utime', self.instance) + if c_usertime is not None and p_usertime is not None: + percent_of_time = 100 * float(c_usertime - p_usertime) / float(1000 * self.__delta_time) + return float("%.2f" % percent_of_time) +@@ -276,8 +237,8 @@ class ProcessStatusUtil: + return None + + def guest_percent(self): +- c_guesttime = self.__metric_repository.current_value('proc.psinfo.guest_time', self.instance) +- p_guesttime = self.__metric_repository.previous_value('proc.psinfo.guest_time', self.instance) ++ c_guesttime = self.__get_value('proc.psinfo.guest_time', self.instance) ++ p_guesttime = self.__get_previous_value('proc.psinfo.guest_time', self.instance) + if c_guesttime is not None and p_guesttime is not None: + percent_of_time = 100 * float(c_guesttime - p_guesttime) / float(1000 * self.__delta_time) + return float("%.2f" % percent_of_time) +@@ -291,8 +252,8 @@ class ProcessStatusUtil: + return None + + def stime(self): +- c_systime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_systime = self.__metric_repository.previous_value('proc.psinfo.stime', self.instance) ++ c_systime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_systime = self.__get_previous_value('proc.psinfo.stime', self.instance) + # sometimes the previous_value seems to be Nonetype, not sure why + if p_systime is None: # print a '?' here + return '?' +@@ -300,21 +261,28 @@ class ProcessStatusUtil: + return c_systime - p_systime + + def start(self): +- s_time = self.__metric_repository.current_value('proc.psinfo.start_time', self.instance) ++ s_time = self.__get_value('proc.psinfo.start_time', self.instance) + group = self.manager['psstat'] +- kernel_boottime = group['kernel.all.boottime'].netValues[0][2] +- ts = group.contextCache.pmLocaltime(int(kernel_boottime + (s_time / 1000))) +- if group.timestamp.tv_sec - (kernel_boottime + s_time / 1000) >= 24*60*60: +- # started one day or more ago, use MmmDD HH:MM ++ try: ++ kernel_boottime = group['kernel.all.boottime'].netValues[0][2] ++ except (KeyError, IndexError, TypeError, AttributeError): ++ return '?' ++ if s_time is None or kernel_boottime is None: ++ return '?' ++ ts_val = kernel_boottime + (s_time / 1000.0) ++ try: ++ ts = group.contextCache.pmLocaltime(int(ts_val)) ++ except Exception: ++ return '?' ++ if group.timestamp.tv_sec - ts_val >= 24*60*60: + return time.strftime("%b%d %H:%M", ts.struct_time()) + else: +- # started less than one day ago, use HH:MM:SS + return time.strftime("%H:%M:%S", ts.struct_time()) + + + def total_time(self): +- c_usertime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_guesttime = self.__metric_repository.previous_value('proc.psinfo.utime', self.instance) ++ c_usertime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_guesttime = self.__get_previous_value('proc.psinfo.utime', self.instance) + timefmt = "%H:%M:%S" + if c_usertime and p_guesttime is not None: + total_time = (c_usertime / 1000) + (p_guesttime / 1000) +@@ -323,29 +291,28 @@ class ProcessStatusUtil: + return time.strftime(timefmt, time.gmtime(total_time)) + + def tty_name(self): +- return self.__metric_repository.current_value('proc.psinfo.ttyname', self.instance) ++ return self.__get_value('proc.psinfo.ttyname', self.instance) + + def user_id(self): +- return self.__metric_repository.current_value('proc.id.uid', self.instance) ++ return self.__get_value('proc.id.uid', self.instance) + + def start_time(self): +- return self.__metric_repository.current_value('proc.psinfo.start_time', self.instance) ++ return self.__get_value('proc.psinfo.start_time', self.instance) + + def func_state(self): +- s_name = self.__metric_repository.current_value('proc.psinfo.sname', self.instance) ++ s_name = self.__get_value('proc.psinfo.sname', self.instance) + if s_name == 'R': +- return 'N/A' ++ return '-' + elif s_name is None: +- return '?' ++ return '-' + else: + return self.wchan_s() + + def policy(self): +- policy_int = self.__metric_repository.current_value('proc.psinfo.policy', self.instance) +- if policy_int is not None and policy_int <= len(SCHED_POLICY): +- # return policy_int ++ policy_int = self.__get_value('proc.psinfo.policy', self.instance) ++ if isinstance(policy_int, int) and 0 <= policy_int < len(SCHED_POLICY): + return SCHED_POLICY[policy_int] +- return None ++ return '?' + + + PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), +@@ -353,16 +320,16 @@ PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), + "start": ("START\t", ProcessStatusUtil.start), + "time": ("TIME\t", ProcessStatusUtil.total_time), + "cls": ("CLS", ProcessStatusUtil.policy), +- "cmd": ("Command\t\t\t", ProcessStatusUtil.process_name), ++ "cmd": ("Command\t\t", ProcessStatusUtil.process_name), + "args": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args), +- "args_last": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args_last), ++ "args_last": ("Command", ProcessStatusUtil.process_name_with_args_last), + "pid": ("PID\t", ProcessStatusUtil.pid), + "ppid": ("PPID\t", ProcessStatusUtil.ppid), + "pri": ("PRI", ProcessStatusUtil.priority), + "state": ("S", ProcessStatusUtil.s_name), + "rss": ("RSS", ProcessStatusUtil.rss), + "rtprio": ("RTPRIO", ProcessStatusUtil.priority), +- "tty": ("TTY\t", ProcessStatusUtil.tty_name), ++ "tty": ("TTY", ProcessStatusUtil.tty_name), + "pname": ("Pname\t\t", ProcessStatusUtil.process_name), + "vsize": ("VSZ", ProcessStatusUtil.vsize), + "uname": ("USER\t", ProcessStatusUtil.user_name), +@@ -376,8 +343,8 @@ class ProcessStatus: + self.__metric_repository = metric_repository + + def get_processes(self, delta_time): +- return map(lambda pid: +- (ProcessStatusUtil(pid, self.__manager, delta_time, self.__metric_repository)), self.__pids()) ++ # Use generator expression for lazy evaluation and reduced memory usage ++ return (ProcessStatusUtil(pid, self.__manager, delta_time, self.__metric_repository) for pid in self.__pids()) + + def __pids(self): + pid_dict = self.__metric_repository.current_values('proc.psinfo.pid') +@@ -397,65 +364,114 @@ class DynamicProcessReporter: + self.processStatOptions.colum_list.index(key) == len(self.processStatOptions.colum_list) - 1 + def print_report(self, timestamp, header_indentation, value_indentation): + +- # when the print count is exhausted exit the program gracefully +- # we can't use break here because it's being called by the run manager +- if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: +- if self.processStatOptions.print_count == 0: +- sys.exit(0) ++ # Exit logic for non-archive context ++ if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE and self.processStatOptions.print_count == 0: ++ sys.exit(0) ++ ++ # Sorting validations ++ sorting_idx = None ++ if self.processStatOptions.sorting_flag: ++ if self.processStatOptions.filterstate == "ALL": ++ if self.processStatOptions.sorting_order == '%mem': ++ sorting_idx = 7 ++ elif self.processStatOptions.sorting_order == '%cpu': ++ sorting_idx = 8 + else: +- self.processStatOptions.print_count -= 1 +- +- if self.processStatOptions.filterstate is not None: +- self.printer("Timestamp" + header_indentation + +- "USER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ\tRSS\tS\tSTARTED\t\tTIME\t\tWCHAN\t\t\t\tCommand") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list) ++ if key == self.processStatOptions.sorting_order), ++ None) ++ if sorting_idx is None: ++ raise ValueError("Sorting order not found in output columns") ++ # Adjust for timestamp column ++ sorting_idx += 1 ++ ++ # Always compute process list ONCE ++ processes = self.process_filter.filter_processes( ++ self.process_report.get_processes(self.delta_time) ++ ) ++ ++ output_list = [] ++ header = None ++ ++ # -------- PATH 1: With filterstate -------- # ++ if self.processStatOptions.filterstate == "ALL": ++ header = ( ++ "Timestamp\tUSER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ" ++ "\tRSS\tS\tSTARTED\t\tTIME\t\t" ++ "WCHAN\t\t\t\tCommand" ++ ) ++ ++ # Precompute format string ++ # fmt = ( ++ # "{ts}{indent}{user}\t{pid}\t{ppid}\t{pri}\t{cpu}\t{mem}\t" ++ # "{vsz}\t{rss}\t{s}\t{started}\t{time}\t{wchan}\t{cmd}" ++ # ) + for process in processes: +- total_percent = process.total_percent() +- current_process_pid = process.pid() +- current_process_sname = process.s_name() +- if process.wchan_s() is not None: +- wchan = process.wchan_s() +- else: +- wchan = '-' +- key = (current_process_sname, current_process_pid) +- if key in process_state_info: +- process_state_info[key] = process_state_info[key] + self.delta_time +- else: +- process_state_info[key] = self.delta_time +- process_name = process.process_name_with_args() +- if 7 < len(wchan) < 15: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) +- elif len(wchan) >= 15: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) +- else: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) ++ # Maintain state info ++ key = (process.s_name(), process.pid()) ++ process_state_info[key] = process_state_info.get(key, 0) + self.delta_time ++ row = [timestamp] ++ row.extend([ ++ process.user_name(), ++ process.pid(), ++ process.ppid(), ++ process.priority(), ++ process.total_percent(), ++ process.system_percent(), ++ process.vsize(), ++ process.rss(), ++ process.s_name(), ++ process.start(), ++ process.total_time(), ++ process.wchan_s(), ++ process.process_name_with_args_last()[:45] ++ ]) ++ output_list.append( ++ "\t".join( ++ str(x) if x is not None else '' for x in row ++ ) ++ ) ++ ++ # -------- PATH 2: Customized column list -------- # + elif self.processStatOptions.colum_list is not None: +- header = "Timestamp" + '\t' ++ ++ header = "Timestamp\t" + for key in self.processStatOptions.colum_list: + if key in PIDINFO_PAIR: +- header += PIDINFO_PAIR[key][0] + '\t\t' +- print(header) +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ header += PIDINFO_PAIR[key][0] + "\t" ++ + for process in processes: +- data_to_print = timestamp + '\t' ++ row = [timestamp] + for key in self.processStatOptions.colum_list: + if self._is_last_and_args(key): +- data_to_print += str(PIDINFO_PAIR["args_last"][1](process)) + '\t\t' ++ row.append(str(PIDINFO_PAIR["args_last"][1](process))) + elif key in PIDINFO_PAIR: +- data_to_print += str(PIDINFO_PAIR[key][1](process)) + '\t\t' +- print(data_to_print) ++ row.append(str(PIDINFO_PAIR[key][1](process))) ++ # print(row) ++ output_list.append("\t".join(str(x) if x is not None else '' for x in row)) ++ ++ # -------- PATH 3: Invalid filterstate or column list -------- # ++ # This should never happen, but just in case ++ else: ++ raise ValueError("No valid filterstate or column list provided") ++ ++ # Sorting logic ++ if self.processStatOptions.sorting_flag and sorting_idx is not None: ++ output_list.sort( ++ key=lambda x: ( ++ sorting_idx, ++ float('inf') if ( ++ len(x.split()) <= sorting_idx or ++ not x.split()[sorting_idx].replace('.', '', 1).isdigit() ++ ) else float(x.split()[sorting_idx]) ++ ), ++ reverse=True ++ ) ++ ++ # --------- Print output --------- # ++ self.printer(header) ++ self.printer('\n'.join(output_list)) ++ + + + class ProcessStatusReporter: +@@ -467,92 +483,110 @@ class ProcessStatusReporter: + self.processStatOptions = processStatOptions + + def print_report(self, timestamp, header_indentation, value_indentation): +- +- # when the print count is exhausted exit the program gracefully +- # we can't use break here because it's being called by the run manager +- if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: +- if self.processStatOptions.print_count == 0: +- sys.exit(0) +- else: +- self.processStatOptions.print_count -= 1 +- +- if self.processStatOptions.show_all_process: +- self.printer("Timestamp" + header_indentation + "PID\t\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ if self.processStatOptions.debug_mode: ++ print("option selected: %s" % self.processStatOptions.universal_flag) ++ print("filer option status: %s" % self.processStatOptions.filterstate) ++ FORMAT_MAP = { ++ "empty_arg" : "Timestamp" + header_indentation + "PID\t\tTIME\t\tCMD", ++ "all" : "Timestamp" + header_indentation + "PID\t\t\tTTY\tTIME\t\tCMD", ++ "user": "Timestamp" + header_indentation + "USERNAME\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + ++ "TTY\tSTAT\tTIME\t\tSTART\t\tCOMMAND", ++ "pid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", ++ "ppid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", ++ "username": "Timestamp" + header_indentation + "USERNAME\t\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + ++ "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND", ++ "command": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD" ++ } ++ selected_flag = self.processStatOptions.universal_flag ++ header = FORMAT_MAP.get(selected_flag, FORMAT_MAP["all"]) ++ # Bulk buffer for all rows, print all at once for efficiency ++ output_rows = [] ++ cpu_idx , mem_idx = 0, 0 ++ processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ def safe_str(val): ++ return '' if val is None else str(val) ++ if selected_flag == "all": + for process in processes: +- command = process.process_name_with_args(True) +- ttyname = process.tty_name() +- self.printer("%s%s%s\t\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ttyname, +- process.total_time(), command)) +- elif self.processStatOptions.empty_arg_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.tty_name()), safe_str(process.total_time()), ++ safe_str(process.process_name_with_args(True)))) ++ elif selected_flag == "empty_arg": + for process in processes: +- pid = process.pid() +- command = process.process_name() +- self.printer("%s%s%s\t%s\t%s" % (timestamp, value_indentation, pid, +- process.total_time(), command)) +- elif self.processStatOptions.pid_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "pid": + for process in processes: +- pid = process.pid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, pid, process.ppid(), +- ttyname, process.total_time(), command)) +- elif self.processStatOptions.ppid_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), safe_str(process.ppid()), ++ safe_str(process.tty_name()), safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "ppid": + for process in processes: +- ppid = process.ppid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ppid, ttyname, +- process.total_time(), command)) +- elif self.processStatOptions.username_filter_flag: +- self.printer("Timestamp" + header_indentation + "USERNAME\t\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + +- "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.ppid()), safe_str(process.tty_name()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "username": + for process in processes: +- self.printer("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( +- timestamp, value_indentation, process.user_name(), process.pid(), +- process.system_percent(), process.total_percent(), process.vsize(), process.rss(), +- process.tty_name(), process.ppid(), process.total_time(), process.start(), +- process.process_name())) +- +- elif self.processStatOptions.user_oriented_format: +- self.printer("Timestamp" + header_indentation + "USERNAME\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + +- "TTY\tSTAT\tTIME\t\tSTART\t\tCOMMAND") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()), ++ safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.total_percent()), ++ safe_str(process.vsize()), safe_str(process.rss()), ++ safe_str(process.tty_name()), safe_str(process.ppid()), safe_str(process.total_time()), ++ safe_str(process.start()), ++ safe_str(process.process_name()))) ++ cpu_idx = 5 ++ mem_idx = 6 ++ elif selected_flag == "user": + for process in processes: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( +- timestamp, value_indentation, process.user_name(), process.pid(), +- process.system_percent(), process.total_percent(), process.vsize(), process.rss(), +- process.tty_name(), process.s_name(), process.total_time(), process.start(), +- process.process_name())) +- +- elif self.processStatOptions.command_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()), ++ safe_str(process.pid()), ++ safe_str(process.system_percent()), safe_str(process.total_percent()), ++ safe_str(process.vsize()), safe_str(process.rss()), ++ safe_str(process.tty_name()), safe_str(process.s_name()), ++ safe_str(process.total_time()), safe_str(process.start()), ++ safe_str(process.process_name()))) ++ cpu_idx = 5 ++ mem_idx = 6 ++ elif selected_flag == "command": + for process in processes: +- ppid = process.ppid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ppid, ttyname, +- process.total_time(), command)) +- ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.ppid()), safe_str(process.tty_name()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ else: # default fallback, print nothing extra ++ pass ++ if self.processStatOptions.sorting_flag: ++ if cpu_idx == 0 and mem_idx == 0: ++ raise ValueError("Sorting indices not set for selected flag, " ++ "please remove sorting flag or choose another flag for output") ++ if self.processStatOptions.sorting_order == '%cpu': ++ output_rows.sort( ++ key=lambda x: float(x.split()[cpu_idx]) if x.split()[cpu_idx].replace('.', '', 1).isdigit() ++ else float('-inf'), ++ reverse=True) ++ elif self.processStatOptions.sorting_order == '%mem': ++ output_rows.sort( ++ key=lambda x: float(x.split()[mem_idx]) if x.split()[mem_idx].replace('.', '', 1).isdigit() ++ else float('-inf'), ++ reverse=True) ++ if output_rows: ++ self.printer(header) ++ self.printer('\n'.join(output_rows)) + + class ProcessStatReport(pmcc.MetricGroupPrinter): + Machine_info_count = 0 + group = None +- def __init__(self, group=None): ++ def __init__(self, group=None, options = None): + self.group = group ++ self.processStatOptions = options + + def timeStampDelta(self): + s = self.group.timestamp.tv_sec - self.group.prevTimestamp.tv_sec +- u = self.group.timestamp.tv_usec - self.group.prevTimestamp.tv_usec +- return s + u / 1000000.0 ++ n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec ++ return s + n / 1000000000.0 + + def print_machine_info(self,context): + timestamp = context.pmLocaltime(self.group.timestamp.tv_sec) +@@ -573,70 +607,85 @@ class ProcessStatReport(pmcc.MetricGroupPrinter): + return group['hinv.ncpu'].netValues[0][2] + + def __print_report(self, manager,timestamp, header_indentation, value_indentation,interval_in_seconds): ++ if self.processStatOptions.debug_mode: ++ print("Printing standard report") + metric_repository = ReportingMetricRepository(self.group) + process_report = ProcessStatus(manager, metric_repository) +- process_filter = ProcessFilter(ProcessStatOptions) ++ process_filter = ProcessFilter(self.processStatOptions) + stdout = StdoutPrinter() + printdecorator = NoneHandlingPrinterDecorator(stdout) + report = ProcessStatusReporter(process_report, process_filter, interval_in_seconds, +- printdecorator.Print, ProcessStatOptions) ++ printdecorator.Print, self.processStatOptions) + report.print_report(timestamp, header_indentation, value_indentation) + def __print_dynamic_report(self, manager,timestamp, header_indentation, value_indentation,interval_in_seconds): ++ if self.processStatOptions.debug_mode: ++ print("Printing dynamic report") + metric_repository = ReportingMetricRepository(self.group) + process_report = ProcessStatus(manager, metric_repository) +- process_filter = ProcessFilter(ProcessStatOptions) ++ process_filter = ProcessFilter(self.processStatOptions) + stdout = StdoutPrinter() + printdecorator = NoneHandlingPrinterDecorator(stdout) + report = DynamicProcessReporter(process_report, process_filter, interval_in_seconds, +- printdecorator.Print, ProcessStatOptions) ++ printdecorator.Print, self.processStatOptions) + report.print_report(timestamp, header_indentation, value_indentation) + def __get_timestamp(self): + ts = self.group.contextCache.pmLocaltime(int(self.group.timestamp)) +- timestamp = time.strftime(ProcessStatOptions.timefmt, ts.struct_time()) ++ timestamp = time.strftime(self.processStatOptions.timefmt, ts.struct_time()) + return timestamp + + def report(self, manager): + try: + if self.group['proc.psinfo.utime'].netPrevValues is None: +- # need two fetches to report rate converted counter metrics +- return ++ return False # Not ready, skip increment + if not self.group['hinv.ncpu'].netValues or not self.group['kernel.uname.sysname'].netValues: +- return ++ return False + try: + if not self.Machine_info_count: + self.print_machine_info(manager) + self.Machine_info_count = 1 + except IndexError: +- return ++ if self.processStatOptions.debug_mode: ++ print("IndexError while printing machine info") ++ return False ++ if self.processStatOptions.debug_mode: ++ print("Starting report generation") ++ print("Need to print samples: %s" % self.processStatOptions.print_count) ++ if self.processStatOptions.print_count == 0: ++ if self.processStatOptions.debug_mode: ++ print("Print count exhausted, exiting") ++ sys.exit(0) + timestamp = self.__get_timestamp() + interval_in_seconds = self.timeStampDelta() + header_indentation = " " if len(timestamp) < 9 else (len(timestamp) - 7) * " " + value_indentation = ((len(header_indentation) + 9) - len(timestamp)) * " " + + # Doing this for one single print instance in case there is no count specified +- if ProcessStatOptions.print_count is None: +- ProcessStatOptions.print_count = 1 ++ if self.processStatOptions.print_count is None \ ++ and self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: ++ self.processStatOptions.print_count = 1 + # ================================================================ +- if ProcessStatOptions.selective_colum_flag: ++ if self.processStatOptions.selective_colum_flag: ++ if self.processStatOptions.debug_mode: ++ print("Selective column flag is set") + self.__print_dynamic_report(manager,timestamp, header_indentation, + value_indentation, interval_in_seconds) + else: + self.__print_report(manager,timestamp, header_indentation, value_indentation, interval_in_seconds) ++ if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: ++ self.processStatOptions.print_count -= 1 ++ return True # Data was printed + finally: + sys.stdout.flush() + + + class ProcessStatOptions(pmapi.pmOptions): +- show_all_process = False +- command_filter_flag = False +- ppid_filter_flag = False +- pid_filter_flag = False +- username_filter_flag = False ++ universal_flag = None + selective_colum_flag = False + filter_flag = False +- user_oriented_format = False +- empty_arg_flag = False + filterstate = None ++ debug_mode = False ++ sorting_flag = False ++ sorting_order = None + timefmt = "%H:%M:%S" + print_count = None + colum_list = [] +@@ -647,7 +696,7 @@ class ProcessStatOptions(pmapi.pmOptions): + context = None + + def __init__(self): +- pmapi.pmOptions.__init__(self, "t:c:e::p:ukVZ:z?:o:P:l:U:k") ++ pmapi.pmOptions.__init__(self, "t:c:e::p:ukVZ:z?:o:P:l:U:k:O:d") + self.pmSetOptionCallback(self.extraOptions) + self.pmSetOverrideCallback(self.override) + self.options() +@@ -662,108 +711,214 @@ class ProcessStatOptions(pmapi.pmOptions): + "Select the process by process ID") + self.pmSetLongOption("", 1, "P", "[ppid1,ppid2,...]", "Select the process by process parent ID") + self.pmSetLongOption("", 1, "U", "[User Name]", "Select the process by user name") +- self.pmSetLongOption("", 1, "o", "[col1,col2,... Or ALL]", "User -defined format " + +- "USE -o [all] or -B [col1, col2 , ...]" + +- "\n\t\t\tsupported user defined colums are command, wchan, started, Time, pid, ppid, " +- "%mem, pri, user, %cpu and S " +- "\n\t\t\tALL option shows USER,PID,PPID,PRI,%CPU,%MEM,VSZ,RSS,S,STARTED,TIME,WCHAN and " +- "Command") ++ self.pmSetLongOption( ++ "", ++ 1, ++ "o", ++ "[col1,col2,... Or ALL]", ++ ( ++ "User -defined format USE -o [all] or -B [col1, col2 , ...]\n" ++ "\t\t\tsupported user defined colums are command, wchan, started, Time, pid, ppid, " ++ "%mem, pri, user, %cpu and S\n" ++ "\t\t\tALL option shows USER,PID,PPID,PRI,%CPU,%MEM,VSZ,RSS,S,STARTED,TIME,WCHAN and\n" ++ "Command" ++ ), ++ ) + self.pmSetLongOptionText("\tCOL\tHEADER \tDESCRIPTION") + self.pmSetLongOptionText("\t%cpu\t%CPU \tcpu utilization of the process") + self.pmSetLongOptionText("\t%mem\t%MEM \tphysical memory on the machine expressed as a percentage") + self.pmSetLongOptionText("\tstart\tSTART \ttime the command started") + self.pmSetLongOptionText("\ttime\tTIME \taccumulated cpu time, user + system") +- self.pmSetLongOptionText("\tcls\tCLS \tscheduling class of the process") +- self.pmSetLongOptionText("\tcmd\tCMD\tsee args. (alias args, command).") +- self.pmSetLongOptionText("\tpid\tPID \tthe process ID") +- self.pmSetLongOptionText("\tppid\tPPID\tparent process ID") +- self.pmSetLongOptionText("\tpri\tPRI \tpriority of the process") +- self.pmSetLongOptionText("\tstate\tS \tsee s") +- self.pmSetLongOptionText("\trss\tRSS \tthe non-swapped physical memory that a task has used") +- self.pmSetLongOptionText("\trtprio\tRTPRIO \trealtime priority") +- self.pmSetLongOptionText("\tpname\tPname\tProcess name") +- # self.pmSetLongOptionText("\ttime\tTIME \tcumulative CPU time") +- self.pmSetLongOptionText("\ttty\tTT \tcontrolling tty (terminal)") +- self.pmSetLongOptionText("\tuid\tUID \tsee euid") +- self.pmSetLongOptionText("\tvsize\tVSZ \tsee vsz") +- self.pmSetLongOptionText("\tuname\tUSER \tsee euser") +- self.pmSetLongOptionText("\twchan\tWCHAN \tname of the kernel function in which the process is sleeping") +- self.pmSetLongOption("", 0, 'u', "", "Display user-oriented format") ++ self.pmSetLongOptionText( ++ "\tcls\tCLS \tscheduling class of the process" ++ ) ++ self.pmSetLongOptionText( ++ "\tcmd\tCMD\tsee args. (alias args, command)." ++ ) ++ self.pmSetLongOptionText( ++ "\tpid\tPID \tthe process ID" ++ ) ++ self.pmSetLongOptionText( ++ "\tppid\tPPID\tparent process ID" ++ ) ++ self.pmSetLongOptionText( ++ "\tpri\tPRI \tpriority of the process" ++ ) ++ self.pmSetLongOptionText( ++ "\tstate\tS \tsee s" ++ ) ++ self.pmSetLongOptionText( ++ "\trss\tRSS \tthe non-swapped physical memory that a task has used" ++ ) ++ self.pmSetLongOptionText( ++ "\trtprio\tRTPRIO \trealtime priority" ++ ) ++ self.pmSetLongOptionText( ++ "\tpname\tPname\tProcess name" ++ ) ++ self.pmSetLongOptionText( ++ "\ttty\tTT \tcontrolling tty (terminal)" ++ ) ++ self.pmSetLongOptionText( ++ "\tuid\tUID \tsee euid" ++ ) ++ self.pmSetLongOptionText( ++ "\tvsize\tVSZ \tsee vsz" ++ ) ++ self.pmSetLongOptionText( ++ "\tuname\tUSER \tsee euser" ++ ) ++ self.pmSetLongOptionText( ++ "\twchan\tWCHAN \tname of the kernel function in which the process is sleeping" ++ ) ++ self.pmSetLongOption("", 0, 'u', "", ++ "Display user-oriented format" ++ ) ++ self.pmSetLongOption( ++ "sort", 1, "O", "%cpu,%mem", ++ "sort the process list by %cpu or %mem values " ++ ) ++ self.pmSetLongOption("", 0, "d", "", "enable debug mode") + self.pmSetLongOptionVersion() + self.pmSetLongOptionTimeZone() + self.pmSetLongOptionHostZone() + self.pmSetLongOptionHelp() + + def override(self, opts): +- ProcessStatOptions.print_count = self.pmGetOptionSamples() ++ self.print_count = self.pmGetOptionSamples() + # """Override standard Pcp-ps option to show all process """ +- return bool(opts in ['p', 'c', 'o', 'P', 'U']) ++ return bool(opts in ['p', 'c', 'o', 'P', 'U', 'O', 'd']) + + def extraOptions(self, opts, optarg, index): +- if opts == 'e': +- ProcessStatOptions.show_all_process = True +- elif opts == 'c': +- ProcessStatOptions.command_filter_flag = True +- ProcessStatOptions.filter_flag = True ++ ++ def handle_e(): ++ if self.debug_mode: ++ print("e option selected") ++ self.universal_flag = "all" ++ ++ def handle_c(): ++ if self.debug_mode: ++ print("command option selected") ++ self.universal_flag = "command" ++ self.command_filter_flag = True ++ self.filter_flag = True + try: + if optarg is not None: +- ProcessStatOptions.command_list += optarg.replace(',', ' ').split(' ') ++ self.command_list += optarg.replace(',', ' ').split(' ') ++ if self.debug_mode: ++ print("Command List: %s" % self.command_list) + except ValueError: + print("Invalid command Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'p': +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.pid_filter_flag = True ++ def handle_d(): ++ print("Debug mode selected") ++ self.debug_mode = True ++ ++ def handle_p(): ++ if self.debug_mode: ++ print("pid option selected") ++ self.universal_flag = "pid" ++ self.filter_flag = True ++ self.pid_filter_flag = True + try: + if optarg is not None: + dummy_list = optarg.replace(',', ' ').split(' ') +- ProcessStatOptions.pid_list += [int(x) for x in dummy_list] ++ self.pid_list += [int(x) for x in dummy_list] + except ValueError: + print("Invalid pid Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'P': +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.ppid_filter_flag = True ++ ++ def handle_P(): ++ if self.debug_mode: ++ print("ppid option selected") ++ self.universal_flag = "ppid" ++ self.filter_flag = True ++ self.ppid_filter_flag = True + try: + if optarg is not None: + dummy_list = optarg.replace(',', ' ').split(' ') +- ProcessStatOptions.ppid_list += [int(x) for x in dummy_list] ++ self.ppid_list += [int(x) for x in dummy_list] + except ValueError: + print("Invalid ppid Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'u': +- ProcessStatOptions.user_oriented_format = True +- elif opts == 'o': +- ProcessStatOptions.selective_colum_flag = True ++ ++ def handle_u(): ++ if self.debug_mode: ++ print("User-oriented format option selected") ++ self.universal_flag = "user" ++ self.user_oriented_format = True ++ ++ def handle_o(): ++ if self.debug_mode: ++ print("User-defined format option selected") ++ self.selective_colum_flag = True + try: + if optarg.upper() == "ALL": +- ProcessStatOptions.filterstate = optarg.upper() ++ self.filterstate = optarg.upper() + else: + dummy_list = optarg.replace(',', ' ').split(' ') ++ if self.debug_mode: ++ print("Custom Column List: %s" % dummy_list) + for key in dummy_list: + if key.lower() in PIDINFO_PAIR: +- ProcessStatOptions.colum_list.append(key.lower()) ++ self.colum_list.append(key.lower()) + else: + raise ValueError + except ValueError: +- print("Invalid ppid Id List: Either column name is not correct " +- "or use comma separated column names without whitespaces") ++ print("Invalid Column List: incorrect name or improper comma-separated format") + sys.exit(1) +- elif opts == 'U': +- ProcessStatOptions.username_filter_flag = True +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.filtered_process_user = optarg +- elif opts is None: +- ProcessStatOptions.show_all_process = True +- +- @staticmethod +- def checkOptions(): +- if ProcessStatOptions.selective_colum_flag or \ +- ProcessStatOptions.filter_flag or \ +- ProcessStatOptions.user_oriented_format: ++ ++ def handle_U(): ++ if self.debug_mode: ++ print("username option selected") ++ self.universal_flag = "username" ++ self.username_filter_flag = True ++ self.filter_flag = True ++ self.filtered_process_user = optarg ++ ++ def handle_none(): ++ if self.debug_mode: ++ print("No option selected, defaulting to show all process") ++ self.universal_flag = "all" ++ # self.show_all_process = True ++ ++ def handle_O(): ++ if self.debug_mode: ++ print("Sorting option selected") ++ self.sorting_flag = True ++ if optarg.lower() not in ['%cpu', '%mem']: ++ print("Invalid sorting option, defaulting to %cpu") ++ self.sorting_order = '%cpu' # default to %cpu ++ return ++ self.sorting_order = optarg.lower() ++ ++ # Dispatch map simulating switch-case ++ dispatch = { ++ 'e': handle_e, ++ 'c': handle_c, ++ 'p': handle_p, ++ 'P': handle_P, ++ 'u': handle_u, ++ 'o': handle_o, ++ 'U': handle_U, ++ 'O': handle_O, ++ 'd': handle_d, ++ None: handle_none, ++ } ++ # Execute handler or fallback to error ++ handler = dispatch.get(opts) ++ if handler: ++ handler() ++ else: ++ print("Unknown option: %s" % opts) ++ sys.exit(1) ++ def checkOptions(self): ++ if self.universal_flag is not None : + return True + else: +- ProcessStatOptions.empty_arg_flag = True ++ if self.debug_mode: ++ print("No filtering option selected, defaulting to empty argument mode") ++ self.universal_flag = "empty_arg" + return True + + +@@ -779,7 +934,7 @@ if __name__ == "__main__": + sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing) + sys.exit(1) + manager['psstat'] = PSSTAT_METRICS +- manager.printer = ProcessStatReport(manager['psstat']) ++ manager.printer = ProcessStatReport(manager['psstat'],opts) + sts = manager.run() + sys.exit(sts) + except pmapi.pmErr as pmerror: +@@ -787,6 +942,9 @@ if __name__ == "__main__": + except pmapi.pmUsageErr as usage: + usage.message() + sys.exit(1) ++ except ValueError as e: ++ sys.stderr.write("%s\n" % str(e)) ++ sys.exit(1) + except IOError: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except KeyboardInterrupt: +-- +2.43.7 + diff --git a/SOURCES/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch b/SOURCES/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch new file mode 100644 index 0000000..84dbb7c --- /dev/null +++ b/SOURCES/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch @@ -0,0 +1,486 @@ +From e02b8ba31512a3e4448330b28b734f010b157dce Mon Sep 17 00:00:00 2001 +From: Sagar Sagar +Date: Tue, 3 Mar 2026 10:11:49 +0000 +Subject: [PATCH OL9 1015/1016] pmlogger_daily -d/--disk option for archive + space limit + +Added -d/--disk option in pmlogger_daily for archive size limit +Doc: Explain $PCP_SPACELIMIT env var for disk usage in PCP archives +Clarify archive retention/purging with space limits and env/CLI opts +utilproc.sh: Refactor _convert_to_kb(), unit checks, env + +upstream ref:- 7b7c38c16c6904960ede400efa5225732935b830 + +[orabug:38757778] +Signed-off-by: sagar sagar +--- + man/man1/pmlogger_daily.1 | 83 +++++++++++++- + src/pmlogger/pmlogger_daily.sh | 193 ++++++++++++++++++++++++++++++++- + src/pmlogger/utilproc.sh | 83 ++++++++++++++ + 3 files changed, 357 insertions(+), 2 deletions(-) + +diff --git a/man/man1/pmlogger_daily.1 b/man/man1/pmlogger_daily.1 +index 347fcfe..f0f0742 100644 +--- a/man/man1/pmlogger_daily.1 ++++ b/man/man1/pmlogger_daily.1 +@@ -20,6 +20,7 @@ + .B $PCP_BINADM_DIR/pmlogger_daily + [\f3\-DEfKMNoprRVzZ?\f1] + [\f3\-c\f1 \f2control\f1] ++[\f3\-d\f1 \f2fssize\f1] + [\f3\-k\f1 \f2time\f1] + [\f3\-l\f1 \f2logfile\f1] + [\f3\-m\f1 \f2addresses\f1] +@@ -120,6 +121,72 @@ Do not perform the conditional + .BR pmlogger_daily_report (1) + processing as described below. + .TP 5 ++\fB\-d\fR \fIfssize\fR, \fB\-\-disk\fR=\fIfssize\fR ++This option enforces a maximum total file system space usage per host directory for PCP archive files stored under each ++.IR $PCP_ARCHIVE_DIR/ ++and ++.IR $PCP_REMOTE_ARCHIVE_DIR/ ++location. The ++.I fssize ++must be specified as an integer with a suffix ++.B K ++or ++.B k ++for kilobytes, ++.B M ++or ++.B m ++for megabytes, ++.B G ++or ++.B g ++for gigabytes (e.g. 500m or 10G). ++As a special case, ++.I fssize ++may be the keyword ++.B unlimited ++to prevent any file system space limit enforcement (this is the default ++behaviour). ++.RS ++.PP ++Alternatively, the file system space limit may be set by exporting the ++.B $PCP_SPACELIMIT ++environment variable, which is handled analogously to ++.BR $PCP_CULLAFTER . ++If both ++.B $PCP_SPACELIMIT ++and ++.B \-d ++are provided and specify different values then the value from ++the environment variable is used and a warning is issued. ++.PP ++After normal daily log processing (compression, merging, culling by age, etc.) ++completes, ++if the total file system space consumed by any ++.BR pmlogger (1) ++instance exceeds ++.IR fssize , ++then additional archive files will be purged (i.e. removed), ++oldest first, until ++the file system usage is reduced to be not more than ++.IR fssize . ++This purging operation never deletes archive files with today's date, ++and so always preserves the most recent archives required by active ++.BR pmlogger (1) ++processes. ++.PP ++Enforcement is only performed when ++.B pmlogger_daily ++runs (typically once a day), not continuously. ++As a result, the total file system usage for any ++.BR pmlogger (1) ++instance may exceed a specified maximum between ++.B pmlogger_daily ++executions (for example, as new archives are created ++or when archives are uncompressed for merging or ++prior to asynchronous archive compression). ++.RE ++.TP 5 + \fB\-E\fR, \fB\-\-expunge\fR + This option causes + .B pmlogger_daily +@@ -458,7 +525,7 @@ maximizes the diagnostic capabilities for debugging. + .TP 5 + \fB\-x\fR \fItime\fR, \fB\-\-compress\-after\fR=\fItime\fR + Archive data files can optionally be compressed after some period +-to conserve disk space. ++to conserve file system space. + This is particularly useful for large numbers of + .B pmlogger + processes under the control of +@@ -876,6 +943,20 @@ if this file exists, then this is treated as equivalent to using + on the command line and the file will be removed once all rewriting + has been done. + .SH PCP ENVIRONMENT ++.TP 5 ++.B $PCP_SPACELIMIT ++If set, specifies a maximum allowed total file system space (in kilobytes, or with optional K/M/G suffix as for the \-d option) for each per-host archive directory under ++.I $PCP_ARCHIVE_DIR ++or ++.I $PCP_REMOTE_ARCHIVE_DIR . ++This value is used by ++.B pmlogger_daily ++when enforcing archive retention limits for file system space, unless the ++.B \-d ++option is provided in which case the command line flag overrides the environment variable and a warning is issued. Enforcement occurs after each run of ++.B pmlogger_daily ++and only applies at that time—not continuously. ++.PP + Environment variables with the prefix \fBPCP_\fP are used to parameterize + the file and directory names used by PCP. + On each installation, the +diff --git a/src/pmlogger/pmlogger_daily.sh b/src/pmlogger/pmlogger_daily.sh +index 6e73a1f..bb22df8 100755 +--- a/src/pmlogger/pmlogger_daily.sh ++++ b/src/pmlogger/pmlogger_daily.sh +@@ -38,6 +38,7 @@ PROGLOG=$PCP_LOG_DIR/pmlogger/$prog.log + MYPROGLOG=$PROGLOG.$$ + USE_SYSLOG=true + ++localhost=`hostname || echo localhost` + # optional begin logging to $PCP_LOG_DIR/NOTICES + # + if $PCP_LOG_RC_SCRIPTS +@@ -339,6 +340,7 @@ Options: + -E,--expunge expunge metrics with metadata inconsistencies when merging archives + -f,--force force actions (intended for QA, not production) + -k=TIME,--discard=TIME remove archives after TIME (format DD[:HH[:MM]]) ++ -d=fssize,--disk=fssize set maximum disk usage for archives for each pmlogger instance + -K compress, but no other changes + -l=FILE,--logfile=FILE send important diagnostic messages to FILE + -m=ADDRs,--mail=ADDRs send daily NOTICES entries to email addresses +@@ -384,6 +386,8 @@ EXPUNGE="" + FORCE=false + KILL=pmsignal + DO_DAILY_REPORT=true ++SPACELIMIT_CMDLINE="" ++SPACELIMIT_DEFAULT="unlimited" + + ARGS=`pmgetopt --progname=$prog --config=$tmp/usage -- "$@"` + [ $? != 0 ] && exit 1 +@@ -399,6 +403,24 @@ do + ;; + -D) DO_DAILY_REPORT=false + ;; ++ -d) SPACELIMIT_CMDLINE="$2" ++ shift ++ if [ -n "$PCP_SPACELIMIT" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ] ++ then ++ echo "Warning: -d value ($SPACELIMIT_CMDLINE) ignored because \$PCP_SPACELIMIT ($PCP_SPACELIMIT) set in environment" ++ SPACELIMIT_CMDLINE="" ++ continue ++ fi ++ if [ "$SPACELIMIT_CMDLINE" != unlimited ] ++ then ++ if ! _convert_to_kb "$SPACELIMIT_CMDLINE" >/dev/null ++ then ++ echo "Error: -d value ($SPACELIMIT_CMDLINE) not valid" ++ $NOERROR || status=1 ++ exit ++ fi ++ fi ++ ;; + -E) EXPUNGE="-E" + ;; + -f) FORCE=true +@@ -763,6 +785,44 @@ _warning() + echo "Warning: $@" + } + ++# Given a list of candidate directories, calculate the total ++# size below them in Kbytes ++# ++_calculate_total_size() ++{ ++ du -sk "$@" 2>$tmp/cts_err \ ++ | awk >$tmp/cts_out ' ++BEGIN { kb = 0 } ++ { kb += $1 } ++END { print kb }' >$tmp/cts_out ++ if [ -s $tmp/cts_err ] && $VERY_VERBOSE ++ then ++ echo >&2 "Warning: _calculate_total_size: du -sk $@ produced errors ..." ++ cat >&2 $tmp/cts_err ++ fi ++ $VERY_VERBOSE && echo >&2 "Info: _calculate_total_size -> `cat $tmp/cts_out` Kbytes" ++ cat $tmp/cts_out ++} ++ ++# Calculate the total size of the archive with basename $1 ++# ++_calculate_archive_size() ++{ ++ du -sk "$1".* 2>$tmp/cts_err \ ++ | awk >$tmp/cts_out ' ++BEGIN { kb = 0 } ++ { kb += $1 } ++END { print kb }' >$tmp/cts_out ++ if [ -s $tmp/cts_err ] && $VERY_VERBOSE ++ then ++ echo >&2 "Warning: _calculate_archive_size: du -sk $1.* produced errors ..." ++ cat >&2 $tmp/cts_err ++ fi ++ $VERY_VERBOSE && echo >&2 "Info: _calculate_archive_size $1 -> `cat $tmp/cts_out` Kbytes" ++ cat $tmp/cts_out ++} ++ ++ + _skipping() + { + echo "$prog: Warning: $@" +@@ -1070,6 +1130,86 @@ BEGIN { seenslash = 0; lastc = ""; inshell = 0; nesting = 0; out = "" } + END { print out }' + } + ++# Check disk space allocation for one pmlogger instance, and if more ++# than $SPACELIMIT Kbytes, purge files to try and get the space ++# allocated to be not more than $SPACELIMIT Kbytes. ++# ++# Oldest files are purged first and the most recent archives for today ++# (including those required by active pmlogger (1) processes are ++# never deleted. ++# ++# On entry, $find_dirs is a list of one or more directories holding ++# archives for a pmlogger instance, set in _callback_log_control(), ++# and $host is the host name field from the control line, set in ++# _parse_log_control(). ++# ++_do_purge() ++{ ++ SPACELIMIT="$PCP_SPACELIMIT" ++ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_CMDLINE" ++ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_DEFAULT" ++ [ "$SPACELIMIT" = unlimited ] && return ++ if ! _convert_to_kb "$SPACELIMIT" >$tmp/tmp ++ then ++ _warning "skipping purging because of invalid space limit" ++ return ++ fi ++ SPACELIMIT=`cat $tmp/tmp` ++ $VERY_VERBOSE && echo >&2 "SPACELIMIT=$SPACELIMIT" ++ __total_size=`_calculate_total_size $find_dirs` ++ if [ "$__total_size" -le "$SPACELIMIT" ] ++ then ++ $VERBOSE && echo "Info: No purging required for $host, archives ($__total_size Kbytes) <= size limit ($SPACELIMIT Kbytes)" ++ return ++ fi ++ $VERBOSE && echo "Info: archives for $host ($__total_size Kbytes) >= size limit ($SPACELIMIT Kbytes)" ++ ++ # algorithm to find archive basenames borrowed from _do_merge() ++ # output is in this format ... ++ # | ++ # ++ TODAY=`date +%Y%m%d` ++ find $find_dirs -maxdepth 1 -type f \ ++ | sed -n \ ++ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \ ++ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \ ++ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \ ++ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \ ++ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \ ++ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \ ++ | sort -t'|' -n -k2,2 \ ++ | $PCP_AWK_PROG -F'|' ' ++$2 == "'$TODAY'" { next } ++$2 ~ /^'$TODAY'/ { next } ++ { print }' >$tmp/purge_candidates ++ if [ ! -s $tmp/purge_candidates ] ++ then ++ $VERBOSE && echo "Info: No candidates to purge." ++ return ++ fi ++ ++ sed -e 's/|/ /' <$tmp/purge_candidates \ ++ | while read __dir __file ++ do ++ [ "$__dir" != "." ] && __file="$__dir/$__file" ++ __arch_size=`_calculate_archive_size "$__file"` ++ if $SHOWME ++ then ++ echo "+ rm `echo $__file.**`" ++ __total_size=`expr $__total_size - $__arch_size` ++ else ++ if rm "$__file".* ++ then ++ $VERBOSE && echo "Info: purge $__file, reclaims $__arch_size Kbytes" ++ __total_size=`expr $__total_size - $__arch_size` ++ else ++ $VERBOSE && echo "Warning: rm $__file.* failed" ++ fi ++ fi ++ [ "$__total_size" -le "$SPACELIMIT" ] && break ++ done ++} ++ + # replace `....` in $orig_dir by * ($orig_dir is on stdin) + # + _unbackquote() +@@ -1277,7 +1417,50 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p + $VERBOSE && echo "Using \$PCP_AUTOSAVE_DIR: $PCP_AUTOSAVE_DIR" + fi + ;; +- ++ 'export PCP_SPACELIMIT;'*) ++ _old_value="$PCP_SPACELIMIT" ++ _check=`echo "$_cmd" | sed -e 's/.*=//' -e 's/ *$//'` ++ if [ -n "$_check" ] ++ then ++ if [ "$_check" = unlimited ] ++ then ++ # no conversion ++ : ++ else ++ # check syntax & convert to canonical Kbytes ++ # ++ _kb=`_convert_to_kb "$_check"` ++ if [ $? != 0 ] ++ then ++ _warning "\$PCP_SPACELIMIT value ($_check) is invalid. Must be a positive integer and a unit (e.g. 100M)" ++ _cmd='' ++ else ++ $SHOWME && echo "+ $_cmd (normalized to $_kb Kbytes)" ++ # need to put back the "K" units here ++ # # because it will get re-processed by ++ # _convert_to_kb() later ++ # ++ _cmd=`echo "$_cmd" | sed -e "s/=.*/=${_kb}K/"` ++ fi ++ fi ++ if [ -n "$_cmd" ] ++ then ++ echo eval $_cmd >>$tmp/_cmd ++ eval $_cmd ++ if [ -n "$_old_value" -a "$_old_value" != "$PCP_SPACELIMIT" ] ++ then ++ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, previous value ($_old_value) ignored" ++ fi ++ if [ -n "$PCP_SPACELIMIT" -a -n "$SPACELIMIT_CMDLINE" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ] ++ then ++ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, -d value ($SPACELIMIT_CMDLINE) ignored" ++ SPACELIMIT_CMDLINE="" ++ fi ++ fi ++ else ++ _warning "\$PCP_SPACELIMIT from control file missing a value, will be ignored" ++ fi ++ ;; + *) + $SHOWME && echo "+ $cmd" + echo eval $cmd >>$tmp/cmd +@@ -2164,6 +2347,14 @@ p + fi + fi + ++ # if space limit specified, potentially purge old archives if ++ # space limit exceeded ++ # ++ if [ -n "$PCP_SPACELIMIT" -o -n "$SPACELIMIT_CMDLINE" ] ++ then ++ _do_purge ++ fi ++ + _unlock "$dir" + done + } +diff --git a/src/pmlogger/utilproc.sh b/src/pmlogger/utilproc.sh +index a0b62c9..5a939f9 100644 +--- a/src/pmlogger/utilproc.sh ++++ b/src/pmlogger/utilproc.sh +@@ -255,6 +255,89 @@ END { exit sts }' + fi + } + ++# Converts a size string like 10G, 100M, 100K to integer KBytes for limit ++# checks. ++# Usage: _convert_to_kb size_string ++# Outputs: integer KBytes on stdout, returns 0 if OK, 1 if error ++_convert_to_kb() ++{ ++ __input="$1" ++ __num= ++ __unit= ++ __kb= ++ if [ -z "$__input" ] ++ then ++ echo "Error: _convert_to_kb(): missing argument" >&2 ++ return 1 ++ fi ++ __num=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\1/'` ++ case "$__num" ++ in ++ "" ) ++ echo "Error: _convert_to_kb(): argument '$__input' does not start with a number" >&2 ++ return 1 ++ ;; ++ *[!0-9]* ) ++ echo "Error: _convert_to_kb(): argument '$__input' has a non-numeric value" >&2 ++ return 1 ++ ;; ++ 0 ) ++ echo "Error: _convert_to_kb(): argument '$__input' resolves to zero (not allowed)" >&2 ++ return 1 ++ ;; ++ esac ++ __unit=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\2/'` ++ case "$__unit" ++ in ++ G|g|M|m|K|k) ++ ;; ++ '') ++ echo "Error: _convert_to_kb(): missing unit after '$__num'" >&2 ++ return 1 ++ ;; ++ *) ++ echo "Error: _convert_to_kb(): invalid unit '$__unit'" >&2 ++ return 1 ++ ;; ++ esac ++ ++ MAX_INT32=2147483647 ++ ++ case "$__unit" ++ in ++ G|g) ++ # Check before multiplying ++ __max=`expr $MAX_INT32 / \( 1024 \* 1024 \)` ++ if [ $__num -gt $__max ] ++ then ++ echo "Error: overflow, $__num Gbytes too large for Kbytes in a 32-bit signed int" >&2 ++ return 1 ++ fi ++ __kb=`expr $__num \* 1024 \* 1024` ++ ;; ++ M|m) ++ # Check before multiplying ++ __max=`expr $MAX_INT32 / 1024` ++ if [ $__num -gt $__max ] ++ then ++ echo "Error: overflow, $__num Mbytes too large for Kbytes in a 32-bit signed int" >&2 ++ return 1 ++ fi ++ __kb=`expr $__num \* 1024` ++ ;; ++ K|k) ++ if [ $__num -gt $MAX_INT32 ] ++ then ++ echo "Error: overflow, $__num Kbytes too large for 32-bit signed int" >&2 ++ return 1 ++ fi ++ __kb=$__num ++ ;; ++ esac ++ echo "$__kb" ++ return 0 ++} ++ + # current time to the highest precision available from date(1) and + # strftime(3) + # +-- +2.43.7 + diff --git a/SOURCES/1016-pcp-system-tools-restore-backward-compatibility-with.patch b/SOURCES/1016-pcp-system-tools-restore-backward-compatibility-with.patch new file mode 100644 index 0000000..b01541b --- /dev/null +++ b/SOURCES/1016-pcp-system-tools-restore-backward-compatibility-with.patch @@ -0,0 +1,328 @@ +From cc127dfca5ab71fcbd34ed27be8844e3aa2be0c3 Mon Sep 17 00:00:00 2001 +From: Sagar Sagar +Date: Tue, 3 Mar 2026 10:13:09 +0000 +Subject: [PATCH OL9 1016/1016] pcp-system-tools: restore backward + compatibility with older pmapi versions + +Commit 0a37ed0 introduced support for pmapi version 4, but upgrading to the latest pmapi is not currently desired. +This change updates pcp-iostat, mpstat, pidstat, and ps to fall back to tv_usec when tv_nsec is unavailable +during timestamp delta calculation, ensuring compatibility with older pmapi versions and collectors that only expose tv_usec + +- optimized code in pcp-ps for dynamic sorting +- fixed sorting colum indexes in -u case + +[Orabug: 38719615] +Signed-off-by: Sagar Sagar +--- + src/pcp/iostat/pcp-iostat.py | 15 +++- + src/pcp/mpstat/pcp-mpstat.py | 13 +++- + src/pcp/pidstat/pcp-pidstat.py | 13 +++- + src/pcp/ps/pcp-ps.py | 123 ++++++++++++------------------- + src/pcp/tapestat/pcp-tapestat.py | 15 +++- + 5 files changed, 95 insertions(+), 84 deletions(-) + +diff --git a/src/pcp/iostat/pcp-iostat.py b/src/pcp/iostat/pcp-iostat.py +index ae503ed..11a7245 100755 +--- a/src/pcp/iostat/pcp-iostat.py ++++ b/src/pcp/iostat/pcp-iostat.py +@@ -56,9 +56,18 @@ class IostatReport(pmcc.MetricGroupPrinter): + Hcount = 0 + def timeStampDelta(self, group): + s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec +- u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec +- # u may be negative here, calculation is still correct. +- return s + u / 1000000.0 ++ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds) ++ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds ++ # when available, but gracefully fall back to microseconds to avoid ++ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'): ++ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec ++ # n may be negative here, calculation is still correct. ++ return s + n / 1000000000.0 ++ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'): ++ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec ++ return s + u / 1000000.0 ++ # it should not reach here ++ return s + + def instlist(self, group, name): + return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys() +diff --git a/src/pcp/mpstat/pcp-mpstat.py b/src/pcp/mpstat/pcp-mpstat.py +index f398e95..fb6efb9 100755 +--- a/src/pcp/mpstat/pcp-mpstat.py ++++ b/src/pcp/mpstat/pcp-mpstat.py +@@ -503,8 +503,17 @@ class MpstatReport(pmcc.MetricGroupPrinter): + + def timeStampDelta(self, group): + s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec +- u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec +- return s + u / 1000000.0 ++ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds) ++ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds ++ # when available, but gracefully fall back to microseconds to avoid ++ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'): ++ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec ++ return s + n / 1000000000.0 ++ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'): ++ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec ++ return s + u / 1000000.0 ++ # it should not reach here ++ return s + + def print_machine_info(self,group, context): + self.get_summary_metrics(group) +diff --git a/src/pcp/pidstat/pcp-pidstat.py b/src/pcp/pidstat/pcp-pidstat.py +index 285aa51..b7613e1 100755 +--- a/src/pcp/pidstat/pcp-pidstat.py ++++ b/src/pcp/pidstat/pcp-pidstat.py +@@ -925,8 +925,17 @@ class PidstatReport(pmcc.MetricGroupPrinter): + + def timeStampDelta(self, group): + s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec +- u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec +- return s + u / 1000000.0 ++ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds) ++ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds ++ # when available, but gracefully fall back to microseconds to avoid ++ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'): ++ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec ++ return s + n / 1000000000.0 ++ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'): ++ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec ++ return s + u / 1000000.0 ++ # it should not reach here ++ return s + + def print_machine_info(self,group, context): + timestamp = context.pmLocaltime(group.timestamp.tv_sec) +diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py +index c8f2e9d..35f34b5 100755 +--- a/src/pcp/ps/pcp-ps.py ++++ b/src/pcp/ps/pcp-ps.py +@@ -249,7 +249,7 @@ class ProcessStatusUtil: + if self.user_percent() is not None and self.guest_percent() is not None and self.system_percent() is not None: + return float("%.2f" % (self.user_percent() + self.guest_percent() + self.system_percent())) + else: +- return None ++ return 0.0 + + def stime(self): + c_systime = self.__get_value('proc.psinfo.stime', self.instance) +@@ -359,6 +359,20 @@ class DynamicProcessReporter: + self.printer = printer + self.processStatOptions = processStatOptions + ++ def __sort_by_idx(self, output_list, sorting_idx, reverse=True): ++ # Rows are tab-delimited; splitting on whitespace breaks when ++ # command/args contain spaces and shifts sortable column indexes. ++ return sorted( ++ output_list, ++ key=lambda row: ( ++ float(row.split('\t')[sorting_idx].strip()) ++ if sorting_idx < len(row.split('\t')) ++ and row.split('\t')[sorting_idx].strip().replace('.', '', 1).replace('-', '', 1).isdigit() ++ else float('-inf') ++ ), ++ reverse=reverse ++ ) ++ + def _is_last_and_args(self, key): + return (key == "args") and \ + self.processStatOptions.colum_list.index(key) == len(self.processStatOptions.colum_list) - 1 +@@ -371,19 +385,15 @@ class DynamicProcessReporter: + # Sorting validations + sorting_idx = None + if self.processStatOptions.sorting_flag: +- if self.processStatOptions.filterstate == "ALL": +- if self.processStatOptions.sorting_order == '%mem': +- sorting_idx = 7 +- elif self.processStatOptions.sorting_order == '%cpu': +- sorting_idx = 8 +- else: +- sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list) +- if key == self.processStatOptions.sorting_order), +- None) +- if sorting_idx is None: +- raise ValueError("Sorting order not found in output columns") +- # Adjust for timestamp column +- sorting_idx += 1 ++ # For dynamic output, sorting key must be present in selected columns. ++ if self.processStatOptions.sorting_order not in self.processStatOptions.colum_list: ++ raise ValueError("Sorting order not found in output columns") ++ ++ # Find sorting column index and adjust for Timestamp at position 0. ++ sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list) ++ if key == self.processStatOptions.sorting_order), None) ++ # to account for Timestamp colum ++ sorting_idx += 1 + + # Always compute process list ONCE + processes = self.process_filter.filter_processes( +@@ -393,47 +403,8 @@ class DynamicProcessReporter: + output_list = [] + header = None + +- # -------- PATH 1: With filterstate -------- # +- if self.processStatOptions.filterstate == "ALL": +- header = ( +- "Timestamp\tUSER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ" +- "\tRSS\tS\tSTARTED\t\tTIME\t\t" +- "WCHAN\t\t\t\tCommand" +- ) +- +- # Precompute format string +- # fmt = ( +- # "{ts}{indent}{user}\t{pid}\t{ppid}\t{pri}\t{cpu}\t{mem}\t" +- # "{vsz}\t{rss}\t{s}\t{started}\t{time}\t{wchan}\t{cmd}" +- # ) +- for process in processes: +- # Maintain state info +- key = (process.s_name(), process.pid()) +- process_state_info[key] = process_state_info.get(key, 0) + self.delta_time +- row = [timestamp] +- row.extend([ +- process.user_name(), +- process.pid(), +- process.ppid(), +- process.priority(), +- process.total_percent(), +- process.system_percent(), +- process.vsize(), +- process.rss(), +- process.s_name(), +- process.start(), +- process.total_time(), +- process.wchan_s(), +- process.process_name_with_args_last()[:45] +- ]) +- output_list.append( +- "\t".join( +- str(x) if x is not None else '' for x in row +- ) +- ) +- +- # -------- PATH 2: Customized column list -------- # +- elif self.processStatOptions.colum_list is not None: ++ # -------- Dynamic column list path -------- # ++ if self.processStatOptions.colum_list is not None: + + header = "Timestamp\t" + for key in self.processStatOptions.colum_list: +@@ -450,23 +421,14 @@ class DynamicProcessReporter: + # print(row) + output_list.append("\t".join(str(x) if x is not None else '' for x in row)) + +- # -------- PATH 3: Invalid filterstate or column list -------- # ++ # -------- Invalid column list -------- # + # This should never happen, but just in case + else: + raise ValueError("No valid filterstate or column list provided") + + # Sorting logic + if self.processStatOptions.sorting_flag and sorting_idx is not None: +- output_list.sort( +- key=lambda x: ( +- sorting_idx, +- float('inf') if ( +- len(x.split()) <= sorting_idx or +- not x.split()[sorting_idx].replace('.', '', 1).isdigit() +- ) else float(x.split()[sorting_idx]) +- ), +- reverse=True +- ) ++ output_list = self.__sort_by_idx(output_list, sorting_idx, reverse=True) + + # --------- Print output --------- # + self.printer(header) +@@ -531,25 +493,25 @@ class ProcessStatusReporter: + for process in processes: + output_rows.append("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( + safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()), +- safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.total_percent()), ++ safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.mem()), + safe_str(process.vsize()), safe_str(process.rss()), + safe_str(process.tty_name()), safe_str(process.ppid()), safe_str(process.total_time()), + safe_str(process.start()), + safe_str(process.process_name()))) +- cpu_idx = 5 +- mem_idx = 6 ++ cpu_idx = 3 ++ mem_idx = 4 + elif selected_flag == "user": + for process in processes: + output_rows.append("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( + safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()), + safe_str(process.pid()), +- safe_str(process.system_percent()), safe_str(process.total_percent()), ++ safe_str(process.system_percent()), safe_str(process.mem()), + safe_str(process.vsize()), safe_str(process.rss()), + safe_str(process.tty_name()), safe_str(process.s_name()), + safe_str(process.total_time()), safe_str(process.start()), + safe_str(process.process_name()))) +- cpu_idx = 5 +- mem_idx = 6 ++ cpu_idx = 3 ++ mem_idx = 4 + elif selected_flag == "command": + for process in processes: + output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( +@@ -585,8 +547,17 @@ class ProcessStatReport(pmcc.MetricGroupPrinter): + + def timeStampDelta(self): + s = self.group.timestamp.tv_sec - self.group.prevTimestamp.tv_sec +- n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec +- return s + n / 1000000000.0 ++ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds) ++ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds ++ # when available, but gracefully fall back to microseconds to avoid ++ if hasattr(self.group.timestamp, 'tv_nsec') and hasattr(self.group.prevTimestamp, 'tv_nsec'): ++ n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec ++ return s + n / 1000000000.0 ++ elif hasattr(self.group.timestamp, 'tv_usec') and hasattr(self.group.prevTimestamp, 'tv_usec'): ++ u = self.group.timestamp.tv_usec - self.group.prevTimestamp.tv_usec ++ return s + u / 1000000.0 ++ # it should not reach here ++ return s + + def print_machine_info(self,context): + timestamp = context.pmLocaltime(self.group.timestamp.tv_sec) +@@ -855,6 +826,10 @@ class ProcessStatOptions(pmapi.pmOptions): + try: + if optarg.upper() == "ALL": + self.filterstate = optarg.upper() ++ self.colum_list = [ ++ "uname", "pid", "ppid", "pri", "%cpu", "%mem", ++ "vsize", "rss", "state", "start", "time", "wchan", "args" ++ ] + else: + dummy_list = optarg.replace(',', ' ').split(' ') + if self.debug_mode: +diff --git a/src/pcp/tapestat/pcp-tapestat.py b/src/pcp/tapestat/pcp-tapestat.py +index 38dc3ee..e8c93dc 100755 +--- a/src/pcp/tapestat/pcp-tapestat.py ++++ b/src/pcp/tapestat/pcp-tapestat.py +@@ -68,9 +68,18 @@ class TapestatReport(pmcc.MetricGroupPrinter): + Hcount = 0 + def timeStampDelta(self, group): + s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec +- u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec +- # u may be negative here, calculation is still correct. +- return s + u / 1000000.0 ++ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds) ++ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds ++ # when available, but gracefully fall back to microseconds to avoid ++ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'): ++ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec ++ # n may be negative here, calculation is still correct. ++ return s + n / 1000000000.0 ++ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'): ++ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec ++ return s + u / 1000000.0 ++ # it should not reach here ++ return s + def instlist(self, group, name): + return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys() + +-- +2.43.7 + diff --git a/SOURCES/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch b/SOURCES/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch new file mode 100644 index 0000000..eb311f2 --- /dev/null +++ b/SOURCES/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch @@ -0,0 +1,364 @@ +From c79976edb317272dcc8ae9352778d5d335763f10 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Wed, 25 Feb 2026 12:45:43 +0000 +Subject: [PATCH] Add interval and count support for nfsiostat tool + +Signed-off-by: Sourav Sharma + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/02d9b5a7b3c2ea16de2665c1f8cd6b81843fee15 + +Orabug: 39068870 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/nfsiostat/pcp-nfsiostat.py | 269 ++++++++++++++++------------- + 1 file changed, 145 insertions(+), 124 deletions(-) + +diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py +index 0b0376d..e19fa41 100644 +--- a/src/pcp/nfsiostat/pcp-nfsiostat.py ++++ b/src/pcp/nfsiostat/pcp-nfsiostat.py +@@ -22,7 +22,7 @@ import signal + import sys + import time + from pcp import pmapi, pmcc +-from cpmapi import PM_CONTEXT_ARCHIVE ++from cpmapi import PM_CONTEXT_ARCHIVE, PM_MODE_FORW + + SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release", + "kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"] +@@ -40,98 +40,54 @@ def adjust_length(name): + class ReportingMetricRepository: + + def __init__(self,group): +- self.group=group +- self.current_cached_values = {} +- +- def __sorted(self,data): +- return dict(sorted(data.items(), key=lambda item: item[0].lower())) +- +- def __fetch_current_value(self,metric): +- val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues)) +- val=self.__sorted(val) +- return dict(val) +- +- def current_value(self,metric): +- if not metric in self.group: +- return None +- if self.current_cached_values.get(metric) is None: +- first_value=self.__fetch_current_value(metric) +- self.current_cached_values[metric]=first_value +- return self.current_cached_values[metric] ++ self.group = group ++ self._current_cache = {} ++ self._previous_cache = {} ++ ++ def _fetch_values(self, metric, use_previous=False): ++ """Fetch values - always returns a dictionary.""" ++ if metric not in self.group: ++ return {} ++ attr = "netPrevValues" if use_previous else "netValues" ++ values = getattr(self.group[metric], attr, []) ++ return {x[0].inst: x[2] for x in values} if values else {} ++ ++ def _get_values_dict(self, metric, use_previous=False): ++ """Get cached dictionary of all values for a metric.""" ++ cache = self._previous_cache if use_previous else self._current_cache ++ if metric not in cache: ++ cache[metric] = self._fetch_values(metric, use_previous) ++ return cache[metric] ++ ++ def previous_value(self, metric, instance=None): ++ """Get previous value. Returns single value if instance given, else returns dict.""" ++ values_dict = self._get_values_dict(metric, use_previous=True) ++ if instance is not None: ++ return values_dict.get(instance) ++ return values_dict ++ ++ def current_value(self, metric, instance=None): ++ """Get current value. Returns single value if instance given, else returns dict.""" ++ values_dict = self._get_values_dict(metric, use_previous=False) ++ if instance is not None: ++ return values_dict.get(instance) ++ return values_dict ++ ++ def previous_value(self, metric, instance=None): ++ """Get previous value. Returns single value if instance given, else returns dict.""" ++ values_dict = self._get_values_dict(metric, use_previous=True) ++ if instance is not None: ++ return values_dict.get(instance) ++ return values_dict + + class NfsioStatUtil: + def __init__(self,metrics_repository): + self.__metric_repository=metrics_repository + self.report=ReportingMetricRepository(self.__metric_repository) + +- def mount_point(self): +- return self.report.current_value('nfsclient.mountpoint') +- +- def mount_share(self): +- return self.report.current_value('nfsclient.export') +- +- def mount_share_keys(self): +- data = self.report.current_value('nfsclient.export') +- return data.keys() +- +- def sample_time(self): +- return self.report.current_value('nfsclient.age') +- +- def xprt_sends(self): +- return self.report.current_value('nfsclient.xprt.sends') +- +- def xprt_backlog(self): +- return self.report.current_value('nfsclient.xprt.backlog_u') +- +- def readops(self): +- return self.report.current_value('nfsclient.ops.read.ops') +- +- def readerrors(self): +- return self.report.current_value('nfsclient.ops.read.errors') +- +- def readexecute(self): +- return self.report.current_value('nfsclient.ops.read.execute') +- +- def readrtt(self): +- return self.report.current_value('nfsclient.ops.read.rtt') +- +- def readqueue(self): +- return self.report.current_value('nfsclient.ops.read.queue') +- +- def readbytesrecv(self): +- return self.report.current_value('nfsclient.ops.read.bytes_recv') +- +- def readbytessent(self): +- return self.report.current_value('nfsclient.ops.read.bytes_sent') +- +- def readntrans(self): +- return self.report.current_value('nfsclient.ops.read.ntrans') +- +- def writeops(self): +- return self.report.current_value('nfsclient.ops.write.ops') +- +- def writeerrors(self): +- return self.report.current_value('nfsclient.ops.write.errors') +- +- def writeexecute(self): +- return self.report.current_value('nfsclient.ops.write.execute') +- +- def writertt(self): +- return self.report.current_value('nfsclient.ops.write.rtt') +- +- def writequeue(self): +- return self.report.current_value('nfsclient.ops.write.queue') +- +- def writebytesrecv(self): +- return self.report.current_value('nfsclient.ops.write.bytes_recv') +- +- def writebytessent(self): +- return self.report.current_value('nfsclient.ops.write.bytes_sent') +- +- def writentrans(self): +- return self.report.current_value('nfsclient.ops.write.ntrans') +- + class NfsiostatReport(pmcc.MetricGroupPrinter): ++ machine_info_count = 0 ++ + def __init__(self,opts,group): + self.opts = opts + self.group = group +@@ -156,34 +112,67 @@ class NfsiostatReport(pmcc.MetricGroupPrinter): + header_string += context['kernel.uname.machine'].netValues[0][2] + ' ' + print("%s (%s CPU)" % (header_string, self.__get_ncpu(context))) + +- def __print_values(self,timestamp, nfsstatus): +- n_shares = nfsstatus.mount_share_keys() +- mountshare = nfsstatus.mount_share() +- mountpoint = nfsstatus.mount_point() +- sampletime = nfsstatus.sample_time() +- sends = nfsstatus.xprt_sends() +- backlog = nfsstatus.xprt_backlog() +- readops = nfsstatus.readops() +- readerrors = nfsstatus.readerrors() +- readexecute = nfsstatus.readexecute() +- readrtt = nfsstatus.readrtt() +- readqueue = nfsstatus.readqueue() +- readbytesrecv = nfsstatus.readbytesrecv() +- readbytessent = nfsstatus.readbytessent() +- readntrans = nfsstatus.readntrans() +- writeops = nfsstatus.writeops() +- writeerrors = nfsstatus.writeerrors() +- writeexecute = nfsstatus.writeexecute() +- writertt = nfsstatus.writertt() +- writequeue = nfsstatus.writequeue() +- writebytesrecv = nfsstatus.writebytesrecv() +- writebytessent = nfsstatus.writebytessent() +- writentrans = nfsstatus.writentrans() ++ # -------------------------------------------------------- ++ ++ def __collect(self, nfs): ++ return { ++ metric: nfs.report.current_value(metric) ++ for metric in NFSIOSTAT_METRICS ++ } ++ ++ # -------------------------------------------------------- ++ ++ def __delta(self, new: dict, nfs): ++ delta = {} ++ old = { ++ metric: nfs.report.previous_value(metric) ++ for metric in NFSIOSTAT_METRICS ++ } ++ ++ for metric in new: ++ delta[metric] = {} ++ ++ for inst in new[metric]: ++ new_val = new[metric][inst] ++ old_val = old.get(metric, {}).get(inst, 0) ++ ++ # If value is numeric → subtract ++ if isinstance(new_val, (int, float)): ++ delta[metric][inst] = new_val - old_val ++ else: ++ # If string → just copy (no subtraction) ++ delta[metric][inst] = new_val ++ ++ return delta ++ ++ def __print_values(self,timestamp, delta): ++ ++ sampletime = delta["nfsclient.age"] ++ readops = delta["nfsclient.ops.read.ops"] ++ writeops = delta["nfsclient.ops.write.ops"] ++ readbytesrecv = delta["nfsclient.ops.read.bytes_recv"] ++ writebytesrecv = delta["nfsclient.ops.write.bytes_recv"] ++ mountpoint = delta["nfsclient.mountpoint"] ++ mountshare = delta["nfsclient.export"] ++ sends = delta["nfsclient.xprt.sends"] ++ backlog = delta["nfsclient.xprt.backlog_u"] ++ readerrors = delta["nfsclient.ops.read.errors"] ++ readexecute = delta["nfsclient.ops.read.execute"] ++ readrtt = delta["nfsclient.ops.read.rtt"] ++ readqueue = delta["nfsclient.ops.read.queue"] ++ readbytessent = delta["nfsclient.ops.read.bytes_sent"] ++ readntrans = delta["nfsclient.ops.read.ntrans"] ++ writeerrors = delta["nfsclient.ops.write.errors"] ++ writeexecute = delta["nfsclient.ops.write.execute"] ++ writertt = delta["nfsclient.ops.write.rtt"] ++ writequeue = delta["nfsclient.ops.write.queue"] ++ writebytessent = delta["nfsclient.ops.write.bytes_sent"] ++ writentrans = delta["nfsclient.ops.write.ntrans"] + + print("%-18s:%s"%("Timestamp", timestamp)) + print() + +- for name in n_shares: ++ for name in mountshare: + # read + r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024 + if sampletime[name] > 0: +@@ -277,13 +266,24 @@ class NfsiostatReport(pmcc.MetricGroupPrinter): + ) + print() + +- def print_report(self,group,timestamp, manager_nfsiostat): ++ def get_timestamp(self, group): ++ t_s = group.contextCache.pmLocaltime(int(group.timestamp)) ++ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time()) ++ return timestamp ++ ++ def print_report(self,group, manager_nfsiostat, mgr): + def __print_nfs_status(): +- nfsstatus = NfsioStatUtil(manager_nfsiostat) +- if nfsstatus.mount_share(): ++ timestamp = self.get_timestamp(group) ++ nfs = NfsioStatUtil(manager_nfsiostat) ++ if nfs.report.current_value("nfsclient.export"): + try: +- self.__print_machine_info(group) +- self.__print_values(timestamp, nfsstatus) ++ if self.machine_info_count == 0: ++ self.__print_machine_info(group) ++ self.machine_info_count = 1 ++ current = self.__collect(nfs) ++ diff_dict = self.__delta(current, nfs) ++ self. __print_values(timestamp, diff_dict) ++ + except IndexError: + print("Incorrect machine info due to some missing metrics") + return +@@ -292,7 +292,6 @@ class NfsiostatReport(pmcc.MetricGroupPrinter): + + if self.context != PM_CONTEXT_ARCHIVE and self.samples is None: + __print_nfs_status() +- sys.exit(0) + elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None: + __print_nfs_status() + elif self.samples >=1: +@@ -302,30 +301,52 @@ class NfsiostatReport(pmcc.MetricGroupPrinter): + pass + + def report(self, manager): +- group = manager["sysinfo"] + self.samples = self.opts.pmGetOptionSamples() +- t_s = group.contextCache.pmLocaltime(int(group.timestamp)) +- timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time()) +- self.print_report(group,timestamp,manager['nfsiostat']) ++ self.print_report(manager["sysinfo"] ,manager['nfsiostat'], manager) + + class NfsiostatOptions(pmapi.pmOptions): + timefmt = "%m/%d/%Y %H:%M:%S" ++ uflag = False ++ def checkOptions(self, manager): ++ if NfsiostatOptions.uflag: ++ if manager._options.pmGetOptionInterval(): ++ print("Error: -t incompatible with -u") ++ return False ++ if manager.type != PM_CONTEXT_ARCHIVE: ++ print("Error: -u can only be specified with -a archive") ++ return False ++ return True ++ ++ def extraOptions(self, opt, optarg, index): ++ if opt == "u": ++ NfsiostatOptions.uflag = True ++ + def __init__(self): +- pmapi.pmOptions.__init__(self, "a:s:Z:zV?") ++ pmapi.pmOptions.__init__(self, "a:s:Z:t:uzV?") ++ self.pmSetOptionCallback(self.extraOptions) + self.pmSetLongOptionHeader("General options") + self.pmSetLongOptionHostZone() + self.pmSetLongOptionTimeZone() +- self.pmSetLongOptionHelp() ++ self.pmSetLongOptionArchive() + self.pmSetLongOptionSamples() ++ self.pmSetLongOptionInterval() ++ self.pmSetLongOption("no-interpolation", 0, "u", "", "disable interpolation mode with archives") ++ self.pmSetLongOptionHelp() + self.pmSetLongOptionVersion() +- self.samples=None +- self.context=None ++ self.context = None ++ self.samples = None + + if __name__ == '__main__': + try: + opts = NfsiostatOptions() + mngr = pmcc.MetricGroupManager.builder(opts,sys.argv) + opts.context=mngr.type ++ if not opts.checkOptions(mngr): ++ raise pmapi.pmUsageErr ++ ++ if NfsiostatOptions.uflag: ++ # -u turns off interpolation ++ mngr.pmSetMode(PM_MODE_FORW, mngr._options.pmGetOptionOrigin(), None) + missing = mngr.checkMissingMetrics(ALL_METRICS) + if missing is not None: + sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing) +-- +2.43.7 + diff --git a/SOURCES/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch b/SOURCES/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch new file mode 100644 index 0000000..1dd5602 --- /dev/null +++ b/SOURCES/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch @@ -0,0 +1,1286 @@ +From 7fde321e47278138f8daee10bb0c30a708471df3 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Wed, 25 Mar 2026 05:50:39 +0000 +Subject: [PATCH] Introduces numa_maps metrics inside linux_proc pmda + +Implemented option -p in pcp numastat tool same as numactl numastat tool + + - Add new per-process NUMA maps metrics derived from /proc//numa_maps: proc.numa_maps.huge, proc.numa_maps.heap, proc.numa_maps.stack, + proc.numa_maps.private (per-node usage in MB, exported as nodeN: pairs). + - Extend pcp numastat/pcp-numastat with -p/--process to display per-process NUMA memory usage in a numastat -p-style layout. + - Convert N= counts to MB using system page size (and Hugepagesize where applicable, with fallback). + + How to Test + + - pcp numastat -p (detail view) and pcp numastat -p (summary view) + +Signed-off-by: Sourav Sharma + +Cherry-pick-commit: +https://github.com/sourav-sharma796/pcp/commit/0ea70c7fda4021cb9e35a0d4e26a30dd97409ddb +https://github.com/sourav-sharma796/pcp/commit/95fe35c176c9394ddcef0af5f4d50606308c77d1 +https://github.com/sourav-sharma796/pcp/commit/757c2a1e1c4b51ee3e304f6c3f3038e6030432f3 + +Orabug: 39096683 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/numastat/pcp-numastat.1 | 23 +- + src/pcp/numastat/pcp-numastat.py | 573 ++++++++++++++++++++++++++++++- + src/pmdas/linux_proc/clusters.h | 3 +- + src/pmdas/linux_proc/help | 13 + + src/pmdas/linux_proc/indom.h | 1 + + src/pmdas/linux_proc/pmda.c | 74 ++++ + src/pmdas/linux_proc/pmdaproc.1 | 4 + + src/pmdas/linux_proc/proc_pid.c | 269 +++++++++++++++ + src/pmdas/linux_proc/proc_pid.h | 17 + + src/pmdas/linux_proc/root_proc | 8 + + 10 files changed, 972 insertions(+), 13 deletions(-) + +diff --git a/src/pcp/numastat/pcp-numastat.1 b/src/pcp/numastat/pcp-numastat.1 +index 65910a4..e6a9ada 100644 +--- a/src/pcp/numastat/pcp-numastat.1 ++++ b/src/pcp/numastat/pcp-numastat.1 +@@ -18,7 +18,7 @@ + \f3pcp-numastat\f1 \- report on NUMA memory allocation + .SH SYNOPSIS + \f3pcp\f1 [\f2pcp\ options\f1] \f3numastat\f1 +-[\fB-V\fR] [\fB-w\fR \fIwidth\fR] [\fB-m\fR][-n\fR] [\fB-?\fR] ++[\fB-V\fR] [\fB-w\fR \fIwidth\fR] [\fB-m\fR][-n\fR] [\fB\-p\fR [\fIprocess\fR ...]] [\fB-?\fR] + .SH DESCRIPTION + .B pcp-numastat + displays NUMA allocation statistics from the kernel memory +@@ -75,13 +75,32 @@ Display meminfo-like system-wide memory usage. + \fB-n\fR, \fB--numastat\fR + Display the numastat statistics info. + .TP ++\fB\-p\fR, \fB\-\-process\fR ++Display per-process NUMA memory usage from \fBproc.numa_maps\fR metrics. ++Any trailing \fIprocess\fR operands are treated as process IDs (when the ++operand is all digits) or regular expressions for command name matching. ++Matching for command names is performed against the full command line ++(as reported by \fBproc.psinfo.psargs\fR). ++When a single process ID operand is provided and it matches exactly one ++process, a detailed table of Hugepage, Heap, Stack and Private mappings is ++shown (as in \fBnumastat \-p\fR). When multiple process ID operands are ++provided, a per-process summary table is shown (also like ++\fBnumastat \-p\fR) and may be split into multiple node column blocks ++based on the available display width. ++.PP ++In the per-process view, node columns are always shown for all NUMA nodes ++present on the system (as reported by PCP NUMA inventory metrics), even if ++individual \fBproc.numa_maps\fR metric instances omit some nodes. Missing ++per-process metrics are reported as zero. ++.TP + \fB\-?\fR, \fB\-\-help\fR + Display usage message and exit. + .SH NOTES + .B pcp-numastat + is inspired by the + .BR numastat (1) +-command and produces exactly the same output. ++command and produces the same output for the system-wide views. The ++per-process view (\fB\-p\fR) follows the \fBnumastat \-p\fR layout. + .SH PCP ENVIRONMENT + Environment variables with the prefix \fBPCP_\fP are used to parameterize + the file and directory names used by PCP. +diff --git a/src/pcp/numastat/pcp-numastat.py b/src/pcp/numastat/pcp-numastat.py +index 2a044c9..decdf12 100755 +--- a/src/pcp/numastat/pcp-numastat.py ++++ b/src/pcp/numastat/pcp-numastat.py +@@ -17,6 +17,7 @@ + """ Display NUMA memory allocation statistucs """ + + import os ++import re + import signal + import sys + import time +@@ -86,6 +87,23 @@ SYS_METRICS = [ + + ALL_METRICS = NUMA_METRICS + MEM_METRICS + ++PROCESS_METRICS = [ ++ "proc.psinfo.pid", ++ "proc.psinfo.cmd", ++ "proc.psinfo.psargs", ++ "proc.numa_maps.hugepage", ++ "proc.numa_maps.heap", ++ "proc.numa_maps.stack", ++ "proc.numa_maps.private", ++] ++ ++PROCESS_NUMA_METRICS = [ ++ ("Huge", "proc.numa_maps.hugepage"), ++ ("Heap", "proc.numa_maps.heap"), ++ ("Stack", "proc.numa_maps.stack"), ++ ("Private", "proc.numa_maps.private"), ++] ++ + def prefix(metric): + last_part = metric.split('.')[-1] + result = last_part[0].upper() + last_part[1:] +@@ -96,6 +114,7 @@ class MetricRepository: + self.group = group + self.current_cached_values = {} + self.previous_cached_values = {} ++ self.current_cached_instance_names = {} + + def _fetch_current_values(self, metric, instance): + if instance is not None: +@@ -192,22 +211,465 @@ class NUMAStat: + def print_numa(self, width, nodes, data): + self.__format_table(width, nodes, data) + ++class ProcessNUMAStat: ++ def __init__(self, group, ignore_pid=None): ++ self.group = group ++ self.repo = MetricRepository(group) ++ self.ignore_pid = ignore_pid ++ ++ def __resize(self, width): ++ """ Find a suitable display width limit (matches NUMAStat.resize) """ ++ if width == 0: ++ if not sys.stdout.isatty(): ++ width = 1000000000 # mimic numastat(1) here ++ else: ++ try: ++ # popen() is SAFE, command is a literal string ++ (_, width) = os.popen('stty size', 'r').read().split() ++ width = int(width) ++ except Exception: ++ width = 80 ++ width = int(os.getenv('NUMASTAT_WIDTH', str(width))) ++ return max(int(width), 32) ++ ++ def __normalize_value(self, value): ++ if value is None: ++ return "" ++ if hasattr(value, "decode"): ++ try: ++ return value.decode("utf-8") ++ except Exception: ++ return value.decode("utf-8", "ignore") ++ return str(value) ++ ++ def __normalize_cmdline(self, value): ++ if value is None: ++ return "" ++ ++ # Convert bytes → string ++ if isinstance(value, bytes): ++ value = value.decode("utf-8", "ignore") ++ ++ # /proc//cmdline uses NULL separators ++ if "\0" in value: ++ parts = value.split("\0") ++ value = " ".join(p for p in parts if p) ++ ++ return value.strip() ++ ++ def __is_missing_command(self, command): ++ normalized = self.__normalize_value(command).strip() ++ return normalized == "" or normalized.lower() == "(null)" ++ ++ def __parse_nodes(self, value): ++ node_values = {} ++ text = self.__normalize_value(value) ++ if not text: ++ return node_values ++ for token in text.split(','): ++ if ':' not in token: ++ continue ++ name, raw_value = token.split(':', 1) ++ name = name.strip() ++ if not name.startswith("node"): ++ continue ++ try: ++ node_id = int(name[4:]) ++ node_values[node_id] = float(raw_value) ++ except (TypeError, ValueError): ++ continue ++ return node_values ++ ++ def __matches_process(self, pid, selectors, haystack): ++ if not selectors: ++ return True ++ haystack = self.__normalize_value(haystack) ++ for selector in selectors: ++ selector_text = self.__normalize_value(selector) ++ if re.fullmatch(r"\d+", selector_text): ++ try: ++ if int(selector_text) == int(pid): ++ return True ++ except (TypeError, ValueError): ++ continue ++ continue ++ if selector_text in haystack: ++ return True ++ return False ++ ++ def __collect_process_categories(self, inst_id, metric_maps): ++ category_values = {} ++ nodes = set() ++ has_data = False ++ for label, metric in PROCESS_NUMA_METRICS: ++ parsed = self.__parse_nodes(metric_maps[metric].get(inst_id, "")) ++ if parsed: ++ has_data = True ++ category_values[label] = parsed ++ nodes.update(parsed.keys()) ++ return category_values, nodes, has_data ++ ++ def __sum_categories(self, category_values): ++ node_totals = {} ++ for values in category_values.values(): ++ for node_id, value in values.items(): ++ node_totals[node_id] = node_totals.get(node_id, 0.0) + value ++ return node_totals ++ ++ def __process_rows(self, selectors, system_nodes=None): ++ rows = [] ++ nodes = set(system_nodes or []) ++ selectors_provided = bool(selectors) ++ requested_pids = set() ++ for selector in selectors or []: ++ selector_text = self.__normalize_value(selector) ++ if re.fullmatch(r"\d+", selector_text): ++ try: ++ requested_pids.add(int(selector_text)) ++ except (TypeError, ValueError): ++ continue ++ pid_map = self.repo.current_values("proc.psinfo.pid") or {} ++ command_map = self.repo.current_values("proc.psinfo.cmd") or {} ++ psargs_map = self.repo.current_values("proc.psinfo.psargs") or {} ++ metric_maps = {} ++ for _, metric in PROCESS_NUMA_METRICS: ++ metric_maps[metric] = self.repo.current_values(metric) or {} ++ ++ # Restrict scanning to processes that are actually present in proc.numa_maps.*. ++ numa_inst_ids = set() ++ for values in metric_maps.values(): ++ try: ++ numa_inst_ids.update(values.keys()) ++ except Exception: ++ continue ++ ++ for inst_id, pid in sorted(pid_map.items(), key=lambda item: item[1]): ++ if inst_id not in numa_inst_ids and int(pid) not in requested_pids: ++ continue ++ if ( ++ self.ignore_pid is not None ++ and int(pid) == int(self.ignore_pid) ++ and int(pid) not in requested_pids ++ ): ++ continue ++ command = command_map.get(inst_id, "") ++ full_command = psargs_map.get(inst_id, command) ++ if self.__is_missing_command(command): ++ if int(pid) in requested_pids: ++ command = "unknown" ++ full_command = "unknown" ++ else: ++ continue ++ # Constrain matching to the same process set and labels as the ++ # proc.numa_maps.* instance domains (e.g. `pminfo -f proc.numa_maps.heap`). ++ match_text = "%s %s" % ( ++ self.__normalize_value(command), ++ self.__normalize_cmdline(full_command), ++ ) ++ if not self.__matches_process(pid, selectors, match_text): ++ continue ++ ++ category_values, category_nodes, has_data = self.__collect_process_categories( ++ inst_id, ++ metric_maps, ++ ) ++ if not has_data and not selectors_provided: ++ continue ++ if not has_data and self.__is_missing_command(command): ++ continue ++ ++ if category_nodes: ++ nodes.update(category_nodes) ++ node_totals = self.__sum_categories(category_values) ++ rows.append((pid, command, node_totals, category_values)) ++ return rows, sorted(nodes) ++ ++ def __node_blocks_for_table(self, nodes, width, pid_col_width): ++ num_col_width = 15 ++ sep = " " ++ col_width = len(sep) + num_col_width ++ ++ width = self.__resize(width) ++ max_cols = int((width - pid_col_width) / col_width) ++ max_cols = max(1, max_cols) ++ max_nodes_no_total = max_cols ++ ++ # If we can only fit one numeric column, there is no room to display ++ # any node column together with a Total column. Emit the nodes first ++ # (one per block), then a final Total-only block. ++ if max_cols <= 1: ++ for node_id in nodes: ++ yield [node_id], False ++ yield [], True ++ return ++ ++ max_nodes_with_total = max_cols - 1 ++ ++ if len(nodes) <= max_nodes_with_total: ++ yield nodes, True ++ return ++ ++ done = 0 ++ while len(nodes) - done > max_nodes_with_total: ++ remaining = len(nodes) - done ++ chunk_size = min(max_nodes_no_total, remaining) ++ ++ # Avoid consuming all remaining nodes in a non-total block (which ++ # would otherwise suppress the Total column entirely when nodes ++ # exactly fill the display width). ++ if remaining <= max_nodes_no_total and remaining - chunk_size == 0: ++ chunk_size = max(1, remaining - 1) ++ ++ chunk = nodes[done:done + chunk_size] ++ if not chunk: ++ break ++ yield chunk, False ++ done += chunk_size ++ ++ yield nodes[done:], True ++ ++ def __print_process_table(self, rows, nodes, width): ++ print("Per-node process memory usage (in MBs)") ++ pid_col_width = max( ++ 16, ++ max( ++ len("%s (%s)" % (pid, self.__normalize_value(command))) ++ for pid, command, _, _ in rows ++ ), ++ ) ++ num_col_width = 15 ++ sep = " " ++ ++ node_totals_all = dict((node_id, 0.0) for node_id in nodes) ++ row_totals = {} ++ grand_total = 0.0 ++ for pid, _, per_node, _ in rows: ++ total = 0.0 ++ for node_id in nodes: ++ value = per_node.get(node_id, 0.0) ++ node_totals_all[node_id] += value ++ total += value ++ row_totals[pid] = total ++ grand_total += total ++ ++ for chunk, include_total in self.__node_blocks_for_table(nodes, width, pid_col_width): ++ header = "%-*s" % (pid_col_width, "PID") ++ for node_id in chunk: ++ header += "%s%*s" % (sep, num_col_width, "Node %d" % node_id) ++ if include_total: ++ header += "%s%*s" % (sep, num_col_width, "Total") ++ print(header) ++ ++ line = "-" * pid_col_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ line += "%s%s" % (sep, "-" * num_col_width) ++ print(line) ++ ++ for pid, command, per_node, _ in rows: ++ label = "%s (%s)" % (pid, self.__normalize_value(command)) ++ row = "%-*s" % (pid_col_width, label) ++ for node_id in chunk: ++ row += "%s%*.2f" % (sep, num_col_width, per_node.get(node_id, 0.0)) ++ if include_total: ++ row += "%s%*.2f" % (sep, num_col_width, row_totals.get(pid, 0.0)) ++ print(row) ++ ++ print(line) ++ total_row = "%-*s" % (pid_col_width, "Total") ++ for node_id in chunk: ++ total_row += "%s%*.2f" % (sep, num_col_width, node_totals_all.get(node_id, 0.0)) ++ if include_total: ++ total_row += "%s%*.2f" % (sep, num_col_width, grand_total) ++ print(total_row) ++ print() ++ ++ def __node_blocks(self, nodes, width): ++ label_width = 16 ++ num_col_width = 15 ++ sep = " " ++ col_width = len(sep) + num_col_width ++ ++ width = self.__resize(width) ++ max_cols = int((width - label_width) / col_width) ++ max_cols = max(1, max_cols) ++ max_nodes_no_total = max_cols ++ ++ # If we can only fit one numeric column, there is no room to display ++ # any node column together with a Total column. Emit the nodes first ++ # (one per block), then a final Total-only block. ++ if max_cols <= 1: ++ for node_id in nodes: ++ yield [node_id], False ++ yield [], True ++ return ++ ++ max_nodes_with_total = max_cols - 1 ++ ++ if len(nodes) <= max_nodes_with_total: ++ yield nodes, True ++ return ++ ++ done = 0 ++ while len(nodes) - done > max_nodes_with_total: ++ remaining = len(nodes) - done ++ chunk_size = min(max_nodes_no_total, remaining) ++ ++ # Avoid consuming all remaining nodes in a non-total block (which ++ # would otherwise suppress the Total column entirely when nodes ++ # exactly fill the display width). ++ if remaining <= max_nodes_no_total and remaining - chunk_size == 0: ++ chunk_size = max(1, remaining - 1) ++ ++ chunk = nodes[done:done + chunk_size] ++ if not chunk: ++ break ++ yield chunk, False ++ done += chunk_size ++ ++ yield nodes[done:], True ++ ++ def __print_process_detail(self, row, nodes, width): ++ pid, command, _, categories = row ++ print("Per-node process memory usage (in MBs) for PID %s (%s)" % ++ (pid, self.__normalize_value(command))) ++ label_width = 16 ++ num_col_width = 15 ++ sep = " " ++ ++ if not nodes: ++ nodes = [0] ++ ++ category_totals = {} ++ node_totals = dict((node_id, 0.0) for node_id in nodes) ++ all_total = 0.0 ++ for label, _ in PROCESS_NUMA_METRICS: ++ values = categories.get(label, {}) ++ total = 0.0 ++ for node_id in nodes: ++ value = values.get(node_id, 0.0) ++ total += value ++ node_totals[node_id] += value ++ category_totals[label] = total ++ all_total += total ++ ++ for chunk, include_total in self.__node_blocks(nodes, width): ++ header = "%-*s" % (label_width, "") ++ for node_id in chunk: ++ header += "%s%*s" % (sep, num_col_width, "Node %d" % node_id) ++ if include_total: ++ header += "%s%*s" % (sep, num_col_width, "Total") ++ print(header) ++ ++ header_line = " " * label_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ header_line += "%s%s" % (sep, "-" * num_col_width) ++ print(header_line) ++ ++ for label, _ in PROCESS_NUMA_METRICS: ++ row = "%-*s" % (label_width, label) ++ values = categories.get(label, {}) ++ for node_id in chunk: ++ row += "%s%*.2f" % (sep, num_col_width, values.get(node_id, 0.0)) ++ if include_total: ++ row += "%s%*.2f" % (sep, num_col_width, category_totals.get(label, 0.0)) ++ print(row) ++ ++ line = "-" * label_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ line += "%s%s" % (sep, "-" * num_col_width) ++ print(line) ++ ++ total_row = "%-*s" % (label_width, "Total") ++ for node_id in chunk: ++ total_row += "%s%*.2f" % (sep, num_col_width, node_totals.get(node_id, 0.0)) ++ if include_total: ++ total_row += "%s%*.2f" % (sep, num_col_width, all_total) ++ print(total_row) ++ print() ++ ++ def print_processes(self, selectors, system_nodes=None, width=0): ++ rows, nodes = self.__process_rows(selectors, system_nodes=system_nodes) ++ if not rows: ++ if selectors: ++ print("No matching processes with NUMA maps data.") ++ else: ++ print("No processes with NUMA maps data.") ++ print() ++ return ++ if not nodes: ++ nodes = [0] ++ selector_texts = [self.__normalize_value(s).strip() for s in (selectors or [])] ++ selectors_are_pids = bool(selector_texts) and all( ++ re.fullmatch(r"\d+", text) for text in selector_texts ++ ) ++ if selectors_are_pids: ++ requested = [] ++ for text in selector_texts: ++ try: ++ requested.append(int(text)) ++ except (TypeError, ValueError): ++ continue ++ ++ rows_by_pid = {} ++ for pid, command, per_node, categories in rows: ++ try: ++ rows_by_pid[int(pid)] = (pid, command, per_node, categories) ++ except (TypeError, ValueError): ++ continue ++ ++ if len(requested) == 1: ++ pid = requested[0] ++ found = rows_by_pid.get(pid) ++ if found is None: ++ print("No matching process with NUMA maps data for PID %s." % pid) ++ print() ++ return ++ self.__print_process_detail(found, nodes, width) ++ return ++ ++ selected_rows = [] ++ for pid in requested: ++ found = rows_by_pid.get(pid) ++ if found is None: ++ print("No matching process with NUMA maps data for PID %s." % pid) ++ print() ++ continue ++ row_pid, command, per_node, categories = found ++ selected_rows.append((row_pid, command, per_node, categories)) ++ if selected_rows: ++ self.__print_process_table(selected_rows, nodes, width) ++ return ++ ++ single_selector = len(selectors or []) == 1 ++ if single_selector and len(rows) == 1: ++ self.__print_process_detail(rows[0], nodes, width) ++ return ++ ++ self.__print_process_table(rows, nodes, width) ++ + class NumaStatOption(pmapi.pmOptions): + context = None + timefmt = "%m/%d/%Y %H:%M:%S" + width = 0 + mem_out = False + numa_out = False ++ process_out = False ++ process_filters = [] + + def override(self,opt): + """ Override standard PCP options to match numastat(1) """ +- if opt == 'n': ++ if opt in ('n', 'p'): + return True + return False + + def __init__(self): + pmapi.pmOptions.__init__(self) +- self.pmSetShortOptions("w:mV?:n") ++ self.width = 0 ++ self.mem_out = False ++ self.numa_out = False ++ self.process_out = False ++ self.process_filters = [] ++ self.pmSetShortOptions("w:mV?np") + self.pmSetOptionCallback(self.extraOptions) + self.pmSetOverrideCallback(self.override) + self.pmSetLongOptionHeader("Numastat options") +@@ -215,6 +677,7 @@ class NumaStatOption(pmapi.pmOptions): + # Map long options to our non-conflicting short letters + self.pmSetLongOption("meminfo", 0, 'm', "", "show meminfo-like system-wide memory usage") + self.pmSetLongOption("numastat", 0, 'n', "", "show the numastat statistics info") ++ self.pmSetLongOption("process", 0, 'p', "", "show per-process NUMA memory usage") + self.pmSetLongOptionVersion() + self.pmSetLongOptionHelp() + +@@ -225,6 +688,8 @@ class NumaStatOption(pmapi.pmOptions): + self.mem_out = True + elif opt == "n": + self.numa_out = True ++ elif opt == "p": ++ self.process_out = True + elif opt == "V": + pass + else: +@@ -232,7 +697,7 @@ class NumaStatOption(pmapi.pmOptions): + return True + + def checkoptions(self): +- if (not self.mem_out) and (not self.numa_out) and (self.width == 0): ++ if (not self.mem_out) and (not self.numa_out) and (not self.process_out): + self.numa_out = True + if self.width < 0: + return False +@@ -284,6 +749,33 @@ class NumaStatReport(pmcc.MetricGroupPrinter): + nodes.sort(key=lambda t: t[0]) + return nodes + ++ def __discover_nodes_all(self, group, name): ++ # Build list of nodes from instances (instance id, instance name) ++ nodes = [] ++ try: ++ for ent in group[name].netValues: ++ inst_id = ent[0].inst ++ inst_name = ent[1] # usually "node0", "node1", ... ++ nodes.append((inst_id, inst_name)) ++ except Exception: ++ pass ++ nodes.sort(key=lambda t: t[0]) ++ return nodes ++ ++ def __node_ids(self, nodes): ++ node_ids = [] ++ for inst_id, inst_name in nodes or []: ++ match = re.match(r"node(\d+)$", str(inst_name)) ++ if match: ++ node_ids.append(int(match.group(1))) ++ continue ++ try: ++ node_ids.append(int(inst_id)) ++ except (TypeError, ValueError): ++ continue ++ node_ids = sorted(set(node_ids)) ++ return node_ids or None ++ + def report(self, manager): + # Print in a stable order + group = manager["sys_info"] +@@ -296,17 +788,54 @@ class NumaStatReport(pmcc.MetricGroupPrinter): + + output_numa = ( + self.options.numa_out +- or (not self.options.mem_out and not self.options.numa_out) + ) + output_mem = self.options.mem_out +- group = manager["numastat"] +- nodes = self.__discover_nodes(group, "mem.numa.util.total") +- timestamp = self.__get_timestamp(group) ++ output_process = self.options.process_out ++ ++ if output_mem or output_numa: ++ group = manager["numastat"] ++ timestamp_group = group ++ elif output_process: ++ group = manager["process_numa"] ++ timestamp_group = group ++ else: ++ return ++ ++ timestamp = self.__get_timestamp(timestamp_group) + print("%-20s : %s"%("Timestamp", timestamp)) + if output_mem: ++ nodes = self.__discover_nodes(group, "mem.numa.util.total") + NUMAStat(group).print_mem(self.options.width, nodes, "meminfo") + if output_numa: ++ nodes = self.__discover_nodes(group, "mem.numa.util.total") + NUMAStat(group).print_numa(self.options.width, nodes, "numastat") ++ if output_process: ++ ignore_pid = None ++ if ( ++ NumaStatOption.context is not PM_CONTEXT_ARCHIVE ++ and not self.options.pmGetOptionHosts() ++ ): ++ ignore_pid = os.getpid() ++ system_nodes = None ++ try: ++ system_nodes = self.__node_ids( ++ self.__discover_nodes(manager["sys_info"], "hinv.node.online") ++ ) ++ except Exception: ++ system_nodes = None ++ if system_nodes is None: ++ try: ++ system_nodes = self.__node_ids( ++ self.__discover_nodes_all(manager["sys_info"], "mem.numa.alloc.hit") ++ ) ++ except Exception: ++ system_nodes = None ++ ++ ProcessNUMAStat(manager["process_numa"], ignore_pid).print_processes( ++ self.options.process_filters, ++ system_nodes=system_nodes, ++ width=self.options.width, ++ ) + + if ( + NumaStatOption.context is not PM_CONTEXT_ARCHIVE +@@ -323,13 +852,37 @@ if __name__ == '__main__': + raise pmapi.pmUsageErr() + NumaStatOption.context = mngr.type + +- missing = mngr.checkMissingMetrics(ALL_METRICS) ++ opts.process_filters = opts.pmGetOperands() ++ if opts.process_filters and not opts.process_out: ++ print("Process selectors require -p/--process option") ++ raise pmapi.pmUsageErr() ++ if not opts.process_filters and opts.process_out: ++ print("Provide pid or process name for -p/--process option") ++ raise pmapi.pmUsageErr() ++ ++ required_metrics = list(SYS_METRICS) ++ if opts.mem_out or opts.numa_out: ++ required_metrics.extend(ALL_METRICS) ++ if opts.process_out: ++ required_metrics.extend(PROCESS_METRICS) ++ required_metrics = list(dict.fromkeys(required_metrics)) ++ ++ missing = mngr.checkMissingMetrics(required_metrics) + if missing is not None: + sys.stderr.write('Error: not all required metrics are available\nMissing: %s\n' % (missing)) + sys.exit(1) + +- mngr["numastat"] = ALL_METRICS +- mngr["sys_info"] = SYS_METRICS ++ sys_info_metrics = list(SYS_METRICS) ++ if mngr.checkMissingMetrics(["hinv.node.online"]) is None: ++ sys_info_metrics.append("hinv.node.online") ++ if mngr.checkMissingMetrics(["mem.numa.alloc.hit"]) is None: ++ sys_info_metrics.append("mem.numa.alloc.hit") ++ ++ if opts.mem_out or opts.numa_out: ++ mngr["numastat"] = ALL_METRICS ++ if opts.process_out: ++ mngr["process_numa"] = PROCESS_METRICS ++ mngr["sys_info"] = sys_info_metrics + mngr.printer = NumaStatReport(opts) + sts = mngr.run() + sys.exit(sts) +diff --git a/src/pmdas/linux_proc/clusters.h b/src/pmdas/linux_proc/clusters.h +index edfd5ad..547c195 100644 +--- a/src/pmdas/linux_proc/clusters.h ++++ b/src/pmdas/linux_proc/clusters.h +@@ -72,8 +72,9 @@ + #define CLUSTER_CGROUP2_IRQ_PRESSURE 76 + #define CLUSTER_PID_FDINFO 77 /* /proc//fdinfo */ + #define CLUSTER_HOTPROC_PID_FDINFO 78 /* /proc//fdinfo */ ++#define CLUSTER_PID_NUMA_MAPS 79 /* /proc//numa_maps */ + + #define MIN_CLUSTER 8 /* first cluster number we use here */ +-#define MAX_CLUSTER 79 /* one more than highest cluster number used */ ++#define MAX_CLUSTER 80 /* one more than highest cluster number used */ + + #endif /* _CLUSTERS_H */ +diff --git a/src/pmdas/linux_proc/help b/src/pmdas/linux_proc/help +index 79824bb..c1b2198 100644 +--- a/src/pmdas/linux_proc/help ++++ b/src/pmdas/linux_proc/help +@@ -75,6 +75,19 @@ kernel threads + @ proc.runq.kernel number of kernel threads + Instantaneous number of processes with virtual size of zero (kernel threads) + ++@ proc.numa_maps.hugepage per-node hugepage mapped memory in MB (/proc//numa_maps) ++Hugepage map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.heap per-node heap mapped memory in MB (/proc//numa_maps) ++Heap map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.stack per-node stack mapped memory in MB (/proc//numa_maps) ++Stack map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.private per-node private mapped memory in MB (/proc//numa_maps) ++Private map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++ + @ proc.control.all.threads process indom includes threads + If set to one, the process instance domain as reported by pmdaproc + contains all threads as well as the processes that started them. +diff --git a/src/pmdas/linux_proc/indom.h b/src/pmdas/linux_proc/indom.h +index abe2596..c5c00a6 100644 +--- a/src/pmdas/linux_proc/indom.h ++++ b/src/pmdas/linux_proc/indom.h +@@ -62,6 +62,7 @@ extern FILE *proc_statsfile(const char *, char *, int); + + /* Generic globals setup during PMDA startup */ + extern size_t _pm_system_pagesize; ++extern size_t _pm_system_hugepagesize; + extern long _pm_hertz; + + /* +diff --git a/src/pmdas/linux_proc/pmda.c b/src/pmdas/linux_proc/pmda.c +index 0c8eb16..f81e348 100644 +--- a/src/pmdas/linux_proc/pmda.c ++++ b/src/pmdas/linux_proc/pmda.c +@@ -66,6 +66,7 @@ static int autogroup = -1; /* =1 autogroup enabled */ + static unsigned int threads; /* control.all.threads */ + static char * cgroups; /* control.all.cgroups */ + size_t _pm_system_pagesize; ++size_t _pm_system_hugepagesize; + long _pm_hertz; + + /* +@@ -1395,6 +1396,23 @@ static pmdaMetric metrictab[] = { + { NULL, { PMDA_PMID(CLUSTER_PID_FDINFO,12), PM_TYPE_U64, PROC_INDOM, + PM_SEM_INSTANT, PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0)}}, + ++/* ++* numa_maps cluster ++*/ ++ ++/* proc.numa_maps.hugepage */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,0), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.heap */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,1), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.stack */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,2), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.private */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,3), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++ + }; + + pmInDom +@@ -3534,6 +3552,34 @@ proc_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) + return PM_ERR_PMID; + } + break; ++ case CLUSTER_PID_NUMA_MAPS: ++ if (!have_access) ++ return PM_ERR_PERMISSION; ++ if ((entry = fetch_proc_pid_numa_maps(inst, active_proc_pid, &sts)) == NULL) ++ return sts; ++ if (!(entry->success & PROC_PID_FLAG_NUMA_MAPS)) ++ return 0; ++ ++ switch(item) { ++ case 0: /* proc.numa_maps.hugepage */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.huge_id); ++ break; ++ ++ case 1: /* proc.numa_maps.heap */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.heap_id); ++ break; ++ ++ case 2: /* proc.numa_maps.stack */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.stack_id); ++ break; ++ ++ case 3: /* proc.numa_maps.private */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.private_id); ++ break; ++ default: /* unknown cluster */ ++ return PM_ERR_PMID; ++ } ++ break; + default: /* unknown cluster */ + return PM_ERR_PMID; + } +@@ -3896,6 +3942,30 @@ proc_gidname_lookup(int gid) + return ""; + } + ++static size_t ++proc_hugepagesize(void) ++{ ++ unsigned long huge_page_size_kb = 0; ++ char buf[128]; ++ FILE *fs; ++ ++ if ((fs = fopen("/proc/meminfo", "r")) == NULL) ++ return 0; ++ ++ while (fgets(buf, sizeof(buf), fs)) { ++ if (sscanf(buf, "Hugepagesize: %lu kB", &huge_page_size_kb) == 1) ++ break; ++ } ++ fclose(fs); ++ ++ /* Guard against overflow (CID 502014) */ ++ if (huge_page_size_kb > ULONG_MAX / 1024UL) { ++ return 0; ++ } ++ ++ return huge_page_size_kb * 1024UL; ++} ++ + /* + * Initialise the agent (both daemon and DSO). + */ +@@ -3917,6 +3987,10 @@ proc_init(pmdaInterface *dp) + _pm_system_pagesize = atoi(envpath); + else + _pm_system_pagesize = getpagesize(); ++ if ((envpath = getenv("PROC_HUGEPAGESIZE")) != NULL) ++ _pm_system_hugepagesize = atoi(envpath); ++ else ++ _pm_system_hugepagesize = proc_hugepagesize(); + if ((envpath = getenv("PROC_STATSPATH")) != NULL) + proc_statspath = envpath; + if ((envpath = getenv("PROC_THREADS")) != NULL) +diff --git a/src/pmdas/linux_proc/pmdaproc.1 b/src/pmdas/linux_proc/pmdaproc.1 +index 602b3b6..565120f 100644 +--- a/src/pmdas/linux_proc/pmdaproc.1 ++++ b/src/pmdas/linux_proc/pmdaproc.1 +@@ -34,6 +34,10 @@ The + PMDA exports metrics that measure the memory, processor and + other resource use of each process, as well as summary information + collated across all of the running processes. ++This includes per-process NUMA placement summaries from ++.I /proc//numa_maps ++via the ++.BR proc.numa_maps.*\ metrics. + The PMDA uses credentials passed from the + .BR PMAPI (3) + monitoring tool identifying the user requesting the information, +diff --git a/src/pmdas/linux_proc/proc_pid.c b/src/pmdas/linux_proc/proc_pid.c +index 80a678d..2706f98 100644 +--- a/src/pmdas/linux_proc/proc_pid.c ++++ b/src/pmdas/linux_proc/proc_pid.c +@@ -2530,3 +2530,272 @@ fetch_proc_pid_fdinfo(int id, proc_pid_t *proc_pid, int *sts) + + return (*sts < 0) ? NULL : ep; + } ++ ++#define PROCESS_HUGE_INDEX 0 ++#define PROCESS_HEAP_INDEX 1 ++#define PROCESS_STACK_INDEX 2 ++#define PROCESS_PRIVATE_INDEX 3 ++#define PROCESS_CATEGORY_COUNT 4 ++ ++#define MEGABYTE (1024.0 * 1024.0) ++ ++static const char *process_mem_tokens[] = { ++ "huge", ++ "heap", ++ "stack", ++}; ++ ++typedef struct { ++ char *s; ++ size_t len; ++ size_t cap; ++} strbuf_t; ++ ++typedef struct { ++ int node; ++ double values[PROCESS_CATEGORY_COUNT]; ++} numa_node_totals_t; ++ ++static int ++append_numa_maps_node(strbuf_t *b, int node_num, double value_mb) ++{ ++ char tmp[64]; ++ char *newptr; ++ size_t needed, newcap; ++ int n; ++ ++ n = pmsprintf(tmp, sizeof(tmp), "%snode%d:%.2f", ++ b->len > 0 ? "," : "", node_num, value_mb); ++ if (n < 0 || n >= (int)sizeof(tmp)) ++ return -E2BIG; ++ ++ if (b->s == NULL) { ++ b->cap = 128; ++ b->s = (char *)malloc(b->cap); ++ if (b->s == NULL) ++ return -ENOMEM; ++ b->len = 0; ++ b->s[0] = '\0'; ++ } ++ ++ needed = b->len + (size_t)n + 1; ++ if (needed > b->cap) { ++ newcap = b->cap; ++ while (newcap < needed) ++ newcap *= 2; ++ ++ newptr = (char *)realloc(b->s, newcap); ++ if (newptr == NULL) ++ return -ENOMEM; ++ ++ b->s = newptr; ++ b->cap = newcap; ++ } ++ ++ memcpy(b->s + b->len, tmp, (size_t)n); ++ b->len += (size_t)n; ++ b->s[b->len] = '\0'; ++ return 0; ++} ++ ++static int ++find_or_add_numa_node(numa_node_totals_t **nodes, int *node_count, int node) ++{ ++ numa_node_totals_t *new_nodes; ++ int i; ++ ++ for (i = 0; i < *node_count; i++) { ++ if ((*nodes)[i].node == node) ++ return i; ++ } ++ ++ new_nodes = (numa_node_totals_t *)realloc(*nodes, ++ (*node_count + 1) * sizeof(*new_nodes)); ++ if (new_nodes == NULL) ++ return -ENOMEM; ++ ++ *nodes = new_nodes; ++ (*nodes)[*node_count].node = node; ++ memset((*nodes)[*node_count].values, 0, sizeof((*nodes)[*node_count].values)); ++ (*node_count)++; ++ return *node_count - 1; ++} ++ ++static int ++compare_numa_node_totals(const void *a, const void *b) ++{ ++ const numa_node_totals_t *node_a = (const numa_node_totals_t *)a; ++ const numa_node_totals_t *node_b = (const numa_node_totals_t *)b; ++ ++ return node_a->node - node_b->node; ++} ++ ++static int ++numa_maps_category(const char *line) ++{ ++ char *copy, *tok, *saveptr = NULL; ++ int i; ++ ++ if ((copy = strdup(line)) == NULL) ++ return PROCESS_PRIVATE_INDEX; ++ ++ tok = strtok_r(copy, " \t", &saveptr); ++ while (tok != NULL) { ++ for (i = PROCESS_HUGE_INDEX; i <= PROCESS_STACK_INDEX; i++) { ++ if (strcmp(tok, process_mem_tokens[i]) == 0) { ++ free(copy); ++ return i; ++ } ++ } ++ tok = strtok_r(NULL, " \t", &saveptr); ++ } ++ free(copy); ++ return PROCESS_PRIVATE_INDEX; ++} ++ ++static int ++parse_proc_numa_maps(proc_pid_entry_t *ep, size_t buflen, char *buf) ++{ ++ strbuf_t huge = {0}, heap = {0}, stack = {0}, priv = {0}; ++ numa_node_totals_t *nodes = NULL; ++ double page_size_bytes, huge_page_size_bytes; ++ int node_count = 0; ++ int sts = 0; ++ char *cur = buf; ++ char *end; ++ ++ if (buf == NULL || buflen == 0) ++ return 0; ++ /* ++ * Ensure the proc buffer is NUL-terminated so the string routines below ++ * cannot read past the end. read_proc_entry() allocates len+1 bytes and ++ * uses buflen (len) for the bytes-read value passed here. ++ */ ++ buf[buflen] = '\0'; ++ end = buf + buflen; ++ ++ page_size_bytes = (double)_pm_system_pagesize; ++ if (page_size_bytes <= 0.0) ++ page_size_bytes = 4096.0; ++ ++ huge_page_size_bytes = (double)_pm_system_hugepagesize; ++ if (huge_page_size_bytes <= 0.0) ++ huge_page_size_bytes = 2048 * 1024; // Taking a common hugepagesize value as a fallback ++ ++ while (cur < end && *cur) { ++ char *nl = memchr(cur, '\n', (size_t)(end - cur)); ++ char *tok, *saveptr = NULL; ++ int category; ++ ++ if (nl != NULL) ++ *nl = '\0'; ++ ++ category = numa_maps_category(cur); ++ tok = strtok_r(cur, " \t", &saveptr); ++ while (tok != NULL) { ++ int node, index; ++ double pages, bytes; ++ ++ if (tok[0] == 'N' && sscanf(tok, "N%d=%lf", &node, &pages) == 2) { ++ index = find_or_add_numa_node(&nodes, &node_count, node); ++ if (index < 0) { ++ sts = index; ++ goto cleanup; ++ } ++ ++ if (category == PROCESS_HUGE_INDEX) ++ bytes = pages * huge_page_size_bytes; ++ else ++ bytes = pages * page_size_bytes; ++ ++ nodes[index].values[category] += bytes / MEGABYTE; ++ } ++ tok = strtok_r(NULL, " \t", &saveptr); ++ } ++ ++ if (nl == NULL) ++ break; ++ *nl = '\n'; ++ cur = nl + 1; ++ } ++ ++ if (node_count > 1) ++ qsort(nodes, node_count, sizeof(*nodes), compare_numa_node_totals); ++ ++ for (int i = 0; i < node_count; i++) { ++ sts = append_numa_maps_node(&huge, nodes[i].node, ++ nodes[i].values[PROCESS_HUGE_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&heap, nodes[i].node, ++ nodes[i].values[PROCESS_HEAP_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&stack, nodes[i].node, ++ nodes[i].values[PROCESS_STACK_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&priv, nodes[i].node, ++ nodes[i].values[PROCESS_PRIVATE_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ } ++ ++ if (huge.s != NULL) ++ ep->numa_maps.huge_id = proc_strings_insert(huge.s); ++ if (heap.s != NULL) ++ ep->numa_maps.heap_id = proc_strings_insert(heap.s); ++ if (stack.s != NULL) ++ ep->numa_maps.stack_id = proc_strings_insert(stack.s); ++ if (priv.s != NULL) ++ ep->numa_maps.private_id = proc_strings_insert(priv.s); ++ ++cleanup: ++ free(nodes); ++ free(huge.s); ++ free(heap.s); ++ free(stack.s); ++ free(priv.s); ++ return sts; ++} ++ ++static int ++refresh_proc_pid_numa_maps(proc_pid_entry_t *ep) ++{ ++ int fd, sts; ++ ++ if (ep->success & PROC_PID_FLAG_NUMA_MAPS) ++ return 0; ++ if ((fd = proc_open("numa_maps", ep)) < 0) ++ return maperr(); ++ ep->numa_maps.huge_id = -1; ++ ep->numa_maps.heap_id = -1; ++ ep->numa_maps.stack_id = -1; ++ ep->numa_maps.private_id = -1; ++ if ((sts = read_proc_entry(fd, &procbuflen, &procbuf)) >= 0) { ++ sts = parse_proc_numa_maps(ep, procbuflen, procbuf); ++ if (sts >= 0) ++ ep->success |= PROC_PID_FLAG_NUMA_MAPS; ++ } ++ close(fd); ++ return sts; ++} ++ ++/* ++ * fetch data from /proc//numa_maps entries for pid ++ */ ++proc_pid_entry_t * ++fetch_proc_pid_numa_maps(int id, proc_pid_t *proc_pid, int *sts) ++{ ++ proc_pid_entry_t *ep = proc_pid_entry_lookup(id, proc_pid); ++ ++ *sts = 0; ++ if (!ep) ++ return NULL; ++ ++ if (!(ep->fetched & PROC_PID_FLAG_NUMA_MAPS)) { ++ *sts = refresh_proc_pid_numa_maps(ep); ++ ep->fetched |= PROC_PID_FLAG_NUMA_MAPS; ++ } ++ return (*sts < 0) ? NULL : ep; ++} +diff --git a/src/pmdas/linux_proc/proc_pid.h b/src/pmdas/linux_proc/proc_pid.h +index f5762aa..145ec8d 100644 +--- a/src/pmdas/linux_proc/proc_pid.h ++++ b/src/pmdas/linux_proc/proc_pid.h +@@ -202,6 +202,16 @@ typedef struct { + uint64_t locked; + } proc_pid_smaps_t; + ++/* ++ * metrics in /proc//numa_maps ++ */ ++typedef struct { ++ int huge_id; ++ int heap_id; ++ int stack_id; ++ int private_id; ++} proc_pid_numa_maps_t; ++ + /* + * metrics in /proc//fdinfo/ + */ +@@ -245,6 +255,7 @@ enum { + PROC_PID_FLAG_EXE = 1<<15, + PROC_PID_FLAG_AUTOGROUP = 1<<16, + PROC_PID_FLAG_FDINFO = 1<<17, ++ PROC_PID_FLAG_NUMA_MAPS = 1<<18, + }; + + typedef struct { +@@ -279,6 +290,9 @@ typedef struct { + /* /proc//smaps_rollup cluster */ + proc_pid_smaps_t smaps; + ++ /* /proc//numa_maps cluster */ ++ proc_pid_numa_maps_t numa_maps; ++ + /* /proc//maps cluster */ + size_t maps_buflen; + +@@ -330,6 +344,9 @@ typedef struct { + /* lookup a proc hash entry */ + extern proc_pid_entry_t *proc_pid_entry_lookup(int, proc_pid_t *); + ++/* fetch a proc//numa_maps entry for pid */ ++extern proc_pid_entry_t *fetch_proc_pid_numa_maps(int, proc_pid_t *, int *); ++ + /* refresh the proc indom, reset all "fetched" flags */ + extern int refresh_proc_pid(proc_pid_t *, proc_runq_t *, int, const char *, const char *, int); + +diff --git a/src/pmdas/linux_proc/root_proc b/src/pmdas/linux_proc/root_proc +index 3e5b50d..16e75dd 100644 +--- a/src/pmdas/linux_proc/root_proc ++++ b/src/pmdas/linux_proc/root_proc +@@ -454,6 +454,14 @@ proc { + smaps PROC:*:* + autogroup PROC:*:* + control ++ numa_maps ++} ++ ++proc.numa_maps { ++ hugepage PROC:79:0 ++ heap PROC:79:1 ++ stack PROC:79:2 ++ private PROC:79:3 + } + + hotproc { +-- +2.43.7 + diff --git a/SOURCES/pcp-6.3.7-CVE-2026-16524.patch b/SOURCES/pcp-6.3.7-CVE-2026-16524.patch new file mode 100644 index 0000000..789d29e --- /dev/null +++ b/SOURCES/pcp-6.3.7-CVE-2026-16524.patch @@ -0,0 +1,269 @@ +From 4412004b8bdc2d9e63a4242a30346f41bd784311 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Thu, 2 Jul 2026 15:41:11 +1000 +Subject: [PATCH] linux_sockets pmda: fix command injection via + network.persocket.filter (CWE-78) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The sockets_check_filter() validation helper returns 1 for safe input +and 0 for unsafe input. The guard in sockets_store() tested +if (sockets_check_filter(av.cp)) — rejecting safe input and accepting +malicious input containing shell metacharacters. The accepted filter +was later passed to popen() via shell interpretation, enabling arbitrary +command execution as the PMDA process user. + +Fix: +- Invert the guard: if (!sockets_check_filter(av.cp)) +- Replace popen()/pclose() in ss_open_stream() with the libpcp + __pmProcessAddArg()/__pmProcessPipe()/__pmProcessPipeClose() API + which uses execvp() internally, eliminating shell interpretation + of the filter string entirely +- Add qa/2101 verifying that valid filters are accepted and shell + metacharacters (semicolons, backticks, pipes) are rejected + +Reported-by: Francisco Alisson Bezerra, TIM Security Red Team +Reported-by: Lucas Gabriel Alves, TIM Security Red Team +Reported-by: Massimiliano Brolli, TIM Security Red Team + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + qa/2101 | 70 +++++++++++++++++++++++++++++ + qa/2101.out | 15 +++++++ + qa/group | 1 + + src/pmdas/linux_sockets/pmda.c | 8 ++-- + src/pmdas/linux_sockets/ss_stream.c | 53 ++++++++++++++++++---- + 5 files changed, 133 insertions(+), 14 deletions(-) + create mode 100755 qa/2101 + create mode 100644 qa/2101.out + +diff --git a/qa/2101 b/qa/2101 +new file mode 100755 +index 000000000..733b7f70c +--- /dev/null ++++ b/qa/2101 +@@ -0,0 +1,70 @@ ++#!/bin/sh ++# PCP QA Test No. 2101 ++# Verify linux_sockets PMDA filter validation rejects shell metacharacters ++# and accepts valid filter expressions (CWE-78 fix verification) ++# ++# Copyright (c) 2026 Red Hat. All Rights Reserved. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++# get standard environment, filters and checks ++. ./common.product ++. ./common.filter ++. ./common.check ++ ++[ $PCP_PLATFORM = linux ] || _notrun "Linux-specific sockets testing" ++[ -f $PCP_PMDAS_DIR/sockets/pmdasockets ] || _notrun "sockets PMDA not installed" ++ ++_cleanup() ++{ ++ _cleanup_pmda sockets ++ cd $here ++ $sudo rm -rf $tmp $tmp.* ++} ++ ++status=0 # success is the default! ++trap "_cleanup; exit \$status" 0 1 2 3 15 ++ ++_prepare_pmda sockets ++_stop_auto_restart pmcd ++ ++# install the sockets PMDA ++cd $PCP_PMDAS_DIR/sockets ++$sudo ./Remove >/dev/null 2>&1 ++$sudo ./Install $tmp.out 2>&1 ++cat $tmp.out >>$seq_full ++ ++# check the PMDA is alive ++pmprobe -v network.persocket.filter >$tmp.probe 2>&1 ++grep -q 'No PMCD agent' $tmp.probe && _notrun "sockets PMDA failed to install" ++ ++# real QA test starts here ++ ++echo "=== valid filter should be accepted ===" ++pmstore network.persocket.filter "sport == 22" 2>&1 \ ++| grep -q 'Bad input' && echo "FAIL: valid filter rejected" || echo "valid filter accepted" ++ ++echo ++echo "=== shell metacharacter semicolon should be rejected ===" ++pmstore network.persocket.filter ';id' 2>&1 \ ++| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected" ++ ++echo ++echo "=== shell metacharacter backtick should be rejected ===" ++pmstore network.persocket.filter '`id`' 2>&1 \ ++| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected" ++ ++echo ++echo "=== shell metacharacter pipe should be rejected ===" ++pmstore network.persocket.filter '|cat /etc/passwd' 2>&1 \ ++| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected" ++ ++echo ++echo "=== shell metacharacter dollar should be rejected ===" ++pmstore network.persocket.filter '${IFS}id' 2>&1 \ ++| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected" ++ ++# success, all done ++exit +diff --git a/qa/2101.out b/qa/2101.out +new file mode 100644 +index 000000000..02f9ac565 +--- /dev/null ++++ b/qa/2101.out +@@ -0,0 +1,15 @@ ++QA output created by 2101 ++=== valid filter should be accepted === ++valid filter accepted ++ ++=== shell metacharacter semicolon should be rejected === ++metacharacter rejected ++ ++=== shell metacharacter backtick should be rejected === ++metacharacter rejected ++ ++=== shell metacharacter pipe should be rejected === ++metacharacter rejected ++ ++=== shell metacharacter dollar should be rejected === ++metacharacter rejected +diff --git a/qa/group b/qa/group +index a1b2c3d4e..b2c3d4e5f 100644 +--- a/qa/group ++++ b/qa/group +@@ -2218,5 +2218,6 @@ pmcd.pdu + 1992 pmda.uwsgi local + 2100 pmproxy local security ++2101 pmda.sockets local security + 2104 libpcp local security + 2105 libpcp pmcd local security pmcd.pdu + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/src/pmdas/linux_sockets/pmda.c b/src/pmdas/linux_sockets/pmda.c +index 5a3018d8a..59e69a675 100644 +--- a/src/pmdas/linux_sockets/pmda.c ++++ b/src/pmdas/linux_sockets/pmda.c +@@ -162,11 +162,9 @@ sockets_check_filter(const char *string) + const char *p; + + for (p = string; *p; p++) { +- if (isspace(*p)) ++ if (isspace(*p) || isalnum(*p)) + continue; +- if (isalnum(*p)) +- continue; +- if (*p == '(' || *p == ')') ++ if (strchr("()=!<>:.*/-,", *p) != NULL) + continue; + return 0; /* disallow */ + } +@@ -191,7 +189,7 @@ sockets_store(pmResult *result, pmdaExt *pmda) + case 0: /* network.persocket.filter */ + if ((sts = pmExtractValue(vsp->valfmt, &vsp->vlist[0], + PM_TYPE_STRING, &av, PM_TYPE_STRING)) >= 0) { +- if (sockets_check_filter(av.cp)) { ++ if (!sockets_check_filter(av.cp)) { + sts = PM_ERR_BADSTORE; + free(av.cp); + break; +diff --git a/src/pmdas/linux_sockets/ss_stream.c b/src/pmdas/linux_sockets/ss_stream.c +index 421c65fd1..833fc275a 100644 +--- a/src/pmdas/linux_sockets/ss_stream.c ++++ b/src/pmdas/linux_sockets/ss_stream.c +@@ -14,18 +14,19 @@ + + #include + #include ++#include + #include "ss_stats.h" + + #define SS_OPTIONS "-noemitauO" + +-char *ss_filter = NULL; /* storable: network.persocket.filter */ ++char *ss_filter; /* storable: network.persocket.filter */ ++static int using_pipe; /* pipe is normal operation, QA uses files */ + + FILE * + ss_open_stream() + { +- FILE *fp; ++ FILE *fp = NULL; + char *path; +- char cmd[MAXPATHLEN]; + + if (ss_filter == NULL) { + /* pmstore to network.persocket.filter frees this if changing */ +@@ -38,17 +39,51 @@ ss_open_stream() + fp = fopen(path, "r"); + if (pmDebugOptions.appl0) + fprintf(stderr, "ss_open_stream: open PCPQA_PMDA_SOCKETS=%s\n", path); ++ using_pipe = 0; + } else { ++ __pmExecCtl_t *argp = NULL; ++ int sts; ++ + if (access((path = "/usr/sbin/ss"), X_OK) != 0) { + if (access((path = "/usr/bin/ss"), X_OK) != 0) { + fprintf(stderr, "Error: no \"ss\" binary found\n"); + return NULL; + } + } +- pmsprintf(cmd, sizeof(cmd), "%s %s %s", path, SS_OPTIONS, ss_filter); +- fp = popen(cmd, "r"); ++ if ((sts = __pmProcessAddArg(&argp, path)) < 0 || ++ (sts = __pmProcessAddArg(&argp, SS_OPTIONS)) < 0) { ++ if (pmDebugOptions.appl0) ++ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n", ++ pmErrStr(sts)); ++ return NULL; ++ } ++ if (ss_filter[0] != '\0') { ++ char *s, *tok, *saveptr; ++ ++ if ((s = strdup(ss_filter)) == NULL) ++ return NULL; ++ for (tok = strtok_r(s, " \t", &saveptr); tok != NULL; ++ tok = strtok_r(NULL, " \t", &saveptr)) { ++ if ((sts = __pmProcessAddArg(&argp, tok)) < 0) { ++ free(s); ++ if (pmDebugOptions.appl0) ++ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n", ++ pmErrStr(sts)); ++ return NULL; ++ } ++ } ++ free(s); ++ } ++ if ((sts = __pmProcessPipe(&argp, "r", PM_EXEC_TOSS_NONE, &fp)) < 0) { ++ if (pmDebugOptions.appl0) ++ fprintf(stderr, "ss_open_stream: __pmProcessPipe failed: %s\n", ++ pmErrStr(sts)); ++ return NULL; ++ } + if (pmDebugOptions.appl0) +- fprintf(stderr, "ss_open_stream: popen %s\n", cmd); ++ fprintf(stderr, "ss_open_stream: exec %s %s %s\n", ++ path, SS_OPTIONS, ss_filter); ++ using_pipe = 1; + } + + return fp; +@@ -57,8 +92,8 @@ ss_open_stream() + void + ss_close_stream(FILE *fp) + { +- if (getenv("PCPQA_PMDA_SOCKETS") != NULL) +- fclose(fp); ++ if (using_pipe) ++ __pmProcessPipeClose(fp); + else +- pclose(fp); ++ fclose(fp); + } diff --git a/SOURCES/pcp-6.3.7-CVE-2026-16526.patch b/SOURCES/pcp-6.3.7-CVE-2026-16526.patch new file mode 100644 index 0000000..8eeb5b0 --- /dev/null +++ b/SOURCES/pcp-6.3.7-CVE-2026-16526.patch @@ -0,0 +1,219 @@ +From 8d418f7348a51ea8801e4725a78c8f85bcdf2f7d Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Thu, 2 Jul 2026 16:23:26 +1000 +Subject: [PATCH] libpcp, libpcp_pmda: set FD_CLOEXEC on AF_UNIX sockets + (CWE-403) + +The __pmInitSocket() function returns early for AF_UNIX sockets, +skipping all subsequent socket hardening including FD_CLOEXEC. +This causes the pmdaroot Unix socket fd to be inherited by child +processes spawned via popen()/fork(), enabling privilege escalation +when combined with the linux_sockets command injection (vuln 3): +an attacker's popen() child inherits the pmdaroot fd and can send +a PDUROOT_STARTPMDA_REQ to execute commands as root. + +Fix: +- Set FD_CLOEXEC on AF_UNIX sockets in __pmInitSocket() before the + early return, matching the behavior TCP sockets get via + __pmConnectRestoreFlags() +- Set FD_CLOEXEC on pmdarootfd in pmdaRootConnect() after connect() + succeeds, as belt-and-suspenders for this critical fd +- Add qa/src/check_cloexec.c and qa/2104 verifying FD_CLOEXEC is set + on sockets created by __pmCreateUnixSocket() + +Note: SO_PEERCRED peer credential verification on the pmdaroot server +side is a separate hardening measure to be addressed as a follow-up. + +Reported-by: Francisco Alisson Bezerra, TIM Security Red Team +Reported-by: Lucas Gabriel Alves, TIM Security Red Team +Reported-by: Massimiliano Brolli, TIM Security Red Team + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + qa/2104 | 31 ++++++++++++++++++++++++++++++ + qa/2104.out | 2 ++ + qa/group | 1 + + qa/src/GNUlocaldefs | 10 +++++++++- + qa/src/check_cloexec.c | 38 +++++++++++++++++++++++++++++++++++++ + src/libpcp/src/auxconnect.c | 6 +++++- + src/libpcp_pmda/src/root.c | 5 ++++- + 7 files changed, 90 insertions(+), 3 deletions(-) + create mode 100755 qa/2104 + create mode 100644 qa/2104.out + create mode 100644 qa/src/check_cloexec.c + +diff --git a/qa/2104 b/qa/2104 +new file mode 100755 +index 000000000..0b2b0b602 +--- /dev/null ++++ b/qa/2104 +@@ -0,0 +1,31 @@ ++#!/bin/sh ++# PCP QA Test No. 2104 ++# Verify AF_UNIX sockets have FD_CLOEXEC set (CWE-403 fix) ++# ++# Copyright (c) 2026 Red Hat. All Rights Reserved. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++# get standard environment, filters and checks ++. ./common.product ++. ./common.filter ++. ./common.check ++ ++[ -f src/check_cloexec ] || _notrun "check_cloexec not built" ++ ++_cleanup() ++{ ++ cd $here ++ $sudo rm -rf $tmp $tmp.* ++} ++ ++status=0 # success is the default! ++trap "_cleanup; exit \$status" 0 1 2 3 15 ++ ++# real QA test starts here ++src/check_cloexec ++ ++# success, all done ++exit +diff --git a/qa/2104.out b/qa/2104.out +new file mode 100644 +index 000000000..900a95ec7 +--- /dev/null ++++ b/qa/2104.out +@@ -0,0 +1,2 @@ ++QA output created by 2104 ++FD_CLOEXEC is set +diff --git a/qa/group b/qa/group +index d4da513ce..e87583d69 100644 +--- a/qa/group ++++ b/qa/group +@@ -2216,4 +2216,5 @@ pmcd.pdu + 1990 pcp buddyinfo python local + 1991 pcp netstat python local + 1992 pmda.uwsgi local ++2104 libpcp local security + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs +index f83826068..53f0d06c6 100644 +--- a/qa/src/GNUlocaldefs ++++ b/qa/src/GNUlocaldefs +@@ -54,7 +54,8 @@ CFILES = disk_test.c exercise.c context_test.c chkoptfetch.c \ + stampconv.c time_stamp.c archend.c scandata.c wait_for_values.c \ + dumpstack.c usergroup.c derived_help.c ready-or-not.c cleanmapdir.c \ + throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \ +- newcontext.c ++ newcontext.c check_cloexec.c ++ + + ifeq ($(shell test -f ../localconfig && echo 1), 1) + include ../localconfig +@@ -574,6 +575,11 @@ sortinst: sortinst.c + # --- need libpcp_import + # + ++check_cloexec: check_cloexec.c ++ rm -f $@ ++ $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS) ++ $(LINKER_MAKERULE) ++ + check_import: check_import.c + rm -f $@ + $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS) -lpcp_import +@@ -880,6 +886,8 @@ xmktime.o: libpcp.h + xxx.o: libpcp.h + y2038.o: libpcp.h + ++check_cloexec.o: libpcp.h ++ + bozo: + @echo CFILES_TARGETS=$(CFILES_TARGETS) + @echo "patsubst ->" $(patsubst %.c,%,$(CFILES_TARGETS)) +diff --git a/qa/src/check_cloexec.c b/qa/src/check_cloexec.c +new file mode 100644 +index 000000000..ebc438d30 +--- /dev/null ++++ b/qa/src/check_cloexec.c +@@ -0,0 +1,38 @@ ++/* ++ * Verify that AF_UNIX sockets created by __pmCreateUnixSocket() ++ * have FD_CLOEXEC set. ++ */ ++ ++#include ++#include "libpcp.h" ++#include ++ ++int ++main(int argc, char **argv) ++{ ++ int fd, flags; ++ ++ pmSetProgname(argv[0]); ++ ++ fd = __pmCreateUnixSocket(); ++ if (fd < 0) { ++ fprintf(stderr, "Error: __pmCreateUnixSocket failed: %s\n", ++ pmErrStr(fd)); ++ return 1; ++ } ++ ++ flags = fcntl(fd, F_GETFD); ++ if (flags < 0) { ++ fprintf(stderr, "Error: fcntl F_GETFD failed\n"); ++ close(fd); ++ return 1; ++ } ++ ++ if (flags & FD_CLOEXEC) ++ printf("FD_CLOEXEC is set\n"); ++ else ++ printf("FAIL: FD_CLOEXEC is NOT set\n"); ++ ++ close(fd); ++ return 0; ++} +diff --git a/src/libpcp/src/auxconnect.c b/src/libpcp/src/auxconnect.c +index 62f6f5451..67227e3c6 100644 +--- a/src/libpcp/src/auxconnect.c ++++ b/src/libpcp/src/auxconnect.c +@@ -516,8 +516,12 @@ __pmInitSocket(int fd, int family) + } + + #if defined(HAVE_STRUCT_SOCKADDR_UN) +- if (family == AF_UNIX) ++ if (family == AF_UNIX) { ++ int fdFlags; ++ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0) ++ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC); + return fd; ++ } + #endif + + /* Avoid 200 ms delay. This option is not supported for unix domain sockets. */ +diff --git a/src/libpcp_pmda/src/root.c b/src/libpcp_pmda/src/root.c +index 1d3223b57..3950e4f1e 100644 +--- a/src/libpcp_pmda/src/root.c ++++ b/src/libpcp_pmda/src/root.c +@@ -32,7 +32,7 @@ pmdaRootConnect(const char *path) + char *tmpdir; + char socketpath[MAXPATHLEN]; + char errmsg[PM_MAXERRMSGLEN]; +- int fd, sts, version, features; ++ int fd, sts, version, features, fdFlags; + + /* Initialize the socket address. */ + if ((addr = __pmSockAddrAlloc()) == NULL) +@@ -72,6 +72,9 @@ pmdaRootConnect(const char *path) + return sts; + } + ++ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0) ++ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC); ++ + /* Check server connection information */ + if ((sts = __pmdaRecvRootPDUInfo(fd, &version, &features)) < 0) { + pmNotifyErr(LOG_ERR, diff --git a/SOURCES/pcp-6.3.7-CVE-2026-16527.patch b/SOURCES/pcp-6.3.7-CVE-2026-16527.patch new file mode 100644 index 0000000..67c7e0e --- /dev/null +++ b/SOURCES/pcp-6.3.7-CVE-2026-16527.patch @@ -0,0 +1,206 @@ +From 9dc1484fcab68aa395beec5f13758c2fab4e6033 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Thu, 2 Jul 2026 15:28:34 +1000 +Subject: [PATCH] pmproxy: fix missing -Q and -S authentication flags (CWE-306) + +The pmproxy -Q (require client certificate) and -S (require +authenticated clients) flags existed as case blocks in the option +parser but were absent from the short_options string and the longopts +table, making them permanently unreachable. An unauthenticated HTTP +client could access all REST API endpoints including /store and /derive. + +Fix: +- Add Q and S to short_options so pmgetopt_r() delivers them +- Add --certreqd and --reqauth entries to the longopts table +- Document both flags in the pmproxy(1) man page +- Add qa/2100 verifying the flags are accepted and that -S correctly + rejects unauthenticated REST API requests with HTTP 403 + +Note: -S enforcement in the REST API path already exists in http.c and +webapi.c. -Q (CERT_REQD) enforcement is only implemented for the +legacy PCP wire protocol path, not the REST API; this is a pre-existing +limitation to be addressed separately. + +Reported-by: Francisco Alisson Bezerra, TIM Security Red Team +Reported-by: Lucas Gabriel Alves, TIM Security Red Team +Reported-by: Massimiliano Brolli, TIM Security Red Team + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + man/man1/pmproxy.1 | 6 +++ + qa/2100 | 85 +++++++++++++++++++++++++++++++++++++++ + qa/2100.out | 12 ++++++ + qa/group | 1 + + src/pmproxy/src/pmproxy.c | 4 +- + 5 files changed, 107 insertions(+), 1 deletion(-) + create mode 100755 qa/2100 + create mode 100644 qa/2100.out + +diff --git a/man/man1/pmproxy.1 b/man/man1/pmproxy.1 +index f2f831b6e..65d66c5d2 100644 +--- a/man/man1/pmproxy.1 ++++ b/man/man1/pmproxy.1 +@@ -229,6 +229,9 @@ Specify an alternate + number to listen on for client connections. + The default value is 44322. + .TP ++\fB\-Q\f1, \fB\-\-certreqd\f1 ++Require that all client connections provide a trusted client certificate. ++.TP + \f3\-r\f1 \f2port\f1, \f3\-\-keyport\f1=\f2port\f1 + Specify an alternate key-value server + .I port +@@ -243,6 +246,9 @@ The default value is + .IR $PCP_RUN_DIR/pmproxy.socket . + This option implies \f3pmproxy\f1 is running in \f3timeseries\f1 mode. + .TP ++\fB\-S\f1, \fB\-\-reqauth\f1 ++Require that all client connections be authenticated. ++.TP + \fB\-t\f1, \fB\-\-timeseries\f1 + Operate in automatic archive timeseries discovery mode. + This mode of operation will enable the +diff --git a/qa/2100 b/qa/2100 +new file mode 100755 +index 000000000..1de957a2f +--- /dev/null ++++ b/qa/2100 +@@ -0,0 +1,85 @@ ++#!/bin/sh ++# PCP QA Test No. 2100 ++# Verify pmproxy -Q and -S authentication flags are accepted ++# and that -S (reqauth) enforces authentication on REST API ++# ++# Copyright (c) 2026 Red Hat. All Rights Reserved. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++# get standard environment, filters and checks ++. ./common.product ++. ./common.filter ++. ./common.check ++ ++which curl >/dev/null 2>&1 || _notrun "no curl executable installed" ++ ++_cleanup() ++{ ++ [ -n "$__pid" ] && kill $__pid 2>/dev/null ++ wait $__pid 2>/dev/null ++ cd $here ++ $sudo rm -rf $tmp $tmp.* ++} ++ ++status=0 # success is the default! ++__pid="" ++trap "_cleanup; exit \$status" 0 1 2 3 15 ++ ++# real QA test starts here ++ ++echo "=== checking -Q and -S appear in usage ===" ++pmproxy --help 2>&1 | grep -E '\-[QS]' | sed -e 's/^ *//' ++ ++echo ++echo "=== checking -S enforces authentication on REST API ===" ++__port=`_find_free_port` ++$PCP_BINADM_DIR/pmproxy -S -f -p $__port -l $tmp.log & ++__pid=$! ++sleep 1 ++if kill -0 $__pid 2>/dev/null; then ++ echo "pmproxy with -S started" ++ ++ # unauthenticated request should be rejected ++ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null` ++ if [ "$__code" = "403" ]; then ++ echo "unauthenticated request correctly rejected (HTTP $__code)" ++ else ++ echo "FAIL: expected HTTP 403, got HTTP $__code" ++ fi ++ ++ kill $__pid ++ wait $__pid 2>/dev/null ++ __pid="" ++else ++ echo "FAIL: pmproxy with -S did not start" ++fi ++ ++echo ++echo "=== checking without -S allows unauthenticated access ===" ++__port=`_find_free_port` ++$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log2 & ++__pid=$! ++sleep 1 ++if kill -0 $__pid 2>/dev/null; then ++ echo "pmproxy without -S started" ++ ++ # unauthenticated request should succeed ++ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null` ++ if [ "$__code" = "200" ]; then ++ echo "unauthenticated request correctly allowed (HTTP $__code)" ++ else ++ echo "FAIL: expected HTTP 200, got HTTP $__code" ++ fi ++ ++ kill $__pid ++ wait $__pid 2>/dev/null ++ __pid="" ++else ++ echo "FAIL: pmproxy without -S did not start" ++fi ++ ++# success, all done ++exit +diff --git a/qa/2100.out b/qa/2100.out +new file mode 100644 +index 000000000..5302aea77 +--- /dev/null ++++ b/qa/2100.out +@@ -0,0 +1,12 @@ ++QA output created by 2100 ++=== checking -Q and -S appear in usage === ++-Q, --certreqd require client certificate authentication ++-S, --reqauth require all client connections to be authenticated ++ ++=== checking -S enforces authentication on REST API === ++pmproxy with -S started ++unauthenticated request correctly rejected (HTTP 403) ++ ++=== checking without -S allows unauthenticated access === ++pmproxy without -S started ++unauthenticated request correctly allowed (HTTP 200) +diff --git a/qa/group b/qa/group +index 76430211f..a1b2c3d4e 100644 +--- a/qa/group ++++ b/qa/group +@@ -2216,6 +2216,7 @@ pmcd.pdu + 1990 pcp buddyinfo python local + 1991 pcp netstat python local + 1992 pmda.uwsgi local ++2100 pmproxy local security + 2104 libpcp local security + 2105 libpcp pmcd local security pmcd.pdu + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/src/pmproxy/src/pmproxy.c b/src/pmproxy/src/pmproxy.c +index 845479c46..ff18396bf 100644 +--- a/src/pmproxy/src/pmproxy.c ++++ b/src/pmproxy/src/pmproxy.c +@@ -82,7 +82,9 @@ static pmLongOptions longopts[] = { + PMAPI_OPTIONS_HEADER("Connection options"), + { "interface", 1, 'i', "ADDR", "accept connections on this IP address" }, + { "port", 1, 'p', "PORT", "accept connections on this port" }, ++ { "certreqd", 0, 'Q', 0, "require client certificate authentication" }, + { "socket", 1, 's', "PATH", "Unix domain socket file [default $PCP_RUN_DIR/pmproxy.socket]" }, ++ { "reqauth", 0, 'S', 0, "require all client connections to be authenticated" }, + { "keyport", 1, 'r', "PORT", "Connect to key server on this TCP/IP port (implies --timeseries)" }, + { "keyhost", 1, 'h', "HOST", "Connect to key server on this host name (implies --timeseries)" }, + { "redisport", 1, 'r', "PORT", "Backwards-compatibility option, do not use" }, +@@ -95,7 +97,7 @@ static pmLongOptions longopts[] = { + }; + + static pmOptions opts = { +- .short_options = "Ac:dD:Ffh:i:l:L:p:r:s:tT:U:x:?", ++ .short_options = "Ac:dD:Ffh:i:l:L:p:Qr:s:StT:U:x:?", + .long_options = longopts, + }; + diff --git a/SOURCES/pcp-6.3.7-CVE-2026-16529.patch b/SOURCES/pcp-6.3.7-CVE-2026-16529.patch new file mode 100644 index 0000000..2f839a6 --- /dev/null +++ b/SOURCES/pcp-6.3.7-CVE-2026-16529.patch @@ -0,0 +1,137 @@ +From b3f2265227ff5fd32cdd1de99a143ea79214806b Mon Sep 17 00:00:00 2001 +From: Nathan Scott +Date: Thu, 2 Jul 2026 16:59:54 +1000 +Subject: [PATCH] libpcp: fix integer overflow in __pmGetPDU() (CWE-190) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When php->len is near INT_MAX (e.g. 0x7FFFFFFF), the buffer size +computation PDU_CHUNK * (1 + php->len / PDU_CHUNK) overflows signed +int, producing a negative value that permanently corrupts the static +maxsize variable. Every subsequent __pmFindPDUBuf() call returns NULL, +rendering the affected daemon (pmlogger, pmcd) unable to process any +further PDUs for the remainder of its lifetime — a persistent denial +of service requiring a restart. + +Fix: add an overflow guard (php->len > INT_MAX - PDU_CHUNK) before +the multiplication, returning PM_ERR_TOOBIG for absurdly large PDU +lengths. This protects the NO_LIMIT code path used by pmcd and +pmlogger that is not covered by the existing ceiling check. + +Also add _filter_pmcd() to qa/common.pmcd.pdu to normalize fd=N in +pmcd log output, and qa/2105 with a crafted PDU exercising the +overflow. + +Reported-by: Francisco Alisson Bezerra, TIM Security Red Team +Reported-by: Lucas Gabriel Alves, TIM Security Red Team +Reported-by: Massimiliano Brolli, TIM Security Red Team + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + qa/2105 | 17 +++++++++++++++++ + qa/2105.out | 10 ++++++++++ + qa/common.pmcd.pdu | 10 +++++++++- + qa/group | 1 + + src/libpcp/src/pdu.c | 8 ++++++++ + 5 files changed, 45 insertions(+), 1 deletion(-) + create mode 100755 qa/2105 + create mode 100644 qa/2105.out + +diff --git a/qa/2105 b/qa/2105 +new file mode 100755 +index 000000000..44b849ad3 +--- /dev/null ++++ b/qa/2105 +@@ -0,0 +1,17 @@ ++#!/bin/sh ++# PCP QA Test No. 2105 ++# Verify __pmGetPDU rejects PDU with len near INT_MAX ++# (integer overflow in buffer size computation, CWE-190) ++# ++# Copyright (c) 2026 Red Hat. All Rights Reserved. ++# ++ ++seq=`basename $0` ++echo "QA output created by $seq" ++ ++pdu_data=pdudata/pdu-getpdu-overflow ++grep_pattern="bad PDU len=.*exceeds maximum|PDU len=.*too large" ++ ++# this is one of the generic pmcd PDU exerciser tests ... ++# ++. ./common.pmcd.pdu +diff --git a/qa/2105.out b/qa/2105.out +new file mode 100644 +index 000000000..a304f9d91 +--- /dev/null ++++ b/qa/2105.out +@@ -0,0 +1,10 @@ ++QA output created by 2105 ++expect error(s) to be logged ... ++__pmGetPDU: fd=N type=0x8000 bad PDU len=2147483647 in hdr exceeds maximum client PDU size (65536) ++ ++and no valgrind badness ... ++Memcheck, a memory error detector ++LEAK SUMMARY: ++definitely lost: 0 bytes in 0 blocks ++indirectly lost: 0 bytes in 0 blocks ++ERROR SUMMARY: 0 errors from 0 contexts ... +diff --git a/qa/common.pmcd.pdu b/qa/common.pmcd.pdu +index d9e3c6c5c..9882284b5 100644 +--- a/qa/common.pmcd.pdu ++++ b/qa/common.pmcd.pdu +@@ -55,6 +55,14 @@ _filter() + # end + } + ++_filter_pmcd() ++{ ++ sed \ ++ -e 's/fd=[0-9][0-9]*/fd=N/g' \ ++ -e 's/^\[.*\] pmcd([0-9]*) [A-Za-z]*: //' \ ++ # end ++} ++ + mkdir $tmp || exit 1 + cd $tmp + grep sampledso $PCP_PMCDCONF_PATH >pmcd.conf +@@ -91,7 +99,7 @@ wait + [ -s $tmp.err ] && cat $tmp.err + + echo "expect error(s) to be logged ..." +-grep -E "$grep_pattern" pmcd.log ++grep -E "$grep_pattern" pmcd.log | _filter_pmcd + + echo + echo "and no valgrind badness ..." +diff --git a/qa/group b/qa/group +index e87583d69..76430211f 100644 +--- a/qa/group ++++ b/qa/group +@@ -2216,5 +2216,6 @@ pmcd.pdu + 1990 pcp buddyinfo python local + 1991 pcp netstat python local + 1992 pmda.uwsgi local + 2104 libpcp local security ++2105 libpcp pmcd local security pmcd.pdu + 4751 libpcp threads valgrind local pcp helgrind +diff --git a/src/libpcp/src/pdu.c b/src/libpcp/src/pdu.c +index 5845932be..0a4ae75b2 100644 +--- a/src/libpcp/src/pdu.c ++++ b/src/libpcp/src/pdu.c +@@ -658,6 +658,14 @@ check_read_len: + + PM_LOCK(pdu_lock); + if (php->len > maxsize) { ++ if (php->len > INT_MAX - PDU_CHUNK) { ++ PM_UNLOCK(pdu_lock); ++ if (pmDebugOptions.pdu) ++ pmNotifyErr(LOG_ERR, "%s: fd=%d PDU len=%d too large", ++ __FUNCTION__, fd, php->len); ++ __pmUnpinPDUBuf(pdubuf); ++ return PM_ERR_TOOBIG; ++ } + tmpsize = PDU_CHUNK * ( 1 + php->len / PDU_CHUNK); + maxsize = tmpsize; + } diff --git a/SPECS/pcp.spec b/SPECS/pcp.spec index 0999714..fcb4db4 100644 --- a/SPECS/pcp.spec +++ b/SPECS/pcp.spec @@ -1,6 +1,6 @@ Name: pcp Version: 6.3.7 -Release: 8%{?dist} +Release: 8.0.1%{?dist}.4 Summary: System-level performance monitoring and performance management License: GPL-2.0-or-later AND LGPL-2.1-or-later AND CC-BY-3.0 URL: https://pcp.io @@ -25,6 +25,39 @@ Patch13: atop-cpu-utilization.patch Patch14: pmda-openmetrics-performance.patch Patch15: pcp-RHEL-133548.patch Patch16: memory-leaks.patch +# https://issues.redhat.com/browse/RHEL-213688 +# https://github.com/performancecopilot/pcp/commit/7e27614006ff6fc4925991edbedaf1eab6b14731 +Patch17: pcp-6.3.7-CVE-2026-16526.patch +# https://github.com/performancecopilot/pcp/commit/ef848fb978d26335f9676933f541c73b58f130a6 +Patch18: pcp-6.3.7-CVE-2026-16529.patch +# https://issues.redhat.com/browse/RHEL-213712 +# https://github.com/performancecopilot/pcp/commit/d96ba5a716eeff7840138eb08fbab0d11a57f641 +Patch19: pcp-6.3.7-CVE-2026-16527.patch +# https://issues.redhat.com/browse/RHEL-213668 +# https://github.com/performancecopilot/pcp/commit/c5cbeceb7d3c2af357c04065cdd911efdc270de0 +Patch20: pcp-6.3.7-CVE-2026-16524.patch + +# Oracle Patches +# rocestat-pmda +Patch1001: 1001-Merge-branch-mohith-kumar-thummaluru-rocestat-pmda.patch +# pcp-rocestat client +Patch1002: 1002-Merge-branch-pcp-rocestat-of-https-github.com-mohith.patch +Patch1003: 1003-pcp-meminfo-additional-metrics-added-for-mem.util-to.patch +Patch1004: 1004-meminfo-added-kreclaimable-and-hugtlb-metrics.patch +Patch1005: 1005-fix-mpstat-showing-inconsistent-values.patch +Patch1006: 1006-fix-broken-pipe-error-iostat.patch +Patch1007: 1007-add-missing-numastat-metrics.patch +Patch1008: 1008-add-numastat-support-for-mn-options.patch +Patch1009: 1009-Fixes-higepagesize-metric-value-from-bytes-to-KB.patch +Patch1010: 1010-xz-default-compression-changed-to-level-3.patch +Patch1011: 1011-orabug38724866-fix-nfsclient-per-op-parsing.patch +Patch1012: 1012-orabug38817068-Introduce-PCP-implementation-of-nfsiostat.patch +Patch1013: 1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch +Patch1014: 1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch +Patch1015: 1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch +Patch1016: 1016-pcp-system-tools-restore-backward-compatibility-with.patch +Patch1017: 1017-orabug39068870-adds-interval-option-in-nfsiostat.patch +Patch1018: 1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch %if 0%{?fedora} >= 40 || 0%{?rhel} >= 10 ExcludeArch: %{ix86} @@ -627,7 +660,7 @@ Requires: pcp-export-pcp2spark pcp-export-pcp2xml pcp-export-pcp2zabbix Requires: pcp-pmda-gluster pcp-pmda-zswap pcp-pmda-unbound pcp-pmda-mic Requires: pcp-pmda-libvirt pcp-pmda-lio pcp-pmda-openmetrics pcp-pmda-haproxy Requires: pcp-pmda-lmsensors pcp-pmda-netcheck pcp-pmda-rabbitmq pcp-pmda-uwsgi -Requires: pcp-pmda-openvswitch +Requires: pcp-pmda-openvswitch pcp-pmda-rocestat %endif %if !%{disable_mongodb} Requires: pcp-pmda-mongodb @@ -1659,6 +1692,24 @@ This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about the gluster filesystem. # end pcp-pmda-gluster +# +# pcp-pmda-rocestat +# +%package pmda-rocestat +License: GPLv2+ +Summary: Performance Co-Pilot (PCP) metrics for the reporting Nvidia RoCE device metrics +URL: https://pcp.io +Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} +%if !%{disable_python3} +Requires: python3-pcp +%else +Requires: %{__python2}-pcp +%endif +%description pmda-rocestat +This package contains the PCP Performance Metrics Domain Agent (PMDA) for +collecting metrics about the Nvidia RoCE device metrics. +# end pcp-pmda-rocestat + # # pcp-pmda-nfsclient # @@ -2506,6 +2557,9 @@ updated policy package. %prep %autosetup -p1 +# Create binary QA test data for CVE-2026-16529 overflow test (8 bytes) +/usr/bin/printf '\x7f\xff\xff\xff\x00\x00\x80\x00' > qa/pdudata/pdu-getpdu-overflow + %build # the buildsubdir macro gets defined in %%setup and is apparently only available in the next step (i.e. the %%build step) %global __strip %{_builddir}/%{?buildsubdir}/build/rpm/custom-strip @@ -2737,6 +2791,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-fil basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files +basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files basic_manifest | keep '(etc/pcp|pmdas)/roomtemp(/|$)' >pcp-pmda-roomtemp-files basic_manifest | keep '(etc/pcp|pmdas)/rpm(/|$)' >pcp-pmda-rpm-files basic_manifest | keep '(etc/pcp|pmdas)/rsyslog(/|$)' >pcp-pmda-rsyslog-files @@ -2775,7 +2830,7 @@ for pmda_package in \ nutcracker nvidia \ openmetrics openvswitch oracle \ pdns perfevent podman postfix postgresql \ - rabbitmq redis resctrl roomtemp rpm rsyslog \ + rabbitmq redis resctrl rocestat roomtemp rpm rsyslog \ samba sendmail shping slurm smart snmp \ sockets statsd summary systemd \ unbound uwsgi \ @@ -3121,6 +3176,9 @@ exit 0 %preun pmda-gluster %{pmda_remove "$1" "gluster"} +%preun pmda-rocestat +%{pmda_remove "$1" "rocestat"} + %preun pmda-zswap %{pmda_remove "$1" "zswap"} @@ -3278,6 +3336,26 @@ done %endif %endif +%post pmda-rocestat +PCP_PMDAS_DIR=%{_pmdasdir} +PCP_SYSCONFIG_DIR=%{_sysconfdir}/sysconfig +PCP_PMCDCONF_PATH=%{_confdir}/pmcd/pmcd.conf + +# Auto-install rocestat PMDA if not already in pmcd.conf +if ! grep -q "rocestat/pmdarocestat" "$PCP_PMCDCONF_PATH"; then + if [ ! -d /sys/class/infiniband ]; then + if ! lsmod | grep -q '^ib_core'; then + echo "Skipping install for PMDA Rocestat (IB kernel modules are not loaded)" >&2 + else + echo "Skipping install for PMDA Rocestat (No IB devices detected)" >&2 + fi + else + cd "$PCP_PMDAS_DIR/rocestat" && \ + chmod +x Install && \ + ./Install < /dev/null + fi +fi + %post PCP_PMNS_DIR=%{_pmnsdir} PCP_LOG_DIR=%{_logsdir} @@ -3474,6 +3552,8 @@ fi %files pmda-gluster -f pcp-pmda-gluster-files.rpm +%files pmda-rocestat -f pcp-pmda-rocestat-files.rpm + %files pmda-zswap -f pcp-pmda-zswap-files.rpm %files pmda-unbound -f pcp-pmda-unbound-files.rpm @@ -3635,6 +3715,37 @@ fi %files zeroconf -f pcp-zeroconf-files.rpm %changelog +* Mon Aug 17 2026 EL Errata - 6.3.7-8.0.1.el9_8.4 +- Adds interval option support in PCP nfsiostat tool [Orabug: 39068870] +- Introduces proc.numa_maps metrics in linux_proc PMDA [Orabug: 39096683] +- adds numastat process option in the tool +- Added support for size based cleanup for pcp archives [Orabug: 38757778] +- Implement sorting option in pcp ps based on %cpu, %mem [Orabug: 38719615] +- Fixed pmlogger incorrectly attempts to terminate +- unauthorized process [Orabug: 38598244] +- Merges new PCP nfsiostat parser in OL [Orabug: 38817068] +- pmdanfsclient: fix regex to correctly parse NFS op stats [Orabug: 38724866] +- pmda/rocestat: skip installation when IB is absent [Orabug: 38742679] +- pmlogger_daily default xz compression changed to level 3 [Orabug: 38674828] +- Backports various pcp bugs and enhancements [Orabug: 38526328] +- [Orabug: 38526351] [Orabug: 38526381] [Orabug: 38526381] +- [Orabug: 38526450] [Orabug: 38527066] +- Add support for rocestat pmda [Orabug: 38109263] +- Add support for rocestat client [Orabug: 38109331] + +* Thu Jul 30 2026 RHEL Packaging Agent - 6.3.7-8.4 +- Fix CVE-2026-16524 command injection in linux_sockets PMDA (RHEL-213668) + +* Thu Jul 30 2026 RHEL Packaging Agent - 6.3.7-8.3 +- Fix missing pmproxy -Q and -S authentication flags (CVE-2026-16527, + RHEL-213712) + +* Thu Jul 30 2026 RHEL Packaging Agent - 6.3.7-8.2 +- Fix integer overflow in __pmGetPDU() (CVE-2026-16529, RHEL-213737) + +* Thu Jul 30 2026 RHEL Packaging Agent - 6.3.7-8.1 +- Fix CVE-2026-16526: set FD_CLOEXEC on AF_UNIX sockets (RHEL-213688) + * Thu Mar 5 2026 Jan Kurik - 6.3.7-8 - Backported memory leaks patches from pcp-7.0.3