1use crate::fplugin::Vmo;
6use crate::{AttributionDataProvider, ResourcesVisitor, ZXName};
7use regex::bytes::Regex;
8use serde::de::Error;
9use serde::{Deserialize, Deserializer};
10use std::collections::hash_map::Entry::Occupied;
11use std::collections::HashMap;
12use {fidl_fuchsia_kernel as fkernel, fidl_fuchsia_memory_attribution_plugin as fplugin};
13
14const UNDIGESTED: &str = "Undigested";
15const ORPHANED: &str = "Orphaned";
16const KERNEL: &str = "Kernel";
17const FREE: &str = "Free";
18const PAGER_TOTAL: &str = "[Addl]PagerTotal";
19const PAGER_NEWEST: &str = "[Addl]PagerNewest";
20const PAGER_OLDEST: &str = "[Addl]PagerOldest";
21const DISCARDABLE_LOCKED: &str = "[Addl]DiscardableLocked";
22const DISCARDABLE_UNLOCKED: &str = "[Addl]DiscardableUnlocked";
23const ZRAM_COMPRESSED_BYTES: &str = "[Addl]ZramCompressedBytes";
24
25#[derive(Clone, Debug, Deserialize)]
33pub struct BucketDefinition {
34 pub name: String,
35 #[serde(deserialize_with = "deserialize_regex")]
36 pub process: Option<Regex>,
37 #[serde(deserialize_with = "deserialize_regex")]
38 pub vmo: Option<Regex>,
39 pub event_code: u64,
40}
41
42impl BucketDefinition {
43 fn process_match(&self, process: &ZXName) -> bool {
45 self.process.as_ref().map_or(true, |p| p.is_match(process.as_bstr()))
46 }
47
48 fn vmo_match(&self, vmo: &ZXName) -> bool {
50 self.vmo.as_ref().map_or(true, |v| v.is_match(vmo.as_bstr()))
51 }
52}
53
54fn deserialize_regex<'de, D>(d: D) -> Result<Option<Regex>, D::Error>
56where
57 D: Deserializer<'de>,
58{
59 Option::<String>::deserialize(d)
61 .and_then(|os| {
63 os
64 .map(|s| {
66 Regex::new(&s)
67 .map_err(D::Error::custom)
70 })
71 .transpose()
74 })
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct Bucket {
80 pub name: String,
81 pub size: u64,
82}
83#[derive(Debug, PartialEq, Eq)]
86pub struct Digest {
87 pub buckets: Vec<Bucket>,
88}
89
90struct DigestComputer<'a> {
92 buckets: Vec<(&'a BucketDefinition, Bucket)>,
94 undigested_vmos: HashMap<zx_types::zx_koid_t, (Vmo, ZXName)>,
96}
97
98impl<'a> DigestComputer<'a> {
99 fn new(bucket_definitions: &'a [BucketDefinition]) -> DigestComputer<'a> {
100 DigestComputer {
101 buckets: bucket_definitions
102 .iter()
103 .map(|def| (def, Bucket { name: def.name.clone(), size: 0 }))
104 .collect(),
105 undigested_vmos: Default::default(),
106 }
107 }
108}
109
110impl ResourcesVisitor for DigestComputer<'_> {
111 fn on_job(
112 &mut self,
113 _job_koid: zx_types::zx_koid_t,
114 _job_name: &ZXName,
115 _job: fplugin::Job,
116 ) -> Result<(), zx_status::Status> {
117 Ok(())
118 }
119
120 fn on_process(
121 &mut self,
122 _process_koid: zx_types::zx_koid_t,
123 process_name: &ZXName,
124 process: fplugin::Process,
125 ) -> Result<(), zx_status::Status> {
126 for (bucket_definition, bucket) in self.buckets.iter_mut() {
127 if bucket_definition.process_match(process_name) {
128 for koid in process.vmos.iter().flatten() {
129 bucket.size += match self.undigested_vmos.entry(*koid) {
130 Occupied(e) => {
131 let (_vmo, name) = e.get();
132 if bucket_definition.vmo_match(&name) {
133 let (_, (vmo, _name)) = e.remove_entry();
134 vmo.total_committed_bytes.unwrap_or_default()
135 } else {
136 0
137 }
138 }
139 _ => 0,
140 };
141 }
142 }
143 }
144 Ok(())
145 }
146
147 fn on_vmo(
148 &mut self,
149 vmo_koid: zx_types::zx_koid_t,
150 vmo_name: &ZXName,
151 vmo: fplugin::Vmo,
152 ) -> Result<(), zx_status::Status> {
153 self.undigested_vmos.insert(vmo_koid, (vmo, vmo_name.clone()));
154 Ok(())
155 }
156}
157
158impl Digest {
159 pub fn compute(
162 attribution_data_service: &impl AttributionDataProvider,
163 kmem_stats: &fkernel::MemoryStats,
164 kmem_stats_compression: &fkernel::MemoryStatsCompression,
165 bucket_definitions: &[BucketDefinition],
166 ) -> Result<Digest, anyhow::Error> {
167 let mut digest_visitor = DigestComputer::new(bucket_definitions);
168 attribution_data_service.for_each_resource(&mut digest_visitor)?;
169 let mut buckets: Vec<Bucket> =
170 digest_visitor.buckets.drain(..).map(|(_, bucket)| bucket).collect();
171
172 let vmo_size: u64 = buckets.iter().map(|Bucket { size, .. }| size).sum();
173 buckets.extend(vec![
176 Bucket {
179 name: UNDIGESTED.to_string(),
180 size: digest_visitor
181 .undigested_vmos
182 .values()
183 .filter_map(|(vmo, _)| vmo.total_committed_bytes)
184 .sum(),
185 },
186 Bucket {
189 name: ORPHANED.to_string(),
190 size: kmem_stats.vmo_bytes.unwrap_or(0).saturating_sub(vmo_size),
191 },
192 Bucket {
194 name: KERNEL.to_string(),
195 size: (|| {
196 Some(
197 kmem_stats.wired_bytes?
198 + kmem_stats.total_heap_bytes?
199 + kmem_stats.mmu_overhead_bytes?
200 + kmem_stats.ipc_bytes?
201 + kmem_stats.other_bytes?,
202 )
203 })()
204 .unwrap_or(0),
205 },
206 Bucket { name: FREE.to_string(), size: kmem_stats.free_bytes.unwrap_or(0) },
208 Bucket {
210 name: PAGER_TOTAL.to_string(),
211 size: kmem_stats.vmo_reclaim_total_bytes.unwrap_or(0),
212 },
213 Bucket {
214 name: PAGER_NEWEST.to_string(),
215 size: kmem_stats.vmo_reclaim_newest_bytes.unwrap_or(0),
216 },
217 Bucket {
218 name: PAGER_OLDEST.to_string(),
219 size: kmem_stats.vmo_reclaim_oldest_bytes.unwrap_or(0),
220 },
221 Bucket {
223 name: DISCARDABLE_LOCKED.to_string(),
224 size: kmem_stats.vmo_discardable_locked_bytes.unwrap_or(0),
225 },
226 Bucket {
227 name: DISCARDABLE_UNLOCKED.to_string(),
228 size: kmem_stats.vmo_discardable_unlocked_bytes.unwrap_or(0),
229 },
230 Bucket {
232 name: ZRAM_COMPRESSED_BYTES.to_string(),
233 size: kmem_stats_compression.compressed_storage_bytes.unwrap_or(0),
234 },
235 ]);
236 Ok(Digest { buckets })
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::testing::FakeAttributionDataProvider;
244 use crate::{
245 Attribution, AttributionData, Principal, PrincipalDescription, PrincipalIdentifier,
246 PrincipalType, Resource, ResourceReference,
247 };
248 use fidl_fuchsia_memory_attribution_plugin as fplugin;
249
250 fn get_attribution_data_provider() -> FakeAttributionDataProvider {
251 let attribution_data = AttributionData {
252 principals_vec: vec![Principal {
253 identifier: PrincipalIdentifier(1),
254 description: PrincipalDescription::Component("principal".to_owned()),
255 principal_type: PrincipalType::Runnable,
256 parent: None,
257 }],
258 resources_vec: vec![
259 Resource {
260 koid: 10,
261 name_index: 0,
262 resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
263 parent: None,
264 private_committed_bytes: Some(1024),
265 private_populated_bytes: Some(2048),
266 scaled_committed_bytes: Some(1024),
267 scaled_populated_bytes: Some(2048),
268 total_committed_bytes: Some(1024),
269 total_populated_bytes: Some(2048),
270 ..Default::default()
271 }),
272 },
273 Resource {
274 koid: 20,
275 name_index: 1,
276 resource_type: fplugin::ResourceType::Vmo(fplugin::Vmo {
277 parent: None,
278 private_committed_bytes: Some(1024),
279 private_populated_bytes: Some(2048),
280 scaled_committed_bytes: Some(1024),
281 scaled_populated_bytes: Some(2048),
282 total_committed_bytes: Some(1024),
283 total_populated_bytes: Some(2048),
284 ..Default::default()
285 }),
286 },
287 Resource {
288 koid: 30,
289 name_index: 1,
290 resource_type: fplugin::ResourceType::Process(fplugin::Process {
291 vmos: Some(vec![10, 20]),
292 ..Default::default()
293 }),
294 },
295 ],
296 resource_names: vec![
297 ZXName::try_from_bytes(b"resource").unwrap(),
298 ZXName::try_from_bytes(b"matched").unwrap(),
299 ],
300 attributions: vec![Attribution {
301 source: PrincipalIdentifier(1),
302 subject: PrincipalIdentifier(1),
303 resources: vec![ResourceReference::KernelObject(10)],
304 }],
305 };
306 FakeAttributionDataProvider { attribution_data }
307 }
308
309 fn get_kernel_stats() -> (fkernel::MemoryStats, fkernel::MemoryStatsCompression) {
310 (
311 fkernel::MemoryStats {
312 total_bytes: Some(1),
313 free_bytes: Some(2),
314 wired_bytes: Some(3),
315 total_heap_bytes: Some(4),
316 free_heap_bytes: Some(5),
317 vmo_bytes: Some(10000),
318 mmu_overhead_bytes: Some(7),
319 ipc_bytes: Some(8),
320 other_bytes: Some(9),
321 free_loaned_bytes: Some(10),
322 cache_bytes: Some(11),
323 slab_bytes: Some(12),
324 zram_bytes: Some(13),
325 vmo_reclaim_total_bytes: Some(14),
326 vmo_reclaim_newest_bytes: Some(15),
327 vmo_reclaim_oldest_bytes: Some(16),
328 vmo_reclaim_disabled_bytes: Some(17),
329 vmo_discardable_locked_bytes: Some(18),
330 vmo_discardable_unlocked_bytes: Some(19),
331 ..Default::default()
332 },
333 fkernel::MemoryStatsCompression {
334 uncompressed_storage_bytes: Some(20),
335 compressed_storage_bytes: Some(21),
336 compressed_fragmentation_bytes: Some(22),
337 compression_time: Some(23),
338 decompression_time: Some(24),
339 total_page_compression_attempts: Some(25),
340 failed_page_compression_attempts: Some(26),
341 total_page_decompressions: Some(27),
342 compressed_page_evictions: Some(28),
343 eager_page_compressions: Some(29),
344 memory_pressure_page_compressions: Some(30),
345 critical_memory_page_compressions: Some(31),
346 pages_decompressed_unit_ns: Some(32),
347 pages_decompressed_within_log_time: Some([40, 41, 42, 43, 44, 45, 46, 47]),
348 ..Default::default()
349 },
350 )
351 }
352
353 #[test]
354 fn test_digest_no_definitions() {
355 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
356 let digest = Digest::compute(
357 &get_attribution_data_provider(),
358 &kernel_stats,
359 &kernel_stats_compression,
360 &vec![],
361 )
362 .unwrap();
363 let expected_buckets = vec![
364 Bucket { name: UNDIGESTED.to_string(), size: 2048 }, Bucket { name: ORPHANED.to_string(), size: 10000 }, Bucket { name: KERNEL.to_string(), size: 31 }, Bucket { name: FREE.to_string(), size: 2 },
368 Bucket { name: PAGER_TOTAL.to_string(), size: 14 },
369 Bucket { name: PAGER_NEWEST.to_string(), size: 15 },
370 Bucket { name: PAGER_OLDEST.to_string(), size: 16 },
371 Bucket { name: DISCARDABLE_LOCKED.to_string(), size: 18 },
372 Bucket { name: DISCARDABLE_UNLOCKED.to_string(), size: 19 },
373 Bucket { name: ZRAM_COMPRESSED_BYTES.to_string(), size: 21 },
374 ];
375
376 assert_eq!(digest.buckets, expected_buckets);
377 }
378
379 #[test]
380 fn test_digest_with_matching_vmo() -> Result<(), anyhow::Error> {
381 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
382 let digest = Digest::compute(
383 &get_attribution_data_provider(),
384 &kernel_stats,
385 &kernel_stats_compression,
386 &vec![BucketDefinition {
387 name: "matched".to_string(),
388 process: None,
389 vmo: Some(Regex::new("matched")?),
390 event_code: Default::default(),
391 }],
392 )
393 .unwrap();
394 let expected_buckets = vec![
395 Bucket { name: "matched".to_string(), size: 1024 }, Bucket { name: UNDIGESTED.to_string(), size: 1024 }, Bucket { name: ORPHANED.to_string(), size: 8976 }, Bucket { name: KERNEL.to_string(), size: 31 }, Bucket { name: FREE.to_string(), size: 2 },
400 Bucket { name: PAGER_TOTAL.to_string(), size: 14 },
401 Bucket { name: PAGER_NEWEST.to_string(), size: 15 },
402 Bucket { name: PAGER_OLDEST.to_string(), size: 16 },
403 Bucket { name: DISCARDABLE_LOCKED.to_string(), size: 18 },
404 Bucket { name: DISCARDABLE_UNLOCKED.to_string(), size: 19 },
405 Bucket { name: ZRAM_COMPRESSED_BYTES.to_string(), size: 21 },
406 ];
407
408 assert_eq!(digest.buckets, expected_buckets);
409 Ok(())
410 }
411
412 #[test]
413 fn test_digest_with_matching_process() -> Result<(), anyhow::Error> {
414 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
415 let digest = Digest::compute(
416 &get_attribution_data_provider(),
417 &kernel_stats,
418 &kernel_stats_compression,
419 &vec![BucketDefinition {
420 name: "matched".to_string(),
421 process: Some(Regex::new("matched")?),
422 vmo: None,
423 event_code: Default::default(),
424 }],
425 )
426 .unwrap();
427 let expected_buckets = vec![
428 Bucket { name: "matched".to_string(), size: 2048 }, Bucket { name: UNDIGESTED.to_string(), size: 0 }, Bucket { name: ORPHANED.to_string(), size: 7952 }, Bucket { name: KERNEL.to_string(), size: 31 }, Bucket { name: FREE.to_string(), size: 2 },
433 Bucket { name: PAGER_TOTAL.to_string(), size: 14 },
434 Bucket { name: PAGER_NEWEST.to_string(), size: 15 },
435 Bucket { name: PAGER_OLDEST.to_string(), size: 16 },
436 Bucket { name: DISCARDABLE_LOCKED.to_string(), size: 18 },
437 Bucket { name: DISCARDABLE_UNLOCKED.to_string(), size: 19 },
438 Bucket { name: ZRAM_COMPRESSED_BYTES.to_string(), size: 21 },
439 ];
440
441 assert_eq!(digest.buckets, expected_buckets);
442 Ok(())
443 }
444
445 #[test]
446 fn test_digest_with_matching_process_and_vmo() -> Result<(), anyhow::Error> {
447 let (kernel_stats, kernel_stats_compression) = get_kernel_stats();
448 let digest = Digest::compute(
449 &get_attribution_data_provider(),
450 &kernel_stats,
451 &kernel_stats_compression,
452 &vec![BucketDefinition {
453 name: "matched".to_string(),
454 process: Some(Regex::new("matched")?),
455 vmo: Some(Regex::new("matched")?),
456 event_code: Default::default(),
457 }],
458 )
459 .unwrap();
460 let expected_buckets = vec![
461 Bucket { name: "matched".to_string(), size: 1024 }, Bucket { name: UNDIGESTED.to_string(), size: 1024 }, Bucket { name: ORPHANED.to_string(), size: 8976 }, Bucket { name: KERNEL.to_string(), size: 31 }, Bucket { name: FREE.to_string(), size: 2 },
466 Bucket { name: PAGER_TOTAL.to_string(), size: 14 },
467 Bucket { name: PAGER_NEWEST.to_string(), size: 15 },
468 Bucket { name: PAGER_OLDEST.to_string(), size: 16 },
469 Bucket { name: DISCARDABLE_LOCKED.to_string(), size: 18 },
470 Bucket { name: DISCARDABLE_UNLOCKED.to_string(), size: 19 },
471 Bucket { name: ZRAM_COMPRESSED_BYTES.to_string(), size: 21 },
472 ];
473
474 assert_eq!(digest.buckets, expected_buckets);
475 Ok(())
476 }
477}