feat: add memory usage analyser tool

Add command line tool that reports per-function code size and stack
usage from ELF binaries built with Rust stack size metadata. Support
table output on stdout by default, and CSV/JSON exports for integration
with other tooling.

Signed-off-by: Imre Kis <imre.kis@arm.com>
Change-Id: I0ed2b14f32c6acc853b2dd79368bf85a9446cc5d
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..b69522e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,330 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "csv"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde_core",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "leb128"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memory-usage-analyser"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "csv",
+ "rustc-demangle",
+ "serde",
+ "serde_json",
+ "stack-sizes",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "stack-sizes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feea0c7c3779faa3e1420b29d8041fdb0afe9c4f4ee2d6cc6a903054b8897f05"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "leb128",
+ "xmas-elf",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "xmas-elf"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22678df5df766e8d1e5d609da69f0c3132d794edf6ab5e75e7abcd2270d4cf58"
+dependencies = [
+ "zero",
+]
+
+[[package]]
+name = "zero"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fe21bcc34ca7fe6dd56cc2cb1261ea59d6b93620215aefb5ea6032265527784"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3657a3b
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,18 @@
+# SPDX-FileCopyrightText: Copyright The memory-usage-analyser Contributors.
+# SPDX-License-Identifier: BSD-3-Clause
+
+[package]
+name = "memory-usage-analyser"
+authors = ["Imre Kis <imre.kis@arm.com>"]
+description = "Gather metrics for each function in the ELF files."
+edition = "2024"
+license = "BSD-3-Clause"
+version = "0.1.0"
+
+[dependencies]
+clap = { version = "4.6.1", features = ["derive"] }
+csv = "1.4.0"
+rustc-demangle = "0.1.27"
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.149"
+stack-sizes = "0.5.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..35ed45c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+SPDX-FileCopyrightText: Copyright The memory-usage-analyser Contributors.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may
+be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..34fcaaa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# Memory Usage Analyser
+
+A tool to generate statistics from an ELF file, such as code and stack usage on a per-function
+basis. Only software implemented in Rust can be analysed through this tool, as it expects symbols
+to be encoded per Rust mangling rules. Outputs data in a number of formats, such as simple text, CSV
+or JSON, for possible integration with other tools.
+
+## Usage
+
+1. Build the project you want to analyse with stack size metadata enabled.
+
+   For a Rust project, this requires the Rust nightly toolchain and the `-Zemit-stack-sizes`
+   compiler flag. For release builds, also keep debug information in the output file with
+   `-C strip=none`.
+
+   ```sh
+   RUSTFLAGS="-Zemit-stack-sizes -C strip=none" cargo +nightly build --release
+   ```
+
+2. Build Memory Usage Analyser.
+
+   ```sh
+   cargo build
+   ```
+
+3. Run Memory Usage Analyser on the generated ELF file.
+
+   ```sh
+   memory-usage-analyser -i path/to/project/target/output
+   ```
+
+   By default, the tool prints a table containing each function's address, stack usage, code size
+   and name.
+
+4. Export the results if you want to process them with other tools.
+
+   ```sh
+   memory-usage-analyser -i path/to/project/target/output -O json -o memory-usage.json
+   memory-usage-analyser -i path/to/project/target/output -O csv -o memory-usage.csv
+   ```
+
+## License
+
+The project is provided under the BSD-3-Clause license, see [LICENSE](./LICENSE). Contributions to
+this project are accepted under the same license and must also be made under the terms of the
+[Developer Certificate of Origin](https://developercertificate.org), confirming that the code
+submitted can (legally) become part of the project.
+
+## Maintainers
+
+memory-usage-analyser is a trustedfirmware.org maintained project. All contributions are ultimately
+merged by the maintainers listed below.
+
+- Bálint Dobszay <balint.dobszay@arm.com>
+  [balint-dobszay-arm](https://github.com/balint-dobszay-arm)
+- Imre Kis <imre.kis@arm.com>
+  [imre-kis-arm](https://github.com/imre-kis-arm)
+- Sandrine Afsa <sandrine.afsa@arm.com>
+  [sandrine-bailleux-arm](https://github.com/sandrine-bailleux-arm)
+
+## Contributing
+
+Please follow the directions of the [Trusted Firmware Processes](https://trusted-firmware-docs.readthedocs.io/en/latest/generic_processes/index.html)
+
+Contributions are handled through [review.trustedfirmware.org](https://review.trustedfirmware.org/q/project:shared/memory-usage-analyser).
+
+--------------
+
+*Copyright The memory-usage-analyser Contributors.*
diff --git a/dco.txt b/dco.txt
new file mode 100644
index 0000000..8201f99
--- /dev/null
+++ b/dco.txt
@@ -0,0 +1,37 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..b3a172f
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,116 @@
+// SPDX-FileCopyrightText: Copyright The memory-usage-analyser Contributors.
+// SPDX-License-Identifier: BSD-3-Clause
+
+#![doc = include_str!("../README.md")]
+
+use clap::{Parser, ValueEnum};
+use rustc_demangle::demangle;
+use serde::{Deserialize, Serialize};
+use stack_sizes::{Function, analyze_executable};
+use std::{
+    fs::{File, read},
+    io::{Write, stdout},
+    path::PathBuf,
+    process::exit,
+};
+
+/// Gathers the stack usage of each function of the ELF file. Compile the binary with
+/// '-Z emit-stack-sizes' rustc flag set.
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+    /// Path of the ELF file input
+    #[arg(short = 'i', long)]
+    input: PathBuf,
+
+    /// Output file
+    #[arg(short = 'o', long)]
+    output: Option<PathBuf>,
+
+    /// Output format
+    #[arg(short = 'O', long)]
+    output_format: Option<OutputFormat>,
+}
+
+/// Output file format
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+enum OutputFormat {
+    Csv,
+    Json,
+}
+
+/// Contains the statistics of a single function.
+#[derive(Serialize, Deserialize, Debug)]
+struct FunctionStat {
+    pub name: String,
+    pub address: u64,
+    pub size: u64,
+    pub stack_usage: Option<u64>,
+}
+
+impl From<(u64, Function<'_>)> for FunctionStat {
+    fn from((address, function): (u64, Function)) -> Self {
+        let name = demangle(function.names()[0]).to_string();
+
+        Self {
+            name,
+            address,
+            size: function.size(),
+            stack_usage: function.stack(),
+        }
+    }
+}
+
+fn main() {
+    let args = Args::parse();
+
+    let Ok(elf_data) = read(&args.input) else {
+        eprintln!("Cannot read file: {:?}", args.input);
+        exit(1);
+    };
+
+    let functions = analyze_executable(&elf_data).unwrap();
+
+    let mut function_stats: Vec<_> = functions
+        .defined
+        .iter()
+        .map(|(address, function)| FunctionStat::from((*address, function.clone())))
+        .collect();
+
+    // Ascending order by stack usage
+    function_stats.sort_by_key(|fs| fs.stack_usage);
+
+    let mut writer: Box<dyn Write> = match args.output {
+        None => Box::new(stdout()),
+        Some(path) => Box::new(File::create(path).expect("Failed to create output file")),
+    };
+
+    match args.output_format {
+        None => {
+            writeln!(writer, "{:16} {:8} {:8} name", "address", "stack", "size")
+                .expect("Failed to write header");
+            for func in function_stats {
+                writeln!(
+                    writer,
+                    "{:016x} {:8x} {:8x} {}",
+                    func.address,
+                    func.stack_usage.unwrap_or(0),
+                    func.size,
+                    func.name
+                )
+                .expect("Failed to write output");
+            }
+        }
+        Some(OutputFormat::Csv) => {
+            let mut csv_writer = csv::Writer::from_writer(writer);
+
+            for record in function_stats {
+                csv_writer
+                    .serialize(record)
+                    .expect("Failed to write CSV output");
+            }
+        }
+        Some(OutputFormat::Json) => serde_json::to_writer_pretty(writer, &function_stats)
+            .expect("Failed to write JSON output"),
+    }
+}