@@ -62,6 +62,117 @@ def wait_for_response_count(imap, type:, count:,
6262 end
6363 end
6464
65+ # assert_linear_performance didn't fail reliably until "n" was far too high,
66+ # even though the problem was very obvious at lower "n" values, by looking at
67+ # the mean (plus stddev) rather than the max (plus variance-based "safety
68+ # factor").
69+ #
70+ # So rather than use "max" as the baseline, this uses μ + 2σ (or max).
71+ def assert_strict_linear_time ( sequence , prepare : proc do end ,
72+ base_repeats : 100 ,
73+ repeats : 5 ,
74+ allow_stdev_above_mean : 2 ,
75+ outlier_safety_factor : 3 ,
76+ mean_safety_factor : 2 ,
77+ verbose : false ,
78+ &code )
79+ pend "No PERFORMANCE_CLOCK found" unless defined? ( PERFORMANCE_CLOCK )
80+
81+ measure = proc do |&block |
82+ st = Process . clock_gettime ( PERFORMANCE_CLOCK )
83+ block . call
84+ t = Process . clock_gettime ( PERFORMANCE_CLOCK )
85+ t - st
86+ end
87+
88+ measure_base = proc do |sequence , prepare :, &code |
89+ stats = RunningStats . new
90+ base_repeats . times do
91+ *args = prepare . ( sequence . first )
92+ time = measure . call { code . call ( *args ) }
93+ warn " - %0.9f" % [ time ] if verbose == :very
94+ stats . push time
95+ end
96+ stats
97+ end
98+
99+ scale = -> ( base , base_size , size ) { base * size . fdiv ( base_size ) }
100+
101+ warn "Measuring (#{ base_repeats } times) for n=#{ sequence . first } ." if verbose
102+ base_stats = measure_base . ( sequence , prepare :, &code )
103+ base_time = [ base_stats . stddev_above_mean ( 3 ) , base_stats . max ] . min
104+
105+ base_timeout_msg = "min=%s max=%s mean=%s stddev=%s timeout=%s" % [
106+ base_stats . min , base_stats . max , base_stats . mean , base_stats . stddev ,
107+ base_time
108+ ] . map { "%0.6f" % _1 }
109+
110+ warn " n=%d -> %p" % [ sequence . first , base_stats ] if verbose
111+ warn " base timeout=%0.6f" % [ base_time ] if verbose
112+
113+ sequence . each . drop ( 1 ) . to_h { |n |
114+ linear_limit = scale . ( base_time , sequence . first , n )
115+ each_timeout = linear_limit * outlier_safety_factor
116+ mean_timeout = linear_limit * mean_safety_factor
117+ full_timeout = mean_timeout * repeats * 1.1
118+ timeout_msg = "for n=%s linear_limit=%0.6f timeout=%0.6f mean_timeout=%0.6f" % [
119+ n , linear_limit , each_timeout , mean_timeout
120+ ]
121+ warn "Measuring (#{ repeats } times) #{ timeout_msg } :" if verbose
122+ timeout_msg = "#{ timeout_msg } #{ base_timeout_msg } "
123+ *args = prepare . call ( n )
124+ times = Timeout . timeout ( full_timeout , Timeout ::Error , timeout_msg ) do
125+ Array . new ( repeats ) {
126+ time = Timeout . timeout ( each_timeout , Timeout ::Error , timeout_msg ) do
127+ measure . call do code . call ( *args ) end
128+ end
129+ assert_operator time , :<= , each_timeout ,
130+ "super-linear time %0.6f %s" % [ time , timeout_msg ]
131+ warn " ---- %0.9f" % [ time ] if verbose == :very
132+ time
133+ }
134+ end
135+ stats = RunningStats . new ( times )
136+ warn " n=%d -> %p" % [ n , stats ] if verbose
137+ assert_operator stats . mean , :<= , mean_timeout ,
138+ "super-linear mean time %0.6f %s" % [ stats . mean , timeout_msg ]
139+ [ n , stats ]
140+ }
141+ end
142+
143+ class RunningStats
144+ attr_reader :samples , :min , :max , :mean
145+
146+ def initialize ( input = nil )
147+ @samples = 0
148+ @mean = 0.0
149+ @s = 0.0
150+ @min = nil
151+ @max = nil
152+ input &.each do push _1 end
153+ end
154+
155+ def push ( x )
156+ @min = @min ? [ @min , x ] . min : x
157+ @max = @max ? [ @max , x ] . max : x
158+ @samples += 1
159+ delta = ( x - @mean )
160+ @mean += delta / @samples
161+ @s += delta * ( x - @mean )
162+ end
163+
164+ def variance ; ( @samples >= 1 ) ? @s / ( @samples - 1 ) : 0.0 end
165+ def stddev ; Math . sqrt ( variance ) end
166+
167+ def stddev_above_mean ( mult = 1 ) = mean + mult * stddev
168+
169+ def inspect
170+ "#<%s samples=%d min=%0.6f max=%0.6f mean=%0.6f stddev=%0.6f>" % [
171+ self . class , samples , min , max , mean , stddev
172+ ]
173+ end
174+ end
175+
65176 # Copied from minitest
66177 def assert_pattern
67178 flunk "assert_pattern requires a block to capture errors." unless block_given?
0 commit comments