• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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