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