persistence/
inspect_server.rs

1// Copyright 2020 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 std::borrow::Cow;
6
7use crate::fetcher::TagData;
8use crate::file_handler::{self, Timestamps};
9use diagnostics_data::{Data, DiagnosticsHierarchy, InspectMetadata, Property};
10use fuchsia_inspect::hierarchy::{ExponentialHistogram, LinearHistogram, MissingValue};
11use fuchsia_inspect::reader::ArrayContent;
12use fuchsia_inspect::{
13    ArrayProperty, ExponentialHistogramParams, HistogramProperty, LinearHistogramParams, Node,
14    component,
15};
16use itertools::Either;
17
18fn store_data(node: &fuchsia_inspect::Node, data: TagData) {
19    let TagData { max_bytes: _, total_bytes, timestamps, selectors: _, data, errors } = data;
20
21    // Record total bytes.
22    node.record_uint("@persist_size", total_bytes as u64);
23
24    // Record errors.
25    let array = node.create_string_array("@errors", errors.len());
26    for (index, err) in errors.iter().enumerate() {
27        array.set(index, err);
28    }
29    node.record(array);
30
31    // Record timestamps.
32    node.record_child("@timestamps", |timestamps_node| {
33        let Timestamps { last_sample_boot, last_sample_utc } = timestamps;
34        timestamps_node.record_int("last_sample_boot", last_sample_boot.into_nanos());
35        timestamps_node.record_int("last_sample_utc", last_sample_utc.into_nanos());
36    });
37
38    // Record Inspect data by moniker.
39    for (moniker, data) in data {
40        node.record_child(moniker.to_string(), |node| {
41            let Data { data_source: _, metadata, moniker: _, payload, version: _ } = data;
42            let InspectMetadata { errors, name: _, component_url: _, timestamp: _, escrowed: _ } =
43                metadata;
44
45            // Record errors, if available.
46            if let Some(errs) = errors
47                && !errs.is_empty()
48            {
49                let array = node.create_string_array("@errors", errs.len());
50                for (index, err) in errs.into_iter().enumerate() {
51                    array.set(index, err.message);
52                }
53                node.record(array);
54            }
55
56            if let Some(payload) = payload {
57                // Skip the root payload, as this will always exist.
58                if payload.name == "root" {
59                    let DiagnosticsHierarchy { name: _, properties, children, missing } = payload;
60                    store_hierarchy_inner(node, properties, children, missing);
61                } else {
62                    store_hierarchy(node, payload);
63                }
64            }
65        })
66    }
67}
68
69fn store_hierarchy(node: &fuchsia_inspect::Node, data: DiagnosticsHierarchy) {
70    let DiagnosticsHierarchy { name, properties, children, missing } = data;
71    // Perform an atomic write such that the child only becomes available when
72    // its fully populated.
73    node.record_child(name, |node| {
74        store_hierarchy_inner(node, properties, children, missing);
75    });
76}
77
78fn store_hierarchy_inner(
79    node: &fuchsia_inspect::Node,
80    properties: Vec<Property>,
81    children: Vec<DiagnosticsHierarchy>,
82    missing: Vec<MissingValue>,
83) {
84    // Record missing values, if available.
85    if !missing.is_empty() {
86        node.record_child("@missing", |missing_node| {
87            for MissingValue { name, reason } in missing {
88                missing_node.record_string(name.clone(), format!("{reason:?}"));
89            }
90        })
91    }
92
93    // Record all properties.
94    for property in properties {
95        match property {
96            Property::String(k, v) => {
97                node.record_string(k, v);
98            }
99            Property::Bytes(k, v) => {
100                node.record_bytes(k, v);
101            }
102            Property::Int(k, v) => {
103                node.record_int(k, v);
104            }
105            Property::Uint(k, v) => {
106                node.record_uint(k, v);
107            }
108            Property::Double(k, v) => {
109                node.record_double(k, v);
110            }
111            Property::Bool(k, v) => {
112                node.record_bool(k, v);
113            }
114            Property::DoubleArray(k, v) => v.record(node, k),
115            Property::IntArray(k, v) => v.record(node, k),
116            Property::UintArray(k, v) => v.record(node, k),
117            Property::StringList(k, v) => {
118                let array = node.create_string_array(k, v.len());
119                for (i, v) in v.iter().enumerate() {
120                    array.set(i, v);
121                }
122                node.record(array);
123            }
124        }
125    }
126
127    // Record all children recursively.
128    for child in children {
129        store_hierarchy(node, child);
130    }
131}
132
133/// Data that is able to be represented in the Inspect VMO.
134trait InspectData<'a> {
135    /// Record the Inspect VMO representation of this type to the specified Inspect node.
136    fn record(self, node: &Node, name: impl Into<Cow<'a, str>>);
137}
138
139fn get_index_counts<T>(
140    indexes: Option<Vec<usize>>,
141    counts: Vec<T>,
142) -> impl Iterator<Item = (usize, T)> {
143    match indexes {
144        None => Either::Left(counts.into_iter().enumerate()),
145        Some(indexes) => Either::Right(indexes.into_iter().zip(counts)),
146    }
147}
148
149impl<'a> InspectData<'a> for ArrayContent<f64>
150where
151    Self: 'a,
152{
153    fn record(self, node: &Node, name: impl Into<Cow<'a, str>>) {
154        match self {
155            Self::Values(values) => {
156                let array = node.create_double_array(name, values.len());
157                for (index, value) in values.iter().enumerate() {
158                    array.set(index, *value);
159                }
160                node.record(array);
161            }
162            Self::LinearHistogram(LinearHistogram { size, floor, step, counts, indexes }) => {
163                let name = name.into();
164
165                let array = node.create_double_linear_histogram(
166                    name,
167                    LinearHistogramParams {
168                        floor,
169                        step_size: step,
170                        // Do not include underflow and overflow buckets.
171                        buckets: size.saturating_sub(2),
172                    },
173                );
174
175                for (bucket_index, count) in get_index_counts(indexes, counts) {
176                    if count < 1.0 {
177                        continue;
178                    }
179                    let value = if bucket_index == 0 {
180                        if floor == f64::NEG_INFINITY {
181                            // Floor starts at the lowest possible value; there
182                            // is no underflow bucket.
183                            continue;
184                        }
185                        f64::NEG_INFINITY
186                    } else {
187                        floor + step * (bucket_index - 1) as f64
188                    };
189                    array.insert_multiple(value, count.round() as usize);
190                }
191
192                node.record(array);
193            }
194            Self::ExponentialHistogram(ExponentialHistogram {
195                size,
196                floor,
197                initial_step,
198                step_multiplier,
199                counts,
200                indexes,
201            }) => {
202                let name = name.into();
203
204                let array = node.create_double_exponential_histogram(
205                    name,
206                    ExponentialHistogramParams {
207                        floor,
208                        initial_step,
209                        step_multiplier,
210                        // Do not include underflow and overflow buckets.
211                        buckets: size.saturating_sub(2),
212                    },
213                );
214
215                for (bucket_index, count) in get_index_counts(indexes, counts) {
216                    if count < 1.0 {
217                        continue;
218                    }
219                    let value = if bucket_index == 0 {
220                        if floor == f64::NEG_INFINITY {
221                            // Floor starts at the lowest possible value; there
222                            // is no underflow bucket.
223                            continue;
224                        }
225                        f64::NEG_INFINITY
226                    } else if bucket_index == 1 {
227                        floor
228                    } else {
229                        let multiplier = step_multiplier.powi((bucket_index - 2) as i32);
230                        initial_step.mul_add(multiplier, floor)
231                    };
232                    array.insert_multiple(value, count.round() as usize);
233                }
234
235                node.record(array);
236            }
237        }
238    }
239}
240
241impl<'a> InspectData<'a> for ArrayContent<i64>
242where
243    Self: 'a,
244{
245    fn record(self, node: &Node, name: impl Into<Cow<'a, str>>) {
246        match self {
247            Self::Values(values) => {
248                let array = node.create_int_array(name, values.len());
249                for (index, value) in values.iter().enumerate() {
250                    array.set(index, *value);
251                }
252                node.record(array);
253            }
254            Self::LinearHistogram(LinearHistogram { size, floor, step, counts, indexes }) => {
255                let name = name.into();
256
257                let array = node.create_int_linear_histogram(
258                    name,
259                    LinearHistogramParams {
260                        floor,
261                        step_size: step,
262                        // Do not include underflow and overflow buckets.
263                        buckets: size.saturating_sub(2),
264                    },
265                );
266
267                for (bucket_index, count) in get_index_counts(indexes, counts) {
268                    if count == 0 {
269                        continue;
270                    }
271                    let value = if bucket_index == 0 {
272                        if let Some(res) = floor.checked_sub(step.signum()) {
273                            res
274                        } else {
275                            // Floor starts at either the minimum or maximum
276                            // value; there is no underflow bucket.
277                            continue;
278                        }
279                    } else {
280                        // Use larger data types to avoid underflow/overflow
281                        // while calculating the value.
282                        let step = step as i128;
283                        let floor = floor as i128;
284
285                        // floor + step * index
286                        let value =
287                            floor.saturating_add(step.saturating_mul((bucket_index - 1) as i128));
288
289                        if value > i64::MAX as i128 {
290                            i64::MAX
291                        } else if value < i64::MIN as i128 {
292                            i64::MIN
293                        } else {
294                            value as i64
295                        }
296                    };
297                    array.insert_multiple(value, count as usize);
298                }
299
300                node.record(array);
301            }
302            Self::ExponentialHistogram(ExponentialHistogram {
303                size,
304                floor,
305                initial_step,
306                step_multiplier,
307                counts,
308                indexes,
309            }) => {
310                let name = name.into();
311
312                let array = node.create_int_exponential_histogram(
313                    name,
314                    ExponentialHistogramParams {
315                        floor,
316                        initial_step,
317                        step_multiplier,
318                        // Do not include underflow and overflow buckets.
319                        buckets: size.saturating_sub(2),
320                    },
321                );
322
323                for (bucket_index, count) in get_index_counts(indexes, counts) {
324                    if count == 0 {
325                        continue;
326                    }
327                    let value = if bucket_index == 0 {
328                        if let Some(res) = floor.checked_sub(initial_step.signum()) {
329                            res
330                        } else {
331                            // Floor starts at either the minimum or maximum
332                            // value; there is no underflow bucket.
333                            continue;
334                        }
335                    } else if bucket_index == 1 {
336                        floor
337                    } else {
338                        // Use larger data types to avoid underflow/overflow
339                        // while calculating the value.
340                        let step_multiplier = step_multiplier as i128;
341                        let initial_step = initial_step as i128;
342                        let floor = floor as i128;
343
344                        // floor + initial_step * step_multiplier^(index)
345                        let value = floor.saturating_add(initial_step.saturating_mul(
346                            step_multiplier.saturating_pow((bucket_index - 2) as u32),
347                        ));
348
349                        if value > i64::MAX as i128 {
350                            i64::MAX
351                        } else if value < i64::MIN as i128 {
352                            i64::MIN
353                        } else {
354                            value as i64
355                        }
356                    };
357                    array.insert_multiple(value, count as usize);
358                }
359
360                node.record(array);
361            }
362        }
363    }
364}
365
366impl<'a> InspectData<'a> for ArrayContent<u64>
367where
368    Self: 'a,
369{
370    fn record(self, node: &Node, name: impl Into<Cow<'a, str>>) {
371        match self {
372            Self::Values(values) => {
373                let array = node.create_uint_array(name, values.len());
374                for (index, value) in values.iter().enumerate() {
375                    array.set(index, *value);
376                }
377                node.record(array);
378            }
379            Self::LinearHistogram(LinearHistogram { size, floor, step, counts, indexes }) => {
380                let name = name.into();
381
382                let array = node.create_uint_linear_histogram(
383                    name,
384                    LinearHistogramParams {
385                        floor,
386                        step_size: step,
387                        // Do not include underflow and overflow buckets.
388                        buckets: size.saturating_sub(2),
389                    },
390                );
391
392                for (bucket_index, count) in get_index_counts(indexes, counts) {
393                    if count == 0 {
394                        continue;
395                    }
396                    let value = if bucket_index == 0 {
397                        if floor == u64::MIN {
398                            // Floor starts at the minimum value; there is no
399                            // underflow bucket.
400                            continue;
401                        }
402                        u64::MIN
403                    } else {
404                        // floor + step * index
405                        floor.saturating_add(step.saturating_mul((bucket_index - 1) as u64))
406                    };
407                    array.insert_multiple(value, count as usize);
408                }
409
410                node.record(array);
411            }
412            Self::ExponentialHistogram(ExponentialHistogram {
413                size,
414                floor,
415                initial_step,
416                step_multiplier,
417                counts,
418                indexes,
419            }) => {
420                let name = name.into();
421
422                let array = node.create_uint_exponential_histogram(
423                    name,
424                    ExponentialHistogramParams {
425                        floor,
426                        initial_step,
427                        step_multiplier,
428                        // Do not include underflow and overflow buckets.
429                        buckets: size.saturating_sub(2),
430                    },
431                );
432
433                for (bucket_index, count) in get_index_counts(indexes, counts) {
434                    if count == 0 {
435                        continue;
436                    }
437                    let value = if bucket_index == 0 {
438                        if floor == u64::MIN {
439                            // Floor starts at the minimum value; there is no
440                            // underflow bucket.
441                            continue;
442                        }
443                        u64::MIN
444                    } else if bucket_index == 1 {
445                        floor
446                    } else {
447                        // floor + initial_step * step_multiplier^(index)
448                        floor.saturating_add(initial_step.saturating_mul(
449                            step_multiplier.saturating_pow((bucket_index - 2) as u32),
450                        ))
451                    };
452                    array.insert_multiple(value, count as usize);
453                }
454
455                node.record(array);
456            }
457        }
458    }
459}
460
461pub async fn record_persist_node(name: &str) -> Result<(), anyhow::Error> {
462    if let Some(data) = file_handler::previous_data().await? {
463        component::inspector().root().record_child(name, |persist_node| {
464            for (service, service_data) in data.0 {
465                persist_node.record_child(service.to_string(), |service_node| {
466                    for (tag, tag_data) in service_data.0 {
467                        service_node.record_child(tag.to_string(), |tag_node| {
468                            store_data(tag_node, tag_data);
469                        })
470                    }
471                });
472            }
473        });
474    }
475    Ok(())
476}
477
478#[cfg(test)]
479mod tests {
480    use std::collections::VecDeque;
481    use std::str::FromStr;
482
483    use super::*;
484    use anyhow::Error;
485    use diagnostics_assertions::{PropertyAssertion, assert_data_tree};
486    use diagnostics_data::{InspectError, InspectHandleName, hierarchy};
487    use flyweights::FlyStr;
488    use fuchsia_inspect::Inspector;
489    use hashbrown::HashMap;
490    use test_case::test_case;
491    use zx::Instant;
492
493    // Verify TagData is published to Inspect in the format we expect.
494    #[fuchsia::test]
495    async fn store_data_works() -> Result<(), Error> {
496        let inspector = Inspector::default();
497        let inspect = inspector.root();
498        assert_data_tree!(
499            inspector,
500            root: contains {
501            }
502        );
503
504        let moniker = diagnostics_data::ExtendedMoniker::from_str("types").unwrap();
505
506        let tag_data = TagData {
507            max_bytes: 1,
508            total_bytes: 2,
509            timestamps: Timestamps {
510                last_sample_boot: zx::BootInstant::from_nanos(3),
511                last_sample_utc: fuchsia_runtime::UtcInstant::from_nanos(4),
512            },
513            selectors: vec![],
514            data: HashMap::from([(
515                moniker.clone().into(),
516                Data {
517                    data_source: diagnostics_data::DataSource::Inspect,
518                    metadata: InspectMetadata {
519                        errors: Some(vec![InspectError {
520                            message: "test InspectError".to_string(),
521                        }]),
522                        name: InspectHandleName::Name(FlyStr::new("inspect_handle_name")),
523                        component_url: FlyStr::new("component_url"),
524                        timestamp: Instant::from_nanos(0),
525                        escrowed: false,
526                    },
527                    moniker,
528                    payload: Some(hierarchy! {
529                        root: {
530                            negint: -5,
531                            int: 42,
532                            unsigned: 9223372036854775808u64,
533                            float: 45.6f64,
534                            bool: true,
535                            obj: {
536                                child: "child",
537                                grandchild: {
538                                    hello: "world",
539                                }
540                            }
541                        }
542                    }),
543                    version: 1,
544                },
545            )]),
546            errors: VecDeque::from(["test TagData error".to_string()]),
547        };
548
549        store_data(inspect, tag_data);
550
551        assert_data_tree!(
552            inspector,
553            root: {
554                "@errors": vec!["test TagData error"],
555                "@persist_size": 2u64,
556                "@timestamps": {
557                    last_sample_boot: 3,
558                    last_sample_utc: 4,
559                },
560                types: {
561                    "@errors": vec!["test InspectError"],
562                    negint: -5i64,
563                    int: 42i64,
564                    unsigned: 9223372036854775808u64,
565                    float: 45.6f64,
566                    bool: true,
567                    obj: {
568                        child: "child",
569                        grandchild: {
570                            hello: "world",
571                        }
572                    }
573                }
574            }
575        );
576        Ok(())
577    }
578
579    #[test_case(vec![1.0, 2.0, 3.0, 4.0] ; "f64_many")]
580    #[test_case(vec![1i64, 2i64, 3i64, 4i64] ; "i64_many")]
581    #[test_case(vec![1u64, 2u64, 3u64, 4u64] ; "u64_many")]
582    #[test_case(vec![1.0] ; "f64_one")]
583    #[test_case(vec![1i64] ; "i64_one")]
584    #[test_case(vec![1u64] ; "u64_one")]
585    #[test_case(Vec::<f64>::new() ; "f64_none")]
586    #[test_case(Vec::<i64>::new() ; "i64_none")]
587    #[test_case(Vec::<u64>::new() ; "u64_none")]
588    #[fuchsia::test]
589    async fn record_values<'a, T>(values: Vec<T>)
590    where
591        ArrayContent<T>: InspectData<'a>,
592        Vec<T>: PropertyAssertion,
593        T: Clone + 'static,
594    {
595        let inspector = Inspector::default();
596        ArrayContent::Values(values.clone()).record(inspector.root(), "child");
597        assert_data_tree!(
598            inspector,
599            root: {
600                child: values,
601            }
602        );
603    }
604
605    #[test_case(
606        LinearHistogram {
607            size: 4,
608            floor: 0.0,
609            step: 10.0,
610            counts: vec![1.0, 2.0, 3.0, 4.0],
611            indexes: None,
612        } ;
613        "f64_dense"
614    )]
615    #[test_case(
616        LinearHistogram {
617            size: 4,
618            floor: 0i64,
619            step: 10i64,
620            counts: vec![1i64, 2i64, 3i64, 4i64],
621            indexes: None,
622        } ;
623        "i64_dense"
624    )]
625    #[test_case(
626        LinearHistogram {
627            size: 4,
628            floor: 0u64,
629            step: 10u64,
630            // Not possible to have an underflow bucket with floor = 0.
631            counts: vec![0u64, 2u64, 3u64, 4u64],
632            indexes: None,
633        } ;
634        "u64_dense"
635    )]
636    #[test_case(
637        LinearHistogram {
638            size: 4,
639            floor: 0.0,
640            step: 10.0,
641            counts: vec![5.0],
642            indexes: Some(vec![1]),
643        } ;
644        "f64_sparse"
645    )]
646    #[test_case(
647        LinearHistogram {
648            size: 4,
649            floor: 0i64,
650            step: 10i64,
651            counts: vec![5i64],
652            indexes: Some(vec![1]),
653        } ;
654        "i64_sparse"
655    )]
656    #[test_case(
657        LinearHistogram {
658            size: 4,
659            floor: 0u64,
660            step: 10u64,
661            counts: vec![5u64],
662            indexes: Some(vec![1]),
663        } ;
664        "u64_sparse"
665    )]
666    // | Bucket | Range       |
667    // | ------ | ----------- |
668    // |      0 | (-inf, MIN) |
669    // |      1 | [MIN, 0)    |
670    // |      2 | [0, MAX)    |
671    // |      3 | [MAX, +inf) |
672    #[test_case(
673        // f64 defines its underflow/overflow buckets ranges based on -inf/+inf
674        // instead of min/max values.
675        LinearHistogram {
676            size: 4,
677            floor: f64::MIN,
678            step: f64::MAX,
679            counts: vec![1.0, 2.0, 3.0, 4.0],
680            indexes: None,
681        } ;
682        "f64_bounds"
683    )]
684    // | Bucket | Range          |
685    // | ------ | -------------- |
686    // |      0 | (-inf, MIN)    |
687    // |      1 | [MIN-1, MIN-2) |
688    // |      2 | [MIN-2, +inf)  |
689    #[test_case(
690        // Should not have an underflow bucket due to bucket ranges.
691        LinearHistogram {
692            size: 3,
693            floor: i64::MIN,
694            step: 1,
695            counts: vec![0i64, 1i64, 2i64],
696            indexes: None,
697        } ;
698        "i64_bounds_min"
699    )]
700    // | Bucket | Range         |
701    // | ------ | ------------- |
702    // |      0 | (-inf, MIN+1) |
703    // |      1 | [MIN+1, 0)    |
704    // |      2 | [0, MAX)      |
705    // |      3 | [MAX, +inf)   |
706    #[test_case(
707        LinearHistogram {
708            size: 4,
709            // MIN is 1 less than MAX due to two's complement. Modify this such
710            // that bucket ends with MAX.
711            floor: i64::MIN+1,
712            step: i64::MAX,
713            counts: vec![1i64, 2i64, 3i64, 4i64],
714            indexes: None,
715        } ;
716        "i64_bounds_max"
717    )]
718    #[test_case(
719        LinearHistogram {
720            size: 3,
721            floor: u64::MIN,
722            step: u64::MAX / 2,
723            // Should not have an underflow bucket due to bucket ranges.
724            counts: vec![0u64, 1u64, 2u64],
725            indexes: None,
726        } ;
727        "u64_bounds"
728    )]
729    #[fuchsia::test]
730    async fn record_linear_histogram<'a, T>(histogram: LinearHistogram<T>)
731    where
732        ArrayContent<T>: InspectData<'a>,
733        LinearHistogram<T>: PropertyAssertion + Clone,
734        T: 'static,
735    {
736        let inspector = Inspector::default();
737        ArrayContent::LinearHistogram(histogram.clone()).record(inspector.root(), "child");
738        assert_data_tree!(
739            inspector,
740            root: {
741                child: histogram,
742            }
743        );
744    }
745
746    // Table for the dense/sparse exponential tests:
747    //
748    // | Bucket | Range            |
749    // | ------ | ---------------- |
750    // |      0 | (-inf, 1)        |
751    // |      1 | [1, 1+1*2^0 = 2) |
752    // |      2 | [2, 1+1*2^1 = 3) |
753    // |      3 | [3, 1+1*2^2 = 5) |
754    // |      4 | [5, 1+1*2^3 = 9) |
755    // |      5 | [9, +inf)        |
756    #[test_case(
757        ExponentialHistogram {
758            size: 6,
759            floor: 1.0,
760            initial_step: 1.0,
761            step_multiplier: 2.0,
762            counts: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
763            indexes: None,
764        } ;
765        "f64_dense"
766    )]
767    #[test_case(
768        ExponentialHistogram {
769            size: 6,
770            floor: 1i64,
771            initial_step: 1i64,
772            step_multiplier: 2i64,
773            counts: vec![1i64, 2i64, 3i64, 4i64, 5i64, 6i64],
774            indexes: None,
775        } ;
776        "i64_dense"
777    )]
778    #[test_case(
779        ExponentialHistogram {
780            size: 6,
781            floor: 1u64,
782            initial_step: 1u64,
783            step_multiplier: 2u64,
784            counts: vec![1u64, 2u64, 3u64, 4u64, 5u64, 6u64],
785            indexes: None,
786        } ;
787        "u64_dense"
788    )]
789    #[test_case(
790        ExponentialHistogram {
791            size: 10,
792            floor: 1.0,
793            initial_step: 1.0,
794            step_multiplier: 2.0,
795            counts: vec![5.0],
796            indexes: Some(vec![5]),
797        } ;
798        "f64_sparse"
799    )]
800    #[test_case(
801        ExponentialHistogram {
802            size: 10,
803            floor: 1i64,
804            initial_step: 1i64,
805            step_multiplier: 2i64,
806            counts: vec![5i64],
807            indexes: Some(vec![5]),
808        } ;
809        "i64_sparse"
810    )]
811    #[test_case(
812        ExponentialHistogram {
813            size: 10,
814            floor: 1u64,
815            initial_step: 1u64,
816            step_multiplier: 2u64,
817            counts: vec![5u64],
818            indexes: Some(vec![5]),
819        } ;
820        "u64_sparse"
821    )]
822    // | Bucket | Range        |
823    // | ------ | ------------ |
824    // |      0 | (-inf, MIN)  |
825    // |      1 | [MIN, 0)     |
826    // |      2 | [0, MAX)     |
827    // |      3 | [MAX, +inf)  |
828    //
829    #[test_case(
830        ExponentialHistogram {
831            size: 4,
832            floor: f64::MIN,
833            initial_step: f64::MAX,
834            step_multiplier: 2.0,
835            counts: vec![1.0, 2.0, 3.0, 4.0],
836            indexes: None,
837        } ;
838        "f64_bounds"
839    )]
840    // | Bucket | Range         |
841    // | ------ | ------------- |
842    // |      0 | (-inf, MIN+1) |
843    // |      1 | [MIN+1, 0)    |
844    // |      2 | [0, MAX)      |
845    // |      3 | [MAX, +inf)   |
846    #[test_case(
847        ExponentialHistogram {
848            size: 4,
849            // MIN is 1 less than MAX due to two's complement. Modify this such
850            // that bucket ends with MAX.
851            floor: i64::MIN + 1,
852            initial_step: i64::MAX,
853            step_multiplier: 2i64,
854            counts: vec![1i64, 2i64, 3i64, 4i64],
855            indexes: None,
856        } ;
857        "i64_bounds"
858    )]
859    // | Bucket | Range        |
860    // | ------ | ------------ |
861    // |      0 | (-inf, 0)    |
862    // |      1 | [0, MAX)     |
863    // |      2 | [MAX, +inf)  |
864    #[test_case(
865        ExponentialHistogram {
866            size: 3,
867            floor: u64::MIN,
868            initial_step: u64::MAX,
869            step_multiplier: 2u64,
870            // Not possible to go below u64::MIN.
871            counts: vec![0u64, 1u64, 2u64],
872            indexes: None,
873        } ;
874        "u64_bounds"
875    )]
876    #[fuchsia::test]
877    async fn record_exponential_histogram<'a, T>(histogram: ExponentialHistogram<T>)
878    where
879        ArrayContent<T>: InspectData<'a>,
880        ExponentialHistogram<T>: PropertyAssertion + Clone,
881        T: 'static,
882    {
883        let inspector = Inspector::default();
884        ArrayContent::ExponentialHistogram(histogram.clone()).record(inspector.root(), "child");
885        assert_data_tree!(
886            inspector,
887            root: {
888                child: histogram,
889            }
890        );
891    }
892}