• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 //! Helper library to handle the contents of /proc/allocinfo
18 
19 use std::collections::HashMap;
20 use std::fs::File;
21 use std::io::{self, BufRead, BufReader};
22 
23 /// `PROC_ALLOCINFO` is a constant string that represents the default path to the allocinfo file.
24 ///
25 /// This file is expected to contain allocation information in a specific format, which is parsed by the `parse_allocinfo` function.
26 pub const PROC_ALLOCINFO: &str = "/proc/allocinfo";
27 
28 /// `AllocInfo` represents a single allocation record.
29 #[derive(Debug, PartialEq, Eq)]
30 pub struct AllocInfo {
31     /// The total size of all allocations in bytes (unsigned 64-bit integer).
32     pub size: u64,
33     /// The total number of all allocation calls (unsigned 64-bit integer).
34     pub calls: u64,
35     /// A string representing the tag or label associated with this allocation (source code path and line, and function name).
36     pub tag: String,
37 }
38 
39 /// `AllocGlobal` represents aggregated global allocation statistics.
40 pub struct AllocGlobal {
41     /// The total size of all allocations in bytes (unsigned 64-bit integer).
42     pub size: u64,
43     /// The total number of all allocation calls (unsigned 64-bit integer).
44     pub calls: u64,
45 }
46 
47 /// `SortBy` is an enumeration representing the different criteria by which allocation data can be sorted.
48 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
49 pub enum SortBy {
50     /// Sort by the size of the allocation.
51     Size,
52     /// Sort by the number of allocation calls.
53     Calls,
54     /// Sort by the allocation tag.
55     Tag,
56 }
57 
58 /// `parse_allocinfo` parses allocation information from a file.
59 ///
60 /// This function reads and parses an allocinfo file, returning a vector of `AllocInfo` structs.
61 /// It expects each line of the file (after the first header line) to contain at least three whitespace-separated fields:
62 /// size, calls, and tag.
63 ///
64 /// # Arguments
65 ///
66 /// * `filename`: The path to the allocinfo file.
67 ///
68 /// # Returns
69 ///
70 /// A `Result` containing either a vector of `AllocInfo` structs or an `io::Error` if an error occurred during file reading or parsing.
parse_allocinfo(filename: &str) -> io::Result<Vec<AllocInfo>>71 pub fn parse_allocinfo(filename: &str) -> io::Result<Vec<AllocInfo>> {
72     let file = File::open(filename)?;
73     let reader = BufReader::new(file);
74     let mut alloc_info_list = Vec::new();
75 
76     for (index, line) in reader.lines().enumerate() {
77         // Skip the first line (also the second line can be skipped, but it's handled as a comment)
78         if index < 1 {
79             continue;
80         }
81 
82         let line = line?;
83         let fields: Vec<&str> = line.split_whitespace().collect();
84 
85         if fields.len() >= 3 && fields[0] != "#" {
86             let size = fields[0].parse::<u64>().unwrap_or(0);
87             let calls = fields[1].parse::<u64>().unwrap_or(0);
88             let tag = fields[2..].join(" ");
89 
90             // One possible implementation would be to check for the minimum size here, but skipping
91             // lines at parsing time won't give correct results when the data is aggregated (e.g., for
92             // tree view).
93             alloc_info_list.push(AllocInfo { size, calls, tag });
94         }
95     }
96 
97     Ok(alloc_info_list)
98 }
99 
100 /// `sort_allocinfo` sorts a slice of `AllocInfo` structs based on the specified criteria.
101 ///
102 /// # Arguments
103 ///
104 /// * `data`: A mutable slice of `AllocInfo` structs to be sorted.
105 /// * `sort_by`: The criteria by which to sort the data, as defined by the `SortBy` enum.
sort_allocinfo(data: &mut [AllocInfo], sort_by: SortBy)106 pub fn sort_allocinfo(data: &mut [AllocInfo], sort_by: SortBy) {
107     match sort_by {
108         SortBy::Size => data.sort_by(|a, b| b.size.cmp(&a.size)),
109         SortBy::Calls => data.sort_by(|a, b| b.calls.cmp(&a.calls)),
110         SortBy::Tag => data.sort_by(|a, b| a.tag.cmp(&b.tag)),
111     }
112 }
113 
114 /// `aggregate_tree` aggregates allocation data into a tree structure based on hierarchical tags.
115 ///
116 /// This function takes a slice of `AllocInfo` and aggregates the size and calls for each unique tag prefix,
117 /// creating a hierarchical representation of the allocation data.
118 ///
119 /// # Arguments
120 ///
121 /// * `data`: A slice of `AllocInfo` structs to be aggregated.
122 ///
123 /// # Returns
124 ///
125 /// A `HashMap` where keys are tag prefixes (representing nodes in the tree) and values are tuples containing
126 /// the aggregated size and calls for that tag prefix.
aggregate_tree(data: &[AllocInfo]) -> HashMap<String, (u64, u64)>127 pub fn aggregate_tree(data: &[AllocInfo]) -> HashMap<String, (u64, u64)> {
128     let mut aggregated_data: HashMap<String, (u64, u64)> = HashMap::new();
129 
130     for info in data {
131         let parts: Vec<&str> = info.tag.split('/').collect();
132         for i in 0..parts.len() {
133             let tag_prefix = parts[..=i].join("/");
134             let entry = aggregated_data.entry(tag_prefix).or_insert((0, 0));
135             entry.0 += info.size;
136             entry.1 += info.calls;
137         }
138     }
139 
140     aggregated_data
141 }
142 
143 /// `print_tree_data` prints the aggregated tree data, filtering by a minimum size.
144 ///
145 /// This function prints the aggregated allocation data in a tree-like format, sorted by tag.
146 /// It only prints entries where the aggregated size is greater than or equal to `min_size`.
147 ///
148 /// # Arguments
149 ///
150 /// * `data`: A reference to a `HashMap` containing the aggregated tree data, as produced by `aggregate_tree`.
151 /// * `min_size`: The minimum aggregated size (in bytes) for an entry to be printed.
print_tree_data(data: &HashMap<String, (u64, u64)>, min_size: u64)152 pub fn print_tree_data(data: &HashMap<String, (u64, u64)>, min_size: u64) {
153     let mut sorted_data: Vec<_> = data.iter().collect();
154     sorted_data.sort_by(|a, b| a.0.cmp(b.0));
155 
156     println!("{:>10} {:>10} Tag", "Size", "Calls");
157     for (tag, (size, calls)) in sorted_data {
158         if *size < min_size {
159             continue;
160         }
161         println!("{:>10} {:>10} {}", size, calls, tag);
162     }
163 }
164 
165 /// `aggregate_global` aggregates allocation data to calculate global statistics.
166 ///
167 /// This function computes the total size and total number of calls across all allocations.
168 ///
169 /// # Arguments
170 ///
171 /// * `data`: A slice of `AllocInfo` structs to be aggregated.
172 ///
173 /// # Returns
174 ///
175 /// An `AllocGlobal` struct containing the total size and total number of calls.
aggregate_global(data: &[AllocInfo]) -> AllocGlobal176 pub fn aggregate_global(data: &[AllocInfo]) -> AllocGlobal {
177     let mut globals = AllocGlobal { size: 0, calls: 0 };
178 
179     for info in data {
180         globals.size += info.size;
181         globals.calls += info.calls;
182     }
183 
184     globals
185 }
186 
187 /// `print_aggregated_global_data` prints the aggregated global allocation statistics.
188 ///
189 /// This function prints the total size and total number of allocation calls.
190 ///
191 /// # Arguments
192 ///
193 /// * `data`: A reference to an `AllocGlobal` struct containing the aggregated data.
print_aggregated_global_data(data: &AllocGlobal)194 pub fn print_aggregated_global_data(data: &AllocGlobal) {
195     println!("{:>11} : {}", "Total Size", data.size);
196     println!("{:>11} : {}\n", "Total Calls", data.calls);
197 }
198 
199 /// `run` is the main entry point for the allocation analysis logic.
200 ///
201 /// This function orchestrates the process of reading, parsing, aggregating, and displaying allocation information.
202 /// It handles both flat and tree-based aggregation and display, based on the provided options.
203 ///
204 /// # Arguments
205 ///
206 /// * `max_lines`: The maximum number of lines to print in the flat view.
207 /// * `sort_by`: An optional `SortBy` enum value indicating how to sort the data in the flat view.
208 /// * `min_size`: The minimum size for an allocation to be included in the output (flat view) or printed (tree view).
209 /// * `use_tree`: A boolean flag indicating whether to use tree-based aggregation and display.
210 /// * `filename`: The path to the allocinfo file.
211 ///
212 /// # Returns
213 ///
214 /// A `Result` indicating success (`Ok(())`) or an error message (`Err(String)`) if an error occurred.
run( max_lines: usize, sort_by: Option<SortBy>, min_size: u64, use_tree: bool, filename: &str, ) -> Result<(), String>215 pub fn run(
216     max_lines: usize,
217     sort_by: Option<SortBy>,
218     min_size: u64,
219     use_tree: bool,
220     filename: &str,
221 ) -> Result<(), String> {
222     match parse_allocinfo(filename) {
223         Ok(mut data) => {
224             {
225                 let aggregated_data = aggregate_global(&data);
226                 print_aggregated_global_data(&aggregated_data);
227             }
228 
229             if use_tree {
230                 let tree_data = aggregate_tree(&data);
231                 print_tree_data(&tree_data, min_size);
232             } else {
233                 data.retain(|alloc_info| alloc_info.size >= min_size);
234 
235                 if let Some(sort_by) = sort_by {
236                     sort_allocinfo(&mut data, sort_by);
237                 }
238 
239                 let printable_lines = if max_lines <= data.len() { max_lines } else { data.len() };
240                 println!("{:>10} {:>10} Tag", "Size", "Calls");
241                 for info in &data[0..printable_lines] {
242                     println!("{:>10} {:>10} {}", info.size, info.calls, info.tag);
243                 }
244             }
245             Ok(())
246         }
247         Err(e) => Err(format!("Error reading or parsing allocinfo: {}", e)),
248     }
249 }
250