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