@@ -3,15 +3,18 @@ use anyhow::Result;
33use sightglass_data:: { EffectSize , Engine , Measurement , Phase , Summary } ;
44use std:: { collections:: BTreeSet , io:: Write } ;
55
6- /// Find the effect size (and confidence interval) of between two different
7- /// engines (i.e. two different commits of Wasmtime).
6+ /// Find the effect size (and confidence interval) between different engines
7+ /// (i.e. different commits of Wasmtime).
88///
99/// This allows us to justify statements like "we are 99% confident that the new
1010/// register allocator is 13.6% faster (± 1.7%) than the old register
1111/// allocator."
1212///
13- /// This can only test differences between the results for exactly two different
14- /// engines. If there aren't exactly two different engines represented in
13+ /// The `measurements` must contain results for two or more different engines.
14+ /// Each returned [`EffectSize`] compares exactly two different engines: for
15+ /// every group of measurements that share an architecture, benchmark, phase,
16+ /// and event, one `EffectSize` is produced for each pair of engines present in
17+ /// that group. If fewer than two different engines are represented in
1518/// `measurements` then an error is returned.
1619pub fn calculate < ' a > (
1720 significance_level : f64 ,
@@ -24,52 +27,58 @@ pub fn calculate<'a>(
2427 Found {significance_level}."
2528 ) ;
2629
30+ // We need at least two different engines to have anything to compare.
31+ let all_engines: BTreeSet < _ > = measurements. iter ( ) . map ( |m| & m. engine ) . collect ( ) ;
32+ anyhow:: ensure!(
33+ all_engines. len( ) >= 2 ,
34+ "Comparing effect sizes requires two or more different engines. Found {} \
35+ different engines.",
36+ all_engines. len( )
37+ ) ;
38+
2739 let keys = KeyBuilder :: all ( )
2840 . engine ( false )
2941 . engine_flags ( false )
3042 . keys ( measurements) ;
31- let mut results = Vec :: with_capacity ( keys . len ( ) ) ;
43+ let mut results = Vec :: new ( ) ;
3244
3345 for key in keys {
3446 let key_measurements: Vec < _ > = measurements. iter ( ) . filter ( |m| key. matches ( m) ) . collect ( ) ;
3547
36- // NB: `BTreeSet` so they're always sorted.
37- let engines: BTreeSet < _ > = key_measurements. iter ( ) . map ( |m| & m. engine ) . collect ( ) ;
38- anyhow:: ensure!(
39- engines. len( ) == 2 ,
40- "Can only test significance between exactly two different engines. Found {} \
41- different engines.",
42- engines. len( )
43- ) ;
48+ let mut engines: Vec < _ > = key_measurements. iter ( ) . map ( |m| & m. engine ) . collect ( ) ;
49+ engines. sort ( ) ;
4450
45- let mut engines = engines. into_iter ( ) ;
46- let engine_a = engines. next ( ) . unwrap ( ) ;
47- let engine_b = engines. next ( ) . unwrap ( ) ;
51+ // Create an `EffectSize` comparing each pair of distinct engines within
52+ // this group of measurements.
53+ for ( i, engine_a) in engines. iter ( ) . enumerate ( ) {
54+ for engine_b in & engines[ i + 1 ..] {
55+ let a: behrens_fisher:: Stats = key_measurements
56+ . iter ( )
57+ . filter ( |m| & m. engine == * engine_a)
58+ . map ( |m| m. count as f64 )
59+ . collect ( ) ;
60+ let b: behrens_fisher:: Stats = key_measurements
61+ . iter ( )
62+ . filter ( |m| & m. engine == * engine_b)
63+ . map ( |m| m. count as f64 )
64+ . collect ( ) ;
4865
49- let a: behrens_fisher:: Stats = key_measurements
50- . iter ( )
51- . filter ( |m| & m. engine == engine_a)
52- . map ( |m| m. count as f64 )
53- . collect ( ) ;
54- let b: behrens_fisher:: Stats = key_measurements
55- . iter ( )
56- . filter ( |m| & m. engine == engine_b)
57- . map ( |m| m. count as f64 )
58- . collect ( ) ;
59-
60- let ci = behrens_fisher:: confidence_interval ( 1.0 - significance_level, a, b) . unwrap_or ( 0.0 ) ;
61- results. push ( EffectSize {
62- arch : key. arch . unwrap ( ) ,
63- wasm : key. wasm . unwrap ( ) ,
64- phase : key. phase . unwrap ( ) ,
65- event : key. event . unwrap ( ) ,
66- a_engine : engine_a. clone ( ) ,
67- a_mean : a. mean ,
68- b_engine : engine_b. clone ( ) ,
69- b_mean : b. mean ,
70- significance_level,
71- half_width_confidence_interval : ci,
72- } ) ;
66+ let ci = behrens_fisher:: confidence_interval ( 1.0 - significance_level, a, b)
67+ . unwrap_or ( 0.0 ) ;
68+ results. push ( EffectSize {
69+ arch : key. arch . clone ( ) . unwrap ( ) ,
70+ wasm : key. wasm . clone ( ) . unwrap ( ) ,
71+ phase : key. phase . unwrap ( ) ,
72+ event : key. event . clone ( ) . unwrap ( ) ,
73+ a_engine : ( * engine_a) . clone ( ) ,
74+ a_mean : a. mean ,
75+ b_engine : ( * engine_b) . clone ( ) ,
76+ b_mean : b. mean ,
77+ significance_level,
78+ half_width_confidence_interval : ci,
79+ } ) ;
80+ }
81+ }
7382 }
7483
7584 Ok ( results)
@@ -182,3 +191,66 @@ pub fn write(
182191
183192 Ok ( ( ) )
184193}
194+
195+ #[ cfg( test) ]
196+ mod tests {
197+ use super :: * ;
198+
199+ fn measurement < ' a > ( engine : & ' a str , count : u64 ) -> Measurement < ' a > {
200+ Measurement {
201+ arch : "x86_64" . into ( ) ,
202+ engine : Engine {
203+ name : engine. into ( ) ,
204+ flags : None ,
205+ } ,
206+ wasm : "bench.wasm" . into ( ) ,
207+ process : 0 ,
208+ iteration : 0 ,
209+ phase : Phase :: Execution ,
210+ event : "cycles" . into ( ) ,
211+ count,
212+ }
213+ }
214+
215+ #[ test]
216+ fn effect_size_for_each_pair_of_engines ( ) -> Result < ( ) > {
217+ // Three engines within a single (arch, wasm, phase, event) group should
218+ // yield one `EffectSize` per unordered pair of engines: (a, b), (a, c),
219+ // and (b, c).
220+ let mut measurements = vec ! [ ] ;
221+ for ( engine, base) in [ ( "a" , 100 ) , ( "b" , 200 ) , ( "c" , 300 ) ] {
222+ for i in 0 ..5 {
223+ measurements. push ( measurement ( engine, base + i) ) ;
224+ }
225+ }
226+
227+ let effect_sizes = calculate ( 0.01 , & measurements) ?;
228+ assert_eq ! ( effect_sizes. len( ) , 3 ) ;
229+
230+ let pairs: Vec < ( String , String ) > = effect_sizes
231+ . iter ( )
232+ . map ( |e| ( e. a_engine . name . to_string ( ) , e. b_engine . name . to_string ( ) ) )
233+ . collect ( ) ;
234+ assert_eq ! (
235+ pairs,
236+ vec![
237+ ( "a" . to_string( ) , "b" . to_string( ) ) ,
238+ ( "a" . to_string( ) , "c" . to_string( ) ) ,
239+ ( "b" . to_string( ) , "c" . to_string( ) ) ,
240+ ]
241+ ) ;
242+
243+ // Every `EffectSize` compares two *different* engines.
244+ for e in & effect_sizes {
245+ assert_ne ! ( e. a_engine, e. b_engine) ;
246+ }
247+
248+ Ok ( ( ) )
249+ }
250+
251+ #[ test]
252+ fn effect_size_requires_at_least_two_engines ( ) {
253+ let measurements = vec ! [ measurement( "only" , 1 ) , measurement( "only" , 2 ) ] ;
254+ assert ! ( calculate( 0.01 , & measurements) . is_err( ) ) ;
255+ }
256+ }
0 commit comments