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