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