attribution_processing/
digest.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::{ProcessedAttributionData, ZXName};
6use anyhow::Result;
7use regex::bytes::Regex;
8use serde::de::Error;
9use serde::{Deserialize, Deserializer, Serialize};
10use std::collections::HashMap;
11use std::collections::hash_map::Entry::Occupied;
12#[cfg(target_os = "fuchsia")]
13use {crate::CATEGORY_MEMORY_CAPTURE, fuchsia_trace::duration};
14use {fidl_fuchsia_kernel as fkernel, fidl_fuchsia_memory_attribution_plugin as fplugin};
15
16const UNDIGESTED: &str = "Undigested";
17const ORPHANED: &str = "Orphaned";
18const KERNEL: &str = "Kernel";
19const FREE: &str = "Free";
20const PAGER_TOTAL: &str = "[Addl]PagerTotal";
21const PAGER_NEWEST: &str = "[Addl]PagerNewest";
22const PAGER_OLDEST: &str = "[Addl]PagerOldest";
23const DISCARDABLE_LOCKED: &str = "[Addl]DiscardableLocked";
24const DISCARDABLE_UNLOCKED: &str = "[Addl]DiscardableUnlocked";
25const ZRAM_COMPRESSED_BYTES: &str = "[Addl]ZramCompressedBytes";
26
27/// Represents a specification for aggregating memory usage in meaningful groups.
28///
29/// `name` represents the meaningful name of the group; grouping is done based on process and VMO
30/// names.
31///
32// Note: This needs to mirror `//src/lib/assembly/memory_buckets/src/memory_buckets.rs`, but cannot
33// reuse it directly because it is an host-only library.
34#[derive(Clone, Debug, Deserialize)]
35pub struct BucketDefinition {
36    pub name: String,
37    #[serde(deserialize_with = "deserialize_regex")]
38    pub process: Option<Regex>,
39    #[serde(deserialize_with = "deserialize_regex")]
40    pub vmo: Option<Regex>,
41    #[serde(default, deserialize_with = "deserialize_regex")]
42    pub principal: Option<Regex>,
43    pub event_code: u64,
44}
45
46impl BucketDefinition {
47    /// Tests whether a process matches this bucket's definition, based on its name.
48    fn process_match(&self, process: &ZXName) -> bool {
49        self.process.as_ref().is_none_or(|p| p.is_match(process.as_bstr()))
50    }
51
52    /// Tests whether a VMO matches this bucket's definition, based on its name.
53    fn vmo_match(&self, vmo: &ZXName) -> bool {
54        self.vmo.as_ref().is_none_or(|v| v.is_match(vmo.as_bstr()))
55    }
56
57    /// Tests whether any of the specified principal names match this bucket's definition.
58    fn principals_match(&self, principals: &Vec<&str>) -> bool {
59        self.principal
60            .as_ref()
61            .is_none_or(|a| principals.iter().any(|name| a.is_match(name.as_bytes())))
62    }
63}
64
65// Teach serde to deserialize an optional regex.
66fn deserialize_regex<'de, D>(d: D) -> Result<Option<Regex>, D::Error>
67where
68    D: Deserializer<'de>,
69{
70    // Deserialize as Option<&str>
71    Option::<String>::deserialize(d)
72        // If the parsing failed, return the error, otherwise transform the value
73        .and_then(|os| {
74            os
75                // If there is a value, try to parse it as a Regex.
76                .map(|s| {
77                    Regex::new(&s)
78                        // If the regex compilation failed, wrap the error in the error type expected
79                        // by serde.
80                        .map_err(D::Error::custom)
81                })
82                // If there was a value but it failed to compile, return an error, otherwise return
83                // the potentially parsed option.
84                .transpose()
85        })
86}
87
88/// Aggregates bytes in categories with human readable names.
89#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
90pub struct Bucket {
91    pub name: String,
92    pub populated_size: u64,
93    pub committed_size: u64,
94    pub vmos: Option<Vec<NamedVmo>>,
95}
96
97/// Contains a view of the system's memory usage, aggregated in groups called buckets, which are
98/// configurable.
99#[derive(Debug, Default, PartialEq, Eq, Serialize)]
100pub struct Digest {
101    pub buckets: Vec<Bucket>,
102}
103
104/// Non-owning structure to keep track of known undigested VMOs.
105struct UndigestedVmo<'a> {
106    populated_size: u64,
107    committed_size: u64,
108    name: &'a ZXName,
109    principals: &'a Vec<&'a str>,
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
113/// Owning structure to report known VMOs.
114pub struct NamedVmo {
115    pub name: ZXName,
116    pub populated_size: u64,
117    pub committed_size: u64,
118    pub principals: Vec<String>,
119}
120
121impl Digest {
122    /// Given means to query the system for memory usage, and a specification, this function
123    /// aggregates the current memory usage into human displayable units we call buckets.
124    pub fn compute(
125        attribution_data: &ProcessedAttributionData,
126        kmem_stats: &fkernel::MemoryStats,
127        kmem_stats_compression: &fkernel::MemoryStatsCompression,
128        bucket_definitions: &[BucketDefinition],
129        detailed_vmos: bool,
130    ) -> Result<Digest> {
131        #[cfg(target_os = "fuchsia")]
132        duration!(CATEGORY_MEMORY_CAPTURE, c"Digest::compute");
133
134        // Maps resources' (VMO, Process, Job. See Resource) ids
135        // to their owner, i.e. the principal they have been
136        // attributed to.
137        let owners: HashMap<u64, Vec<&str>> = {
138            let koid_to_principal = attribution_data
139                .principals
140                .iter()
141                .flat_map(|(_, p)| p.resources.iter().map(|r| (*r, p.name())));
142
143            let mut owners: HashMap<u64, Vec<_>> = HashMap::new();
144            for (koid, principal) in koid_to_principal {
145                let principals = owners.entry(koid).or_default();
146                principals.push(principal);
147            }
148            owners
149        };
150
151        let no_principals = vec![];
152        let mut undigested_vmos: HashMap<u64, UndigestedVmo<'_>> = attribution_data
153            .resources
154            .iter()
155            .filter_map(|(koid, r)| match &r.resource.resource_type {
156                fplugin::ResourceType::Vmo(vmo) => {
157                    attribution_data.resource_names.get(r.resource.name_index).and_then(|name| {
158                        let populated_size = vmo.scaled_populated_bytes?;
159                        let committed_size = vmo.scaled_committed_bytes?;
160                        Some((
161                            *koid,
162                            UndigestedVmo {
163                                name,
164                                populated_size,
165                                committed_size,
166                                principals: owners.get(koid).unwrap_or(&no_principals),
167                            },
168                        ))
169                    })
170                }
171                _ => None,
172            })
173            .collect();
174        let processes: Vec<(&ZXName, &fplugin::Process)> = attribution_data
175            .resources
176            .values()
177            .filter_map(|r| match &r.resource.resource_type {
178                fplugin::ResourceType::Process(process) => attribution_data
179                    .resource_names
180                    .get(r.resource.name_index)
181                    .map(|name| (name, process)),
182                _ => None,
183            })
184            .collect();
185
186        let mut buckets: Vec<Bucket> = bucket_definitions
187            .iter()
188            .map(|bd| {
189                let mut bucket = Bucket {
190                    name: bd.name.to_owned(),
191                    populated_size: 0,
192                    committed_size: 0,
193                    vmos: None,
194                };
195                processes.iter().for_each(|(process_name, process)| {
196                    if bd.process_match(process_name) {
197                        for koid in process.vmos.iter().flatten() {
198                            let (populated_size, committed_size) = match undigested_vmos
199                                .entry(*koid)
200                            {
201                                Occupied(e) => {
202                                    let UndigestedVmo { name, principals, .. } = e.get();
203                                    if bd.vmo_match(&name) && bd.principals_match(principals) {
204                                        let (_, vmo) = e.remove_entry();
205                                        if detailed_vmos {
206                                            bucket.vmos.get_or_insert_default().push(NamedVmo {
207                                                name: vmo.name.clone(),
208                                                populated_size: vmo.populated_size,
209                                                committed_size: vmo.committed_size,
210                                                principals: vmo
211                                                    .principals
212                                                    .into_iter()
213                                                    .map(|&name| name.to_owned())
214                                                    .collect(),
215                                            });
216                                        }
217                                        (vmo.populated_size, vmo.committed_size)
218                                    } else {
219                                        (0, 0)
220                                    }
221                                }
222                                _ => (0, 0),
223                            };
224                            bucket.committed_size += committed_size;
225                            bucket.populated_size += populated_size;
226                        }
227                    };
228                });
229                bucket
230            })
231            .collect();
232
233        // This bucket contains the total size of the known VMOs that have not been covered
234        // by any other bucket.
235        let undigested = {
236            let (populated_size, committed_size) = undigested_vmos
237                .values()
238                .map(|UndigestedVmo { populated_size, committed_size, .. }| {
239                    (*populated_size, *committed_size)
240                })
241                .fold((0, 0), |(total_populated, total_committed), (populated, committed)| {
242                    (total_populated + populated, total_committed + committed)
243                });
244
245            Bucket {
246                name: UNDIGESTED.to_string(),
247                populated_size: populated_size,
248                committed_size,
249                vmos: if detailed_vmos {
250                    Some(
251                        undigested_vmos
252                            .values()
253                            .map(|vmo| NamedVmo {
254                                name: vmo.name.clone(),
255                                populated_size: vmo.populated_size,
256                                committed_size: vmo.committed_size,
257                                principals: vmo
258                                    .principals
259                                    .into_iter()
260                                    .map(|&name| name.to_owned())
261                                    .collect(),
262                            })
263                            .collect(),
264                    )
265                } else {
266                    None
267                },
268            }
269        };
270
271        let total_vmo_size: u64 = undigested.committed_size
272            + buckets.iter().map(|Bucket { committed_size, .. }| committed_size).sum::<u64>();
273
274        // Extend the configured aggregation with a number of additional, occasionally useful meta
275        // aggregations.
276        buckets.extend([
277            undigested,
278            // This bucket accounts for VMO bytes that have been allocated by the kernel, but not
279            // claimed by any VMO (anymore).
280            {
281                let size = kmem_stats.vmo_bytes.unwrap_or(0).saturating_sub(total_vmo_size);
282                Bucket {
283                    name: ORPHANED.to_string(),
284                    populated_size: size,
285                    committed_size: size,
286                    vmos: None,
287                }
288            },
289            // This bucket aggregates overall kernel memory usage.
290            {
291                let size = (|| {
292                    Some(
293                        kmem_stats.wired_bytes?
294                            + kmem_stats.total_heap_bytes?
295                            + kmem_stats.mmu_overhead_bytes?
296                            + kmem_stats.ipc_bytes?
297                            + kmem_stats.other_bytes?,
298                    )
299                })()
300                .unwrap_or(0);
301                Bucket {
302                    name: KERNEL.to_string(),
303                    populated_size: size,
304                    committed_size: size,
305                    vmos: None,
306                }
307            },
308            // This bucket contains the amount of free memory in the system.
309            {
310                let size = kmem_stats.free_bytes.unwrap_or(0);
311                Bucket {
312                    name: FREE.to_string(),
313                    populated_size: size,
314                    committed_size: size,
315                    vmos: None,
316                }
317            },
318            // Those buckets contain pager related information.
319            {
320                let size = kmem_stats.vmo_reclaim_total_bytes.unwrap_or(0);
321                Bucket {
322                    name: PAGER_TOTAL.to_string(),
323                    populated_size: size,
324                    committed_size: size,
325                    vmos: None,
326                }
327            },
328            {
329                let size = kmem_stats.vmo_reclaim_newest_bytes.unwrap_or(0);
330                Bucket {
331                    name: PAGER_NEWEST.to_string(),
332                    populated_size: size,
333                    committed_size: size,
334                    vmos: None,
335                }
336            },
337            {
338                let size = kmem_stats.vmo_reclaim_oldest_bytes.unwrap_or(0);
339                Bucket {
340                    name: PAGER_OLDEST.to_string(),
341                    populated_size: size,
342                    committed_size: size,
343                    vmos: None,
344                }
345            },
346            // Those buckets account for discardable memory.
347            {
348                let size = kmem_stats.vmo_discardable_locked_bytes.unwrap_or(0);
349                Bucket {
350                    name: DISCARDABLE_LOCKED.to_string(),
351                    populated_size: size,
352                    committed_size: size,
353                    vmos: None,
354                }
355            },
356            {
357                let size = kmem_stats.vmo_discardable_unlocked_bytes.unwrap_or(0);
358                Bucket {
359                    name: DISCARDABLE_UNLOCKED.to_string(),
360                    populated_size: size,
361                    committed_size: size,
362                    vmos: None,
363                }
364            },
365            // This bucket accounts for compressed memory.
366            {
367                let size = kmem_stats_compression.compressed_storage_bytes.unwrap_or(0);
368                Bucket {
369                    name: ZRAM_COMPRESSED_BYTES.to_string(),
370                    populated_size: size,
371                    committed_size: size,
372                    vmos: None,
373                }
374            },
375        ]);
376        Ok(Digest { buckets })
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::{
384        Attribution, AttributionData, GlobalPrincipalIdentifier, Principal, PrincipalDescription,
385        PrincipalType, ProcessedAttributionData, Resource, ResourceReference, attribute_vmos,
386    };
387    use fidl_fuchsia_memory_attribution_plugin as fplugin;
388
389    fn get_attribution_data() -> ProcessedAttributionData {
390        attribute_vmos(AttributionData {
391            principals_vec: vec![
392                Principal {
393                    identifier: GlobalPrincipalIdentifier::new_for_test(1),
394                    description: Some(PrincipalDescription::Component("principal".to_owned())),
395                    principal_type: PrincipalType::Runnable,
396                    parent: Some(GlobalPrincipalIdentifier::new_for_test(2)),
397                },
398                Principal {
399                    identifier: GlobalPrincipalIdentifier::new_for_test(2),
400                    description: Some(PrincipalDescription::Component("parent".to_owned())),
401                    principal_type: PrincipalType::Runnable,
402                    parent: None,
403                },
404            ],
405            resources_vec: vec![
406                Resource {
407                    koid: 10,
408                    name_index: 0,
409                    resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
410                        parent: None,
411                        private_committed_bytes: Some(1024),
412                        private_populated_bytes: Some(2048),
413                        scaled_committed_bytes: Some(512),
414                        scaled_populated_bytes: Some(2048),
415                        total_committed_bytes: Some(1024),
416                        total_populated_bytes: Some(2048),
417                        ..Default::default()
418                    }),
419                },
420                Resource {
421                    koid: 20,
422                    name_index: 1,
423                    resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
424                        parent: None,
425                        private_committed_bytes: Some(1024),
426                        private_populated_bytes: Some(2048),
427                        scaled_committed_bytes: Some(512),
428                        scaled_populated_bytes: Some(2048),
429                        total_committed_bytes: Some(1024),
430                        total_populated_bytes: Some(2048),
431                        ..Default::default()
432                    }),
433                },
434                Resource {
435                    koid: 30,
436                    name_index: 1,
437                    resource_type: fplugin::ResourceType::Process(fplugin::Process {
438                        vmos: Some(vec![10, 20]),
439                        ..Default::default()
440                    }),
441                },
442            ],
443            resource_names: vec![
444                ZXName::try_from_bytes(b"resource").unwrap(),
445                ZXName::try_from_bytes(b"matched").unwrap(),
446            ],
447            attributions: vec![Attribution {
448                source: GlobalPrincipalIdentifier::new_for_test(1),
449                subject: GlobalPrincipalIdentifier::new_for_test(1),
450                resources: vec![ResourceReference::KernelObject(20)],
451            }],
452        })
453    }
454
455    fn get_kernel_stats() -> (fkernel::MemoryStats, fkernel::MemoryStatsCompression) {
456        (
457            fkernel::MemoryStats {
458                total_bytes: Some(1),
459                free_bytes: Some(2),
460                wired_bytes: Some(3),
461                total_heap_bytes: Some(4),
462                free_heap_bytes: Some(5),
463                vmo_bytes: Some(10000),
464                mmu_overhead_bytes: Some(7),
465                ipc_bytes: Some(8),
466                other_bytes: Some(9),
467                free_loaned_bytes: Some(10),
468                cache_bytes: Some(11),
469                slab_bytes: Some(12),
470                zram_bytes: Some(13),
471                vmo_reclaim_total_bytes: Some(14),
472                vmo_reclaim_newest_bytes: Some(15),
473                vmo_reclaim_oldest_bytes: Some(16),
474                vmo_reclaim_disabled_bytes: Some(17),
475                vmo_discardable_locked_bytes: Some(18),
476                vmo_discardable_unlocked_bytes: Some(19),
477                ..Default::default()
478            },
479            fkernel::MemoryStatsCompression {
480                uncompressed_storage_bytes: Some(20),
481                compressed_storage_bytes: Some(21),
482                compressed_fragmentation_bytes: Some(22),
483                compression_time: Some(23),
484                decompression_time: Some(24),
485                total_page_compression_attempts: Some(25),
486                failed_page_compression_attempts: Some(26),
487                total_page_decompressions: Some(27),
488                compressed_page_evictions: Some(28),
489                eager_page_compressions: Some(29),
490                memory_pressure_page_compressions: Some(30),
491                critical_memory_page_compressions: Some(31),
492                pages_decompressed_unit_ns: Some(32),
493                pages_decompressed_within_log_time: Some([40, 41, 42, 43, 44, 45, 46, 47]),
494                ..Default::default()
495            },
496        )
497    }
498
499    fn sort_buckets_for_assert(digest: &mut Digest) {
500        for bucket in digest.buckets.iter_mut() {
501            for vmos in bucket.vmos.iter_mut() {
502                vmos.sort_by(|vmo1, vmo2| vmo1.name.cmp(&vmo2.name));
503            }
504        }
505    }
506
507    #[test]
508    fn test_digest_no_definitions() {
509        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
510        let digest = {
511            let mut digest = Digest::compute(
512                &get_attribution_data(),
513                &kernel_stats,
514                &kernel_stats_compression,
515                &vec![],
516                true,
517            )
518            .unwrap();
519            sort_buckets_for_assert(&mut digest);
520            digest
521        };
522        let expected_buckets = vec![
523            // The two VMOs are unmatched, 512 + 512
524            Bucket {
525                name: UNDIGESTED.to_string(),
526                populated_size: 4096,
527                committed_size: 1024,
528                vmos: Some(vec![
529                    NamedVmo {
530                        name: ZXName::from_string_lossy("matched"),
531                        populated_size: 2048,
532                        committed_size: 512,
533                        principals: vec!["principal".to_string()],
534                    },
535                    NamedVmo {
536                        name: ZXName::from_string_lossy("resource"),
537                        populated_size: 2048,
538                        committed_size: 512,
539                        principals: vec![],
540                    },
541                ]),
542            },
543            // No matched VMOs, one UNDIGESTED VMO => 10000 - 1024 = 8976
544            Bucket {
545                name: ORPHANED.to_string(),
546                populated_size: 8976,
547                committed_size: 8976,
548                vmos: None,
549            },
550            // wired + heap + mmu + ipc + other => 3 + 4 + 7 + 8 + 9 = 31
551            Bucket { name: KERNEL.to_string(), populated_size: 31, committed_size: 31, vmos: None },
552            Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
553            Bucket {
554                name: PAGER_TOTAL.to_string(),
555                populated_size: 14,
556                committed_size: 14,
557                vmos: None,
558            },
559            Bucket {
560                name: PAGER_NEWEST.to_string(),
561                populated_size: 15,
562                committed_size: 15,
563                vmos: None,
564            },
565            Bucket {
566                name: PAGER_OLDEST.to_string(),
567                populated_size: 16,
568                committed_size: 16,
569                vmos: None,
570            },
571            Bucket {
572                name: DISCARDABLE_LOCKED.to_string(),
573                populated_size: 18,
574                committed_size: 18,
575                vmos: None,
576            },
577            Bucket {
578                name: DISCARDABLE_UNLOCKED.to_string(),
579                populated_size: 19,
580                committed_size: 19,
581                vmos: None,
582            },
583            Bucket {
584                name: ZRAM_COMPRESSED_BYTES.to_string(),
585                populated_size: 21,
586                committed_size: 21,
587                vmos: None,
588            },
589        ];
590
591        assert_eq!(digest.buckets, expected_buckets);
592    }
593
594    #[test]
595    fn test_digest_with_matching_vmo() -> Result<(), anyhow::Error> {
596        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
597        let digest = {
598            let mut digest = Digest::compute(
599                &get_attribution_data(),
600                &kernel_stats,
601                &kernel_stats_compression,
602                &vec![BucketDefinition {
603                    name: "matched".to_string(),
604                    process: None,
605                    vmo: Some(Regex::new("matched")?),
606                    principal: None,
607                    event_code: Default::default(),
608                }],
609                true,
610            )
611            .unwrap();
612            sort_buckets_for_assert(&mut digest);
613            digest
614        };
615        let expected_buckets = vec![
616            // One VMO is matched, the other is not
617            Bucket {
618                name: "matched".to_string(),
619                populated_size: 2048,
620                committed_size: 512,
621                vmos: Some(vec![NamedVmo {
622                    name: ZXName::from_string_lossy("matched"),
623                    populated_size: 2048,
624                    committed_size: 512,
625                    principals: vec!["principal".to_owned()],
626                }]),
627            },
628            // One unmatched VMO
629            Bucket {
630                name: UNDIGESTED.to_string(),
631                populated_size: 2048,
632                committed_size: 512,
633                vmos: Some(vec![NamedVmo {
634                    name: ZXName::from_string_lossy("resource"),
635                    populated_size: 2048,
636                    committed_size: 512,
637                    principals: vec![],
638                }]),
639            },
640            // One matched VMO, one unmatched VMO //=> 10000 - 512 - 512 = 8976
641            Bucket {
642                name: ORPHANED.to_string(),
643                populated_size: 8976,
644                committed_size: 8976,
645                vmos: None,
646            },
647            // wired + heap + mmu + ipc + other => 3 + 4 + 7 + 8 + 9 = 31
648            Bucket { name: KERNEL.to_string(), populated_size: 31, committed_size: 31, vmos: None },
649            Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
650            Bucket {
651                name: PAGER_TOTAL.to_string(),
652                populated_size: 14,
653                committed_size: 14,
654                vmos: None,
655            },
656            Bucket {
657                name: PAGER_NEWEST.to_string(),
658                populated_size: 15,
659                committed_size: 15,
660                vmos: None,
661            },
662            Bucket {
663                name: PAGER_OLDEST.to_string(),
664                populated_size: 16,
665                committed_size: 16,
666                vmos: None,
667            },
668            Bucket {
669                name: DISCARDABLE_LOCKED.to_string(),
670                populated_size: 18,
671                committed_size: 18,
672                vmos: None,
673            },
674            Bucket {
675                name: DISCARDABLE_UNLOCKED.to_string(),
676                populated_size: 19,
677                committed_size: 19,
678                vmos: None,
679            },
680            Bucket {
681                name: ZRAM_COMPRESSED_BYTES.to_string(),
682                populated_size: 21,
683                committed_size: 21,
684                vmos: None,
685            },
686        ];
687
688        assert_eq!(digest.buckets, expected_buckets);
689        Ok(())
690    }
691
692    #[test]
693    fn test_digest_with_matching_process() -> Result<(), anyhow::Error> {
694        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
695        let digest = {
696            let mut digest = Digest::compute(
697                &get_attribution_data(),
698                &kernel_stats,
699                &kernel_stats_compression,
700                &vec![BucketDefinition {
701                    name: "matched".to_string(),
702                    process: Some(Regex::new("matched")?),
703                    vmo: None,
704                    principal: None,
705                    event_code: Default::default(),
706                }],
707                true,
708            )
709            .unwrap();
710            sort_buckets_for_assert(&mut digest);
711            digest
712        };
713        let expected_buckets = vec![
714            // Both VMOs are matched => 512 + 512 = 1024
715            Bucket {
716                name: "matched".to_string(),
717                populated_size: 4096,
718                committed_size: 1024,
719                vmos: Some(vec![
720                    NamedVmo {
721                        name: ZXName::from_string_lossy("matched"),
722                        populated_size: 2048,
723                        committed_size: 512,
724                        principals: vec!["principal".to_owned()],
725                    },
726                    NamedVmo {
727                        name: ZXName::from_string_lossy("resource"),
728                        populated_size: 2048,
729                        committed_size: 512,
730                        principals: vec![],
731                    },
732                ]),
733            },
734            // No unmatched VMO
735            Bucket {
736                name: UNDIGESTED.to_string(),
737                populated_size: 0,
738                committed_size: 0,
739                vmos: Some(vec![]),
740            },
741            // Two matched VMO => 10000 - 512 - 512 = 8976
742            Bucket {
743                name: ORPHANED.to_string(),
744                populated_size: 8976,
745                committed_size: 8976,
746                vmos: None,
747            },
748            // wired + heap + mmu + ipc + other => 3 + 4 + 7 + 8 + 9 = 31
749            Bucket { name: KERNEL.to_string(), populated_size: 31, committed_size: 31, vmos: None },
750            Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
751            Bucket {
752                name: PAGER_TOTAL.to_string(),
753                populated_size: 14,
754                committed_size: 14,
755                vmos: None,
756            },
757            Bucket {
758                name: PAGER_NEWEST.to_string(),
759                populated_size: 15,
760                committed_size: 15,
761                vmos: None,
762            },
763            Bucket {
764                name: PAGER_OLDEST.to_string(),
765                populated_size: 16,
766                committed_size: 16,
767                vmos: None,
768            },
769            Bucket {
770                name: DISCARDABLE_LOCKED.to_string(),
771                populated_size: 18,
772                committed_size: 18,
773                vmos: None,
774            },
775            Bucket {
776                name: DISCARDABLE_UNLOCKED.to_string(),
777                populated_size: 19,
778                committed_size: 19,
779                vmos: None,
780            },
781            Bucket {
782                name: ZRAM_COMPRESSED_BYTES.to_string(),
783                populated_size: 21,
784                committed_size: 21,
785                vmos: None,
786            },
787        ];
788
789        assert_eq!(digest.buckets, expected_buckets);
790        Ok(())
791    }
792
793    #[test]
794    fn test_digest_with_matching_principal() -> Result<(), anyhow::Error> {
795        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
796        let digest = {
797            let mut digest = Digest::compute(
798                &get_attribution_data(),
799                &kernel_stats,
800                &kernel_stats_compression,
801                &vec![BucketDefinition {
802                    name: "matched".to_string(),
803                    process: None,
804                    vmo: None,
805                    principal: Some(Regex::new("principal")?),
806                    event_code: Default::default(),
807                }],
808                true,
809            )
810            .unwrap();
811            sort_buckets_for_assert(&mut digest);
812            digest
813        };
814        let expected_buckets = vec![
815            // One VMO is matched, the other is not
816            Bucket {
817                name: "matched".to_string(),
818                populated_size: 2048,
819                committed_size: 512,
820                vmos: Some(vec![NamedVmo {
821                    name: ZXName::from_string_lossy("matched"),
822                    populated_size: 2048,
823                    committed_size: 512,
824                    principals: vec!["principal".to_owned()],
825                }]),
826            },
827            // One unmatched VMO
828            Bucket {
829                name: UNDIGESTED.to_string(),
830                populated_size: 2048,
831                committed_size: 512,
832                vmos: Some(vec![NamedVmo {
833                    name: ZXName::from_string_lossy("resource"),
834                    populated_size: 2048,
835                    committed_size: 512,
836                    principals: vec![],
837                }]),
838            },
839            // One matched VMO, one unmatched VMO //=> 10000 - 512 - 512 = 8976
840            Bucket {
841                name: ORPHANED.to_string(),
842                populated_size: 8976,
843                committed_size: 8976,
844                vmos: None,
845            },
846            // wired + heap + mmu + ipc + other => 3 + 4 + 7 + 8 + 9 = 31
847            Bucket { name: KERNEL.to_string(), populated_size: 31, committed_size: 31, vmos: None },
848            Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
849            Bucket {
850                name: PAGER_TOTAL.to_string(),
851                populated_size: 14,
852                committed_size: 14,
853                vmos: None,
854            },
855            Bucket {
856                name: PAGER_NEWEST.to_string(),
857                populated_size: 15,
858                committed_size: 15,
859                vmos: None,
860            },
861            Bucket {
862                name: PAGER_OLDEST.to_string(),
863                populated_size: 16,
864                committed_size: 16,
865                vmos: None,
866            },
867            Bucket {
868                name: DISCARDABLE_LOCKED.to_string(),
869                populated_size: 18,
870                committed_size: 18,
871                vmos: None,
872            },
873            Bucket {
874                name: DISCARDABLE_UNLOCKED.to_string(),
875                populated_size: 19,
876                committed_size: 19,
877                vmos: None,
878            },
879            Bucket {
880                name: ZRAM_COMPRESSED_BYTES.to_string(),
881                populated_size: 21,
882                committed_size: 21,
883                vmos: None,
884            },
885        ];
886
887        assert_eq!(digest.buckets, expected_buckets);
888        Ok(())
889    }
890
891    #[test]
892    fn test_digest_with_matching_principal_process_and_vmo() -> Result<(), anyhow::Error> {
893        let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
894        let digest = {
895            let mut digest = Digest::compute(
896                &get_attribution_data(),
897                &kernel_stats,
898                &kernel_stats_compression,
899                &vec![BucketDefinition {
900                    name: "matched".to_string(),
901                    process: Some(Regex::new("matched")?),
902                    vmo: Some(Regex::new("matched")?),
903                    principal: Some(Regex::new("principal")?),
904                    event_code: Default::default(),
905                }],
906                true,
907            )
908            .unwrap();
909            sort_buckets_for_assert(&mut digest);
910            digest
911        };
912        let expected_buckets = vec![
913            // One VMO is matched, the other is not
914            Bucket {
915                name: "matched".to_string(),
916                populated_size: 2048,
917                committed_size: 512,
918                vmos: Some(vec![NamedVmo {
919                    name: ZXName::from_string_lossy("matched"),
920                    populated_size: 2048,
921                    committed_size: 512,
922                    principals: vec!["principal".to_owned()],
923                }]),
924            },
925            // One unmatched VMO
926            Bucket {
927                name: UNDIGESTED.to_string(),
928                populated_size: 2048,
929                committed_size: 512,
930                vmos: Some(vec![NamedVmo {
931                    name: ZXName::from_string_lossy("resource"),
932                    populated_size: 2048,
933                    committed_size: 512,
934                    principals: vec![],
935                }]),
936            },
937            // One matched VMO, one unmatched VMO => 10000 - 512 - 512 = 8976
938            Bucket {
939                name: ORPHANED.to_string(),
940                populated_size: 8976,
941                committed_size: 8976,
942                vmos: None,
943            },
944            // wired + heap + mmu + ipc + other => 3 + 4 + 7 + 8 + 9 = 31
945            Bucket { name: KERNEL.to_string(), populated_size: 31, committed_size: 31, vmos: None },
946            Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
947            Bucket {
948                name: PAGER_TOTAL.to_string(),
949                populated_size: 14,
950                committed_size: 14,
951                vmos: None,
952            },
953            Bucket {
954                name: PAGER_NEWEST.to_string(),
955                populated_size: 15,
956                committed_size: 15,
957                vmos: None,
958            },
959            Bucket {
960                name: PAGER_OLDEST.to_string(),
961                populated_size: 16,
962                committed_size: 16,
963                vmos: None,
964            },
965            Bucket {
966                name: DISCARDABLE_LOCKED.to_string(),
967                populated_size: 18,
968                committed_size: 18,
969                vmos: None,
970            },
971            Bucket {
972                name: DISCARDABLE_UNLOCKED.to_string(),
973                populated_size: 19,
974                committed_size: 19,
975                vmos: None,
976            },
977            Bucket {
978                name: ZRAM_COMPRESSED_BYTES.to_string(),
979                populated_size: 21,
980                committed_size: 21,
981                vmos: None,
982            },
983        ];
984
985        assert_eq!(digest.buckets, expected_buckets);
986        Ok(())
987    }
988}