1pub mod aggregations;
6pub mod experimental;
7
8use crate::aggregations::SumAndCount;
9use fuchsia_async as fasync;
10use fuchsia_inspect::{ArrayProperty, Node as InspectNode};
11
12use std::collections::VecDeque;
13use std::fmt::{self, Debug};
14
15pub struct WindowedStats<T> {
16 stats: VecDeque<T>,
17 capacity: usize,
18 aggregation_fn: Box<dyn Fn(&T, &T) -> T + Send>,
19}
20
21impl<T: Debug> Debug for WindowedStats<T> {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.debug_struct("WindowedStats")
24 .field("stats", &self.stats)
25 .field("capacity", &self.capacity)
26 .finish_non_exhaustive()
27 }
28}
29
30impl<T: Default> WindowedStats<T> {
31 pub fn new(capacity: usize, aggregation_fn: Box<dyn Fn(&T, &T) -> T + Send>) -> Self {
32 let mut stats = VecDeque::with_capacity(capacity);
33 stats.push_back(T::default());
34 Self { stats, capacity, aggregation_fn }
35 }
36
37 pub fn windowed_stat(&self, n: Option<usize>) -> T {
40 let mut aggregated_value = T::default();
41 let n = n.unwrap_or(self.stats.len());
42 for value in self.stats.iter().rev().take(n) {
43 aggregated_value = (self.aggregation_fn)(&aggregated_value, &value);
44 }
45 aggregated_value
46 }
47
48 pub fn slide_window(&mut self) {
49 if !self.stats.is_empty() && self.stats.len() >= self.capacity {
50 let _ = self.stats.pop_front();
51 }
52 self.stats.push_back(T::default());
53 }
54}
55
56impl<T> WindowedStats<T> {
57 pub fn log_value(&mut self, value: &T) {
58 if let Some(latest) = self.stats.back_mut() {
59 *latest = (self.aggregation_fn)(latest, value);
60 }
61 }
62}
63
64impl<T: Into<u64> + Clone> WindowedStats<T> {
65 pub fn log_inspect_uint_array(&self, node: &InspectNode, property_name: &'static str) {
66 let iter = self.stats.iter();
67 let inspect_array = node.create_uint_array(property_name, iter.len());
68 for (i, c) in iter.enumerate() {
69 inspect_array.set(i, (*c).clone());
70 }
71 node.record(inspect_array);
72 }
73}
74
75impl<T: Into<i64> + Clone> WindowedStats<T> {
76 pub fn log_inspect_int_array(&self, node: &InspectNode, property_name: &'static str) {
77 let iter = self.stats.iter();
78 let inspect_array = node.create_int_array(property_name, iter.len());
79 for (i, c) in iter.enumerate() {
80 inspect_array.set(i, (*c).clone());
81 }
82 node.record(inspect_array);
83 }
84}
85
86impl WindowedStats<SumAndCount> {
87 pub fn log_avg_inspect_double_array(&self, node: &InspectNode, property_name: &'static str) {
88 let iter = self.stats.iter();
89 let inspect_array = node.create_double_array(property_name, iter.len());
90 for (i, c) in iter.enumerate() {
91 let value = if c.avg().is_finite() { c.avg() } else { 0f64 };
92 inspect_array.set(i, value);
93 }
94 node.record(inspect_array);
95 }
96}
97
98fn get_start_bound(
101 timestamp: fasync::BootInstant,
102 granularity: zx::BootDuration,
103) -> fasync::BootInstant {
104 timestamp - zx::BootDuration::from_nanos(timestamp.into_nanos() % granularity.into_nanos())
105}
106
107fn get_num_slides_needed(
110 prev: fasync::BootInstant,
111 current: fasync::BootInstant,
112 granularity: zx::BootDuration,
113) -> usize {
114 let prev_start_bound = get_start_bound(prev, granularity);
115 let current_start_bound = get_start_bound(current, granularity);
116 ((current_start_bound - prev_start_bound).into_nanos() / granularity.into_nanos())
117 .try_into()
118 .unwrap_or(0)
119}
120
121pub struct MinutelyWindows(pub usize);
122pub struct FifteenMinutelyWindows(pub usize);
123pub struct HourlyWindows(pub usize);
124
125pub struct TimeSeries<T> {
126 minutely: WindowedStats<T>,
127 fifteen_minutely: WindowedStats<T>,
128 hourly: WindowedStats<T>,
129 created_timestamp: fasync::BootInstant,
131 last_timestamp: fasync::BootInstant,
133}
134
135impl<T: Debug> Debug for TimeSeries<T> {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("TimeSeries")
138 .field("minutely", &self.minutely)
139 .field("fifteen_minutely", &self.fifteen_minutely)
140 .field("hourly", &self.hourly)
141 .field("created_timestamp", &self.created_timestamp)
142 .field("last_timestamp", &self.last_timestamp)
143 .finish()
144 }
145}
146
147impl<T: Default> TimeSeries<T> {
148 pub fn new(create_aggregation_fn: impl Fn() -> Box<dyn Fn(&T, &T) -> T + Send>) -> Self {
149 Self::with_n_windows(
150 MinutelyWindows(60),
151 FifteenMinutelyWindows(24),
152 HourlyWindows(24),
153 create_aggregation_fn,
154 )
155 }
156
157 pub fn with_n_windows(
158 minutely_windows: MinutelyWindows,
159 fifteen_minutely_windows: FifteenMinutelyWindows,
160 hourly_windows: HourlyWindows,
161 create_aggregation_fn: impl Fn() -> Box<dyn Fn(&T, &T) -> T + Send>,
162 ) -> Self {
163 let now = fasync::BootInstant::now();
164 Self {
165 minutely: WindowedStats::new(minutely_windows.0, create_aggregation_fn()),
166 fifteen_minutely: WindowedStats::new(
167 fifteen_minutely_windows.0,
168 create_aggregation_fn(),
169 ),
170 hourly: WindowedStats::new(hourly_windows.0, create_aggregation_fn()),
171 created_timestamp: now,
172 last_timestamp: now,
173 }
174 }
175
176 pub fn update_windows(&mut self) {
179 let now = fasync::BootInstant::now();
180 for _i in
181 0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_minutes(1))
182 {
183 self.minutely.slide_window();
184 }
185 for _i in
186 0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_minutes(15))
187 {
188 self.fifteen_minutely.slide_window();
189 }
190 for _i in
191 0..get_num_slides_needed(self.last_timestamp, now, zx::BootDuration::from_hours(1))
192 {
193 self.hourly.slide_window();
194 }
195 self.last_timestamp = now;
196 }
197
198 pub fn log_value(&mut self, item: &T) {
200 self.update_windows();
201 self.minutely.log_value(item);
202 self.fifteen_minutely.log_value(item);
203 self.hourly.log_value(item);
204 }
205
206 pub fn minutely_iter<'a>(&'a self) -> impl ExactSizeIterator<Item = &'a T> {
208 self.minutely.stats.iter()
209 }
210
211 pub fn get_aggregated_value(&mut self) -> T {
213 self.hourly.windowed_stat(None)
214 }
215
216 pub fn record_schema(&mut self, node: &InspectNode) {
217 let schema = node.create_child("schema");
218 schema.record_int("created_timestamp", self.created_timestamp.into_nanos());
219 schema.record_int("last_timestamp", self.last_timestamp.into_nanos());
220 node.record(schema);
221 }
222}
223
224impl<T: Into<u64> + Clone + Default> TimeSeries<T> {
225 pub fn log_inspect_uint_array(&mut self, node: &InspectNode, child_name: &'static str) {
226 self.update_windows();
227
228 let child = node.create_child(child_name);
229 self.record_schema(&child);
230 self.minutely.log_inspect_uint_array(&child, "1m");
231 self.fifteen_minutely.log_inspect_uint_array(&child, "15m");
232 self.hourly.log_inspect_uint_array(&child, "1h");
233 node.record(child);
234 }
235}
236
237impl<T: Into<i64> + Clone + Default> TimeSeries<T> {
238 pub fn log_inspect_int_array(&mut self, node: &InspectNode, child_name: &'static str) {
239 self.update_windows();
240
241 let child = node.create_child(child_name);
242 self.record_schema(&child);
243 self.minutely.log_inspect_int_array(&child, "1m");
244 self.fifteen_minutely.log_inspect_int_array(&child, "15m");
245 self.hourly.log_inspect_int_array(&child, "1h");
246 node.record(child);
247 }
248}
249
250impl TimeSeries<SumAndCount> {
251 pub fn log_avg_inspect_double_array(&mut self, node: &InspectNode, child_name: &'static str) {
252 self.update_windows();
253
254 let child = node.create_child(child_name);
255 self.record_schema(&child);
256 self.minutely.log_avg_inspect_double_array(&child, "1m");
257 self.fifteen_minutely.log_avg_inspect_double_array(&child, "15m");
258 self.hourly.log_avg_inspect_double_array(&child, "1h");
259 node.record(child);
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::aggregations::create_saturating_add_fn;
267 use diagnostics_assertions::assert_data_tree;
268 use fuchsia_inspect::Inspector;
269
270 #[test]
271 fn windowed_stats_some_windows_populated() {
272 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
273 windowed_stats.log_value(&1u32);
274 windowed_stats.log_value(&2u32);
275 assert_eq!(windowed_stats.windowed_stat(None), 3u32);
276
277 windowed_stats.slide_window();
278 windowed_stats.log_value(&3u32);
279 assert_eq!(windowed_stats.windowed_stat(None), 6u32);
280 }
281
282 #[test]
283 fn windowed_stats_all_windows_populated() {
284 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
285 windowed_stats.log_value(&1u32);
286 assert_eq!(windowed_stats.windowed_stat(None), 1u32);
287
288 windowed_stats.slide_window();
289 windowed_stats.log_value(&2u32);
290 assert_eq!(windowed_stats.windowed_stat(None), 3u32);
291
292 windowed_stats.slide_window();
293 windowed_stats.log_value(&3u32);
294 assert_eq!(windowed_stats.windowed_stat(None), 6u32);
295
296 windowed_stats.slide_window();
297 windowed_stats.log_value(&10u32);
298 assert_eq!(windowed_stats.windowed_stat(None), 15u32);
300 }
301
302 #[test]
303 fn windowed_stats_large_number() {
304 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
305 windowed_stats.log_value(&10u32);
306
307 windowed_stats.slide_window();
308 windowed_stats.log_value(&10u32);
309
310 windowed_stats.slide_window();
311 windowed_stats.log_value(&(u32::MAX - 20u32));
312 assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
313
314 windowed_stats.slide_window();
315 windowed_stats.log_value(&9u32);
316 assert_eq!(windowed_stats.windowed_stat(None), u32::MAX - 1);
317 }
318
319 #[test]
320 fn windowed_stats_test_overflow() {
321 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
322 windowed_stats.log_value(&u32::MAX);
324 windowed_stats.log_value(&1u32);
325 assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
326
327 windowed_stats.slide_window();
328 windowed_stats.log_value(&10u32);
329 assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
330 windowed_stats.slide_window();
331 windowed_stats.log_value(&5u32);
332 assert_eq!(windowed_stats.windowed_stat(None), u32::MAX);
333 windowed_stats.slide_window();
334 windowed_stats.log_value(&3u32);
335 assert_eq!(windowed_stats.windowed_stat(None), 18u32);
336 }
337
338 #[test]
339 fn windowed_stats_n_arg() {
340 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
341 windowed_stats.log_value(&1u32);
342 assert_eq!(windowed_stats.windowed_stat(Some(0)), 0u32);
343 assert_eq!(windowed_stats.windowed_stat(Some(1)), 1u32);
344 assert_eq!(windowed_stats.windowed_stat(Some(2)), 1u32);
345
346 windowed_stats.slide_window();
347 windowed_stats.log_value(&2u32);
348 assert_eq!(windowed_stats.windowed_stat(Some(1)), 2u32);
349 assert_eq!(windowed_stats.windowed_stat(Some(2)), 3u32);
350 }
351
352 #[fuchsia::test]
353 async fn windowed_stats_log_inspect_uint_array() {
354 let inspector = Inspector::default();
355 let mut windowed_stats = WindowedStats::<u32>::new(3, create_saturating_add_fn());
356 windowed_stats.log_value(&1u32);
357 windowed_stats.slide_window();
358 windowed_stats.log_value(&2u32);
359
360 windowed_stats.log_inspect_uint_array(inspector.root(), "stats");
361
362 assert_data_tree!(inspector, root: {
363 stats: vec![1u64, 2],
364 });
365 }
366
367 #[fuchsia::test]
368 async fn windowed_stats_log_inspect_int_array() {
369 let inspector = Inspector::default();
370 let mut windowed_stats = WindowedStats::<i32>::new(3, create_saturating_add_fn());
371 windowed_stats.log_value(&1i32);
372 windowed_stats.slide_window();
373 windowed_stats.log_value(&2i32);
374
375 windowed_stats.log_inspect_int_array(inspector.root(), "stats");
376
377 assert_data_tree!(inspector, root: {
378 stats: vec![1i64, 2],
379 });
380 }
381
382 #[fuchsia::test]
383 async fn windowed_stats_sum_and_count_log_avg_inspect_double_array() {
384 let inspector = Inspector::default();
385 let mut windowed_stats = WindowedStats::<SumAndCount>::new(3, create_saturating_add_fn());
386 windowed_stats.log_value(&SumAndCount { sum: 1u32, count: 1 });
387 windowed_stats.log_value(&SumAndCount { sum: 2u32, count: 1 });
388
389 windowed_stats.log_avg_inspect_double_array(inspector.root(), "stats");
390
391 assert_data_tree!(inspector, root: {
392 stats: vec![3f64 / 2f64],
393 });
394 }
395
396 #[fuchsia::test]
397 async fn windowed_stats_sum_and_count_log_avg_inspect_double_array_with_nan() {
398 let inspector = Inspector::default();
399 let windowed_stats = WindowedStats::<SumAndCount>::new(3, create_saturating_add_fn());
400
401 windowed_stats.log_avg_inspect_double_array(inspector.root(), "stats");
402
403 assert_data_tree!(inspector, root: {
404 stats: vec![0f64],
405 });
406 }
407
408 #[test]
409 fn time_series_window_transition() {
410 let mut exec = fasync::TestExecutor::new_with_fake_time();
411 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3599_000_000_000));
412 let inspector = Inspector::default();
413
414 let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
415 time_series.log_value(&1u32);
416
417 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3600_000_000_001));
420 time_series.log_value(&2u32);
421
422 time_series.log_inspect_uint_array(inspector.root(), "stats");
423 assert_data_tree!(@executor exec, inspector, root: {
424 stats: {
425 "1m": vec![1u64, 2],
426 "15m": vec![1u64, 2],
427 "1h": vec![1u64, 2],
428 schema: {
429 "created_timestamp": 3599_000_000_000i64,
430 "last_timestamp": 3600_000_000_001i64,
431 }
432 }
433 });
434
435 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3659_000_000_000));
436 time_series.log_value(&3u32);
437 time_series.log_inspect_uint_array(inspector.root(), "stats2");
438
439 assert_data_tree!(@executor exec, inspector, root: contains {
441 stats2: {
442 "1m": vec![1u64, 5],
443 "15m": vec![1u64, 5],
444 "1h": vec![1u64, 5],
445 schema: {
446 "created_timestamp": 3599_000_000_000i64,
447 "last_timestamp": 3659_000_000_000i64,
448 }
449 }
450 });
451
452 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3660_000_000_000));
453 time_series.log_value(&4u32);
454 time_series.log_inspect_uint_array(inspector.root(), "stats3");
455
456 assert_data_tree!(@executor exec, inspector, root: contains {
459 stats3: {
460 "1m": vec![1u64, 5, 4],
461 "15m": vec![1u64, 9],
462 "1h": vec![1u64, 9],
463 schema: {
464 "created_timestamp": 3599_000_000_000i64,
465 "last_timestamp": 3660_000_000_000i64,
466 }
467 }
468 });
469
470 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(4500_000_000_001));
471 time_series.log_value(&5u32);
472 time_series.log_inspect_uint_array(inspector.root(), "stats4");
473
474 assert_data_tree!(@executor exec, inspector, root: contains {
478 stats4: {
479 "1m": vec![1u64, 5, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
480 "15m": vec![1u64, 9, 5],
481 "1h": vec![1u64, 14],
482 schema: {
483 "created_timestamp": 3599_000_000_000i64,
484 "last_timestamp": 4500_000_000_001i64,
485 }
486 }
487 });
488 }
489
490 #[test]
491 fn time_series_minutely_iter() {
492 let exec = fasync::TestExecutor::new_with_fake_time();
493 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(59_000_000_000));
494 let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
495 time_series.log_value(&1u32);
496 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
497 time_series.log_value(&2u32);
498 let minutely_data: Vec<_> = time_series.minutely_iter().map(|v| *v).collect();
499 assert_eq!(minutely_data, [1, 2]);
500 }
501
502 #[test]
503 fn time_series_get_aggregated_value() {
504 let exec = fasync::TestExecutor::new_with_fake_time();
505 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3599_000_000_000));
506 let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
507 time_series.log_value(&1u32);
508 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(3600_000_000_001));
509 time_series.log_value(&2u32);
510 assert_eq!(time_series.get_aggregated_value(), 3);
511 }
512
513 #[fuchsia::test]
514 fn time_series_log_inspect_uint_array() {
515 let mut exec = fasync::TestExecutor::new_with_fake_time();
516 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
517 let inspector = Inspector::default();
518
519 let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
520 time_series.log_value(&1u32);
521
522 time_series.log_inspect_uint_array(inspector.root(), "stats");
523
524 assert_data_tree!(@executor exec, inspector, root: {
525 stats: {
526 "1m": vec![1u64],
527 "15m": vec![1u64],
528 "1h": vec![1u64],
529 schema: {
530 "created_timestamp": 961_000_000_000i64,
531 "last_timestamp": 961_000_000_000i64,
532 }
533 }
534 });
535 }
536
537 #[test]
538 fn time_series_log_inspect_uint_array_automatically_update_windows() {
539 let mut exec = fasync::TestExecutor::new_with_fake_time();
540 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
541 let inspector = Inspector::default();
542
543 let mut time_series = TimeSeries::<u32>::new(create_saturating_add_fn);
544 time_series.log_value(&1u32);
545
546 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
547 time_series.log_inspect_uint_array(inspector.root(), "stats");
548
549 assert_data_tree!(@executor exec, inspector, root: {
550 stats: {
551 "1m": vec![1u64, 0],
552 "15m": vec![1u64],
553 "1h": vec![1u64],
554 schema: {
555 "created_timestamp": 961_000_000_000i64,
556 "last_timestamp": 1021_000_000_000i64,
557 }
558 }
559 });
560 }
561
562 #[test]
563 fn time_series_stats_log_inspect_int_array() {
564 let mut exec = fasync::TestExecutor::new_with_fake_time();
565 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
566 let inspector = Inspector::default();
567
568 let mut time_series = TimeSeries::<i32>::new(create_saturating_add_fn);
569 time_series.log_value(&1i32);
570
571 time_series.log_inspect_int_array(inspector.root(), "stats");
572
573 assert_data_tree!(@executor exec, inspector, root: {
574 stats: {
575 "1m": vec![1i64],
576 "15m": vec![1i64],
577 "1h": vec![1i64],
578 schema: {
579 "created_timestamp": 961_000_000_000i64,
580 "last_timestamp": 961_000_000_000i64,
581 }
582 }
583 });
584 }
585
586 #[test]
587 fn time_series_log_inspect_int_array_automatically_update_windows() {
588 let mut exec = fasync::TestExecutor::new_with_fake_time();
589 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
590 let inspector = Inspector::default();
591
592 let mut time_series = TimeSeries::<i32>::new(create_saturating_add_fn);
593 time_series.log_value(&1i32);
594
595 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
596 time_series.log_inspect_int_array(inspector.root(), "stats");
597
598 assert_data_tree!(@executor exec, inspector, root: {
599 stats: {
600 "1m": vec![1i64, 0],
601 "15m": vec![1i64],
602 "1h": vec![1i64],
603 schema: {
604 "created_timestamp": 961_000_000_000i64,
605 "last_timestamp": 1021_000_000_000i64,
606 }
607 }
608 });
609 }
610
611 #[test]
612 fn time_series_sum_and_count_log_avg_inspect_double_array() {
613 let mut exec = fasync::TestExecutor::new_with_fake_time();
614 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
615 let inspector = Inspector::default();
616 let mut time_series = TimeSeries::<SumAndCount>::new(create_saturating_add_fn);
617 time_series.log_value(&SumAndCount { sum: 1u32, count: 1 });
618 time_series.log_value(&SumAndCount { sum: 2u32, count: 1 });
619
620 time_series.log_avg_inspect_double_array(inspector.root(), "stats");
621
622 assert_data_tree!(@executor exec, inspector, root: {
623 stats: {
624 "1m": vec![3f64 / 2f64],
625 "15m": vec![3f64 / 2f64],
626 "1h": vec![3f64 / 2f64],
627 schema: {
628 "created_timestamp": 961_000_000_000i64,
629 "last_timestamp": 961_000_000_000i64,
630 }
631 }
632 });
633 }
634
635 #[test]
636 fn time_series_sum_and_count_log_avg_inspect_double_array_automatically_update_windows() {
637 let mut exec = fasync::TestExecutor::new_with_fake_time();
638 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(961_000_000_000));
639 let inspector = Inspector::default();
640 let mut time_series = TimeSeries::<SumAndCount>::new(create_saturating_add_fn);
641 time_series.log_value(&SumAndCount { sum: 1u32, count: 1 });
642 time_series.log_value(&SumAndCount { sum: 2u32, count: 1 });
643
644 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1021_000_000_000));
645 time_series.log_avg_inspect_double_array(inspector.root(), "stats");
646
647 assert_data_tree!(@executor exec, inspector, root: {
648 stats: {
649 "1m": vec![3f64 / 2f64, 0f64],
650 "15m": vec![3f64 / 2f64],
651 "1h": vec![3f64 / 2f64],
652 schema: {
653 "created_timestamp": 961_000_000_000i64,
654 "last_timestamp": 1021_000_000_000i64,
655 }
656 }
657 });
658 }
659}