smem2.smem2

   1import os
   2import pwd
   3import re
   4import sys
   5from dataclasses import dataclass, field
   6from typing import Set
   7
   8
   9@dataclass
  10class SmemConfig:
  11    """Stores the configuration for smem2.
  12
  13    This dataclass holds all the configuration options for smem2,
  14    mostly populated from command-line arguments.
  15    """
  16
  17    # from parse_arguments
  18    no_header: bool = False
  19    columns: str = ""
  20    autosize: bool = False
  21    realmem: str | None = None
  22    kernel: str | None = None
  23    basename: bool = False
  24    quiet: bool = False
  25    processfilter: str | None = None
  26    mapfilter: str | None = None
  27    userfilter: str | None = None
  28    pid: int | None = None
  29    ignorecase: bool = False
  30    mappings: bool = False
  31    users: bool = False
  32    system: bool = False
  33    sysdetail: bool = False
  34    groupcmd: bool = False
  35    percent: bool = False
  36    abbreviate: bool = False
  37    totals: bool = False
  38    totalsonly: bool = False
  39    format: str = "raw"
  40    numeric: bool = False
  41    sort: str | None = None
  42    reverse: bool = False
  43    cmd_width: int = -1
  44    name_width: int = -1
  45    user_width: int = -1
  46    mapping_width: int = -1
  47
  48    # from setdatasources and other globals
  49    # these are not set by user but determined at runtime
  50    rssdetail: bool = False
  51    pssdetail: bool = True
  52    rollup: bool = True
  53    swappss: bool = False
  54    ownpid: int = 0
  55    ignore_autosize: Set[str] = field(default_factory=set)
  56
  57
  58class UIDCache(object):
  59    """A cache for user ID to username mappings.
  60
  61    This class avoids repeated lookups of UIDs by caching the results.
  62    """
  63
  64    def __init__(self):
  65        """Initializes the UIDCache."""
  66        self._cache = {}
  67
  68    def __call__(self, uid):
  69        """Retrieves the username for a given UID, populating the cache if necessary.
  70
  71        Args:
  72            uid (int): The user ID to look up.
  73
  74        Returns:
  75            str: The username associated with the UID.
  76        """
  77        return self._cache.setdefault(uid, self._getpwuid(uid))
  78
  79    @staticmethod
  80    def _getpwuid(uid):
  81        """Gets the username for a UID.
  82
  83        Args:
  84            uid (int): The user ID.
  85
  86        Returns:
  87            str: The username, or the UID as a string if the lookup fails.
  88        """
  89        try:
  90            return pwd.getpwuid(uid)[0]
  91        except KeyError:
  92            return str(uid)
  93
  94
  95class Proc(object):
  96    """Helper class to handle /proc/ filesystem data"""
  97
  98    def __init__(self):
  99        """Initializes the Proc helper."""
 100        pass
 101
 102    @staticmethod
 103    def listdir():
 104        """Lists the contents of /proc.
 105
 106        Returns:
 107            list[str]: A list of filenames in /proc.
 108        """
 109        return os.listdir("/proc")
 110
 111    @staticmethod
 112    def read(filename):
 113        """Reads a file from the /proc filesystem.
 114
 115        Args:
 116            filename (str): The name of the file in /proc to read.
 117
 118        Returns:
 119            str: The content of the file.
 120
 121        Raises:
 122            RuntimeError: If the file does not exist.
 123        """
 124        if not os.path.exists("/proc/" + filename):
 125            raise RuntimeError(
 126                f"File /proc/{filename} does not exist, are you sure you are running on a Linux system?"
 127            )
 128        return open("/proc/" + filename).read()
 129
 130    def readlines(self, filename):
 131        """Reads a file from /proc and splits it into lines.
 132
 133        Args:
 134            filename (str): The name of the file in /proc to read.
 135
 136        Returns:
 137            list[str]: A list of lines from the file.
 138        """
 139        return self.read(filename).splitlines(True)
 140
 141    def version(self):
 142        """Gets the Linux kernel version string.
 143
 144        Returns:
 145            str: The first line of /proc/version.
 146        """
 147        return self.readlines("version")[0]
 148
 149
 150class MemData(Proc):
 151    """Class accessing and storing /proc/meminfo data"""
 152
 153    def __init__(self):
 154        """Initializes MemData and parses /proc/meminfo."""
 155        self._memdata = {}
 156
 157        regex = re.compile("(?P<name>\\S+):\\s+(?P<amount>\\d+) kB")
 158        for line in self.readlines("meminfo"):
 159            match = regex.match(line)
 160            if match:
 161                self._memdata[match.group("name").lower()] = int(match.group("amount"))
 162
 163    def __call__(self, entry):
 164        """Retrieves a value from the stored meminfo data.
 165
 166        Args:
 167            entry (str): The key for the meminfo data to retrieve (e.g., 'memtotal').
 168
 169        Returns:
 170            int: The value from /proc/meminfo in kilobytes.
 171        """
 172        return self._memdata[entry]
 173
 174
 175class ProcessData(Proc):
 176    """Helper class to handle /proc/<pid> filesystem data"""
 177
 178    def __init__(self, config: SmemConfig):
 179        """Initializes ProcessData.
 180
 181        Args:
 182            config (SmemConfig): The smem2 configuration.
 183        """
 184        self._uidcache = UIDCache()
 185        self.config = config
 186
 187    def _iskernel(self, pid):
 188        """Checks if a process ID belongs to a kernel thread.
 189
 190        Kernel threads have an empty command line.
 191
 192        Args:
 193            pid (str): The process ID.
 194
 195        Returns:
 196            bool: True if the PID is a kernel thread, False otherwise.
 197        """
 198        return self.pidcmd(pid) == ""
 199
 200    @staticmethod
 201    def _stat(pid):
 202        """Gets the stat result for a process.
 203
 204        Args:
 205            pid (str): The process ID.
 206
 207        Returns:
 208            os.stat_result: The result of os.stat() on /proc/<pid>.
 209        """
 210        return os.stat("/proc/" + pid)
 211
 212    def pids(self):
 213        """Gets a list of process IDs.
 214
 215        Filters the list based on the configuration (e.g., specific PID).
 216        Excludes kernel threads and the smem2 process itself if a filter is active.
 217
 218        Returns:
 219            list[int]: A list of process IDs.
 220        """
 221        return [
 222            int(e)
 223            for e in self.listdir()
 224            if e.isdigit()
 225            and not self._iskernel(e)
 226            and ((self.config.pid and self.config.pid == int(e)) or not self.config.pid)
 227            and not ((int(e) == self.config.ownpid) and self.config.processfilter)
 228        ]
 229        # exclude own process when processfilter
 230
 231    def mapdata(self, pid):
 232        """Gets the smaps data for a process.
 233
 234        It prefers using smaps_rollup for performance if available and configured.
 235
 236        Args:
 237            pid (str): The process ID.
 238
 239        Returns:
 240            list[str]: The lines from the smaps or smaps_rollup file, or an empty list on error.
 241        """
 242        if self.config.rollup:
 243            try:
 244                return self.readlines(
 245                    "%s/smaps_rollup" % pid
 246                )  # smaps_rollup new from kernel 4.14, at least 7x faster for non maping listing
 247            except Exception:
 248                pass
 249        try:
 250            return self.readlines("%s/smaps" % pid)
 251        except (RuntimeError, PermissionError):
 252            return []
 253
 254    def pidcmd(self, pid):
 255        """Gets the command line for a process.
 256
 257        Args:
 258            pid (str): The process ID.
 259
 260        Returns:
 261            str: The command line of the process, or '?' on error.
 262        """
 263        try:
 264            c = self.read("%s/cmdline" % pid)[:-1]
 265            c = c.replace("\0", " ")
 266            if self.config.basename and not c == "":
 267                c = os.path.basename(c.split()[0])
 268            return c
 269        except Exception:
 270            return "?"
 271
 272    def piduser(self, pid):
 273        """Gets the UID of the process owner.
 274
 275        Args:
 276            pid (str): The process ID.
 277
 278        Returns:
 279            int: The user ID of the process owner, or -1 on error.
 280        """
 281        try:
 282            return self._stat("%d" % pid).st_uid
 283        except Exception:
 284            return -1
 285
 286    def username(self, uid):
 287        """Gets the username for a UID.
 288
 289        Args:
 290            uid (int): The user ID.
 291
 292        Returns:
 293            str: The username, or '?' if the UID is -1.
 294        """
 295        return "?" if uid == -1 else self._uidcache(uid)
 296
 297    def pidusername(self, pid):
 298        """Gets the username of the process owner.
 299
 300        Args:
 301            pid (int or str): The process ID.
 302
 303        Returns:
 304            str: The username of the process owner.
 305        """
 306        return self.username(self.piduser(pid))
 307
 308    def pidtostr(self, pid):
 309        """Converts a PID to a string.
 310
 311        Args:
 312            pid (int): The process ID.
 313
 314        Returns:
 315            str: The process ID as a string.
 316        """
 317        return str(pid)
 318
 319    def pidname(self, pid):
 320        """Gets the process name (from /proc/<pid>/comm).
 321
 322        Args:
 323            pid (str): The process ID.
 324
 325        Returns:
 326            str: The name of the process, or '?' on error.
 327        """
 328        try:
 329            return self.read("%s/comm" % pid)[:-1]
 330        except Exception:
 331            return "?"
 332
 333
 334def totalmem(config: SmemConfig):
 335    """Calculates the total memory to be used as a reference for percentages.
 336
 337    It can be based on real memory from dmidecode, a user-provided value,
 338    or the 'MemTotal' value from /proc/meminfo.
 339
 340    Args:
 341        config (SmemConfig): The smem2 configuration.
 342
 343    Returns:
 344        int: The total memory in kilobytes.
 345    """
 346    if config.realmem:
 347        return fromunits(config.realmem) / 1024
 348    else:
 349        if config.system or config.sysdetail:
 350            try:
 351                # only for systems running directly on HW
 352                ram = (
 353                    os.popen(
 354                        "dmidecode --type memory 2> /dev/null|awk '/Size: [0-9]+ MB/{sum+=$2};END{print sum}'",
 355                    )
 356                    .read()
 357                    .strip()
 358                )
 359                if ram.isdigit():
 360                    return fromunits(str(int(ram)) + "M") / 1024
 361            except Exception:
 362                pass
 363
 364        return MemData()("memtotal")
 365
 366
 367def pidmaps(pid, proc: ProcessData, config: SmemConfig, nomaps=False):
 368    """Parses the memory maps for a given process.
 369
 370    It reads /proc/<pid>/smaps or /proc/<pid>/smaps_rollup to get memory
 371    usage information for each mapping.
 372
 373    Args:
 374        pid (int or str): The process ID.
 375        proc (ProcessData): The ProcessData instance.
 376        config (SmemConfig): The smem2 configuration.
 377        nomaps (bool, optional): If True, only read the first mapping entry,
 378            used for feature detection. Defaults to False.
 379
 380    Returns:
 381        dict: A dictionary of memory mappings, where keys are start addresses.
 382    """
 383    maps: dict = {}
 384    start = None
 385    status = None
 386    if config.rollup or config.pssdetail or config.rssdetail:
 387        try:
 388            status = proc.readlines("%s/status" % pid)
 389        except (RuntimeError, PermissionError):
 390            status = None
 391
 392    for l in proc.mapdata(pid):
 393        f = l.split()
 394        if f[-1] == "kB":
 395            maps[start][f[0][:-1].lower()] = int(f[1])
 396        elif "-" in f[0] and ":" not in f[0]:  # looks like a mapping range
 397            start, end = f[0].split("-")
 398            start = int(start, 16)
 399            name = "<anonymous>"
 400            if len(f) > 5:
 401                name = f[5]
 402                if config.basename:
 403                    name = os.path.basename(name)
 404            maps[start] = dict(
 405                end=int(end, 16),
 406                perm=f[1],
 407                offset=int(f[2], 16),
 408                major=int(f[3].split(":")[0], 16),
 409                minor=int(f[3].split(":")[1], 16),
 410                inode=int(f[4]),
 411                name=name,
 412                pss=0,
 413                rss=0,
 414                uss=0,
 415                swap=0,
 416                swappss=0,
 417                pcent=0,
 418                size=0,
 419            )
 420            if config.pssdetail:
 421                (
 422                    maps[start]["pss_anon"],
 423                    maps[start]["pss_file"],
 424                    maps[start]["pss_shmem"],
 425                ) = (0, 0, 0)
 426
 427            # for nomaps mode: stop reading mappings
 428            if nomaps:
 429                break
 430
 431    # add pss, rss, uss if available from smaps
 432    if not config.rollup:
 433        if not config.pssdetail:
 434            for m in maps:
 435                maps[m]["uss"] = maps[m].get("private_clean", 0) + maps[m].get(
 436                    "private_dirty", 0
 437                )
 438                maps[m]["pss"] = maps[m]["uss"] + maps[m].get("shared_clean", 0) / 2
 439                maps[m]["rss"] = maps[m].get("shared_clean", 0) + maps[m].get(
 440                    "shared_dirty", 0
 441                )
 442        if config.rssdetail and status:
 443            # from /proc/pid/status
 444            for s in status:
 445                if "RssAnon" in s:
 446                    maps[start]["rss_anon"] = int(s.split()[1])
 447                elif "RssFile" in s:
 448                    maps[start]["rss_file"] = int(s.split()[1])
 449                elif "RssShmem" in s:
 450                    maps[start]["rss_shmem"] = int(s.split()[1])
 451
 452        # Parse VmSize for VSS calculation
 453        if status:
 454            for s in status:
 455                if "VmSize:" in s:
 456                    # Set size for the last mapping (rollup case) or all mappings
 457                    if config.rollup and start:
 458                        maps[start]["size"] = int(s.split()[1])
 459                    else:
 460                        # For non-rollup, distribute VmSize across all mappings
 461                        total_size = int(s.split()[1])
 462                        if maps:
 463                            size_per_map = (
 464                                total_size // len(maps) if len(maps) > 0 else 0
 465                            )
 466                            for m in maps:
 467                                maps[m]["size"] = size_per_map
 468                    break
 469
 470    # Apply mapfilter if specified
 471    if config.mapfilter and not nomaps:
 472        f = {}
 473        for m in maps:
 474            if not filters(config.mapfilter, m, config, lambda x: maps[x]["name"]):
 475                f[m] = maps[m]
 476        return f
 477    return maps
 478
 479
 480def maptotals(pids, proc: ProcessData, config: SmemConfig):
 481    """Aggregates memory usage data for a list of pids, including map details.
 482
 483    For each process, it gets memory totals (PSS, RSS, USS, etc.) and
 484    detailed mapping information. It applies process and user filters.
 485
 486    Args:
 487        pids (list[int]): A list of process IDs.
 488        proc (ProcessData): The ProcessData instance.
 489        config (SmemConfig): The smem2 configuration.
 490
 491    Returns:
 492        dict: A dictionary where keys are process IDs and values are dictionaries
 493              containing memory totals and mapping details for each process.
 494    """
 495    pt: dict = {}
 496    for p in pids:
 497        if filters(
 498            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
 499        ) or filters(config.userfilter, p, config, proc.pidusername):
 500            continue
 501        maps = pidmaps(p, proc, config)
 502        if maps:
 503            pt[p] = {}
 504            pt[p]["maps"] = maps
 505            totals = pidtotals(p, proc, config)
 506            # Flatten the totals into the main dict for compatibility with display code
 507            pt[p].update(totals)
 508    return pt
 509
 510
 511def pidtotals(pid, proc: ProcessData, config: SmemConfig):
 512    """Calculates the total memory usage (PSS, RSS, USS, etc.) for a single process.
 513
 514    It sums up the values from all memory mappings of the process.
 515
 516    Args:
 517        pid (int or str): The process ID.
 518        proc (ProcessData): The ProcessData instance.
 519        config (SmemConfig): The smem2 configuration.
 520
 521    Returns:
 522        dict: A dictionary with total PSS, RSS, USS, Swap, etc., for the process.
 523    """
 524    totals: dict = {}
 525    maps = pidmaps(pid, proc, config)
 526    if maps:
 527        maplist = maps.values()
 528        totals["pss"] = sum([m.get("pss", 0) for m in maplist])
 529        totals["rss"] = sum([m.get("rss", 0) for m in maplist])
 530        private_clean = sum(m.get("private_clean", 0) for m in maplist)
 531        private_dirty = sum(m.get("private_dirty", 0) for m in maplist)
 532        totals["uss"] = private_clean + private_dirty
 533        totals["swap"] = sum([m.get("swap", 0) for m in maplist])
 534        totals["size"] = sum([m.get("size", 0) for m in maplist])
 535        totals["maps"] = len(maps)
 536        if config.swappss:
 537            totals["swappss"] = sum([m.get("swappss", 0) for m in maplist])
 538        if config.pssdetail:
 539            totals["pss_anon"] = sum([m.get("pss_anon", 0) for m in maplist])
 540            totals["pss_file"] = sum([m.get("pss_file", 0) for m in maplist])
 541            totals["pss_shmem"] = sum([m.get("pss_shmem", 0) for m in maplist])
 542        if config.rssdetail:
 543            totals["rss_anon"] = sum([m.get("rss_anon", 0) for m in maplist])
 544            totals["rss_file"] = sum([m.get("rss_file", 0) for m in maplist])
 545            totals["rss_shmem"] = sum([m.get("rss_shmem", 0) for m in maplist])
 546    return totals
 547
 548
 549def usertotals(pids, proc: ProcessData, config: SmemConfig):
 550    """Aggregates memory usage by user.
 551
 552    It groups processes by user and calculates the total memory usage for each user.
 553
 554    Args:
 555        pids (list[int]): A list of process IDs to analyze.
 556        proc (ProcessData): The ProcessData instance.
 557        config (SmemConfig): The smem2 configuration.
 558
 559    Returns:
 560        dict: A dictionary where keys are user IDs and values are dictionaries
 561              containing process lists and memory totals for each user.
 562    """
 563    ut: dict = {}
 564    for p in pids:
 565        if filters(
 566            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
 567        ) or filters(config.userfilter, p, config, proc.pidusername):
 568            continue
 569        u = proc.piduser(p)
 570        if u not in ut:
 571            ut[u] = {"pids": [], "name": "?"}
 572            ut[u]["name"] = proc.username(u)
 573        ut[u]["pids"].append(p)
 574
 575    # get totals per user
 576    for u in ut:
 577        ut[u]["totals"] = processtotals(ut[u]["pids"], proc, config)
 578    return ut
 579
 580
 581def cmdtotals(pids, proc: ProcessData, config: SmemConfig):
 582    """Aggregates memory usage by command line.
 583
 584    It groups processes by their command line and calculates the total memory
 585    usage for each command.
 586
 587    Args:
 588        pids (list[int]): A list of process IDs to analyze.
 589        proc (ProcessData): The ProcessData instance.
 590        config (SmemConfig): The smem2 configuration.
 591
 592    Returns:
 593        dict: A dictionary where keys are command lines and values are
 594              dictionaries containing process lists and memory totals.
 595    """
 596    # totals per command
 597    ct: dict = {}
 598
 599    for p in pids:
 600        if filters(
 601            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
 602        ) or filters(config.userfilter, p, config, proc.pidusername):
 603            continue
 604        cmdline = proc.pidcmd(p)
 605        if not cmdline:
 606            continue
 607
 608        parts = cmdline.split()
 609        if not parts:
 610            continue
 611        c = parts[0]
 612
 613        # if multiple processes per command, then a list of pids
 614        if c in ct:
 615            ct[c]["pids"].append(p)
 616        else:
 617            # first pid for this command
 618            ct[c] = {"pids": [p]}
 619
 620    # get totals per command
 621    for c in ct:
 622        ct[c]["totals"] = processtotals(ct[c]["pids"], proc, config)
 623        if "totals" in ct[c] and ct[c]["totals"]:
 624            ct[c]["name"] = c
 625            ct[c]["user"] = proc.pidusername(ct[c]["pids"][0])
 626
 627    # remove commands with empty totals
 628    for c in list(ct.keys()):
 629        if "totals" not in ct[c] or not ct[c]["totals"]:
 630            del ct[c]
 631    return ct
 632
 633
 634def sortmaps(totals, key):
 635    """Sorts a dictionary of totals by a given key.
 636
 637    Args:
 638        totals (dict): A dictionary where values are another dictionary
 639                       containing a 'totals' key.
 640        key (str): The key within the nested 'totals' dictionary to sort by.
 641
 642    Returns:
 643        list: A sorted list of the input dictionary's values.
 644    """
 645    # sort by key
 646    return sorted(totals.values(), key=lambda x: x["totals"][key], reverse=True)
 647
 648
 649def units(x):
 650    """Converts a value in bytes to a human-readable string with units (K, M, G).
 651
 652    Args:
 653        x (int or float): The value in bytes.
 654
 655    Returns:
 656        str: A human-readable string representation of the value.
 657    """
 658    if x > 1024 * 1024 * 1024:
 659        return "%.1fG" % (x / (1024.0 * 1024 * 1024))
 660    if x > 1024 * 1024:
 661        return "%.1fM" % (x / (1024.0 * 1024))
 662    if x > 1024:
 663        return "%.1fK" % (x / 1024.0)
 664    return str(x)
 665
 666
 667def fromunits(x):
 668    """Converts a human-readable string with units (K, M, G, etc.) to bytes.
 669
 670    Args:
 671        x (str): The string with a value and unit (e.g., "1024M").
 672
 673    Returns:
 674        int: The value in bytes.
 675
 676    Raises:
 677        SystemExit: If the unit is not recognized.
 678    """
 679    s = dict(
 680        k=2**10,
 681        K=2**10,
 682        kB=2**10,
 683        KB=2**10,
 684        M=2**20,
 685        MB=2**20,
 686        G=2**30,
 687        GB=2**30,
 688        T=2**40,
 689        TB=2**40,
 690    )
 691    for k, v in list(s.items()):
 692        if x.endswith(k):
 693            return int(float(x[: -len(k)]) * v)
 694    sys.stderr.write("Memory size should be written with units, for example 1024M\n")
 695    sys.exit(-1)
 696
 697
 698def processtotals(pids, proc: ProcessData, config: SmemConfig):
 699    """Sums up memory usage totals for a list of processes.
 700
 701    Args:
 702        pids (list[int]): The list of process IDs.
 703        proc (ProcessData): The ProcessData instance.
 704        config (SmemConfig): The smem2 configuration.
 705
 706    Returns:
 707        dict: A dictionary with the summed PSS, RSS, USS, etc., for the
 708              given processes.
 709    """
 710    # Sums up totals of a pid list
 711    t = {"uss": 0, "pss": 0, "rss": 0, "swap": 0, "size": 0}
 712    if config.swappss:
 713        t["swappss"] = 0
 714    for p in pids:
 715        pt = pidtotals(p, proc, config)
 716        if pt:
 717            for k in t:
 718                if k in pt:
 719                    t[k] += pt[k]
 720    return t
 721
 722
 723def get_process_data(proc: ProcessData, config: SmemConfig):
 724    """Collects and returns memory usage data for each process.
 725
 726    Args:
 727        proc (ProcessData): The ProcessData instance.
 728        config (SmemConfig): The smem2 configuration.
 729
 730    Returns:
 731        dict: A dictionary containing memory data for each process,
 732              as returned by maptotals.
 733    """
 734    # Collect process data
 735    pids = proc.pids()
 736    data = maptotals(pids, proc, config)
 737    return data
 738
 739
 740def get_map_data(proc: ProcessData, config: SmemConfig):
 741    """Collects and returns memory usage data aggregated by mapping name.
 742
 743    Args:
 744        proc (ProcessData): The ProcessData instance.
 745        config (SmemConfig): The smem2 configuration.
 746
 747    Returns:
 748        dict: A dictionary containing memory data aggregated by mapping name.
 749    """
 750    # Collect map data - aggregate by mapping name
 751    pids = proc.pids()
 752    data = mapnametotals(pids, proc, config)
 753    return data
 754
 755
 756def mapnametotals(pids, proc: ProcessData, config: SmemConfig):
 757    """Aggregates memory usage by mapping name across a list of processes.
 758
 759    It iterates through processes and their memory maps, summing up memory
 760    statistics for each unique mapping name.
 761
 762    Args:
 763        pids (list[int]): A list of process IDs.
 764        proc (ProcessData): The ProcessData instance.
 765        config (SmemConfig): The smem2 configuration.
 766
 767    Returns:
 768        dict: A dictionary where keys are mapping names and values are
 769              dictionaries of aggregated memory statistics.
 770    """
 771    totals: dict = {}
 772    for pid in pids:
 773        # Apply filters like the original version
 774        if filters(
 775            config.processfilter, pid, config, proc.pidcmd, proc.pidtostr, proc.pidname
 776        ) or filters(config.userfilter, pid, config, proc.pidusername):
 777            continue
 778        try:
 779            maps = pidmaps(pid, proc, config)
 780            seen = {}
 781            for m in list(maps.keys()):
 782                name = maps[m]["name"]
 783                if name not in totals:
 784                    t = dict(
 785                        size=0,
 786                        rss=0,
 787                        pss=0,
 788                        shared_clean=0,
 789                        shared_dirty=0,
 790                        private_clean=0,
 791                        count=0,
 792                        private_dirty=0,
 793                        referenced=0,
 794                        swap=0,
 795                        pids=0,
 796                    )
 797                    if config.swappss:
 798                        t["swappss"] = 0
 799                else:
 800                    t = totals[name]
 801                for k in t:
 802                    t[k] += maps[m].get(k, 0)
 803                t["count"] += 1
 804                if name not in seen:
 805                    t["pids"] += 1
 806                    seen[name] = 1
 807                totals[name] = t
 808        except (OSError, IOError):
 809            continue
 810    return totals
 811
 812
 813def get_user_data(proc: ProcessData, config: SmemConfig):
 814    """Collects and returns memory usage data aggregated by user.
 815
 816    Args:
 817        proc (ProcessData): The ProcessData instance.
 818        config (SmemConfig): The smem2 configuration.
 819
 820    Returns:
 821        dict: A dictionary containing memory data aggregated by user.
 822    """
 823    # Collect user data
 824    pids = proc.pids()
 825    data = usertotals(pids, proc, config)
 826    return data
 827
 828
 829def get_cmd_data(proc: ProcessData, config: SmemConfig):
 830    """Collects and returns memory usage data aggregated by command.
 831
 832    Args:
 833        proc (ProcessData): The ProcessData instance.
 834        config (SmemConfig): The smem2 configuration.
 835
 836    Returns:
 837        dict: A dictionary containing memory data aggregated by command.
 838    """
 839    # Collect cmd data
 840    pids = proc.pids()
 841    data = cmdtotals(pids, proc, config)
 842    return data
 843
 844
 845def get_system_data(proc: ProcessData, config: SmemConfig):
 846    """Collects and returns system-wide memory usage data.
 847
 848    This provides a breakdown of memory usage by different system components
 849    like kernel, firmware, userspace, etc.
 850
 851    Args:
 852        proc (ProcessData): The ProcessData instance.
 853        config (SmemConfig): The smem2 configuration.
 854
 855    Returns:
 856        list[tuple]: A list of tuples, where each tuple represents a memory
 857                     area and its usage in PSS and RSS.
 858    """
 859    t = totalmem(config)
 860    m = MemData()
 861    mt = m("memtotal")
 862    f = m("memfree")
 863    kernel = kernelsize(config)
 864
 865    # total amount mapped into userspace (ie mapped an unmapped pages) /dev/zero mappings
 866    u = m("anonpages") + m("mapped")
 867
 868    # total amount used by hardware
 869    fh = int(max(t - mt - kernel, 0))
 870
 871    # total amount allocated by kernel not for userspace
 872    kd = mt - f - u
 873
 874    # total amount in kernel caches
 875    kdc = m("buffers") + m("sreclaimable") + m("cached") - m("mapped")
 876
 877    if config.sysdetail:
 878        shmp = mapshared(proc, config)  # shared memory mapped by processes
 879        mapzero = mapdevzero(
 880            proc, config
 881        )  # dev/zero mapped as files should be subtracted from files Mapped from userspace
 882        mapped = m("mapped") - mapzero
 883        modules = kernelmodsize(proc, config)
 884        shm = m("shmem") - shmp
 885
 886        mounts = proc.readlines("mounts")
 887        ramfs = 0
 888        for r in mounts:
 889            if "ramfs" in r:
 890                try:
 891                    ramfs += int(
 892                        (os.popen("du -sk %s" % r.split()[1]).read()).split()[0]
 893                    )
 894                except Exception:
 895                    pass
 896        filecache = m("cached") - mapped - shm - ramfs
 897        unknown = (
 898            mt
 899            - f
 900            - u
 901            - (
 902                modules
 903                + m("pagetables")
 904                + m("kernelstack")
 905                + m("slab")
 906                + m("buffers")
 907                + filecache
 908                + shm
 909                + ramfs
 910            )
 911        )
 912        if unknown < 0:
 913            unknown = 0
 914
 915    lines: list[tuple]
 916    if config.sysdetail:
 917        lines = [
 918            ("firmware/hardware", fh, 0, 0),
 919            ("kernel image", kernel, 0, 0),
 920            ("kernel modules", modules, 0, 0),
 921            ("page tables", m("pagetables"), 0, 0),
 922            ("kernel stack", m("kernelstack"), 0, 0),
 923            ("slab (all/SReclaimable)", m("slab"), m("sreclaimable"), 0),
 924            ("buffers", m("buffers"), m("buffers"), 0),
 925            ("cached (w/o mapped,tmpfs,ramfs)", filecache, filecache, 0),
 926            ("shared (non process tmpfs)", shm, 0, 0),
 927            ("ramfs", ramfs, 0, 0),
 928            ("unknown", unknown, 0, 0),
 929            ("processes (all/mapped files)", u, mapped, 0),
 930            ("free memory", f, f, 0),
 931        ]
 932        if "all" in config.columns or "details" in config.columns:
 933            line_all: list[tuple] = [
 934                ("-------------------------------", 0, 0, 0),
 935                ("/dev/zero mapped", 0, 0, mapzero),
 936                ("shared by processes", 0, 0, shmp),
 937                ("unevictable", 0, 0, m("unevictable")),
 938                ("dirty (unwritten to file)", 0, 0, m("dirty")),
 939                ("swapped", 0, 0, m("swaptotal") - m("swapfree")),
 940                ("available (estimated)", 0, 0, m("memavailable")),
 941            ]
 942            lines = lines + line_all
 943    else:
 944        lines = [
 945            ("firmware/hardware", fh, 0),
 946            ("kernel image", kernel, 0),
 947            ("kernel dynamic memory", kd, kdc),
 948            ("userspace memory", u, m("mapped")),
 949            ("free memory", f, f),
 950        ]
 951    return lines
 952
 953
 954def mapdevzero(proc: ProcessData, config: SmemConfig):
 955    """Calculates memory mapped to /dev/zero.
 956
 957    Some Linux versions count memory mapped to /dev/zero as 'Mapped' instead
 958    of 'AnonPages'. This function calculates that amount.
 959
 960    Args:
 961        proc (ProcessData): The ProcessData instance.
 962        config (SmemConfig): The smem2 configuration.
 963
 964    Returns:
 965        int: Total PSS of memory mapped to /dev/zero, in kilobytes.
 966    """
 967    ps = proc.pids()
 968    config.mapfilter = "^/dev/zero$"
 969    pt = maptotals(ps, proc, config)
 970    t = 0.0
 971    for r in pt:
 972        t += pt[r]["pss"]
 973    return int(t)
 974
 975
 976def mapshared(proc: ProcessData, config: SmemConfig):
 977    """Calculates memory shared by multiple processes (via SYSV IPC).
 978
 979    Args:
 980        proc (ProcessData): The ProcessData instance.
 981        config (SmemConfig): The smem2 configuration.
 982
 983    Returns:
 984        int: Total PSS of memory from SYSV shared segments, in kilobytes.
 985    """
 986    ps = proc.pids()
 987    config.mapfilter = "^/SYSV*"
 988    pt = maptotals(ps, proc, config)
 989    t = 0.0
 990    for r in pt:
 991        t += pt[r]["pss"]
 992    return int(t)
 993
 994
 995def kernelsize(config: SmemConfig):
 996    """Determines the size of the kernel image from the file specified.
 997
 998    Args:
 999        config (SmemConfig): The smem2 configuration.
1000
1001    Returns:
1002        int: The kernel image size in kilobytes.
1003    """
1004    kernelsize_val = 0
1005    if config.kernel:
1006        try:
1007            d = os.popen("size %s" % config.kernel).readlines()[1].split()
1008            if int(d[1]) == 0:  # data part missing, seems like packed file
1009                # try some heuristic to find gzipped part in kernel image
1010                with open(config.kernel, "rb") as f:
1011                    packedkernel = f.read()
1012                pos = packedkernel.find(b"\x1f\x8b")
1013                if pos >= 0 and pos < 25000:
1014                    if not config.quiet:
1015                        sys.stderr.write(
1016                            "Parameter '%s' should be an original uncompressed compiled kernel file.\n"
1017                            % config.kernel
1018                        )
1019                        sys.stderr.write(
1020                            "Maybe uncompressed kernel can be extracted by the command:\n"
1021                            "  dd if=%s bs=1 skip=%d | gzip -d >%s.unpacked\n\n"
1022                            % (config.kernel, pos, config.kernel)
1023                        )
1024            else:
1025                kernelsize_val = int(int(d[3]) / 1024 + 0.5)
1026        except Exception:
1027            pass  # Fail silently if size command fails or kernel file is not found
1028    return kernelsize_val
1029
1030
1031def kernelmodsize(proc: ProcessData, config: SmemConfig):
1032    """Calculates the total size of loaded kernel modules.
1033
1034    It reads /proc/modules to get the size of each module.
1035
1036    Args:
1037        proc (ProcessData): The ProcessData instance.
1038        config (SmemConfig): The smem2 configuration.
1039
1040    Returns:
1041        int: The total size of kernel modules in kilobytes.
1042    """
1043    ms = 0.0
1044    try:
1045        md = proc.readlines("modules")
1046        for m in md:
1047            ms += int(m.split()[1])
1048        ms /= 1024
1049    except Exception:
1050        if not config.quiet:
1051            sys.stderr.write("Detection of kernel modules size failed\n")
1052    return int(ms)
1053
1054
1055def setdatasources(config: SmemConfig, proc: ProcessData):
1056    """Detects kernel features and updates the configuration accordingly.
1057
1058    It checks for support of PSS, SwapPSS, and detailed PSS/RSS reporting
1059    by inspecting the smaps of the current process. It also determines if
1060    the faster smaps_rollup can be used.
1061
1062    Args:
1063        config (SmemConfig): The smem2 configuration object to update.
1064        proc (ProcessData): The ProcessData instance.
1065
1066    Returns:
1067        SmemConfig: The updated configuration object.
1068    """
1069    """New kernel features detection"""
1070    config.ownpid = os.getpid()
1071
1072    config.rssdetail = False
1073    config.pssdetail = True
1074    config.rollup = True
1075    rd = proc.readlines("%s/status" % config.ownpid)
1076    for r in rd:
1077        if "RssAnon:" in r:
1078            config.rssdetail = True
1079            break
1080
1081    map_data = pidmaps(config.ownpid, proc, config, nomaps=True)
1082    map_data = map_data[list(map_data.keys())[0]]
1083
1084    if "pss" not in map_data and not config.quiet:
1085        sys.stderr.write("Warning: Kernel does not appear to support PSS measurement\n")
1086
1087    if "swappss" in map_data:
1088        config.swappss = True
1089    else:
1090        config.swappss = False
1091        if not config.quiet:
1092            sys.stderr.write(
1093                "Warning: Kernel does not appear to support SwapPSS measurement\n"
1094            )
1095
1096    if "pss_anon" in map_data:
1097        config.pssdetail = True
1098    else:
1099        config.pssdetail = False
1100        if not config.quiet:
1101            sys.stderr.write(
1102                "Warning: Kernel does not appear to support Pss Anon/File/Shmem measurement\n"
1103            )
1104
1105    """ some smem2 features need full smaps"""
1106    if (
1107        config.mapfilter
1108        or config.mappings
1109        or "maps" in config.columns
1110        or ("all" in config.columns and not config.groupcmd)
1111    ):
1112        config.rollup = False
1113    return config
1114
1115
1116def filters(opt, arg, config: SmemConfig, *sources):
1117    """A generic filter function based on regular expressions.
1118
1119    It checks if a given argument should be filtered out by matching a regex
1120    against one or more source strings.
1121
1122    Args:
1123        opt (str or None): The regex pattern to search for. If None or empty,
1124            the function returns False.
1125        arg: The argument to pass to the source functions (e.g., a PID).
1126        config (SmemConfig): The smem2 configuration (for ignorecase flag).
1127        *sources (callable): Variable length list of functions that take `arg`
1128            and return a string to be searched.
1129
1130    Returns:
1131        bool: True if the item should be filtered out (no match found),
1132              False otherwise (a match was found or no filter is active).
1133    """
1134    if not opt:
1135        return False
1136    for f in sources:
1137        if re.search(opt, f(arg), re.I if config.ignorecase else 0):
1138            return False
1139    return True
@dataclass
class SmemConfig:
10@dataclass
11class SmemConfig:
12    """Stores the configuration for smem2.
13
14    This dataclass holds all the configuration options for smem2,
15    mostly populated from command-line arguments.
16    """
17
18    # from parse_arguments
19    no_header: bool = False
20    columns: str = ""
21    autosize: bool = False
22    realmem: str | None = None
23    kernel: str | None = None
24    basename: bool = False
25    quiet: bool = False
26    processfilter: str | None = None
27    mapfilter: str | None = None
28    userfilter: str | None = None
29    pid: int | None = None
30    ignorecase: bool = False
31    mappings: bool = False
32    users: bool = False
33    system: bool = False
34    sysdetail: bool = False
35    groupcmd: bool = False
36    percent: bool = False
37    abbreviate: bool = False
38    totals: bool = False
39    totalsonly: bool = False
40    format: str = "raw"
41    numeric: bool = False
42    sort: str | None = None
43    reverse: bool = False
44    cmd_width: int = -1
45    name_width: int = -1
46    user_width: int = -1
47    mapping_width: int = -1
48
49    # from setdatasources and other globals
50    # these are not set by user but determined at runtime
51    rssdetail: bool = False
52    pssdetail: bool = True
53    rollup: bool = True
54    swappss: bool = False
55    ownpid: int = 0
56    ignore_autosize: Set[str] = field(default_factory=set)

Stores the configuration for smem2.

This dataclass holds all the configuration options for smem2, mostly populated from command-line arguments.

SmemConfig( no_header: bool = False, columns: str = '', autosize: bool = False, realmem: str | None = None, kernel: str | None = None, basename: bool = False, quiet: bool = False, processfilter: str | None = None, mapfilter: str | None = None, userfilter: str | None = None, pid: int | None = None, ignorecase: bool = False, mappings: bool = False, users: bool = False, system: bool = False, sysdetail: bool = False, groupcmd: bool = False, percent: bool = False, abbreviate: bool = False, totals: bool = False, totalsonly: bool = False, format: str = 'raw', numeric: bool = False, sort: str | None = None, reverse: bool = False, cmd_width: int = -1, name_width: int = -1, user_width: int = -1, mapping_width: int = -1, rssdetail: bool = False, pssdetail: bool = True, rollup: bool = True, swappss: bool = False, ownpid: int = 0, ignore_autosize: Set[str] = <factory>)
no_header: bool = False
columns: str = ''
autosize: bool = False
realmem: str | None = None
kernel: str | None = None
basename: bool = False
quiet: bool = False
processfilter: str | None = None
mapfilter: str | None = None
userfilter: str | None = None
pid: int | None = None
ignorecase: bool = False
mappings: bool = False
users: bool = False
system: bool = False
sysdetail: bool = False
groupcmd: bool = False
percent: bool = False
abbreviate: bool = False
totals: bool = False
totalsonly: bool = False
format: str = 'raw'
numeric: bool = False
sort: str | None = None
reverse: bool = False
cmd_width: int = -1
name_width: int = -1
user_width: int = -1
mapping_width: int = -1
rssdetail: bool = False
pssdetail: bool = True
rollup: bool = True
swappss: bool = False
ownpid: int = 0
ignore_autosize: Set[str]
class UIDCache:
59class UIDCache(object):
60    """A cache for user ID to username mappings.
61
62    This class avoids repeated lookups of UIDs by caching the results.
63    """
64
65    def __init__(self):
66        """Initializes the UIDCache."""
67        self._cache = {}
68
69    def __call__(self, uid):
70        """Retrieves the username for a given UID, populating the cache if necessary.
71
72        Args:
73            uid (int): The user ID to look up.
74
75        Returns:
76            str: The username associated with the UID.
77        """
78        return self._cache.setdefault(uid, self._getpwuid(uid))
79
80    @staticmethod
81    def _getpwuid(uid):
82        """Gets the username for a UID.
83
84        Args:
85            uid (int): The user ID.
86
87        Returns:
88            str: The username, or the UID as a string if the lookup fails.
89        """
90        try:
91            return pwd.getpwuid(uid)[0]
92        except KeyError:
93            return str(uid)

A cache for user ID to username mappings.

This class avoids repeated lookups of UIDs by caching the results.

UIDCache()
65    def __init__(self):
66        """Initializes the UIDCache."""
67        self._cache = {}

Initializes the UIDCache.

class Proc:
 96class Proc(object):
 97    """Helper class to handle /proc/ filesystem data"""
 98
 99    def __init__(self):
100        """Initializes the Proc helper."""
101        pass
102
103    @staticmethod
104    def listdir():
105        """Lists the contents of /proc.
106
107        Returns:
108            list[str]: A list of filenames in /proc.
109        """
110        return os.listdir("/proc")
111
112    @staticmethod
113    def read(filename):
114        """Reads a file from the /proc filesystem.
115
116        Args:
117            filename (str): The name of the file in /proc to read.
118
119        Returns:
120            str: The content of the file.
121
122        Raises:
123            RuntimeError: If the file does not exist.
124        """
125        if not os.path.exists("/proc/" + filename):
126            raise RuntimeError(
127                f"File /proc/{filename} does not exist, are you sure you are running on a Linux system?"
128            )
129        return open("/proc/" + filename).read()
130
131    def readlines(self, filename):
132        """Reads a file from /proc and splits it into lines.
133
134        Args:
135            filename (str): The name of the file in /proc to read.
136
137        Returns:
138            list[str]: A list of lines from the file.
139        """
140        return self.read(filename).splitlines(True)
141
142    def version(self):
143        """Gets the Linux kernel version string.
144
145        Returns:
146            str: The first line of /proc/version.
147        """
148        return self.readlines("version")[0]

Helper class to handle /proc/ filesystem data

Proc()
 99    def __init__(self):
100        """Initializes the Proc helper."""
101        pass

Initializes the Proc helper.

@staticmethod
def listdir():
103    @staticmethod
104    def listdir():
105        """Lists the contents of /proc.
106
107        Returns:
108            list[str]: A list of filenames in /proc.
109        """
110        return os.listdir("/proc")

Lists the contents of /proc.

Returns:

list[str]: A list of filenames in /proc.

@staticmethod
def read(filename):
112    @staticmethod
113    def read(filename):
114        """Reads a file from the /proc filesystem.
115
116        Args:
117            filename (str): The name of the file in /proc to read.
118
119        Returns:
120            str: The content of the file.
121
122        Raises:
123            RuntimeError: If the file does not exist.
124        """
125        if not os.path.exists("/proc/" + filename):
126            raise RuntimeError(
127                f"File /proc/{filename} does not exist, are you sure you are running on a Linux system?"
128            )
129        return open("/proc/" + filename).read()

Reads a file from the /proc filesystem.

Arguments:
  • filename (str): The name of the file in /proc to read.
Returns:

str: The content of the file.

Raises:
  • RuntimeError: If the file does not exist.
def readlines(self, filename):
131    def readlines(self, filename):
132        """Reads a file from /proc and splits it into lines.
133
134        Args:
135            filename (str): The name of the file in /proc to read.
136
137        Returns:
138            list[str]: A list of lines from the file.
139        """
140        return self.read(filename).splitlines(True)

Reads a file from /proc and splits it into lines.

Arguments:
  • filename (str): The name of the file in /proc to read.
Returns:

list[str]: A list of lines from the file.

def version(self):
142    def version(self):
143        """Gets the Linux kernel version string.
144
145        Returns:
146            str: The first line of /proc/version.
147        """
148        return self.readlines("version")[0]

Gets the Linux kernel version string.

Returns:

str: The first line of /proc/version.

class MemData(Proc):
151class MemData(Proc):
152    """Class accessing and storing /proc/meminfo data"""
153
154    def __init__(self):
155        """Initializes MemData and parses /proc/meminfo."""
156        self._memdata = {}
157
158        regex = re.compile("(?P<name>\\S+):\\s+(?P<amount>\\d+) kB")
159        for line in self.readlines("meminfo"):
160            match = regex.match(line)
161            if match:
162                self._memdata[match.group("name").lower()] = int(match.group("amount"))
163
164    def __call__(self, entry):
165        """Retrieves a value from the stored meminfo data.
166
167        Args:
168            entry (str): The key for the meminfo data to retrieve (e.g., 'memtotal').
169
170        Returns:
171            int: The value from /proc/meminfo in kilobytes.
172        """
173        return self._memdata[entry]

Class accessing and storing /proc/meminfo data

MemData()
154    def __init__(self):
155        """Initializes MemData and parses /proc/meminfo."""
156        self._memdata = {}
157
158        regex = re.compile("(?P<name>\\S+):\\s+(?P<amount>\\d+) kB")
159        for line in self.readlines("meminfo"):
160            match = regex.match(line)
161            if match:
162                self._memdata[match.group("name").lower()] = int(match.group("amount"))

Initializes MemData and parses /proc/meminfo.

Inherited Members
Proc
listdir
read
readlines
version
class ProcessData(Proc):
176class ProcessData(Proc):
177    """Helper class to handle /proc/<pid> filesystem data"""
178
179    def __init__(self, config: SmemConfig):
180        """Initializes ProcessData.
181
182        Args:
183            config (SmemConfig): The smem2 configuration.
184        """
185        self._uidcache = UIDCache()
186        self.config = config
187
188    def _iskernel(self, pid):
189        """Checks if a process ID belongs to a kernel thread.
190
191        Kernel threads have an empty command line.
192
193        Args:
194            pid (str): The process ID.
195
196        Returns:
197            bool: True if the PID is a kernel thread, False otherwise.
198        """
199        return self.pidcmd(pid) == ""
200
201    @staticmethod
202    def _stat(pid):
203        """Gets the stat result for a process.
204
205        Args:
206            pid (str): The process ID.
207
208        Returns:
209            os.stat_result: The result of os.stat() on /proc/<pid>.
210        """
211        return os.stat("/proc/" + pid)
212
213    def pids(self):
214        """Gets a list of process IDs.
215
216        Filters the list based on the configuration (e.g., specific PID).
217        Excludes kernel threads and the smem2 process itself if a filter is active.
218
219        Returns:
220            list[int]: A list of process IDs.
221        """
222        return [
223            int(e)
224            for e in self.listdir()
225            if e.isdigit()
226            and not self._iskernel(e)
227            and ((self.config.pid and self.config.pid == int(e)) or not self.config.pid)
228            and not ((int(e) == self.config.ownpid) and self.config.processfilter)
229        ]
230        # exclude own process when processfilter
231
232    def mapdata(self, pid):
233        """Gets the smaps data for a process.
234
235        It prefers using smaps_rollup for performance if available and configured.
236
237        Args:
238            pid (str): The process ID.
239
240        Returns:
241            list[str]: The lines from the smaps or smaps_rollup file, or an empty list on error.
242        """
243        if self.config.rollup:
244            try:
245                return self.readlines(
246                    "%s/smaps_rollup" % pid
247                )  # smaps_rollup new from kernel 4.14, at least 7x faster for non maping listing
248            except Exception:
249                pass
250        try:
251            return self.readlines("%s/smaps" % pid)
252        except (RuntimeError, PermissionError):
253            return []
254
255    def pidcmd(self, pid):
256        """Gets the command line for a process.
257
258        Args:
259            pid (str): The process ID.
260
261        Returns:
262            str: The command line of the process, or '?' on error.
263        """
264        try:
265            c = self.read("%s/cmdline" % pid)[:-1]
266            c = c.replace("\0", " ")
267            if self.config.basename and not c == "":
268                c = os.path.basename(c.split()[0])
269            return c
270        except Exception:
271            return "?"
272
273    def piduser(self, pid):
274        """Gets the UID of the process owner.
275
276        Args:
277            pid (str): The process ID.
278
279        Returns:
280            int: The user ID of the process owner, or -1 on error.
281        """
282        try:
283            return self._stat("%d" % pid).st_uid
284        except Exception:
285            return -1
286
287    def username(self, uid):
288        """Gets the username for a UID.
289
290        Args:
291            uid (int): The user ID.
292
293        Returns:
294            str: The username, or '?' if the UID is -1.
295        """
296        return "?" if uid == -1 else self._uidcache(uid)
297
298    def pidusername(self, pid):
299        """Gets the username of the process owner.
300
301        Args:
302            pid (int or str): The process ID.
303
304        Returns:
305            str: The username of the process owner.
306        """
307        return self.username(self.piduser(pid))
308
309    def pidtostr(self, pid):
310        """Converts a PID to a string.
311
312        Args:
313            pid (int): The process ID.
314
315        Returns:
316            str: The process ID as a string.
317        """
318        return str(pid)
319
320    def pidname(self, pid):
321        """Gets the process name (from /proc/<pid>/comm).
322
323        Args:
324            pid (str): The process ID.
325
326        Returns:
327            str: The name of the process, or '?' on error.
328        """
329        try:
330            return self.read("%s/comm" % pid)[:-1]
331        except Exception:
332            return "?"

Helper class to handle /proc/ filesystem data

ProcessData(config: SmemConfig)
179    def __init__(self, config: SmemConfig):
180        """Initializes ProcessData.
181
182        Args:
183            config (SmemConfig): The smem2 configuration.
184        """
185        self._uidcache = UIDCache()
186        self.config = config

Initializes ProcessData.

Arguments:
  • config (SmemConfig): The smem2 configuration.
config
def pids(self):
213    def pids(self):
214        """Gets a list of process IDs.
215
216        Filters the list based on the configuration (e.g., specific PID).
217        Excludes kernel threads and the smem2 process itself if a filter is active.
218
219        Returns:
220            list[int]: A list of process IDs.
221        """
222        return [
223            int(e)
224            for e in self.listdir()
225            if e.isdigit()
226            and not self._iskernel(e)
227            and ((self.config.pid and self.config.pid == int(e)) or not self.config.pid)
228            and not ((int(e) == self.config.ownpid) and self.config.processfilter)
229        ]
230        # exclude own process when processfilter

Gets a list of process IDs.

Filters the list based on the configuration (e.g., specific PID). Excludes kernel threads and the smem2 process itself if a filter is active.

Returns:

list[int]: A list of process IDs.

def mapdata(self, pid):
232    def mapdata(self, pid):
233        """Gets the smaps data for a process.
234
235        It prefers using smaps_rollup for performance if available and configured.
236
237        Args:
238            pid (str): The process ID.
239
240        Returns:
241            list[str]: The lines from the smaps or smaps_rollup file, or an empty list on error.
242        """
243        if self.config.rollup:
244            try:
245                return self.readlines(
246                    "%s/smaps_rollup" % pid
247                )  # smaps_rollup new from kernel 4.14, at least 7x faster for non maping listing
248            except Exception:
249                pass
250        try:
251            return self.readlines("%s/smaps" % pid)
252        except (RuntimeError, PermissionError):
253            return []

Gets the smaps data for a process.

It prefers using smaps_rollup for performance if available and configured.

Arguments:
  • pid (str): The process ID.
Returns:

list[str]: The lines from the smaps or smaps_rollup file, or an empty list on error.

def pidcmd(self, pid):
255    def pidcmd(self, pid):
256        """Gets the command line for a process.
257
258        Args:
259            pid (str): The process ID.
260
261        Returns:
262            str: The command line of the process, or '?' on error.
263        """
264        try:
265            c = self.read("%s/cmdline" % pid)[:-1]
266            c = c.replace("\0", " ")
267            if self.config.basename and not c == "":
268                c = os.path.basename(c.split()[0])
269            return c
270        except Exception:
271            return "?"

Gets the command line for a process.

Arguments:
  • pid (str): The process ID.
Returns:

str: The command line of the process, or '?' on error.

def piduser(self, pid):
273    def piduser(self, pid):
274        """Gets the UID of the process owner.
275
276        Args:
277            pid (str): The process ID.
278
279        Returns:
280            int: The user ID of the process owner, or -1 on error.
281        """
282        try:
283            return self._stat("%d" % pid).st_uid
284        except Exception:
285            return -1

Gets the UID of the process owner.

Arguments:
  • pid (str): The process ID.
Returns:

int: The user ID of the process owner, or -1 on error.

def username(self, uid):
287    def username(self, uid):
288        """Gets the username for a UID.
289
290        Args:
291            uid (int): The user ID.
292
293        Returns:
294            str: The username, or '?' if the UID is -1.
295        """
296        return "?" if uid == -1 else self._uidcache(uid)

Gets the username for a UID.

Arguments:
  • uid (int): The user ID.
Returns:

str: The username, or '?' if the UID is -1.

def pidusername(self, pid):
298    def pidusername(self, pid):
299        """Gets the username of the process owner.
300
301        Args:
302            pid (int or str): The process ID.
303
304        Returns:
305            str: The username of the process owner.
306        """
307        return self.username(self.piduser(pid))

Gets the username of the process owner.

Arguments:
  • pid (int or str): The process ID.
Returns:

str: The username of the process owner.

def pidtostr(self, pid):
309    def pidtostr(self, pid):
310        """Converts a PID to a string.
311
312        Args:
313            pid (int): The process ID.
314
315        Returns:
316            str: The process ID as a string.
317        """
318        return str(pid)

Converts a PID to a string.

Arguments:
  • pid (int): The process ID.
Returns:

str: The process ID as a string.

def pidname(self, pid):
320    def pidname(self, pid):
321        """Gets the process name (from /proc/<pid>/comm).
322
323        Args:
324            pid (str): The process ID.
325
326        Returns:
327            str: The name of the process, or '?' on error.
328        """
329        try:
330            return self.read("%s/comm" % pid)[:-1]
331        except Exception:
332            return "?"

Gets the process name (from /proc//comm).

Arguments:
  • pid (str): The process ID.
Returns:

str: The name of the process, or '?' on error.

Inherited Members
Proc
listdir
read
readlines
version
def totalmem(config: SmemConfig):
335def totalmem(config: SmemConfig):
336    """Calculates the total memory to be used as a reference for percentages.
337
338    It can be based on real memory from dmidecode, a user-provided value,
339    or the 'MemTotal' value from /proc/meminfo.
340
341    Args:
342        config (SmemConfig): The smem2 configuration.
343
344    Returns:
345        int: The total memory in kilobytes.
346    """
347    if config.realmem:
348        return fromunits(config.realmem) / 1024
349    else:
350        if config.system or config.sysdetail:
351            try:
352                # only for systems running directly on HW
353                ram = (
354                    os.popen(
355                        "dmidecode --type memory 2> /dev/null|awk '/Size: [0-9]+ MB/{sum+=$2};END{print sum}'",
356                    )
357                    .read()
358                    .strip()
359                )
360                if ram.isdigit():
361                    return fromunits(str(int(ram)) + "M") / 1024
362            except Exception:
363                pass
364
365        return MemData()("memtotal")

Calculates the total memory to be used as a reference for percentages.

It can be based on real memory from dmidecode, a user-provided value, or the 'MemTotal' value from /proc/meminfo.

Arguments:
  • config (SmemConfig): The smem2 configuration.
Returns:

int: The total memory in kilobytes.

def pidmaps( pid, proc: ProcessData, config: SmemConfig, nomaps=False):
368def pidmaps(pid, proc: ProcessData, config: SmemConfig, nomaps=False):
369    """Parses the memory maps for a given process.
370
371    It reads /proc/<pid>/smaps or /proc/<pid>/smaps_rollup to get memory
372    usage information for each mapping.
373
374    Args:
375        pid (int or str): The process ID.
376        proc (ProcessData): The ProcessData instance.
377        config (SmemConfig): The smem2 configuration.
378        nomaps (bool, optional): If True, only read the first mapping entry,
379            used for feature detection. Defaults to False.
380
381    Returns:
382        dict: A dictionary of memory mappings, where keys are start addresses.
383    """
384    maps: dict = {}
385    start = None
386    status = None
387    if config.rollup or config.pssdetail or config.rssdetail:
388        try:
389            status = proc.readlines("%s/status" % pid)
390        except (RuntimeError, PermissionError):
391            status = None
392
393    for l in proc.mapdata(pid):
394        f = l.split()
395        if f[-1] == "kB":
396            maps[start][f[0][:-1].lower()] = int(f[1])
397        elif "-" in f[0] and ":" not in f[0]:  # looks like a mapping range
398            start, end = f[0].split("-")
399            start = int(start, 16)
400            name = "<anonymous>"
401            if len(f) > 5:
402                name = f[5]
403                if config.basename:
404                    name = os.path.basename(name)
405            maps[start] = dict(
406                end=int(end, 16),
407                perm=f[1],
408                offset=int(f[2], 16),
409                major=int(f[3].split(":")[0], 16),
410                minor=int(f[3].split(":")[1], 16),
411                inode=int(f[4]),
412                name=name,
413                pss=0,
414                rss=0,
415                uss=0,
416                swap=0,
417                swappss=0,
418                pcent=0,
419                size=0,
420            )
421            if config.pssdetail:
422                (
423                    maps[start]["pss_anon"],
424                    maps[start]["pss_file"],
425                    maps[start]["pss_shmem"],
426                ) = (0, 0, 0)
427
428            # for nomaps mode: stop reading mappings
429            if nomaps:
430                break
431
432    # add pss, rss, uss if available from smaps
433    if not config.rollup:
434        if not config.pssdetail:
435            for m in maps:
436                maps[m]["uss"] = maps[m].get("private_clean", 0) + maps[m].get(
437                    "private_dirty", 0
438                )
439                maps[m]["pss"] = maps[m]["uss"] + maps[m].get("shared_clean", 0) / 2
440                maps[m]["rss"] = maps[m].get("shared_clean", 0) + maps[m].get(
441                    "shared_dirty", 0
442                )
443        if config.rssdetail and status:
444            # from /proc/pid/status
445            for s in status:
446                if "RssAnon" in s:
447                    maps[start]["rss_anon"] = int(s.split()[1])
448                elif "RssFile" in s:
449                    maps[start]["rss_file"] = int(s.split()[1])
450                elif "RssShmem" in s:
451                    maps[start]["rss_shmem"] = int(s.split()[1])
452
453        # Parse VmSize for VSS calculation
454        if status:
455            for s in status:
456                if "VmSize:" in s:
457                    # Set size for the last mapping (rollup case) or all mappings
458                    if config.rollup and start:
459                        maps[start]["size"] = int(s.split()[1])
460                    else:
461                        # For non-rollup, distribute VmSize across all mappings
462                        total_size = int(s.split()[1])
463                        if maps:
464                            size_per_map = (
465                                total_size // len(maps) if len(maps) > 0 else 0
466                            )
467                            for m in maps:
468                                maps[m]["size"] = size_per_map
469                    break
470
471    # Apply mapfilter if specified
472    if config.mapfilter and not nomaps:
473        f = {}
474        for m in maps:
475            if not filters(config.mapfilter, m, config, lambda x: maps[x]["name"]):
476                f[m] = maps[m]
477        return f
478    return maps

Parses the memory maps for a given process.

It reads /proc//smaps or /proc//smaps_rollup to get memory usage information for each mapping.

Arguments:
  • pid (int or str): The process ID.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
  • nomaps (bool, optional): If True, only read the first mapping entry, used for feature detection. Defaults to False.
Returns:

dict: A dictionary of memory mappings, where keys are start addresses.

def maptotals(pids, proc: ProcessData, config: SmemConfig):
481def maptotals(pids, proc: ProcessData, config: SmemConfig):
482    """Aggregates memory usage data for a list of pids, including map details.
483
484    For each process, it gets memory totals (PSS, RSS, USS, etc.) and
485    detailed mapping information. It applies process and user filters.
486
487    Args:
488        pids (list[int]): A list of process IDs.
489        proc (ProcessData): The ProcessData instance.
490        config (SmemConfig): The smem2 configuration.
491
492    Returns:
493        dict: A dictionary where keys are process IDs and values are dictionaries
494              containing memory totals and mapping details for each process.
495    """
496    pt: dict = {}
497    for p in pids:
498        if filters(
499            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
500        ) or filters(config.userfilter, p, config, proc.pidusername):
501            continue
502        maps = pidmaps(p, proc, config)
503        if maps:
504            pt[p] = {}
505            pt[p]["maps"] = maps
506            totals = pidtotals(p, proc, config)
507            # Flatten the totals into the main dict for compatibility with display code
508            pt[p].update(totals)
509    return pt

Aggregates memory usage data for a list of pids, including map details.

For each process, it gets memory totals (PSS, RSS, USS, etc.) and detailed mapping information. It applies process and user filters.

Arguments:
  • pids (list[int]): A list of process IDs.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary where keys are process IDs and values are dictionaries containing memory totals and mapping details for each process.

def pidtotals(pid, proc: ProcessData, config: SmemConfig):
512def pidtotals(pid, proc: ProcessData, config: SmemConfig):
513    """Calculates the total memory usage (PSS, RSS, USS, etc.) for a single process.
514
515    It sums up the values from all memory mappings of the process.
516
517    Args:
518        pid (int or str): The process ID.
519        proc (ProcessData): The ProcessData instance.
520        config (SmemConfig): The smem2 configuration.
521
522    Returns:
523        dict: A dictionary with total PSS, RSS, USS, Swap, etc., for the process.
524    """
525    totals: dict = {}
526    maps = pidmaps(pid, proc, config)
527    if maps:
528        maplist = maps.values()
529        totals["pss"] = sum([m.get("pss", 0) for m in maplist])
530        totals["rss"] = sum([m.get("rss", 0) for m in maplist])
531        private_clean = sum(m.get("private_clean", 0) for m in maplist)
532        private_dirty = sum(m.get("private_dirty", 0) for m in maplist)
533        totals["uss"] = private_clean + private_dirty
534        totals["swap"] = sum([m.get("swap", 0) for m in maplist])
535        totals["size"] = sum([m.get("size", 0) for m in maplist])
536        totals["maps"] = len(maps)
537        if config.swappss:
538            totals["swappss"] = sum([m.get("swappss", 0) for m in maplist])
539        if config.pssdetail:
540            totals["pss_anon"] = sum([m.get("pss_anon", 0) for m in maplist])
541            totals["pss_file"] = sum([m.get("pss_file", 0) for m in maplist])
542            totals["pss_shmem"] = sum([m.get("pss_shmem", 0) for m in maplist])
543        if config.rssdetail:
544            totals["rss_anon"] = sum([m.get("rss_anon", 0) for m in maplist])
545            totals["rss_file"] = sum([m.get("rss_file", 0) for m in maplist])
546            totals["rss_shmem"] = sum([m.get("rss_shmem", 0) for m in maplist])
547    return totals

Calculates the total memory usage (PSS, RSS, USS, etc.) for a single process.

It sums up the values from all memory mappings of the process.

Arguments:
  • pid (int or str): The process ID.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary with total PSS, RSS, USS, Swap, etc., for the process.

def usertotals(pids, proc: ProcessData, config: SmemConfig):
550def usertotals(pids, proc: ProcessData, config: SmemConfig):
551    """Aggregates memory usage by user.
552
553    It groups processes by user and calculates the total memory usage for each user.
554
555    Args:
556        pids (list[int]): A list of process IDs to analyze.
557        proc (ProcessData): The ProcessData instance.
558        config (SmemConfig): The smem2 configuration.
559
560    Returns:
561        dict: A dictionary where keys are user IDs and values are dictionaries
562              containing process lists and memory totals for each user.
563    """
564    ut: dict = {}
565    for p in pids:
566        if filters(
567            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
568        ) or filters(config.userfilter, p, config, proc.pidusername):
569            continue
570        u = proc.piduser(p)
571        if u not in ut:
572            ut[u] = {"pids": [], "name": "?"}
573            ut[u]["name"] = proc.username(u)
574        ut[u]["pids"].append(p)
575
576    # get totals per user
577    for u in ut:
578        ut[u]["totals"] = processtotals(ut[u]["pids"], proc, config)
579    return ut

Aggregates memory usage by user.

It groups processes by user and calculates the total memory usage for each user.

Arguments:
  • pids (list[int]): A list of process IDs to analyze.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary where keys are user IDs and values are dictionaries containing process lists and memory totals for each user.

def cmdtotals(pids, proc: ProcessData, config: SmemConfig):
582def cmdtotals(pids, proc: ProcessData, config: SmemConfig):
583    """Aggregates memory usage by command line.
584
585    It groups processes by their command line and calculates the total memory
586    usage for each command.
587
588    Args:
589        pids (list[int]): A list of process IDs to analyze.
590        proc (ProcessData): The ProcessData instance.
591        config (SmemConfig): The smem2 configuration.
592
593    Returns:
594        dict: A dictionary where keys are command lines and values are
595              dictionaries containing process lists and memory totals.
596    """
597    # totals per command
598    ct: dict = {}
599
600    for p in pids:
601        if filters(
602            config.processfilter, p, config, proc.pidcmd, proc.pidtostr, proc.pidname
603        ) or filters(config.userfilter, p, config, proc.pidusername):
604            continue
605        cmdline = proc.pidcmd(p)
606        if not cmdline:
607            continue
608
609        parts = cmdline.split()
610        if not parts:
611            continue
612        c = parts[0]
613
614        # if multiple processes per command, then a list of pids
615        if c in ct:
616            ct[c]["pids"].append(p)
617        else:
618            # first pid for this command
619            ct[c] = {"pids": [p]}
620
621    # get totals per command
622    for c in ct:
623        ct[c]["totals"] = processtotals(ct[c]["pids"], proc, config)
624        if "totals" in ct[c] and ct[c]["totals"]:
625            ct[c]["name"] = c
626            ct[c]["user"] = proc.pidusername(ct[c]["pids"][0])
627
628    # remove commands with empty totals
629    for c in list(ct.keys()):
630        if "totals" not in ct[c] or not ct[c]["totals"]:
631            del ct[c]
632    return ct

Aggregates memory usage by command line.

It groups processes by their command line and calculates the total memory usage for each command.

Arguments:
  • pids (list[int]): A list of process IDs to analyze.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary where keys are command lines and values are dictionaries containing process lists and memory totals.

def sortmaps(totals, key):
635def sortmaps(totals, key):
636    """Sorts a dictionary of totals by a given key.
637
638    Args:
639        totals (dict): A dictionary where values are another dictionary
640                       containing a 'totals' key.
641        key (str): The key within the nested 'totals' dictionary to sort by.
642
643    Returns:
644        list: A sorted list of the input dictionary's values.
645    """
646    # sort by key
647    return sorted(totals.values(), key=lambda x: x["totals"][key], reverse=True)

Sorts a dictionary of totals by a given key.

Arguments:
  • totals (dict): A dictionary where values are another dictionary containing a 'totals' key.
  • key (str): The key within the nested 'totals' dictionary to sort by.
Returns:

list: A sorted list of the input dictionary's values.

def units(x):
650def units(x):
651    """Converts a value in bytes to a human-readable string with units (K, M, G).
652
653    Args:
654        x (int or float): The value in bytes.
655
656    Returns:
657        str: A human-readable string representation of the value.
658    """
659    if x > 1024 * 1024 * 1024:
660        return "%.1fG" % (x / (1024.0 * 1024 * 1024))
661    if x > 1024 * 1024:
662        return "%.1fM" % (x / (1024.0 * 1024))
663    if x > 1024:
664        return "%.1fK" % (x / 1024.0)
665    return str(x)

Converts a value in bytes to a human-readable string with units (K, M, G).

Arguments:
  • x (int or float): The value in bytes.
Returns:

str: A human-readable string representation of the value.

def fromunits(x):
668def fromunits(x):
669    """Converts a human-readable string with units (K, M, G, etc.) to bytes.
670
671    Args:
672        x (str): The string with a value and unit (e.g., "1024M").
673
674    Returns:
675        int: The value in bytes.
676
677    Raises:
678        SystemExit: If the unit is not recognized.
679    """
680    s = dict(
681        k=2**10,
682        K=2**10,
683        kB=2**10,
684        KB=2**10,
685        M=2**20,
686        MB=2**20,
687        G=2**30,
688        GB=2**30,
689        T=2**40,
690        TB=2**40,
691    )
692    for k, v in list(s.items()):
693        if x.endswith(k):
694            return int(float(x[: -len(k)]) * v)
695    sys.stderr.write("Memory size should be written with units, for example 1024M\n")
696    sys.exit(-1)

Converts a human-readable string with units (K, M, G, etc.) to bytes.

Arguments:
  • x (str): The string with a value and unit (e.g., "1024M").
Returns:

int: The value in bytes.

Raises:
  • SystemExit: If the unit is not recognized.
def processtotals(pids, proc: ProcessData, config: SmemConfig):
699def processtotals(pids, proc: ProcessData, config: SmemConfig):
700    """Sums up memory usage totals for a list of processes.
701
702    Args:
703        pids (list[int]): The list of process IDs.
704        proc (ProcessData): The ProcessData instance.
705        config (SmemConfig): The smem2 configuration.
706
707    Returns:
708        dict: A dictionary with the summed PSS, RSS, USS, etc., for the
709              given processes.
710    """
711    # Sums up totals of a pid list
712    t = {"uss": 0, "pss": 0, "rss": 0, "swap": 0, "size": 0}
713    if config.swappss:
714        t["swappss"] = 0
715    for p in pids:
716        pt = pidtotals(p, proc, config)
717        if pt:
718            for k in t:
719                if k in pt:
720                    t[k] += pt[k]
721    return t

Sums up memory usage totals for a list of processes.

Arguments:
  • pids (list[int]): The list of process IDs.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary with the summed PSS, RSS, USS, etc., for the given processes.

def get_process_data(proc: ProcessData, config: SmemConfig):
724def get_process_data(proc: ProcessData, config: SmemConfig):
725    """Collects and returns memory usage data for each process.
726
727    Args:
728        proc (ProcessData): The ProcessData instance.
729        config (SmemConfig): The smem2 configuration.
730
731    Returns:
732        dict: A dictionary containing memory data for each process,
733              as returned by maptotals.
734    """
735    # Collect process data
736    pids = proc.pids()
737    data = maptotals(pids, proc, config)
738    return data

Collects and returns memory usage data for each process.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary containing memory data for each process, as returned by maptotals.

def get_map_data(proc: ProcessData, config: SmemConfig):
741def get_map_data(proc: ProcessData, config: SmemConfig):
742    """Collects and returns memory usage data aggregated by mapping name.
743
744    Args:
745        proc (ProcessData): The ProcessData instance.
746        config (SmemConfig): The smem2 configuration.
747
748    Returns:
749        dict: A dictionary containing memory data aggregated by mapping name.
750    """
751    # Collect map data - aggregate by mapping name
752    pids = proc.pids()
753    data = mapnametotals(pids, proc, config)
754    return data

Collects and returns memory usage data aggregated by mapping name.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary containing memory data aggregated by mapping name.

def mapnametotals(pids, proc: ProcessData, config: SmemConfig):
757def mapnametotals(pids, proc: ProcessData, config: SmemConfig):
758    """Aggregates memory usage by mapping name across a list of processes.
759
760    It iterates through processes and their memory maps, summing up memory
761    statistics for each unique mapping name.
762
763    Args:
764        pids (list[int]): A list of process IDs.
765        proc (ProcessData): The ProcessData instance.
766        config (SmemConfig): The smem2 configuration.
767
768    Returns:
769        dict: A dictionary where keys are mapping names and values are
770              dictionaries of aggregated memory statistics.
771    """
772    totals: dict = {}
773    for pid in pids:
774        # Apply filters like the original version
775        if filters(
776            config.processfilter, pid, config, proc.pidcmd, proc.pidtostr, proc.pidname
777        ) or filters(config.userfilter, pid, config, proc.pidusername):
778            continue
779        try:
780            maps = pidmaps(pid, proc, config)
781            seen = {}
782            for m in list(maps.keys()):
783                name = maps[m]["name"]
784                if name not in totals:
785                    t = dict(
786                        size=0,
787                        rss=0,
788                        pss=0,
789                        shared_clean=0,
790                        shared_dirty=0,
791                        private_clean=0,
792                        count=0,
793                        private_dirty=0,
794                        referenced=0,
795                        swap=0,
796                        pids=0,
797                    )
798                    if config.swappss:
799                        t["swappss"] = 0
800                else:
801                    t = totals[name]
802                for k in t:
803                    t[k] += maps[m].get(k, 0)
804                t["count"] += 1
805                if name not in seen:
806                    t["pids"] += 1
807                    seen[name] = 1
808                totals[name] = t
809        except (OSError, IOError):
810            continue
811    return totals

Aggregates memory usage by mapping name across a list of processes.

It iterates through processes and their memory maps, summing up memory statistics for each unique mapping name.

Arguments:
  • pids (list[int]): A list of process IDs.
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary where keys are mapping names and values are dictionaries of aggregated memory statistics.

def get_user_data(proc: ProcessData, config: SmemConfig):
814def get_user_data(proc: ProcessData, config: SmemConfig):
815    """Collects and returns memory usage data aggregated by user.
816
817    Args:
818        proc (ProcessData): The ProcessData instance.
819        config (SmemConfig): The smem2 configuration.
820
821    Returns:
822        dict: A dictionary containing memory data aggregated by user.
823    """
824    # Collect user data
825    pids = proc.pids()
826    data = usertotals(pids, proc, config)
827    return data

Collects and returns memory usage data aggregated by user.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary containing memory data aggregated by user.

def get_cmd_data(proc: ProcessData, config: SmemConfig):
830def get_cmd_data(proc: ProcessData, config: SmemConfig):
831    """Collects and returns memory usage data aggregated by command.
832
833    Args:
834        proc (ProcessData): The ProcessData instance.
835        config (SmemConfig): The smem2 configuration.
836
837    Returns:
838        dict: A dictionary containing memory data aggregated by command.
839    """
840    # Collect cmd data
841    pids = proc.pids()
842    data = cmdtotals(pids, proc, config)
843    return data

Collects and returns memory usage data aggregated by command.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

dict: A dictionary containing memory data aggregated by command.

def get_system_data(proc: ProcessData, config: SmemConfig):
846def get_system_data(proc: ProcessData, config: SmemConfig):
847    """Collects and returns system-wide memory usage data.
848
849    This provides a breakdown of memory usage by different system components
850    like kernel, firmware, userspace, etc.
851
852    Args:
853        proc (ProcessData): The ProcessData instance.
854        config (SmemConfig): The smem2 configuration.
855
856    Returns:
857        list[tuple]: A list of tuples, where each tuple represents a memory
858                     area and its usage in PSS and RSS.
859    """
860    t = totalmem(config)
861    m = MemData()
862    mt = m("memtotal")
863    f = m("memfree")
864    kernel = kernelsize(config)
865
866    # total amount mapped into userspace (ie mapped an unmapped pages) /dev/zero mappings
867    u = m("anonpages") + m("mapped")
868
869    # total amount used by hardware
870    fh = int(max(t - mt - kernel, 0))
871
872    # total amount allocated by kernel not for userspace
873    kd = mt - f - u
874
875    # total amount in kernel caches
876    kdc = m("buffers") + m("sreclaimable") + m("cached") - m("mapped")
877
878    if config.sysdetail:
879        shmp = mapshared(proc, config)  # shared memory mapped by processes
880        mapzero = mapdevzero(
881            proc, config
882        )  # dev/zero mapped as files should be subtracted from files Mapped from userspace
883        mapped = m("mapped") - mapzero
884        modules = kernelmodsize(proc, config)
885        shm = m("shmem") - shmp
886
887        mounts = proc.readlines("mounts")
888        ramfs = 0
889        for r in mounts:
890            if "ramfs" in r:
891                try:
892                    ramfs += int(
893                        (os.popen("du -sk %s" % r.split()[1]).read()).split()[0]
894                    )
895                except Exception:
896                    pass
897        filecache = m("cached") - mapped - shm - ramfs
898        unknown = (
899            mt
900            - f
901            - u
902            - (
903                modules
904                + m("pagetables")
905                + m("kernelstack")
906                + m("slab")
907                + m("buffers")
908                + filecache
909                + shm
910                + ramfs
911            )
912        )
913        if unknown < 0:
914            unknown = 0
915
916    lines: list[tuple]
917    if config.sysdetail:
918        lines = [
919            ("firmware/hardware", fh, 0, 0),
920            ("kernel image", kernel, 0, 0),
921            ("kernel modules", modules, 0, 0),
922            ("page tables", m("pagetables"), 0, 0),
923            ("kernel stack", m("kernelstack"), 0, 0),
924            ("slab (all/SReclaimable)", m("slab"), m("sreclaimable"), 0),
925            ("buffers", m("buffers"), m("buffers"), 0),
926            ("cached (w/o mapped,tmpfs,ramfs)", filecache, filecache, 0),
927            ("shared (non process tmpfs)", shm, 0, 0),
928            ("ramfs", ramfs, 0, 0),
929            ("unknown", unknown, 0, 0),
930            ("processes (all/mapped files)", u, mapped, 0),
931            ("free memory", f, f, 0),
932        ]
933        if "all" in config.columns or "details" in config.columns:
934            line_all: list[tuple] = [
935                ("-------------------------------", 0, 0, 0),
936                ("/dev/zero mapped", 0, 0, mapzero),
937                ("shared by processes", 0, 0, shmp),
938                ("unevictable", 0, 0, m("unevictable")),
939                ("dirty (unwritten to file)", 0, 0, m("dirty")),
940                ("swapped", 0, 0, m("swaptotal") - m("swapfree")),
941                ("available (estimated)", 0, 0, m("memavailable")),
942            ]
943            lines = lines + line_all
944    else:
945        lines = [
946            ("firmware/hardware", fh, 0),
947            ("kernel image", kernel, 0),
948            ("kernel dynamic memory", kd, kdc),
949            ("userspace memory", u, m("mapped")),
950            ("free memory", f, f),
951        ]
952    return lines

Collects and returns system-wide memory usage data.

This provides a breakdown of memory usage by different system components like kernel, firmware, userspace, etc.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

list[tuple]: A list of tuples, where each tuple represents a memory area and its usage in PSS and RSS.

def mapdevzero(proc: ProcessData, config: SmemConfig):
955def mapdevzero(proc: ProcessData, config: SmemConfig):
956    """Calculates memory mapped to /dev/zero.
957
958    Some Linux versions count memory mapped to /dev/zero as 'Mapped' instead
959    of 'AnonPages'. This function calculates that amount.
960
961    Args:
962        proc (ProcessData): The ProcessData instance.
963        config (SmemConfig): The smem2 configuration.
964
965    Returns:
966        int: Total PSS of memory mapped to /dev/zero, in kilobytes.
967    """
968    ps = proc.pids()
969    config.mapfilter = "^/dev/zero$"
970    pt = maptotals(ps, proc, config)
971    t = 0.0
972    for r in pt:
973        t += pt[r]["pss"]
974    return int(t)

Calculates memory mapped to /dev/zero.

Some Linux versions count memory mapped to /dev/zero as 'Mapped' instead of 'AnonPages'. This function calculates that amount.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

int: Total PSS of memory mapped to /dev/zero, in kilobytes.

def mapshared(proc: ProcessData, config: SmemConfig):
977def mapshared(proc: ProcessData, config: SmemConfig):
978    """Calculates memory shared by multiple processes (via SYSV IPC).
979
980    Args:
981        proc (ProcessData): The ProcessData instance.
982        config (SmemConfig): The smem2 configuration.
983
984    Returns:
985        int: Total PSS of memory from SYSV shared segments, in kilobytes.
986    """
987    ps = proc.pids()
988    config.mapfilter = "^/SYSV*"
989    pt = maptotals(ps, proc, config)
990    t = 0.0
991    for r in pt:
992        t += pt[r]["pss"]
993    return int(t)

Calculates memory shared by multiple processes (via SYSV IPC).

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

int: Total PSS of memory from SYSV shared segments, in kilobytes.

def kernelsize(config: SmemConfig):
 996def kernelsize(config: SmemConfig):
 997    """Determines the size of the kernel image from the file specified.
 998
 999    Args:
1000        config (SmemConfig): The smem2 configuration.
1001
1002    Returns:
1003        int: The kernel image size in kilobytes.
1004    """
1005    kernelsize_val = 0
1006    if config.kernel:
1007        try:
1008            d = os.popen("size %s" % config.kernel).readlines()[1].split()
1009            if int(d[1]) == 0:  # data part missing, seems like packed file
1010                # try some heuristic to find gzipped part in kernel image
1011                with open(config.kernel, "rb") as f:
1012                    packedkernel = f.read()
1013                pos = packedkernel.find(b"\x1f\x8b")
1014                if pos >= 0 and pos < 25000:
1015                    if not config.quiet:
1016                        sys.stderr.write(
1017                            "Parameter '%s' should be an original uncompressed compiled kernel file.\n"
1018                            % config.kernel
1019                        )
1020                        sys.stderr.write(
1021                            "Maybe uncompressed kernel can be extracted by the command:\n"
1022                            "  dd if=%s bs=1 skip=%d | gzip -d >%s.unpacked\n\n"
1023                            % (config.kernel, pos, config.kernel)
1024                        )
1025            else:
1026                kernelsize_val = int(int(d[3]) / 1024 + 0.5)
1027        except Exception:
1028            pass  # Fail silently if size command fails or kernel file is not found
1029    return kernelsize_val

Determines the size of the kernel image from the file specified.

Arguments:
  • config (SmemConfig): The smem2 configuration.
Returns:

int: The kernel image size in kilobytes.

def kernelmodsize(proc: ProcessData, config: SmemConfig):
1032def kernelmodsize(proc: ProcessData, config: SmemConfig):
1033    """Calculates the total size of loaded kernel modules.
1034
1035    It reads /proc/modules to get the size of each module.
1036
1037    Args:
1038        proc (ProcessData): The ProcessData instance.
1039        config (SmemConfig): The smem2 configuration.
1040
1041    Returns:
1042        int: The total size of kernel modules in kilobytes.
1043    """
1044    ms = 0.0
1045    try:
1046        md = proc.readlines("modules")
1047        for m in md:
1048            ms += int(m.split()[1])
1049        ms /= 1024
1050    except Exception:
1051        if not config.quiet:
1052            sys.stderr.write("Detection of kernel modules size failed\n")
1053    return int(ms)

Calculates the total size of loaded kernel modules.

It reads /proc/modules to get the size of each module.

Arguments:
  • proc (ProcessData): The ProcessData instance.
  • config (SmemConfig): The smem2 configuration.
Returns:

int: The total size of kernel modules in kilobytes.

def setdatasources(config: SmemConfig, proc: ProcessData):
1056def setdatasources(config: SmemConfig, proc: ProcessData):
1057    """Detects kernel features and updates the configuration accordingly.
1058
1059    It checks for support of PSS, SwapPSS, and detailed PSS/RSS reporting
1060    by inspecting the smaps of the current process. It also determines if
1061    the faster smaps_rollup can be used.
1062
1063    Args:
1064        config (SmemConfig): The smem2 configuration object to update.
1065        proc (ProcessData): The ProcessData instance.
1066
1067    Returns:
1068        SmemConfig: The updated configuration object.
1069    """
1070    """New kernel features detection"""
1071    config.ownpid = os.getpid()
1072
1073    config.rssdetail = False
1074    config.pssdetail = True
1075    config.rollup = True
1076    rd = proc.readlines("%s/status" % config.ownpid)
1077    for r in rd:
1078        if "RssAnon:" in r:
1079            config.rssdetail = True
1080            break
1081
1082    map_data = pidmaps(config.ownpid, proc, config, nomaps=True)
1083    map_data = map_data[list(map_data.keys())[0]]
1084
1085    if "pss" not in map_data and not config.quiet:
1086        sys.stderr.write("Warning: Kernel does not appear to support PSS measurement\n")
1087
1088    if "swappss" in map_data:
1089        config.swappss = True
1090    else:
1091        config.swappss = False
1092        if not config.quiet:
1093            sys.stderr.write(
1094                "Warning: Kernel does not appear to support SwapPSS measurement\n"
1095            )
1096
1097    if "pss_anon" in map_data:
1098        config.pssdetail = True
1099    else:
1100        config.pssdetail = False
1101        if not config.quiet:
1102            sys.stderr.write(
1103                "Warning: Kernel does not appear to support Pss Anon/File/Shmem measurement\n"
1104            )
1105
1106    """ some smem2 features need full smaps"""
1107    if (
1108        config.mapfilter
1109        or config.mappings
1110        or "maps" in config.columns
1111        or ("all" in config.columns and not config.groupcmd)
1112    ):
1113        config.rollup = False
1114    return config

Detects kernel features and updates the configuration accordingly.

It checks for support of PSS, SwapPSS, and detailed PSS/RSS reporting by inspecting the smaps of the current process. It also determines if the faster smaps_rollup can be used.

Arguments:
  • config (SmemConfig): The smem2 configuration object to update.
  • proc (ProcessData): The ProcessData instance.
Returns:

SmemConfig: The updated configuration object.

def filters(opt, arg, config: SmemConfig, *sources):
1117def filters(opt, arg, config: SmemConfig, *sources):
1118    """A generic filter function based on regular expressions.
1119
1120    It checks if a given argument should be filtered out by matching a regex
1121    against one or more source strings.
1122
1123    Args:
1124        opt (str or None): The regex pattern to search for. If None or empty,
1125            the function returns False.
1126        arg: The argument to pass to the source functions (e.g., a PID).
1127        config (SmemConfig): The smem2 configuration (for ignorecase flag).
1128        *sources (callable): Variable length list of functions that take `arg`
1129            and return a string to be searched.
1130
1131    Returns:
1132        bool: True if the item should be filtered out (no match found),
1133              False otherwise (a match was found or no filter is active).
1134    """
1135    if not opt:
1136        return False
1137    for f in sources:
1138        if re.search(opt, f(arg), re.I if config.ignorecase else 0):
1139            return False
1140    return True

A generic filter function based on regular expressions.

It checks if a given argument should be filtered out by matching a regex against one or more source strings.

Arguments:
  • opt (str or None): The regex pattern to search for. If None or empty, the function returns False.
  • arg: The argument to pass to the source functions (e.g., a PID).
  • config (SmemConfig): The smem2 configuration (for ignorecase flag).
  • *sources (callable): Variable length list of functions that take arg and return a string to be searched.
Returns:

bool: True if the item should be filtered out (no match found), False otherwise (a match was found or no filter is active).