attribution_processing/
summary.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{PrincipalType, ResourceReference};
6use core::default::Default;
7use std::cell::RefCell;
8use std::collections::{HashMap, HashSet};
9
10use fidl_fuchsia_memory_attribution_plugin as fplugin;
11use fplugin::Vmo;
12
13use crate::{InflatedPrincipal, InflatedResource, PrincipalIdentifier};
14
15/// Consider that two floats are equals if they differ less than [FLOAT_COMPARISON_EPSILON].
16const FLOAT_COMPARISON_EPSILON: f64 = 1e-10;
17
18/// Summary view of the memory usage on a device.
19///
20/// This view aggregates the memory usage for each Principal, and, for each Principal, for VMOs
21/// sharing the same name or belonging to the same logical group. This is a view appropriate to
22/// display to developers who want to understand the memory usage of their Principal.
23pub struct MemorySummary {
24    pub principals: Vec<PrincipalSummary>,
25    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
26    pub undigested: u64,
27}
28
29impl MemorySummary {
30    pub(crate) fn build(
31        principals: &HashMap<PrincipalIdentifier, RefCell<InflatedPrincipal>>,
32        resources: &HashMap<u64, RefCell<InflatedResource>>,
33        resource_names: &Vec<String>,
34    ) -> MemorySummary {
35        let mut output = MemorySummary { principals: Default::default(), undigested: 0 };
36        for principal in principals.values() {
37            output.principals.push(MemorySummary::build_one_principal(
38                &principal,
39                &principals,
40                &resources,
41                &resource_names,
42            ));
43        }
44
45        output.principals.sort_unstable_by_key(|p| -(p.populated_total as i64));
46
47        let mut undigested = 0;
48        for (_, resource_ref) in resources {
49            let resource = &resource_ref.borrow();
50            if resource.claims.is_empty() {
51                match &resource.resource.resource_type {
52                    fplugin::ResourceType::Job(_) | fplugin::ResourceType::Process(_) => {}
53                    fplugin::ResourceType::Vmo(vmo) => {
54                        undigested += vmo.scaled_populated_bytes.unwrap();
55                    }
56                    _ => todo!(),
57                }
58            }
59        }
60        output.undigested = undigested;
61        output
62    }
63
64    fn build_one_principal(
65        principal_cell: &RefCell<InflatedPrincipal>,
66        principals: &HashMap<PrincipalIdentifier, RefCell<InflatedPrincipal>>,
67        resources: &HashMap<u64, RefCell<InflatedResource>>,
68        resource_names: &Vec<String>,
69    ) -> PrincipalSummary {
70        let principal = principal_cell.borrow();
71        let mut output = PrincipalSummary {
72            name: principal.name().to_owned(),
73            id: principal.principal.identifier.0,
74            principal_type: match &principal.principal.principal_type {
75                PrincipalType::Runnable => "R",
76                PrincipalType::Part => "P",
77            }
78            .to_owned(),
79            committed_private: 0,
80            committed_scaled: 0.0,
81            committed_total: 0,
82            populated_private: 0,
83            populated_scaled: 0.0,
84            populated_total: 0,
85            attributor: principal
86                .principal
87                .parent
88                .as_ref()
89                .map(|p| principals.get(p))
90                .flatten()
91                .map(|p| p.borrow().name().to_owned()),
92            processes: Vec::new(),
93            vmos: HashMap::new(),
94        };
95
96        for resource_id in &principal.resources {
97            if !resources.contains_key(resource_id) {
98                continue;
99            }
100
101            let resource = resources.get(resource_id).unwrap().borrow();
102            let share_count = resource
103                .claims
104                .iter()
105                .map(|c| c.subject)
106                .collect::<HashSet<PrincipalIdentifier>>()
107                .len();
108            match &resource.resource.resource_type {
109                fplugin::ResourceType::Job(_) => todo!(),
110                fplugin::ResourceType::Process(_) => {
111                    output.processes.push(format!(
112                        "{} ({})",
113                        resource_names.get(resource.resource.name_index).unwrap().clone(),
114                        resource.resource.koid
115                    ));
116                }
117                fplugin::ResourceType::Vmo(vmo_info) => {
118                    output.committed_total += vmo_info.total_committed_bytes.unwrap();
119                    output.populated_total += vmo_info.total_populated_bytes.unwrap();
120                    output.committed_scaled +=
121                        vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
122                    output.populated_scaled +=
123                        vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
124                    if share_count == 1 {
125                        output.committed_private += vmo_info.private_committed_bytes.unwrap();
126                        output.populated_private += vmo_info.private_populated_bytes.unwrap();
127                    }
128                    output
129                        .vmos
130                        .entry(
131                            vmo_name_to_digest_name(
132                                &resource_names.get(resource.resource.name_index).unwrap(),
133                            )
134                            .to_owned(),
135                        )
136                        .or_default()
137                        .merge(vmo_info, share_count);
138                }
139                _ => todo!(),
140            }
141        }
142
143        for (_source, attribution) in &principal.attribution_claims {
144            for resource in &attribution.resources {
145                if let ResourceReference::ProcessMapped {
146                    process: process_mapped,
147                    base: _,
148                    len: _,
149                } = resource
150                {
151                    if let Some(process_ref) = resources.get(&process_mapped) {
152                        let process = process_ref.borrow();
153                        output.processes.push(format!(
154                            "{} ({})",
155                            resource_names.get(process.resource.name_index).unwrap().clone(),
156                            process.resource.koid
157                        ));
158                    }
159                }
160            }
161        }
162
163        output.processes.sort();
164        output
165    }
166}
167
168/// Summary of a Principal memory usage, and its breakdown per VMO group.
169#[derive(Debug)]
170pub struct PrincipalSummary {
171    /// Identifier for the Principal. This number is not meaningful outside of the memory
172    /// attribution system.
173    pub id: u64,
174    /// Display name of the Principal.
175    pub name: String,
176    /// Type of the Principal.
177    pub principal_type: String,
178    /// Number of committed private bytes of the Principal.
179    pub committed_private: u64,
180    /// Number of committed bytes of all VMOs accessible to the Principal, scaled by the number of
181    /// Principals that can access them.
182    pub committed_scaled: f64,
183    /// Total number of committed bytes of all the VMOs accessible to the Principal.
184    pub committed_total: u64,
185    /// Number of populated private bytes of the Principal.
186    pub populated_private: u64,
187    /// Number of populated bytes of all VMOs accessible to the Principal, scaled by the number of
188    /// Principals that can access them.
189    pub populated_scaled: f64,
190    /// Total number of populated bytes of all the VMOs accessible to the Principal.
191    pub populated_total: u64,
192
193    /// Name of the Principal who gave attribution information for this Principal.
194    pub attributor: Option<String>,
195    /// List of Zircon processes attributed (even partially) to this Principal.
196    pub processes: Vec<String>,
197    /// Summary of memory usage for the VMOs accessible to this Principal, grouped by VMO name.
198    pub vmos: HashMap<String, VmoSummary>,
199}
200
201impl PartialEq for PrincipalSummary {
202    fn eq(&self, other: &Self) -> bool {
203        self.id == other.id
204            && self.name == other.name
205            && self.principal_type == other.principal_type
206            && self.committed_private == other.committed_private
207            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
208            && self.committed_total == other.committed_total
209            && self.populated_private == other.populated_private
210            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
211            && self.populated_total == other.populated_total
212            && self.attributor == other.attributor
213            && self.processes == other.processes
214            && self.vmos == other.vmos
215    }
216}
217
218/// Group of VMOs sharing the same name.
219#[derive(Default, Debug)]
220pub struct VmoSummary {
221    /// Number of distinct VMOs under the same name.
222    pub count: u64,
223    /// Number of committed bytes of this VMO group only accessible by the Principal this group
224    /// belongs.
225    pub committed_private: u64,
226    /// Number of committed bytes of this VMO group, scaled by the number of Principals that can
227    /// access them.
228    pub committed_scaled: f64,
229    /// Total number of committed bytes of this VMO group.
230    pub committed_total: u64,
231    /// Number of populated bytes of this VMO group only accessible by the Principal this group
232    /// belongs.
233    pub populated_private: u64,
234    /// Number of populated bytes of this VMO group, scaled by the number of Principals that can
235    /// access them.
236    pub populated_scaled: f64,
237    /// Total number of populated bytes of this VMO group.
238    pub populated_total: u64,
239}
240
241impl VmoSummary {
242    fn merge(&mut self, vmo_info: &Vmo, share_count: usize) {
243        self.count += 1;
244        self.committed_total += vmo_info.total_committed_bytes.unwrap();
245        self.populated_total += vmo_info.total_populated_bytes.unwrap();
246        self.committed_scaled +=
247            vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
248        self.populated_scaled +=
249            vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
250        if share_count == 1 {
251            self.committed_private += vmo_info.private_committed_bytes.unwrap();
252            self.populated_private += vmo_info.private_populated_bytes.unwrap();
253        }
254    }
255}
256
257impl PartialEq for VmoSummary {
258    fn eq(&self, other: &Self) -> bool {
259        self.count == other.count
260            && self.committed_private == other.committed_private
261            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
262            && self.committed_total == other.committed_total
263            && self.populated_private == other.populated_private
264            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
265            && self.populated_total == other.populated_total
266    }
267}
268
269/// Returns the name of a VMO category when the name match on of the rules.
270/// This is used for presentation and aggregation.
271pub fn vmo_name_to_digest_name(name: &str) -> &str {
272    /// Default, global regex match.
273    static RULES: std::sync::LazyLock<[(regex::Regex, &'static str); 13]> =
274        std::sync::LazyLock::new(|| {
275            [
276                (
277                    regex::Regex::new("ld\\.so\\.1-internal-heap|(^stack: msg of.*)").unwrap(),
278                    "[process-bootstrap]",
279                ),
280                (regex::Regex::new("^blob-[0-9a-f]+$").unwrap(), "[blobs]"),
281                (regex::Regex::new("^inactive-blob-[0-9a-f]+$").unwrap(), "[inactive blobs]"),
282                (
283                    regex::Regex::new("^thrd_t:0x.*|initial-thread|pthread_t:0x.*$").unwrap(),
284                    "[stacks]",
285                ),
286                (regex::Regex::new("^data[0-9]*:.*$").unwrap(), "[data]"),
287                (regex::Regex::new("^bss[0-9]*:.*$").unwrap(), "[bss]"),
288                (regex::Regex::new("^relro:.*$").unwrap(), "[relro]"),
289                (regex::Regex::new("^$").unwrap(), "[unnamed]"),
290                (regex::Regex::new("^scudo:.*$").unwrap(), "[scudo]"),
291                (regex::Regex::new("^.*\\.so.*$").unwrap(), "[bootfs-libraries]"),
292                (regex::Regex::new("^stack_and_tls:.*$").unwrap(), "[bionic-stack]"),
293                (regex::Regex::new("^ext4!.*$").unwrap(), "[ext4]"),
294                (regex::Regex::new("^dalvik-.*$").unwrap(), "[dalvik]"),
295            ]
296        });
297    RULES.iter().find(|(regex, _)| regex.is_match(name)).map_or(name, |rule| rule.1)
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn rename_test() {
306        pretty_assertions::assert_eq!(
307            vmo_name_to_digest_name("ld.so.1-internal-heap"),
308            "[process-bootstrap]"
309        );
310        pretty_assertions::assert_eq!(
311            vmo_name_to_digest_name("stack: msg of 123"),
312            "[process-bootstrap]"
313        );
314        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-123"), "[blobs]");
315        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-15e0da8e"), "[blobs]");
316        pretty_assertions::assert_eq!(
317            vmo_name_to_digest_name("inactive-blob-123"),
318            "[inactive blobs]"
319        );
320        pretty_assertions::assert_eq!(vmo_name_to_digest_name("thrd_t:0x123"), "[stacks]");
321        pretty_assertions::assert_eq!(vmo_name_to_digest_name("initial-thread"), "[stacks]");
322        pretty_assertions::assert_eq!(vmo_name_to_digest_name("pthread_t:0x123"), "[stacks]");
323        pretty_assertions::assert_eq!(vmo_name_to_digest_name("data456:"), "[data]");
324        pretty_assertions::assert_eq!(vmo_name_to_digest_name("bss456:"), "[bss]");
325        pretty_assertions::assert_eq!(vmo_name_to_digest_name("relro:foobar"), "[relro]");
326        pretty_assertions::assert_eq!(vmo_name_to_digest_name(""), "[unnamed]");
327        pretty_assertions::assert_eq!(vmo_name_to_digest_name("scudo:primary"), "[scudo]");
328        pretty_assertions::assert_eq!(vmo_name_to_digest_name("libfoo.so.1"), "[bootfs-libraries]");
329        pretty_assertions::assert_eq!(vmo_name_to_digest_name("foobar"), "foobar");
330        pretty_assertions::assert_eq!(
331            vmo_name_to_digest_name("stack_and_tls:2331"),
332            "[bionic-stack]"
333        );
334        pretty_assertions::assert_eq!(vmo_name_to_digest_name("ext4!foobar"), "[ext4]");
335        pretty_assertions::assert_eq!(vmo_name_to_digest_name("dalvik-data1234"), "[dalvik]");
336    }
337}