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;
13#[cfg(target_os = "fuchsia")]
14use fuchsia_trace::duration;
15use serde::Serialize;
16use std::collections::{HashMap, HashSet};
17use std::fmt::Display;
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, Default, PartialEq, Serialize)]
22pub struct ComponentSummaryProfileResult {
23    pub kernel: fplugin_serde::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 unclaimed: u64,
27    #[serde(with = "fplugin_serde::PerformanceImpactMetricsDef")]
28    pub performance: fplugin::PerformanceImpactMetrics,
29    pub digest: Option<Digest>,
30}
31
32/// Summary view of the memory usage on a device.
33///
34/// This view aggregates the memory usage for each Principal, and, for each Principal, for VMOs
35/// sharing the same name or belonging to the same logical group. This is a view appropriate to
36/// display to developers who want to understand the memory usage of their Principal.
37#[derive(Debug, PartialEq, Serialize)]
38pub struct MemorySummary {
39    pub principals: Vec<PrincipalSummary>,
40    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
41    pub unclaimed: u64,
42}
43
44impl MemorySummary {
45    pub(crate) fn build(
46        principals: &HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
47        resources: &HashMap<u64, InflatedResource>,
48        resource_names: &Vec<ZXName>,
49    ) -> MemorySummary {
50        #[cfg(target_os = "fuchsia")]
51        duration!(crate::CATEGORY_MEMORY_CAPTURE, c"MemorySummary::build");
52        let mut output = MemorySummary { principals: Default::default(), unclaimed: 0 };
53        for principal in principals.values() {
54            output.principals.push(MemorySummary::build_one_principal(
55                &principal,
56                &principals,
57                &resources,
58                &resource_names,
59            ));
60        }
61
62        output.principals.sort_unstable_by_key(|p| -(p.populated_total as i64));
63
64        let mut unclaimed = 0;
65        for (_, resource) in resources {
66            if resource.claims.is_empty() {
67                match &resource.resource.resource_type {
68                    fplugin::ResourceType::Job(_) | fplugin::ResourceType::Process(_) => {}
69                    fplugin::ResourceType::Vmo(vmo) => {
70                        unclaimed += vmo.scaled_populated_bytes.unwrap();
71                    }
72                    _ => todo!(),
73                }
74            }
75        }
76        output.unclaimed = unclaimed;
77        output
78    }
79
80    fn build_one_principal(
81        principal: &InflatedPrincipal,
82        principals: &HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
83        resources: &HashMap<u64, InflatedResource>,
84        resource_names: &Vec<ZXName>,
85    ) -> PrincipalSummary {
86        let mut output = PrincipalSummary {
87            name: principal.name().to_owned(),
88            id: principal.principal.identifier.0.into(),
89            principal_type: match &principal.principal.principal_type {
90                PrincipalType::Runnable => "R",
91                PrincipalType::Part => "P",
92            }
93            .to_owned(),
94            committed_private: 0,
95            committed_scaled: 0.0,
96            committed_total: 0,
97            populated_private: 0,
98            populated_scaled: 0.0,
99            populated_total: 0,
100            attributor: principal
101                .principal
102                .parent
103                .as_ref()
104                .and_then(|p| principals.get(p))
105                .map(|p| p.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();
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) = resources.get(&process_mapped) {
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    /// Name of the Principal who gave attribution information for this Principal.
213    pub attributor: Option<String>,
214    /// List of Zircon processes attributed (even partially) to this Principal.
215    pub processes: Vec<String>,
216    /// Summary of memory usage for the VMOs accessible to this Principal, grouped by VMO name.
217    pub vmos: HashMap<ZXName, VmoSummary>,
218}
219
220impl PartialEq for PrincipalSummary {
221    fn eq(&self, other: &Self) -> bool {
222        self.id == other.id
223            && self.name == other.name
224            && self.principal_type == other.principal_type
225            && self.committed_private == other.committed_private
226            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
227            && self.committed_total == other.committed_total
228            && self.populated_private == other.populated_private
229            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
230            && self.populated_total == other.populated_total
231            && self.attributor == other.attributor
232            && self.processes == other.processes
233            && self.vmos == other.vmos
234    }
235}
236
237/// Group of VMOs sharing the same name.
238#[derive(Default, Debug, Serialize)]
239pub struct VmoSummary {
240    /// Number of distinct VMOs under the same name.
241    pub count: u64,
242    /// Number of committed bytes of this VMO group only accessible by the Principal this group
243    /// belongs.
244    pub committed_private: u64,
245    /// Number of committed bytes of this VMO group, scaled by the number of Principals that can
246    /// access them.
247    pub committed_scaled: f64,
248    /// Total number of committed bytes of this VMO group.
249    pub committed_total: u64,
250    /// Number of populated bytes of this VMO group only accessible by the Principal this group
251    /// belongs.
252    pub populated_private: u64,
253    /// Number of populated bytes of this VMO group, scaled by the number of Principals that can
254    /// access them.
255    pub populated_scaled: f64,
256    /// Total number of populated bytes of this VMO group.
257    pub populated_total: u64,
258}
259
260impl VmoSummary {
261    fn merge(&mut self, vmo_info: &Vmo, share_count: usize) {
262        self.count += 1;
263        self.committed_total += vmo_info.total_committed_bytes.unwrap();
264        self.populated_total += vmo_info.total_populated_bytes.unwrap();
265        self.committed_scaled +=
266            vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
267        self.populated_scaled +=
268            vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
269        if share_count == 1 {
270            self.committed_private += vmo_info.private_committed_bytes.unwrap();
271            self.populated_private += vmo_info.private_populated_bytes.unwrap();
272        }
273    }
274}
275
276impl PartialEq for VmoSummary {
277    fn eq(&self, other: &Self) -> bool {
278        self.count == other.count
279            && self.committed_private == other.committed_private
280            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
281            && self.committed_total == other.committed_total
282            && self.populated_private == other.populated_private
283            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
284            && self.populated_total == other.populated_total
285    }
286}
287const VMO_DIGEST_NAME_MAPPING: [(&str, &str); 13] = [
288    ("ld\\.so\\.1-internal-heap|(^stack: msg of.*)", "[process-bootstrap]"),
289    ("^blob-[0-9a-f]+$", "[blobs]"),
290    ("^inactive-blob-[0-9a-f]+$", "[inactive blobs]"),
291    ("^thrd_t:0x.*|initial-thread|pthread_t:0x.*$", "[stacks]"),
292    ("^data[0-9]*:.*$", "[data]"),
293    ("^bss[0-9]*:.*$", "[bss]"),
294    ("^relro:.*$", "[relro]"),
295    ("^$", "[unnamed]"),
296    ("^scudo:.*$", "[scudo]"),
297    ("^.*\\.so.*$", "[bootfs-libraries]"),
298    ("^stack_and_tls:.*$", "[bionic-stack]"),
299    ("^ext4!.*$", "[ext4]"),
300    ("^dalvik-.*$", "[dalvik]"),
301];
302
303/// Returns the name of a VMO category when the name match on of the rules.
304/// This is used for presentation and aggregation.
305pub fn vmo_name_to_digest_name(name: &str) -> &str {
306    static RULES: std::sync::LazyLock<Vec<(regex::Regex, &'static str)>> =
307        std::sync::LazyLock::new(|| {
308            VMO_DIGEST_NAME_MAPPING
309                .iter()
310                .map(|&(pattern, replacement)| (regex::Regex::new(pattern).unwrap(), replacement))
311                .collect()
312        });
313    RULES.iter().find(|(regex, _)| regex.is_match(name.trim())).map_or(name, |rule| rule.1)
314}
315
316pub fn vmo_name_to_digest_zxname(name: &ZXName) -> &ZXName {
317    static RULES: std::sync::LazyLock<Vec<(regex::bytes::Regex, ZXName)>> =
318        std::sync::LazyLock::new(|| {
319            VMO_DIGEST_NAME_MAPPING
320                .iter()
321                .map(|&(pattern, replacement)| {
322                    (
323                        regex::bytes::Regex::new(pattern).unwrap(),
324                        ZXName::try_from_bytes(replacement.as_bytes()).unwrap(),
325                    )
326                })
327                .collect()
328        });
329    RULES.iter().find(|(regex, _)| regex.is_match(name.as_bstr())).map_or(name, |rule| &rule.1)
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn rename_zx_test() {
338        pretty_assertions::assert_eq!(
339            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("ld.so.1-internal-heap")),
340            &ZXName::from_string_lossy("[process-bootstrap]"),
341        );
342    }
343
344    #[test]
345    fn rename_zx_test_small_name() {
346        // Verify that we can match regular expressions anchored at both ends even when the name is
347        // not taking the full size of a [ZXName].
348        pretty_assertions::assert_eq!(
349            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("blob-1234")),
350            &ZXName::from_string_lossy("[blobs]"),
351        );
352    }
353
354    #[test]
355    fn rename_test() {
356        pretty_assertions::assert_eq!(
357            vmo_name_to_digest_name("ld.so.1-internal-heap"),
358            "[process-bootstrap]"
359        );
360        pretty_assertions::assert_eq!(
361            vmo_name_to_digest_name("stack: msg of 123"),
362            "[process-bootstrap]"
363        );
364        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-123"), "[blobs]");
365        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-15e0da8e"), "[blobs]");
366        pretty_assertions::assert_eq!(
367            vmo_name_to_digest_name("inactive-blob-123"),
368            "[inactive blobs]"
369        );
370        pretty_assertions::assert_eq!(vmo_name_to_digest_name("thrd_t:0x123"), "[stacks]");
371        pretty_assertions::assert_eq!(vmo_name_to_digest_name("initial-thread"), "[stacks]");
372        pretty_assertions::assert_eq!(vmo_name_to_digest_name("pthread_t:0x123"), "[stacks]");
373        pretty_assertions::assert_eq!(vmo_name_to_digest_name("data456:"), "[data]");
374        pretty_assertions::assert_eq!(vmo_name_to_digest_name("bss456:"), "[bss]");
375        pretty_assertions::assert_eq!(vmo_name_to_digest_name("relro:foobar"), "[relro]");
376        pretty_assertions::assert_eq!(vmo_name_to_digest_name(""), "[unnamed]");
377        pretty_assertions::assert_eq!(vmo_name_to_digest_name("scudo:primary"), "[scudo]");
378        pretty_assertions::assert_eq!(vmo_name_to_digest_name("libfoo.so.1"), "[bootfs-libraries]");
379        pretty_assertions::assert_eq!(vmo_name_to_digest_name("foobar"), "foobar");
380        pretty_assertions::assert_eq!(
381            vmo_name_to_digest_name("stack_and_tls:2331"),
382            "[bionic-stack]"
383        );
384        pretty_assertions::assert_eq!(vmo_name_to_digest_name("ext4!foobar"), "[ext4]");
385        pretty_assertions::assert_eq!(vmo_name_to_digest_name("dalvik-data1234"), "[dalvik]");
386    }
387}