# -*- coding: UTF-8 -*- # This file is part of the jetson_stats package (https://github.com/rbonghi/jetson_stats or http://rnext.it). # Copyright (c) 2019-2023 Raffaello Bonghi. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import os import re import stat import shlex # Logging import logging import subprocess as sp from .processes import read_process_table from .engine import read_engine from .common import cat, GenericInterface from .command import Command # Create logger logger = logging.getLogger(__name__) # Memory regular exception MEMINFO_REG = re.compile(r'(?P.+):\s+(?P.+) (?P.)B') FSTAB_RE = re.compile(r'^(?P[^ ]+) +(?P[^ ]+) +(?P[^ ]+) +(?P[^ ]+) +(?P\d+) +(?P\d+)$') BUDDYINFO_REG = re.compile(r'Node\s+(?P\d+).*zone\s+(?P\w+)\s+(?P.*)') SWAP_REG = re.compile(r'(?P[^ ]+)\s+(?P[^ ]+)\s+(?P\d+)\s+(?P\d+)\s+(?P-?\d+)') # Swap configuration PATH_FSTAB = '/etc/fstab' CONFIG_DEFAULT_SWAP_DIRECTORY = '' CONFIG_DEFAULT_SWAP_NAME = 'swfile' def meminfo(): # Read meminfo and decode # https://access.redhat.com/solutions/406773 status_mem = {} with open("/proc/meminfo", 'r') as fp: for line in fp: # Search line match = re.search(MEMINFO_REG, line.strip()) if match: parsed_line = match.groupdict() status_mem[parsed_line['key']] = int(parsed_line['value']) return status_mem def buddyinfo(page_size): # Read status free memory # http://andorian.blogspot.com/2014/03/making-sense-of-procbuddyinfo.html buddyhash = {} with open("/proc/buddyinfo", 'r') as fp: buddyinfo = fp.readlines() for line in buddyinfo: # Decode line parsed_line = re.match(BUDDYINFO_REG, line.strip()).groupdict() # detect buddy size numa_node = int(parsed_line["numa_node"]) free_fragments = [int(i) for i in parsed_line["nr_free"].split()] max_order = len(free_fragments) fragment_sizes = [page_size * 2**order for order in range(0, max_order)] usage_in_bytes = [free * fragmented for free, fragmented in zip(free_fragments, fragment_sizes)] data = { "zone": parsed_line["zone"], "nr_free": free_fragments, "sz_fragment": fragment_sizes, "usage": usage_in_bytes} buddyhash[numa_node] = buddyhash[numa_node] + [data] if numa_node in buddyhash else [data] return buddyhash def read_swapon(): """ This method list all processes working with GPU ============== ======================= ======== ============= name type prio size ============== ======================= ======== ============= name partition type: partition or file priority dictionary ============== ======================= ======== ============= :return: list of all processes :type spin: list """ table = {} swap = Command(['swapon', '--show', '--raw', '--byte']) lines = swap() for line in lines: # Search line match = re.search(SWAP_REG, line.strip()) if match: parsed_line = match.groupdict() name = parsed_line['name'] data = { # Improve this detection. (Now only checking if zram is on name to saving time) 'type': parsed_line['type'] if 'zram' not in name else 'zram', 'prio': int(parsed_line['prio']), 'size': int(parsed_line['size']) // 1024, 'used': int(parsed_line['used']) // 1024, } table[name] = data return table def read_fstab(): fstab = {} with open(PATH_FSTAB, "r") as fp: for line in fp: match = re.search(FSTAB_RE, line.strip()) if match: parsed_line = match.groupdict() path = parsed_line['path'] del parsed_line['path'] fstab[path] = parsed_line return fstab def read_emc(root_path): emc = {} # Initialize emc['cur'] to avoid a crash when starting this service emc['cur'] = 1 if os.path.isdir(root_path + "/debug/bpmp/debug/clk/emc"): path = root_path + "/debug/bpmp/debug/clk/emc" # Check if access to this file if os.access(path + "/rate", os.R_OK): with open(path + "/rate", 'r') as f: # Write min emc['cur'] = int(f.read()) // 1000 # Check if access to this file if os.access(path + "/max_rate", os.R_OK): with open(path + "/max_rate", 'r') as f: # Write min emc['max'] = int(f.read()) // 1000 # Check if access to this file if os.access(path + "/min_rate", os.R_OK): with open(path + "/min_rate", 'r') as f: # Write min emc['min'] = int(f.read()) // 1000 # Check if access to this file if os.access(path + "/mrq_rate_locked", os.R_OK): with open(path + "/mrq_rate_locked", 'r') as f: # Write min emc['override'] = int(f.read()) // 1000 elif os.path.isdir(root_path + "/debug/tegra_bwmgr"): path = root_path + "/debug/clk/override.emc" # Check if access to this file if os.access(path + "/clk_rate", os.R_OK): with open(path + "/clk_rate", 'r') as f: # Write min emc['cur'] = int(f.read()) // 1000 # Check if access to this file if os.access(path + "/clk_state", os.R_OK): with open(path + "/clk_state", 'r') as f: # Write min emc['override'] = int(f.read()) // 1000 # Decode from tegra_bwmgr path = root_path + "/tegra_bwmgr" # Check if access to this file if os.access(path + "/emc_max_rate", os.R_OK): with open(path + "/emc_max_rate", 'r') as f: # Write min emc['max'] = int(f.read()) // 1000 # Check if access to this file if os.access(path + "/emc_min_rate", os.R_OK): with open(path + "/emc_min_rate", 'r') as f: # Write min emc['min'] = int(f.read()) // 1000 elif os.path.isdir(root_path + "/clk/emc"): emc = read_engine(root_path + "/clk/emc") # Fix max frequency emc_cap = 0 # Check if access to this file if os.access(root_path + "/nvpmodel_emc_cap/emc_iso_cap", os.R_OK): with open(root_path + "/nvpmodel_emc_cap/emc_iso_cap", 'r') as f: # Write min emc_cap = int(f.read()) // 1000 # Fix max EMC if 'max' in emc: if emc_cap > 0 and emc_cap < emc['max']: emc['max'] = emc_cap # Percentage utilization # https://forums.developer.nvidia.com/t/real-time-emc-bandwidth-with-sysfs/107479/3 if os.access(root_path + "/debug/cactmon/mc_all", os.R_OK): with open(root_path + "/debug/cactmon/mc_all", 'r') as f: utilization = int(f.read()) elif os.access(root_path + "/actmon_avg_activity/mc_all", os.R_OK): with open(root_path + "/actmon_avg_activity/mc_all", 'r') as f: utilization = int(f.read()) else: # if utilization not accesibile return empty EMC data return {} emc['val'] = utilization // emc['cur'] # Set always online this engine emc['online'] = True return emc class Memory(GenericInterface): """ This class get the output from your memory, this class is readable like a dictionary, please read the documentation on :py:attr:`~jtop.jtop.memory` but is also usable to enable, disable swap on your device .. code-block:: python with jtop() as jetson: if jetson.ok(): jetson.memory.swap_set(10, on_boot=False) or if you want to deactivate a swap you can use this command .. code-block:: python with jtop() as jetson: if jetson.ok(): jetson.memory.swap_deactivate() Below all methods available using the :py:attr:`~jtop.jtop.memory` attribute """ def __init__(self): super(Memory, self).__init__() def swap_path(self): """ Return the default SWAP path, Default path `/` :return: Path swap :rtype: str """ return self._init def clear_cache(self): """ Clear the memory cache """ # Set new swap size configuration self._controller.put({'clear_cache': ''}) def swap_is_enable(self, path): """ Check if a swap is on list :param path: Path swap :type path: str :return: Status swap :rtype: bool """ return path in self._data['SWAP']['table'] def swap_set(self, value, path='', on_boot=False): """ Create a new swap on a default path. :py:func:`~swap_path` :param value: Size in **G** of a new SWAP :type value: int :param path: Path swap, defaults '' :type path: str, optional :param on_boot: Set this swap on boot, defaults False :type on_boot: bool, optional :raises ValueError: value is not an :py:class:`int` or a :py:class:`float` """ if not isinstance(value, (int, float)): raise ValueError("Need a Number") # if path_swap is empty load from default configuration if not path: path = self._init # Set new swap size configuration self._controller.put({'swap': {'command': 'set', 'path': path, 'size': value, 'boot': on_boot}}) def swap_deactivate(self, path=''): """ Deactivate a swap from a path or from default location `/` :param path: Path swap :type path: str, optional """ # if path_swap is empty load from default configuration if not path: path = self._init # Set new swap size configuration self._controller.put({'swap': {'command': 'unset', 'path': path}}) class MemoryService(object): def __init__(self, config): self._config = config # Extract memory page size self._page_size = os.sysconf("SC_PAGE_SIZE") # board type self._root_path = "/sys/kernel" if os.getenv('JTOP_TESTING', False): self._root_path = "/fake_sys/kernel" logger.warning("Running in JTOP_TESTING folder={root_dir}".format(root_dir=self._root_path)) self._is_emc = True if read_emc(self._root_path) else False if self._is_emc: logger.info("Found EMC!") self._is_iram = os.path.isdir(self._root_path + "/debug/nvmap/iram") if self._is_iram: logger.info("Found IRAM!") # Initialization memory logger.info("Memory service started") def swap_path(self): config = self._config.get('swap', {}) directory = config.get('directory', CONFIG_DEFAULT_SWAP_DIRECTORY) swap_name = config.get('name', CONFIG_DEFAULT_SWAP_NAME) return "{directory}/{name}".format(directory=directory, name=swap_name) def clear_cache(self): """ Clear cache following https://coderwall.com/p/ef1gcw/managing-ram-and-swap """ clear_cache = Command(['sysctl', 'vm.drop_caches=3']) out = clear_cache() return True if out else False @staticmethod def swap_set(size, path_swap, on_boot): if os.path.isfile(path_swap): logger.error("{path_swap} already exist".format(path_swap=path_swap)) return # Load swap configuration logger.info("Activate {path_swap} auto={on_boot}".format(path_swap=path_swap, on_boot=on_boot)) # Create a swapfile for Ubuntu at the current directory location sp.call(shlex.split('fallocate -l {size}G {path_swap}'.format(size=size, path_swap=path_swap))) # Change permissions so that only root can use it # https://www.tutorialspoint.com/python/os_chmod.htm # Equivalent permission 600 srw------- os.chmod(path_swap, stat.S_IREAD | stat.S_IWRITE) # Set up the Linux swap area sp.call(shlex.split('mkswap {path_swap}'.format(path_swap=path_swap))) # Now start using the swapfile sp.call(shlex.split('swapon {path_swap}'.format(path_swap=path_swap))) # Add not on boot return if not on_boot: return # Find if is already on boot swap_string_boot = "{path_swap} none swap sw 0 0".format(path_swap=path_swap) fstab = read_fstab() if path_swap in fstab: logger.warn("{path_swap} Already on boot".format(path_swap=path_swap)) return # Append swap line file_object = open(PATH_FSTAB, 'a') file_object.write("{swap_string_boot}\n".format(swap_string_boot=swap_string_boot)) file_object.close() @staticmethod def swap_deactivate(path_swap): # Check if exist swap if not os.path.isfile(path_swap): logger.error("{path_swap} Does not exist".format(path_swap=path_swap)) return # Disable swap sp.call(shlex.split('swapoff {path_swap}'.format(path_swap=path_swap))) # Remove swap os.remove(path_swap) # Run script logger.info("Deactivate {path_swap}".format(path_swap=path_swap)) # Remove if on fstab swap_string_boot = "{path_swap} none swap sw 0 0".format(path_swap=path_swap) fstab = read_fstab() if path_swap not in fstab: return # Check if is on boot logger.info("Removing {path_swap} from fstab".format(path_swap=path_swap)) with open(PATH_FSTAB, "r") as f: lines = f.readlines() with open(PATH_FSTAB, "w") as f: for line in lines: if line.strip("\n") != swap_string_boot: f.write(line) # Run script logger.info("Removed {path_swap} from boot".format(path_swap=path_swap)) def get_status(self, mem_total): memory = {} # Measure the largest free bank for 4MB mem_size = buddyinfo(self._page_size) # Count only the biggest Large free bank (lfb) large_free_bank = 0 for _, data in mem_size.items(): large_free_bank += sum([zone['nr_free'][-1] for zone in data]) # Status Memory status_mem = meminfo() # Read memory use # NvMapMemUsed: Is the shared memory between CPU and GPU # This key is always available on Jetson (not really always) ram_shared = status_mem.get('NvMapMemUsed', 0) if mem_total: # Update shared size ram_shared = mem_total if ram_shared == 0 else ram_shared # Extract memory info ram_total = status_mem.get('MemTotal', 0) ram_free = status_mem.get('MemFree', 0) # ram_available = status_mem.get('MemAvailable', 0) ram_buffer = status_mem.get('Buffers', 0) ram_cached = status_mem.get('Cached', 0) ram_SReclaimable = status_mem.get('SReclaimable', 0) # ram_Shmem = status_mem.get('Shmem', 0) total_used_memory = ram_total - ram_free cached_memory = ram_cached + ram_SReclaimable # + ram_Shmem # Add fields for RAM memory['RAM'] = { 'tot': ram_total, 'used': total_used_memory - (ram_buffer + ram_cached), 'free': ram_free, 'buffers': ram_buffer, 'cached': cached_memory, 'shared': ram_shared, 'lfb': large_free_bank, # In 4MB } # Extract swap numbers swap_total = status_mem.get('SwapTotal', 0) swap_free = status_mem.get('SwapFree', 0) swap_cached = status_mem.get('SwapCached', 0) # read all swap and check if are on boot swap_table = read_swapon() fstab = read_fstab() for name in swap_table: swap_table[name]['boot'] = name in fstab # Add fields for swap memory['SWAP'] = { 'tot': swap_total, 'used': swap_total - swap_free, 'cached': swap_cached, 'table': swap_table, } # Read EMC status if self._is_emc: memory['EMC'] = read_emc(self._root_path) # Read IRAM if available if self._is_iram: size = 0 if os.path.isfile(self._root_path + "/debug/nvmap/iram/size"): # Convert from Hex to decimal - Number in bytes size = int(cat(self._root_path + "/debug/nvmap/iram/size"), 16) // 1024 used_total, _ = read_process_table(self._root_path + "/debug/nvmap/iram/clients") memory['IRAM'] = { 'tot': size, 'used': used_total, 'lfb': size - used_total, # TODO To check } return memory # EOF