Skip to content

Commit 7c99650

Browse files
committed
Python script analyzes the memory usage of a compiled binary from local build
1 parent 6292e6e commit 7c99650

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

scripts/tools/memory/local_sizes.py

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 os
65+
import pathlib
66+
import sys
67+
68+
import memdf.collect
69+
import memdf.report
70+
import memdf.select
71+
import memdf.util
72+
from memdf import Config, ConfigDescription, DFs, SectionDF
73+
74+
PLATFORM_CONFIG_DIR = pathlib.Path('scripts/tools/memory/platform')
75+
76+
77+
def main(argv):
78+
status = 0
79+
80+
try:
81+
_, platform, config_name, target_name, binary, *args = argv
82+
except ValueError:
83+
program = pathlib.Path(argv[0])
84+
logging.error(
85+
"""
86+
Usage: %s platform config target binary [output] [options]
87+
88+
For other purposes, a general program for the same operations is
89+
%s/report_summary.py
90+
91+
""", program.name, program.parent)
92+
return 1
93+
94+
try:
95+
config_file = pathlib.Path(platform)
96+
if config_file.is_file():
97+
platform = config_file.stem
98+
else:
99+
config_file = (PLATFORM_CONFIG_DIR / platform).with_suffix('.cfg')
100+
101+
output_base = f'{platform}-{config_name}-{target_name}-sizes.json'
102+
if args and not args[0].startswith('-'):
103+
out, *args = args
104+
output = pathlib.Path(out)
105+
if out.endswith('/') and not output.exists():
106+
output.mkdir(parents=True)
107+
if output.is_dir():
108+
output = output / output_base
109+
else:
110+
output = pathlib.Path(binary).parent / output_base
111+
112+
config_desc = {
113+
**memdf.util.config.CONFIG,
114+
**memdf.collect.CONFIG,
115+
**memdf.select.CONFIG,
116+
**memdf.report.OUTPUT_CONFIG,
117+
}
118+
# In case there is no platform configuration file, default to using a popular set of section names.
119+
config_desc['section.select']['default'] = [
120+
'.text', '.rodata', '.data', '.bss']
121+
122+
config = Config().init(config_desc)
123+
config.put('output.file', output)
124+
config.put('output.format', 'json_records')
125+
if config_file.is_file():
126+
config.read_config_file(config_file)
127+
else:
128+
logging.warning('Missing config file: %s', config_file)
129+
config.parse([argv[0]] + args)
130+
131+
config.put('output.metadata.platform', platform)
132+
config.put('output.metadata.config', config_name)
133+
config.put('output.metadata.target', target_name)
134+
config.put('output.metadata.time', int(datetime.datetime.now().timestamp()))
135+
config.put('output.metadata.input', binary)
136+
config.put('output.metadata.by', 'section')
137+
138+
# In case there is no platform configuration file or it does not define regions,
139+
# try to find reasonable groups.
140+
if not config.get('region.sections'):
141+
sections = {'FLASH': [], 'RAM': []}
142+
for section in config.get('section.select'):
143+
print('section:', section)
144+
for substring, region in [('text', 'FLASH'), ('rodata', 'FLASH'), ('data', 'RAM'), ('bss', 'RAM')]:
145+
if substring in section:
146+
sections[region].append(section)
147+
break
148+
config.put('region.sections', sections)
149+
150+
collected: DFs = memdf.collect.collect_files(config, [binary])
151+
152+
sections = collected[SectionDF.name]
153+
section_summary = sections[['section',
154+
'size']].sort_values(by='section')
155+
section_summary.attrs['name'] = "section"
156+
157+
region_summary = memdf.select.groupby(
158+
config, collected['section'], 'region')
159+
region_summary.attrs['name'] = "region"
160+
161+
summaries = {
162+
'section': section_summary,
163+
'region': region_summary,
164+
}
165+
166+
# Write configured (json) report to the output file.
167+
memdf.report.write_dfs(config, summaries)
168+
169+
# Write text report to stdout.
170+
memdf.report.write_dfs(config,
171+
summaries,
172+
sys.stdout,
173+
'simple',
174+
floatfmt='.0f')
175+
176+
except Exception as exception:
177+
raise exception
178+
179+
return status
180+
181+
182+
if __name__ == '__main__':
183+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)