|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# Copyright (c) 2025 Project CHIP Authors |
| 4 | +# |
| 5 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | +# you may not use this file except in compliance with the License. |
| 7 | +# You may obtain a copy of the License at |
| 8 | +# |
| 9 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# Unless required by applicable law or agreed to in writing, software |
| 12 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | +# See the License for the specific language governing permissions and |
| 15 | +# limitations under the License. |
| 16 | +# |
| 17 | +""" |
| 18 | +This Python script analyzes the memory usage of a compiled binary and generates |
| 19 | +a JSON report. It collects memory usage details and outputs them in a structured |
| 20 | +JSON format. |
| 21 | +
|
| 22 | +While similar to scripts/tools/memory/gh_sizes.py, this version extracts memory |
| 23 | +information directly from a binary built from the current working directory |
| 24 | +(rather than a GitHub workflow) and uses the tip of the current branch instead of |
| 25 | +targeting a specific commit. |
| 26 | +
|
| 27 | +Usage: local_sizes.py ‹platform› ‹config› ‹target› ‹binary› [‹output›] [‹option›…] |
| 28 | + ‹platform› - Platform name, corresponding to a config file |
| 29 | + in scripts/tools/memory/platform/ |
| 30 | + ‹config› - Configuration identification string. |
| 31 | + ‹target› - Build artifact identification string. |
| 32 | + ‹binary› - Binary build artifact. |
| 33 | + ‹output› - Output name or directory. |
| 34 | + ‹option›… - Other options as for report_summary. |
| 35 | +
|
| 36 | +Default output file is {platform}-{configname}-{buildname}-sizes.json in the |
| 37 | +binary's directory. This file has the form: |
| 38 | +
|
| 39 | + { |
| 40 | + "platform": "‹platform›", |
| 41 | + "config": "‹config›", |
| 42 | + "target": "‹target›", |
| 43 | + "time": 1317645296, |
| 44 | + "input": "‹binary›", |
| 45 | + "by": "section", |
| 46 | + "ref": "refs/pull/12345/merge" |
| 47 | + "frames": { |
| 48 | + "section": [ |
| 49 | + {"section": ".bss", "size": 260496}, |
| 50 | + {"section": ".data", "size": 1648}, |
| 51 | + {"section": ".text", "size": 740236} |
| 52 | + ], |
| 53 | + "region": [ |
| 54 | + {"region": "FLASH", "size": 262144}, |
| 55 | + {"region": "RAM", "size": 74023} |
| 56 | + ] |
| 57 | + } |
| 58 | + } |
| 59 | +
|
| 60 | +""" |
| 61 | + |
| 62 | +import datetime |
| 63 | +import logging |
| 64 | +import pathlib |
| 65 | +import sys |
| 66 | + |
| 67 | +import memdf.collect |
| 68 | +import memdf.report |
| 69 | +import memdf.select |
| 70 | +import memdf.util |
| 71 | +from memdf import Config, DFs, SectionDF |
| 72 | + |
| 73 | +PLATFORM_CONFIG_DIR = pathlib.Path('scripts/tools/memory/platform') |
| 74 | + |
| 75 | + |
| 76 | +def main(argv): |
| 77 | + status = 0 |
| 78 | + |
| 79 | + try: |
| 80 | + _, platform, config_name, target_name, binary, *args = argv |
| 81 | + except ValueError: |
| 82 | + program = pathlib.Path(argv[0]) |
| 83 | + logging.error( |
| 84 | + """ |
| 85 | + Usage: %s platform config target binary [output] [options] |
| 86 | +
|
| 87 | + For other purposes, a general program for the same operations is |
| 88 | + %s/report_summary.py |
| 89 | +
|
| 90 | + """, program.name, program.parent) |
| 91 | + return 1 |
| 92 | + |
| 93 | + try: |
| 94 | + config_file = pathlib.Path(platform) |
| 95 | + if config_file.is_file(): |
| 96 | + platform = config_file.stem |
| 97 | + else: |
| 98 | + config_file = (PLATFORM_CONFIG_DIR / platform).with_suffix('.cfg') |
| 99 | + |
| 100 | + output_base = f'{platform}-{config_name}-{target_name}-sizes.json' |
| 101 | + if args and not args[0].startswith('-'): |
| 102 | + out, *args = args |
| 103 | + output = pathlib.Path(out) |
| 104 | + if out.endswith('/') and not output.exists(): |
| 105 | + output.mkdir(parents=True) |
| 106 | + if output.is_dir(): |
| 107 | + output = output / output_base |
| 108 | + else: |
| 109 | + output = pathlib.Path(binary).parent / output_base |
| 110 | + |
| 111 | + config_desc = { |
| 112 | + **memdf.util.config.CONFIG, |
| 113 | + **memdf.collect.CONFIG, |
| 114 | + **memdf.select.CONFIG, |
| 115 | + **memdf.report.OUTPUT_CONFIG, |
| 116 | + } |
| 117 | + # In case there is no platform configuration file, default to using a popular set of section names. |
| 118 | + config_desc['section.select']['default'] = [ |
| 119 | + '.text', '.rodata', '.data', '.bss'] |
| 120 | + |
| 121 | + config = Config().init(config_desc) |
| 122 | + config.put('output.file', output) |
| 123 | + config.put('output.format', 'json_records') |
| 124 | + if config_file.is_file(): |
| 125 | + config.read_config_file(config_file) |
| 126 | + else: |
| 127 | + logging.warning('Missing config file: %s', config_file) |
| 128 | + config.parse([argv[0]] + args) |
| 129 | + |
| 130 | + config.put('output.metadata.platform', platform) |
| 131 | + config.put('output.metadata.config', config_name) |
| 132 | + config.put('output.metadata.target', target_name) |
| 133 | + config.put('output.metadata.time', int(datetime.datetime.now().timestamp())) |
| 134 | + config.put('output.metadata.input', binary) |
| 135 | + config.put('output.metadata.by', 'section') |
| 136 | + |
| 137 | + # In case there is no platform configuration file or it does not define regions, |
| 138 | + # try to find reasonable groups. |
| 139 | + if not config.get('region.sections'): |
| 140 | + sections = {'FLASH': [], 'RAM': []} |
| 141 | + for section in config.get('section.select'): |
| 142 | + print('section:', section) |
| 143 | + for substring, region in [('text', 'FLASH'), ('rodata', 'FLASH'), ('data', 'RAM'), ('bss', 'RAM')]: |
| 144 | + if substring in section: |
| 145 | + sections[region].append(section) |
| 146 | + break |
| 147 | + config.put('region.sections', sections) |
| 148 | + |
| 149 | + collected: DFs = memdf.collect.collect_files(config, [binary]) |
| 150 | + |
| 151 | + sections = collected[SectionDF.name] |
| 152 | + section_summary = sections[['section', |
| 153 | + 'size']].sort_values(by='section') |
| 154 | + section_summary.attrs['name'] = "section" |
| 155 | + |
| 156 | + region_summary = memdf.select.groupby( |
| 157 | + config, collected['section'], 'region') |
| 158 | + region_summary.attrs['name'] = "region" |
| 159 | + |
| 160 | + summaries = { |
| 161 | + 'section': section_summary, |
| 162 | + 'region': region_summary, |
| 163 | + } |
| 164 | + |
| 165 | + # Write configured (json) report to the output file. |
| 166 | + memdf.report.write_dfs(config, summaries) |
| 167 | + |
| 168 | + # Write text report to stdout. |
| 169 | + memdf.report.write_dfs(config, |
| 170 | + summaries, |
| 171 | + sys.stdout, |
| 172 | + 'simple', |
| 173 | + floatfmt='.0f') |
| 174 | + |
| 175 | + except Exception as exception: |
| 176 | + raise exception |
| 177 | + |
| 178 | + return status |
| 179 | + |
| 180 | + |
| 181 | +if __name__ == '__main__': |
| 182 | + sys.exit(main(sys.argv)) |
0 commit comments