1use anyhow::{format_err, Error};
6use fuchsia_inspect::reader::snapshot::ScannedBlock;
7use inspect_format::{Array, BlockType, Buffer, Extent, Name, PropertyFormat, StringRef, Unknown};
8use serde::Serialize;
9use std::collections::HashMap;
10
11const NUMERIC_TYPE_SIZE: usize = 8;
26
27#[derive(Debug)]
29pub struct BlockMetrics {
30 description: String,
31 header_bytes: usize,
32 data_bytes: usize,
33 total_bytes: usize,
34}
35
36impl BlockMetrics {
37 pub fn set_data_bytes(&mut self, bytes: usize) {
38 self.data_bytes = bytes;
39 }
40
41 #[cfg(test)]
42 pub(crate) fn sample_for_test(
43 description: String,
44 header_bytes: usize,
45 data_bytes: usize,
46 total_bytes: usize,
47 ) -> BlockMetrics {
48 BlockMetrics { description, header_bytes, data_bytes, total_bytes }
49 }
50}
51
52#[derive(PartialEq)]
54pub(crate) enum BlockStatus {
55 Used,
56 NotUsed,
57}
58
59#[derive(Debug, Default, Serialize, PartialEq)]
61pub struct BlockStatistics {
62 pub count: u64,
63 pub header_bytes: usize,
64 pub data_bytes: usize,
65 pub total_bytes: usize,
66 pub data_percent: u64,
67}
68
69impl BlockStatistics {
70 fn update(&mut self, numbers: &BlockMetrics, status: BlockStatus) {
71 let BlockMetrics { header_bytes, data_bytes, total_bytes, .. } = numbers;
72 self.header_bytes += header_bytes;
73 if status == BlockStatus::Used {
74 self.data_bytes += data_bytes;
75 }
76 self.total_bytes += total_bytes;
77 if self.total_bytes > 0 {
78 self.data_percent = (self.data_bytes * 100 / self.total_bytes) as u64;
79 }
80 }
81}
82
83#[derive(Debug, Serialize, Default)]
85pub struct Metrics {
86 pub block_count: u64,
87 pub size: usize,
88 pub block_statistics: HashMap<String, BlockStatistics>,
89}
90
91impl Metrics {
92 pub fn new() -> Metrics {
93 Self::default()
94 }
95
96 pub(crate) fn record(&mut self, metrics: &BlockMetrics, status: BlockStatus) {
97 let description = match status {
98 BlockStatus::NotUsed => format!("{}(UNUSED)", metrics.description),
99 BlockStatus::Used => metrics.description.clone(),
100 };
101 let statistics = self.block_statistics.entry(description).or_default();
102 statistics.count += 1;
103 statistics.update(metrics, status);
104 self.block_count += 1;
105 self.size += metrics.total_bytes;
106 }
107
108 pub fn process(&mut self, block: ScannedBlock<'_, Unknown>) -> Result<(), Error> {
111 self.record(&Metrics::analyze(block)?, BlockStatus::Used);
112 Ok(())
113 }
114
115 pub fn analyze(block: ScannedBlock<'_, Unknown>) -> Result<BlockMetrics, Error> {
116 let block_type = block.block_type().ok_or_else(|| format_err!("Missing block type"))?;
117 let mut description = None;
118
119 let total_bytes = inspect_format::constants::MIN_ORDER_SIZE << block.order();
120 let data_bytes = match block_type {
121 BlockType::Header => 4,
122 BlockType::Reserved
123 | BlockType::NodeValue
124 | BlockType::Free
125 | BlockType::Tombstone
126 | BlockType::LinkValue => 0,
127 BlockType::IntValue
128 | BlockType::UintValue
129 | BlockType::DoubleValue
130 | BlockType::BoolValue => NUMERIC_TYPE_SIZE,
131 BlockType::BufferValue => {
132 let block = block.cast::<Buffer>().unwrap();
133 description =
134 Some(match block.format().ok_or(format_err!("Missing property format"))? {
135 PropertyFormat::String => "STRING".to_owned(),
136 PropertyFormat::Bytes => "BYTES".to_owned(),
137 PropertyFormat::StringReference => "STRING_REFERENCE".to_owned(),
138 });
139 0
140 }
141 BlockType::ArrayValue => {
142 let block = block.cast::<Array<Unknown>>().unwrap();
143 let entry_type = block.entry_type().ok_or(format_err!("format missing"))?;
144 let format = block.format().ok_or(format_err!("format missing"))?;
145 description = Some(format!("ARRAY({format:?}, {entry_type})"));
146 block.entry_type_size().ok_or(format_err!("entry type must be sized"))?
147 * block.slots()
148 }
149 BlockType::Name => {
150 let block = block.cast::<Name>().unwrap();
151 block.length()
152 }
153 BlockType::Extent => {
154 let block = block.cast::<Extent>().unwrap();
155 block.contents()?.len()
156 }
157 BlockType::StringReference => {
158 let block = block.cast::<StringRef>().unwrap();
159 block.total_length()
160 }
161 };
162
163 let header_bytes = match block_type {
164 BlockType::Header
165 | BlockType::NodeValue
166 | BlockType::BufferValue
167 | BlockType::Free
168 | BlockType::Reserved
169 | BlockType::Tombstone
170 | BlockType::ArrayValue
171 | BlockType::LinkValue => 16,
172 BlockType::StringReference => 12,
173 BlockType::IntValue
174 | BlockType::DoubleValue
175 | BlockType::UintValue
176 | BlockType::BoolValue
177 | BlockType::Name
178 | BlockType::Extent => 8,
179 };
180
181 Ok(BlockMetrics {
182 description: description.unwrap_or_else(|| format!("{block_type}")),
183 data_bytes,
184 header_bytes,
185 total_bytes,
186 })
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::results::Results;
194 use crate::{data, puppet};
195 use anyhow::{bail, format_err};
196 use inspect_format::{
197 constants, ArrayFormat, BlockAccessorMutExt, BlockIndex, HeaderFields, PayloadFields,
198 };
199
200 #[fuchsia::test]
201 async fn metrics_work() -> Result<(), Error> {
202 let puppet = puppet::tests::local_incomplete_puppet().await?;
203 let metrics = puppet.metrics().unwrap();
204 let mut results = Results::new();
205 results.remember_metrics(metrics, "trialfoo", 42, "stepfoo");
206 let json = results.to_json();
207 assert!(json.contains("\"trial_name\":\"trialfoo\""), "{}", json);
208 assert!(json.contains(&format!("\"size\":{}", puppet::VMO_SIZE)), "{}", json);
209 assert!(json.contains("\"step_index\":42"), "{}", json);
210 assert!(json.contains("\"step_name\":\"stepfoo\""), "{}", json);
211 assert!(json.contains("\"block_count\":8"), "{}", json);
212 assert!(json.contains("\"HEADER\":{\"count\":1,\"header_bytes\":16,\"data_bytes\":4,\"total_bytes\":32,\"data_percent\":12}"), "{}", json);
213 Ok(())
214 }
215
216 #[allow(clippy::too_many_arguments)]
217 #[track_caller]
218 fn test_metrics(
219 buffer: &[u8],
220 block_count: u64,
221 size: usize,
222 description: &str,
223 correct_statistics: BlockStatistics,
224 ) -> Result<(), Error> {
225 let metrics = data::Scanner::try_from(buffer).map(|d| d.metrics())?;
226 assert_eq!(metrics.block_count, block_count, "Bad block_count for {}", description);
227 assert_eq!(metrics.size, size, "Bad size for {}", description);
228 match metrics.block_statistics.get(description) {
229 None => {
230 return Err(format_err!(
231 "block {} not found in {:?}",
232 description,
233 metrics.block_statistics.keys()
234 ))
235 }
236 Some(statistics) if statistics == &correct_statistics => {}
237 Some(unexpected) => bail!(
238 "Value mismatch, {:?} vs {:?} for {}",
239 unexpected,
240 correct_statistics,
241 description
242 ),
243 }
244 Ok(())
245 }
246
247 fn copy_into(source: &[u8], dest: &mut [u8], index: usize, offset: usize) {
248 let offset = index * 16 + offset;
249 dest[offset..offset + source.len()].copy_from_slice(source);
250 }
251
252 macro_rules! put_header {
253 ($block:ident, $index:expr, $buffer:expr) => {
254 copy_into(&HeaderFields::value(&$block).to_le_bytes(), $buffer, $index, 0);
255 };
256 }
257 macro_rules! put_payload {
258 ($block:ident, $index:expr, $buffer:expr) => {
259 copy_into(&PayloadFields::value(&$block).to_le_bytes(), $buffer, $index, 8);
260 };
261 }
262 macro_rules! set_type {
263 ($block:ident, $block_type:ident) => {
264 HeaderFields::set_block_type(&mut $block, BlockType::$block_type as u8)
265 };
266 }
267
268 const NAME_INDEX: u32 = 3;
269
270 fn init_vmo_contents(mut buffer: &mut [u8]) {
274 const HEADER_INDEX: usize = 0;
275
276 let mut container = [0u8; 16];
277 let mut header = container.block_at_mut(BlockIndex::EMPTY);
278 HeaderFields::set_order(&mut header, constants::HEADER_ORDER);
279 set_type!(header, Header);
280 HeaderFields::set_header_magic(&mut header, constants::HEADER_MAGIC_NUMBER);
281 HeaderFields::set_header_version(&mut header, constants::HEADER_VERSION_NUMBER);
282 put_header!(header, HEADER_INDEX, &mut buffer);
283 let mut container = [0u8; 16];
284 let mut name_header = container.block_at_mut(BlockIndex::EMPTY);
285 set_type!(name_header, Name);
286 HeaderFields::set_name_length(&mut name_header, 4);
287 put_header!(name_header, NAME_INDEX as usize, &mut buffer);
288 }
289
290 #[fuchsia::test]
291 fn header_metrics() -> Result<(), Error> {
292 let mut buffer = [0u8; 256];
293 init_vmo_contents(&mut buffer);
294 test_metrics(
295 &buffer,
296 15,
297 256,
298 "HEADER",
299 BlockStatistics {
300 count: 1,
301 header_bytes: 16,
302 data_bytes: 4,
303 total_bytes: 32,
304 data_percent: 12,
305 },
306 )?;
307 test_metrics(
308 &buffer,
309 15,
310 256,
311 "FREE",
312 BlockStatistics {
313 count: 13,
314 header_bytes: 208,
315 data_bytes: 0,
316 total_bytes: 208,
317 data_percent: 0,
318 },
319 )?;
320 test_metrics(
321 &buffer,
322 15,
323 256,
324 "NAME(UNUSED)",
325 BlockStatistics {
326 count: 1,
327 header_bytes: 8,
328 data_bytes: 0,
329 total_bytes: 16,
330 data_percent: 0,
331 },
332 )?;
333 Ok(())
334 }
335
336 #[fuchsia::test]
337 fn reserved_metrics() -> Result<(), Error> {
338 let mut buffer = [0u8; 256];
339 init_vmo_contents(&mut buffer);
340 let mut container = [0u8; 16];
341 let mut reserved_header = container.block_at_mut(BlockIndex::EMPTY);
342 set_type!(reserved_header, Reserved);
343 HeaderFields::set_order(&mut reserved_header, 1);
344 put_header!(reserved_header, 2, &mut buffer);
345 test_metrics(
346 &buffer,
347 14,
348 256,
349 "RESERVED",
350 BlockStatistics {
351 count: 1,
352 header_bytes: 16,
353 data_bytes: 0,
354 total_bytes: 32,
355 data_percent: 0,
356 },
357 )?;
358 Ok(())
359 }
360
361 #[fuchsia::test]
362 fn node_metrics() -> Result<(), Error> {
363 let mut buffer = [0u8; 256];
364 init_vmo_contents(&mut buffer);
365 let mut container = [0u8; 16];
366 let mut node_header = container.block_at_mut(BlockIndex::EMPTY);
367 set_type!(node_header, NodeValue);
368 HeaderFields::set_value_parent_index(&mut node_header, 1);
369 put_header!(node_header, 2, &mut buffer);
370 test_metrics(
371 &buffer,
372 15,
373 256,
374 "NODE_VALUE(UNUSED)",
375 BlockStatistics {
376 count: 1,
377 header_bytes: 16,
378 data_bytes: 0,
379 total_bytes: 16,
380 data_percent: 0,
381 },
382 )?;
383 HeaderFields::set_value_name_index(&mut node_header, NAME_INDEX);
384 HeaderFields::set_value_parent_index(&mut node_header, 0);
385 put_header!(node_header, 2, &mut buffer);
386 test_metrics(
387 &buffer,
388 15,
389 256,
390 "NODE_VALUE",
391 BlockStatistics {
392 count: 1,
393 header_bytes: 16,
394 data_bytes: 0,
395 total_bytes: 16,
396 data_percent: 0,
397 },
398 )?;
399 test_metrics(
400 &buffer,
401 15,
402 256,
403 "NAME",
404 BlockStatistics {
405 count: 1,
406 header_bytes: 8,
407 data_bytes: 4,
408 total_bytes: 16,
409 data_percent: 25,
410 },
411 )?;
412 set_type!(node_header, Tombstone);
413 put_header!(node_header, 2, &mut buffer);
414 test_metrics(
415 &buffer,
416 15,
417 256,
418 "TOMBSTONE",
419 BlockStatistics {
420 count: 1,
421 header_bytes: 16,
422 data_bytes: 0,
423 total_bytes: 16,
424 data_percent: 0,
425 },
426 )?;
427 test_metrics(
428 &buffer,
429 15,
430 256,
431 "NAME(UNUSED)",
432 BlockStatistics {
433 count: 1,
434 header_bytes: 8,
435 data_bytes: 0,
436 total_bytes: 16,
437 data_percent: 0,
438 },
439 )?;
440 Ok(())
441 }
442
443 #[fuchsia::test]
444 fn number_metrics() -> Result<(), Error> {
445 let mut buffer = [0u8; 256];
446 init_vmo_contents(&mut buffer);
447 macro_rules! test_number {
448 ($number_type:ident, $parent:expr, $block_string:expr, $data_size:expr, $data_percent:expr) => {
449 let mut container = [0u8; 16];
450 let mut value = container.block_at_mut(BlockIndex::EMPTY);
451 set_type!(value, $number_type);
452 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
453 HeaderFields::set_value_parent_index(&mut value, $parent);
454 put_header!(value, 2, &mut buffer);
455 test_metrics(
456 &buffer,
457 15,
458 256,
459 $block_string,
460 BlockStatistics {
461 count: 1,
462 header_bytes: 8,
463 data_bytes: $data_size,
464 total_bytes: 16,
465 data_percent: $data_percent,
466 },
467 )?;
468 };
469 }
470 test_number!(IntValue, 0, "INT_VALUE", 8, 50);
471 test_number!(IntValue, 5, "INT_VALUE(UNUSED)", 0, 0);
472 test_number!(DoubleValue, 0, "DOUBLE_VALUE", 8, 50);
473 test_number!(DoubleValue, 5, "DOUBLE_VALUE(UNUSED)", 0, 0);
474 test_number!(UintValue, 0, "UINT_VALUE", 8, 50);
475 test_number!(UintValue, 5, "UINT_VALUE(UNUSED)", 0, 0);
476 Ok(())
477 }
478
479 #[fuchsia::test]
480 fn property_metrics() -> Result<(), Error> {
481 let mut buffer = [0u8; 256];
482 init_vmo_contents(&mut buffer);
483 let mut container = [0u8; 16];
484 let mut value = container.block_at_mut(BlockIndex::EMPTY);
485 set_type!(value, BufferValue);
486 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
487 HeaderFields::set_value_parent_index(&mut value, 0);
488 put_header!(value, 2, &mut buffer);
489 PayloadFields::set_property_total_length(&mut value, 12);
490 PayloadFields::set_property_extent_index(&mut value, 4);
491 PayloadFields::set_property_flags(&mut value, PropertyFormat::String as u8);
492 put_payload!(value, 2, &mut buffer);
493 let mut container = [0u8; 16];
494 let mut extent = container.block_at_mut(BlockIndex::EMPTY);
495 set_type!(extent, Extent);
496 HeaderFields::set_extent_next_index(&mut extent, 5);
497 put_header!(extent, 4, &mut buffer);
498 HeaderFields::set_extent_next_index(&mut extent, 0);
499 put_header!(extent, 5, &mut buffer);
500 test_metrics(
501 &buffer,
502 15,
503 256,
504 "EXTENT",
505 BlockStatistics {
506 count: 2,
507 header_bytes: 16,
508 data_bytes: 12,
509 total_bytes: 32,
510 data_percent: 37,
511 },
512 )?;
513 test_metrics(
514 &buffer,
515 15,
516 256,
517 "STRING",
518 BlockStatistics {
519 count: 1,
520 header_bytes: 16,
521 data_bytes: 0,
522 total_bytes: 16,
523 data_percent: 0,
524 },
525 )?;
526 PayloadFields::set_property_flags(&mut value, PropertyFormat::Bytes as u8);
527 put_payload!(value, 2, &mut buffer);
528 test_metrics(
529 &buffer,
530 15,
531 256,
532 "EXTENT",
533 BlockStatistics {
534 count: 2,
535 header_bytes: 16,
536 data_bytes: 12,
537 total_bytes: 32,
538 data_percent: 37,
539 },
540 )?;
541 test_metrics(
542 &buffer,
543 15,
544 256,
545 "BYTES",
546 BlockStatistics {
547 count: 1,
548 header_bytes: 16,
549 data_bytes: 0,
550 total_bytes: 16,
551 data_percent: 0,
552 },
553 )?;
554 HeaderFields::set_value_parent_index(&mut value, 7);
555 put_header!(value, 2, &mut buffer);
556 test_metrics(
557 &buffer,
558 15,
559 256,
560 "EXTENT(UNUSED)",
561 BlockStatistics {
562 count: 2,
563 header_bytes: 16,
564 data_bytes: 0,
565 total_bytes: 32,
566 data_percent: 0,
567 },
568 )?;
569 test_metrics(
570 &buffer,
571 15,
572 256,
573 "BYTES(UNUSED)",
574 BlockStatistics {
575 count: 1,
576 header_bytes: 16,
577 data_bytes: 0,
578 total_bytes: 16,
579 data_percent: 0,
580 },
581 )?;
582 Ok(())
583 }
584
585 #[fuchsia::test]
586 fn array_metrics() {
587 let mut buffer = [0u8; 256];
588 init_vmo_contents(&mut buffer);
589 let mut container = [0u8; 16];
590 let mut value = container.block_at_mut(BlockIndex::EMPTY);
591
592 set_type!(value, ArrayValue);
593 HeaderFields::set_order(&mut value, 3);
594 HeaderFields::set_value_name_index(&mut value, NAME_INDEX);
595 HeaderFields::set_value_parent_index(&mut value, 0);
596 put_header!(value, 4, &mut buffer);
597 PayloadFields::set_array_entry_type(&mut value, BlockType::IntValue as u8);
598 PayloadFields::set_array_flags(&mut value, ArrayFormat::Default as u8);
599 PayloadFields::set_array_slots_count(&mut value, 4);
600 put_payload!(value, 4, &mut buffer);
601 test_metrics(
602 &buffer,
603 8,
604 256,
605 "ARRAY(Default, INT_VALUE)",
606 BlockStatistics {
607 count: 1,
608 header_bytes: 16,
609 data_bytes: 32,
610 total_bytes: 128,
611 data_percent: 25,
612 },
613 )
614 .unwrap();
615
616 PayloadFields::set_array_entry_type(&mut value, BlockType::StringReference as u8);
617 PayloadFields::set_array_flags(&mut value, ArrayFormat::Default as u8);
618 PayloadFields::set_array_slots_count(&mut value, 4);
619 put_payload!(value, 4, &mut buffer);
620 test_metrics(
621 &buffer,
622 8,
623 256,
624 "ARRAY(Default, STRING_REFERENCE)",
625 BlockStatistics {
626 count: 1,
627 header_bytes: 16,
628 data_bytes: 16,
629 total_bytes: 128,
630 data_percent: 12,
631 },
632 )
633 .unwrap();
634
635 PayloadFields::set_array_flags(&mut value, ArrayFormat::LinearHistogram as u8);
636 PayloadFields::set_array_entry_type(&mut value, BlockType::IntValue as u8);
637 PayloadFields::set_array_slots_count(&mut value, 8);
638 put_payload!(value, 4, &mut buffer);
639 test_metrics(
640 &buffer,
641 8,
642 256,
643 "ARRAY(LinearHistogram, INT_VALUE)",
644 BlockStatistics {
645 count: 1,
646 header_bytes: 16,
647 data_bytes: 64,
648 total_bytes: 128,
649 data_percent: 50,
650 },
651 )
652 .unwrap();
653
654 PayloadFields::set_array_flags(&mut value, ArrayFormat::ExponentialHistogram as u8);
655 let name = "ARRAY(ExponentialHistogram, INT_VALUE)";
657 put_payload!(value, 4, &mut buffer);
658 test_metrics(
659 &buffer,
660 8,
661 256,
662 name,
663 BlockStatistics {
664 count: 1,
665 header_bytes: 16,
666 data_bytes: 64,
667 total_bytes: 128,
668 data_percent: 50,
669 },
670 )
671 .unwrap();
672
673 PayloadFields::set_array_entry_type(&mut value, BlockType::UintValue as u8);
674 let name = "ARRAY(ExponentialHistogram, UINT_VALUE)";
675 put_payload!(value, 4, &mut buffer);
676 test_metrics(
677 &buffer,
678 8,
679 256,
680 name,
681 BlockStatistics {
682 count: 1,
683 header_bytes: 16,
684 data_bytes: 64,
685 total_bytes: 128,
686 data_percent: 50,
687 },
688 )
689 .unwrap();
690
691 PayloadFields::set_array_entry_type(&mut value, BlockType::DoubleValue as u8);
692 let name = "ARRAY(ExponentialHistogram, DOUBLE_VALUE)";
693 put_payload!(value, 4, &mut buffer);
694 test_metrics(
695 &buffer,
696 8,
697 256,
698 name,
699 BlockStatistics {
700 count: 1,
701 header_bytes: 16,
702 data_bytes: 64,
703 total_bytes: 128,
704 data_percent: 50,
705 },
706 )
707 .unwrap();
708
709 HeaderFields::set_value_parent_index(&mut value, 1);
710 let name = "ARRAY(ExponentialHistogram, DOUBLE_VALUE)(UNUSED)";
711 put_header!(value, 4, &mut buffer);
712 test_metrics(
713 &buffer,
714 8,
715 256,
716 name,
717 BlockStatistics {
718 count: 1,
719 header_bytes: 16,
720 data_bytes: 0,
721 total_bytes: 128,
722 data_percent: 0,
723 },
724 )
725 .unwrap();
726 }
727}