Skip to content

Commit 3044eeb

Browse files
Add an additional binary ELF size differ we can use for size differences and docs (project-chip#37325)
* Add the binary size differ * Add documentation for tooling * Restyle * Fix text --------- Co-authored-by: Andrei Litvin <andreilitvin@google.com>
1 parent e28f439 commit 3044eeb

File tree

4 files changed

+282
-0
lines changed

4 files changed

+282
-0
lines changed

docs/tools/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Source files for these tools are located at `scripts/tools`.
2828
2929
../scripts/tools/memory/README
3030
../scripts/tools/spake2p/README
31+
../scripts/tools/ELF_SIZE_TOOLING
3132
3233
```
3334

scripts/tools/ELF_SIZE_TOOLING.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# ELF binary size information
2+
3+
## Individual size information
4+
5+
`file_size_from_nm.py` is able to build an interactive tree map of
6+
methods/namespaces sizes within an elf binary.
7+
8+
Use it to determine how much space specific parts of the code take. For example:
9+
10+
```
11+
./scripts/tools/file_size_from_nm.py \
12+
--zoom '::chip::app' \
13+
./out/qpg-qpg6105-light/chip-qpg6105-lighting-example.out
14+
```
15+
16+
could result in a graph like:
17+
18+
![image](./FileSizeOutputExample.png)
19+
20+
## Determine difference between two binaries
21+
22+
`binary_elf_size_diff` provides the ability to compare two elf files. Usually
23+
you can build the master branch of a binary and save it somewhere like
24+
`./out/master.elf` and then re-build with changes and compare.
25+
26+
Example runs:
27+
28+
```
29+
> ~/devel/chip-scripts/bindiff.py \
30+
./out/qpg-qpg6105-light/chip-qpg6105-lighting-example.out \
31+
./out/qpg-master.out
32+
33+
Type Size Function
34+
------- ------ -----------------------------------------------------------------------------------------------------------------------
35+
CHANGED -128 chip::app::CodegenDataModelProvider::WriteAttribute(chip::app::DataModel::WriteAttributeRequest const&, chip::app::A...
36+
CHANGED -76 chip::app::InteractionModelEngine::CheckCommandExistence(chip::app::ConcreteCommandPath const&, chip::app::DataModel...
37+
CHANGED -74 chip::app::reporting::Engine::CheckAccessDeniedEventPaths(chip::TLV::TLVWriter&, bool&, chip::app::ReadHandler*)
38+
REMOVED -58 chip::app::DataModel::EndpointFinder::EndpointFinder(chip::app::DataModel::ProviderMetadataTree*)
39+
REMOVED -44 chip::app::DataModel::EndpointFinder::Find(unsigned short)
40+
CHANGED 18 chip::app::WriteHandler::WriteClusterData(chip::Access::SubjectDescriptor const&, chip::app::ConcreteDataAttributePa...
41+
ADDED 104 chip::app::DataModel::ValidateClusterPath(chip::app::DataModel::ProviderMetadataTree*, chip::app::ConcreteClusterPat...
42+
ADDED 224 chip::app::WriteHandler::CheckWriteAllowed(chip::Access::SubjectDescriptor const&, chip::app::ConcreteDataAttributeP...
43+
TOTAL -34
44+
45+
46+
```
47+
48+
```
49+
> ~/devel/chip-scripts/bindiff.py \
50+
--output csv --skip-total \
51+
./out/qpg-qpg6105-light/chip-qpg6105-lighting-example.out ./out/qpg-master.out
52+
53+
Type,Size,Function
54+
CHANGED,-128,"chip::app::CodegenDataModelProvider::WriteAttribute(chip::app::DataModel::WriteAttributeRequest const&, chip::app::AttributeValueDecoder&)"
55+
CHANGED,-76,"chip::app::InteractionModelEngine::CheckCommandExistence(chip::app::ConcreteCommandPath const&, chip::app::DataModel::AcceptedCommandEntry&)"
56+
CHANGED,-74,"chip::app::reporting::Engine::CheckAccessDeniedEventPaths(chip::TLV::TLVWriter&, bool&, chip::app::ReadHandler*)"
57+
REMOVED,-58,chip::app::DataModel::EndpointFinder::EndpointFinder(chip::app::DataModel::ProviderMetadataTree*)
58+
REMOVED,-44,chip::app::DataModel::EndpointFinder::Find(unsigned short)
59+
CHANGED,18,"chip::app::WriteHandler::WriteClusterData(chip::Access::SubjectDescriptor const&, chip::app::ConcreteDataAttributePath const&, chip::TLV::TLVReader&)"
60+
ADDED,104,"chip::app::DataModel::ValidateClusterPath(chip::app::DataModel::ProviderMetadataTree*, chip::app::ConcreteClusterPath const&, chip::Protocols::InteractionModel::Status)"
61+
ADDED,224,"chip::app::WriteHandler::CheckWriteAllowed(chip::Access::SubjectDescriptor const&, chip::app::ConcreteDataAttributePath const&)"
62+
63+
```
349 KB
Loading

scripts/tools/binary_elf_size_diff.py

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#!/usr/bin/env -S python3 -B
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+
# Processes 2 ELF files via `nm` and outputs the
19+
# diferences in size. Example calls:
20+
#
21+
# scripts/tools/bindiff.py \
22+
# ./out/updated_binary.elf \
23+
# ./out/master_build.elf
24+
#
25+
# scripts/tools/bindiff.py \
26+
# --output csv \
27+
# --no-demangle \
28+
# ./out/updated_binary.elf \
29+
# ./out/master_build.elf
30+
#
31+
#
32+
# Requires:
33+
# - click
34+
# - coloredlogs
35+
# - cxxfilt
36+
# - tabulate
37+
38+
import csv
39+
import logging
40+
import os
41+
import subprocess
42+
import sys
43+
from dataclasses import dataclass
44+
from enum import Enum, auto
45+
from pathlib import Path
46+
47+
import click
48+
import coloredlogs
49+
import cxxfilt
50+
import tabulate
51+
52+
53+
@dataclass
54+
class Symbol:
55+
symbol_type: str
56+
name: str
57+
size: int
58+
59+
60+
# Supported log levels, mapping string values required for argument
61+
# parsing into logging constants
62+
__LOG_LEVELS__ = {
63+
"debug": logging.DEBUG,
64+
"info": logging.INFO,
65+
"warn": logging.WARN,
66+
"fatal": logging.FATAL,
67+
}
68+
69+
70+
class OutputType(Enum):
71+
TABLE = (auto(),)
72+
CSV = (auto(),)
73+
74+
75+
__OUTPUT_TYPES__ = {
76+
"table": OutputType.TABLE,
77+
"csv": OutputType.CSV,
78+
}
79+
80+
81+
def get_sizes(p: Path, no_demangle: bool):
82+
output = subprocess.check_output(
83+
["nm", "--print-size", "--size-sort", "--radix=d", p.as_posix()]
84+
).decode("utf8")
85+
86+
result = {}
87+
88+
for line in output.split("\n"):
89+
if not line.strip():
90+
continue
91+
92+
_, size, t, name = line.split(" ")
93+
size = int(size, 10)
94+
95+
if not no_demangle:
96+
name = cxxfilt.demangle(name)
97+
98+
result[name] = Symbol(symbol_type=t, name=name, size=size)
99+
100+
return result
101+
102+
103+
def default_cols():
104+
try:
105+
# if terminal output, try to fit
106+
return os.get_terminal_size().columns - 29
107+
except Exception:
108+
return 120
109+
110+
111+
@click.command()
112+
@click.option(
113+
"--log-level",
114+
default="INFO",
115+
show_default=True,
116+
type=click.Choice(list(__LOG_LEVELS__.keys()), case_sensitive=False),
117+
help="Determines the verbosity of script output.",
118+
)
119+
@click.option(
120+
"--output",
121+
default="TABLE",
122+
show_default=True,
123+
type=click.Choice(list(__OUTPUT_TYPES__.keys()), case_sensitive=False),
124+
help="Determines the type of the output (use CSV for easier parsing).",
125+
)
126+
@click.option(
127+
"--skip-total",
128+
default=False,
129+
is_flag=True,
130+
help="Skip the output of a TOTAL line (i.e. a sum of all size deltas)"
131+
)
132+
@click.option(
133+
"--no-demangle",
134+
default=False,
135+
is_flag=True,
136+
help="Skip CXX demangling. Note that this will not deduplicate inline method instantiations."
137+
)
138+
@click.option(
139+
"--style",
140+
default="simple",
141+
show_default=True,
142+
help="tablefmt style for table output (e.g.: simple, plain, grid, fancy_grid, pipe, orgtbl, jira, presto, pretty, psql, rst)",
143+
)
144+
@click.option(
145+
"--name-truncate",
146+
default=default_cols(),
147+
show_default=True,
148+
type=int,
149+
help="Truncate function name to this length (for table output only). use <= 10 to disable",
150+
)
151+
@click.argument("f1", type=Path)
152+
@click.argument("f2", type=Path)
153+
def main(
154+
log_level,
155+
output,
156+
skip_total,
157+
no_demangle,
158+
style: str,
159+
name_truncate: int,
160+
f1: Path,
161+
f2: Path,
162+
):
163+
log_fmt = "%(asctime)s %(levelname)-7s %(message)s"
164+
coloredlogs.install(level=__LOG_LEVELS__[log_level], fmt=log_fmt)
165+
166+
r1 = get_sizes(f1, no_demangle)
167+
r2 = get_sizes(f2, no_demangle)
168+
169+
output_type = __OUTPUT_TYPES__[output]
170+
171+
# at this point every key has a size information
172+
# We are interested in sizes that are DIFFERENT (add/remove or changed)
173+
delta = []
174+
total = 0
175+
for k in set(r1.keys()) | set(r2.keys()):
176+
if k in r1 and k in r2 and r1[k].size == r2[k].size:
177+
continue
178+
179+
# At this point the value is in v1 or v2
180+
s1 = r1[k].size if k in r1 else 0
181+
s2 = r2[k].size if k in r2 else 0
182+
name = r1[k].name if k in r1 else r2[k].name
183+
184+
if k in r1 and k in r2:
185+
change = "CHANGED"
186+
elif k in r1:
187+
change = "ADDED"
188+
else:
189+
change = "REMOVED"
190+
191+
if (
192+
output_type == OutputType.TABLE
193+
and name_truncate > 10
194+
and len(name) > name_truncate
195+
):
196+
name = name[: name_truncate - 4] + "..."
197+
198+
delta.append([change, s1 - s2, name])
199+
total += s1 - s2
200+
201+
delta.sort(key=lambda x: x[1])
202+
if not skip_total:
203+
delta.append(["TOTAL", total, ""])
204+
205+
HEADER = ["Type", "Size", "Function"]
206+
207+
if output_type == OutputType.TABLE:
208+
print(tabulate.tabulate(delta, headers=HEADER, tablefmt=style))
209+
elif output_type == OutputType.CSV:
210+
writer = csv.writer(sys.stdout)
211+
writer.writerow(HEADER)
212+
writer.writerows(delta)
213+
else:
214+
raise Exception("Unknown output type: %r" % output)
215+
216+
217+
if __name__ == "__main__":
218+
main(auto_envvar_prefix="CHIP")

0 commit comments

Comments
 (0)