xref: /aosp_15_r20/external/crosvm/audio_streams_conformance_test/src/performance_data.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::fmt;
6 use std::time::Duration;
7 
8 use serde::Serialize;
9 
10 use crate::args::Args;
11 use crate::error::*;
12 
13 const NANOS_PER_MICROS: f32 = 1_000_000.0;
14 
15 /// `PerformanceReport` is the estimated buffer consumption rate and error term
16 /// derived by the linear regression of `BufferConsumptionRecord`.
17 #[derive(Debug, Serialize)]
18 pub struct PerformanceReport {
19     args: Args,
20     cold_start_latency: Duration,
21     record_count: usize,
22     rate: EstimatedRate,
23     /// {min, max, avg, stddev}_time for per "next_buffer + zero write + commit" call
24     min_time: Duration,
25     max_time: Duration,
26     avg_time: Duration,
27     stddev_time: Duration,
28     /// How many times that consumed frames are different from buffer_frames.
29     mismatched_frame_count: u32,
30 }
31 
32 impl fmt::Display for PerformanceReport {
33     #[allow(clippy::print_in_format_impl)]
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result34     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35         if self.mismatched_frame_count != 0 {
36             eprint!(
37                 "[Error] {} consumed buffers size != {} frames",
38                 self.mismatched_frame_count, self.args.buffer_frames
39             );
40         }
41         write!(
42             f,
43             r#"{}
44 Cold start latency: {:?}
45 Records count: {}
46 [Step] min: {:.2} ms, max: {:.2} ms, average: {:.2} ms, standard deviation: {:.2} ms.
47 {}
48 "#,
49             self.args,
50             self.cold_start_latency,
51             self.record_count,
52             to_micros(self.min_time),
53             to_micros(self.max_time),
54             to_micros(self.avg_time),
55             to_micros(self.stddev_time),
56             self.rate,
57         )
58     }
59 }
60 
61 /// `BufferConsumptionRecord` records the timestamp and the
62 /// accumulated number of consumed frames at every stream buffer commit.
63 /// It is used to compute the buffer consumption rate.
64 #[derive(Debug, Default)]
65 pub struct BufferConsumptionRecord {
66     pub ts: Duration,
67     pub frames: usize,
68 }
69 
70 impl BufferConsumptionRecord {
new(frames: usize, ts: Duration) -> Self71     pub fn new(frames: usize, ts: Duration) -> Self {
72         Self { ts, frames }
73     }
74 }
75 
76 #[derive(Debug, Serialize, PartialEq)]
77 pub struct EstimatedRate {
78     /// linear coefficients of LINEST(frames,timestamps).
79     rate: f64,
80     /// STEYX(frames, timestamps).
81     error: f64,
82 }
83 
84 impl EstimatedRate {
new(rate: f64, error: f64) -> Self85     fn new(rate: f64, error: f64) -> Self {
86         Self { rate, error }
87     }
88 }
89 
90 impl fmt::Display for EstimatedRate {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result91     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92         write!(
93             f,
94             "[Linear Regression] rate: {:.2} frames/s, standard error: {:.2} ",
95             self.rate, self.error
96         )
97     }
98 }
99 
100 #[derive(Debug, Default)]
101 pub struct PerformanceData {
102     pub cold_start: Duration,
103     pub records: Vec<BufferConsumptionRecord>,
104 }
105 
to_micros(t: Duration) -> f32106 fn to_micros(t: Duration) -> f32 {
107     t.as_nanos() as f32 / NANOS_PER_MICROS
108 }
109 
linear_regression(x: &[f64], y: &[f64]) -> Result<EstimatedRate>110 fn linear_regression(x: &[f64], y: &[f64]) -> Result<EstimatedRate> {
111     if x.len() != y.len() {
112         return Err(Error::MismatchedSamples);
113     }
114 
115     if x.len() <= 2 {
116         return Err(Error::NotEnoughSamples);
117     }
118 
119     /* hat(y_i) = b(x_i) + a */
120     let x_sum: f64 = x.iter().sum();
121     let x_average = x_sum / x.len() as f64;
122     // sum(x_i * x_i)
123     let x_square_sum: f64 = x.iter().map(|&xi| xi * xi).sum();
124     // sum(x_i * y_i)
125     let x_y_sum: f64 = x.iter().zip(y.iter()).map(|(&xi, &yi)| xi * yi).sum();
126 
127     let y_sum: f64 = y.iter().sum();
128 
129     let y_square_sum: f64 = y.iter().map(|yi| yi * yi).sum();
130     /* b = (n * sum(x * y) - sum(x) * sum(y)) / (n * sum(x ^ 2) - sum(x) ^ 2)
131     = (sum(x * y) - avg(x) * sum(y)) / (sum(x ^ 2) - avg(x) * sum(x)) */
132     let b = (x_y_sum - x_average * y_sum) / (x_square_sum - x_average * x_sum);
133     let n = y.len() as f64;
134     /* err = sqrt(sum((y_i - hat(y_i)) ^ 2) / n) */
135     let err: f64 =
136         ((n * y_square_sum - y_sum * y_sum - b * b * (n * x_square_sum - x_sum * x_sum))
137             / (n * (n - 2.0)))
138             .sqrt();
139 
140     Ok(EstimatedRate::new(b, err))
141 }
142 
143 impl PerformanceData {
print_records(&self)144     pub fn print_records(&self) {
145         println!("TS\t\tTS_DIFF\t\tPLAYED");
146         let mut previous_ts = 0.0;
147         for record in &self.records {
148             println!(
149                 "{:.6}\t{:.6}\t{}",
150                 record.ts.as_secs_f64(),
151                 record.ts.as_secs_f64() - previous_ts,
152                 record.frames
153             );
154             previous_ts = record.ts.as_secs_f64();
155         }
156     }
gen_report(&self, args: Args) -> Result<PerformanceReport>157     pub fn gen_report(&self, args: Args) -> Result<PerformanceReport> {
158         let time_records: Vec<f64> = self
159             .records
160             .iter()
161             .map(|record| record.ts.as_secs_f64())
162             .collect();
163 
164         let frames: Vec<f64> = self
165             .records
166             .iter()
167             .map(|record| record.frames as f64)
168             .collect();
169 
170         let mut steps = Vec::new();
171         let mut mismatched_frame_count = 0;
172         for i in 1..frames.len() {
173             let time_diff = self.records[i].ts - self.records[i - 1].ts;
174             steps.push(time_diff);
175 
176             let frame_diff = self.records[i].frames - self.records[i - 1].frames;
177             if frame_diff != args.buffer_frames {
178                 mismatched_frame_count += 1;
179             }
180         }
181         let avg_time = steps
182             .iter()
183             .sum::<Duration>()
184             .checked_div(steps.len() as u32)
185             .ok_or(Error::NotEnoughSamples)?;
186         let stddev_time = (steps
187             .iter()
188             .map(|x| {
189                 (x.as_nanos().abs_diff(avg_time.as_nanos())
190                     * x.as_nanos().abs_diff(avg_time.as_nanos())) as f64
191             })
192             .sum::<f64>()
193             / steps.len() as f64)
194             .sqrt();
195 
196         let rate = linear_regression(&time_records, &frames)?;
197         let min_time = steps.iter().min().unwrap().to_owned();
198         let max_time = steps.iter().max().unwrap().to_owned();
199 
200         Ok(PerformanceReport {
201             args,
202             cold_start_latency: self.cold_start,
203             record_count: self.records.len(),
204             rate,
205             min_time,
206             max_time,
207             avg_time,
208             stddev_time: Duration::from_nanos(stddev_time as u64),
209             mismatched_frame_count,
210         })
211     }
212 }
213 
214 #[cfg(test)]
215 mod tests {
216     use super::*;
217 
218     #[test]
test1()219     fn test1() {
220         let xs: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
221         let ys: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
222 
223         assert_eq!(
224             EstimatedRate::new(1.0, 0.0),
225             linear_regression(&xs, &ys).expect("test1 should pass")
226         );
227     }
228 
229     #[test]
test2()230     fn test2() {
231         let xs: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
232         let ys: Vec<f64> = vec![2.0, 4.0, 5.0, 4.0, 5.0];
233 
234         assert_eq!(
235             EstimatedRate::new(0.6, 0.8944271909999159),
236             linear_regression(&xs, &ys).expect("test2 should pass")
237         );
238     }
239 }
240