Skip to content

Commit ecdf4eb

Browse files
authored
Python script analyzes the memory usage of a compiled binary from local build (project-chip#37395)
1 parent 951830b commit ecdf4eb

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

scripts/tools/memory/local_sizes.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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

Comments
 (0)