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    fplugin_serde, InflatedPrincipal, InflatedResource, PrincipalIdentifier, PrincipalType,
8    ResourceReference, 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/// 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 ComponentProfileResult {
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<PrincipalIdentifier, 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<PrincipalIdentifier, 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,
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<PrincipalIdentifier>>()
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                } = resource
164                {
165                    if let Some(process_ref) = resources.get(&process_mapped) {
166                        let process = process_ref.borrow();
167                        output.processes.push(format!(
168                            "{} ({})",
169                            resource_names.get(process.resource.name_index).unwrap().clone(),
170                            process.resource.koid
171                        ));
172                    }
173                }
174            }
175        }
176
177        output.processes.sort();
178        output
179    }
180}
181
182impl Display for MemorySummary {
183    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        Ok(())
185    }
186}
187
188/// Summary of a Principal memory usage, and its breakdown per VMO group.
189#[derive(Debug, Serialize)]
190pub struct PrincipalSummary {
191    /// Identifier for the Principal. This number is not meaningful outside of the memory
192    /// attribution system.
193    pub id: u64,
194    /// Display name of the Principal.
195    pub name: String,
196    /// Type of the Principal.
197    pub principal_type: String,
198    /// Number of committed private bytes of the Principal.
199    pub committed_private: u64,
200    /// Number of committed bytes of all VMOs accessible to the Principal, scaled by the number of
201    /// Principals that can access them.
202    pub committed_scaled: f64,
203    /// Total number of committed bytes of all the VMOs accessible to the Principal.
204    pub committed_total: u64,
205    /// Number of populated private bytes of the Principal.
206    pub populated_private: u64,
207    /// Number of populated bytes of all VMOs accessible to the Principal, scaled by the number of
208    /// Principals that can access them.
209    pub populated_scaled: f64,
210    /// Total number of populated bytes of all the VMOs accessible to the Principal.
211    pub populated_total: u64,
212
213    /// Name of the Principal who gave attribution information for this Principal.
214    pub attributor: Option<String>,
215    /// List of Zircon processes attributed (even partially) to this Principal.
216    pub processes: Vec<String>,
217    /// Summary of memory usage for the VMOs accessible to this Principal, grouped by VMO name.
218    pub vmos: HashMap<ZXName, VmoSummary>,
219}
220
221impl PartialEq for PrincipalSummary {
222    fn eq(&self, other: &Self) -> bool {
223        self.id == other.id
224            && self.name == other.name
225            && self.principal_type == other.principal_type
226            && self.committed_private == other.committed_private
227            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
228            && self.committed_total == other.committed_total
229            && self.populated_private == other.populated_private
230            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
231            && self.populated_total == other.populated_total
232            && self.attributor == other.attributor
233            && self.processes == other.processes
234            && self.vmos == other.vmos
235    }
236}
237
238/// Group of VMOs sharing the same name.
239#[derive(Default, Debug, Serialize)]
240pub struct VmoSummary {
241    /// Number of distinct VMOs under the same name.
242    pub count: u64,
243    /// Number of committed bytes of this VMO group only accessible by the Principal this group
244    /// belongs.
245    pub committed_private: u64,
246    /// Number of committed bytes of this VMO group, scaled by the number of Principals that can
247    /// access them.
248    pub committed_scaled: f64,
249    /// Total number of committed bytes of this VMO group.
250    pub committed_total: u64,
251    /// Number of populated bytes of this VMO group only accessible by the Principal this group
252    /// belongs.
253    pub populated_private: u64,
254    /// Number of populated bytes of this VMO group, scaled by the number of Principals that can
255    /// access them.
256    pub populated_scaled: f64,
257    /// Total number of populated bytes of this VMO group.
258    pub populated_total: u64,
259}
260
261impl VmoSummary {
262    fn merge(&mut self, vmo_info: &Vmo, share_count: usize) {
263        self.count += 1;
264        self.committed_total += vmo_info.total_committed_bytes.unwrap();
265        self.populated_total += vmo_info.total_populated_bytes.unwrap();
266        self.committed_scaled +=
267            vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
268        self.populated_scaled +=
269            vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
270        if share_count == 1 {
271            self.committed_private += vmo_info.private_committed_bytes.unwrap();
272            self.populated_private += vmo_info.private_populated_bytes.unwrap();
273        }
274    }
275}
276
277impl PartialEq for VmoSummary {
278    fn eq(&self, other: &Self) -> bool {
279        self.count == other.count
280            && self.committed_private == other.committed_private
281            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
282            && self.committed_total == other.committed_total
283            && self.populated_private == other.populated_private
284            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
285            && self.populated_total == other.populated_total
286    }
287}
288const VMO_DIGEST_NAME_MAPPING: [(&str, &str); 13] = [
289    ("ld\\.so\\.1-internal-heap|(^stack: msg of.*)", "[process-bootstrap]"),
290    ("^blob-[0-9a-f]+$", "[blobs]"),
291    ("^inactive-blob-[0-9a-f]+$", "[inactive blobs]"),
292    ("^thrd_t:0x.*|initial-thread|pthread_t:0x.*$", "[stacks]"),
293    ("^data[0-9]*:.*$", "[data]"),
294    ("^bss[0-9]*:.*$", "[bss]"),
295    ("^relro:.*$", "[relro]"),
296    ("^$", "[unnamed]"),
297    ("^scudo:.*$", "[scudo]"),
298    ("^.*\\.so.*$", "[bootfs-libraries]"),
299    ("^stack_and_tls:.*$", "[bionic-stack]"),
300    ("^ext4!.*$", "[ext4]"),
301    ("^dalvik-.*$", "[dalvik]"),
302];
303
304/// Returns the name of a VMO category when the name match on of the rules.
305/// This is used for presentation and aggregation.
306pub fn vmo_name_to_digest_name(name: &str) -> &str {
307    static RULES: std::sync::LazyLock<Vec<(regex::Regex, &'static str)>> =
308        std::sync::LazyLock::new(|| {
309            VMO_DIGEST_NAME_MAPPING
310                .iter()
311                .map(|&(pattern, replacement)| (regex::Regex::new(pattern).unwrap(), replacement))
312                .collect()
313        });
314    RULES.iter().find(|(regex, _)| regex.is_match(name.trim())).map_or(name, |rule| rule.1)
315}
316
317pub fn vmo_name_to_digest_zxname(name: &ZXName) -> &ZXName {
318    static RULES: std::sync::LazyLock<Vec<(regex::bytes::Regex, ZXName)>> =
319        std::sync::LazyLock::new(|| {
320            VMO_DIGEST_NAME_MAPPING
321                .iter()
322                .map(|&(pattern, replacement)| {
323                    (
324                        regex::bytes::Regex::new(pattern).unwrap(),
325                        ZXName::try_from_bytes(replacement.as_bytes()).unwrap(),
326                    )
327                })
328                .collect()
329        });
330    RULES.iter().find(|(regex, _)| regex.is_match(name.as_bstr())).map_or(name, |rule| &rule.1)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn rename_zx_test() {
339        pretty_assertions::assert_eq!(
340            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("ld.so.1-internal-heap")),
341            &ZXName::from_string_lossy("[process-bootstrap]"),
342        );
343    }
344
345    #[test]
346    fn rename_zx_test_small_name() {
347        // Verify that we can match regular expressions anchored at both ends even when the name is
348        // not taking the full size of a [ZXName].
349        pretty_assertions::assert_eq!(
350            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("blob-1234")),
351            &ZXName::from_string_lossy("[blobs]"),
352        );
353    }
354
355    #[test]
356    fn rename_test() {
357        pretty_assertions::assert_eq!(
358            vmo_name_to_digest_name("ld.so.1-internal-heap"),
359            "[process-bootstrap]"
360        );
361        pretty_assertions::assert_eq!(
362            vmo_name_to_digest_name("stack: msg of 123"),
363            "[process-bootstrap]"
364        );
365        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-123"), "[blobs]");
366        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-15e0da8e"), "[blobs]");
367        pretty_assertions::assert_eq!(
368            vmo_name_to_digest_name("inactive-blob-123"),
369            "[inactive blobs]"
370        );
371        pretty_assertions::assert_eq!(vmo_name_to_digest_name("thrd_t:0x123"), "[stacks]");
372        pretty_assertions::assert_eq!(vmo_name_to_digest_name("initial-thread"), "[stacks]");
373        pretty_assertions::assert_eq!(vmo_name_to_digest_name("pthread_t:0x123"), "[stacks]");
374        pretty_assertions::assert_eq!(vmo_name_to_digest_name("data456:"), "[data]");
375        pretty_assertions::assert_eq!(vmo_name_to_digest_name("bss456:"), "[bss]");
376        pretty_assertions::assert_eq!(vmo_name_to_digest_name("relro:foobar"), "[relro]");
377        pretty_assertions::assert_eq!(vmo_name_to_digest_name(""), "[unnamed]");
378        pretty_assertions::assert_eq!(vmo_name_to_digest_name("scudo:primary"), "[scudo]");
379        pretty_assertions::assert_eq!(vmo_name_to_digest_name("libfoo.so.1"), "[bootfs-libraries]");
380        pretty_assertions::assert_eq!(vmo_name_to_digest_name("foobar"), "foobar");
381        pretty_assertions::assert_eq!(
382            vmo_name_to_digest_name("stack_and_tls:2331"),
383            "[bionic-stack]"
384        );
385        pretty_assertions::assert_eq!(vmo_name_to_digest_name("ext4!foobar"), "[ext4]");
386        pretty_assertions::assert_eq!(vmo_name_to_digest_name("dalvik-data1234"), "[dalvik]");
387    }
388}