Skip to main content

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::macros::vmo_digests;
7use crate::{
8    Claim, GlobalPrincipalIdentifier, InflatedPrincipal, InflatedResource, PrincipalType, ZXName,
9    fplugin_serde,
10};
11use bstr::ByteSlice;
12use core::default::Default;
13use fidl_fuchsia_memory_attribution_plugin_common as fplugin;
14use fplugin::Vmo;
15#[cfg(target_os = "fuchsia")]
16use fuchsia_trace::duration;
17use rustc_hash::FxHashMap;
18use serde::Serialize;
19use std::collections::{HashMap, HashSet};
20use std::fmt::Display;
21
22/// Consider that two floats are equals if they differ less than [FLOAT_COMPARISON_EPSILON].
23const FLOAT_COMPARISON_EPSILON: f64 = 1e-10;
24
25#[derive(Debug, Default, PartialEq, Serialize)]
26pub struct ComponentSummaryProfileResult {
27    pub kernel: fplugin_serde::KernelStatistics,
28    pub principals: Vec<PrincipalSummary>,
29    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
30    pub unclaimed: u64,
31    #[serde(with = "fplugin_serde::PerformanceImpactMetricsDef")]
32    pub performance: fplugin::PerformanceImpactMetrics,
33    pub digest: Option<Digest>,
34}
35
36/// Summary view of the memory usage on a device.
37///
38/// This view aggregates the memory usage for each Principal, and, for each Principal, for VMOs
39/// sharing the same name or belonging to the same logical group. This is a view appropriate to
40/// display to developers who want to understand the memory usage of their Principal.
41#[derive(Debug, PartialEq, Serialize)]
42pub struct MemorySummary {
43    pub principals: Vec<PrincipalSummary>,
44    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
45    pub unclaimed: u64,
46}
47
48fn compute_share_count(
49    claims: &HashSet<Claim>,
50    subjects_buf: &mut Vec<GlobalPrincipalIdentifier>,
51) -> usize {
52    match claims.len() {
53        0 => 0,
54        1 => 1,
55        2 => {
56            let mut iter = claims.iter();
57            let s1 = iter.next().unwrap().subject;
58            let s2 = iter.next().unwrap().subject;
59            if s1 == s2 { 1 } else { 2 }
60        }
61        _ => {
62            subjects_buf.clear();
63            subjects_buf.extend(claims.iter().map(|c| c.subject));
64            subjects_buf.sort_unstable();
65            subjects_buf.dedup();
66            subjects_buf.len()
67        }
68    }
69}
70
71impl MemorySummary {
72    pub(crate) fn build(
73        principals: &FxHashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
74        resources: &FxHashMap<u64, InflatedResource>,
75        resource_names: &Vec<ZXName>,
76    ) -> MemorySummary {
77        #[cfg(target_os = "fuchsia")]
78        duration!(crate::CATEGORY_MEMORY_CAPTURE, c"MemorySummary::build");
79        let digested_names: Vec<&ZXName> =
80            resource_names.iter().map(vmo_name_to_digest_zxname).collect();
81        let mut subjects_buf = Vec::new();
82        let share_counts: FxHashMap<u64, usize> = resources
83            .iter()
84            .map(|(&koid, resource)| {
85                (koid, compute_share_count(&resource.claims, &mut subjects_buf))
86            })
87            .collect();
88
89        let mut output = MemorySummary { principals: Default::default(), unclaimed: 0 };
90        for principal in principals.values() {
91            output.principals.push(MemorySummary::build_one_principal(
92                &principal,
93                &principals,
94                &resources,
95                &resource_names,
96                &digested_names,
97                &share_counts,
98            ));
99        }
100
101        output.principals.sort_unstable_by(|a, b| b.populated_total.cmp(&a.populated_total));
102
103        let mut unclaimed = 0;
104        for (_, resource) in resources {
105            if resource.claims.is_empty() {
106                match &resource.resource.resource_type {
107                    fplugin::ResourceType::Job(_) | fplugin::ResourceType::Process(_) => {}
108                    fplugin::ResourceType::Vmo(vmo) => {
109                        unclaimed += vmo.scaled_populated_bytes.unwrap();
110                    }
111                    _ => todo!(),
112                }
113            }
114        }
115        output.unclaimed = unclaimed;
116        output
117    }
118
119    fn build_one_principal(
120        principal: &InflatedPrincipal,
121        principals: &FxHashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
122        resources: &FxHashMap<u64, InflatedResource>,
123        resource_names: &Vec<ZXName>,
124        digested_names: &[&ZXName],
125        share_counts: &FxHashMap<u64, usize>,
126    ) -> PrincipalSummary {
127        let mut output = PrincipalSummary {
128            name: principal.name().to_owned(),
129            id: principal.principal.identifier.0.into(),
130            principal_type: match &principal.principal.principal_type {
131                PrincipalType::Runnable => "R",
132                PrincipalType::Part => "P",
133            }
134            .to_owned(),
135            committed_private: 0,
136            committed_scaled: 0.0,
137            committed_total: 0,
138            populated_private: 0,
139            populated_scaled: 0.0,
140            populated_total: 0,
141            attributor: principal
142                .principal
143                .parent
144                .as_ref()
145                .and_then(|p| principals.get(p))
146                .map(|p| p.name().to_owned()),
147            processes: Vec::new(),
148            vmos: HashMap::new(),
149        };
150
151        for resource_id in &principal.resources {
152            let Some(resource) = resources.get(resource_id) else {
153                continue;
154            };
155            let share_count = *share_counts.get(resource_id).unwrap();
156            match &resource.resource.resource_type {
157                fplugin::ResourceType::Job(_) => todo!(),
158                fplugin::ResourceType::Process(_) => {
159                    output.processes.push(format!(
160                        "{} ({})",
161                        resource_names.get(resource.resource.name_index).unwrap(),
162                        resource.resource.koid
163                    ));
164                }
165                fplugin::ResourceType::Vmo(vmo_info) => {
166                    output.committed_total += vmo_info.total_committed_bytes.unwrap();
167                    output.populated_total += vmo_info.total_populated_bytes.unwrap();
168                    output.committed_scaled +=
169                        vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
170                    output.populated_scaled +=
171                        vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
172                    if share_count == 1 {
173                        output.committed_private += vmo_info.private_committed_bytes.unwrap();
174                        output.populated_private += vmo_info.private_populated_bytes.unwrap();
175                    }
176                    let digest_name = digested_names[resource.resource.name_index];
177                    // This avoids using .entry(), which forces us to clone the key even when the
178                    // entry already exists.
179                    if let Some(summary) = output.vmos.get_mut(digest_name) {
180                        summary.merge(vmo_info, share_count);
181                    } else {
182                        let mut summary = VmoSummary::default();
183                        summary.merge(vmo_info, share_count);
184                        output.vmos.insert(digest_name.clone(), summary);
185                    }
186                }
187                _ => todo!(),
188            }
189        }
190
191        for process_mapped in &principal.mapped_processes {
192            if let Some(process) = resources.get(process_mapped) {
193                output.processes.push(format!(
194                    "{} ({})",
195                    resource_names.get(process.resource.name_index).unwrap(),
196                    process.resource.koid
197                ));
198            }
199        }
200
201        output.processes.sort();
202        output
203    }
204}
205
206impl Display for MemorySummary {
207    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        Ok(())
209    }
210}
211
212/// Summary of a Principal memory usage, and its breakdown per VMO group.
213#[derive(Debug, Serialize)]
214pub struct PrincipalSummary {
215    /// Identifier for the Principal. This number is not meaningful outside of the memory
216    /// attribution system.
217    pub id: u64,
218    /// Display name of the Principal.
219    pub name: String,
220    /// Type of the Principal.
221    pub principal_type: String,
222    /// Number of committed private bytes of the Principal.
223    pub committed_private: u64,
224    /// Number of committed bytes of all VMOs accessible to the Principal, scaled by the number of
225    /// Principals that can access them.
226    pub committed_scaled: f64,
227    /// Total number of committed bytes of all the VMOs accessible to the Principal.
228    pub committed_total: u64,
229    /// Number of populated private bytes of the Principal.
230    pub populated_private: u64,
231    /// Number of populated bytes of all VMOs accessible to the Principal, scaled by the number of
232    /// Principals that can access them.
233    pub populated_scaled: f64,
234    /// Total number of populated bytes of all the VMOs accessible to the Principal.
235    pub populated_total: u64,
236    /// Name of the Principal who gave attribution information for this Principal.
237    pub attributor: Option<String>,
238    /// List of Zircon processes attributed (even partially) to this Principal.
239    pub processes: Vec<String>,
240    /// Summary of memory usage for the VMOs accessible to this Principal, grouped by VMO name.
241    pub vmos: HashMap<ZXName, VmoSummary>,
242}
243
244impl PartialEq for PrincipalSummary {
245    fn eq(&self, other: &Self) -> bool {
246        self.id == other.id
247            && self.name == other.name
248            && self.principal_type == other.principal_type
249            && self.committed_private == other.committed_private
250            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
251            && self.committed_total == other.committed_total
252            && self.populated_private == other.populated_private
253            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
254            && self.populated_total == other.populated_total
255            && self.attributor == other.attributor
256            && self.processes == other.processes
257            && self.vmos == other.vmos
258    }
259}
260
261/// Group of VMOs sharing the same name.
262#[derive(Default, Debug, Serialize)]
263pub struct VmoSummary {
264    /// Number of distinct VMOs under the same name.
265    pub count: u64,
266    /// Number of committed bytes of this VMO group only accessible by the Principal this group
267    /// belongs.
268    pub committed_private: u64,
269    /// Number of committed bytes of this VMO group, scaled by the number of Principals that can
270    /// access them.
271    pub committed_scaled: f64,
272    /// Total number of committed bytes of this VMO group.
273    pub committed_total: u64,
274    /// Number of populated bytes of this VMO group only accessible by the Principal this group
275    /// belongs.
276    pub populated_private: u64,
277    /// Number of populated bytes of this VMO group, scaled by the number of Principals that can
278    /// access them.
279    pub populated_scaled: f64,
280    /// Total number of populated bytes of this VMO group.
281    pub populated_total: u64,
282}
283
284impl VmoSummary {
285    fn merge(&mut self, vmo_info: &Vmo, share_count: usize) {
286        self.count += 1;
287        self.committed_total += vmo_info.total_committed_bytes.unwrap();
288        self.populated_total += vmo_info.total_populated_bytes.unwrap();
289        self.committed_scaled +=
290            vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
291        self.populated_scaled +=
292            vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
293        if share_count == 1 {
294            self.committed_private += vmo_info.private_committed_bytes.unwrap();
295            self.populated_private += vmo_info.private_populated_bytes.unwrap();
296        }
297    }
298}
299
300impl PartialEq for VmoSummary {
301    fn eq(&self, other: &Self) -> bool {
302        self.count == other.count
303            && self.committed_private == other.committed_private
304            && (self.committed_scaled - other.committed_scaled).abs() < FLOAT_COMPARISON_EPSILON
305            && self.committed_total == other.committed_total
306            && self.populated_private == other.populated_private
307            && (self.populated_scaled - other.populated_scaled).abs() < FLOAT_COMPARISON_EPSILON
308            && self.populated_total == other.populated_total
309    }
310}
311vmo_digests! {
312    (
313        ProcessBootstrap,
314        "[process-bootstrap]",
315        or(contains("ld.so.1-internal-heap"), starts_with("stack: msg of"))
316    ),
317    (Blobs, "[blobs]", exact("blob-", hex())),
318    (InactiveBlobs, "[inactive blobs]", exact("inactive-blob-", hex())),
319    (
320        Stacks,
321        "[stacks]",
322        or(
323            starts_with("thrd_t:0x"),
324            contains("initial-thread"),
325            contains("pthread_t:0x"),
326            contains("pthread_create:0x")
327        )
328    ),
329    (Data, "[data]", starts_with("data", digits(), ":")),
330    (Bss, "[bss]", starts_with("bss", digits(), ":")),
331    (Relro, "[relro]", starts_with("relro:")),
332    (Unnamed, "[unnamed]", exact("")),
333    (Scudo, "[scudo]", starts_with("scudo:")),
334    (BootfsLibraries, "[bootfs-libraries]", contains(".so")),
335    (BionicStack, "[bionic-stack]", starts_with("stack_and_tls:")),
336    (Ext4, "[ext4]", starts_with("ext4!")),
337    (Dalvik, "[dalvik]", starts_with("dalvik-")),
338    (Bootfs, "[bootfs]", or(exact("bootfs"), starts_with("bootfs:"))),
339    (RestrictedStateVmo, "[restricted_state_vmo]", exact("restricted_state_vmo:", digits())),
340}
341
342#[cfg(test)]
343const VMO_DIGEST_NAME_MAPPING: [(&str, &str); 15] = [
344    ("ld\\.so\\.1-internal-heap|(^stack: msg of.*)", "[process-bootstrap]"),
345    ("^blob-[0-9a-f]+$", "[blobs]"),
346    ("^inactive-blob-[0-9a-f]+$", "[inactive blobs]"),
347    ("^thrd_t:0x.*|initial-thread|pthread_(t|create):0x.*$", "[stacks]"),
348    ("^data[0-9]*:.*$", "[data]"),
349    ("^bss[0-9]*:.*$", "[bss]"),
350    ("^relro:.*$", "[relro]"),
351    ("^$", "[unnamed]"),
352    ("^scudo:.*$", "[scudo]"),
353    ("^.*\\.so.*$", "[bootfs-libraries]"),
354    ("^stack_and_tls:.*$", "[bionic-stack]"),
355    ("^ext4!.*$", "[ext4]"),
356    ("^dalvik-.*$", "[dalvik]"),
357    ("^bootfs(:.*)?$", "[bootfs]"),
358    ("^restricted_state_vmo:[0-9]+$", "[restricted_state_vmo]"),
359];
360
361/// Returns the name of a VMO category when the name matches one of the rules.
362/// This is used for presentation and aggregation.
363pub fn vmo_name_to_digest_name(name: &str) -> &str {
364    if let Some(category) = match_vmo_digest(name.trim()) { category.as_str() } else { name }
365}
366
367pub fn vmo_name_to_digest_zxname(name: &ZXName) -> &ZXName {
368    if let Ok(name_str) = name.as_bstr().to_str() {
369        if let Some(category) = match_vmo_digest(name_str) {
370            return category.as_zxname();
371        }
372    }
373    name
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::{Claim, ClaimType, GlobalPrincipalIdentifier, InflatedPrincipal, InflatedResource};
380
381    #[test]
382    fn rename_zx_test() {
383        pretty_assertions::assert_eq!(
384            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("ld.so.1-internal-heap")),
385            &ZXName::from_string_lossy("[process-bootstrap]"),
386        );
387    }
388
389    #[test]
390    fn rename_zx_test_small_name() {
391        // Verify that we can match regular expressions anchored at both ends even when the name is
392        // not taking the full size of a [ZXName].
393        pretty_assertions::assert_eq!(
394            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("blob-1234")),
395            &ZXName::from_string_lossy("[blobs]"),
396        );
397    }
398
399    #[test]
400    fn rename_test() {
401        pretty_assertions::assert_eq!(
402            vmo_name_to_digest_name("ld.so.1-internal-heap"),
403            "[process-bootstrap]"
404        );
405        pretty_assertions::assert_eq!(
406            vmo_name_to_digest_name("stack: msg of 123"),
407            "[process-bootstrap]"
408        );
409        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-123"), "[blobs]");
410        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-15e0da8e"), "[blobs]");
411        pretty_assertions::assert_eq!(
412            vmo_name_to_digest_name("inactive-blob-123"),
413            "[inactive blobs]"
414        );
415        pretty_assertions::assert_eq!(vmo_name_to_digest_name("thrd_t:0x123"), "[stacks]");
416        pretty_assertions::assert_eq!(vmo_name_to_digest_name("initial-thread"), "[stacks]");
417        pretty_assertions::assert_eq!(vmo_name_to_digest_name("pthread_t:0x123"), "[stacks]");
418        pretty_assertions::assert_eq!(
419            vmo_name_to_digest_name("pthread_create:0xfa124714"),
420            "[stacks]"
421        );
422        pretty_assertions::assert_eq!(vmo_name_to_digest_name("data456:"), "[data]");
423        pretty_assertions::assert_eq!(vmo_name_to_digest_name("bss456:"), "[bss]");
424        pretty_assertions::assert_eq!(vmo_name_to_digest_name("relro:foobar"), "[relro]");
425        pretty_assertions::assert_eq!(vmo_name_to_digest_name(""), "[unnamed]");
426        pretty_assertions::assert_eq!(vmo_name_to_digest_name("scudo:primary"), "[scudo]");
427        pretty_assertions::assert_eq!(vmo_name_to_digest_name("libfoo.so.1"), "[bootfs-libraries]");
428        pretty_assertions::assert_eq!(vmo_name_to_digest_name("foobar"), "foobar");
429        pretty_assertions::assert_eq!(
430            vmo_name_to_digest_name("stack_and_tls:2331"),
431            "[bionic-stack]"
432        );
433        pretty_assertions::assert_eq!(vmo_name_to_digest_name("ext4!foobar"), "[ext4]");
434        pretty_assertions::assert_eq!(vmo_name_to_digest_name("dalvik-data1234"), "[dalvik]");
435        pretty_assertions::assert_eq!(
436            vmo_name_to_digest_name("restricted_state_vmo:119723"),
437            "[restricted_state_vmo]"
438        );
439    }
440
441    // Verifies that the fast string matching rules match the regex rules.
442    #[test]
443    fn test_vmo_digest_rules_match_regex() {
444        let test_strings = [
445            "ld.so.1-internal-heap",
446            "prefix-ld.so.1-internal-heap",
447            "stack: msg of something",
448            "stack: msg of",
449            "stack: msg",
450            "blob-1234",
451            "blob-abcdef",
452            "blob-0123456789abcdef",
453            "blob-",
454            "blob-123g",
455            "blob-ABC",
456            "inactive-blob-1234",
457            "inactive-blob-abcdef",
458            "inactive-blob-",
459            "inactive-blob-xyz",
460            "thrd_t:0x123",
461            "thrd_t:0x",
462            "prefix-thrd_t:0x123",
463            "initial-thread",
464            "prefix-initial-thread-suffix",
465            "pthread_t:0x123",
466            "pthread_create:0xfa124714",
467            "data:",
468            "data0:",
469            "data123:foo",
470            "data:bar",
471            "data_foo:",
472            "data",
473            "bss:",
474            "bss99:",
475            "bss456:bar",
476            "bss_foo:",
477            "bss",
478            "relro:",
479            "relro:foo",
480            "relro_other",
481            "",
482            "scudo:",
483            "scudo:primary",
484            "scudo_other",
485            "libfoo.so.1",
486            "test.so",
487            ".so",
488            "stack_and_tls:123",
489            "stack_and_tls:",
490            "ext4!foobar",
491            "ext4!",
492            "dalvik-data",
493            "dalvik-",
494            "bootfs",
495            "bootfs:",
496            "bootfs:bin",
497            "bootfs_other",
498            "restricted_state_vmo:",
499            "restricted_state_vmo:0",
500            "restricted_state_vmo:12345",
501            "restricted_state_vmo:abc",
502            "restricted_state_vmo:12a",
503            "foobar",
504            "random_string_123",
505            "other-blob-1234",
506        ];
507
508        static RULES: std::sync::LazyLock<Vec<(regex_lite::Regex, &'static str)>> =
509            std::sync::LazyLock::new(|| {
510                VMO_DIGEST_NAME_MAPPING
511                    .iter()
512                    .map(|&(pattern, replacement)| {
513                        (regex_lite::Regex::new(pattern).unwrap(), replacement)
514                    })
515                    .collect()
516            });
517
518        for s in test_strings {
519            let expected =
520                RULES.iter().find(|(regex, _)| regex.is_match(s)).map_or(s, |rule| rule.1);
521            let actual = vmo_name_to_digest_name(s);
522            assert_eq!(actual, expected, "Mismatch for string: {:?}", s);
523
524            let zx_in = ZXName::from_string_lossy(s);
525            let zx_expected = ZXName::from_string_lossy(expected);
526            let zx_actual = vmo_name_to_digest_zxname(&zx_in);
527            assert_eq!(zx_actual, &zx_expected, "ZXName mismatch for string: {:?}", s);
528        }
529    }
530
531    fn make_test_principal(id: u64, name: &str) -> InflatedPrincipal {
532        InflatedPrincipal::new(
533            fplugin::Principal {
534                identifier: Some(fplugin::PrincipalIdentifier { id }),
535                description: Some(fplugin::Description::Component(name.to_owned())),
536                principal_type: Some(fplugin::PrincipalType::Runnable),
537                parent: None,
538                ..Default::default()
539            }
540            .into(),
541        )
542    }
543
544    fn make_test_vmo_resource(
545        koid: u64,
546        name_index: usize,
547        committed: u64,
548        populated: u64,
549        claims: Vec<(u64, u64)>,
550    ) -> InflatedResource {
551        let mut res = InflatedResource::new(
552            fplugin::Resource {
553                koid: Some(koid),
554                name_index: Some(name_index as u64),
555                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
556                    private_committed_bytes: Some(committed),
557                    private_populated_bytes: Some(populated),
558                    scaled_committed_bytes: Some(committed),
559                    scaled_populated_bytes: Some(populated),
560                    total_committed_bytes: Some(committed),
561                    total_populated_bytes: Some(populated),
562                    ..Default::default()
563                })),
564                ..Default::default()
565            }
566            .into(),
567        );
568        for (source, subject) in claims {
569            res.claims.insert(Claim {
570                source: GlobalPrincipalIdentifier::new_for_test(source),
571                subject: GlobalPrincipalIdentifier::new_for_test(subject),
572                claim_type: ClaimType::Direct,
573            });
574        }
575        res
576    }
577
578    /// What is tested: `MemorySummary::build` sorting of `PrincipalSummary` entries by
579    /// `populated_total` in descending order.
580    ///
581    /// Expectations verified:
582    /// - Principals in `summary.principals` are ordered descending by their total populated bytes
583    ///   (`1_000_000_000` -> `500_000_000` -> `100_000_000`).
584    /// - Verifies that large unsigned byte totals are handled correctly without sign-overflow when
585    ///   sorting comparator logic is refactored.
586    #[test]
587    fn test_memory_summary_build_sorting_and_overflow() {
588        let mut principals = FxHashMap::default();
589        let mut p1 = make_test_principal(1, "small_principal");
590        p1.resources.push(101);
591        let mut p2 = make_test_principal(2, "large_principal");
592        p2.resources.push(102);
593        let mut p3 = make_test_principal(3, "medium_principal");
594        p3.resources.push(103);
595        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
596        principals.insert(GlobalPrincipalIdentifier::new_for_test(2), p2);
597        principals.insert(GlobalPrincipalIdentifier::new_for_test(3), p3);
598
599        let mut resources = FxHashMap::default();
600        resources
601            .insert(101, make_test_vmo_resource(101, 0, 100_000_000, 100_000_000, vec![(1, 1)]));
602        resources.insert(
603            102,
604            make_test_vmo_resource(102, 1, 1_000_000_000, 1_000_000_000, vec![(2, 2)]),
605        );
606        resources
607            .insert(103, make_test_vmo_resource(103, 2, 500_000_000, 500_000_000, vec![(3, 3)]));
608
609        let resource_names = vec![
610            ZXName::from_string_lossy("vmo_1"),
611            ZXName::from_string_lossy("vmo_2"),
612            ZXName::from_string_lossy("vmo_3"),
613        ];
614
615        let summary = MemorySummary::build(&principals, &resources, &resource_names);
616        assert_eq!(summary.principals.len(), 3);
617        assert_eq!(summary.principals[0].name, "large_principal");
618        assert_eq!(summary.principals[0].populated_total, 1_000_000_000);
619        assert_eq!(summary.principals[1].name, "medium_principal");
620        assert_eq!(summary.principals[1].populated_total, 500_000_000);
621        assert_eq!(summary.principals[2].name, "small_principal");
622        assert_eq!(summary.principals[2].populated_total, 100_000_000);
623    }
624
625    /// What is tested: VMO digest aggregation and merging when they have the same name.
626    ///
627    /// Expectations verified:
628    /// - When multiple VMOs owned by a principal have distinct names ("blob-1111", "blob-2222")
629    ///   that digest to the same bucket ("[blobs]"), they are merged into a single `VmoSummary`
630    ///   entry.
631    /// - Verifies `vmo_summary.count == 2` and that all committed/populated byte metrics (total and
632    ///   private) are accurately summed across the aggregated VMOs.
633    #[test]
634    fn test_memory_summary_vmo_digest_aggregation() {
635        let mut principals = FxHashMap::default();
636        let mut p1 = make_test_principal(1, "blob_owner");
637        p1.resources.push(1001);
638        p1.resources.push(1002);
639        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
640
641        let mut resources = FxHashMap::default();
642        resources.insert(1001, make_test_vmo_resource(1001, 0, 100, 200, vec![(1, 1)]));
643        resources.insert(1002, make_test_vmo_resource(1002, 1, 300, 400, vec![(1, 1)]));
644
645        let resource_names =
646            vec![ZXName::from_string_lossy("blob-1111"), ZXName::from_string_lossy("blob-2222")];
647
648        let summary = MemorySummary::build(&principals, &resources, &resource_names);
649        assert_eq!(summary.principals.len(), 1);
650        let p_summary = &summary.principals[0];
651        assert_eq!(p_summary.vmos.len(), 1);
652
653        let blob_digest = ZXName::from_string_lossy("[blobs]");
654        let vmo_summary = p_summary.vmos.get(&blob_digest).expect("Should aggregate under [blobs]");
655        assert_eq!(vmo_summary.count, 2);
656        assert_eq!(vmo_summary.committed_total, 400);
657        assert_eq!(vmo_summary.populated_total, 600);
658        assert_eq!(vmo_summary.committed_private, 400);
659        assert_eq!(vmo_summary.populated_private, 600);
660    }
661
662    /// What is tested: Process formatting and alphabetical sorting of process strings in
663    /// `PrincipalSummary.processes`.
664    ///
665    /// Expectations verified:
666    /// - Multiple distinct process resources attributed to a principal are formatted as `"name
667    ///   (koid)"` and sorted alphabetically (`"alpha_process (2002)"` before `"zeta_process (2001)
668    ///   "`).
669    #[test]
670    fn test_memory_summary_process_formatting_and_sorting() {
671        let mut principals = FxHashMap::default();
672        let mut p1 = make_test_principal(1, "proc_owner");
673        p1.resources.push(2001);
674        p1.resources.push(2002);
675        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
676
677        let mut resources = FxHashMap::default();
678        let r1 = InflatedResource::new(
679            fplugin::Resource {
680                koid: Some(2001),
681                name_index: Some(0),
682                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
683                    vmos: Some(vec![]),
684                    mappings: None,
685                    ..Default::default()
686                })),
687                ..Default::default()
688            }
689            .into(),
690        );
691        let r2 = InflatedResource::new(
692            fplugin::Resource {
693                koid: Some(2002),
694                name_index: Some(1),
695                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
696                    vmos: Some(vec![]),
697                    mappings: None,
698                    ..Default::default()
699                })),
700                ..Default::default()
701            }
702            .into(),
703        );
704        resources.insert(2001, r1);
705        resources.insert(2002, r2);
706
707        let resource_names = vec![
708            ZXName::from_string_lossy("zeta_process"),
709            ZXName::from_string_lossy("alpha_process"),
710        ];
711
712        let summary = MemorySummary::build(&principals, &resources, &resource_names);
713        assert_eq!(summary.principals.len(), 1);
714        assert_eq!(
715            summary.principals[0].processes,
716            vec!["alpha_process (2002)".to_owned(), "zeta_process (2001)".to_owned()]
717        );
718    }
719
720    /// What is tested: `share_count` division and private vs. scaled memory calculations when a VMO
721    /// is shared across multiple principals.
722    ///
723    /// Expectations verified:
724    /// - When a VMO is shared among 2 distinct principals (`share_count == 2`), scaled bytes equal
725    ///   `total / 2.0`.
726    /// - Because `share_count > 1`, `committed_private` and `populated_private` are exactly 0 for
727    ///   both sharing principals.
728    #[test]
729    fn test_memory_summary_share_count_calculation() {
730        let mut principals = FxHashMap::default();
731        let mut p1 = make_test_principal(1, "owner1");
732        let mut p2 = make_test_principal(2, "owner2");
733        p1.resources.push(3001);
734        p2.resources.push(3001);
735        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
736        principals.insert(GlobalPrincipalIdentifier::new_for_test(2), p2);
737
738        let mut resources = FxHashMap::default();
739        resources.insert(3001, make_test_vmo_resource(3001, 0, 1000, 2000, vec![(1, 1), (2, 2)]));
740
741        let resource_names = vec![ZXName::from_string_lossy("shared_mem")];
742        let summary = MemorySummary::build(&principals, &resources, &resource_names);
743
744        assert_eq!(summary.principals.len(), 2);
745        for p_sum in &summary.principals {
746            assert_eq!(p_sum.committed_total, 1000);
747            assert_eq!(p_sum.populated_total, 2000);
748            assert_eq!(p_sum.committed_scaled, 500.0);
749            assert_eq!(p_sum.populated_scaled, 1000.0);
750            assert_eq!(p_sum.committed_private, 0);
751            assert_eq!(p_sum.populated_private, 0);
752        }
753    }
754
755    /// What is tested: Aggregation of unclaimed VMOs (VMO resources with an empty claims list) into
756    /// `MemorySummary.unclaimed`.
757    ///
758    /// Expectations verified:
759    /// - A VMO with no attribution claims has its `scaled_populated_bytes` added to `summary.
760    ///   unclaimed`.
761    #[test]
762    fn test_memory_summary_unclaimed_vmos() {
763        let principals = FxHashMap::default();
764        let mut resources = FxHashMap::default();
765        resources.insert(4001, make_test_vmo_resource(4001, 0, 500, 1234, vec![]));
766
767        let resource_names = vec![ZXName::from_string_lossy("unclaimed_vmo")];
768        let summary = MemorySummary::build(&principals, &resources, &resource_names);
769        assert_eq!(summary.unclaimed, 1234);
770    }
771
772    /// What is tested: `compute_share_count` properly deduplicates subjects.
773    #[test]
774    fn test_compute_share_count() {
775        let mut subjects_buf = Vec::new();
776
777        let empty_claims = HashSet::new();
778        assert_eq!(compute_share_count(&empty_claims, &mut subjects_buf), 0);
779
780        let mut single_claim = HashSet::new();
781        single_claim.insert(Claim {
782            source: GlobalPrincipalIdentifier::new_for_test(1),
783            subject: GlobalPrincipalIdentifier::new_for_test(1),
784            claim_type: ClaimType::Direct,
785        });
786        assert_eq!(compute_share_count(&single_claim, &mut subjects_buf), 1);
787
788        let mut two_same_subject = HashSet::new();
789        two_same_subject.insert(Claim {
790            source: GlobalPrincipalIdentifier::new_for_test(1),
791            subject: GlobalPrincipalIdentifier::new_for_test(10),
792            claim_type: ClaimType::Direct,
793        });
794        two_same_subject.insert(Claim {
795            source: GlobalPrincipalIdentifier::new_for_test(2),
796            subject: GlobalPrincipalIdentifier::new_for_test(10),
797            claim_type: ClaimType::Indirect,
798        });
799        assert_eq!(compute_share_count(&two_same_subject, &mut subjects_buf), 1);
800
801        let mut two_diff_subject = HashSet::new();
802        two_diff_subject.insert(Claim {
803            source: GlobalPrincipalIdentifier::new_for_test(1),
804            subject: GlobalPrincipalIdentifier::new_for_test(10),
805            claim_type: ClaimType::Direct,
806        });
807        two_diff_subject.insert(Claim {
808            source: GlobalPrincipalIdentifier::new_for_test(2),
809            subject: GlobalPrincipalIdentifier::new_for_test(20),
810            claim_type: ClaimType::Direct,
811        });
812        assert_eq!(compute_share_count(&two_diff_subject, &mut subjects_buf), 2);
813
814        let mut multi_claims = HashSet::new();
815        multi_claims.insert(Claim {
816            source: GlobalPrincipalIdentifier::new_for_test(1),
817            subject: GlobalPrincipalIdentifier::new_for_test(10),
818            claim_type: ClaimType::Direct,
819        });
820        multi_claims.insert(Claim {
821            source: GlobalPrincipalIdentifier::new_for_test(2),
822            subject: GlobalPrincipalIdentifier::new_for_test(10),
823            claim_type: ClaimType::Indirect,
824        });
825        multi_claims.insert(Claim {
826            source: GlobalPrincipalIdentifier::new_for_test(3),
827            subject: GlobalPrincipalIdentifier::new_for_test(20),
828            claim_type: ClaimType::Direct,
829        });
830        multi_claims.insert(Claim {
831            source: GlobalPrincipalIdentifier::new_for_test(4),
832            subject: GlobalPrincipalIdentifier::new_for_test(30),
833            claim_type: ClaimType::Direct,
834        });
835        assert_eq!(compute_share_count(&multi_claims, &mut subjects_buf), 3);
836    }
837
838    /// What is tested: `MemorySummary::build` scaling when a VMO has multiple claims from the same
839    /// principal as well as distinct principals.
840    ///
841    /// Expectations verified:
842    /// - 3 claims across 2 distinct principals -> `share_count == 2`.
843    /// - Scaled bytes are divided by 2.0.
844    #[test]
845    fn test_memory_summary_share_count_multi_and_duplicate_claims() {
846        let mut principals = FxHashMap::default();
847        let mut p1 = make_test_principal(1, "principal1");
848        let mut p2 = make_test_principal(2, "principal2");
849        p1.resources.push(5001);
850        p2.resources.push(5001);
851        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
852        principals.insert(GlobalPrincipalIdentifier::new_for_test(2), p2);
853
854        let mut resources = FxHashMap::default();
855        // 3 claims: (1, 1), (2, 1), (2, 2) -> subjects: 1, 1, 2 -> unique subjects: 1, 2 -> share_count = 2
856        resources
857            .insert(5001, make_test_vmo_resource(5001, 0, 600, 1200, vec![(1, 1), (2, 1), (2, 2)]));
858
859        let resource_names = vec![ZXName::from_string_lossy("multi_claim_vmo")];
860        let summary = MemorySummary::build(&principals, &resources, &resource_names);
861
862        assert_eq!(summary.principals.len(), 2);
863        for p_sum in &summary.principals {
864            assert_eq!(p_sum.committed_total, 600);
865            assert_eq!(p_sum.populated_total, 1200);
866            assert_eq!(p_sum.committed_scaled, 300.0);
867            assert_eq!(p_sum.populated_scaled, 600.0);
868            assert_eq!(p_sum.committed_private, 0);
869            assert_eq!(p_sum.populated_private, 0);
870        }
871    }
872
873    /// What is tested: `MemorySummary::build` gracefully skips resource IDs in a principal's
874    /// resource list that do not exist in the `resources` map.
875    ///
876    /// Expectations verified:
877    /// - A principal referencing valid resource 5001 and non-existent resource 99999
878    ///   does not panic and attributes only 5001.
879    #[test]
880    fn test_memory_summary_skips_missing_resource_id() {
881        let mut principals = FxHashMap::default();
882        let mut p1 = make_test_principal(1, "principal_with_missing_res");
883        p1.resources.push(5001);
884        p1.resources.push(99999);
885        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
886
887        let mut resources = FxHashMap::default();
888        resources.insert(5001, make_test_vmo_resource(5001, 0, 400, 800, vec![(1, 1)]));
889
890        let resource_names = vec![ZXName::from_string_lossy("valid_vmo")];
891        let summary = MemorySummary::build(&principals, &resources, &resource_names);
892
893        assert_eq!(summary.principals.len(), 1);
894        assert_eq!(summary.principals[0].committed_total, 400);
895        assert_eq!(summary.principals[0].populated_total, 800);
896    }
897}