├── .gitignore ├── justfile ├── Cargo.toml ├── src ├── error.rs ├── lib.rs ├── main.rs ├── packages.rs ├── tree.rs └── tool.rs ├── .github └── workflows │ ├── release.yml │ └── test.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /dist 4 | /.kiro 5 | /*.json -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --list 3 | 4 | bloaty-build: 5 | cargo build --profile bloaty 6 | bloaty-csv: 7 | bloaty ./target/bloaty/bloaty-metafile -d sections,symbols -n 0 --csv > meta.csv 8 | bloaty-json: 9 | bloaty-metafile meta.csv > meta.json 10 | bloaty: bloaty-build bloaty-csv bloaty-json 11 | 12 | clippy: 13 | cargo clippy --fix --allow-dirty --allow-staged --all-targets 14 | fmt: 15 | cargo fmt 16 | check: fmt clippy 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bloaty-metafile" 3 | version = "0.1.8" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "bloaty-metafile" 7 | repository = "https://github.com/ahaoboy/bloaty-metafile" 8 | homepage = "https://github.com/ahaoboy/bloaty-metafile" 9 | authors = ["ahaoboy"] 10 | include = ["/src", "/Cargo.toml", "/README.md"] 11 | 12 | [dependencies] 13 | serde-metafile = "0.1" 14 | serde_json = "1" 15 | cargo-lock = { version = "11", features = ["dependency-tree"] } 16 | clap = { version = "4", features = ["derive"] } 17 | csv = "1" 18 | serde = "1" 19 | thiserror = "2" 20 | git-version = "0.3" 21 | const-str = "0.7" 22 | 23 | [profile.release] 24 | debug = false 25 | lto = true 26 | strip = true 27 | opt-level = 3 28 | codegen-units = 1 29 | 30 | [profile.bloaty] 31 | debug = true 32 | lto = false 33 | strip = false 34 | inherits = 'release' 35 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Custom error type for bloaty-metafile operations 4 | #[derive(Error, Debug)] 5 | pub enum BloatyError { 6 | /// Error reading a file from the filesystem 7 | #[error("Failed to read file: {path}")] 8 | FileRead { 9 | path: String, 10 | #[source] 11 | source: std::io::Error, 12 | }, 13 | 14 | /// Error parsing CSV data 15 | #[error("Failed to parse CSV")] 16 | CsvParse(#[from] csv::Error), 17 | 18 | /// Error serializing to JSON 19 | #[error("Failed to serialize JSON")] 20 | JsonSerialize(#[from] serde_json::Error), 21 | 22 | /// Error loading Cargo.lock file 23 | #[error("Failed to load Cargo.lock: {path}")] 24 | LockfileLoad { 25 | path: String, 26 | #[source] 27 | source: cargo_lock::Error, 28 | }, 29 | } 30 | 31 | /// Result type alias for bloaty-metafile operations 32 | pub type Result = std::result::Result; 33 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde_metafile::Metafile; 2 | use tree::Tree; 3 | 4 | mod error; 5 | mod packages; 6 | mod tool; 7 | mod tree; 8 | 9 | pub use error::{BloatyError, Result}; 10 | 11 | /// Convert bloaty CSV output to esbuild metafile format 12 | /// 13 | /// # Arguments 14 | /// 15 | /// * `csv` - CSV string containing bloaty output with sections, symbols, vmsize, and filesize columns 16 | /// * `name` - Name for the output binary in the metafile 17 | /// * `lock` - Optional path to Cargo.lock file for dependency resolution (defaults to "Cargo.lock") 18 | /// * `deep` - Maximum depth for tree traversal (0 means unlimited) 19 | /// * `no_sections` - If true, exclude section-level entries from the output 20 | /// 21 | /// # Returns 22 | /// 23 | /// Returns a `Result` containing the generated `Metafile` or a `BloatyError` if parsing fails 24 | /// 25 | /// # Example 26 | /// 27 | /// ```no_run 28 | /// use bloaty_metafile::from_csv; 29 | /// 30 | /// let csv = "sections,symbols,vmsize,filesize\n.text,main,1000,1000"; 31 | /// let metafile = from_csv(csv, "binary", None, 0, false)?; 32 | /// # Ok::<(), bloaty_metafile::BloatyError>(()) 33 | /// ``` 34 | pub fn from_csv( 35 | csv: &str, 36 | name: &str, 37 | lock: Option, 38 | deep: usize, 39 | no_sections: bool, 40 | ) -> Result { 41 | let tree = Tree::new(csv, lock, no_sections)?; 42 | Ok(tree.to_metafile(name, deep)) 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: [push, pull_request] 7 | 8 | defaults: 9 | run: 10 | shell: bash --noprofile --norc -CeEuo pipefail {0} 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - target: x86_64-apple-darwin 19 | os: macos-latest 20 | - target: aarch64-apple-darwin 21 | os: macos-latest 22 | - target: x86_64-pc-windows-msvc 23 | os: windows-latest 24 | RUSTFLAGS: -C target-feature=+crt-static 25 | - target: x86_64-pc-windows-gnu 26 | os: windows-latest 27 | # - target: arm64ec-pc-windows-msvc 28 | # os: windows-latest 29 | - target: aarch64-unknown-linux-musl 30 | os: ubuntu-latest 31 | - target: aarch64-unknown-linux-gnu 32 | os: ubuntu-latest 33 | - target: x86_64-unknown-linux-musl 34 | os: ubuntu-latest 35 | - target: x86_64-unknown-linux-gnu 36 | os: ubuntu-latest 37 | - target: aarch64-linux-android 38 | os: ubuntu-latest 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: run cross-build 43 | uses: ahaoboy/cross-build@v1 44 | with: 45 | bin: bloaty-metafile 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | target: ${{ matrix.target }} 48 | RUSTFLAGS: ${{ matrix.RUSTFLAGS }} 49 | tag: nightly 50 | allowUpdates: true 51 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use bloaty_metafile::{BloatyError, from_csv}; 2 | use clap::Parser; 3 | 4 | const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); 5 | const GIT_HASH: &str = git_version::git_version!(); 6 | const VERSION: &str = const_str::concat!(CARGO_PKG_VERSION, " ", GIT_HASH); 7 | 8 | #[derive(Parser, Debug, Clone)] 9 | #[command(version=VERSION, about, long_about = None)] 10 | pub struct Args { 11 | #[arg(short, long, default_value = "BINARY")] 12 | pub name: String, 13 | 14 | #[arg(short, long)] 15 | pub lock: Option, 16 | 17 | #[arg(short, long, default_value = "0")] 18 | pub deep: usize, 19 | 20 | #[arg(long, default_value = "false")] 21 | pub no_sections: bool, 22 | 23 | #[arg()] 24 | pub path: Option, 25 | } 26 | 27 | fn main() -> Result<(), BloatyError> { 28 | let Args { 29 | name, 30 | lock, 31 | deep, 32 | path, 33 | no_sections, 34 | } = Args::parse(); 35 | 36 | // Read CSV input from file or stdin 37 | let csv = if let Some(ref file_path) = path { 38 | std::fs::read_to_string(file_path).map_err(|source| BloatyError::FileRead { 39 | path: file_path.clone(), 40 | source, 41 | })? 42 | } else { 43 | std::io::read_to_string(std::io::stdin()).map_err(|source| BloatyError::FileRead { 44 | path: "stdin".to_string(), 45 | source, 46 | })? 47 | }; 48 | 49 | // Parse CSV and generate metafile 50 | let meta = from_csv(&csv, &name, lock, deep, no_sections)?; 51 | 52 | // Serialize to JSON 53 | let s = serde_json::to_string(&meta)?; 54 | 55 | // Check if JSON string is too large (JavaScript string length limit) 56 | // JavaScript max string length is 2^30 - 1 (0x3fffffff) characters 57 | // But V8 uses 0x1fffffe8 as practical limit 58 | const MAX_JSON_LENGTH: usize = 0x1fff_ffe8; // ~536MB 59 | 60 | let json_len = s.len(); 61 | 62 | if json_len > MAX_JSON_LENGTH { 63 | eprintln!( 64 | "Warning: JSON output is too large ({} bytes, {} MB)", 65 | json_len, 66 | json_len >> 20 67 | ); 68 | eprintln!("This exceeds JavaScript's maximum string length (0x1fffffe8 characters)"); 69 | eprintln!("The output may not be usable in web-based tools like esbuild analyzer"); 70 | } 71 | 72 | println!("{s}"); 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: [push, pull_request] 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | defaults: 13 | run: 14 | shell: bash --noprofile --norc -CeEuo pipefail {0} 15 | 16 | jobs: 17 | rust-test: 18 | strategy: 19 | matrix: 20 | include: 21 | # - target: x86_64-apple-darwin 22 | # os: macos-latest 23 | - target: aarch64-apple-darwin 24 | os: macos-latest 25 | # - target: x86_64-pc-windows-gnu 26 | # os: windows-latest 27 | - target: x86_64-unknown-linux-gnu 28 | os: ubuntu-latest 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | ref: ${{ github.head_ref || github.ref_name }} 34 | - uses: actions/setup-node@v4 35 | - name: Install latest nightly 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: nightly 39 | override: true 40 | - name: cargo test 41 | run: | 42 | cargo test 43 | - name: cargo install 44 | run: | 45 | cargo install --path=. 46 | - name: install easy-install 47 | uses: ahaoboy/easy-setup@v1 48 | with: 49 | url: |- 50 | https://github.com/nushell/nushell 51 | https://github.com/ahaoboy/bloaty-build 52 | https://github.com/ahaoboy/metafile-viz 53 | 54 | - name: install metafile-image 55 | run: | 56 | npm i metafile-image -g 57 | metafile-image --version 58 | metafile-viz --version 59 | - name: Build & Run All 60 | run: | 61 | cat << 'EOF' > resources.json 62 | [ 63 | { 64 | "name": "nu", 65 | "lock": "https://github.com/nushell/nushell/raw/refs/heads/main/Cargo.lock" 66 | }, 67 | { 68 | "name": "cargo", 69 | "lock": "https://github.com/rust-lang/cargo/raw/refs/heads/master/Cargo.lock" 70 | }, 71 | { 72 | "name": "rustc", 73 | "lock": "https://github.com/rust-lang/rust/raw/refs/heads/main/Cargo.lock" 74 | } 75 | ] 76 | EOF 77 | 78 | jq -c '.[]' resources.json | while read item; do 79 | name=$(echo "$item" | jq -r '.name') 80 | lock_url=$(echo "$item" | jq -r '.lock') 81 | bin=$(which "$name") 82 | 83 | curl -L -o "$name.lock" "$lock_url" 84 | 85 | csv="${name}-${{ matrix.os }}.csv" 86 | json="${name}-${{ matrix.os }}.json" 87 | 88 | bloaty "$bin" -d sections,symbols -n 0 --csv > "$csv" 89 | 90 | bloaty-metafile "$csv" --name="$name" --lock="$name.lock" --no-sections > "$json" 91 | 92 | metafile-image "$json" "${name}-${{ matrix.os }}.png" 93 | metafile-viz "$json" -o "${name}-${{ matrix.os }}-viz.png" 94 | done 95 | 96 | ls -lh . 97 | 98 | - name: Upload 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: bloaty-metafile-${{ matrix.os }} 102 | path: | 103 | *-${{ matrix.os }}* 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bloaty-metafile is a cli tool to convert csv files generated by [bloaty](https://github.com/google/bloaty) to esbuild's [metafile](https://esbuild.github.io/api/#metafile) format, so that you can use [online tools](https://esbuild.github.io/analyze/) to analyze the size of the program 2 | 3 | ```bash 4 | cargo binstall bloaty-metafile 5 | 6 | # or install from github 7 | cargo install --git https://github.com/ahaoboy/bloaty-metafile 8 | 9 | # https://github.com/google/bloaty/blob/main/doc/using.md 10 | bloaty ./bloaty -d sections,symbols -n 0 --csv | bloaty-metafile > meta.json 11 | bloaty-metafile meta.csv > meta.json 12 | 13 | bloaty ./target/bloaty/bloaty-metafile -d sections,symbols -n 0 --csv | bloaty-metafile --name=bloaty-metafile --lock=Cargo.lock > meta.json 14 | ``` 15 | 16 | ## profile 17 | 18 | In order for bloaty to parse symbol information properly, it is recommended to keep debug information and turn off lto and strip 19 | 20 | ```toml 21 | [profile.bloaty] 22 | debug = true 23 | lto = false 24 | strip = false 25 | inherits = 'release' 26 | ``` 27 | 28 | ```bash 29 | cargo build --profile bloaty 30 | 31 | bloaty ./target/bloaty/bloaty-metafile -d sections,symbols -n 0 --csv > meta.csv 32 | 33 | bloaty-metafile meta.csv > meta.json 34 | bloaty-metafile meta.csv --no-sections > meta.json 35 | ``` 36 | 37 | ## csv format 38 | 39 | Please make sure bloaty generates a csv file in the following format. If the program is too large and the generated json exceeds 100mb, use the -n parameter to reduce the amount of data. 40 | 41 | ``` 42 | sections,symbols,vmsize,filesize 43 | .text,ossl_aes_gcm_encrypt_avx512,337642,337642 44 | .text,ossl_aes_gcm_decrypt_avx512,337638,337638 45 | ``` 46 | 47 | ## Esbuild Bundle Size Analyzer 48 | 49 | https://esbuild.github.io/analyze/ 50 | 51 | ## Generate image from json 52 | 53 | You can use [metafile-image](https://github.com/ahaoboy/metafile-image) implemented by nodejs to generate json files into image format, without manually uploading and screenshots 54 | 55 | ## Usage 56 | 57 | ### lock file 58 | 59 | Because bloaty's output does not include crate dependency information, the sizes of crates are all displayed separately. 60 | 61 | ![llrt-no-lock](https://github.com/user-attachments/assets/669c033f-72e8-49e9-b030-dffc370b6580) 62 | 63 | If a lock file can be provided, by default, the Cargo.lock file in the current directory is used. the dependency size can be correctly displayed by analyzing the crate dependencies. 64 | 65 | ![llrt-lock](https://github.com/user-attachments/assets/756bb69e-d8b5-42b2-946f-8e5439284209) 66 | 67 | ### deep 68 | 69 | For large applications, the dependency tree will be very deep, which will cause the generated JSON to be very large and contain too much useless information. You can use the --deep option to limit the maximum depth of the dependency. 70 | 71 | The default value of deep is 0(no limit) 72 | 73 | deep: 4, json: 6.7M 74 | ![llrt-deep-4](https://github.com/user-attachments/assets/2780c0ff-3a04-4aa3-946f-5c024347f1dd) 75 | 76 | deep: 8, json: 12M 77 | ![llrt-deep-8](https://github.com/user-attachments/assets/89a786ff-45e6-47b7-a931-edd59d1dff30) 78 | 79 | deep: 0, json: 80M 80 | ![llrt-deep-0](https://github.com/user-attachments/assets/b2cbf935-340e-4dbd-8ca3-191340c9ae35) 81 | 82 | 83 | ### no-sections 84 | 85 | Filter out SECTIONS that failed to count crates, and only display the recognized crate size usage. 86 | 87 | The default value of no-sections is false 88 | 89 | 90 | ## Conversion rules 91 | 92 | The symbol `.text,easy_install::install::artifact` will be converted to `easy_install/.text/install/artifact`. 93 | 94 | Additionally, if symbol ends with `.map`, to prevent the esbuild analyzer from treating it as a JavaScript sourcemap file, the suffix will be converted to `.map_`. 95 | 96 | If symbol is empty, it will be added to the `UNKNOWN` section. 97 | 98 | ## windows 99 | 100 | bloaty: PE doesn't support this data source 101 | 102 | bloaty-metafile just converts the csv output by bloaty to json. You can generate csv files on other platforms with bloaty, and then convert them on windows. 103 | -------------------------------------------------------------------------------- /src/packages.rs: -------------------------------------------------------------------------------- 1 | use crate::{tool::get_crate_name, tree::SectionRecord}; 2 | use cargo_lock::dependency::{ 3 | Tree, 4 | graph::{Graph, NodeIndex}, 5 | }; 6 | use std::collections::{HashMap, HashSet, VecDeque}; 7 | 8 | /// Package dependency resolver 9 | /// Maps crate names to their dependency paths in the dependency tree 10 | #[derive(Debug, Default, Clone)] 11 | pub struct Packages { 12 | parent: HashMap>, 13 | } 14 | 15 | /// Helper function to normalize crate names by replacing hyphens with underscores 16 | #[inline] 17 | fn normalize_crate_name(name: &str) -> String { 18 | name.replace('-', "_") 19 | } 20 | 21 | /// Node used in breadth-first search traversal of the dependency graph 22 | struct BfsNode { 23 | name: Box, 24 | path: Vec, 25 | index: NodeIndex, 26 | } 27 | 28 | impl BfsNode { 29 | /// Create a BFS node from a graph index with an optional parent path 30 | /// If parent_path is None, creates a root node; otherwise extends the path 31 | #[inline] 32 | fn from_graph(g: &Graph, index: NodeIndex, parent_path: Option>) -> Self { 33 | let name = normalize_crate_name(g[index].name.as_str()); 34 | let name_boxed: Box = name.as_str().into(); 35 | 36 | let path = match parent_path { 37 | Some(mut p) => { 38 | p.push(name); 39 | p 40 | } 41 | None => vec![name], 42 | }; 43 | 44 | Self { 45 | name: name_boxed, 46 | path, 47 | index, 48 | } 49 | } 50 | } 51 | 52 | impl Packages { 53 | /// Create a new Packages resolver from a dependency tree and section records 54 | /// Uses BFS to find the shortest path to each crate in the dependency graph 55 | pub fn new(tree: &Tree, records: &[SectionRecord]) -> Self { 56 | // Build set of crate names from records 57 | let crates: HashSet = records 58 | .iter() 59 | .filter_map(|record| get_crate_name(&record.symbols)) 60 | .map(|(name, _)| name) 61 | .collect(); 62 | 63 | let g = tree.graph(); 64 | let roots = tree.roots().to_vec(); 65 | 66 | // Pre-allocate collections with estimated capacity 67 | let estimated_nodes = g.node_count(); 68 | let mut visited = HashSet::with_capacity(estimated_nodes); 69 | let mut queue = VecDeque::with_capacity(estimated_nodes / 4); 70 | let mut parent: HashMap> = HashMap::with_capacity(crates.len()); 71 | 72 | // Initialize queue with root nodes 73 | for &start in &roots { 74 | queue.push_back(BfsNode::from_graph(g, start, None)); 75 | } 76 | 77 | // BFS traversal to find shortest paths 78 | while let Some(BfsNode { name, path, index }) = queue.pop_front() { 79 | if visited.contains(&index) { 80 | continue; 81 | } 82 | 83 | let name_str = name.as_ref(); 84 | 85 | // Insert or update path for this crate 86 | parent 87 | .entry(name_str.to_string()) 88 | .and_modify(|entry| { 89 | // Keep shorter path if crate is in records 90 | if crates.contains(name_str) && entry.len() > path.len() { 91 | *entry = path.clone(); 92 | } 93 | }) 94 | .or_insert_with(|| path.clone()); 95 | 96 | visited.insert(index); 97 | 98 | // Add unvisited neighbors to queue 99 | for neighbor in g.neighbors(index) { 100 | if !visited.contains(&neighbor) { 101 | queue.push_back(BfsNode::from_graph(g, neighbor, Some(path.clone()))); 102 | } 103 | } 104 | } 105 | 106 | // Ensure standard library crates (std, alloc) have entries 107 | for crate_name in crates { 108 | parent 109 | .entry(crate_name.clone()) 110 | .or_insert_with(|| vec![crate_name]); 111 | } 112 | 113 | Self { parent } 114 | } 115 | 116 | /// Get the dependency path for a crate by ID 117 | /// Returns a reference to avoid cloning when possible 118 | pub fn get_path(&self, id: &str) -> &[String] { 119 | self.parent.get(id).map(|v| v.as_slice()).unwrap_or(&[]) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{BloatyError, Result}, 3 | packages::Packages, 4 | tool::{ROOT_NAME, SECTIONS_NAME, UNKNOWN_NAME, get_path_from_record}, 5 | }; 6 | use cargo_lock::Lockfile; 7 | use serde::Deserialize; 8 | use serde_metafile::{Import, Input, InputDetail, Metafile, Output}; 9 | use std::collections::HashMap; 10 | 11 | /// Tree node representing a symbol or section in the binary 12 | /// Contains size information and child nodes 13 | #[derive(Debug, Clone)] 14 | pub struct Node { 15 | pub name: Box, 16 | pub vmsize: u64, 17 | pub filesize: u64, 18 | pub total_vmsize: u64, 19 | pub total_filesize: u64, 20 | pub nodes: HashMap, Node>, 21 | } 22 | 23 | impl Default for Node { 24 | fn default() -> Self { 25 | Self { 26 | name: String::new().into_boxed_str(), 27 | vmsize: 0, 28 | filesize: 0, 29 | total_vmsize: 0, 30 | total_filesize: 0, 31 | nodes: HashMap::new(), 32 | } 33 | } 34 | } 35 | 36 | /// CSV record from bloaty output 37 | /// Contains section name, symbol name, virtual memory size, and file size 38 | #[derive(Debug, Deserialize)] 39 | pub struct SectionRecord { 40 | pub sections: String, 41 | pub symbols: String, 42 | pub vmsize: u64, 43 | pub filesize: u64, 44 | } 45 | 46 | /// Hierarchical tree structure for organizing binary symbols and sections 47 | pub struct Tree { 48 | root: Node, 49 | } 50 | 51 | impl Tree { 52 | /// Create a new tree from CSV data and optional Cargo.lock file 53 | /// Parses CSV records and builds a hierarchical structure 54 | pub fn new(csv: &str, lock: Option, no_sections: bool) -> Result { 55 | let mut tree = Tree { 56 | root: Node { 57 | name: ROOT_NAME.to_string().into_boxed_str(), 58 | vmsize: 0, 59 | filesize: 0, 60 | nodes: HashMap::new(), 61 | total_filesize: 0, 62 | total_vmsize: 0, 63 | }, 64 | }; 65 | 66 | // Parse CSV records 67 | let mut rdr = csv::Reader::from_reader(csv.as_bytes()); 68 | let records: Vec<_> = rdr 69 | .deserialize::() 70 | .collect::, csv::Error>>() 71 | .map_err(BloatyError::CsvParse)?; 72 | 73 | // Load Cargo.lock and resolve package dependencies 74 | let lock_path = lock.unwrap_or_else(|| "Cargo.lock".to_string()); 75 | let packages = Lockfile::load(&lock_path) 76 | .map_err(|source| BloatyError::LockfileLoad { 77 | path: lock_path.clone(), 78 | source, 79 | }) 80 | .and_then(|lock| { 81 | lock.dependency_tree() 82 | .map_err(|source| BloatyError::LockfileLoad { 83 | path: lock_path.clone(), 84 | source, 85 | }) 86 | }) 87 | .map(|dep_tree| Packages::new(&dep_tree, &records)) 88 | .unwrap_or_default(); 89 | 90 | // Build tree from records 91 | for record in records { 92 | let sym = if record.symbols.is_empty() { 93 | UNKNOWN_NAME.to_string() 94 | } else { 95 | record.symbols 96 | }; 97 | let path = get_path_from_record(sym, record.sections, &packages); 98 | if no_sections && path[0] == SECTIONS_NAME { 99 | continue; 100 | } 101 | tree.add_path(&path, record.vmsize, record.filesize); 102 | } 103 | 104 | Ok(tree) 105 | } 106 | 107 | /// Convert the tree to an esbuild metafile format 108 | /// Traverses the tree and generates the metafile structure 109 | pub fn to_metafile(&self, name: &str, deep: usize) -> Metafile { 110 | let root = &self.root; 111 | 112 | // Pre-allocate HashMap with estimated capacity 113 | let mut inputs = HashMap::with_capacity(root.nodes.len() * 4); 114 | 115 | // Traverse all root nodes to build inputs 116 | for node in root.nodes.values() { 117 | node.traverse(&mut inputs, None, deep); 118 | } 119 | 120 | // Build output_inputs using iterator chain 121 | let output_inputs: HashMap<_, _> = inputs 122 | .iter() 123 | .map(|(path, input)| { 124 | ( 125 | path.clone(), 126 | InputDetail { 127 | bytes_in_output: input.bytes, 128 | }, 129 | ) 130 | }) 131 | .collect(); 132 | 133 | let output = Output { 134 | bytes: root.total_filesize, 135 | inputs: output_inputs, 136 | imports: vec![], 137 | exports: vec![], 138 | entry_point: None, 139 | css_bundle: None, 140 | }; 141 | 142 | let outputs = HashMap::from([(name.to_string(), output)]); 143 | Metafile { inputs, outputs } 144 | } 145 | 146 | /// Add a path to the tree with associated size information 147 | /// Creates intermediate nodes as needed 148 | fn add_path(&mut self, path: &[String], vmsize: u64, filesize: u64) { 149 | let mut current = &mut self.root; 150 | let last_idx = path.len() - 1; 151 | 152 | for (i, part) in path.iter().enumerate() { 153 | current.total_vmsize += vmsize; 154 | current.total_filesize += filesize; 155 | 156 | let is_leaf = i == last_idx; 157 | let part_boxed: Box = part.as_str().into(); 158 | 159 | // Use entry API to avoid double lookup 160 | current = current.nodes.entry(part_boxed.clone()).or_insert_with(|| { 161 | Node::create_node( 162 | part_boxed.clone(), 163 | 0, // Initialize with 0, will be accumulated below 164 | 0, 165 | is_leaf, 166 | ) 167 | }); 168 | 169 | // Accumulate leaf node values (don't overwrite) 170 | if is_leaf { 171 | current.vmsize += vmsize; 172 | current.filesize += filesize; 173 | } 174 | } 175 | } 176 | } 177 | 178 | impl Node { 179 | /// Helper function to create a new node with given parameters 180 | #[inline] 181 | fn create_node(name: Box, vmsize: u64, filesize: u64, is_leaf: bool) -> Self { 182 | Self { 183 | name, 184 | vmsize, 185 | filesize, 186 | nodes: if is_leaf { 187 | HashMap::new() 188 | } else { 189 | HashMap::with_capacity(4) // Pre-allocate for intermediate nodes 190 | }, 191 | total_filesize: 0, 192 | total_vmsize: 0, 193 | } 194 | } 195 | 196 | /// Recursively traverse the tree to build metafile inputs 197 | /// Respects the depth limit if specified 198 | fn traverse(&self, inputs: &mut HashMap, dir: Option, deep: usize) { 199 | // Build directory path with capacity pre-allocation 200 | let dir: String = match &dir { 201 | Some(parent) => { 202 | let mut path = String::with_capacity(parent.len() + 1 + self.name.len()); 203 | path.push_str(parent); 204 | path.push('/'); 205 | path.push_str(&self.name); 206 | path 207 | } 208 | None => self.name.to_string(), 209 | }; 210 | 211 | let current_depth = dir.matches('/').count(); 212 | 213 | // Check if we're at the depth limit 214 | let at_depth_limit = deep != 0 && current_depth >= deep; 215 | 216 | // Build imports (only if not at depth limit) 217 | let imports: Vec = if at_depth_limit { 218 | vec![] 219 | } else { 220 | self.nodes 221 | .values() 222 | .map(|child| { 223 | let mut import_path = String::with_capacity(dir.len() + 1 + child.name.len()); 224 | import_path.push_str(&dir); 225 | import_path.push('/'); 226 | import_path.push_str(&child.name); 227 | Import { 228 | path: import_path, 229 | kind: None, 230 | external: false, 231 | original: None, 232 | with: None, 233 | } 234 | }) 235 | .collect() 236 | }; 237 | 238 | // Use total_filesize when at depth limit to include all children's sizes 239 | let bytes = if at_depth_limit { 240 | self.total_filesize 241 | } else { 242 | self.filesize 243 | }; 244 | 245 | let input = Input { 246 | bytes, 247 | imports, 248 | format: None, 249 | with: None, 250 | }; 251 | 252 | inputs.insert(dir.clone(), input); 253 | 254 | // Recurse into children only if not at depth limit 255 | if !at_depth_limit && !self.nodes.is_empty() { 256 | let dir_ref = Some(dir); 257 | for child in self.nodes.values() { 258 | child.traverse(inputs, dir_ref.clone(), deep); 259 | } 260 | } 261 | } 262 | } 263 | 264 | #[cfg(test)] 265 | mod test { 266 | use crate::tree::Tree; 267 | 268 | #[test] 269 | fn test_get_tree() { 270 | for csv in [ 271 | r#" 272 | sections,symbols,vmsize,filesize 273 | "__TEXT,__text",[1848 Others],918108,918108 274 | "#, 275 | r#" 276 | sections,symbols,vmsize,filesize 277 | .text,[1843 Others],1086372,1086372 278 | "#, 279 | ] { 280 | let tree = Tree::new(csv, None, false).expect("Failed to create tree"); 281 | assert_eq!(tree.root.nodes.len(), 1) 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/tool.rs: -------------------------------------------------------------------------------- 1 | use crate::packages::Packages; 2 | 3 | pub const ROOT_NAME: &str = "ROOT"; 4 | pub const UNKNOWN_NAME: &str = "UNKNOWN"; 5 | pub const SECTIONS_NAME: &str = "SECTIONS"; 6 | 7 | /// Rust primitive types that should be converted to std::primitive::xxx 8 | const PRIMITIVE_TYPES: &[&str] = &[ 9 | "bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", 10 | "u64", "u128", "usize", "str", 11 | ]; 12 | 13 | /// Check if a type is a Rust primitive type 14 | #[inline] 15 | fn is_primitive_type(s: &str) -> bool { 16 | PRIMITIVE_TYPES.contains(&s) 17 | } 18 | 19 | /// Normalize a type string, converting primitives to std::primitive::xxx 20 | /// - `()` -> `std::primitive::unit` 21 | /// - `&str` -> `std::primitive::str` 22 | /// - `u8`, `i32`, etc. -> `std::primitive::xxx` 23 | /// - `[u8]` -> `std::primitive::slice` 24 | /// - `*mut T` / `*const T` -> keeps the inner type 25 | fn normalize_type(s: &str) -> String { 26 | let s = s.trim(); 27 | 28 | // Handle unit type () 29 | if s == "()" { 30 | return "std::primitive::unit".to_string(); 31 | } 32 | 33 | // Handle tuple types like (A, B) 34 | if s.starts_with('(') && s.ends_with(')') { 35 | return "std::primitive::tuple".to_string(); 36 | } 37 | 38 | // Handle slice types like [u8] 39 | if s.starts_with('[') && s.ends_with(']') { 40 | return "std::primitive::slice".to_string(); 41 | } 42 | 43 | // Handle reference types &str, &T 44 | if let Some(inner) = s.strip_prefix('&') { 45 | let inner = inner.trim(); 46 | if is_primitive_type(inner) { 47 | return format!("std::primitive::{}", inner); 48 | } 49 | return normalize_type(inner); 50 | } 51 | 52 | // Handle pointer types *mut T, *const T 53 | if let Some(inner) = s.strip_prefix("*mut ") { 54 | return normalize_type(inner.trim()); 55 | } 56 | if let Some(inner) = s.strip_prefix("*const ") { 57 | return normalize_type(inner.trim()); 58 | } 59 | 60 | // Handle primitive types 61 | if is_primitive_type(s) { 62 | return format!("std::primitive::{}", s); 63 | } 64 | 65 | s.to_string() 66 | } 67 | 68 | /// Check if a symbol string represents a valid crate name 69 | /// Returns false if the symbol contains invalid patterns or is a special marker 70 | #[inline] 71 | pub fn symbol_is_crate(s: &str) -> bool { 72 | // Reject symbols with invalid patterns: ".." or spaces 73 | // Reject special markers that start with '[' 74 | !s.contains("..") && !s.contains(' ') && !s.starts_with('[') 75 | } 76 | 77 | /// Extract crate name and symbol parts from a symbol string 78 | /// Supports both regular symbols and angle bracket symbols: 79 | /// - Regular: `crate::module::func` 80 | /// - Closure: `std::sys::backtrace::_print_fmt::{closure#1}::{closure#0}` 81 | /// - Trait impl: `<&core::alloc::layout::Layout as core::fmt::Debug>::fmt` 82 | /// - Type method: `::set_password` 83 | /// - Nested: `::to_vec_in::ConvertVec>::to_vec::<>` 84 | /// - Double angle: `<::method as OtherTrait>::func` 85 | /// Returns the crate name and all symbol parts if valid, None otherwise 86 | pub fn get_crate_name(symbols: &str) -> Option<(String, Vec)> { 87 | // Handle angle bracket symbols (trait impls, type methods) 88 | if symbols.starts_with('<') { 89 | return parse_angle_bracket_symbol(symbols); 90 | } 91 | 92 | // Handle regular symbols (including closures like {closure#0}) 93 | let parts = split_symbol_parts(symbols); 94 | 95 | if parts.is_empty() { 96 | return None; 97 | } 98 | 99 | let first = &parts[0]; 100 | if !symbol_is_crate(first) { 101 | return None; 102 | } 103 | 104 | if parts.len() > 1 { 105 | Some((parts[0].clone(), parts)) 106 | } else { 107 | None 108 | } 109 | } 110 | 111 | /// Parse angle bracket symbols like trait impls and type methods 112 | /// Extracts only the innermost type path and the outermost method name 113 | fn parse_angle_bracket_symbol(symbols: &str) -> Option<(String, Vec)> { 114 | // Extract innermost type and outermost method 115 | let (inner_type, outer_method) = extract_inner_type_and_outer_method(symbols)?; 116 | 117 | let mut parts = Vec::with_capacity(4); 118 | 119 | // Normalize and add type path parts 120 | let normalized_type = normalize_type(&inner_type); 121 | for part in split_symbol_parts(&normalized_type) { 122 | if !part.is_empty() && part != "<>" { 123 | parts.push(part); 124 | } 125 | } 126 | 127 | // Add outer method 128 | if !outer_method.is_empty() { 129 | for part in split_symbol_parts(&outer_method) { 130 | if !part.is_empty() && part != "<>" { 131 | parts.push(part); 132 | } 133 | } 134 | } 135 | 136 | if parts.len() > 1 { 137 | let crate_name = parts[0].clone(); 138 | if symbol_is_crate(&crate_name) { 139 | return Some((crate_name, parts)); 140 | } 141 | } 142 | None 143 | } 144 | 145 | /// Extract the innermost type and outermost method from nested angle bracket expression 146 | /// For `<::method1 as Trait2>::method2` returns ("u64", "method2") 147 | fn extract_inner_type_and_outer_method(s: &str) -> Option<(String, String)> { 148 | let s = s.trim(); 149 | 150 | if !s.starts_with('<') { 151 | return None; 152 | } 153 | 154 | // Find matching '>' for the outermost angle bracket 155 | let mut depth = 0; 156 | let mut close_pos = None; 157 | for (i, c) in s.char_indices() { 158 | match c { 159 | '<' => depth += 1, 160 | '>' => { 161 | depth -= 1; 162 | if depth == 0 { 163 | close_pos = Some(i); 164 | break; 165 | } 166 | } 167 | _ => {} 168 | } 169 | } 170 | 171 | let close_pos = close_pos?; 172 | 173 | // Get outer method (after `>::`) 174 | let outer_method = s[close_pos..].strip_prefix(">::").unwrap_or("").to_string(); 175 | 176 | // Get inner content 177 | let inner = &s[1..close_pos]; 178 | 179 | // Find type part (before " as " at depth 0) 180 | let type_part = find_type_part(inner); 181 | 182 | // If type_part starts with '<', recursively extract innermost type 183 | if type_part.starts_with('<') { 184 | let (inner_type, _) = extract_inner_type_and_outer_method(type_part)?; 185 | Some((inner_type, outer_method)) 186 | } else { 187 | Some((type_part.to_string(), outer_method)) 188 | } 189 | } 190 | 191 | /// Clean a symbol part to make it a valid identifier 192 | /// Removes trailing `<>`, `()`, and other invalid characters 193 | fn clean_symbol_part(s: &str) -> String { 194 | let mut result = s.to_string(); 195 | 196 | // Remove trailing () and <> 197 | while result.ends_with("()") || result.ends_with("<>") { 198 | if result.ends_with("()") { 199 | result.truncate(result.len() - 2); 200 | } 201 | if result.ends_with("<>") { 202 | result.truncate(result.len() - 2); 203 | } 204 | } 205 | 206 | result 207 | } 208 | 209 | /// Split symbol string into parts, handling special syntax like {closure#0}, {shim:vtable#0}, ::<> 210 | fn split_symbol_parts(s: &str) -> Vec { 211 | let mut parts = Vec::new(); 212 | let mut current = String::new(); 213 | let mut chars = s.chars().peekable(); 214 | let mut brace_depth = 0; 215 | let mut angle_depth = 0; 216 | 217 | while let Some(c) = chars.next() { 218 | match c { 219 | '{' => { 220 | brace_depth += 1; 221 | current.push(c); 222 | } 223 | '}' => { 224 | brace_depth -= 1; 225 | current.push(c); 226 | } 227 | '<' => { 228 | angle_depth += 1; 229 | current.push(c); 230 | } 231 | '>' => { 232 | angle_depth -= 1; 233 | current.push(c); 234 | } 235 | ':' if brace_depth == 0 && angle_depth == 0 => { 236 | // Check for `::` 237 | if chars.peek() == Some(&':') { 238 | chars.next(); // consume second ':' 239 | if !current.is_empty() { 240 | parts.push(current); 241 | current = String::new(); 242 | } 243 | } else { 244 | current.push(c); 245 | } 246 | } 247 | _ => current.push(c), 248 | } 249 | } 250 | 251 | if !current.is_empty() { 252 | parts.push(current); 253 | } 254 | 255 | // Clean and filter parts 256 | parts 257 | .into_iter() 258 | .map(|p| clean_symbol_part(&p)) 259 | .filter(|p| !p.is_empty() && p != "<>") 260 | .collect() 261 | } 262 | 263 | /// Find the type part in an angle bracket expression 264 | /// Handles nested angle brackets like `::to_vec_in::ConvertVec>` 265 | fn find_type_part(inner: &str) -> &str { 266 | // Find " as " that is not inside nested angle brackets 267 | let mut depth = 0; 268 | let bytes = inner.as_bytes(); 269 | let as_pattern = b" as "; 270 | 271 | for i in 0..inner.len() { 272 | match bytes[i] { 273 | b'<' => depth += 1, 274 | b'>' => depth -= 1, 275 | b' ' if depth == 0 && i + 4 <= inner.len() && &bytes[i..i + 4] == as_pattern => { 276 | return &inner[..i]; 277 | } 278 | _ => {} 279 | } 280 | } 281 | inner 282 | } 283 | 284 | /// Build a hierarchical path from a symbol record 285 | /// Combines package dependencies, sections, and symbol parts into a single path 286 | pub fn get_path_from_record(symbols: String, sections: String, packages: &Packages) -> Vec { 287 | match get_crate_name(&symbols) { 288 | None => { 289 | // No crate found: build path from sections 290 | // Pre-allocate capacity for sections + symbol parts 291 | let symbol_parts_count = symbols.matches("::").count() + 1; 292 | let mut path = Vec::with_capacity(2 + symbol_parts_count); 293 | path.push(SECTIONS_NAME.to_string()); 294 | path.push(sections); 295 | path.extend(symbols.split("::").map(String::from)); 296 | path 297 | } 298 | Some((crate_name, symbols_parts)) => { 299 | // Build path: crate dependency path + section + symbol parts 300 | // Example: .text,llrt_utils::clone::structured_clone -> llrt/llrt_utils/.text/clone/structured_clone 301 | let pkg_path = packages.get_path(&crate_name); 302 | let mut path = Vec::with_capacity(pkg_path.len() + 1 + symbols_parts.len() - 1); 303 | path.extend_from_slice(pkg_path); 304 | path.push(sections); 305 | path.extend_from_slice(&symbols_parts[1..]); 306 | path 307 | } 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod test { 313 | use super::{get_crate_name, symbol_is_crate}; 314 | 315 | #[test] 316 | fn test_symbol_is_crate() { 317 | let test_cases = [ 318 | ("[16482 Others]", false), 319 | ( 320 | "_$LT$alloc..string..String$u20$as$u20$core..fmt..Write$GT$", 321 | false, 322 | ), 323 | ("valid_crate", true), 324 | ("another::valid::crate", true), 325 | ("invalid crate", false), 326 | ("..invalid", false), 327 | ]; 328 | 329 | for (symbol, expected) in test_cases.iter() { 330 | assert_eq!( 331 | symbol_is_crate(symbol), 332 | *expected, 333 | "Failed for symbol: {}", 334 | symbol 335 | ); 336 | } 337 | } 338 | 339 | #[test] 340 | fn test_get_crate_name_angle_bracket() { 341 | // Test trait impl: <&core::alloc::layout::Layout as core::fmt::Debug>::fmt 342 | let result = get_crate_name("<&core::alloc::layout::Layout as core::fmt::Debug>::fmt"); 343 | assert!(result.is_some()); 344 | let (crate_name, parts) = result.unwrap(); 345 | assert_eq!(crate_name, "core"); 346 | assert_eq!(parts, vec!["core", "alloc", "layout", "Layout", "fmt"]); 347 | 348 | // Test type method: ::set_password 349 | let result = get_crate_name("::set_password"); 350 | assert!(result.is_some()); 351 | let (crate_name, parts) = result.unwrap(); 352 | assert_eq!(crate_name, "url"); 353 | assert_eq!(parts, vec!["url", "Url", "set_password"]); 354 | 355 | // Test: ::fmt 356 | let result = get_crate_name("::fmt"); 357 | assert!(result.is_some()); 358 | let (crate_name, parts) = result.unwrap(); 359 | assert_eq!(crate_name, "core"); 360 | assert_eq!(parts, vec!["core", "alloc", "layout", "LayoutError", "fmt"]); 361 | 362 | // Test regular symbol still works 363 | let result = get_crate_name("llrt_utils::clone::structured_clone"); 364 | assert!(result.is_some()); 365 | let (crate_name, parts) = result.unwrap(); 366 | assert_eq!(crate_name, "llrt_utils"); 367 | assert_eq!(parts, vec!["llrt_utils", "clone", "structured_clone"]); 368 | 369 | // Test nested angle brackets: ::to_vec_in::ConvertVec>::to_vec::<> 370 | let result = get_crate_name("::to_vec_in::ConvertVec>::to_vec::<>"); 371 | assert!(result.is_some()); 372 | let (crate_name, parts) = result.unwrap(); 373 | assert_eq!(crate_name, "std"); 374 | assert_eq!(parts, vec!["std", "primitive", "u8", "to_vec"]); 375 | 376 | // Test closure syntax 377 | let result = get_crate_name("std::sys::backtrace::_print_fmt::{closure#1}::{closure#0}"); 378 | assert!(result.is_some()); 379 | let (crate_name, parts) = result.unwrap(); 380 | assert_eq!(crate_name, "std"); 381 | assert_eq!( 382 | parts, 383 | vec![ 384 | "std", 385 | "sys", 386 | "backtrace", 387 | "_print_fmt", 388 | "{closure#1}", 389 | "{closure#0}" 390 | ] 391 | ); 392 | 393 | // Test shim syntax 394 | let result = get_crate_name( 395 | "::{closure#0} as core::ops::function::FnOnce::<>>::call_once::{shim:vtable#0}", 396 | ); 397 | assert!(result.is_some()); 398 | let (crate_name, _parts) = result.unwrap(); 399 | assert_eq!(crate_name, "signal_hook_registry"); 400 | 401 | // Test double angle bracket with multiple "as" - should only keep innermost type + outermost method 402 | // Note: u64 is now normalized to std::primitive::u64 403 | let result = get_crate_name( 404 | "<::deserialize::PrimitiveVisitor as serde_core::de::Visitor>::expecting", 405 | ); 406 | assert!(result.is_some()); 407 | let (crate_name, parts) = result.unwrap(); 408 | assert_eq!(crate_name, "std"); 409 | assert_eq!( 410 | parts, 411 | vec!["std", "primitive", "u64", "expecting"], 412 | "parts: {:?}", 413 | parts 414 | ); 415 | 416 | // Test another complex case with multiple as 417 | let result = get_crate_name( 418 | "<::deserialize::__FieldVisitor as serde_core::de::Visitor>::expecting", 419 | ); 420 | assert!(result.is_some()); 421 | let (crate_name, parts) = result.unwrap(); 422 | assert_eq!(crate_name, "easy_install"); 423 | assert_eq!( 424 | parts, 425 | vec!["easy_install", "manfiest", "AssetKind", "expecting"], 426 | "parts: {:?}", 427 | parts 428 | ); 429 | 430 | // Test unit type () 431 | let result = get_crate_name("<() as rquickjs_core::value::convert::IntoJs>::into_js"); 432 | assert!(result.is_some()); 433 | let (crate_name, parts) = result.unwrap(); 434 | assert_eq!(crate_name, "std"); 435 | assert_eq!( 436 | parts, 437 | vec!["std", "primitive", "unit", "into_js"], 438 | "parts: {:?}", 439 | parts 440 | ); 441 | 442 | // Test &str type 443 | let result = get_crate_name("<&str as rquickjs_core::value::convert::IntoJs>::into_js"); 444 | assert!(result.is_some()); 445 | let (crate_name, parts) = result.unwrap(); 446 | assert_eq!(crate_name, "std"); 447 | assert_eq!( 448 | parts, 449 | vec!["std", "primitive", "str", "into_js"], 450 | "parts: {:?}", 451 | parts 452 | ); 453 | 454 | // Test *mut pointer type 455 | let result = get_crate_name("<*mut core::ffi::c_void as core::fmt::Debug>::fmt"); 456 | assert!(result.is_some()); 457 | let (crate_name, parts) = result.unwrap(); 458 | assert_eq!(crate_name, "core"); 459 | assert_eq!(parts, vec!["core", "ffi", "c_void", "fmt"], "parts: {:?}", parts); 460 | 461 | // Test slice type [u8] 462 | let result = get_crate_name("<[u8] as core::fmt::Debug>::fmt"); 463 | assert!(result.is_some()); 464 | let (crate_name, parts) = result.unwrap(); 465 | assert_eq!(crate_name, "std"); 466 | assert_eq!( 467 | parts, 468 | vec!["std", "primitive", "slice", "fmt"], 469 | "parts: {:?}", 470 | parts 471 | ); 472 | 473 | // Test tuple type 474 | let result = get_crate_name("<(swc_common::syntax_pos::Span, swc_ecma_parser::error::SyntaxError) as core::clone::Clone>::clone"); 475 | assert!(result.is_some()); 476 | let (crate_name, parts) = result.unwrap(); 477 | assert_eq!(crate_name, "std"); 478 | assert_eq!( 479 | parts, 480 | vec!["std", "primitive", "tuple", "clone"], 481 | "parts: {:?}", 482 | parts 483 | ); 484 | 485 | // Test C++ style symbols - <> and () should be removed 486 | let result = get_crate_name("snmalloc::FreeListMPSCQ<>::destroy_and_iterate<>()"); 487 | assert!(result.is_some()); 488 | let (crate_name, parts) = result.unwrap(); 489 | assert_eq!(crate_name, "snmalloc"); 490 | assert_eq!( 491 | parts, 492 | vec!["snmalloc", "FreeListMPSCQ", "destroy_and_iterate"], 493 | "parts: {:?}", 494 | parts 495 | ); 496 | 497 | // Test C++ style with template 498 | let result = get_crate_name("snmalloc::LocalAllocator<>::init()"); 499 | assert!(result.is_some()); 500 | let (crate_name, parts) = result.unwrap(); 501 | assert_eq!(crate_name, "snmalloc"); 502 | assert_eq!( 503 | parts, 504 | vec!["snmalloc", "LocalAllocator", "init"], 505 | "parts: {:?}", 506 | parts 507 | ); 508 | 509 | // Test C++ style static member 510 | let result = get_crate_name("snmalloc::StandardConfigClientMeta<>::initialisation_lock"); 511 | assert!(result.is_some()); 512 | let (crate_name, parts) = result.unwrap(); 513 | assert_eq!(crate_name, "snmalloc"); 514 | assert_eq!( 515 | parts, 516 | vec!["snmalloc", "StandardConfigClientMeta", "initialisation_lock"], 517 | "parts: {:?}", 518 | parts 519 | ); 520 | } 521 | } 522 | --------------------------------------------------------------------------------