Skip to content

Commit bbee13e

Browse files
committed
Add memory_report.py to generate a FLASH/RAM report
This PR adds a python script to analyze built targets and generate an HTML (or CSV) report of FLASH/RAM usage for each example app.
1 parent 9c8cb33 commit bbee13e

File tree

4 files changed

+305
-1
lines changed

4 files changed

+305
-1
lines changed

integrations/appengine/webapp_config.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ handlers:
66
- url: /conformance/
77
static_files: html/conformance_report.html
88
upload: html/conformance_report.html
9+
- url: /memory/
10+
static_files: html/memory_report.html
11+
upload: html/memory_report.html
912
- url: /(.*)
1013
static_files: html/\1
1114
upload: html/(.*)

integrations/compute_engine/startup-script.sh

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ source scripts/activate.sh
3232
python3 -u scripts/examples/conformance_report.py
3333
cp /tmp/conformance_report/conformance_report.html out/coverage/coverage/html
3434

35+
# Generate Memory Usage Report
36+
python3 -u scripts/tools/memory/memory_report.py
37+
cp /tmp/memory_report/memory_report.html out/coverage/coverage/html
38+
3539
# Upload
3640
cd out/coverage/coverage
3741
gcloud app deploy webapp_config.yaml 2>&1 | tee /tmp/matter_publish.log

scripts/tools/memory/memdf/select.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def select_configured(config: Config, df: DF, columns=SELECTION_CHOICES) -> DF:
163163
def groupby(config: Config, df: DF, by: Optional[str] = None):
164164
if not by:
165165
by = config['report.by']
166-
df = df[[by, 'size']].groupby(by).aggregate(np.sum).reset_index()
166+
df = df[[by, 'size']].groupby(by).aggregate("sum").reset_index()
167167
if by in SYNTHESIZE:
168168
df = SYNTHESIZE[by][1](df)
169169
return df

scripts/tools/memory/memory_report.py

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import argparse
2+
import csv
3+
import os
4+
import subprocess
5+
from datetime import datetime
6+
from dataclasses import dataclass
7+
8+
import memdf.collect
9+
import memdf.report
10+
import memdf.select
11+
import memdf.util
12+
from memdf import Config, DFs, SectionDF
13+
14+
# Constants for output directories
15+
TMP_RESULTS_DIR = "/tmp/memory_report"
16+
OUT_DIR = "./out"
17+
18+
19+
@dataclass
20+
class TargetInfo:
21+
name: str
22+
config_file: str
23+
executable_path: str
24+
25+
26+
# Configuration file mapping
27+
config_files_dict = {
28+
"linux-": "scripts/tools/memory/platform/linux.cfg",
29+
"efr32-": "scripts/tools/memory/platform/efr32.cfg",
30+
"esp32-": "scripts/tools/memory/platform/esp32.cfg",
31+
"telink-": "scripts/tools/memory/platform/telink.cfg",
32+
"tizen-": "scripts/tools/memory/platform/tizen.cfg",
33+
}
34+
35+
36+
def find_built_targets(config_files_dict: dict[str, str], out_dir: str) -> list[TargetInfo]:
37+
"""Finds built targets and their associated configuration files.
38+
39+
Args:
40+
config_files_dict: Dictionary mapping prefixes to config file paths.
41+
out_dir: Directory to search for built targets.
42+
43+
Returns:
44+
A list of TargetInfo objects.
45+
"""
46+
print(f"Searching targets built in {out_dir}")
47+
targets = []
48+
if not os.path.isdir(out_dir):
49+
print(f"Warning: Output directory '{out_dir}' does not exist.")
50+
return targets
51+
52+
for dir_name in sorted(os.listdir(out_dir)):
53+
dir_path = os.path.join(out_dir, dir_name)
54+
if not os.path.isdir(dir_path):
55+
continue
56+
57+
for prefix, config_file in config_files_dict.items():
58+
if dir_name.startswith(prefix):
59+
executable_path = find_executable(dir_path)
60+
if executable_path:
61+
targets.append(
62+
TargetInfo(name=dir_name, config_file=config_file, executable_path=executable_path)
63+
)
64+
break # Move to the next directory
65+
66+
print(f"Found {len(targets)} built targets!")
67+
return targets
68+
69+
70+
def find_executable(directory: str) -> str | None:
71+
"""Finds the first executable file in a directory.
72+
73+
Args:
74+
directory: The directory to search.
75+
76+
Returns:
77+
The path to the executable, or None if no executable is found.
78+
"""
79+
if not os.path.isdir(directory):
80+
return None
81+
82+
for filename in os.listdir(directory):
83+
if filename.endswith((".sh", ".py")): # More robust exclusion
84+
continue
85+
filepath = os.path.join(directory, filename)
86+
if os.path.isfile(filepath) and os.access(filepath, os.X_OK):
87+
return filepath
88+
return None
89+
90+
91+
def calculate_flash_ram(target_info: TargetInfo) -> tuple[int, int, str] | None:
92+
"""Calculates the flash and RAM usage of a binary.
93+
94+
Args:
95+
target_info: TargetInfo object
96+
97+
Returns:
98+
A tuple containing (flash usage, RAM usage, details string), or None on error.
99+
"""
100+
try:
101+
config_desc = {
102+
**memdf.util.config.CONFIG,
103+
**memdf.collect.CONFIG,
104+
**memdf.select.CONFIG,
105+
**memdf.report.OUTPUT_CONFIG,
106+
}
107+
config = Config().init(config_desc)
108+
config.parse(['', '--config-file', target_info.config_file])
109+
110+
collected: DFs = memdf.collect.collect_files(config, [target_info.executable_path])
111+
sections = collected[SectionDF.name]
112+
section_summary = sections[['section', 'size']].sort_values(by='section')
113+
section_summary.attrs['name'] = "section"
114+
115+
region_summary = memdf.select.groupby(config, collected['section'], 'region')
116+
region_summary.attrs['name'] = "region"
117+
118+
flash = region_summary[region_summary['region'] == 'FLASH']['size'].iloc[0]
119+
ram = region_summary[region_summary['region'] == 'RAM']['size'].iloc[0]
120+
details = str(section_summary)
121+
122+
return (flash, ram, details)
123+
124+
except (KeyError, IndexError, FileNotFoundError) as e:
125+
print(f"Error processing {target_info.name}: {e}")
126+
return None
127+
except Exception as e:
128+
print(f"An unexpected error occurred while processing {target_info.name}: {e}")
129+
return None
130+
131+
132+
def generate_csv_report(results: dict[str, tuple[int, int, str]], csv_filename: str):
133+
"""Generates a CSV report of the memory usage results.
134+
135+
Args:
136+
results: A dictionary mapping target names to (flash, ram, details) tuples.
137+
csv_filename: The output CSV filename.
138+
"""
139+
try:
140+
with open(csv_filename, 'w', newline='') as f: # Use newline='' for correct CSV handling
141+
writer = csv.writer(f)
142+
writer.writerow(["Application", "FLASH (bytes)", "RAM (bytes)", "Details"])
143+
for app, (flash, ram, details) in results.items():
144+
writer.writerow([app, flash, ram, details])
145+
print(f"CSV Memory summary saved to {csv_filename}")
146+
except Exception as e:
147+
print(f"Error generating CSV report: {e}")
148+
149+
150+
def generate_html_report(csv_file_path: str, html_page_title: str, html_table_title: str, html_out_dir: str, sha: str):
151+
"""Generates an HTML report from a CSV file, inlined."""
152+
try:
153+
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
154+
html_report = f"""
155+
<!DOCTYPE html>
156+
<html>
157+
<head>
158+
<style>
159+
h1 {{
160+
font-family: Tahoma, Geneva, sans-serif;
161+
font-size: 32px; color: #333;
162+
text-align: center;
163+
}}
164+
h2 {{
165+
font-family: Tahoma, Geneva, sans-serif;
166+
font-size: 22px; color: #333;
167+
text-align: center;
168+
}}
169+
h4 {{
170+
font-family: Tahoma, Geneva, sans-serif;
171+
font-size: 14px; color: #333;
172+
text-align: left;
173+
}}
174+
table {{
175+
border-collapse: collapse;
176+
font-family: Tahoma, Geneva, sans-serif;
177+
margin-left: auto;
178+
margin-right: auto;
179+
width: 80%;
180+
}}
181+
table td {{
182+
padding: 15px;
183+
}}
184+
td[value="FAIL"] {{
185+
color: red;
186+
}}
187+
td[value="PASS"] {{
188+
color: green;
189+
}}
190+
th {{
191+
background-color: #54585d;
192+
color: #ffffff;
193+
font-weight: bold;
194+
font-size: 15px;
195+
border: 1px solid #54585d;
196+
}}
197+
table tbody td {{
198+
color: #636363;
199+
border: 1px solid #dddfe1;
200+
}}
201+
table tbody tr {{
202+
background-color: #f9fafb;
203+
}}
204+
table tbody tr:nth-child(odd) {{
205+
background-color: #ffffff;
206+
}}
207+
</style>
208+
<title>{html_page_title}</title>
209+
</head>
210+
<body>
211+
<h1>{html_page_title}</h1>
212+
<hr>
213+
<h4>Generated on: {now}<br>SHA: {sha}</h4>
214+
<hr>
215+
"""
216+
217+
with open(csv_file_path, 'r') as csv_file:
218+
reader = csv.reader(csv_file)
219+
headers = next(reader)
220+
data = list(reader)
221+
222+
html_table = f"<h2>{html_table_title}</h2><table>"
223+
html_table += "<tr>" + "".join(f"<th>{header}</th>" for header in headers) + "</tr>"
224+
for row in data:
225+
html_table += "<tr>"
226+
for cell in row:
227+
if len(cell) > 100:
228+
html_table += "<td><details><summary>Show/Hide</summary>" + cell.replace('\n', '<br>') + "</details></td>"
229+
elif cell in ("PASS", "FAIL"):
230+
html_table += f"<td value='{cell}'>{cell}</td>"
231+
else:
232+
html_table += "<td>" + cell.replace('\n', '<br>') + "</td>"
233+
html_table += "</tr>"
234+
html_table += "</table>"
235+
html_report += html_table
236+
html_report += """
237+
</body>
238+
</html>
239+
"""
240+
241+
html_file = os.path.join(html_out_dir, "memory_report.html")
242+
print(f"Saving HTML report to {html_file}")
243+
with open(html_file, "w") as f:
244+
f.write(html_report)
245+
246+
except FileNotFoundError:
247+
print(f"Error: Could not find {csv_file_path}")
248+
except Exception as e:
249+
print(f"Error generating HTML report: {e}")
250+
251+
252+
def get_git_revision_hash() -> str:
253+
"""Gets the current Git revision hash."""
254+
try:
255+
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
256+
except subprocess.CalledProcessError:
257+
return "N/A" # Not a git repository, or git not available
258+
259+
260+
def main():
261+
"""Main function to parse arguments and run the memory analysis."""
262+
parser = argparse.ArgumentParser(
263+
description="Calculate FLASH and RAM usage on example apps and generate a report."
264+
)
265+
parser.add_argument(
266+
"--out-dir",
267+
help="Override the default ./out directory to search for built targets.",
268+
default=OUT_DIR
269+
)
270+
parser.add_argument(
271+
"--html-out-dir",
272+
help="Specify the directory to save the HTML report.",
273+
default=TMP_RESULTS_DIR
274+
)
275+
276+
args = parser.parse_args()
277+
278+
targets = find_built_targets(config_files_dict, args.out_dir)
279+
280+
results = {}
281+
print("APP\tFLASH\tRAM") # header for text output.
282+
for target in targets:
283+
result = calculate_flash_ram(target)
284+
if result:
285+
flash, ram, details = result
286+
results[target.name] = (flash, ram, details)
287+
print(f"{target.name}\t{flash}\t{ram}")
288+
289+
os.makedirs(args.html_out_dir, exist_ok=True)
290+
csv_filename = os.path.join(args.html_out_dir, "flash_ram.csv")
291+
generate_csv_report(results, csv_filename)
292+
generate_html_report(csv_filename, "Matter SDK Memory Usage Report",
293+
"Example Apps Memory Usage", args.html_out_dir, get_git_revision_hash())
294+
295+
296+
if __name__ == "__main__":
297+
main()

0 commit comments

Comments
 (0)