diff --git a/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch b/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch deleted file mode 100644 index 3303ad1..0000000 --- a/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch +++ /dev/null @@ -1,38 +0,0 @@ -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: 38724847 - -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/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch b/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch deleted file mode 100644 index 0db3d2c..0000000 --- a/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch +++ /dev/null @@ -1,575 +0,0 @@ -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: 38817053 - -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/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch b/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch deleted file mode 100644 index 67ed7ae..0000000 --- a/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch +++ /dev/null @@ -1,178 +0,0 @@ -From 7bc4a59bfe06f9b988ea21ad6a2decc86c942097 Mon Sep 17 00:00:00 2001 -From: Sourav Sharma -Date: Thu, 26 Mar 2026 14:16:48 +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 | 27 +++++++++++--- - src/pmlogger/pmlogger_farm.defaults | 8 ---- - src/pmlogger/pmlogger_janitor.sh | 57 ++++++++++++++++++++++++++++- - 3 files changed, 76 insertions(+), 16 deletions(-) - -diff --git a/src/pmlogger/pmlogger_check.sh b/src/pmlogger/pmlogger_check.sh -index c941df0..27a0a1e 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 - # -@@ -703,7 +700,7 @@ _callback_log_control() - 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 -@@ -721,7 +718,7 @@ _callback_log_control() - return - 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 -@@ -896,10 +893,28 @@ 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 - $VERY_VERBOSE && echo "Running: pmlogger_janitor $daily_args" -- $PCP_BINADM_DIR/pmlogger_janitor $daily_args -+ args="$daily_args" -+ $PCP_BINADM_DIR/pmlogger_janitor $args - fi - - if [ -f $tmp/err ] -diff --git a/src/pmlogger/pmlogger_farm.defaults b/src/pmlogger/pmlogger_farm.defaults -index 10a0132..af06446 100644 ---- a/src/pmlogger/pmlogger_farm.defaults -+++ b/src/pmlogger/pmlogger_farm.defaults -@@ -25,11 +25,3 @@ - # setting PMLOGGER_CHECK_SKIP_LOGCONF to yes disables the regeneration - # 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 bc0360a..7c8ec0c 100755 ---- a/src/pmlogger/pmlogger_janitor.sh -+++ b/src/pmlogger/pmlogger_janitor.sh -@@ -348,8 +348,15 @@ _callback_log_control() - - if [ -n "$pid" ] - then -- # found matching pmlogger ... cull this one from -- $VERY_VERBOSE && echo "[$filename:$line] match PID $pid, nothing to be done" -+ # found matching pmlogger ... cull this one from $tmp/loggers -+ # -+ if $VERY_VERBOSE -+ then -+ echo "[$filename:$line] match PID $pid, nothing to be done" -+ elif $VERBOSE -+ then -+ echo "Pass 3: PID $pid matches control [$filename:$line], nothing to be done" -+ fi - sed <$tmp/loggers >$tmp/tmp -e "/^$pid /d" - mv $tmp/tmp $tmp/loggers - fi -@@ -365,6 +372,52 @@ 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 ... - # --- -2.43.7 - diff --git a/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch b/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch deleted file mode 100644 index 978e16e..0000000 --- a/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch +++ /dev/null @@ -1,1277 +0,0 @@ -From abfdd79d85c170ec896594f210c69667a2b2a7ae Mon Sep 17 00:00:00 2001 -From: Sourav Sharma -Date: Thu, 26 Mar 2026 15:36:00 +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 | 17 + - src/pcp/ps/pcp-ps.py | 926 +++++++++++++++++++++++++------------------ - 2 files changed, 566 insertions(+), 377 deletions(-) - -diff --git a/src/pcp/ps/pcp-ps.1 b/src/pcp/ps/pcp-ps.1 -index b824ebd..be2eb9c 100755 ---- a/src/pcp/ps/pcp-ps.1 -+++ b/src/pcp/ps/pcp-ps.1 -@@ -27,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] -@@ -243,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 987c09c..e2992ea 100755 ---- a/src/pcp/ps/pcp-ps.py -+++ b/src/pcp/ps/pcp-ps.py -@@ -60,106 +60,87 @@ 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 -+ def filter_processes(self, processes): -+ """Yield processes matching all active predicates.""" -+ return (proc for proc in processes if all(pred(proc) for pred in self._predicates)) - - - class ProcessStatusUtil: -@@ -169,106 +150,89 @@ 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 +240,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 +255,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 +264,30 @@ 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 +296,30 @@ 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,8 +327,8 @@ 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), -- "args": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args), -+ "cmd": ("Command\t\t", ProcessStatusUtil.process_name), -+ "args_last": ("Command", ProcessStatusUtil.process_name_with_args_last), - "args_last": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args_last), - "pid": ("PID\t", ProcessStatusUtil.pid), - "ppid": ("PPID\t", ProcessStatusUtil.ppid), -@@ -362,7 +336,7 @@ PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), - "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 +350,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,66 +371,113 @@ 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 -+ 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}" -+ # ) - -- 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)) - 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: - def __init__(self, process_report, process_filter, delta_time, printer, processStatOptions): -@@ -467,87 +488,105 @@ 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 -@@ -573,70 +612,88 @@ 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 +704,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 +719,220 @@ 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 +948,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 +956,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/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch b/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch deleted file mode 100644 index 2ee15ec..0000000 --- a/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch +++ /dev/null @@ -1,604 +0,0 @@ -From 09f39fb28a3171300afa5ed15e7f75ac0fbfdabc Mon Sep 17 00:00:00 2001 -From: Sourav Sharma -Date: Thu, 26 Mar 2026 16:10:37 +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 | 85 +++++++++++++++- - src/pmlogger/pmlogger_daily.sh | 176 +++++++++++++++++++++++++++++++-- - src/pmlogger/utilproc.sh | 142 +++++++++++++++++++++++++- - 3 files changed, 391 insertions(+), 12 deletions(-) - -diff --git a/man/man1/pmlogger_daily.1 b/man/man1/pmlogger_daily.1 -index c7b30ae..0371f63 100644 ---- a/man/man1/pmlogger_daily.1 -+++ b/man/man1/pmlogger_daily.1 -@@ -20,6 +20,7 @@ - .B $PCP_BINADM_DIR/pmlogger_daily - [\f3\-DEfKMNnoPpQrRVzZ?\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 -@@ -524,7 +591,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 - .BR pmlogger (1) - processes under the control of -@@ -796,7 +863,7 @@ may appear literally in - and will be substituted at execution time to generate the destination - directory name. For example: - .ft CR --.in +6n -+.in +2n - $PCP_AUTOSAVE_DIR=/gpfs/LOCALHOSTNAME/DATEYYYY/DATEMM-DATEDD - .br - .PP -@@ -957,6 +1024,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 3ebd17c..be915d6 100755 ---- a/src/pmlogger/pmlogger_daily.sh -+++ b/src/pmlogger/pmlogger_daily.sh -@@ -37,6 +37,7 @@ prog=`basename $0` - PROGLOG=$PCP_LOG_DIR/pmlogger/$prog.log - MYPROGLOG=$PROGLOG.$$ - USE_SYSLOG=true -+localhost=`hostname || echo localhost` - - # optional begin logging to $PCP_LOG_DIR/NOTICES - # -@@ -353,6 +354,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 -@@ -404,6 +406,8 @@ DO_DAILY_REPORT=true - NOPROXY=false - PROXYONLY=false - NOERROR=false -+SPACELIMIT_CMDLINE="" -+SPACELIMIT_DEFAULT="unlimited" - - ARGS=`pmgetopt --progname=$prog --config=$tmp/usage -- "$@"` - [ $? != 0 ] && exit 1 -@@ -419,6 +423,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 -@@ -781,10 +803,49 @@ fi - if [ ! -f "$CONTROL" ] - then - echo "$prog: Error: cannot find control file ($CONTROL)" -+ echo "... I am here ... `pwd` ... and these files are here ..." -+ ls -l - $NOERROR || status=1 - exit - fi - -+# 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: $@" -@@ -1062,15 +1123,93 @@ BEGIN { seenslash = 0; inshell = 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 -+} -+ - # come here from _parse_log_control() once per valid line in a control - # file ... see utilproc.sh for interface definitions - # - _callback_log_control() - { -- # nothing to do for pmlogger pushing to a remote pmproxy -- # -- $logpush && return -- - if $VERBOSE - then - echo -@@ -1130,6 +1269,10 @@ _callback_log_control() - find_dirs=`echo $orig_dir | _unbackquote` - $VERBOSE && echo "Embedded \`...\`: find_dirs=$find_dirs" - fi -+ # and rewrite LOCALHOSTNAME if it is included -+ # -+ find_dirs=`echo "$find_dirs" | sed -e "s;LOCALHOSTNAME;$localhost;g"` -+ $VERY_VERBOSE && echo "Info: find_dirs=$find_dirs" - - # For archive rewriting (to make metadata consistent across - # archives) find the rules as follows: -@@ -1424,6 +1567,14 @@ _callback_log_control() - $VERY_VERBOSE && echo >&2 "Warning: no trace files found to cull" - 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 - } - - # Paranoid archive saving -@@ -1779,7 +1930,10 @@ _do_compress() - do - # pmlc may race with pmlogger starting up here - a timeout is required - # to avoid pmlc blocking forever and hanging pmlogger_daily. RHBZ#1892326 -- [ -z "$PMLOGGER_REQUEST_TIMEOUT" ] && export PMLOGGER_REQUEST_TIMEOUT=2 -+ if [ -z "$PMLOGGER_REQUEST_TIMEOUT" ] -+ then -+ PMLOGGER_REQUEST_TIMEOUT=2; export PMLOGGER_REQUEST_TIMEOUT -+ fi - if pmlc "$pid" &1 | tee $tmp/out \ - | grep "^Connected to .*pmlogger" >/dev/null - then -@@ -2017,6 +2171,7 @@ else - # - if cd "$PCP_REMOTE_ARCHIVE_DIR" - then -+ $VERY_VERBOSE && echo "Info: do_compress: cd to $PCP_REMOTE_ARCHIVE_DIR OK" - # one-trip guard if there is something to be done - # - rm -f $tmp/proxy_sighup -@@ -2077,7 +2232,16 @@ else - _parse_log_control $tmp/control - fi - done -- cd $here -+ if cd $here -+ then -+ $VERY_VERBOSE && echo "Info: do_compress: cd back to $here OK" -+ else -+ echo >&2 "Error: do_compress: failed to cd back to $here" -+ $NOERROR || status=1 -+ fi -+ else -+ echo >&2 "Error: do_compress: faild to cd to $PCP_REMOTE_ARCHIVE_DIR" -+ $NOERROR || status=1 - fi - fi - -diff --git a/src/pmlogger/utilproc.sh b/src/pmlogger/utilproc.sh -index ae9c72f..9e5582c 100644 ---- a/src/pmlogger/utilproc.sh -+++ b/src/pmlogger/utilproc.sh -@@ -256,6 +256,7 @@ BEGIN { i = 0 } - [ $? -eq 0 ] && _PWDCMD="$_PWDCMD -P" - fi - _here=`$_PWDCMD` -+ $VERY_VERBOSE && echo >&2 "_parse_log_control: initial pwd=$_here" - - if echo "$1" | grep -q -e '\.rpmsave$' -e '\.rpmnew$' -e '\.rpmorig$' -e '\.dpkg-dist$' -e '\.dpkg-old$' -e '\.dpkg-new$' - then -@@ -270,7 +271,10 @@ BEGIN { i = 0 } - | while read host primary socks dir args - do - # start in one place for each iteration (beware of relative paths) -- cd "$_here" -+ if ! cd "$_here" -+ then -+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here" -+ fi - line=`expr $line + 1` - - if $VERY_VERBOSE -@@ -350,6 +354,51 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p - 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 -+ ;; -+ - 'export PCP_COMPRESS;'*) - _old_value="$PCP_COMPRESS" - $SHOWME && echo "+ $_cmd" -@@ -537,10 +586,8 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p - # check $dir, cd there, acquire lock - # - $SHOWME && echo "+ cd $dir" -- if cd "$dir" -+ if ! cd "$dir" - then -- : -- else - if $SHOWME - then - echo "+ ... cannot show any more for this control line" -@@ -659,6 +706,10 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p - fi - - done -+ if ! cd "$_here" -+ then -+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here at return" -+ fi - } - - # Called from _callback_log_control() [in pmlogger_check and pmlogger_janitor] -@@ -859,6 +910,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/1016-pcp-system-tools-restore-backward-compatibility-with.patch b/1016-pcp-system-tools-restore-backward-compatibility-with.patch deleted file mode 100644 index 1f14993..0000000 --- a/1016-pcp-system-tools-restore-backward-compatibility-with.patch +++ /dev/null @@ -1,332 +0,0 @@ -From a6bb22bf407522ef95557de0cebde717de347bde Mon Sep 17 00:00:00 2001 -From: Sourav Sharma -Date: Thu, 26 Mar 2026 16:31:22 +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 | 125 +++++++++++++------------------ - src/pcp/tapestat/pcp-tapestat.py | 16 +++- - 5 files changed, 97 insertions(+), 85 deletions(-) - -diff --git a/src/pcp/iostat/pcp-iostat.py b/src/pcp/iostat/pcp-iostat.py -index fd049a6..79d8c9b 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 -- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec -- # n may be negative here, calculation is still correct. -- 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(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 2b0bd59..4e0cdf6 100755 ---- a/src/pcp/mpstat/pcp-mpstat.py -+++ b/src/pcp/mpstat/pcp-mpstat.py -@@ -499,8 +499,17 @@ class MpstatReport(pmcc.MetricGroupPrinter): - - def timeStampDelta(self, group): - s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec -- n = group.timestamp.tv_nsec - 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(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 32bf925..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 -- n = group.timestamp.tv_nsec - 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(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 e2992ea..e5abf34 100755 ---- a/src/pcp/ps/pcp-ps.py -+++ b/src/pcp/ps/pcp-ps.py -@@ -252,7 +252,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) -@@ -366,6 +366,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 -@@ -378,19 +392,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( -@@ -400,48 +410,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: -@@ -457,23 +427,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) -@@ -536,25 +497,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" % ( -@@ -590,8 +551,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) -@@ -868,6 +838,11 @@ 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 dd54c70..ac76c68 100755 ---- a/src/pcp/tapestat/pcp-tapestat.py -+++ b/src/pcp/tapestat/pcp-tapestat.py -@@ -68,9 +68,19 @@ class TapestatReport(pmcc.MetricGroupPrinter): - Hcount = 0 - def timeStampDelta(self, group): - s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec -- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec -- # n may be negative here, calculation is still correct. -- 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(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/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch b/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch deleted file mode 100644 index eb311f2..0000000 --- a/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch +++ /dev/null @@ -1,364 +0,0 @@ -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/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch b/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch deleted file mode 100644 index 1e11c0b..0000000 --- a/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch +++ /dev/null @@ -1,1287 +0,0 @@ -From 8d05c8d7756fbe50ee87aa433626ddb88e8a0a70 Mon Sep 17 00:00:00 2001 -From: Sourav Sharma -Date: Thu, 26 Mar 2026 13:16:48 +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 d7f0304..efa9560 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\-mnV?\fR] [\fB\-w\fR \fIwidth\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 -@@ -68,6 +68,24 @@ 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\-V\fR, \fB\-\-version\fR - Display the current version of the command. - .TP -@@ -81,7 +99,8 @@ Display usage message and exit. - .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 80d9653..0083419 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 b817d97..1c6f281 100644 ---- a/src/pmdas/linux_proc/pmdaproc.1 -+++ b/src/pmdas/linux_proc/pmdaproc.1 -@@ -35,6 +35,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 acb046c..0f8ea7d 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/pcp.spec b/pcp.spec index 6d98656..506fbab 100644 --- a/pcp.spec +++ b/pcp.spec @@ -1,6 +1,6 @@ Name: pcp Version: 7.0.3 -Release: 5.0.1%{?dist} +Release: 5%{?dist} 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 @@ -29,14 +29,6 @@ Patch18: pcp-7.0.3-pmproxy-rest-certreqd.patch Patch19: pcp-7.0.3-pmproxy-logger-auth.patch Patch20: pcp-7.0.3-pmproxy-logger-meta-network.patch Patch21: pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch -Patch1011: 1011-orabug38724847-fix-nfsclient-per-op-parsing.patch -Patch1012: 1012-orabug38817053-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} @@ -3091,26 +3083,6 @@ 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} @@ -3475,20 +3447,6 @@ fi %files zeroconf -f pcp-zeroconf-files.rpm %changelog -* Mon Aug 17 2026 EL Errata - 7.0.3-5.0.1 -- Fix unterminated backtick in pmlogger_janitor fix not to terminate unauthorized process patch - [Orabug: 39270658] -- 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: 38817053] -- pmdanfsclient: fix regex to correctly parse NFS op stats [Orabug: 38724847] -- pmda/rocestat: pmda/rocestat: skip installation when IB is absent [Orabug: 38595797] - * Fri Aug 14 2026 Jan Kuřík - 7.0.3-5 - Fix CVE-2026-16530: __pmLogLoadInDom OOB pointer dereference (RHEL-213746) - Fix CVE-2026-16531: pmproxy logger servlet path traversal (RHEL-213756)