1use crate::{ProcessedAttributionData, ZXName};
6use anyhow::Result;
7use bstr::ByteSlice;
8use fidl_fuchsia_kernel_common as fkernel;
9use fidl_fuchsia_memory_attribution_plugin_common as fplugin;
10use regex_lite::Regex;
11use rustc_hash::FxHashMap;
12use serde::de::Error;
13use serde::{Deserialize, Deserializer, Serialize};
14use smallvec::SmallVec;
15use std::collections::hash_map::Entry::Occupied;
16#[cfg(target_os = "fuchsia")]
17use {crate::CATEGORY_MEMORY_CAPTURE, fuchsia_trace::duration};
18
19const UNDIGESTED: &str = "Undigested";
20const ORPHANED: &str = "Orphaned";
21const KERNEL: &str = "Kernel";
22const FREE: &str = "Free";
23const PAGER_TOTAL: &str = "[Addl]PagerTotal";
24const PAGER_NEWEST: &str = "[Addl]PagerNewest";
25const PAGER_OLDEST: &str = "[Addl]PagerOldest";
26const DISCARDABLE_LOCKED: &str = "[Addl]DiscardableLocked";
27const DISCARDABLE_UNLOCKED: &str = "[Addl]DiscardableUnlocked";
28const ZRAM_COMPRESSED_BYTES: &str = "[Addl]ZramCompressedBytes";
29const POPULATED_ANONYMOUS_BYTES: &str = "[Addl]PopulatedAnonymousBytes";
30
31#[derive(Clone, Debug, Deserialize)]
39pub struct BucketDefinition {
40 pub name: String,
41 #[serde(deserialize_with = "deserialize_regex")]
42 pub process: Option<Regex>,
43 #[serde(deserialize_with = "deserialize_regex")]
44 pub vmo: Option<Regex>,
45 #[serde(default, deserialize_with = "deserialize_regex")]
46 pub principal: Option<Regex>,
47 pub event_code: u64,
48}
49
50impl BucketDefinition {
51 fn process_match(&self, process: &ZXName) -> bool {
53 self.process.as_ref().is_none_or(|process_regex| {
54 process
55 .as_bstr()
56 .to_str()
57 .is_ok_and(|process_name| process_regex.is_match(process_name))
58 })
59 }
60
61 fn vmo_match(&self, vmo: &ZXName) -> bool {
63 self.vmo.as_ref().is_none_or(|vmo_regex| {
64 vmo.as_bstr().to_str().is_ok_and(|vmo_name| vmo_regex.is_match(vmo_name))
65 })
66 }
67
68 fn principals_match(&self, principals: &[&str]) -> bool {
70 self.principal.as_ref().is_none_or(|a| principals.iter().any(|name| a.is_match(name)))
71 }
72}
73
74fn deserialize_regex<'de, D>(d: D) -> Result<Option<Regex>, D::Error>
76where
77 D: Deserializer<'de>,
78{
79 Option::<String>::deserialize(d)
81 .and_then(|os| {
83 os
84 .map(|s| {
86 Regex::new(&s)
87 .map_err(D::Error::custom)
90 })
91 .transpose()
94 })
95}
96
97#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
99pub struct Bucket {
100 pub name: String,
101 pub populated_size: u64,
102 pub committed_size: u64,
103 pub vmos: Option<Vec<NamedVmo>>,
104}
105
106#[derive(Debug, Default, PartialEq, Eq, Serialize)]
109pub struct Digest {
110 pub buckets: Vec<Bucket>,
111}
112
113struct UndigestedVmo<'a> {
115 populated_size: u64,
116 committed_size: u64,
117 name: &'a ZXName,
118 principals: &'a [&'a str],
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
122pub struct NamedVmo {
124 pub name: ZXName,
125 pub populated_size: u64,
126 pub committed_size: u64,
127 pub principals: Vec<String>,
128}
129
130impl Digest {
131 pub fn compute(
134 attribution_data: &ProcessedAttributionData,
135 kmem_stats: &fkernel::MemoryStats,
136 kmem_stats_compression: &fkernel::MemoryStatsCompression,
137 bucket_definitions: &[BucketDefinition],
138 detailed_vmos: bool,
139 ) -> Result<Digest> {
140 #[cfg(target_os = "fuchsia")]
141 duration!(CATEGORY_MEMORY_CAPTURE, c"Digest::compute");
142
143 let owners: FxHashMap<u64, SmallVec<[&str; 1]>> = {
158 let mut owners: FxHashMap<u64, SmallVec<[&str; 1]>> = FxHashMap::default();
162 for (_, p) in &attribution_data.principals {
163 let p_name = p.name();
164 for r in &p.resources {
165 owners.entry(*r).or_default().push(p_name);
166 }
167 }
168 owners
169 };
170
171 let mut populated_reclaimable_bytes = 0;
172 let mut undigested_vmos: FxHashMap<u64, UndigestedVmo<'_>> = attribution_data
173 .resources
174 .iter()
175 .filter_map(|(koid, r)| match &r.resource.resource_type {
176 fplugin::ResourceType::Vmo(vmo) => {
177 attribution_data.resource_names.get(r.resource.name_index).and_then(|name| {
178 let populated_size = vmo.scaled_populated_bytes?;
179 let committed_size = vmo.scaled_committed_bytes?;
180 if vmo.flags.map_or(false, |flags| {
181 flags
182 & (zx_types::ZX_INFO_VMO_PAGER_BACKED
183 | zx_types::ZX_INFO_VMO_DISCARDABLE)
184 != 0
185 }) {
186 populated_reclaimable_bytes += populated_size;
187 }
188 Some((
189 *koid,
190 UndigestedVmo {
191 name,
192 populated_size,
193 committed_size,
194 principals: owners.get(koid).map_or(&[], |v| v.as_slice()),
195 },
196 ))
197 })
198 }
199 _ => None,
200 })
201 .collect();
202 let processes: Vec<(&ZXName, &fplugin::Process)> = attribution_data
203 .resources
204 .values()
205 .filter_map(|r| match &r.resource.resource_type {
206 fplugin::ResourceType::Process(process) => attribution_data
207 .resource_names
208 .get(r.resource.name_index)
209 .map(|name| (name, process)),
210 _ => None,
211 })
212 .collect();
213
214 let mut buckets: Vec<Bucket> = bucket_definitions
215 .iter()
216 .map(|bd| {
217 let mut bucket = Bucket {
218 name: bd.name.to_owned(),
219 populated_size: 0,
220 committed_size: 0,
221 vmos: None,
222 };
223 processes.iter().for_each(|(process_name, process)| {
224 if bd.process_match(process_name) {
225 for koid in process.vmos.iter().flatten() {
226 let (populated_size, committed_size) = match undigested_vmos
227 .entry(*koid)
228 {
229 Occupied(e) => {
230 let UndigestedVmo { name, principals, .. } = e.get();
231 if bd.vmo_match(&name) && bd.principals_match(principals) {
232 let (_, vmo) = e.remove_entry();
233 if detailed_vmos {
234 bucket.vmos.get_or_insert_default().push(NamedVmo {
235 name: vmo.name.clone(),
236 populated_size: vmo.populated_size,
237 committed_size: vmo.committed_size,
238 principals: vmo
239 .principals
240 .iter()
241 .map(|&name| name.to_owned())
242 .collect(),
243 });
244 }
245 (vmo.populated_size, vmo.committed_size)
246 } else {
247 (0, 0)
248 }
249 }
250 _ => (0, 0),
251 };
252 bucket.committed_size += committed_size;
253 bucket.populated_size += populated_size;
254 }
255 };
256 });
257 bucket
258 })
259 .collect();
260
261 let undigested = {
264 let (populated_size, committed_size) = undigested_vmos
265 .values()
266 .map(|UndigestedVmo { populated_size, committed_size, .. }| {
267 (*populated_size, *committed_size)
268 })
269 .fold((0, 0), |(total_populated, total_committed), (populated, committed)| {
270 (total_populated + populated, total_committed + committed)
271 });
272
273 Bucket {
274 name: UNDIGESTED.to_string(),
275 populated_size: populated_size,
276 committed_size,
277 vmos: if detailed_vmos {
278 Some(
279 undigested_vmos
280 .values()
281 .map(|vmo| NamedVmo {
282 name: vmo.name.clone(),
283 populated_size: vmo.populated_size,
284 committed_size: vmo.committed_size,
285 principals: vmo
286 .principals
287 .iter()
288 .map(|&name| name.to_owned())
289 .collect(),
290 })
291 .collect(),
292 )
293 } else {
294 None
295 },
296 }
297 };
298
299 let total_vmo_size: u64 = undigested.committed_size
300 + buckets.iter().map(|Bucket { committed_size, .. }| committed_size).sum::<u64>();
301
302 buckets.extend([
305 undigested,
306 {
309 let size = kmem_stats.vmo_bytes.unwrap_or(0).saturating_sub(total_vmo_size);
310 Bucket {
311 name: ORPHANED.to_string(),
312 populated_size: size,
313 committed_size: size,
314 vmos: None,
315 }
316 },
317 {
319 let size = (|| {
320 Some(
321 kmem_stats.wired_bytes?
322 + kmem_stats.total_heap_bytes?
323 + kmem_stats.mmu_overhead_bytes?
324 + kmem_stats.ipc_bytes?
325 + kmem_stats.other_bytes?
326 + kmem_stats.slab_bytes?
327 + kmem_stats.cache_bytes?,
328 )
329 })()
330 .unwrap_or(0);
331 Bucket {
332 name: KERNEL.to_string(),
333 populated_size: size,
334 committed_size: size,
335 vmos: None,
336 }
337 },
338 {
340 let size = kmem_stats.free_bytes.unwrap_or(0);
341 Bucket {
342 name: FREE.to_string(),
343 populated_size: size,
344 committed_size: size,
345 vmos: None,
346 }
347 },
348 {
350 let size = kmem_stats.vmo_reclaim_total_bytes.unwrap_or(0);
351 Bucket {
352 name: PAGER_TOTAL.to_string(),
353 populated_size: size,
354 committed_size: size,
355 vmos: None,
356 }
357 },
358 {
359 let size = kmem_stats.vmo_reclaim_newest_bytes.unwrap_or(0);
360 Bucket {
361 name: PAGER_NEWEST.to_string(),
362 populated_size: size,
363 committed_size: size,
364 vmos: None,
365 }
366 },
367 {
368 let size = kmem_stats.vmo_reclaim_oldest_bytes.unwrap_or(0);
369 Bucket {
370 name: PAGER_OLDEST.to_string(),
371 populated_size: size,
372 committed_size: size,
373 vmos: None,
374 }
375 },
376 {
378 let size = kmem_stats.vmo_discardable_locked_bytes.unwrap_or(0);
379 Bucket {
380 name: DISCARDABLE_LOCKED.to_string(),
381 populated_size: size,
382 committed_size: size,
383 vmos: None,
384 }
385 },
386 {
387 let size = kmem_stats.vmo_discardable_unlocked_bytes.unwrap_or(0);
388 Bucket {
389 name: DISCARDABLE_UNLOCKED.to_string(),
390 populated_size: size,
391 committed_size: size,
392 vmos: None,
393 }
394 },
395 {
397 let size = kmem_stats_compression.compressed_storage_bytes.unwrap_or(0);
398 Bucket {
399 name: ZRAM_COMPRESSED_BYTES.to_string(),
400 populated_size: size,
401 committed_size: size,
402 vmos: None,
403 }
404 },
405 {
407 let size = (kmem_stats.total_bytes.unwrap_or(0)
408 + kmem_stats_compression.uncompressed_storage_bytes.unwrap_or(0))
409 .saturating_sub(kmem_stats.free_bytes.unwrap_or(0))
410 .saturating_sub(kmem_stats.zram_bytes.unwrap_or(0))
411 .saturating_sub(populated_reclaimable_bytes);
412
413 Bucket {
414 name: POPULATED_ANONYMOUS_BYTES.to_string(),
415 populated_size: size,
416 committed_size: size,
417 vmos: None,
418 }
419 },
420 ]);
421 Ok(Digest { buckets })
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::{
429 Attribution, AttributionData, GlobalPrincipalIdentifier, Principal, PrincipalDescription,
430 PrincipalType, ProcessedAttributionData, Resource, ResourceReference, attribute_vmos,
431 };
432 use fidl_fuchsia_memory_attribution_plugin_common as fplugin;
433 use regex_lite::Regex;
434
435 fn get_attribution_data() -> ProcessedAttributionData {
436 attribute_vmos(AttributionData {
437 principals_vec: vec![
438 Principal {
439 identifier: GlobalPrincipalIdentifier::new_for_test(1),
440 description: Some(PrincipalDescription::Component("principal".to_owned())),
441 principal_type: PrincipalType::Runnable,
442 parent: Some(GlobalPrincipalIdentifier::new_for_test(2)),
443 },
444 Principal {
445 identifier: GlobalPrincipalIdentifier::new_for_test(2),
446 description: Some(PrincipalDescription::Component("parent".to_owned())),
447 principal_type: PrincipalType::Runnable,
448 parent: None,
449 },
450 ],
451 resources_vec: vec![
452 Resource {
453 koid: 10,
454 name_index: 0,
455 resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
456 parent: None,
457 private_committed_bytes: Some(1024),
458 private_populated_bytes: Some(2048),
459 scaled_committed_bytes: Some(512),
460 scaled_populated_bytes: Some(2048),
461 total_committed_bytes: Some(1024),
462 total_populated_bytes: Some(2048),
463 ..Default::default()
464 }),
465 },
466 Resource {
467 koid: 20,
468 name_index: 1,
469 resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
470 parent: None,
471 private_committed_bytes: Some(1024),
472 private_populated_bytes: Some(2048),
473 scaled_committed_bytes: Some(512),
474 scaled_populated_bytes: Some(2048),
475 total_committed_bytes: Some(1024),
476 total_populated_bytes: Some(2048),
477 ..Default::default()
478 }),
479 },
480 Resource {
481 koid: 30,
482 name_index: 1,
483 resource_type: fplugin::ResourceType::Process(fplugin::Process {
484 vmos: Some(vec![10, 20]),
485 ..Default::default()
486 }),
487 },
488 ],
489 resource_names: vec![
490 ZXName::try_from_bytes(b"resource").unwrap(),
491 ZXName::try_from_bytes(b"matched").unwrap(),
492 ],
493 attributions: vec![Attribution {
494 source: GlobalPrincipalIdentifier::new_for_test(1),
495 subject: GlobalPrincipalIdentifier::new_for_test(1),
496 resources: vec![ResourceReference::KernelObject(20)],
497 }],
498 })
499 }
500
501 fn get_kernel_stats() -> (fkernel::MemoryStats, fkernel::MemoryStatsCompression) {
502 (
503 fkernel::MemoryStats {
504 total_bytes: Some(20),
505 free_bytes: Some(2),
506 wired_bytes: Some(3),
507 total_heap_bytes: Some(4),
508 free_heap_bytes: Some(5),
509 vmo_bytes: Some(10000),
510 mmu_overhead_bytes: Some(7),
511 ipc_bytes: Some(8),
512 other_bytes: Some(9),
513 free_loaned_bytes: Some(10),
514 cache_bytes: Some(11),
515 slab_bytes: Some(12),
516 zram_bytes: Some(13),
517 vmo_reclaim_total_bytes: Some(14),
518 vmo_reclaim_newest_bytes: Some(15),
519 vmo_reclaim_oldest_bytes: Some(16),
520 vmo_reclaim_disabled_bytes: Some(17),
521 vmo_discardable_locked_bytes: Some(18),
522 vmo_discardable_unlocked_bytes: Some(19),
523 ..Default::default()
524 },
525 fkernel::MemoryStatsCompression {
526 uncompressed_storage_bytes: Some(1),
527 compressed_storage_bytes: Some(21),
528 compressed_fragmentation_bytes: Some(22),
529 compression_time: Some(23),
530 decompression_time: Some(24),
531 total_page_compression_attempts: Some(25),
532 failed_page_compression_attempts: Some(26),
533 total_page_decompressions: Some(27),
534 compressed_page_evictions: Some(28),
535 eager_page_compressions: Some(29),
536 memory_pressure_page_compressions: Some(30),
537 critical_memory_page_compressions: Some(31),
538 pages_decompressed_unit_ns: Some(32),
539 pages_decompressed_within_log_time: Some([40, 41, 42, 43, 44, 45, 46, 47]),
540 ..Default::default()
541 },
542 )
543 }
544
545 fn sort_buckets_for_assert(digest: &mut Digest) {
546 for bucket in digest.buckets.iter_mut() {
547 for vmos in bucket.vmos.iter_mut() {
548 vmos.sort_by(|vmo1, vmo2| vmo1.name.cmp(&vmo2.name));
549 }
550 }
551 }
552
553 #[test]
554 fn test_digest_no_definitions() {
555 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
556 let digest = {
557 let mut digest = Digest::compute(
558 &get_attribution_data(),
559 &kernel_stats,
560 &kernel_stats_compression,
561 &vec![],
562 true,
563 )
564 .unwrap();
565 sort_buckets_for_assert(&mut digest);
566 digest
567 };
568 let expected_buckets = vec![
569 Bucket {
571 name: UNDIGESTED.to_string(),
572 populated_size: 4096,
573 committed_size: 1024,
574 vmos: Some(vec![
575 NamedVmo {
576 name: ZXName::from_string_lossy("matched"),
577 populated_size: 2048,
578 committed_size: 512,
579 principals: vec!["principal".to_string()],
580 },
581 NamedVmo {
582 name: ZXName::from_string_lossy("resource"),
583 populated_size: 2048,
584 committed_size: 512,
585 principals: vec![],
586 },
587 ]),
588 },
589 Bucket {
591 name: ORPHANED.to_string(),
592 populated_size: 8976,
593 committed_size: 8976,
594 vmos: None,
595 },
596 Bucket { name: KERNEL.to_string(), populated_size: 54, committed_size: 54, vmos: None },
598 Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
599 Bucket {
600 name: PAGER_TOTAL.to_string(),
601 populated_size: 14,
602 committed_size: 14,
603 vmos: None,
604 },
605 Bucket {
606 name: PAGER_NEWEST.to_string(),
607 populated_size: 15,
608 committed_size: 15,
609 vmos: None,
610 },
611 Bucket {
612 name: PAGER_OLDEST.to_string(),
613 populated_size: 16,
614 committed_size: 16,
615 vmos: None,
616 },
617 Bucket {
618 name: DISCARDABLE_LOCKED.to_string(),
619 populated_size: 18,
620 committed_size: 18,
621 vmos: None,
622 },
623 Bucket {
624 name: DISCARDABLE_UNLOCKED.to_string(),
625 populated_size: 19,
626 committed_size: 19,
627 vmos: None,
628 },
629 Bucket {
630 name: ZRAM_COMPRESSED_BYTES.to_string(),
631 populated_size: 21,
632 committed_size: 21,
633 vmos: None,
634 },
635 Bucket {
636 name: POPULATED_ANONYMOUS_BYTES.to_string(),
637 populated_size: 6,
638 committed_size: 6,
639 vmos: None,
640 },
641 ];
642
643 assert_eq!(digest.buckets, expected_buckets);
644 }
645
646 #[test]
647 fn test_digest_with_matching_vmo() -> Result<(), anyhow::Error> {
648 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
649 let digest = {
650 let mut digest = Digest::compute(
651 &get_attribution_data(),
652 &kernel_stats,
653 &kernel_stats_compression,
654 &vec![BucketDefinition {
655 name: "matched".to_string(),
656 process: None,
657 vmo: Some(Regex::new("matched")?),
658 principal: None,
659 event_code: Default::default(),
660 }],
661 true,
662 )
663 .unwrap();
664 sort_buckets_for_assert(&mut digest);
665 digest
666 };
667 let expected_buckets = vec![
668 Bucket {
670 name: "matched".to_string(),
671 populated_size: 2048,
672 committed_size: 512,
673 vmos: Some(vec![NamedVmo {
674 name: ZXName::from_string_lossy("matched"),
675 populated_size: 2048,
676 committed_size: 512,
677 principals: vec!["principal".to_owned()],
678 }]),
679 },
680 Bucket {
682 name: UNDIGESTED.to_string(),
683 populated_size: 2048,
684 committed_size: 512,
685 vmos: Some(vec![NamedVmo {
686 name: ZXName::from_string_lossy("resource"),
687 populated_size: 2048,
688 committed_size: 512,
689 principals: vec![],
690 }]),
691 },
692 Bucket {
694 name: ORPHANED.to_string(),
695 populated_size: 8976,
696 committed_size: 8976,
697 vmos: None,
698 },
699 Bucket { name: KERNEL.to_string(), populated_size: 54, committed_size: 54, vmos: None },
701 Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
702 Bucket {
703 name: PAGER_TOTAL.to_string(),
704 populated_size: 14,
705 committed_size: 14,
706 vmos: None,
707 },
708 Bucket {
709 name: PAGER_NEWEST.to_string(),
710 populated_size: 15,
711 committed_size: 15,
712 vmos: None,
713 },
714 Bucket {
715 name: PAGER_OLDEST.to_string(),
716 populated_size: 16,
717 committed_size: 16,
718 vmos: None,
719 },
720 Bucket {
721 name: DISCARDABLE_LOCKED.to_string(),
722 populated_size: 18,
723 committed_size: 18,
724 vmos: None,
725 },
726 Bucket {
727 name: DISCARDABLE_UNLOCKED.to_string(),
728 populated_size: 19,
729 committed_size: 19,
730 vmos: None,
731 },
732 Bucket {
733 name: ZRAM_COMPRESSED_BYTES.to_string(),
734 populated_size: 21,
735 committed_size: 21,
736 vmos: None,
737 },
738 Bucket {
739 name: POPULATED_ANONYMOUS_BYTES.to_string(),
740 populated_size: 6,
741 committed_size: 6,
742 vmos: None,
743 },
744 ];
745
746 assert_eq!(digest.buckets, expected_buckets);
747 Ok(())
748 }
749
750 #[test]
751 fn test_digest_with_matching_process() -> Result<(), anyhow::Error> {
752 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
753 let digest = {
754 let mut digest = Digest::compute(
755 &get_attribution_data(),
756 &kernel_stats,
757 &kernel_stats_compression,
758 &vec![BucketDefinition {
759 name: "matched".to_string(),
760 process: Some(Regex::new("matched")?),
761 vmo: None,
762 principal: None,
763 event_code: Default::default(),
764 }],
765 true,
766 )
767 .unwrap();
768 sort_buckets_for_assert(&mut digest);
769 digest
770 };
771 let expected_buckets = vec![
772 Bucket {
774 name: "matched".to_string(),
775 populated_size: 4096,
776 committed_size: 1024,
777 vmos: Some(vec![
778 NamedVmo {
779 name: ZXName::from_string_lossy("matched"),
780 populated_size: 2048,
781 committed_size: 512,
782 principals: vec!["principal".to_owned()],
783 },
784 NamedVmo {
785 name: ZXName::from_string_lossy("resource"),
786 populated_size: 2048,
787 committed_size: 512,
788 principals: vec![],
789 },
790 ]),
791 },
792 Bucket {
794 name: UNDIGESTED.to_string(),
795 populated_size: 0,
796 committed_size: 0,
797 vmos: Some(vec![]),
798 },
799 Bucket {
801 name: ORPHANED.to_string(),
802 populated_size: 8976,
803 committed_size: 8976,
804 vmos: None,
805 },
806 Bucket { name: KERNEL.to_string(), populated_size: 54, committed_size: 54, vmos: None },
808 Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
809 Bucket {
810 name: PAGER_TOTAL.to_string(),
811 populated_size: 14,
812 committed_size: 14,
813 vmos: None,
814 },
815 Bucket {
816 name: PAGER_NEWEST.to_string(),
817 populated_size: 15,
818 committed_size: 15,
819 vmos: None,
820 },
821 Bucket {
822 name: PAGER_OLDEST.to_string(),
823 populated_size: 16,
824 committed_size: 16,
825 vmos: None,
826 },
827 Bucket {
828 name: DISCARDABLE_LOCKED.to_string(),
829 populated_size: 18,
830 committed_size: 18,
831 vmos: None,
832 },
833 Bucket {
834 name: DISCARDABLE_UNLOCKED.to_string(),
835 populated_size: 19,
836 committed_size: 19,
837 vmos: None,
838 },
839 Bucket {
840 name: ZRAM_COMPRESSED_BYTES.to_string(),
841 populated_size: 21,
842 committed_size: 21,
843 vmos: None,
844 },
845 Bucket {
846 name: POPULATED_ANONYMOUS_BYTES.to_string(),
847 populated_size: 6,
848 committed_size: 6,
849 vmos: None,
850 },
851 ];
852
853 assert_eq!(digest.buckets, expected_buckets);
854 Ok(())
855 }
856
857 #[test]
858 fn test_digest_with_matching_principal() -> Result<(), anyhow::Error> {
859 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
860 let digest = {
861 let mut digest = Digest::compute(
862 &get_attribution_data(),
863 &kernel_stats,
864 &kernel_stats_compression,
865 &vec![BucketDefinition {
866 name: "matched".to_string(),
867 process: None,
868 vmo: None,
869 principal: Some(Regex::new("principal")?),
870 event_code: Default::default(),
871 }],
872 true,
873 )
874 .unwrap();
875 sort_buckets_for_assert(&mut digest);
876 digest
877 };
878 let expected_buckets = vec![
879 Bucket {
881 name: "matched".to_string(),
882 populated_size: 2048,
883 committed_size: 512,
884 vmos: Some(vec![NamedVmo {
885 name: ZXName::from_string_lossy("matched"),
886 populated_size: 2048,
887 committed_size: 512,
888 principals: vec!["principal".to_owned()],
889 }]),
890 },
891 Bucket {
893 name: UNDIGESTED.to_string(),
894 populated_size: 2048,
895 committed_size: 512,
896 vmos: Some(vec![NamedVmo {
897 name: ZXName::from_string_lossy("resource"),
898 populated_size: 2048,
899 committed_size: 512,
900 principals: vec![],
901 }]),
902 },
903 Bucket {
905 name: ORPHANED.to_string(),
906 populated_size: 8976,
907 committed_size: 8976,
908 vmos: None,
909 },
910 Bucket { name: KERNEL.to_string(), populated_size: 54, committed_size: 54, vmos: None },
912 Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
913 Bucket {
914 name: PAGER_TOTAL.to_string(),
915 populated_size: 14,
916 committed_size: 14,
917 vmos: None,
918 },
919 Bucket {
920 name: PAGER_NEWEST.to_string(),
921 populated_size: 15,
922 committed_size: 15,
923 vmos: None,
924 },
925 Bucket {
926 name: PAGER_OLDEST.to_string(),
927 populated_size: 16,
928 committed_size: 16,
929 vmos: None,
930 },
931 Bucket {
932 name: DISCARDABLE_LOCKED.to_string(),
933 populated_size: 18,
934 committed_size: 18,
935 vmos: None,
936 },
937 Bucket {
938 name: DISCARDABLE_UNLOCKED.to_string(),
939 populated_size: 19,
940 committed_size: 19,
941 vmos: None,
942 },
943 Bucket {
944 name: ZRAM_COMPRESSED_BYTES.to_string(),
945 populated_size: 21,
946 committed_size: 21,
947 vmos: None,
948 },
949 Bucket {
950 name: POPULATED_ANONYMOUS_BYTES.to_string(),
951 populated_size: 6,
952 committed_size: 6,
953 vmos: None,
954 },
955 ];
956
957 assert_eq!(digest.buckets, expected_buckets);
958 Ok(())
959 }
960
961 #[test]
962 fn test_digest_with_matching_principal_process_and_vmo() -> Result<(), anyhow::Error> {
963 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
964 let digest = {
965 let mut digest = Digest::compute(
966 &get_attribution_data(),
967 &kernel_stats,
968 &kernel_stats_compression,
969 &vec![BucketDefinition {
970 name: "matched".to_string(),
971 process: Some(Regex::new("matched")?),
972 vmo: Some(Regex::new("matched")?),
973 principal: Some(Regex::new("principal")?),
974 event_code: Default::default(),
975 }],
976 true,
977 )
978 .unwrap();
979 sort_buckets_for_assert(&mut digest);
980 digest
981 };
982 let expected_buckets = vec![
983 Bucket {
985 name: "matched".to_string(),
986 populated_size: 2048,
987 committed_size: 512,
988 vmos: Some(vec![NamedVmo {
989 name: ZXName::from_string_lossy("matched"),
990 populated_size: 2048,
991 committed_size: 512,
992 principals: vec!["principal".to_owned()],
993 }]),
994 },
995 Bucket {
997 name: UNDIGESTED.to_string(),
998 populated_size: 2048,
999 committed_size: 512,
1000 vmos: Some(vec![NamedVmo {
1001 name: ZXName::from_string_lossy("resource"),
1002 populated_size: 2048,
1003 committed_size: 512,
1004 principals: vec![],
1005 }]),
1006 },
1007 Bucket {
1009 name: ORPHANED.to_string(),
1010 populated_size: 8976,
1011 committed_size: 8976,
1012 vmos: None,
1013 },
1014 Bucket { name: KERNEL.to_string(), populated_size: 54, committed_size: 54, vmos: None },
1016 Bucket { name: FREE.to_string(), populated_size: 2, committed_size: 2, vmos: None },
1017 Bucket {
1018 name: PAGER_TOTAL.to_string(),
1019 populated_size: 14,
1020 committed_size: 14,
1021 vmos: None,
1022 },
1023 Bucket {
1024 name: PAGER_NEWEST.to_string(),
1025 populated_size: 15,
1026 committed_size: 15,
1027 vmos: None,
1028 },
1029 Bucket {
1030 name: PAGER_OLDEST.to_string(),
1031 populated_size: 16,
1032 committed_size: 16,
1033 vmos: None,
1034 },
1035 Bucket {
1036 name: DISCARDABLE_LOCKED.to_string(),
1037 populated_size: 18,
1038 committed_size: 18,
1039 vmos: None,
1040 },
1041 Bucket {
1042 name: DISCARDABLE_UNLOCKED.to_string(),
1043 populated_size: 19,
1044 committed_size: 19,
1045 vmos: None,
1046 },
1047 Bucket {
1048 name: ZRAM_COMPRESSED_BYTES.to_string(),
1049 populated_size: 21,
1050 committed_size: 21,
1051 vmos: None,
1052 },
1053 Bucket {
1054 name: POPULATED_ANONYMOUS_BYTES.to_string(),
1055 populated_size: 6,
1056 committed_size: 6,
1057 vmos: None,
1058 },
1059 ];
1060
1061 assert_eq!(digest.buckets, expected_buckets);
1062 Ok(())
1063 }
1064
1065 #[test]
1074 fn test_digest_compute_undetailed_vmos_fast_path() -> Result<(), anyhow::Error> {
1075 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
1076 let digest = Digest::compute(
1077 &get_attribution_data(),
1078 &kernel_stats,
1079 &kernel_stats_compression,
1080 &vec![BucketDefinition {
1081 name: "matched".to_string(),
1082 process: None,
1083 vmo: Some(Regex::new("matched")?),
1084 principal: None,
1085 event_code: Default::default(),
1086 }],
1087 false, )?;
1089
1090 for bucket in &digest.buckets {
1091 assert!(bucket.vmos.is_none(), "Bucket '{}' should have vmos == None", bucket.name);
1092 }
1093 let matched_bucket = digest.buckets.iter().find(|b| b.name == "matched").unwrap();
1094 assert_eq!(matched_bucket.committed_size, 512);
1095 assert_eq!(matched_bucket.populated_size, 2048);
1096 let undigested_bucket = digest.buckets.iter().find(|b| b.name == UNDIGESTED).unwrap();
1097 assert_eq!(undigested_bucket.committed_size, 512);
1098 assert_eq!(undigested_bucket.populated_size, 2048);
1099 Ok(())
1100 }
1101
1102 #[test]
1112 fn test_digest_bucket_priority_and_deduplication() -> Result<(), anyhow::Error> {
1113 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
1114 let digest = Digest::compute(
1115 &get_attribution_data(),
1116 &kernel_stats,
1117 &kernel_stats_compression,
1118 &vec![
1119 BucketDefinition {
1120 name: "first_bucket".to_string(),
1121 process: None,
1122 vmo: Some(Regex::new("matched")?),
1123 principal: None,
1124 event_code: Default::default(),
1125 },
1126 BucketDefinition {
1127 name: "second_bucket".to_string(),
1128 process: None,
1129 vmo: Some(Regex::new(".*")?),
1130 principal: None,
1131 event_code: Default::default(),
1132 },
1133 ],
1134 true,
1135 )?;
1136
1137 let b1 = digest.buckets.iter().find(|b| b.name == "first_bucket").unwrap();
1138 assert_eq!(b1.committed_size, 512); let b2 = digest.buckets.iter().find(|b| b.name == "second_bucket").unwrap();
1140 assert_eq!(b2.committed_size, 512); let undigested = digest.buckets.iter().find(|b| b.name == UNDIGESTED).unwrap();
1142 assert_eq!(undigested.committed_size, 0);
1143 Ok(())
1144 }
1145
1146 #[test]
1155 fn test_digest_multi_attribute_matching_and_partial_mismatch() -> Result<(), anyhow::Error> {
1156 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
1157 let digest = Digest::compute(
1158 &get_attribution_data(),
1159 &kernel_stats,
1160 &kernel_stats_compression,
1161 &vec![BucketDefinition {
1162 name: "partial_mismatch".to_string(),
1163 process: Some(Regex::new("matched")?),
1164 vmo: Some(Regex::new("nonexistent_vmo")?),
1165 principal: Some(Regex::new("principal")?),
1166 event_code: Default::default(),
1167 }],
1168 true,
1169 )?;
1170
1171 let b = digest.buckets.iter().find(|b| b.name == "partial_mismatch").unwrap();
1172 assert_eq!(b.committed_size, 0);
1173 let undigested = digest.buckets.iter().find(|b| b.name == UNDIGESTED).unwrap();
1174 assert_eq!(undigested.committed_size, 1024); Ok(())
1176 }
1177
1178 #[test]
1186 fn test_digest_compute_reclaimable_vmo_flags_and_anonymous_bytes() -> Result<(), anyhow::Error>
1187 {
1188 use zx_types::{ZX_INFO_VMO_DISCARDABLE, ZX_INFO_VMO_PAGER_BACKED};
1189 let mut attr_data = get_attribution_data();
1190 let vmo_res = attr_data.resources.get_mut(&20).unwrap();
1191 if let fplugin::ResourceType::Vmo(vmo) = &mut vmo_res.resource.resource_type {
1192 vmo.flags = Some(ZX_INFO_VMO_PAGER_BACKED | ZX_INFO_VMO_DISCARDABLE);
1193 }
1194
1195 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
1196 let digest =
1197 Digest::compute(&attr_data, &kernel_stats, &kernel_stats_compression, &vec![], true)?;
1198
1199 let anon_bucket =
1200 digest.buckets.iter().find(|b| b.name == POPULATED_ANONYMOUS_BYTES).unwrap();
1201 assert_eq!(anon_bucket.populated_size, 0);
1202 Ok(())
1203 }
1204
1205 #[test]
1214 fn test_digest_missing_kernel_stats_and_saturating_orphaned() -> Result<(), anyhow::Error> {
1215 let digest = Digest::compute(
1216 &get_attribution_data(),
1217 &fkernel::MemoryStats::default(),
1218 &fkernel::MemoryStatsCompression::default(),
1219 &vec![],
1220 true,
1221 )?;
1222
1223 let orphaned = digest.buckets.iter().find(|b| b.name == ORPHANED).unwrap();
1224 assert_eq!(orphaned.committed_size, 0);
1225 let kernel = digest.buckets.iter().find(|b| b.name == KERNEL).unwrap();
1226 assert_eq!(kernel.committed_size, 0);
1227 let free = digest.buckets.iter().find(|b| b.name == FREE).unwrap();
1228 assert_eq!(free.committed_size, 0);
1229 let anon = digest.buckets.iter().find(|b| b.name == POPULATED_ANONYMOUS_BYTES).unwrap();
1230 assert_eq!(anon.committed_size, 0);
1231 let zram = digest.buckets.iter().find(|b| b.name == ZRAM_COMPRESSED_BYTES).unwrap();
1232 assert_eq!(zram.committed_size, 0);
1233 Ok(())
1234 }
1235}