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