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::{
7    GlobalPrincipalIdentifier, InflatedPrincipal, InflatedResource, PrincipalType,
8    ResourceReference, ZXName, fplugin_serde,
9};
10use bstr::ByteSlice;
11use core::default::Default;
12use fidl_fuchsia_memory_attribution_plugin_common as fplugin;
13use fplugin::Vmo;
14#[cfg(target_os = "fuchsia")]
15use fuchsia_trace::duration;
16use serde::Serialize;
17use std::collections::{HashMap, HashSet};
18use std::fmt::Display;
19/// Consider that two floats are equals if they differ less than [FLOAT_COMPARISON_EPSILON].
20const FLOAT_COMPARISON_EPSILON: f64 = 1e-10;
21
22#[derive(Debug, Default, PartialEq, Serialize)]
23pub struct ComponentSummaryProfileResult {
24    pub kernel: fplugin_serde::KernelStatistics,
25    pub principals: Vec<PrincipalSummary>,
26    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
27    pub unclaimed: u64,
28    #[serde(with = "fplugin_serde::PerformanceImpactMetricsDef")]
29    pub performance: fplugin::PerformanceImpactMetrics,
30    pub digest: Option<Digest>,
31}
32
33/// Summary view of the memory usage on a device.
34///
35/// This view aggregates the memory usage for each Principal, and, for each Principal, for VMOs
36/// sharing the same name or belonging to the same logical group. This is a view appropriate to
37/// display to developers who want to understand the memory usage of their Principal.
38#[derive(Debug, PartialEq, Serialize)]
39pub struct MemorySummary {
40    pub principals: Vec<PrincipalSummary>,
41    /// Amount, in bytes, of memory that is known but remained unclaimed. Should be equal to zero.
42    pub unclaimed: u64,
43}
44
45impl MemorySummary {
46    pub(crate) fn build(
47        principals: &HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
48        resources: &HashMap<u64, InflatedResource>,
49        resource_names: &Vec<ZXName>,
50    ) -> MemorySummary {
51        #[cfg(target_os = "fuchsia")]
52        duration!(crate::CATEGORY_MEMORY_CAPTURE, c"MemorySummary::build");
53        let mut output = MemorySummary { principals: Default::default(), unclaimed: 0 };
54        for principal in principals.values() {
55            output.principals.push(MemorySummary::build_one_principal(
56                &principal,
57                &principals,
58                &resources,
59                &resource_names,
60            ));
61        }
62
63        output.principals.sort_unstable_by_key(|p| -(p.populated_total as i64));
64
65        let mut unclaimed = 0;
66        for (_, resource) in resources {
67            if resource.claims.is_empty() {
68                match &resource.resource.resource_type {
69                    fplugin::ResourceType::Job(_) | fplugin::ResourceType::Process(_) => {}
70                    fplugin::ResourceType::Vmo(vmo) => {
71                        unclaimed += vmo.scaled_populated_bytes.unwrap();
72                    }
73                    _ => todo!(),
74                }
75            }
76        }
77        output.unclaimed = unclaimed;
78        output
79    }
80
81    fn build_one_principal(
82        principal: &InflatedPrincipal,
83        principals: &HashMap<GlobalPrincipalIdentifier, InflatedPrincipal>,
84        resources: &HashMap<u64, InflatedResource>,
85        resource_names: &Vec<ZXName>,
86    ) -> PrincipalSummary {
87        let mut output = PrincipalSummary {
88            name: principal.name().to_owned(),
89            id: principal.principal.identifier.0.into(),
90            principal_type: match &principal.principal.principal_type {
91                PrincipalType::Runnable => "R",
92                PrincipalType::Part => "P",
93            }
94            .to_owned(),
95            committed_private: 0,
96            committed_scaled: 0.0,
97            committed_total: 0,
98            populated_private: 0,
99            populated_scaled: 0.0,
100            populated_total: 0,
101            attributor: principal
102                .principal
103                .parent
104                .as_ref()
105                .and_then(|p| principals.get(p))
106                .map(|p| p.name().to_owned()),
107            processes: Vec::new(),
108            vmos: HashMap::new(),
109        };
110
111        for resource_id in &principal.resources {
112            if !resources.contains_key(resource_id) {
113                continue;
114            }
115
116            let resource = resources.get(resource_id).unwrap();
117            let share_count = resource
118                .claims
119                .iter()
120                .map(|c| c.subject)
121                .collect::<HashSet<GlobalPrincipalIdentifier>>()
122                .len();
123            match &resource.resource.resource_type {
124                fplugin::ResourceType::Job(_) => todo!(),
125                fplugin::ResourceType::Process(_) => {
126                    output.processes.push(format!(
127                        "{} ({})",
128                        resource_names.get(resource.resource.name_index).unwrap().clone(),
129                        resource.resource.koid
130                    ));
131                }
132                fplugin::ResourceType::Vmo(vmo_info) => {
133                    output.committed_total += vmo_info.total_committed_bytes.unwrap();
134                    output.populated_total += vmo_info.total_populated_bytes.unwrap();
135                    output.committed_scaled +=
136                        vmo_info.scaled_committed_bytes.unwrap() as f64 / share_count as f64;
137                    output.populated_scaled +=
138                        vmo_info.scaled_populated_bytes.unwrap() as f64 / share_count as f64;
139                    if share_count == 1 {
140                        output.committed_private += vmo_info.private_committed_bytes.unwrap();
141                        output.populated_private += vmo_info.private_populated_bytes.unwrap();
142                    }
143                    output
144                        .vmos
145                        .entry(
146                            vmo_name_to_digest_zxname(
147                                &resource_names.get(resource.resource.name_index).unwrap(),
148                            )
149                            .clone(),
150                        )
151                        .or_default()
152                        .merge(vmo_info, share_count);
153                }
154                _ => todo!(),
155            }
156        }
157
158        for (_source, attribution) in &principal.attribution_claims {
159            for resource in &attribution.resources {
160                if let ResourceReference::ProcessMapped {
161                    process: process_mapped,
162                    base: _,
163                    len: _,
164                    hint_skip_handle_table: _,
165                } = resource
166                {
167                    if let Some(process) = resources.get(&process_mapped) {
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    /// 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); 15] = [
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|create):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    ("^bootfs(:.*)?$", "[bootfs]"),
303    ("^restricted_state_vmo:[0-9]*$", "[restricted_state_vmo]"),
304];
305
306/// Returns the name of a VMO category when the name match on of the rules.
307/// This is used for presentation and aggregation.
308pub fn vmo_name_to_digest_name(name: &str) -> &str {
309    static RULES: std::sync::LazyLock<Vec<(regex_lite::Regex, &'static str)>> =
310        std::sync::LazyLock::new(|| {
311            VMO_DIGEST_NAME_MAPPING
312                .iter()
313                .map(|&(pattern, replacement)| {
314                    (regex_lite::Regex::new(pattern).unwrap(), replacement)
315                })
316                .collect()
317        });
318    RULES.iter().find(|(regex, _)| regex.is_match(name.trim())).map_or(name, |rule| rule.1)
319}
320
321pub fn vmo_name_to_digest_zxname(name: &ZXName) -> &ZXName {
322    static RULES: std::sync::LazyLock<Vec<(regex_lite::Regex, ZXName)>> =
323        std::sync::LazyLock::new(|| {
324            VMO_DIGEST_NAME_MAPPING
325                .iter()
326                .map(|&(pattern, replacement)| {
327                    (
328                        regex_lite::Regex::new(pattern).unwrap(),
329                        ZXName::try_from_bytes(replacement.as_bytes()).unwrap(),
330                    )
331                })
332                .collect()
333        });
334    if let Ok(name_str) = name.as_bstr().to_str() {
335        RULES.iter().find(|(regex, _)| regex.is_match(name_str)).map_or(name, |rule| &rule.1)
336    } else {
337        name
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::{Claim, ClaimType, GlobalPrincipalIdentifier, InflatedPrincipal, InflatedResource};
345
346    #[test]
347    fn rename_zx_test() {
348        pretty_assertions::assert_eq!(
349            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("ld.so.1-internal-heap")),
350            &ZXName::from_string_lossy("[process-bootstrap]"),
351        );
352    }
353
354    #[test]
355    fn rename_zx_test_small_name() {
356        // Verify that we can match regular expressions anchored at both ends even when the name is
357        // not taking the full size of a [ZXName].
358        pretty_assertions::assert_eq!(
359            vmo_name_to_digest_zxname(&ZXName::from_string_lossy("blob-1234")),
360            &ZXName::from_string_lossy("[blobs]"),
361        );
362    }
363
364    #[test]
365    fn rename_test() {
366        pretty_assertions::assert_eq!(
367            vmo_name_to_digest_name("ld.so.1-internal-heap"),
368            "[process-bootstrap]"
369        );
370        pretty_assertions::assert_eq!(
371            vmo_name_to_digest_name("stack: msg of 123"),
372            "[process-bootstrap]"
373        );
374        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-123"), "[blobs]");
375        pretty_assertions::assert_eq!(vmo_name_to_digest_name("blob-15e0da8e"), "[blobs]");
376        pretty_assertions::assert_eq!(
377            vmo_name_to_digest_name("inactive-blob-123"),
378            "[inactive blobs]"
379        );
380        pretty_assertions::assert_eq!(vmo_name_to_digest_name("thrd_t:0x123"), "[stacks]");
381        pretty_assertions::assert_eq!(vmo_name_to_digest_name("initial-thread"), "[stacks]");
382        pretty_assertions::assert_eq!(vmo_name_to_digest_name("pthread_t:0x123"), "[stacks]");
383        pretty_assertions::assert_eq!(
384            vmo_name_to_digest_name("pthread_create:0xfa124714"),
385            "[stacks]"
386        );
387        pretty_assertions::assert_eq!(vmo_name_to_digest_name("data456:"), "[data]");
388        pretty_assertions::assert_eq!(vmo_name_to_digest_name("bss456:"), "[bss]");
389        pretty_assertions::assert_eq!(vmo_name_to_digest_name("relro:foobar"), "[relro]");
390        pretty_assertions::assert_eq!(vmo_name_to_digest_name(""), "[unnamed]");
391        pretty_assertions::assert_eq!(vmo_name_to_digest_name("scudo:primary"), "[scudo]");
392        pretty_assertions::assert_eq!(vmo_name_to_digest_name("libfoo.so.1"), "[bootfs-libraries]");
393        pretty_assertions::assert_eq!(vmo_name_to_digest_name("foobar"), "foobar");
394        pretty_assertions::assert_eq!(
395            vmo_name_to_digest_name("stack_and_tls:2331"),
396            "[bionic-stack]"
397        );
398        pretty_assertions::assert_eq!(vmo_name_to_digest_name("ext4!foobar"), "[ext4]");
399        pretty_assertions::assert_eq!(vmo_name_to_digest_name("dalvik-data1234"), "[dalvik]");
400        pretty_assertions::assert_eq!(
401            vmo_name_to_digest_name("restricted_state_vmo:119723"),
402            "[restricted_state_vmo]"
403        );
404    }
405
406    fn make_test_principal(id: u64, name: &str) -> InflatedPrincipal {
407        InflatedPrincipal::new(
408            fplugin::Principal {
409                identifier: Some(fplugin::PrincipalIdentifier { id }),
410                description: Some(fplugin::Description::Component(name.to_owned())),
411                principal_type: Some(fplugin::PrincipalType::Runnable),
412                parent: None,
413                ..Default::default()
414            }
415            .into(),
416        )
417    }
418
419    fn make_test_vmo_resource(
420        koid: u64,
421        name_index: usize,
422        committed: u64,
423        populated: u64,
424        claims: Vec<(u64, u64)>,
425    ) -> InflatedResource {
426        let mut res = InflatedResource::new(
427            fplugin::Resource {
428                koid: Some(koid),
429                name_index: Some(name_index as u64),
430                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
431                    private_committed_bytes: Some(committed),
432                    private_populated_bytes: Some(populated),
433                    scaled_committed_bytes: Some(committed),
434                    scaled_populated_bytes: Some(populated),
435                    total_committed_bytes: Some(committed),
436                    total_populated_bytes: Some(populated),
437                    ..Default::default()
438                })),
439                ..Default::default()
440            }
441            .into(),
442        );
443        for (source, subject) in claims {
444            res.claims.insert(Claim {
445                source: GlobalPrincipalIdentifier::new_for_test(source),
446                subject: GlobalPrincipalIdentifier::new_for_test(subject),
447                claim_type: ClaimType::Direct,
448            });
449        }
450        res
451    }
452
453    /// What is tested: `MemorySummary::build` sorting of `PrincipalSummary` entries by
454    /// `populated_total` in descending order.
455    ///
456    /// Expectations verified:
457    /// - Principals in `summary.principals` are ordered descending by their total populated bytes
458    ///   (`1_000_000_000` -> `500_000_000` -> `100_000_000`).
459    /// - Verifies that large unsigned byte totals are handled correctly without sign-overflow when
460    ///   sorting comparator logic is refactored.
461    #[test]
462    fn test_memory_summary_build_sorting_and_overflow() {
463        let mut principals = HashMap::new();
464        let mut p1 = make_test_principal(1, "small_principal");
465        p1.resources.insert(101);
466        let mut p2 = make_test_principal(2, "large_principal");
467        p2.resources.insert(102);
468        let mut p3 = make_test_principal(3, "medium_principal");
469        p3.resources.insert(103);
470        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
471        principals.insert(GlobalPrincipalIdentifier::new_for_test(2), p2);
472        principals.insert(GlobalPrincipalIdentifier::new_for_test(3), p3);
473
474        let mut resources = HashMap::new();
475        resources
476            .insert(101, make_test_vmo_resource(101, 0, 100_000_000, 100_000_000, vec![(1, 1)]));
477        resources.insert(
478            102,
479            make_test_vmo_resource(102, 1, 1_000_000_000, 1_000_000_000, vec![(2, 2)]),
480        );
481        resources
482            .insert(103, make_test_vmo_resource(103, 2, 500_000_000, 500_000_000, vec![(3, 3)]));
483
484        let resource_names = vec![
485            ZXName::from_string_lossy("vmo_1"),
486            ZXName::from_string_lossy("vmo_2"),
487            ZXName::from_string_lossy("vmo_3"),
488        ];
489
490        let summary = MemorySummary::build(&principals, &resources, &resource_names);
491        assert_eq!(summary.principals.len(), 3);
492        assert_eq!(summary.principals[0].name, "large_principal");
493        assert_eq!(summary.principals[0].populated_total, 1_000_000_000);
494        assert_eq!(summary.principals[1].name, "medium_principal");
495        assert_eq!(summary.principals[1].populated_total, 500_000_000);
496        assert_eq!(summary.principals[2].name, "small_principal");
497        assert_eq!(summary.principals[2].populated_total, 100_000_000);
498    }
499
500    /// What is tested: VMO digest aggregation and merging when they have the same name.
501    ///
502    /// Expectations verified:
503    /// - When multiple VMOs owned by a principal have distinct names ("blob-1111", "blob-2222")
504    ///   that digest to the same bucket ("[blobs]"), they are merged into a single `VmoSummary`
505    ///   entry.
506    /// - Verifies `vmo_summary.count == 2` and that all committed/populated byte metrics (total and
507    ///   private) are accurately summed across the aggregated VMOs.
508    #[test]
509    fn test_memory_summary_vmo_digest_aggregation() {
510        let mut principals = HashMap::new();
511        let mut p1 = make_test_principal(1, "blob_owner");
512        p1.resources.insert(1001);
513        p1.resources.insert(1002);
514        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
515
516        let mut resources = HashMap::new();
517        resources.insert(1001, make_test_vmo_resource(1001, 0, 100, 200, vec![(1, 1)]));
518        resources.insert(1002, make_test_vmo_resource(1002, 1, 300, 400, vec![(1, 1)]));
519
520        let resource_names =
521            vec![ZXName::from_string_lossy("blob-1111"), ZXName::from_string_lossy("blob-2222")];
522
523        let summary = MemorySummary::build(&principals, &resources, &resource_names);
524        assert_eq!(summary.principals.len(), 1);
525        let p_summary = &summary.principals[0];
526        assert_eq!(p_summary.vmos.len(), 1);
527
528        let blob_digest = ZXName::from_string_lossy("[blobs]");
529        let vmo_summary = p_summary.vmos.get(&blob_digest).expect("Should aggregate under [blobs]");
530        assert_eq!(vmo_summary.count, 2);
531        assert_eq!(vmo_summary.committed_total, 400);
532        assert_eq!(vmo_summary.populated_total, 600);
533        assert_eq!(vmo_summary.committed_private, 400);
534        assert_eq!(vmo_summary.populated_private, 600);
535    }
536
537    /// What is tested: Process formatting and alphabetical sorting of process strings in
538    /// `PrincipalSummary.processes`.
539    ///
540    /// Expectations verified:
541    /// - Multiple distinct process resources attributed to a principal are formatted as `"name (koid)"`
542    ///   and sorted alphabetically (`"alpha_process (2002)"` before `"zeta_process (2001)"`).
543    #[test]
544    fn test_memory_summary_process_formatting_and_sorting() {
545        let mut principals = HashMap::new();
546        let mut p1 = make_test_principal(1, "proc_owner");
547        p1.resources.insert(2001);
548        p1.resources.insert(2002);
549        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
550
551        let mut resources = HashMap::new();
552        let r1 = InflatedResource::new(
553            fplugin::Resource {
554                koid: Some(2001),
555                name_index: Some(0),
556                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
557                    vmos: Some(vec![]),
558                    mappings: None,
559                    ..Default::default()
560                })),
561                ..Default::default()
562            }
563            .into(),
564        );
565        let r2 = InflatedResource::new(
566            fplugin::Resource {
567                koid: Some(2002),
568                name_index: Some(1),
569                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
570                    vmos: Some(vec![]),
571                    mappings: None,
572                    ..Default::default()
573                })),
574                ..Default::default()
575            }
576            .into(),
577        );
578        resources.insert(2001, r1);
579        resources.insert(2002, r2);
580
581        let resource_names = vec![
582            ZXName::from_string_lossy("zeta_process"),
583            ZXName::from_string_lossy("alpha_process"),
584        ];
585
586        let summary = MemorySummary::build(&principals, &resources, &resource_names);
587        assert_eq!(summary.principals.len(), 1);
588        assert_eq!(
589            summary.principals[0].processes,
590            vec!["alpha_process (2002)".to_owned(), "zeta_process (2001)".to_owned()]
591        );
592    }
593
594    /// What is tested: `share_count` division and private vs. scaled memory calculations when a VMO
595    /// is shared across multiple principals.
596    ///
597    /// Expectations verified:
598    /// - When a VMO is shared among 2 distinct principals (`share_count == 2`), scaled bytes equal
599    ///   `total / 2.0`.
600    /// - Because `share_count > 1`, `committed_private` and `populated_private` are exactly 0 for
601    ///   both sharing principals.
602    #[test]
603    fn test_memory_summary_share_count_calculation() {
604        let mut principals = HashMap::new();
605        let mut p1 = make_test_principal(1, "owner1");
606        let mut p2 = make_test_principal(2, "owner2");
607        p1.resources.insert(3001);
608        p2.resources.insert(3001);
609        principals.insert(GlobalPrincipalIdentifier::new_for_test(1), p1);
610        principals.insert(GlobalPrincipalIdentifier::new_for_test(2), p2);
611
612        let mut resources = HashMap::new();
613        resources.insert(3001, make_test_vmo_resource(3001, 0, 1000, 2000, vec![(1, 1), (2, 2)]));
614
615        let resource_names = vec![ZXName::from_string_lossy("shared_mem")];
616        let summary = MemorySummary::build(&principals, &resources, &resource_names);
617
618        assert_eq!(summary.principals.len(), 2);
619        for p_sum in &summary.principals {
620            assert_eq!(p_sum.committed_total, 1000);
621            assert_eq!(p_sum.populated_total, 2000);
622            assert_eq!(p_sum.committed_scaled, 500.0);
623            assert_eq!(p_sum.populated_scaled, 1000.0);
624            assert_eq!(p_sum.committed_private, 0);
625            assert_eq!(p_sum.populated_private, 0);
626        }
627    }
628
629    /// What is tested: Aggregation of unclaimed VMOs (VMO resources with an empty claims list) into
630    /// `MemorySummary.unclaimed`.
631    ///
632    /// Expectations verified:
633    /// - A VMO with no attribution claims has its `scaled_populated_bytes` added to `summary.
634    ///   unclaimed`.
635    #[test]
636    fn test_memory_summary_unclaimed_vmos() {
637        let principals = HashMap::new();
638        let mut resources = HashMap::new();
639        resources.insert(4001, make_test_vmo_resource(4001, 0, 500, 1234, vec![]));
640
641        let resource_names = vec![ZXName::from_string_lossy("unclaimed_vmo")];
642        let summary = MemorySummary::build(&principals, &resources, &resource_names);
643        assert_eq!(summary.unclaimed, 1234);
644    }
645}