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