On Tue, Jun 13, 2023 at 05:09:48PM +0100, Ryan Roberts wrote: > Hi All, > > I thought I would try my luck with this pair of patches... Ack on the idea. Actually I have a script to do just this, but it's based on pagemap (attaching the script at the end). > This series adds new entries to /proc/pid/smaps[_rollup] to report on physically > contiguous runs of memory. The first patch reports on the sizes of the runs by > binning into power-of-2 blocks and reporting how much memory is in which bin. > The second patch reports on how much of the memory is contpte-mapped in the page > table (this is a hint that arm64 supports to tell the HW that a range of ptes > map physically contiguous memory). > > With filesystems now supporting large folios in the page cache, this provides a > useful way to see what sizes are actually getting mapped. And with the prospect > of large folios for anonymous memory and contpte mapping for conformant large > folios on the horizon, this reporting will become useful to aid application > performance optimization. > > Perhaps I should really be submitting these patches as part of my large anon > folios and contpte sets (which I plan to post soon), but given this touches > the user ABI, I thought it was sensible to post it early and separately to get > feedback. > > It would specifically be good to get feedback on: > > - The exact set of new fields depend on the system that its being run on. Does > this cause problem for compat? (specifically the bins are determined based > on PAGE_SIZE and PMD_SIZE). > - The ContPTEMapped field is effectively arm64-specific. What is the preferred > way to handle arch-specific values if not here? No strong opinions here. === $ cat memory-histogram/mem_hist.py """Script that scans VMAs, outputting histograms regarding memory allocations. Example usage: python3 mem_hist.py --omit-file-backed --omit-unfaulted-vmas For every process on the system, this script scans each VMA, counting the number of order n allocations for 0 <= n <= MAX_ORDER. An order n allocation is a region of memory aligned to a PAGESIZE * (2 ^ n) sized region consisting of 2 ^ n pages in which every page is present (according to the data in /proc/<pid>/pagemap). VMA information as in /proc/<pid>/maps is output for all scanned VMAs along with a histogram of allocation orders. For example, this histogram states that there are 12 order 0 allocations, 4 order 1 allocations, 5 order 2 allocations, and so on: [12, 4, 5, 9, 5, 10, 6, 2, 2, 4, 3, 4] In addition to per-VMA histograms, per-process histograms are printed. Per-process histograms are the sum of the histograms of all VMAs contained within it, allowing for an overview of the memory allocations patterns of the process as a whole. Processes, and VMAs under each process are printed sorted in reverse-lexographic order of historgrams. That is, VMAs containing more high order allocations will be printed after ones containing more low order allocations. The output can thus be easily visually scanned to find VMAs in which hugepage use shows the most potential benefit. To reduce output clutter, the options --omit-file-backed exists to omit VMAs that are file backed (which, outside of tmpfs, don't support transparent hugepages on Linux). Additionally, the option --omit-unfaulted-vmas exists to omit VMAs containing zero resident pages. """ import argparse import functools import re import struct import subprocess import sys ALL_PIDS_CMD = "ps --no-headers -e | awk '{ print $1 }'" # Maximum order the script creates histograms up to. This is by default 9 # since the usual hugepage size on x86 is 2MB which is 2**9 4KB pages MAX_ORDER = 9 PAGE_SIZE = 2**12 BLANK_HIST = [0] * (MAX_ORDER + 1) class Vma: """Represents a virtual memory area. Attributes: proc: Process object in which this VMA is contained start_vaddr: Start virtual address of VMA end_vaddr: End virtual address of VMA perms: Permission string of VMA as in /proc/<pid>/maps (eg. rw-p) mapped_file: Path to file backing this VMA from /proc/<pid>/maps, empty string if not file backed. Note there are some cases in Linux where this may be nonempty and the VMA not file backed (eg. memfds) hist: This VMA's histogram as a list of integers """ def __init__(self, proc, start_vaddr, end_vaddr, perms, mapped_file): self.proc = proc self.start_vaddr = start_vaddr self.end_vaddr = end_vaddr self.perms = perms self.mapped_file = mapped_file def is_file_backed(self): """Returns true if this VMA is file backed, false otherwise.""" # The output printed for memfds (eg. /memfd:crosvm) also happens to be a # valid file path on *nix, so special case them return (bool(re.match("(?:/[^/]+)+", self.mapped_file)) and not bool(re.match("^/memfd:", self.mapped_file))) @staticmethod def bitmask(hi, lo): """Returns a bitmask with the bits from index hi to low+1 set.""" return ((1 << (hi - lo)) - 1) << lo @property @functools.lru_cache(maxsize=50000) def hist(self): """Returns this VMA's histogram as a list.""" hist = BLANK_HIST[:] pagemap_file = safe_open_procfile(self.proc.pid, "pagemap", "rb") if not pagemap_file: err_print( "Cannot open /proc/{0}/pagemap, not generating histogram".format( self.proc.pid)) return hist # Page index of start/end VMA virtual addresses vma_start_page_i = self.start_vaddr // PAGE_SIZE vma_end_page_i = self.end_vaddr // PAGE_SIZE for order in range(0, MAX_ORDER + 1): # If there are less than two previous order pages, there can be no more # pages of a higher order so just break out to save time if order > 0 and hist[order - 1] < 2: break # First and last pages aligned to 2**order bytes in this VMA first_aligned_page = (vma_start_page_i & self.bitmask(64, order)) + 2**order last_aligned_page = vma_end_page_i & self.bitmask(64, order) # Iterate over all order-sized and order-aligned chunks in this VMA for start_page_i in range(first_aligned_page, last_aligned_page, 2**order): if self._is_region_present(pagemap_file, start_page_i, start_page_i + 2**order): hist[order] += 1 # Subtract two lower order VMAs so that we don't double-count # order n VMAs as two order n-1 VMAs as well if order > 0: hist[order - 1] -= 2 pagemap_file.close() return hist def _is_region_present(self, pagemap_file, start_page_i, end_page_i): """Returns True if all pages in the given range are resident. Args: pagemap_file: Opened /proc/<pid>/pagemap file for this process start_page_i: Start page index for range end_page_i: End page index for range Returns: True if all pages from page index start_page_i to end_page_i are present according to the pagemap file, False otherwise. """ pagemap_file.seek(start_page_i * 8) for _ in range(start_page_i, end_page_i): # /proc/<pid>/pagemaps contains an 8 byte value for every page page_info, = struct.unpack("Q", pagemap_file.read(8)) # Bit 63 is set if the page is present if not page_info & (1 << 63): return False return True def __str__(self): return ("{start:016x}-{end:016x} {size:<8} {perms:<4} {hist:<50} " "{mapped_file:<40}").format( start=self.start_vaddr, end=self.end_vaddr, size="%dk" % ((self.end_vaddr - self.start_vaddr) // 1024), perms=self.perms, hist=str(self.hist), mapped_file=str(self.mapped_file)) class Process: """Represents a running process. Attributes: vmas: List of VMA objects representing this processes's VMAs pid: Process PID name: Name of process (read from /proc/<pid>/status """ _MAPS_LINE_REGEX = ("([0-9a-f]+)-([0-9a-f]+) ([r-][w-][x-][ps-]) " "[0-9a-f]+ [0-9a-f]+:[0-9a-f]+ [0-9]+[ ]*(.*)") def __init__(self, pid): self.vmas = [] self.pid = pid self.name = None self._read_name() self._read_vma_info() def _read_name(self): """Reads this Process's name from /proc/<pid>/status.""" get_name_sp = subprocess.Popen( "grep Name: /proc/%d/status | awk '{ print $2 }'" % self.pid, shell=True, stdout=subprocess.PIPE) self.name = get_name_sp.communicate()[0].decode("ascii").strip() def _read_vma_info(self): """Populates this Process's VMA list.""" f = safe_open_procfile(self.pid, "maps", "r") if not f: err_print("Could not read maps for process {0}".format(self.pid)) return for line in f: match = re.match(Process._MAPS_LINE_REGEX, line) start_vaddr = int(match.group(1), 16) end_vaddr = int(match.group(2), 16) perms = match.group(3) mapped_file = match.group(4) if match.lastindex == 4 else None self.vmas.append(Vma(self, start_vaddr, end_vaddr, perms, mapped_file)) f.close() @property @functools.lru_cache(maxsize=50000) def hist(self): """The process-level memory allocation histogram. This is the sum of all VMA histograms for every VMA in this process. For example, if a process had two VMAs with the following histograms: [1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0] [0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0] This would return: [1, 3, 5, 3, 0, 0, 0, 0, 0, 0, 0] """ return [sum(x) for x in zip(*[vma.hist for vma in self.vmas])] def __str__(self): return "process {pid:<18} {name:<25} {hist:<50}".format( pid=self.pid, name=str(self.name), hist=str(self.hist)) def safe_open_procfile(pid, file_name, mode): """Safely open the given file under /proc/<pid>. This catches a variety of common errors bound to happen when using this script (eg. permission denied, process already exited). Args: pid: Pid of process (used to construct /proc/<pid>/) file_name: File directly under /proc/<pid>/ to open mode: Mode to pass to open (eg. "w", "r") Returns: File object corresponding to file requested or None if there was an error """ full_path = "/proc/{0}/{1}".format(pid, file_name) try: return open(full_path, mode) except PermissionError: err_print("Not accessing {0} (permission denied)".format(full_path)) except FileNotFoundError: err_print( "Not opening {0} (does not exist, process {1} likely exited)".format( full_path, pid)) def err_print(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def print_hists(args): """Prints all process and VMA histograms as/per module documentation.""" pid_list_sp = subprocess.Popen( ALL_PIDS_CMD, shell=True, stdout=subprocess.PIPE) pid_list = map(int, pid_list_sp.communicate()[0].splitlines()) procs = [] for pid in pid_list: procs.append(Process(pid)) for proc in sorted(procs, key=lambda p: p.hist[::-1]): # Don't print info on kernel threads or processes we couldn't collect info # on due to insufficent permissions if not proc.vmas: continue print(proc) for vma in sorted(proc.vmas, key=lambda v: v.hist[::-1]): if args.no_unfaulted_vmas and vma.hist == BLANK_HIST: continue elif args.omit_file_backed and vma.is_file_backed(): continue print(" ", vma) if __name__ == "__main__": parser = argparse.ArgumentParser( description=("Create per-process and per-VMA " "histograms of contigous virtual " "memory allocations")) parser.add_argument( "--omit-unfaulted-vmas", dest="no_unfaulted_vmas", action="store_true", help="Omit VMAs containing 0 present pages from output") parser.add_argument( "--omit-file-backed", dest="omit_file_backed", action="store_true", help="Omit VMAs corresponding to mmaped files") print_hists(parser.parse_args())