xref: /rk3399_ARM-atf/tools/memory/src/memory/summary.py (revision d8fdff38b544b79c4f0b757e3b3c82ce9c8a2f9e)
1#
2# Copyright (c) 2016-2025, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: Apache-2.0
5#
6
7import json
8import os
9import re
10from collections import defaultdict
11from copy import deepcopy
12from os.path import (
13    abspath,
14    basename,
15    commonprefix,
16    dirname,
17    join,
18    relpath,
19    splitext,
20)
21from pathlib import Path
22from sys import stdout
23from typing import IO, Any, Dict, List, Optional, Pattern, Tuple, Union
24
25from jinja2 import FileSystemLoader, StrictUndefined
26from jinja2.environment import Environment
27from prettytable import HEADER, PrettyTable
28
29ModuleStats = Dict[str, int]
30Modules = Dict[str, ModuleStats]
31
32SECTIONS: Tuple[str, ...] = (".text", ".data", ".bss", ".heap", ".stack")
33MISC_FLASH_SECTIONS: Tuple[str, ...] = (".interrupts", ".flash_config")
34OTHER_SECTIONS: Tuple[str, ...] = (
35    ".interrupts_ram",
36    ".init",
37    ".ARM.extab",
38    ".ARM.exidx",
39    ".ARM.attributes",
40    ".eh_frame",
41    ".init_array",
42    ".fini_array",
43    ".jcr",
44    ".stab",
45    ".stabstr",
46    ".ARM.exidx",
47    ".ARM",
48)
49ALL_SECTIONS: Tuple[str, ...] = (
50    SECTIONS + OTHER_SECTIONS + MISC_FLASH_SECTIONS + ("unknown", "OUTPUT")
51)
52
53
54class Parser:
55    """Internal interface for parsing"""
56
57    _RE_OBJECT_FILE: Pattern[str] = re.compile(r"^(.+\/.+\.o(bj)?)$")
58    _RE_LIBRARY_OBJECT: Pattern[str] = re.compile(
59        r"((^.+" + r"" + r"lib.+\.a)\((.+\.o(bj)?)\))$"
60    )
61    _RE_STD_SECTION: Pattern[str] = re.compile(r"^\s+.*0x(\w{8,16})\s+0x(\w+)\s(.+)$")
62    _RE_FILL_SECTION: Pattern[str] = re.compile(
63        r"^\s*\*fill\*\s+0x(\w{8,16})\s+0x(\w+).*$"
64    )
65    _RE_TRANS_FILE: Pattern[str] = re.compile(r"^(.+\/|.+\.ltrans.o(bj)?)$")
66    _OBJECT_EXTENSIONS: Tuple[str, ...] = (".o", ".obj")
67
68    _modules: Modules
69    _fill: bool
70
71    def __init__(self, fill: bool = True):
72        self._modules: Modules = {}
73        self._fill = fill
74
75    def module_add(self, object_name: str, size: int, section: str):
76        """Adds a module or section to the list
77
78        Positional arguments:
79        object_name - name of the entry to add
80        size - the size of the module being added
81        section - the section the module contributes to
82        """
83        if (
84            not object_name
85            or not size
86            or not section
87            or (not self._fill and object_name == "[fill]")
88        ):
89            return
90
91        if object_name in self._modules:
92            self._modules[object_name].setdefault(section, 0)
93            self._modules[object_name][section] += size
94            return
95
96        obj_split = os.sep + basename(object_name)
97        for module_path, contents in self._modules.items():
98            if module_path.endswith(obj_split) or module_path == object_name:
99                contents.setdefault(section, 0)
100                contents[section] += size
101                return
102
103        new_module: ModuleStats = defaultdict(int)
104        new_module[section] = size
105        self._modules[object_name] = new_module
106
107    def module_replace(self, old_object: str, new_object: str):
108        """Replaces an object name with a new one"""
109        if old_object in self._modules:
110            self._modules[new_object] = self._modules.pop(old_object)
111
112    def check_new_section(self, line: str) -> Optional[str]:
113        """Check whether a new section in a map file has been detected
114
115        Positional arguments:
116        line - the line to check for a new section
117
118        return value - A section name, if a new section was found, None
119                       otherwise
120        """
121        line_s = line.strip()
122        for i in ALL_SECTIONS:
123            if line_s.startswith(i):
124                return i
125        if line.startswith("."):
126            return "unknown"
127        else:
128            return None
129
130    def parse_object_name(self, line: str) -> str:
131        """Parse a path to object file
132
133        Positional arguments:
134        line - the path to parse the object and module name from
135
136        return value - an object file name
137        """
138        if re.match(self._RE_TRANS_FILE, line):
139            return "[misc]"
140
141        test_re_file_name = re.match(self._RE_OBJECT_FILE, line)
142
143        if test_re_file_name:
144            object_name = test_re_file_name.group(1)
145
146            return object_name
147        else:
148            test_re_obj_name = re.match(self._RE_LIBRARY_OBJECT, line)
149
150            if test_re_obj_name:
151                return join(test_re_obj_name.group(2), test_re_obj_name.group(3))
152            else:
153                if not line.startswith("LONG") and not line.startswith("linker stubs"):
154                    print("Unknown object name found in GCC map file: %s" % line)
155                return "[misc]"
156
157    def parse_section(self, line: str) -> Tuple[str, int]:
158        """Parse data from a section of gcc map file
159
160        examples:
161                        0x00004308       0x7c ./BUILD/K64F/GCC_ARM/spi_api.o
162         .text          0x00000608      0x198 ./BUILD/K64F/HAL_CM4.o
163
164        Positional arguments:
165        line - the line to parse a section from
166        """
167        is_fill = re.match(self._RE_FILL_SECTION, line)
168        if is_fill:
169            o_name: str = "[fill]"
170            o_size: int = int(is_fill.group(2), 16)
171            return o_name, o_size
172
173        is_section = re.match(self._RE_STD_SECTION, line)
174        if is_section:
175            o_size: int = int(is_section.group(2), 16)
176            if o_size:
177                o_name: str = self.parse_object_name(is_section.group(3))
178                return o_name, o_size
179
180        return "", 0
181
182    def parse_mapfile(self, file_desc: IO[str]) -> Modules:
183        """Main logic to decode gcc map files
184
185        Positional arguments:
186        file_desc - a stream object to parse as a gcc map file
187        """
188        current_section: str = "unknown"
189
190        with file_desc as infile:
191            for line in infile:
192                if line.startswith("Linker script and memory map"):
193                    current_section = "unknown"
194                    break
195
196            for line in infile:
197                next_section = self.check_new_section(line)
198
199                if next_section == "OUTPUT":
200                    break
201                elif next_section:
202                    current_section = next_section
203
204                object_name, object_size = self.parse_section(line)
205                self.module_add(object_name, object_size, current_section)
206
207        def is_obj(name: str) -> bool:
208            return not name.startswith("[") or not name.endswith("]")
209
210        common_prefix: str = dirname(
211            commonprefix([o for o in self._modules.keys() if is_obj(o)])
212        )
213        new_modules: Modules = {}
214        for name, stats in self._modules.items():
215            if is_obj(name):
216                new_modules[relpath(name, common_prefix)] = stats
217            else:
218                new_modules[name] = stats
219        return new_modules
220
221
222class MapParser(object):
223    """An object that represents parsed results, parses the memory map files,
224    and writes out different file types of memory results
225    """
226
227    print_sections: Tuple[str, ...] = (".text", ".data", ".bss")
228    delta_sections: Tuple[str, ...] = (".text-delta", ".data-delta", ".bss-delta")
229
230    # sections to print info (generic for all toolchains)
231    sections: Tuple[str, ...] = SECTIONS
232    misc_flash_sections: Tuple[str, ...] = MISC_FLASH_SECTIONS
233    other_sections: Tuple[str, ...] = OTHER_SECTIONS
234
235    modules: Modules
236    old_modules: Modules
237    short_modules: Modules
238    mem_report: List[Dict[str, Union[str, ModuleStats]]]
239    mem_summary: Dict[str, int]
240    subtotal: Dict[str, int]
241    tc_name: Optional[str]
242
243    RAM_FORMAT_STR: str = "Total Static RAM memory (data + bss): {}({:+}) bytes\n"
244    ROM_FORMAT_STR: str = "Total Flash memory (text + data): {}({:+}) bytes\n"
245
246    def __init__(self):
247        # list of all modules and their sections
248        # full list - doesn't change with depth
249        self.modules: Modules = {}
250        self.old_modules = {}
251        # short version with specific depth
252        self.short_modules: Modules = {}
253
254        # Memory report (sections + summary)
255        self.mem_report: List[Dict[str, Union[str, ModuleStats]]] = []
256
257        # Memory summary
258        self.mem_summary: Dict[str, int] = {}
259
260        # Totals of ".text", ".data" and ".bss"
261        self.subtotal: Dict[str, int] = {}
262
263        # Name of the toolchain, for better headings
264        self.tc_name = None
265
266    def reduce_depth(self, depth: Optional[int]):
267        """
268        populates the short_modules attribute with a truncated module list
269
270        (1) depth = 1:
271        main.o
272        mbed-os
273
274        (2) depth = 2:
275        main.o
276        mbed-os/test.o
277        mbed-os/drivers
278
279        """
280        if depth == 0 or depth is None:
281            self.short_modules = deepcopy(self.modules)
282        else:
283            self.short_modules = dict()
284            for module_name, v in self.modules.items():
285                split_name = module_name.split(os.sep)
286                if split_name[0] == "":
287                    split_name = split_name[1:]
288                new_name = join(*split_name[:depth])
289                self.short_modules.setdefault(new_name, defaultdict(int))
290                for section_idx, value in v.items():
291                    self.short_modules[new_name][section_idx] += value
292                    delta_name = section_idx + "-delta"
293                    self.short_modules[new_name][delta_name] += value
294
295            for module_name, v in self.old_modules.items():
296                split_name = module_name.split(os.sep)
297                if split_name[0] == "":
298                    split_name = split_name[1:]
299                new_name = join(*split_name[:depth])
300                self.short_modules.setdefault(new_name, defaultdict(int))
301                for section_idx, value in v.items():
302                    delta_name = section_idx + "-delta"
303                    self.short_modules[new_name][delta_name] -= value
304
305    export_formats: List[str] = ["json", "html", "table"]
306
307    def generate_output(
308        self,
309        export_format: str,
310        depth: Optional[int],
311        file_output: Optional[str] = None,
312    ) -> Optional[bool]:
313        """Generates summary of memory map data
314
315        Positional arguments:
316        export_format - the format to dump
317
318        Keyword arguments:
319        file_desc - descriptor (either stdout or file)
320        depth - directory depth on report
321
322        Returns: generated string for the 'table' format, otherwise Nonef
323        """
324        if depth is None or depth > 0:
325            self.reduce_depth(depth)
326        self.compute_report()
327        try:
328            if file_output:
329                file_desc = open(file_output, "w")
330            else:
331                file_desc = stdout
332        except IOError as error:
333            print("I/O error({0}): {1}".format(error.errno, error.strerror))
334            return False
335
336        to_call = {
337            "json": self.generate_json,
338            "html": self.generate_html,
339            "table": self.generate_table,
340        }[export_format]
341        to_call(file_desc)
342
343        if file_desc is not stdout:
344            file_desc.close()
345
346    @staticmethod
347    def _move_up_tree(tree: Dict[str, Any], next_module: str) -> Dict[str, Any]:
348        tree.setdefault("children", [])
349        for child in tree["children"]:
350            if child["name"] == next_module:
351                return child
352
353        new_module = {"name": next_module, "value": 0, "delta": 0}
354        tree["children"].append(new_module)
355
356        return new_module
357
358    def generate_html(self, file_desc: IO[str]):
359        """Generate a json file from a memory map for D3
360
361        Positional arguments:
362        file_desc - the file to write out the final report to
363        """
364
365        tree_text = {"name": ".text", "value": 0, "delta": 0}
366        tree_bss = {"name": ".bss", "value": 0, "delta": 0}
367        tree_data = {"name": ".data", "value": 0, "delta": 0}
368
369        def accumulate(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
370            parts = module_name.split(os.sep)
371
372            val = stats.get(size_key, 0)
373            tree_root["value"] += val
374            tree_root["delta"] += val
375
376            cur = tree_root
377            for part in parts:
378                cur = self._move_up_tree(cur, part)
379                cur["value"] += val
380                cur["delta"] += val
381
382        def subtract(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
383            parts = module_name.split(os.sep)
384
385            cur = tree_root
386            cur["delta"] -= stats.get(size_key, 0)
387
388            for part in parts:
389                children = {c["name"]: c for c in cur.get("children", [])}
390                if part not in children:
391                    return
392
393                cur = children[part]
394                cur["delta"] -= stats.get(size_key, 0)
395
396        for module_name, dct in self.modules.items():
397            accumulate(tree_text, ".text", dct)
398            accumulate(tree_data, ".data", dct)
399            accumulate(tree_bss, ".bss", dct)
400
401        for module_name, dct in self.old_modules.items():
402            subtract(tree_text, ".text", dct)
403            subtract(tree_data, ".data", dct)
404            subtract(tree_bss, ".bss", dct)
405
406        jinja_loader = FileSystemLoader(dirname(abspath(__file__)))
407        jinja_environment = Environment(loader=jinja_loader, undefined=StrictUndefined)
408        template = jinja_environment.get_template("templates/summary-flamegraph.html")
409
410        name, _ = splitext(basename(file_desc.name))
411
412        if name.endswith("_map"):
413            name = name[:-4]
414        if self.tc_name:
415            name = f"{name} {self.tc_name}"
416
417        file_desc.write(
418            template.render(
419                {
420                    "name": name,
421                    "rom": json.dumps(
422                        {
423                            "name": "ROM",
424                            "value": tree_text["value"] + tree_data["value"],
425                            "delta": tree_text["delta"] + tree_data["delta"],
426                            "children": [tree_text, tree_data],
427                        }
428                    ),
429                    "ram": json.dumps(
430                        {
431                            "name": "RAM",
432                            "value": tree_bss["value"] + tree_data["value"],
433                            "delta": tree_bss["delta"] + tree_data["delta"],
434                            "children": [tree_bss, tree_data],
435                        }
436                    ),
437                }
438            )
439        )
440
441    def generate_json(self, file_desc: IO[str]):
442        """Generate a json file from a memory map
443
444        Positional arguments:
445        file_desc - the file to write out the final report to
446        """
447        file_desc.write(json.dumps(self.mem_report, indent=4))
448        file_desc.write("\n")
449
450    def generate_table(self, file_desc: IO[str]):
451        """Generate a table from a memory map
452
453        Returns: string of the generated table
454        """
455        # Create table
456        columns = ["Module"]
457        columns.extend(self.print_sections)
458
459        table = PrettyTable(columns, junction_char="|", hrules=HEADER)
460        table.align["Module"] = "l"
461
462        for col in self.print_sections:
463            table.align[col] = "r"
464
465        for i in sorted(self.short_modules):
466            row = [i]
467
468            for k in self.print_sections:
469                row.append(
470                    "{}({:+})".format(
471                        self.short_modules[i][k], self.short_modules[i][k + "-delta"]
472                    )
473                )
474
475            table.add_row(row)
476
477        subtotal_row = ["Subtotals"]
478        for k in self.print_sections:
479            subtotal_row.append(
480                "{}({:+})".format(self.subtotal[k], self.subtotal[k + "-delta"])
481            )
482
483        table.add_row(subtotal_row)
484
485        output = table.get_string()
486        output += "\n"
487
488        output += self.RAM_FORMAT_STR.format(
489            self.mem_summary["static_ram"], self.mem_summary["static_ram_delta"]
490        )
491        output += self.ROM_FORMAT_STR.format(
492            self.mem_summary["total_flash"], self.mem_summary["total_flash_delta"]
493        )
494        file_desc.write(output)
495
496    def compute_report(self):
497        """Generates summary of memory usage for main areas"""
498        self.subtotal = defaultdict(int)
499
500        for mod in self.modules.values():
501            for k in self.sections:
502                self.subtotal[k] += mod[k]
503                self.subtotal[k + "-delta"] += mod[k]
504
505        for mod in self.old_modules.values():
506            for k in self.sections:
507                self.subtotal[k + "-delta"] -= mod[k]
508
509        self.mem_summary = {
510            "static_ram": self.subtotal[".data"] + self.subtotal[".bss"],
511            "static_ram_delta": self.subtotal[".data-delta"]
512            + self.subtotal[".bss-delta"],
513            "total_flash": (self.subtotal[".text"] + self.subtotal[".data"]),
514            "total_flash_delta": self.subtotal[".text-delta"]
515            + self.subtotal[".data-delta"],
516        }
517
518        self.mem_report = []
519        if self.short_modules:
520            for name, sizes in sorted(self.short_modules.items()):
521                self.mem_report.append(
522                    {
523                        "module": name,
524                        "size": {
525                            k: sizes.get(k, 0)
526                            for k in (self.print_sections + self.delta_sections)
527                        },
528                    }
529                )
530
531        self.mem_report.append({"summary": self.mem_summary})
532
533    def parse(
534        self, mapfile: Path, oldfile: Optional[Path] = None, no_fill: bool = False
535    ) -> bool:
536        """Parse and decode map file depending on the toolchain
537
538        Positional arguments:
539        mapfile - the file name of the memory map file
540        toolchain - the toolchain used to create the file
541        """
542        try:
543            with open(mapfile, "r") as file_input:
544                self.modules = Parser(not no_fill).parse_mapfile(file_input)
545            try:
546                if oldfile is not None:
547                    with open(oldfile, "r") as old_input:
548                        self.old_modules = Parser(not no_fill).parse_mapfile(old_input)
549                else:
550                    self.old_modules = self.modules
551            except IOError:
552                self.old_modules = {}
553            return True
554
555        except IOError as error:
556            print("I/O error({0}): {1}".format(error.errno, error.strerror))
557            return False
558