feat(memmap): add summary command to memory map script
This patch adds a `summary` command, invoked with:
poetry run memory summary <map-file>
This command is capable of producing a summary table, similar to the
footprint command, or an HTML flame-graph representing the size of ELF
image sections.
When specified with `-o` (`--old`), an old GNU map file is used as a
past state and subtracted from the GNU map file specified by
`<map-file>` to annotate printed information with additional size
deltas.
Note that many bootloaders include alignment padding and will align to
a page size. To exclude this alignment-padding from the summary, use the
`-e` switch.
For example:
$ poetry run memory summary bl1.map -e -d 1
| Module | .text | .data | .bss |
|-----------|--------------|----------|-----------|
| bl1 | 32616(+24) | 2217(+0) | 11175(+0) |
| lib | 55932(+2060) | 40(+0) | 10628(+0) |
| Subtotals | 88548(+2084) | 2257(+0) | 21803(+0) |
Total Static RAM memory (data + bss): 24060(+0) bytes
Total Flash memory (text + data): 90805(+2084) bytes
Change-Id: Id0bc157d09e654f89730dfbe318fa5f820e9f18d
Co-authored-by: Chris Kay <chris.kay@arm.com>
Signed-off-by: Chris Kay <chris.kay@arm.com>
Signed-off-by: Jimmy Brisson <jimmy.brisson@arm.com>
diff --git a/docs/license.rst b/docs/license.rst
index e35b9bb..05458b9 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -119,6 +119,15 @@
- ``include/lib/hob/mmram.h``
- ``include/lib/hob/mpinfo.h``
+- Some source files originating from the `mbed OS`_ project.
+ These files are licensed under the Apache License, Version 2.0, which is a
+ permissive license compatible with BSD-3-Clause. Any contributions to this
+ code must also be made under the terms of `Apache License 2.0`_.
+ These files are:
+
+ - ``tools/memory/memory/mapsummary.py``
+ - ``tools/memory/memory/mapsummary_flamegraph.hmtl``
+
.. _FreeBSD: http://www.freebsd.org
.. _Linux MIT license: https://raw.githubusercontent.com/torvalds/linux/master/LICENSES/preferred/MIT
.. _SCC: http://www.simple-cc.org/
@@ -126,3 +135,4 @@
.. _Apache License 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
.. _pydevicetree: https://pypi.org/project/pydevicetree/
.. _edk2: https://github.com/tianocore/edk2
+.. _mbed OS: https://github.com/ARMmbed/mbed-os/
diff --git a/docs/tools/memory-layout-tool.rst b/docs/tools/memory-layout-tool.rst
index 2506d2f..0a447c5 100644
--- a/docs/tools/memory-layout-tool.rst
+++ b/docs/tools/memory-layout-tool.rst
@@ -229,6 +229,169 @@
├── 00 4003000 4010000 d000
└── 01 4010000 4021000 11000
+Memory Summary
+~~~~~~~~~~~~~~
+
+The tool provides a by-translation-unit summary of the sizes (``text``, ``bss``,
+``data``) contributed by each translation unit or group of translation units.
+For example, to print a table of an FVP build, with a path depth of 3:
+
+.. code::
+
+ $ poetry run memory summary build/fvp/debug/bl1/bl1.map -d 3
+ | Module | .text | .data | .bss |
+ |----------------------------------------|---------------|-------------|---------------|
+ | [fill] | 3204(+3204) | 0(+0) | 97(+97) |
+ | bl1/aem_generic.o | 0(+0) | 0(+0) | 0(+0) |
+ | bl1/arm_bl1_fwu.o | 224(+224) | 80(+80) | 0(+0) |
+ | bl1/arm_bl1_setup.o | 608(+608) | 0(+0) | 17(+17) |
+ | bl1/arm_common.o | 116(+116) | 0(+0) | 0(+0) |
+ | bl1/arm_console.o | 116(+116) | 0(+0) | 40(+40) |
+ | bl1/arm_dev_rotpk.o | 0(+0) | 0(+0) | 0(+0) |
+ | bl1/arm_dyn_cfg.o | 276(+276) | 0(+0) | 7184(+7184) |
+ | bl1/arm_dyn_cfg_helpers.o | 364(+364) | 0(+0) | 0(+0) |
+ | bl1/arm_err.o | 12(+12) | 0(+0) | 0(+0) |
+ | bl1/arm_fconf_io.o | 0(+0) | 952(+952) | 0(+0) |
+ | bl1/arm_helpers.o | 44(+44) | 0(+0) | 0(+0) |
+ | bl1/arm_io_storage.o | 480(+480) | 0(+0) | 32(+32) |
+ | bl1/auth_mod.o | 1288(+1288) | 0(+0) | 0(+0) |
+ | bl1/backtrace.o | 444(+444) | 0(+0) | 0(+0) |
+ | bl1/bl1_arch_setup.o | 16(+16) | 0(+0) | 0(+0) |
+ | bl1/bl1_context_mgmt.o | 340(+340) | 0(+0) | 1392(+1392) |
+ | bl1/bl1_entrypoint.o | 236(+236) | 0(+0) | 0(+0) |
+ | bl1/bl1_exceptions.o | 2240(+2240) | 0(+0) | 0(+0) |
+ | bl1/bl1_fwu.o | 2188(+2188) | 44(+44) | 0(+0) |
+ | bl1/bl1_main.o | 620(+620) | 0(+0) | 0(+0) |
+ | bl1/bl_common.o | 772(+772) | 0(+0) | 4(+4) |
+ | bl1/board_arm_helpers.o | 44(+44) | 0(+0) | 0(+0) |
+ | bl1/board_arm_trusted_boot.o | 44(+44) | 16(+16) | 0(+0) |
+ | bl1/cache_helpers.o | 112(+112) | 0(+0) | 0(+0) |
+ | bl1/cci.o | 408(+408) | 0(+0) | 24(+24) |
+ | bl1/context.o | 348(+348) | 0(+0) | 0(+0) |
+ | bl1/context_mgmt.o | 1692(+1692) | 0(+0) | 48(+48) |
+ | bl1/cortex_a35.o | 96(+96) | 0(+0) | 0(+0) |
+ | bl1/cortex_a53.o | 248(+248) | 0(+0) | 0(+0) |
+ | bl1/cortex_a57.o | 384(+384) | 0(+0) | 0(+0) |
+ | bl1/cortex_a72.o | 356(+356) | 0(+0) | 0(+0) |
+ | bl1/cortex_a73.o | 304(+304) | 0(+0) | 0(+0) |
+ | bl1/cpu_helpers.o | 200(+200) | 0(+0) | 0(+0) |
+ | bl1/crypto_mod.o | 380(+380) | 0(+0) | 0(+0) |
+ | bl1/debug.o | 224(+224) | 0(+0) | 0(+0) |
+ | bl1/delay_timer.o | 64(+64) | 0(+0) | 8(+8) |
+ | bl1/enable_mmu.o | 112(+112) | 0(+0) | 0(+0) |
+ | bl1/errata_report.o | 564(+564) | 0(+0) | 0(+0) |
+ | bl1/fconf.o | 148(+148) | 0(+0) | 0(+0) |
+ | bl1/fconf_dyn_cfg_getter.o | 656(+656) | 32(+32) | 144(+144) |
+ | bl1/fconf_tbbr_getter.o | 332(+332) | 0(+0) | 24(+24) |
+ | bl1/fdt_wrappers.o | 452(+452) | 0(+0) | 0(+0) |
+ | bl1/fvp_bl1_setup.o | 168(+168) | 0(+0) | 0(+0) |
+ | bl1/fvp_common.o | 512(+512) | 0(+0) | 8(+8) |
+ | bl1/fvp_cpu_pwr.o | 136(+136) | 0(+0) | 0(+0) |
+ | bl1/fvp_err.o | 44(+44) | 0(+0) | 0(+0) |
+ | bl1/fvp_helpers.o | 148(+148) | 0(+0) | 0(+0) |
+ | bl1/fvp_io_storage.o | 228(+228) | 0(+0) | 16(+16) |
+ | bl1/fvp_trusted_boot.o | 292(+292) | 0(+0) | 0(+0) |
+ | bl1/generic_delay_timer.o | 136(+136) | 0(+0) | 16(+16) |
+ | bl1/img_parser_mod.o | 588(+588) | 0(+0) | 20(+20) |
+ | bl1/io_fip.o | 1332(+1332) | 0(+0) | 100(+100) |
+ | bl1/io_memmap.o | 736(+736) | 16(+16) | 32(+32) |
+ | bl1/io_semihosting.o | 648(+648) | 16(+16) | 0(+0) |
+ | bl1/io_storage.o | 1268(+1268) | 0(+0) | 104(+104) |
+ | bl1/mbedtls_common.o | 208(+208) | 0(+0) | 4(+4) |
+ | bl1/mbedtls_crypto.o | 636(+636) | 0(+0) | 0(+0) |
+ | bl1/mbedtls_x509_parser.o | 1588(+1588) | 0(+0) | 120(+120) |
+ | bl1/misc_helpers.o | 392(+392) | 0(+0) | 0(+0) |
+ | bl1/multi_console.o | 528(+528) | 1(+1) | 8(+8) |
+ | bl1/pl011_console.o | 308(+308) | 0(+0) | 0(+0) |
+ | bl1/plat_bl1_common.o | 208(+208) | 0(+0) | 0(+0) |
+ | bl1/plat_bl_common.o | 40(+40) | 0(+0) | 0(+0) |
+ | bl1/plat_common.o | 48(+48) | 0(+0) | 8(+8) |
+ | bl1/plat_log_common.o | 48(+48) | 0(+0) | 0(+0) |
+ | bl1/plat_tbbr.o | 128(+128) | 0(+0) | 0(+0) |
+ | bl1/platform_helpers.o | 12(+12) | 0(+0) | 0(+0) |
+ | bl1/platform_up_stack.o | 16(+16) | 0(+0) | 0(+0) |
+ | bl1/semihosting.o | 352(+352) | 0(+0) | 0(+0) |
+ | bl1/semihosting_call.o | 8(+8) | 0(+0) | 0(+0) |
+ | bl1/smmu_v3.o | 296(+296) | 0(+0) | 0(+0) |
+ | bl1/sp805.o | 64(+64) | 0(+0) | 0(+0) |
+ | bl1/tbbr_cot_bl1.o | 0(+0) | 48(+48) | 156(+156) |
+ | bl1/tbbr_cot_common.o | 0(+0) | 144(+144) | 306(+306) |
+ | bl1/tbbr_img_desc.o | 0(+0) | 768(+768) | 0(+0) |
+ | bl1/tf_log.o | 200(+200) | 4(+4) | 0(+0) |
+ | bl1/xlat_tables_arch.o | 736(+736) | 0(+0) | 0(+0) |
+ | bl1/xlat_tables_context.o | 192(+192) | 96(+96) | 1296(+1296) |
+ | bl1/xlat_tables_core.o | 2112(+2112) | 0(+0) | 0(+0) |
+ | bl1/xlat_tables_utils.o | 8(+8) | 0(+0) | 0(+0) |
+ | lib/libc.a/assert.o | 48(+48) | 0(+0) | 0(+0) |
+ | lib/libc.a/exit.o | 64(+64) | 0(+0) | 8(+8) |
+ | lib/libc.a/memchr.o | 44(+44) | 0(+0) | 0(+0) |
+ | lib/libc.a/memcmp.o | 52(+52) | 0(+0) | 0(+0) |
+ | lib/libc.a/memcpy.o | 32(+32) | 0(+0) | 0(+0) |
+ | lib/libc.a/memmove.o | 52(+52) | 0(+0) | 0(+0) |
+ | lib/libc.a/memset.o | 140(+140) | 0(+0) | 0(+0) |
+ | lib/libc.a/printf.o | 1532(+1532) | 0(+0) | 0(+0) |
+ | lib/libc.a/snprintf.o | 1748(+1748) | 0(+0) | 0(+0) |
+ | lib/libc.a/strcmp.o | 44(+44) | 0(+0) | 0(+0) |
+ | lib/libc.a/strlen.o | 28(+28) | 0(+0) | 0(+0) |
+ | lib/libfdt.a/fdt.o | 1460(+1460) | 0(+0) | 0(+0) |
+ | lib/libfdt.a/fdt_ro.o | 1392(+1392) | 0(+0) | 0(+0) |
+ | lib/libfdt.a/fdt_wip.o | 244(+244) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/asn1parse.o | 956(+956) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/bignum.o | 6796(+6796) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/bignum_core.o | 3252(+3252) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/constant_time.o | 280(+280) | 0(+0) | 8(+8) |
+ | lib/libmbedtls.a/md.o | 504(+504) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/memory_buffer_alloc.o | 1264(+1264) | 0(+0) | 40(+40) |
+ | lib/libmbedtls.a/oid.o | 752(+752) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/pk.o | 872(+872) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/pk_wrap.o | 848(+848) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/pkparse.o | 516(+516) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/platform.o | 92(+92) | 24(+24) | 0(+0) |
+ | lib/libmbedtls.a/platform_util.o | 96(+96) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/rsa.o | 6588(+6588) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/rsa_alt_helpers.o | 2340(+2340) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/sha256.o | 1448(+1448) | 0(+0) | 0(+0) |
+ | lib/libmbedtls.a/x509.o | 1028(+1028) | 0(+0) | 0(+0) |
+ | Subtotals | 69632(+69632) | 2241(+2241) | 11264(+11264) |
+ Total Static RAM memory (data + bss): 13505(+13505) bytes
+ Total Flash memory (text + data): 71873(+71873) bytes
+
+A delta between two images can be generated by passing the ``--old`` option with
+a path to the previous map file.
+
+For example:
+
+.. code::
+
+ $ poetry run memory summary ../maps/fvp-tbb-mbedtls/bl1.map --old ../maps/fvp-tbb-mbedtls/bl1.map.old -d 1
+ | Module | .text | .data | .bss |
+ |-----------|---------------|----------|--------------|
+ | [fill] | 780(-2424) | 0(+0) | 321(+224) |
+ | bl1 | 32024(+108) | 2217(+0) | 11111(+0) |
+ | lib | 45020(+10508) | 24(+0) | 1880(+1824) |
+ | Subtotals | 77824(+8192) | 2241(+0) | 13312(+2048) |
+ Total Static RAM memory (data + bss): 15553(+2048) bytes
+ Total Flash memory (text + data): 80065(+8192) bytes
+
+Note that since the old map file includes the required suffix, specifying the
+``--old`` argument is optional here.
+
+Under some circumstances, some executables are padded to meet certain
+alignments, such as a 4KB page boundary, and excluding that padding can provide
+more helpful diffs. Taking the last example, and adding the ``-e`` argument
+yields such a summary:
+
+.. code::
+
+ $ poetry run memory summary ../maps/fvp-tbb-mbedtls/bl1.map --old ../maps/fvp-tbb-mbedtls/bl1.map.old -d 1 -e
+ | Module | .text | .data | .bss |
+ |-----------|---------------|----------|--------------|
+ | bl1 | 32024(+108) | 2217(+0) | 11111(+0) |
+ | lib | 45020(+10508) | 24(+0) | 1880(+1824) |
+ | Subtotals | 77044(+10616) | 2241(+0) | 12991(+1824) |
+ Total Static RAM memory (data + bss): 15232(+1824) bytes
+ Total Flash memory (text + data): 79285(+10616) bytes
+
--------------
*Copyright (c) 2023-2025, Arm Limited. All rights reserved.*
diff --git a/tools/memory/poetry.lock b/tools/memory/poetry.lock
index 99fbfac..67641ee 100644
--- a/tools/memory/poetry.lock
+++ b/tools/memory/poetry.lock
@@ -40,6 +40,92 @@
]
[[package]]
+name = "jinja2"
+version = "3.1.6"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+ {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+]
+
+[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
@@ -161,4 +247,4 @@
[metadata]
lock-version = "2.0"
python-versions = "^3.8.0"
-content-hash = "91360b55f18ea83b2b9143a9c7662fae1a4570956b07bce96c635327ce3cc1da"
+content-hash = "72f05cdcfe5278c3fb4408ba76cc502c83a56615681d8307bf67fe759a9da442"
diff --git a/tools/memory/pyproject.toml b/tools/memory/pyproject.toml
index f50c26a..70d3de7 100644
--- a/tools/memory/pyproject.toml
+++ b/tools/memory/pyproject.toml
@@ -12,6 +12,7 @@
prettytable = "^3.5.0"
pyelftools = "^0.29.0"
python = "^3.8.0"
+jinja2 = "^3.1.6"
[tool.poetry.scripts]
memory = "memory.memmap:main"
diff --git a/tools/memory/src/memory/memmap.py b/tools/memory/src/memory/memmap.py
index fd24680..e02010b 100755
--- a/tools/memory/src/memory/memmap.py
+++ b/tools/memory/src/memory/memmap.py
@@ -16,6 +16,7 @@
from memory.image import Image
from memory.mapparser import TfaMapParser
from memory.printer import TfaPrettyPrinter
+from memory.summary import MapParser
@dataclass
@@ -190,6 +191,27 @@
obj.printer.print_symbol_table(symbols, list(images.keys()))
+@cli.command()
+@click.option("-o", "--old", type=click.Path(exists=True))
+@click.option("-d", "--depth", type=int, default=2)
+@click.option("-e", "--exclude-fill")
+@click.option(
+ "-t",
+ "--type",
+ type=click.Choice(MapParser.export_formats, case_sensitive=False),
+ default="table",
+)
+@click.argument("file", type=click.Path(exists=True))
+def summary(file: Path, old: Optional[Path], depth: int, exclude_fill: bool, type: str):
+ """Summarize the sizes of translation units within the resulting binary"""
+ memap = MapParser()
+
+ if not memap.parse(file, old, exclude_fill):
+ exit(1)
+
+ memap.generate_output(type, depth)
+
+
def main():
cli(obj=Context())
diff --git a/tools/memory/src/memory/summary.py b/tools/memory/src/memory/summary.py
new file mode 100644
index 0000000..b116caa
--- /dev/null
+++ b/tools/memory/src/memory/summary.py
@@ -0,0 +1,557 @@
+#
+# Copyright (c) 2016-2025, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import json
+import os
+import re
+from collections import defaultdict
+from copy import deepcopy
+from os.path import (
+ abspath,
+ basename,
+ commonprefix,
+ dirname,
+ join,
+ relpath,
+ splitext,
+)
+from pathlib import Path
+from sys import stdout
+from typing import IO, Any, Dict, List, Optional, Pattern, Tuple, Union
+
+from jinja2 import FileSystemLoader, StrictUndefined
+from jinja2.environment import Environment
+from prettytable import HEADER, PrettyTable
+
+ModuleStats = Dict[str, int]
+Modules = Dict[str, ModuleStats]
+
+SECTIONS: Tuple[str, ...] = (".text", ".data", ".bss", ".heap", ".stack")
+MISC_FLASH_SECTIONS: Tuple[str, ...] = (".interrupts", ".flash_config")
+OTHER_SECTIONS: Tuple[str, ...] = (
+ ".interrupts_ram",
+ ".init",
+ ".ARM.extab",
+ ".ARM.exidx",
+ ".ARM.attributes",
+ ".eh_frame",
+ ".init_array",
+ ".fini_array",
+ ".jcr",
+ ".stab",
+ ".stabstr",
+ ".ARM.exidx",
+ ".ARM",
+)
+ALL_SECTIONS: Tuple[str, ...] = (
+ SECTIONS + OTHER_SECTIONS + MISC_FLASH_SECTIONS + ("unknown", "OUTPUT")
+)
+
+
+class Parser:
+ """Internal interface for parsing"""
+
+ _RE_OBJECT_FILE: Pattern[str] = re.compile(r"^(.+\/.+\.o(bj)?)$")
+ _RE_LIBRARY_OBJECT: Pattern[str] = re.compile(
+ r"((^.+" + r"" + r"lib.+\.a)\((.+\.o(bj)?)\))$"
+ )
+ _RE_STD_SECTION: Pattern[str] = re.compile(r"^\s+.*0x(\w{8,16})\s+0x(\w+)\s(.+)$")
+ _RE_FILL_SECTION: Pattern[str] = re.compile(
+ r"^\s*\*fill\*\s+0x(\w{8,16})\s+0x(\w+).*$"
+ )
+ _RE_TRANS_FILE: Pattern[str] = re.compile(r"^(.+\/|.+\.ltrans.o(bj)?)$")
+ _OBJECT_EXTENSIONS: Tuple[str, ...] = (".o", ".obj")
+
+ _modules: Modules
+ _fill: bool
+
+ def __init__(self, fill: bool = True):
+ self._modules: Modules = {}
+ self._fill = fill
+
+ def module_add(self, object_name: str, size: int, section: str):
+ """Adds a module or section to the list
+
+ Positional arguments:
+ object_name - name of the entry to add
+ size - the size of the module being added
+ section - the section the module contributes to
+ """
+ if (
+ not object_name
+ or not size
+ or not section
+ or (not self._fill and object_name == "[fill]")
+ ):
+ return
+
+ if object_name in self._modules:
+ self._modules[object_name].setdefault(section, 0)
+ self._modules[object_name][section] += size
+ return
+
+ obj_split = os.sep + basename(object_name)
+ for module_path, contents in self._modules.items():
+ if module_path.endswith(obj_split) or module_path == object_name:
+ contents.setdefault(section, 0)
+ contents[section] += size
+ return
+
+ new_module: ModuleStats = defaultdict(int)
+ new_module[section] = size
+ self._modules[object_name] = new_module
+
+ def module_replace(self, old_object: str, new_object: str):
+ """Replaces an object name with a new one"""
+ if old_object in self._modules:
+ self._modules[new_object] = self._modules.pop(old_object)
+
+ def check_new_section(self, line: str) -> Optional[str]:
+ """Check whether a new section in a map file has been detected
+
+ Positional arguments:
+ line - the line to check for a new section
+
+ return value - A section name, if a new section was found, None
+ otherwise
+ """
+ line_s = line.strip()
+ for i in ALL_SECTIONS:
+ if line_s.startswith(i):
+ return i
+ if line.startswith("."):
+ return "unknown"
+ else:
+ return None
+
+ def parse_object_name(self, line: str) -> str:
+ """Parse a path to object file
+
+ Positional arguments:
+ line - the path to parse the object and module name from
+
+ return value - an object file name
+ """
+ if re.match(self._RE_TRANS_FILE, line):
+ return "[misc]"
+
+ test_re_file_name = re.match(self._RE_OBJECT_FILE, line)
+
+ if test_re_file_name:
+ object_name = test_re_file_name.group(1)
+
+ return object_name
+ else:
+ test_re_obj_name = re.match(self._RE_LIBRARY_OBJECT, line)
+
+ if test_re_obj_name:
+ return join(test_re_obj_name.group(2), test_re_obj_name.group(3))
+ else:
+ if not line.startswith("LONG") and not line.startswith("linker stubs"):
+ print("Unknown object name found in GCC map file: %s" % line)
+ return "[misc]"
+
+ def parse_section(self, line: str) -> Tuple[str, int]:
+ """Parse data from a section of gcc map file
+
+ examples:
+ 0x00004308 0x7c ./BUILD/K64F/GCC_ARM/spi_api.o
+ .text 0x00000608 0x198 ./BUILD/K64F/HAL_CM4.o
+
+ Positional arguments:
+ line - the line to parse a section from
+ """
+ is_fill = re.match(self._RE_FILL_SECTION, line)
+ if is_fill:
+ o_name: str = "[fill]"
+ o_size: int = int(is_fill.group(2), 16)
+ return o_name, o_size
+
+ is_section = re.match(self._RE_STD_SECTION, line)
+ if is_section:
+ o_size: int = int(is_section.group(2), 16)
+ if o_size:
+ o_name: str = self.parse_object_name(is_section.group(3))
+ return o_name, o_size
+
+ return "", 0
+
+ def parse_mapfile(self, file_desc: IO[str]) -> Modules:
+ """Main logic to decode gcc map files
+
+ Positional arguments:
+ file_desc - a stream object to parse as a gcc map file
+ """
+ current_section: str = "unknown"
+
+ with file_desc as infile:
+ for line in infile:
+ if line.startswith("Linker script and memory map"):
+ current_section = "unknown"
+ break
+
+ for line in infile:
+ next_section = self.check_new_section(line)
+
+ if next_section == "OUTPUT":
+ break
+ elif next_section:
+ current_section = next_section
+
+ object_name, object_size = self.parse_section(line)
+ self.module_add(object_name, object_size, current_section)
+
+ def is_obj(name: str) -> bool:
+ return not name.startswith("[") or not name.endswith("]")
+
+ common_prefix: str = dirname(
+ commonprefix([o for o in self._modules.keys() if is_obj(o)])
+ )
+ new_modules: Modules = {}
+ for name, stats in self._modules.items():
+ if is_obj(name):
+ new_modules[relpath(name, common_prefix)] = stats
+ else:
+ new_modules[name] = stats
+ return new_modules
+
+
+class MapParser(object):
+ """An object that represents parsed results, parses the memory map files,
+ and writes out different file types of memory results
+ """
+
+ print_sections: Tuple[str, ...] = (".text", ".data", ".bss")
+ delta_sections: Tuple[str, ...] = (".text-delta", ".data-delta", ".bss-delta")
+
+ # sections to print info (generic for all toolchains)
+ sections: Tuple[str, ...] = SECTIONS
+ misc_flash_sections: Tuple[str, ...] = MISC_FLASH_SECTIONS
+ other_sections: Tuple[str, ...] = OTHER_SECTIONS
+
+ modules: Modules
+ old_modules: Modules
+ short_modules: Modules
+ mem_report: List[Dict[str, Union[str, ModuleStats]]]
+ mem_summary: Dict[str, int]
+ subtotal: Dict[str, int]
+ tc_name: Optional[str]
+
+ RAM_FORMAT_STR: str = "Total Static RAM memory (data + bss): {}({:+}) bytes\n"
+ ROM_FORMAT_STR: str = "Total Flash memory (text + data): {}({:+}) bytes\n"
+
+ def __init__(self):
+ # list of all modules and their sections
+ # full list - doesn't change with depth
+ self.modules: Modules = {}
+ self.old_modules = {}
+ # short version with specific depth
+ self.short_modules: Modules = {}
+
+ # Memory report (sections + summary)
+ self.mem_report: List[Dict[str, Union[str, ModuleStats]]] = []
+
+ # Memory summary
+ self.mem_summary: Dict[str, int] = {}
+
+ # Totals of ".text", ".data" and ".bss"
+ self.subtotal: Dict[str, int] = {}
+
+ # Name of the toolchain, for better headings
+ self.tc_name = None
+
+ def reduce_depth(self, depth: Optional[int]):
+ """
+ populates the short_modules attribute with a truncated module list
+
+ (1) depth = 1:
+ main.o
+ mbed-os
+
+ (2) depth = 2:
+ main.o
+ mbed-os/test.o
+ mbed-os/drivers
+
+ """
+ if depth == 0 or depth is None:
+ self.short_modules = deepcopy(self.modules)
+ else:
+ self.short_modules = dict()
+ for module_name, v in self.modules.items():
+ split_name = module_name.split(os.sep)
+ if split_name[0] == "":
+ split_name = split_name[1:]
+ new_name = join(*split_name[:depth])
+ self.short_modules.setdefault(new_name, defaultdict(int))
+ for section_idx, value in v.items():
+ self.short_modules[new_name][section_idx] += value
+ delta_name = section_idx + "-delta"
+ self.short_modules[new_name][delta_name] += value
+
+ for module_name, v in self.old_modules.items():
+ split_name = module_name.split(os.sep)
+ if split_name[0] == "":
+ split_name = split_name[1:]
+ new_name = join(*split_name[:depth])
+ self.short_modules.setdefault(new_name, defaultdict(int))
+ for section_idx, value in v.items():
+ delta_name = section_idx + "-delta"
+ self.short_modules[new_name][delta_name] -= value
+
+ export_formats: List[str] = ["json", "html", "table"]
+
+ def generate_output(
+ self,
+ export_format: str,
+ depth: Optional[int],
+ file_output: Optional[str] = None,
+ ) -> Optional[bool]:
+ """Generates summary of memory map data
+
+ Positional arguments:
+ export_format - the format to dump
+
+ Keyword arguments:
+ file_desc - descriptor (either stdout or file)
+ depth - directory depth on report
+
+ Returns: generated string for the 'table' format, otherwise Nonef
+ """
+ if depth is None or depth > 0:
+ self.reduce_depth(depth)
+ self.compute_report()
+ try:
+ if file_output:
+ file_desc = open(file_output, "w")
+ else:
+ file_desc = stdout
+ except IOError as error:
+ print("I/O error({0}): {1}".format(error.errno, error.strerror))
+ return False
+
+ to_call = {
+ "json": self.generate_json,
+ "html": self.generate_html,
+ "table": self.generate_table,
+ }[export_format]
+ to_call(file_desc)
+
+ if file_desc is not stdout:
+ file_desc.close()
+
+ @staticmethod
+ def _move_up_tree(tree: Dict[str, Any], next_module: str) -> Dict[str, Any]:
+ tree.setdefault("children", [])
+ for child in tree["children"]:
+ if child["name"] == next_module:
+ return child
+
+ new_module = {"name": next_module, "value": 0, "delta": 0}
+ tree["children"].append(new_module)
+
+ return new_module
+
+ def generate_html(self, file_desc: IO[str]):
+ """Generate a json file from a memory map for D3
+
+ Positional arguments:
+ file_desc - the file to write out the final report to
+ """
+
+ tree_text = {"name": ".text", "value": 0, "delta": 0}
+ tree_bss = {"name": ".bss", "value": 0, "delta": 0}
+ tree_data = {"name": ".data", "value": 0, "delta": 0}
+
+ def accumulate(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
+ parts = module_name.split(os.sep)
+
+ val = stats.get(size_key, 0)
+ tree_root["value"] += val
+ tree_root["delta"] += val
+
+ cur = tree_root
+ for part in parts:
+ cur = self._move_up_tree(cur, part)
+ cur["value"] += val
+ cur["delta"] += val
+
+ def subtract(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
+ parts = module_name.split(os.sep)
+
+ cur = tree_root
+ cur["delta"] -= stats.get(size_key, 0)
+
+ for part in parts:
+ children = {c["name"]: c for c in cur.get("children", [])}
+ if part not in children:
+ return
+
+ cur = children[part]
+ cur["delta"] -= stats.get(size_key, 0)
+
+ for module_name, dct in self.modules.items():
+ accumulate(tree_text, ".text", dct)
+ accumulate(tree_data, ".data", dct)
+ accumulate(tree_bss, ".bss", dct)
+
+ for module_name, dct in self.old_modules.items():
+ subtract(tree_text, ".text", dct)
+ subtract(tree_data, ".data", dct)
+ subtract(tree_bss, ".bss", dct)
+
+ jinja_loader = FileSystemLoader(dirname(abspath(__file__)))
+ jinja_environment = Environment(loader=jinja_loader, undefined=StrictUndefined)
+ template = jinja_environment.get_template("templates/summary-flamegraph.html")
+
+ name, _ = splitext(basename(file_desc.name))
+
+ if name.endswith("_map"):
+ name = name[:-4]
+ if self.tc_name:
+ name = f"{name} {self.tc_name}"
+
+ file_desc.write(
+ template.render(
+ {
+ "name": name,
+ "rom": json.dumps(
+ {
+ "name": "ROM",
+ "value": tree_text["value"] + tree_data["value"],
+ "delta": tree_text["delta"] + tree_data["delta"],
+ "children": [tree_text, tree_data],
+ }
+ ),
+ "ram": json.dumps(
+ {
+ "name": "RAM",
+ "value": tree_bss["value"] + tree_data["value"],
+ "delta": tree_bss["delta"] + tree_data["delta"],
+ "children": [tree_bss, tree_data],
+ }
+ ),
+ }
+ )
+ )
+
+ def generate_json(self, file_desc: IO[str]):
+ """Generate a json file from a memory map
+
+ Positional arguments:
+ file_desc - the file to write out the final report to
+ """
+ file_desc.write(json.dumps(self.mem_report, indent=4))
+ file_desc.write("\n")
+
+ def generate_table(self, file_desc: IO[str]):
+ """Generate a table from a memory map
+
+ Returns: string of the generated table
+ """
+ # Create table
+ columns = ["Module"]
+ columns.extend(self.print_sections)
+
+ table = PrettyTable(columns, junction_char="|", hrules=HEADER)
+ table.align["Module"] = "l"
+
+ for col in self.print_sections:
+ table.align[col] = "r"
+
+ for i in sorted(self.short_modules):
+ row = [i]
+
+ for k in self.print_sections:
+ row.append(
+ "{}({:+})".format(
+ self.short_modules[i][k], self.short_modules[i][k + "-delta"]
+ )
+ )
+
+ table.add_row(row)
+
+ subtotal_row = ["Subtotals"]
+ for k in self.print_sections:
+ subtotal_row.append(
+ "{}({:+})".format(self.subtotal[k], self.subtotal[k + "-delta"])
+ )
+
+ table.add_row(subtotal_row)
+
+ output = table.get_string()
+ output += "\n"
+
+ output += self.RAM_FORMAT_STR.format(
+ self.mem_summary["static_ram"], self.mem_summary["static_ram_delta"]
+ )
+ output += self.ROM_FORMAT_STR.format(
+ self.mem_summary["total_flash"], self.mem_summary["total_flash_delta"]
+ )
+ file_desc.write(output)
+
+ def compute_report(self):
+ """Generates summary of memory usage for main areas"""
+ self.subtotal = defaultdict(int)
+
+ for mod in self.modules.values():
+ for k in self.sections:
+ self.subtotal[k] += mod[k]
+ self.subtotal[k + "-delta"] += mod[k]
+
+ for mod in self.old_modules.values():
+ for k in self.sections:
+ self.subtotal[k + "-delta"] -= mod[k]
+
+ self.mem_summary = {
+ "static_ram": self.subtotal[".data"] + self.subtotal[".bss"],
+ "static_ram_delta": self.subtotal[".data-delta"]
+ + self.subtotal[".bss-delta"],
+ "total_flash": (self.subtotal[".text"] + self.subtotal[".data"]),
+ "total_flash_delta": self.subtotal[".text-delta"]
+ + self.subtotal[".data-delta"],
+ }
+
+ self.mem_report = []
+ if self.short_modules:
+ for name, sizes in sorted(self.short_modules.items()):
+ self.mem_report.append(
+ {
+ "module": name,
+ "size": {
+ k: sizes.get(k, 0)
+ for k in (self.print_sections + self.delta_sections)
+ },
+ }
+ )
+
+ self.mem_report.append({"summary": self.mem_summary})
+
+ def parse(
+ self, mapfile: Path, oldfile: Optional[Path] = None, no_fill: bool = False
+ ) -> bool:
+ """Parse and decode map file depending on the toolchain
+
+ Positional arguments:
+ mapfile - the file name of the memory map file
+ toolchain - the toolchain used to create the file
+ """
+ try:
+ with open(mapfile, "r") as file_input:
+ self.modules = Parser(not no_fill).parse_mapfile(file_input)
+ try:
+ if oldfile is not None:
+ with open(oldfile, "r") as old_input:
+ self.old_modules = Parser(not no_fill).parse_mapfile(old_input)
+ else:
+ self.old_modules = self.modules
+ except IOError:
+ self.old_modules = {}
+ return True
+
+ except IOError as error:
+ print("I/O error({0}): {1}".format(error.errno, error.strerror))
+ return False
diff --git a/tools/memory/src/memory/templates/summary-flamegraph.html b/tools/memory/src/memory/templates/summary-flamegraph.html
new file mode 100644
index 0000000..9ec8ecb
--- /dev/null
+++ b/tools/memory/src/memory/templates/summary-flamegraph.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
+ integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
+ <link rel="stylesheet" type="text/css"
+ href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@1.0.4/dist/d3.flameGraph.min.css"
+ integrity="sha256-w762vSe6WGrkVZ7gEOpnn2Y+FSmAGlX77jYj7nhuCyY=" crossorigin="anonymous" />
+
+ <style>
+ /* Space out content a bit */
+ body {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ }
+
+ /* Custom page header */
+ .header {
+ padding-bottom: 20px;
+ padding-right: 15px;
+ padding-left: 15px;
+ border-bottom: 1px solid #e5e5e5;
+ }
+
+ /* Make the masthead heading the same height as the navigation */
+ .header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 40px;
+ }
+ </style>
+
+ <title>{{name}} Memory Details</title>
+</head>
+
+<body>
+ <div class="container">
+ <div class="header clearfix">
+ <h3 class="text-muted">{{name}} Memory Details</h3>
+ </div>
+ <div id="chart-rom">
+ </div>
+ <hr />
+ <div id="chart-ram">
+ </div>
+ <hr />
+ <div id="details"></div>
+ </div>
+
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"
+ integrity="sha256-r7j1FXNTvPzHR41+V71Jvej6fIq4v4Kzu5ee7J/RitM=" crossorigin="anonymous">
+ </script>
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js"
+ integrity="sha256-z0A2CQF8xxCKuOJsn4sJ5HBjxiHHRAfTX8hDF4RSN5s=" crossorigin="anonymous">
+ </script>
+ <script type="text/javascript"
+ src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@1.0.4/dist/d3.flameGraph.min.js"
+ integrity="sha256-I1CkrWbmjv+GWjgbulJ4i0vbzdrDGfxqdye2qNlhG3Q=" crossorigin="anonymous">
+ </script>
+
+ <script type="text/javascript">
+ var tip = d3.tip()
+ .direction("s")
+ .offset([8, 0])
+ .attr('class', 'd3-flame-graph-tip')
+ .html(function (d) { return "module: " + d.data.name + ", bytes: " + d.data.value + ", delta: " + d.data.delta; });
+ var colorizer = function (d) {
+ if (d.data.delta > 0) {
+ ratio = (d.data.value - d.data.delta) / d.data.value;
+ green = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+ blue = ("0" + (Number(ratio * 0xEE | 0).toString(16))).slice(-2).toUpperCase();
+ console.log(d.data.name, green, blue);
+ return "#EE" + green + blue
+ } else if (d.data.delta < 0) {
+ ratio = (d.data.value + d.data.delta) / d.data.value;
+ green = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+ red = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+ console.log(d.data.name, red, green);
+ return "#" + red + green + "EE";
+ } else {
+ return "#FFFFEE";
+ }
+ }
+ var flameGraph_rom = d3.flameGraph()
+ .transitionDuration(250)
+ .transitionEase(d3.easeCubic)
+ .sort(true)
+ .color(colorizer)
+ .tooltip(tip);
+ var flameGraph_ram = d3.flameGraph()
+ .transitionDuration(250)
+ .transitionEase(d3.easeCubic)
+ .sort(true)
+ .color(colorizer)
+ .tooltip(tip);
+ var rom_elem = d3.select("#chart-rom");
+ flameGraph_rom.width(rom_elem.node().getBoundingClientRect().width);
+ rom_elem.datum({{ rom }}).call(flameGraph_rom);
+ var ram_elem = d3.select("#chart-ram");
+ flameGraph_ram.width(ram_elem.node().getBoundingClientRect().width);
+ ram_elem.datum({{ ram }}).call(flameGraph_ram);
+ </script>
+</body>
+
+</html>