Skip to main content

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