1#!/usr/bin/env python3 2# -- coding: utf-8 -- 3# Copyright (c) 2021-2022 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16""" 17A tool to get memory usage reports 18""" 19 20import argparse 21from operator import attrgetter 22from time import sleep 23 24PANDA_REGION_RE = r"""\[panda:([^\]]+)\]""" 25 26 27def read_file(path): 28 """Reads file""" 29 30 with open(path) as file: 31 return file.readlines() 32 33 34# pylint: disable=too-few-public-methods 35class MemInfo: 36 """Memory region information: name, size, rss, pss""" 37 38 def __init__(self, name): 39 self.name = name 40 self.size = 0 41 self.rss = 0 42 self.pss = 0 43 44 45def is_hex_digit(char): 46 """Checks whether char is hexadecimal digit""" 47 48 return '0' <= char <= '9' or 'a' <= char <= 'f' 49 50 51def is_start_of_map(line): 52 """Checks whether line is the start of map""" 53 54 return len(line) > 0 and is_hex_digit(line[0]) 55 56 57def is_stack_region(name): 58 """Checks whether memory region is stack""" 59 60 return name == '[stack]' 61 62 63def is_heap_region(name, remote): 64 """Checks whether memory region is heap""" 65 66 if remote: 67 return name == '[anon:libc_malloc]' 68 return name == '[heap]' 69 70 71def is_file_path(name): 72 """Checks whether name is file path""" 73 74 return name.startswith('/') 75 76 77def get_name(line): 78 """Parses line from /proc/pid/maps and returns name. 79 80 The line has format as follow: 81 55e88a5ab000-55e88a7c8000 r-xp 00068000 08:02 14434818 /usr/bin/nvim 82 The last element can contain spaces so we cannot use split here. 83 Find it manually 84 """ 85 pos = 0 86 for _ in range(5): 87 pos = line.index(' ', pos) + 1 88 return line[pos:].strip() 89 90 91# pylint: disable=too-many-instance-attributes 92class Mem: 93 """Represents panda memory regions""" 94 95 def __init__(self): 96 self.stack = MemInfo("Stack") 97 self.heap = MemInfo("Heap") 98 self.so_files = MemInfo(".so files") 99 self.abc_files = MemInfo(".abc files") 100 self.an_files = MemInfo(".an files") 101 self.other_files = MemInfo("Other files") 102 self.other = MemInfo("Other") 103 # This list must be synchronized with panda list in 104 # libpandabase/mem/space.h 105 self.panda_regions = { 106 "[anon:ark-Object Space]": MemInfo("ark-Object Space"), 107 "[anon:ark-Humongous Space]": MemInfo("ark-Humongous Space"), 108 "[anon:ark-Non Movable Space]": MemInfo("ark-Non Movable Space"), 109 "[anon:ark-Internal Space]": MemInfo("ark-Internal Space"), 110 "[anon:ark-Code Space]": MemInfo("ark-Code Space"), 111 "[anon:ark-Compiler Space]": MemInfo("ark-Compiler Space") 112 } 113 114 def get_mem_info(self, name, remote): 115 """Gets memory region information by name""" 116 117 info = self.other 118 if is_stack_region(name): 119 info = self.stack 120 elif is_heap_region(name, remote): 121 info = self.heap 122 elif self.panda_regions.get(name) is not None: 123 info = self.panda_regions.get(name) 124 elif is_file_path(name): 125 if name.endswith('.so'): 126 info = self.so_files 127 elif name.endswith('.abc'): 128 info = self.abc_files 129 elif name.endswith('.an'): 130 info = self.an_files 131 else: 132 info = self.other_files 133 return info 134 135 def gen_report(self, smaps, remote): 136 """Parses smaps and returns memory usage report""" 137 138 info = self.other 139 for line in smaps: 140 if is_start_of_map(line): 141 # the line of format 142 # 55e88a5ab000-55e88a7c8000 r-xp 00068000 08:02 14434818 /usr/bin/nvim 143 name = get_name(line) 144 info = self.get_mem_info(name, remote) 145 else: 146 # the line of format 147 # Size: 2164 kB 148 elems = line.split() 149 if elems[0] == 'Size:': 150 if len(elems) < 3: 151 raise Exception('Invalid file format') 152 info.size += int(elems[1]) 153 elif elems[0] == 'Rss:': 154 if len(elems) < 3: 155 raise Exception('Invalid file format') 156 info.rss += int(elems[1]) 157 elif elems[0] == 'Pss:': 158 if len(elems) < 3: 159 raise Exception('Invalid file format') 160 info.pss += int(elems[1]) 161 162 memusage = [self.stack, self.heap, self.so_files, self.abc_files, 163 self.an_files, self.other_files, self.other] 164 memusage.extend(self.panda_regions.values()) 165 166 return memusage 167 168 169def aggregate(reports): 170 """Aggregates memory usage reports""" 171 172 count = len(reports) 173 memusage = reports.pop(0) 174 while reports: 175 for left, right in zip(memusage, reports.pop(0)): 176 left.size = left.size + right.size 177 left.rss = left.rss + right.rss 178 left.pss = left.pss + right.pss 179 180 for entry in memusage: 181 entry.size = int(float(entry.size) / float(count)) 182 entry.rss = int(float(entry.rss) / float(count)) 183 entry.pss = int(float(entry.pss) / float(count)) 184 185 return memusage 186 187 188def print_report_row(col1, col2, col3, col4): 189 """Prints memory usage report row""" 190 191 print("{: >20} {: >10} {: >10} {: >10}".format(col1, col2, col3, col4)) 192 193 194def print_report(report): 195 """Prints memory usage report""" 196 197 print('Memory usage') 198 print_report_row('Region', 'Size', 'RSS', 'PSS') 199 for record in report: 200 print_report_row(record.name, record.size, record.rss, record.pss) 201 202 203def main(): 204 """Script's entrypoint""" 205 206 parser = argparse.ArgumentParser() 207 parser.add_argument( 208 '-f', '--follow', action='store_true', 209 help='Measure memory for a process periodically until it gets died') 210 parser.add_argument( 211 '-i', '--interval', default=200, type=int, 212 help='Interval in ms between following process ping') 213 parser.add_argument('pid', type=int) 214 215 args = parser.parse_args() 216 217 reports = [] 218 smaps = read_file('/proc/{}/smaps'.format(args.pid)) 219 report = Mem().gen_report(smaps, False) 220 reports.append(report) 221 cont = args.follow 222 while cont: 223 sleep(args.interval / 1000) 224 try: 225 smaps = read_file('/proc/{}/smaps'.format(args.pid)) 226 report = Mem().gen_report(smaps, False) 227 reports.append(report) 228 except FileNotFoundError: 229 cont = False 230 231 report = aggregate(reports) 232 report.sort(key=attrgetter('size'), reverse=True) 233 print_report(report) 234 235 236if __name__ == "__main__": 237 main() 238