attribution_processing/
lib.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 core::cell::RefCell;
6use core::convert::Into;
7use fidl_fuchsia_memory_attribution_plugin as fplugin;
8use futures::future::BoxFuture;
9use serde::Serialize;
10use std::collections::{HashMap, HashSet};
11use summary::MemorySummary;
12
13mod name;
14
15pub mod digest;
16pub mod fkernel_serde;
17pub mod fplugin_serde;
18pub use name::ZXName;
19pub mod summary;
20
21#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, Serialize)]
22pub struct PrincipalIdentifier(pub u64);
23
24impl From<fplugin::PrincipalIdentifier> for PrincipalIdentifier {
25    fn from(value: fplugin::PrincipalIdentifier) -> Self {
26        PrincipalIdentifier(value.id)
27    }
28}
29
30impl Into<fplugin::PrincipalIdentifier> for PrincipalIdentifier {
31    fn into(self) -> fplugin::PrincipalIdentifier {
32        fplugin::PrincipalIdentifier { id: self.0 }
33    }
34}
35
36/// User-understandable description of a Principal
37#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
38pub enum PrincipalDescription {
39    Component(String),
40    Part(String),
41}
42
43impl From<fplugin::Description> for PrincipalDescription {
44    fn from(value: fplugin::Description) -> Self {
45        match value {
46            fplugin::Description::Component(s) => PrincipalDescription::Component(s),
47            fplugin::Description::Part(s) => PrincipalDescription::Part(s),
48            _ => unreachable!(),
49        }
50    }
51}
52
53impl Into<fplugin::Description> for PrincipalDescription {
54    fn into(self) -> fplugin::Description {
55        match self {
56            PrincipalDescription::Component(s) => fplugin::Description::Component(s),
57            PrincipalDescription::Part(s) => fplugin::Description::Part(s),
58        }
59    }
60}
61
62/// Type of a principal.
63#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize)]
64pub enum PrincipalType {
65    Runnable,
66    Part,
67}
68
69impl From<fplugin::PrincipalType> for PrincipalType {
70    fn from(value: fplugin::PrincipalType) -> Self {
71        match value {
72            fplugin::PrincipalType::Runnable => PrincipalType::Runnable,
73            fplugin::PrincipalType::Part => PrincipalType::Part,
74            _ => unreachable!(),
75        }
76    }
77}
78
79impl Into<fplugin::PrincipalType> for PrincipalType {
80    fn into(self) -> fplugin::PrincipalType {
81        match self {
82            PrincipalType::Runnable => fplugin::PrincipalType::Runnable,
83            PrincipalType::Part => fplugin::PrincipalType::Part,
84        }
85    }
86}
87
88#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
89/// A Principal, that can use and claim memory.
90pub struct Principal {
91    // These fields are initialized from [fplugin::Principal].
92    pub identifier: PrincipalIdentifier,
93    pub description: PrincipalDescription,
94    pub principal_type: PrincipalType,
95
96    /// Principal that declared this Principal. None if this Principal is at the root of the system
97    /// hierarchy (the root principal is a statically defined Principal encompassing all resources
98    /// on the system). The Principal hierarchy forms a tree (no cycles).
99    pub parent: Option<PrincipalIdentifier>,
100}
101
102/// Creates a new [Principal] from a [fplugin::Principal] object. The [Principal] object will
103/// contain all the data from [fplugin::Principal] and have its other fields initialized empty.
104impl From<fplugin::Principal> for Principal {
105    fn from(value: fplugin::Principal) -> Self {
106        Principal {
107            identifier: value.identifier.unwrap().into(),
108            description: value.description.unwrap().into(),
109            principal_type: value.principal_type.unwrap().into(),
110            parent: value.parent.map(Into::into),
111        }
112    }
113}
114
115impl Into<fplugin::Principal> for Principal {
116    fn into(self) -> fplugin::Principal {
117        fplugin::Principal {
118            identifier: Some(self.identifier.into()),
119            description: Some(self.description.into()),
120            principal_type: Some(self.principal_type.into()),
121            parent: self.parent.map(Into::into),
122            ..Default::default()
123        }
124    }
125}
126
127/// A Principal, with its attribution claims and resources.
128#[derive(Serialize)]
129pub struct InflatedPrincipal {
130    /// The principal definition.
131    principal: Principal,
132
133    // These fields are computed from the rest of the [fplugin::Snapshot] data.
134    /// Map of attribution claims made about this Principal (this Principal is the subject of the
135    /// claim). This map goes from the source Principal to the attribution claim.
136    attribution_claims: HashMap<PrincipalIdentifier, Attribution>,
137    /// KOIDs of resources attributed to this principal, after resolution of sharing and
138    /// reattributions.
139    resources: HashSet<u64>,
140}
141
142impl InflatedPrincipal {
143    fn new(principal: Principal) -> InflatedPrincipal {
144        InflatedPrincipal {
145            principal,
146            attribution_claims: Default::default(),
147            resources: Default::default(),
148        }
149    }
150}
151
152impl InflatedPrincipal {
153    fn name(&self) -> &str {
154        match &self.principal.description {
155            PrincipalDescription::Component(component_name) => component_name,
156            PrincipalDescription::Part(part_name) => part_name,
157        }
158    }
159}
160
161/// Type of the claim, that changes depending on how the claim was created.
162#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
163pub enum ClaimType {
164    /// A principal claimed this resource directly.
165    Direct,
166    /// A principal claimed a resource that contains this resource (e.g. a process containing a
167    /// VMO).
168    Indirect,
169    /// A principal claimed a child of this resource (e.g. a copy-on-write VMO child of this VMO).
170    Child,
171}
172
173#[derive(Clone, Copy, PartialEq, Eq, Hash)]
174pub struct Koid(u64);
175
176impl From<u64> for Koid {
177    fn from(value: u64) -> Self {
178        Koid(value)
179    }
180}
181
182/// Attribution claim of a Principal on a Resource.
183///
184/// Note that this object is slightly different from the [fplugin::Attribution] object: it goes from
185/// a Resource to a Principal, and covers also indirect attribution claims of sub- and parent
186/// resources.
187#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize)]
188pub struct Claim {
189    /// Principal to which the resources are attributed.
190    subject: PrincipalIdentifier,
191    /// Principal making the attribution claim.
192    source: PrincipalIdentifier,
193    claim_type: ClaimType,
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize)]
197pub struct Resource {
198    pub koid: u64,
199    pub name_index: usize,
200    #[serde(with = "fplugin_serde::ResourceTypeDef")]
201    pub resource_type: fplugin::ResourceType,
202}
203
204impl From<fplugin::Resource> for Resource {
205    fn from(value: fplugin::Resource) -> Self {
206        Resource {
207            koid: value.koid.unwrap(),
208            name_index: value.name_index.unwrap() as usize,
209            resource_type: value.resource_type.unwrap(),
210        }
211    }
212}
213
214impl Into<fplugin::Resource> for Resource {
215    fn into(self) -> fplugin::Resource {
216        fplugin::Resource {
217            koid: Some(self.koid),
218            name_index: Some(self.name_index as u64),
219            resource_type: Some(self.resource_type),
220            ..Default::default()
221        }
222    }
223}
224
225// Claim with a boolean tag, to help find leaves in the claim assignment graph.
226pub struct TaggedClaim(Claim, bool);
227
228#[derive(Debug, Serialize)]
229pub struct InflatedResource {
230    resource: Resource,
231    claims: HashSet<Claim>,
232}
233
234impl InflatedResource {
235    fn new(resource: Resource) -> InflatedResource {
236        InflatedResource { resource, claims: Default::default() }
237    }
238
239    fn children(&self) -> Vec<u64> {
240        match &self.resource.resource_type {
241            fplugin::ResourceType::Job(job) => {
242                let mut r: Vec<u64> = job.child_jobs.iter().flatten().map(|k| *k).collect();
243                r.extend(job.processes.iter().flatten().map(|k| *k));
244                r
245            }
246            fplugin::ResourceType::Process(process) => {
247                process.vmos.iter().flatten().map(|k| *k).collect()
248            }
249            fplugin::ResourceType::Vmo(_) => Vec::new(),
250            _ => todo!(),
251        }
252    }
253
254    /// Process the claims made on this resource to disambiguate between reassignment and sharing.
255    ///
256    /// [process_claims] looks at each claim made on this resource, and removes claims that are
257    /// reassigned by another claim. This happens if a principal A gives a resource to principal B,
258    /// and B then gives it to principal C. However, if two independent principals claim this
259    /// resource, then both their claims are kept.
260    /// This is done by:
261    /// (i)  preserving all self claims, and
262    /// (ii) preserving only leaves in the DAG following claim.source to claim.subject edges.
263    fn process_claims(&mut self) {
264        let mut claims_by_source: HashMap<PrincipalIdentifier, RefCell<Vec<TaggedClaim>>> =
265            Default::default();
266        let mut self_claims = Vec::new();
267
268        for claim in self.claims.iter().cloned() {
269            if claim.source == claim.subject {
270                // Self claims are taken out of the graph because they are never transferred. This
271                // is to implement sharing.
272                self_claims.push(claim);
273            } else {
274                claims_by_source
275                    .entry(claim.source)
276                    .or_default()
277                    .borrow_mut()
278                    .push(TaggedClaim(claim, false));
279            }
280        }
281
282        self.claims = self_claims.into_iter().collect();
283        for (_, claimlist_refcell) in claims_by_source.iter() {
284            let mut claimlist = claimlist_refcell.borrow_mut();
285            for tagged_claim in claimlist.iter_mut() {
286                self.claims.extend(
287                    InflatedResource::process_claims_recursive(tagged_claim, &claims_by_source)
288                        .into_iter(),
289                );
290            }
291        }
292    }
293
294    /// Recursively look at claims to find the ones that are not reassigned.
295    fn process_claims_recursive(
296        tagged_claim: &mut TaggedClaim,
297        claims: &HashMap<PrincipalIdentifier, RefCell<Vec<TaggedClaim>>>,
298    ) -> Vec<Claim> {
299        let claim = match tagged_claim.1 {
300            true => {
301                // We have visited this claim already, we can skip.
302                return vec![];
303            }
304            false => {
305                // We tag visited claims, so we don't visit them again.
306                tagged_claim.1 = true;
307                tagged_claim.0
308            }
309        };
310        let subject = &claim.subject;
311        // We find if this claim has been reassigned.
312        let mut subject_claims = match claims.get(subject) {
313            Some(value_ref) => {
314                // [subject_claims] mutable borrow is held when recursing below, and
315                // [RefCell::try_borrow_mut] returns an error if called when a mutable borrow is
316                // already held. This ensures an error will be thrown at runtime if there is a
317                // cycle.
318                value_ref.try_borrow_mut().expect("Claims form a cycle, this is not supported")
319            }
320            None => {
321                // The claim is not reassigned, we keep the claim.
322                return vec![claim];
323            }
324        };
325        let mut leaves = vec![];
326        for subject_claim in subject_claims.iter_mut() {
327            leaves.append(&mut InflatedResource::process_claims_recursive(subject_claim, claims));
328        }
329        leaves
330    }
331}
332
333#[derive(Clone, Serialize)]
334/// Holds the list of resources attributed to a Principal (subject) by another Principal (source).
335pub struct Attribution {
336    /// Principal making the attribution claim.
337    pub source: PrincipalIdentifier,
338    /// Principal to which the resources are attributed.
339    pub subject: PrincipalIdentifier,
340    /// List of resources attributed to `subject` by `source`.
341    pub resources: Vec<ResourceReference>,
342}
343
344impl From<fplugin::Attribution> for Attribution {
345    fn from(value: fplugin::Attribution) -> Attribution {
346        Attribution {
347            source: value.source.unwrap().into(),
348            subject: value.subject.unwrap().into(),
349            resources: value.resources.unwrap().into_iter().map(|r| r.into()).collect(),
350        }
351    }
352}
353
354impl Into<fplugin::Attribution> for Attribution {
355    fn into(self) -> fplugin::Attribution {
356        fplugin::Attribution {
357            source: Some(self.source.into()),
358            subject: Some(self.subject.into()),
359            resources: Some(self.resources.into_iter().map(|r| r.into()).collect()),
360            ..Default::default()
361        }
362    }
363}
364
365#[derive(Clone, Copy, Serialize)]
366/// References a kernel [`Resource`], or some subset of a [`Resource`] (such as a part of a process
367/// address space).
368pub enum ResourceReference {
369    /// Identifies a kernel object whose memory is being attributed.
370    ///
371    /// Refers to all memory held by VMOs reachable from the object
372    /// (currently a Job, Process or VMO).
373    KernelObject(u64),
374
375    /// Identifies a part of a process address space.
376    ProcessMapped {
377        /// The KOID of the process that this VMAR lives in.
378        process: u64,
379
380        /// Base address of the VMAR.
381        base: u64,
382
383        /// Length of the VMAR.
384        len: u64,
385    },
386}
387
388impl From<fplugin::ResourceReference> for ResourceReference {
389    fn from(value: fplugin::ResourceReference) -> ResourceReference {
390        match value {
391            fidl_fuchsia_memory_attribution_plugin::ResourceReference::KernelObject(ko) => {
392                ResourceReference::KernelObject(ko)
393            }
394            fidl_fuchsia_memory_attribution_plugin::ResourceReference::ProcessMapped(
395                fplugin::ProcessMapped { process, base, len },
396            ) => ResourceReference::ProcessMapped { process, base, len },
397            _ => unimplemented!(),
398        }
399    }
400}
401
402impl Into<fplugin::ResourceReference> for ResourceReference {
403    fn into(self) -> fplugin::ResourceReference {
404        match self {
405            ResourceReference::KernelObject(ko) => {
406                fidl_fuchsia_memory_attribution_plugin::ResourceReference::KernelObject(ko)
407            }
408            ResourceReference::ProcessMapped { process, base, len } => {
409                fidl_fuchsia_memory_attribution_plugin::ResourceReference::ProcessMapped(
410                    fplugin::ProcessMapped { process, base, len },
411                )
412            }
413        }
414    }
415}
416
417/// Capture of the current memory usage of a device, as retrieved through the memory attribution
418/// protocol. In this object, memory attribution is not resolved.
419pub struct AttributionData {
420    pub principals_vec: Vec<Principal>,
421    pub resources_vec: Vec<Resource>,
422    pub resource_names: Vec<ZXName>,
423    pub attributions: Vec<Attribution>,
424}
425
426// TODO(b/411121120): Use zx::Koid when available on host code.
427/// The post-order traversal of the Jobs->Process->VMOs tree guarantees that when
428/// a visitor processes a Process, it has already seen its associated VMOs.
429pub trait ResourcesVisitor {
430    fn on_job(
431        &mut self,
432        job_koid: zx_types::zx_koid_t,
433        job_name: &ZXName,
434        job: fplugin::Job,
435    ) -> Result<(), zx_status::Status>;
436    fn on_process(
437        &mut self,
438        process_koid: zx_types::zx_koid_t,
439        process_name: &ZXName,
440        process: fplugin::Process,
441    ) -> Result<(), zx_status::Status>;
442    fn on_vmo(
443        &mut self,
444        vmo_koid: zx_types::zx_koid_t,
445        vmo_name: &ZXName,
446        vmo: fplugin::Vmo,
447    ) -> Result<(), zx_status::Status>;
448}
449
450pub trait AttributionDataProvider: Send + Sync {
451    /// Collects and returns a structure with all memory resources and attribution specifications.
452    fn get_attribution_data(&self) -> BoxFuture<'_, Result<AttributionData, anyhow::Error>>;
453    /// Enumerates Jobs, Processes and VMOs and call back the visitor.
454    fn for_each_resource(&self, visitor: &mut impl ResourcesVisitor) -> Result<(), anyhow::Error>;
455}
456
457/// Processed snapshot of the memory usage of a device, with attribution of memory resources to
458/// Principals resolved.
459pub struct ProcessedAttributionData {
460    pub principals: HashMap<PrincipalIdentifier, RefCell<InflatedPrincipal>>,
461    pub resources: HashMap<u64, RefCell<InflatedResource>>,
462    pub resource_names: Vec<ZXName>,
463}
464
465impl ProcessedAttributionData {
466    fn new(
467        principals: HashMap<PrincipalIdentifier, RefCell<InflatedPrincipal>>,
468        resources: HashMap<u64, RefCell<InflatedResource>>,
469        resource_names: Vec<ZXName>,
470    ) -> Self {
471        Self { principals, resources, resource_names }
472    }
473
474    /// Create a summary view of the memory attribution_data. See [MemorySummary] for details.
475    pub fn summary(&self) -> MemorySummary {
476        MemorySummary::build(&self.principals, &self.resources, &self.resource_names)
477    }
478}
479
480/// Process data from a [AttributionData] to resolve attribution claims.
481pub fn attribute_vmos(attribution_data: AttributionData) -> ProcessedAttributionData {
482    // Map from moniker token ID to Principal struct.
483    let principals: HashMap<PrincipalIdentifier, RefCell<InflatedPrincipal>> = attribution_data
484        .principals_vec
485        .into_iter()
486        .map(|p| (p.identifier.clone(), RefCell::new(InflatedPrincipal::new(p))))
487        .collect();
488
489    // Map from kernel resource koid to Resource struct.
490    let mut resources: HashMap<u64, RefCell<InflatedResource>> = attribution_data
491        .resources_vec
492        .into_iter()
493        .map(|r| (r.koid, RefCell::new(InflatedResource::new(r))))
494        .collect();
495
496    // Add direct claims to resources.
497    for attribution in attribution_data.attributions {
498        principals.get(&attribution.subject.clone().into()).map(|p| {
499            p.borrow_mut().attribution_claims.insert(attribution.source.into(), attribution.clone())
500        });
501        for resource in attribution.resources {
502            match resource {
503                ResourceReference::KernelObject(koid) => {
504                    if !resources.contains_key(&koid) {
505                        continue;
506                    }
507                    resources.get_mut(&koid).unwrap().get_mut().claims.insert(Claim {
508                        source: attribution.source.into(),
509                        subject: attribution.subject.into(),
510                        claim_type: ClaimType::Direct,
511                    });
512                }
513                ResourceReference::ProcessMapped { process, base, len } => {
514                    if !resources.contains_key(&process) {
515                        continue;
516                    }
517                    let mut matched_vmos = Vec::new();
518                    if let fplugin::ResourceType::Process(process_data) =
519                        &resources.get(&process).unwrap().borrow().resource.resource_type
520                    {
521                        for mapping in process_data.mappings.iter().flatten() {
522                            // We consider an entire VMO to be matched if it has a mapping
523                            // within the claimed region.
524                            if mapping.address_base.unwrap() >= base
525                                && mapping.address_base.unwrap() + mapping.size.unwrap()
526                                    <= base + len
527                            {
528                                matched_vmos.push(mapping.vmo.unwrap());
529                            }
530                        }
531                    }
532                    for vmo_koid in matched_vmos {
533                        match resources.get_mut(&vmo_koid) {
534                            Some(resource) => {
535                                resource.get_mut().claims.insert(Claim {
536                                    source: attribution.source.into(),
537                                    subject: attribution.subject.into(),
538                                    claim_type: ClaimType::Direct,
539                                });
540                            }
541                            None => {
542                                // The VMO is unknown. This can happen when a VMO is created between
543                                // the collection of the list of VMOs and the collection of the
544                                // process mappings.
545                            }
546                        }
547                    }
548                }
549            }
550        }
551    }
552
553    // Propagate claims. We propagate direct claims to child resources recursively until we hit a
554    // resource that is directly claimed: this is because we consider that attributors deeper in the
555    // principal hierarchy will not attribute resources higher in the resource hierarchy than the
556    // ones attributed by their ancestors (ie. attribution is always more precise as we go deeper).
557    for (_, resource_refcell) in &resources {
558        let resource = resource_refcell.borrow_mut();
559        // Extract the list of direct claims to propagate.
560        let direct_claims: Vec<&Claim> = resource
561            .claims
562            .iter()
563            .filter(|claim| match claim.claim_type {
564                ClaimType::Direct => true,
565                _ => false,
566            })
567            .collect();
568
569        if direct_claims.is_empty() {
570            // There is no direct claim to propagate, we can skip this resource.
571            continue;
572        }
573
574        let propagated_claims: Vec<Claim> = direct_claims
575            .into_iter()
576            .map(|claim| Claim {
577                source: claim.source,
578                subject: claim.subject,
579                claim_type: ClaimType::Indirect,
580            })
581            .collect();
582        let mut frontier = Vec::new();
583        frontier.extend(resource.children());
584        while !frontier.is_empty() {
585            let child = frontier.pop().unwrap();
586            let mut child_resource = match resources.get(&child) {
587                Some(resource) => resource.borrow_mut(),
588                None => {
589                    // This can happen if a resource is created or disappears while we were
590                    // collecting information about all the resources in the system. This should
591                    // remain a rare event.
592                    continue;
593                }
594            };
595            if child_resource.claims.iter().any(|c| c.claim_type == ClaimType::Direct) {
596                // If there is a direct claim on the resource, don't propagate.
597                continue;
598            }
599            child_resource.claims.extend(propagated_claims.clone().iter());
600            frontier.extend(child_resource.children().iter());
601        }
602    }
603
604    for (_, resource_refcell) in &resources {
605        let mut resource = resource_refcell.borrow_mut();
606        resource.process_claims();
607    }
608
609    // Push claimed resources to principals. We are interested in VMOs as the VMOs are the resources
610    // actually holding memory. We also keep track of the process to display its name in the output.
611    for (resource_id, resource_refcell) in &resources {
612        let resource = resource_refcell.borrow();
613        if let fplugin::ResourceType::Vmo(vmo) = &resource.resource.resource_type {
614            let mut ancestors = vec![*resource_id];
615            // VMOs created by reference don't behave like COW-clones; their byte count is always
616            // zero, and all pages are attributed to their parent. This is not what we want here,
617            // as we prefer to acknowledge that the pages are shared between the parent and its
618            // children. We currently don't have a way to formally distinguish between VMOs created
619            // by reference and the other VMOs, so we use the heuristic of checking that they
620            // report they are always empty.
621            if vmo.total_populated_bytes.unwrap_or_default() == 0 {
622                let mut current_parent = vmo.parent;
623                // Add the parents of a VMO as "Child" claims. This is done so that slices of VMOs,
624                // with possibly no memory of their own, get attributed the resources of their
625                // parent.
626                while let Some(parent_koid) = current_parent {
627                    if parent_koid == 0 {
628                        panic!("Parent is not None but 0.");
629                    }
630                    ancestors.push(parent_koid);
631                    let mut current_resource = match resources.get(&parent_koid) {
632                        Some(res) => res.borrow_mut(),
633                        None => break,
634                    };
635                    current_resource.claims.extend(resource.claims.iter().map(|c| Claim {
636                        subject: c.subject,
637                        source: c.source,
638                        claim_type: ClaimType::Child,
639                    }));
640                    current_parent = match &current_resource.resource.resource_type {
641                        fplugin::ResourceType::Job(_) => panic!("This should not happen"),
642                        fplugin::ResourceType::Process(_) => panic!("This should not happen"),
643                        fplugin::ResourceType::Vmo(current_vmo) => current_vmo.parent,
644                        _ => unimplemented!(),
645                    };
646                }
647            }
648
649            for claim in &resource.claims {
650                principals
651                    .get(&claim.subject)
652                    .unwrap()
653                    .borrow_mut()
654                    .resources
655                    .extend(ancestors.iter());
656            }
657        } else if let fplugin::ResourceType::Process(_) = &resource.resource.resource_type {
658            for claim in &resource.claims {
659                principals
660                    .get(&claim.subject)
661                    .unwrap()
662                    .borrow_mut()
663                    .resources
664                    .insert(resource.resource.koid);
665            }
666        }
667    }
668
669    ProcessedAttributionData::new(principals, resources, attribution_data.resource_names)
670}
671
672pub mod testing {
673    use crate::{AttributionData, AttributionDataProvider, Resource, ResourcesVisitor};
674    use fidl_fuchsia_memory_attribution_plugin::ResourceType;
675    use futures::future::{ready, BoxFuture};
676
677    pub struct FakeAttributionDataProvider {
678        pub attribution_data: AttributionData,
679    }
680
681    impl AttributionDataProvider for FakeAttributionDataProvider {
682        fn get_attribution_data(&self) -> BoxFuture<'_, Result<AttributionData, anyhow::Error>> {
683            Box::pin(ready(Ok(AttributionData {
684                principals_vec: self.attribution_data.principals_vec.clone(),
685                resources_vec: self.attribution_data.resources_vec.clone(),
686                resource_names: self.attribution_data.resource_names.clone(),
687                attributions: self.attribution_data.attributions.clone(),
688            })))
689        }
690
691        fn for_each_resource(
692            &self,
693            visitor: &mut impl ResourcesVisitor,
694        ) -> Result<(), anyhow::Error> {
695            for resource in &self.attribution_data.resources_vec {
696                if let Resource {
697                    koid, name_index, resource_type: ResourceType::Vmo(vmo), ..
698                } = resource
699                {
700                    visitor.on_vmo(
701                        *koid,
702                        &self.attribution_data.resource_names[*name_index],
703                        vmo.clone(),
704                    )?;
705                }
706            }
707            for resource in &self.attribution_data.resources_vec {
708                if let Resource {
709                    koid,
710                    name_index,
711                    resource_type: ResourceType::Process(process),
712                    ..
713                } = resource
714                {
715                    visitor.on_process(
716                        *koid,
717                        &self.attribution_data.resource_names[*name_index],
718                        process.clone(),
719                    )?;
720                }
721            }
722            for resource in &self.attribution_data.resources_vec {
723                if let Resource {
724                    koid, name_index, resource_type: ResourceType::Job(job), ..
725                } = resource
726                {
727                    visitor.on_job(
728                        *koid,
729                        &self.attribution_data.resource_names[*name_index],
730                        job.clone(),
731                    )?;
732                }
733            }
734            Ok(())
735        }
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use std::collections::HashMap;
743    use summary::{PrincipalSummary, VmoSummary};
744
745    #[test]
746    fn test_gather_resources() {
747        // Create a fake snapshot with 4 principals:
748        // root (0)
749        //  - runner (1)
750        //    - component 3 (3)
751        //  - component 2 (2)
752        //
753        // and the following job/process/vmo hierarchy:
754        // root_job (1000)
755        //  * root_process (1001)
756        //    . root_vmo (1002)
757        //    . shared_vmo (1003)
758        //  - runner_job (1004)
759        //    * runner_process (1005)
760        //      . runner_vmo (1006)
761        //      . component_vmo (1007)
762        //      . component_vmo2 (1012)
763        //      . component_vmo3 (1013)
764        //  - component_2_job (1008)
765        //    * 2_process (1009)
766        //      . 2_vmo (1010)
767        //      . shared_vmo (1003)
768        // And an additional parent VMO for 2_vmo, 2_vmo_parent (1011).
769
770        let resource_names = vec![
771            ZXName::from_string_lossy("root_job"),
772            ZXName::from_string_lossy("root_process"),
773            ZXName::from_string_lossy("root_vmo"),
774            ZXName::from_string_lossy("shared_vmo"),
775            ZXName::from_string_lossy("runner_job"),
776            ZXName::from_string_lossy("runner_process"),
777            ZXName::from_string_lossy("runner_vmo"),
778            ZXName::from_string_lossy("component_vmo"),
779            ZXName::from_string_lossy("component_2_job"),
780            ZXName::from_string_lossy("2_process"),
781            ZXName::from_string_lossy("2_vmo"),
782            ZXName::from_string_lossy("2_vmo_parent"),
783            ZXName::from_string_lossy("component_vmo_mapped"),
784            ZXName::from_string_lossy("component_vmo_mapped2"),
785        ];
786
787        let attributions = vec![
788            fplugin::Attribution {
789                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
790                subject: Some(fplugin::PrincipalIdentifier { id: 0 }),
791                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
792                ..Default::default()
793            },
794            fplugin::Attribution {
795                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
796                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
797                resources: Some(vec![fplugin::ResourceReference::KernelObject(1004)]),
798                ..Default::default()
799            },
800            fplugin::Attribution {
801                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
802                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
803                resources: Some(vec![fplugin::ResourceReference::KernelObject(1008)]),
804                ..Default::default()
805            },
806            fplugin::Attribution {
807                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
808                subject: Some(fplugin::PrincipalIdentifier { id: 3 }),
809                resources: Some(vec![
810                    fplugin::ResourceReference::KernelObject(1007),
811                    fplugin::ResourceReference::ProcessMapped(fplugin::ProcessMapped {
812                        process: 1005,
813                        base: 1024,
814                        len: 1024,
815                    }),
816                ]),
817                ..Default::default()
818            },
819        ]
820        .into_iter()
821        .map(|a| a.into())
822        .collect();
823
824        let principals = vec![
825            fplugin::Principal {
826                identifier: Some(fplugin::PrincipalIdentifier { id: 0 }),
827                description: Some(fplugin::Description::Component("root".to_owned())),
828                principal_type: Some(fplugin::PrincipalType::Runnable),
829                parent: None,
830                ..Default::default()
831            },
832            fplugin::Principal {
833                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
834                description: Some(fplugin::Description::Component("runner".to_owned())),
835                principal_type: Some(fplugin::PrincipalType::Runnable),
836                parent: Some(fplugin::PrincipalIdentifier { id: 0 }),
837                ..Default::default()
838            },
839            fplugin::Principal {
840                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
841                description: Some(fplugin::Description::Component("component 2".to_owned())),
842                principal_type: Some(fplugin::PrincipalType::Runnable),
843                parent: Some(fplugin::PrincipalIdentifier { id: 0 }),
844                ..Default::default()
845            },
846            fplugin::Principal {
847                identifier: Some(fplugin::PrincipalIdentifier { id: 3 }),
848                description: Some(fplugin::Description::Component("component 3".to_owned())),
849                principal_type: Some(fplugin::PrincipalType::Runnable),
850                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
851                ..Default::default()
852            },
853        ]
854        .into_iter()
855        .map(|p| p.into())
856        .collect();
857
858        let resources = vec![
859            fplugin::Resource {
860                koid: Some(1000),
861                name_index: Some(0),
862                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
863                    child_jobs: Some(vec![1004, 1008]),
864                    processes: Some(vec![1001]),
865                    ..Default::default()
866                })),
867                ..Default::default()
868            },
869            fplugin::Resource {
870                koid: Some(1001),
871                name_index: Some(1),
872                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
873                    vmos: Some(vec![1002, 1003]),
874                    mappings: None,
875                    ..Default::default()
876                })),
877                ..Default::default()
878            },
879            fplugin::Resource {
880                koid: Some(1002),
881                name_index: Some(2),
882                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
883                    private_committed_bytes: Some(1024),
884                    private_populated_bytes: Some(2048),
885                    scaled_committed_bytes: Some(1024),
886                    scaled_populated_bytes: Some(2048),
887                    total_committed_bytes: Some(1024),
888                    total_populated_bytes: Some(2048),
889                    ..Default::default()
890                })),
891                ..Default::default()
892            },
893            fplugin::Resource {
894                koid: Some(1003),
895                name_index: Some(3),
896                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
897                    private_committed_bytes: Some(1024),
898                    private_populated_bytes: Some(2048),
899                    scaled_committed_bytes: Some(1024),
900                    scaled_populated_bytes: Some(2048),
901                    total_committed_bytes: Some(1024),
902                    total_populated_bytes: Some(2048),
903                    ..Default::default()
904                })),
905                ..Default::default()
906            },
907            fplugin::Resource {
908                koid: Some(1004),
909                name_index: Some(4),
910                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
911                    child_jobs: Some(vec![]),
912                    processes: Some(vec![1005]),
913                    ..Default::default()
914                })),
915                ..Default::default()
916            },
917            fplugin::Resource {
918                koid: Some(1005),
919                name_index: Some(5),
920                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
921                    vmos: Some(vec![1006, 1007, 1012]),
922                    mappings: Some(vec![
923                        fplugin::Mapping {
924                            vmo: Some(1006),
925                            address_base: Some(0),
926                            size: Some(512),
927                            ..Default::default()
928                        },
929                        fplugin::Mapping {
930                            vmo: Some(1012),
931                            address_base: Some(1024),
932                            size: Some(512),
933                            ..Default::default()
934                        },
935                        fplugin::Mapping {
936                            vmo: Some(1013),
937                            address_base: Some(1536),
938                            size: Some(512),
939                            ..Default::default()
940                        },
941                        fplugin::Mapping {
942                            vmo: Some(1006),
943                            address_base: Some(2048),
944                            size: Some(512),
945                            ..Default::default()
946                        },
947                    ]),
948                    ..Default::default()
949                })),
950                ..Default::default()
951            },
952            fplugin::Resource {
953                koid: Some(1006),
954                name_index: Some(6),
955                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
956                    private_committed_bytes: Some(1024),
957                    private_populated_bytes: Some(2048),
958                    scaled_committed_bytes: Some(1024),
959                    scaled_populated_bytes: Some(2048),
960                    total_committed_bytes: Some(1024),
961                    total_populated_bytes: Some(2048),
962                    ..Default::default()
963                })),
964                ..Default::default()
965            },
966            fplugin::Resource {
967                koid: Some(1007),
968                name_index: Some(7),
969                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
970                    private_committed_bytes: Some(128),
971                    private_populated_bytes: Some(256),
972                    scaled_committed_bytes: Some(128),
973                    scaled_populated_bytes: Some(256),
974                    total_committed_bytes: Some(128),
975                    total_populated_bytes: Some(256),
976                    ..Default::default()
977                })),
978                ..Default::default()
979            },
980            fplugin::Resource {
981                koid: Some(1008),
982                name_index: Some(8),
983                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
984                    child_jobs: Some(vec![]),
985                    processes: Some(vec![1009]),
986                    ..Default::default()
987                })),
988                ..Default::default()
989            },
990            fplugin::Resource {
991                koid: Some(1009),
992                name_index: Some(9),
993                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
994                    vmos: Some(vec![1010, 1003]),
995                    mappings: None,
996                    ..Default::default()
997                })),
998                ..Default::default()
999            },
1000            fplugin::Resource {
1001                koid: Some(1010),
1002                name_index: Some(10),
1003                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1004                    parent: Some(1011),
1005                    private_committed_bytes: Some(1024),
1006                    private_populated_bytes: Some(2048),
1007                    scaled_committed_bytes: Some(1024),
1008                    scaled_populated_bytes: Some(2048),
1009                    total_committed_bytes: Some(1024),
1010                    total_populated_bytes: Some(2048),
1011                    ..Default::default()
1012                })),
1013                ..Default::default()
1014            },
1015            fplugin::Resource {
1016                koid: Some(1011),
1017                name_index: Some(11),
1018                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1019                    private_committed_bytes: Some(1024),
1020                    private_populated_bytes: Some(2048),
1021                    scaled_committed_bytes: Some(1024),
1022                    scaled_populated_bytes: Some(2048),
1023                    total_committed_bytes: Some(1024),
1024                    total_populated_bytes: Some(2048),
1025                    ..Default::default()
1026                })),
1027                ..Default::default()
1028            },
1029            fplugin::Resource {
1030                koid: Some(1012),
1031                name_index: Some(12),
1032                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1033                    private_committed_bytes: Some(1024),
1034                    private_populated_bytes: Some(2048),
1035                    scaled_committed_bytes: Some(1024),
1036                    scaled_populated_bytes: Some(2048),
1037                    total_committed_bytes: Some(1024),
1038                    total_populated_bytes: Some(2048),
1039                    ..Default::default()
1040                })),
1041                ..Default::default()
1042            },
1043            fplugin::Resource {
1044                koid: Some(1013),
1045                name_index: Some(13),
1046                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1047                    private_committed_bytes: Some(1024),
1048                    private_populated_bytes: Some(2048),
1049                    scaled_committed_bytes: Some(1024),
1050                    scaled_populated_bytes: Some(2048),
1051                    total_committed_bytes: Some(1024),
1052                    total_populated_bytes: Some(2048),
1053                    ..Default::default()
1054                })),
1055                ..Default::default()
1056            },
1057        ]
1058        .into_iter()
1059        .map(|r| r.into())
1060        .collect();
1061
1062        let output = attribute_vmos(AttributionData {
1063            principals_vec: principals,
1064            resources_vec: resources,
1065            resource_names,
1066            attributions,
1067        })
1068        .summary();
1069
1070        assert_eq!(output.unclaimed, 2048);
1071        assert_eq!(output.principals.len(), 4);
1072
1073        let principals: HashMap<u64, PrincipalSummary> =
1074            output.principals.into_iter().map(|p| (p.id, p)).collect();
1075
1076        assert_eq!(
1077            principals.get(&0).unwrap(),
1078            &PrincipalSummary {
1079                id: 0,
1080                name: "root".to_owned(),
1081                principal_type: "R".to_owned(),
1082                committed_private: 1024,
1083                committed_scaled: 1536.0,
1084                committed_total: 2048,
1085                populated_private: 2048,
1086                populated_scaled: 3072.0,
1087                populated_total: 4096,
1088                attributor: None,
1089                processes: vec!["root_process (1001)".to_owned()],
1090                vmos: vec![
1091                    (
1092                        ZXName::from_string_lossy("root_vmo"),
1093                        VmoSummary {
1094                            count: 1,
1095                            committed_private: 1024,
1096                            committed_scaled: 1024.0,
1097                            committed_total: 1024,
1098                            populated_private: 2048,
1099                            populated_scaled: 2048.0,
1100                            populated_total: 2048,
1101                            ..Default::default()
1102                        }
1103                    ),
1104                    (
1105                        ZXName::from_string_lossy("shared_vmo"),
1106                        VmoSummary {
1107                            count: 1,
1108                            committed_private: 0,
1109                            committed_scaled: 512.0,
1110                            committed_total: 1024,
1111                            populated_private: 0,
1112                            populated_scaled: 1024.0,
1113                            populated_total: 2048,
1114                            ..Default::default()
1115                        }
1116                    )
1117                ]
1118                .into_iter()
1119                .collect(),
1120            }
1121        );
1122
1123        assert_eq!(
1124            principals.get(&1).unwrap(),
1125            &PrincipalSummary {
1126                id: 1,
1127                name: "runner".to_owned(),
1128                principal_type: "R".to_owned(),
1129                committed_private: 1024,
1130                committed_scaled: 1024.0,
1131                committed_total: 1024,
1132                populated_private: 2048,
1133                populated_scaled: 2048.0,
1134                populated_total: 2048,
1135                attributor: Some("root".to_owned()),
1136                processes: vec!["runner_process (1005)".to_owned()],
1137                vmos: vec![(
1138                    ZXName::from_string_lossy("runner_vmo"),
1139                    VmoSummary {
1140                        count: 1,
1141                        committed_private: 1024,
1142                        committed_scaled: 1024.0,
1143                        committed_total: 1024,
1144                        populated_private: 2048,
1145                        populated_scaled: 2048.0,
1146                        populated_total: 2048,
1147                        ..Default::default()
1148                    }
1149                )]
1150                .into_iter()
1151                .collect(),
1152            }
1153        );
1154
1155        assert_eq!(
1156            principals.get(&2).unwrap(),
1157            &PrincipalSummary {
1158                id: 2,
1159                name: "component 2".to_owned(),
1160                principal_type: "R".to_owned(),
1161                committed_private: 1024,
1162                committed_scaled: 1536.0,
1163                committed_total: 2048,
1164                populated_private: 2048,
1165                populated_scaled: 3072.0,
1166                populated_total: 4096,
1167                attributor: Some("root".to_owned()),
1168                processes: vec!["2_process (1009)".to_owned()],
1169                vmos: vec![
1170                    (
1171                        ZXName::from_string_lossy("shared_vmo"),
1172                        VmoSummary {
1173                            count: 1,
1174                            committed_private: 0,
1175                            committed_scaled: 512.0,
1176                            committed_total: 1024,
1177                            populated_private: 0,
1178                            populated_scaled: 1024.0,
1179                            populated_total: 2048,
1180                            ..Default::default()
1181                        }
1182                    ),
1183                    (
1184                        ZXName::from_string_lossy("2_vmo"),
1185                        VmoSummary {
1186                            count: 1,
1187                            committed_private: 1024,
1188                            committed_scaled: 1024.0,
1189                            committed_total: 1024,
1190                            populated_private: 2048,
1191                            populated_scaled: 2048.0,
1192                            populated_total: 2048,
1193                            ..Default::default()
1194                        }
1195                    )
1196                ]
1197                .into_iter()
1198                .collect(),
1199            }
1200        );
1201
1202        assert_eq!(
1203            principals.get(&3).unwrap(),
1204            &PrincipalSummary {
1205                id: 3,
1206                name: "component 3".to_owned(),
1207                principal_type: "R".to_owned(),
1208                committed_private: 2176,
1209                committed_scaled: 2176.0,
1210                committed_total: 2176,
1211                populated_private: 4352,
1212                populated_scaled: 4352.0,
1213                populated_total: 4352,
1214                attributor: Some("runner".to_owned()),
1215                processes: vec!["runner_process (1005)".to_owned()],
1216                vmos: vec![
1217                    (
1218                        ZXName::from_string_lossy("component_vmo"),
1219                        VmoSummary {
1220                            count: 1,
1221                            committed_private: 128,
1222                            committed_scaled: 128.0,
1223                            committed_total: 128,
1224                            populated_private: 256,
1225                            populated_scaled: 256.0,
1226                            populated_total: 256,
1227                            ..Default::default()
1228                        }
1229                    ),
1230                    (
1231                        ZXName::from_string_lossy("component_vmo_mapped"),
1232                        VmoSummary {
1233                            count: 1,
1234                            committed_private: 1024,
1235                            committed_scaled: 1024.0,
1236                            committed_total: 1024,
1237                            populated_private: 2048,
1238                            populated_scaled: 2048.0,
1239                            populated_total: 2048,
1240                            ..Default::default()
1241                        }
1242                    ),
1243                    (
1244                        ZXName::from_string_lossy("component_vmo_mapped2"),
1245                        VmoSummary {
1246                            count: 1,
1247                            committed_private: 1024,
1248                            committed_scaled: 1024.0,
1249                            committed_total: 1024,
1250                            populated_private: 2048,
1251                            populated_scaled: 2048.0,
1252                            populated_total: 2048,
1253                            ..Default::default()
1254                        }
1255                    )
1256                ]
1257                .into_iter()
1258                .collect(),
1259            }
1260        );
1261    }
1262
1263    #[test]
1264    fn test_reshare_resources() {
1265        // Create a fake snapshot with 3 principals:
1266        // root (0)
1267        //  - component 1 (1)
1268        //    - component 2 (2)
1269        //
1270        // and the following job/process/vmo hierarchy:
1271        // root_job (1000)
1272        //  - component_job (1001)
1273        //    * component_process (1002)
1274        //      . component_vmo (1003)
1275        //
1276        // In this scenario, component 1 reattributes component_job to component 2 entirely.
1277
1278        let resource_names = vec![
1279            ZXName::from_string_lossy("root_job"),
1280            ZXName::from_string_lossy("component_job"),
1281            ZXName::from_string_lossy("component_process"),
1282            ZXName::from_string_lossy("component_vmo"),
1283        ];
1284        let attributions = vec![
1285            fplugin::Attribution {
1286                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
1287                subject: Some(fplugin::PrincipalIdentifier { id: 0 }),
1288                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1289                ..Default::default()
1290            },
1291            fplugin::Attribution {
1292                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
1293                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1294                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1295                ..Default::default()
1296            },
1297            fplugin::Attribution {
1298                source: Some(fplugin::PrincipalIdentifier { id: 1 }),
1299                subject: Some(fplugin::PrincipalIdentifier { id: 2 }),
1300                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1301                ..Default::default()
1302            },
1303        ]
1304        .into_iter()
1305        .map(|a| a.into())
1306        .collect();
1307        let principals = vec![
1308            fplugin::Principal {
1309                identifier: Some(fplugin::PrincipalIdentifier { id: 0 }),
1310                description: Some(fplugin::Description::Component("root".to_owned())),
1311                principal_type: Some(fplugin::PrincipalType::Runnable),
1312                parent: None,
1313                ..Default::default()
1314            },
1315            fplugin::Principal {
1316                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1317                description: Some(fplugin::Description::Component("component 1".to_owned())),
1318                principal_type: Some(fplugin::PrincipalType::Runnable),
1319                parent: Some(fplugin::PrincipalIdentifier { id: 0 }),
1320                ..Default::default()
1321            },
1322            fplugin::Principal {
1323                identifier: Some(fplugin::PrincipalIdentifier { id: 2 }),
1324                description: Some(fplugin::Description::Component("component 2".to_owned())),
1325                principal_type: Some(fplugin::PrincipalType::Runnable),
1326                parent: Some(fplugin::PrincipalIdentifier { id: 1 }),
1327                ..Default::default()
1328            },
1329        ]
1330        .into_iter()
1331        .map(|p| p.into())
1332        .collect();
1333
1334        let resources = vec![
1335            fplugin::Resource {
1336                koid: Some(1000),
1337                name_index: Some(0),
1338                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1339                    child_jobs: Some(vec![1001]),
1340                    processes: Some(vec![]),
1341                    ..Default::default()
1342                })),
1343                ..Default::default()
1344            },
1345            fplugin::Resource {
1346                koid: Some(1001),
1347                name_index: Some(1),
1348                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1349                    child_jobs: Some(vec![]),
1350                    processes: Some(vec![1002]),
1351                    ..Default::default()
1352                })),
1353                ..Default::default()
1354            },
1355            fplugin::Resource {
1356                koid: Some(1002),
1357                name_index: Some(2),
1358                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1359                    vmos: Some(vec![1003]),
1360                    mappings: None,
1361                    ..Default::default()
1362                })),
1363                ..Default::default()
1364            },
1365            fplugin::Resource {
1366                koid: Some(1003),
1367                name_index: Some(3),
1368                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1369                    private_committed_bytes: Some(1024),
1370                    private_populated_bytes: Some(2048),
1371                    scaled_committed_bytes: Some(1024),
1372                    scaled_populated_bytes: Some(2048),
1373                    total_committed_bytes: Some(1024),
1374                    total_populated_bytes: Some(2048),
1375                    ..Default::default()
1376                })),
1377                ..Default::default()
1378            },
1379        ]
1380        .into_iter()
1381        .map(|r| r.into())
1382        .collect();
1383
1384        let output = attribute_vmos(AttributionData {
1385            principals_vec: principals,
1386            resources_vec: resources,
1387            resource_names,
1388            attributions,
1389        })
1390        .summary();
1391
1392        assert_eq!(output.unclaimed, 0);
1393        assert_eq!(output.principals.len(), 3);
1394
1395        let principals: HashMap<u64, PrincipalSummary> =
1396            output.principals.into_iter().map(|p| (p.id, p)).collect();
1397
1398        assert_eq!(
1399            principals.get(&0).unwrap(),
1400            &PrincipalSummary {
1401                id: 0,
1402                name: "root".to_owned(),
1403                principal_type: "R".to_owned(),
1404                committed_private: 0,
1405                committed_scaled: 0.0,
1406                committed_total: 0,
1407                populated_private: 0,
1408                populated_scaled: 0.0,
1409                populated_total: 0,
1410                attributor: None,
1411                processes: vec![],
1412                vmos: vec![].into_iter().collect(),
1413            }
1414        );
1415
1416        assert_eq!(
1417            principals.get(&1).unwrap(),
1418            &PrincipalSummary {
1419                id: 1,
1420                name: "component 1".to_owned(),
1421                principal_type: "R".to_owned(),
1422                committed_private: 0,
1423                committed_scaled: 0.0,
1424                committed_total: 0,
1425                populated_private: 0,
1426                populated_scaled: 0.0,
1427                populated_total: 0,
1428                attributor: Some("root".to_owned()),
1429                processes: vec![],
1430                vmos: vec![].into_iter().collect(),
1431            }
1432        );
1433
1434        assert_eq!(
1435            principals.get(&2).unwrap(),
1436            &PrincipalSummary {
1437                id: 2,
1438                name: "component 2".to_owned(),
1439                principal_type: "R".to_owned(),
1440                committed_private: 1024,
1441                committed_scaled: 1024.0,
1442                committed_total: 1024,
1443                populated_private: 2048,
1444                populated_scaled: 2048.0,
1445                populated_total: 2048,
1446                attributor: Some("component 1".to_owned()),
1447                processes: vec!["component_process (1002)".to_owned()],
1448                vmos: vec![(
1449                    ZXName::from_string_lossy("component_vmo"),
1450                    VmoSummary {
1451                        count: 1,
1452                        committed_private: 1024,
1453                        committed_scaled: 1024.0,
1454                        committed_total: 1024,
1455                        populated_private: 2048,
1456                        populated_scaled: 2048.0,
1457                        populated_total: 2048,
1458                        ..Default::default()
1459                    }
1460                ),]
1461                .into_iter()
1462                .collect(),
1463            }
1464        );
1465    }
1466
1467    #[test]
1468    fn test_conversions() {
1469        let plugin_principal = fplugin::Principal {
1470            identifier: Some(fplugin::PrincipalIdentifier { id: 0 }),
1471            description: Some(fplugin::Description::Component("root".to_owned())),
1472            principal_type: Some(fplugin::PrincipalType::Runnable),
1473            parent: None,
1474            ..Default::default()
1475        };
1476
1477        let data_principal: Principal = plugin_principal.clone().into();
1478
1479        assert_eq!(plugin_principal, data_principal.into());
1480
1481        let plugin_resources = vec![
1482            fplugin::Resource {
1483                koid: Some(1000),
1484                name_index: Some(0),
1485                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1486                    child_jobs: Some(vec![1004, 1008]),
1487                    processes: Some(vec![1001]),
1488                    ..Default::default()
1489                })),
1490                ..Default::default()
1491            },
1492            fplugin::Resource {
1493                koid: Some(1001),
1494                name_index: Some(1),
1495                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1496                    vmos: Some(vec![1002, 1003]),
1497                    mappings: None,
1498                    ..Default::default()
1499                })),
1500                ..Default::default()
1501            },
1502            fplugin::Resource {
1503                koid: Some(1002),
1504                name_index: Some(2),
1505                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1506                    private_committed_bytes: Some(1024),
1507                    private_populated_bytes: Some(2048),
1508                    scaled_committed_bytes: Some(1024),
1509                    scaled_populated_bytes: Some(2048),
1510                    total_committed_bytes: Some(1024),
1511                    total_populated_bytes: Some(2048),
1512                    ..Default::default()
1513                })),
1514                ..Default::default()
1515            },
1516            fplugin::Resource {
1517                koid: Some(1005),
1518                name_index: Some(5),
1519                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1520                    vmos: Some(vec![1006, 1007, 1012]),
1521                    mappings: Some(vec![
1522                        fplugin::Mapping {
1523                            vmo: Some(1006),
1524                            address_base: Some(0),
1525                            size: Some(512),
1526                            ..Default::default()
1527                        },
1528                        fplugin::Mapping {
1529                            vmo: Some(1012),
1530                            address_base: Some(1024),
1531                            size: Some(512),
1532                            ..Default::default()
1533                        },
1534                    ]),
1535                    ..Default::default()
1536                })),
1537                ..Default::default()
1538            },
1539        ];
1540
1541        let data_resources: Vec<Resource> =
1542            plugin_resources.iter().cloned().map(|r| r.into()).collect();
1543
1544        let actual_resources: Vec<fplugin::Resource> =
1545            data_resources.into_iter().map(|r| r.into()).collect();
1546
1547        assert_eq!(plugin_resources, actual_resources);
1548    }
1549
1550    #[test]
1551    fn test_vmo_reference() {
1552        // Create a fake snapshot with 3 principals:
1553        // root (0)
1554        //  - component 1 (1)
1555        //
1556        // and the following job/process/vmo hierarchy:
1557        // root_job (1000)
1558        //  - component_job (1001)
1559        //    * component_process (1002)
1560        //      . component_vmo (1003)
1561        // component_vmo_parent (1004)
1562        //
1563        // In this scenario, component_vmo is a reference to component_vmo_parent and should get
1564        // shared attribution of its pages.
1565
1566        let resource_names = vec![
1567            name::ZXName::from_string_lossy("root_job"),
1568            name::ZXName::from_string_lossy("component_job"),
1569            name::ZXName::from_string_lossy("component_process"),
1570            name::ZXName::from_string_lossy("component_vmo"),
1571            name::ZXName::from_string_lossy("component_vmo_parent"),
1572        ];
1573        let attributions = vec![
1574            fplugin::Attribution {
1575                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
1576                subject: Some(fplugin::PrincipalIdentifier { id: 0 }),
1577                resources: Some(vec![fplugin::ResourceReference::KernelObject(1000)]),
1578                ..Default::default()
1579            },
1580            fplugin::Attribution {
1581                source: Some(fplugin::PrincipalIdentifier { id: 0 }),
1582                subject: Some(fplugin::PrincipalIdentifier { id: 1 }),
1583                resources: Some(vec![fplugin::ResourceReference::KernelObject(1001)]),
1584                ..Default::default()
1585            },
1586        ]
1587        .into_iter()
1588        .map(|a| a.into())
1589        .collect();
1590        let principals = vec![
1591            fplugin::Principal {
1592                identifier: Some(fplugin::PrincipalIdentifier { id: 0 }),
1593                description: Some(fplugin::Description::Component("root".to_owned())),
1594                principal_type: Some(fplugin::PrincipalType::Runnable),
1595                parent: None,
1596                ..Default::default()
1597            },
1598            fplugin::Principal {
1599                identifier: Some(fplugin::PrincipalIdentifier { id: 1 }),
1600                description: Some(fplugin::Description::Component("component 1".to_owned())),
1601                principal_type: Some(fplugin::PrincipalType::Runnable),
1602                parent: Some(fplugin::PrincipalIdentifier { id: 0 }),
1603                ..Default::default()
1604            },
1605        ]
1606        .into_iter()
1607        .map(|p| p.into())
1608        .collect();
1609
1610        let resources = vec![
1611            fplugin::Resource {
1612                koid: Some(1000),
1613                name_index: Some(0),
1614                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1615                    child_jobs: Some(vec![1001]),
1616                    processes: Some(vec![]),
1617                    ..Default::default()
1618                })),
1619                ..Default::default()
1620            },
1621            fplugin::Resource {
1622                koid: Some(1001),
1623                name_index: Some(1),
1624                resource_type: Some(fplugin::ResourceType::Job(fplugin::Job {
1625                    child_jobs: Some(vec![]),
1626                    processes: Some(vec![1002]),
1627                    ..Default::default()
1628                })),
1629                ..Default::default()
1630            },
1631            fplugin::Resource {
1632                koid: Some(1002),
1633                name_index: Some(2),
1634                resource_type: Some(fplugin::ResourceType::Process(fplugin::Process {
1635                    vmos: Some(vec![1003]),
1636                    mappings: None,
1637                    ..Default::default()
1638                })),
1639                ..Default::default()
1640            },
1641            fplugin::Resource {
1642                koid: Some(1003),
1643                name_index: Some(3),
1644                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1645                    parent: Some(1004),
1646                    private_committed_bytes: Some(0),
1647                    private_populated_bytes: Some(0),
1648                    scaled_committed_bytes: Some(0),
1649                    scaled_populated_bytes: Some(0),
1650                    total_committed_bytes: Some(0),
1651                    total_populated_bytes: Some(0),
1652                    ..Default::default()
1653                })),
1654                ..Default::default()
1655            },
1656            fplugin::Resource {
1657                koid: Some(1004),
1658                name_index: Some(4),
1659                resource_type: Some(fplugin::ResourceType::Vmo(fplugin::Vmo {
1660                    private_committed_bytes: Some(1024),
1661                    private_populated_bytes: Some(2048),
1662                    scaled_committed_bytes: Some(1024),
1663                    scaled_populated_bytes: Some(2048),
1664                    total_committed_bytes: Some(1024),
1665                    total_populated_bytes: Some(2048),
1666                    ..Default::default()
1667                })),
1668                ..Default::default()
1669            },
1670        ]
1671        .into_iter()
1672        .map(|r| r.into())
1673        .collect();
1674
1675        let output = attribute_vmos(AttributionData {
1676            principals_vec: principals,
1677            resources_vec: resources,
1678            resource_names,
1679            attributions,
1680        })
1681        .summary();
1682
1683        assert_eq!(output.unclaimed, 0);
1684        assert_eq!(output.principals.len(), 2);
1685
1686        let principals: HashMap<u64, PrincipalSummary> =
1687            output.principals.into_iter().map(|p| (p.id, p)).collect();
1688
1689        assert_eq!(
1690            principals.get(&0).unwrap(),
1691            &PrincipalSummary {
1692                id: 0,
1693                name: "root".to_owned(),
1694                principal_type: "R".to_owned(),
1695                committed_private: 0,
1696                committed_scaled: 0.0,
1697                committed_total: 0,
1698                populated_private: 0,
1699                populated_scaled: 0.0,
1700                populated_total: 0,
1701                attributor: None,
1702                processes: vec![],
1703                vmos: vec![].into_iter().collect(),
1704            }
1705        );
1706
1707        assert_eq!(
1708            principals.get(&1).unwrap(),
1709            &PrincipalSummary {
1710                id: 1,
1711                name: "component 1".to_owned(),
1712                principal_type: "R".to_owned(),
1713                committed_private: 1024,
1714                committed_scaled: 1024.0,
1715                committed_total: 1024,
1716                populated_private: 2048,
1717                populated_scaled: 2048.0,
1718                populated_total: 2048,
1719                attributor: Some("root".to_owned()),
1720                processes: vec!["component_process (1002)".to_owned()],
1721                vmos: vec![
1722                    (
1723                        name::ZXName::from_string_lossy("component_vmo"),
1724                        VmoSummary {
1725                            count: 1,
1726                            committed_private: 0,
1727                            committed_scaled: 0.0,
1728                            committed_total: 0,
1729                            populated_private: 0,
1730                            populated_scaled: 0.0,
1731                            populated_total: 0,
1732                            ..Default::default()
1733                        }
1734                    ),
1735                    (
1736                        name::ZXName::from_string_lossy("component_vmo_parent"),
1737                        VmoSummary {
1738                            count: 1,
1739                            committed_private: 1024,
1740                            committed_scaled: 1024.0,
1741                            committed_total: 1024,
1742                            populated_private: 2048,
1743                            populated_scaled: 2048.0,
1744                            populated_total: 2048,
1745                            ..Default::default()
1746                        }
1747                    ),
1748                ]
1749                .into_iter()
1750                .collect(),
1751            }
1752        );
1753    }
1754}