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