Skip to content

Commit d8120ed

Browse files
bors[bot]matklad
andauthored
Merge #8500
8500: internal: fix flakiness of accidentally quadratic test r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
2 parents 11e7371 + 327323a commit d8120ed

File tree

3 files changed

+131
-82
lines changed

3 files changed

+131
-82
lines changed

crates/ide/src/syntax_highlighting/tests.rs

Lines changed: 17 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ use std::time::Instant;
22

33
use expect_test::{expect_file, ExpectFile};
44
use ide_db::SymbolKind;
5-
use stdx::format_to;
6-
use test_utils::{bench, bench_fixture, skip_slow_tests};
5+
use test_utils::{bench, bench_fixture, skip_slow_tests, AssertLinear};
76

87
use crate::{fixture, FileRange, HlTag, TextRange};
98

@@ -266,90 +265,27 @@ fn syntax_highlighting_not_quadratic() {
266265
return;
267266
}
268267

269-
let mut measures = Vec::new();
270-
for i in 6..=10 {
271-
let n = 1 << i;
272-
let fixture = bench_fixture::big_struct_n(n);
273-
let (analysis, file_id) = fixture::file(&fixture);
268+
let mut al = AssertLinear::default();
269+
while al.next_round() {
270+
for i in 6..=10 {
271+
let n = 1 << i;
274272

275-
let time = Instant::now();
273+
let fixture = bench_fixture::big_struct_n(n);
274+
let (analysis, file_id) = fixture::file(&fixture);
276275

277-
let hash = analysis
278-
.highlight(file_id)
279-
.unwrap()
280-
.iter()
281-
.filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
282-
.count();
283-
assert!(hash > n as usize);
276+
let time = Instant::now();
284277

285-
let elapsed = time.elapsed();
286-
measures.push((n as f64, elapsed.as_millis() as f64))
287-
}
278+
let hash = analysis
279+
.highlight(file_id)
280+
.unwrap()
281+
.iter()
282+
.filter(|it| it.highlight.tag == HlTag::Symbol(SymbolKind::Struct))
283+
.count();
284+
assert!(hash > n as usize);
288285

289-
assert_linear(&measures)
290-
}
291-
292-
/// Checks that a set of measurements looks like a linear function rather than
293-
/// like a quadratic function. Algorithm:
294-
///
295-
/// 1. Linearly scale input to be in [0; 1)
296-
/// 2. Using linear regression, compute the best linear function approximating
297-
/// the input.
298-
/// 3. Compute RMSE and maximal absolute error.
299-
/// 4. Check that errors are within tolerances and that the constant term is not
300-
/// too negative.
301-
///
302-
/// Ideally, we should use a proper "model selection" to directly compare
303-
/// quadratic and linear models, but that sounds rather complicated:
304-
///
305-
/// https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data
306-
fn assert_linear(xy: &[(f64, f64)]) {
307-
let (mut xs, mut ys): (Vec<_>, Vec<_>) = xy.iter().copied().unzip();
308-
normalize(&mut xs);
309-
normalize(&mut ys);
310-
let xy = xs.iter().copied().zip(ys.iter().copied());
311-
312-
// Linear regression: finding a and b to fit y = a + b*x.
313-
314-
let mean_x = mean(&xs);
315-
let mean_y = mean(&ys);
316-
317-
let b = {
318-
let mut num = 0.0;
319-
let mut denom = 0.0;
320-
for (x, y) in xy.clone() {
321-
num += (x - mean_x) * (y - mean_y);
322-
denom += (x - mean_x).powi(2);
286+
let elapsed = time.elapsed();
287+
al.sample(n as f64, elapsed.as_millis() as f64);
323288
}
324-
num / denom
325-
};
326-
327-
let a = mean_y - b * mean_x;
328-
329-
let mut plot = format!("y_pred = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b);
330-
331-
let mut se = 0.0;
332-
let mut max_error = 0.0f64;
333-
for (x, y) in xy {
334-
let y_pred = a + b * x;
335-
se += (y - y_pred).powi(2);
336-
max_error = max_error.max((y_pred - y).abs());
337-
338-
format_to!(plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred);
339-
}
340-
341-
let rmse = (se / xs.len() as f64).sqrt();
342-
format_to!(plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error);
343-
344-
assert!(rmse < 0.05 && max_error < 0.1 && a > -0.1, "\nLooks quadratic\n{}", plot);
345-
346-
fn normalize(xs: &mut Vec<f64>) {
347-
let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
348-
xs.iter_mut().for_each(|it| *it /= max);
349-
}
350-
351-
fn mean(xs: &[f64]) -> f64 {
352-
xs.iter().copied().sum::<f64>() / (xs.len() as f64)
353289
}
354290
}
355291

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Checks that a set of measurements looks like a linear function rather than
2+
//! like a quadratic function. Algorithm:
3+
//!
4+
//! 1. Linearly scale input to be in [0; 1)
5+
//! 2. Using linear regression, compute the best linear function approximating
6+
//! the input.
7+
//! 3. Compute RMSE and maximal absolute error.
8+
//! 4. Check that errors are within tolerances and that the constant term is not
9+
//! too negative.
10+
//!
11+
//! Ideally, we should use a proper "model selection" to directly compare
12+
//! quadratic and linear models, but that sounds rather complicated:
13+
//!
14+
//! https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data
15+
//!
16+
//! We might get false positives on a VM, but never false negatives. So, if the
17+
//! first round fails, we repeat the ordeal three more times and fail only if
18+
//! every time there's a fault.
19+
use stdx::format_to;
20+
21+
#[derive(Default)]
22+
pub struct AssertLinear {
23+
rounds: Vec<Round>,
24+
}
25+
26+
#[derive(Default)]
27+
struct Round {
28+
samples: Vec<(f64, f64)>,
29+
plot: String,
30+
linear: bool,
31+
}
32+
33+
impl AssertLinear {
34+
pub fn next_round(&mut self) -> bool {
35+
if let Some(round) = self.rounds.last_mut() {
36+
round.finish();
37+
}
38+
if self.rounds.iter().any(|it| it.linear) || self.rounds.len() == 4 {
39+
return false;
40+
}
41+
self.rounds.push(Round::default());
42+
true
43+
}
44+
45+
pub fn sample(&mut self, x: f64, y: f64) {
46+
self.rounds.last_mut().unwrap().samples.push((x, y))
47+
}
48+
}
49+
50+
impl Drop for AssertLinear {
51+
fn drop(&mut self) {
52+
assert!(!self.rounds.is_empty());
53+
if self.rounds.iter().all(|it| !it.linear) {
54+
for round in &self.rounds {
55+
eprintln!("\n{}", round.plot);
56+
}
57+
panic!("Doesn't look linear!")
58+
}
59+
}
60+
}
61+
62+
impl Round {
63+
fn finish(&mut self) {
64+
let (mut xs, mut ys): (Vec<_>, Vec<_>) = self.samples.iter().copied().unzip();
65+
normalize(&mut xs);
66+
normalize(&mut ys);
67+
let xy = xs.iter().copied().zip(ys.iter().copied());
68+
69+
// Linear regression: finding a and b to fit y = a + b*x.
70+
71+
let mean_x = mean(&xs);
72+
let mean_y = mean(&ys);
73+
74+
let b = {
75+
let mut num = 0.0;
76+
let mut denom = 0.0;
77+
for (x, y) in xy.clone() {
78+
num += (x - mean_x) * (y - mean_y);
79+
denom += (x - mean_x).powi(2);
80+
}
81+
num / denom
82+
};
83+
84+
let a = mean_y - b * mean_x;
85+
86+
self.plot = format!("y_pred = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b);
87+
88+
let mut se = 0.0;
89+
let mut max_error = 0.0f64;
90+
for (x, y) in xy {
91+
let y_pred = a + b * x;
92+
se += (y - y_pred).powi(2);
93+
max_error = max_error.max((y_pred - y).abs());
94+
95+
format_to!(self.plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred);
96+
}
97+
98+
let rmse = (se / xs.len() as f64).sqrt();
99+
format_to!(self.plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error);
100+
101+
self.linear = rmse < 0.05 && max_error < 0.1 && a > -0.1;
102+
103+
fn normalize(xs: &mut Vec<f64>) {
104+
let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
105+
xs.iter_mut().for_each(|it| *it /= max);
106+
}
107+
108+
fn mean(xs: &[f64]) -> f64 {
109+
xs.iter().copied().sum::<f64>() / (xs.len() as f64)
110+
}
111+
}
112+
}

crates/test_utils/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
pub mod bench_fixture;
1010
mod fixture;
11+
mod assert_linear;
1112

1213
use std::{
1314
convert::{TryFrom, TryInto},
@@ -22,7 +23,7 @@ use text_size::{TextRange, TextSize};
2223
pub use dissimilar::diff as __diff;
2324
pub use rustc_hash::FxHashMap;
2425

25-
pub use crate::fixture::Fixture;
26+
pub use crate::{assert_linear::AssertLinear, fixture::Fixture};
2627

2728
pub const CURSOR_MARKER: &str = "$0";
2829
pub const ESCAPED_CURSOR_MARKER: &str = "\\$0";

0 commit comments

Comments
 (0)