lab_tech 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +323 -0
  4. data/Rakefile +30 -0
  5. data/app/models/lab_tech/application_record.rb +5 -0
  6. data/app/models/lab_tech/default_cleaner.rb +87 -0
  7. data/app/models/lab_tech/experiment.rb +190 -0
  8. data/app/models/lab_tech/observation.rb +40 -0
  9. data/app/models/lab_tech/percentile.rb +41 -0
  10. data/app/models/lab_tech/result.rb +130 -0
  11. data/app/models/lab_tech/speedup.rb +65 -0
  12. data/app/models/lab_tech/summary.rb +183 -0
  13. data/config/routes.rb +2 -0
  14. data/db/migrate/20190815192130_create_experiment_tables.rb +50 -0
  15. data/lib/lab_tech.rb +176 -0
  16. data/lib/lab_tech/engine.rb +6 -0
  17. data/lib/lab_tech/version.rb +3 -0
  18. data/lib/tasks/lab_tech_tasks.rake +4 -0
  19. data/spec/dummy/Rakefile +6 -0
  20. data/spec/dummy/app/assets/config/manifest.js +1 -0
  21. data/spec/dummy/app/assets/javascripts/application.js +14 -0
  22. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  23. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  24. data/spec/dummy/app/jobs/application_job.rb +2 -0
  25. data/spec/dummy/app/models/application_record.rb +3 -0
  26. data/spec/dummy/bin/bundle +3 -0
  27. data/spec/dummy/bin/rails +4 -0
  28. data/spec/dummy/bin/rake +4 -0
  29. data/spec/dummy/bin/setup +33 -0
  30. data/spec/dummy/bin/update +28 -0
  31. data/spec/dummy/config.ru +5 -0
  32. data/spec/dummy/config/application.rb +35 -0
  33. data/spec/dummy/config/boot.rb +5 -0
  34. data/spec/dummy/config/database.yml +25 -0
  35. data/spec/dummy/config/environment.rb +5 -0
  36. data/spec/dummy/config/environments/development.rb +46 -0
  37. data/spec/dummy/config/environments/production.rb +71 -0
  38. data/spec/dummy/config/environments/test.rb +36 -0
  39. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  40. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec/dummy/config/initializers/cors.rb +16 -0
  42. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  43. data/spec/dummy/config/initializers/inflections.rb +16 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  45. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/spec/dummy/config/locales/en.yml +33 -0
  47. data/spec/dummy/config/puma.rb +34 -0
  48. data/spec/dummy/config/routes.rb +3 -0
  49. data/spec/dummy/config/spring.rb +6 -0
  50. data/spec/dummy/db/schema.rb +52 -0
  51. data/spec/dummy/db/test.sqlite3 +0 -0
  52. data/spec/dummy/log/development.log +0 -0
  53. data/spec/dummy/log/test.log +1519 -0
  54. data/spec/examples.txt +79 -0
  55. data/spec/models/lab_tech/default_cleaner_spec.rb +32 -0
  56. data/spec/models/lab_tech/experiment_spec.rb +110 -0
  57. data/spec/models/lab_tech/percentile_spec.rb +85 -0
  58. data/spec/models/lab_tech/result_spec.rb +198 -0
  59. data/spec/models/lab_tech/speedup_spec.rb +133 -0
  60. data/spec/models/lab_tech/summary_spec.rb +325 -0
  61. data/spec/models/lab_tech_spec.rb +23 -0
  62. data/spec/rails_helper.rb +62 -0
  63. data/spec/spec_helper.rb +98 -0
  64. data/spec/support/misc_helpers.rb +7 -0
  65. metadata +238 -0
@@ -0,0 +1,133 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe LabTech::Speedup do
4
+ # Some quick reference calculations.
5
+ #
6
+ # baseline | comparison | time | factor | comment
7
+ # 2.0 | 1.0 | +1.0 | +2.0 |
8
+ # 2.0 | 1.5 | +0.5 | +1.333 |
9
+ # 2.0 | 2.0 | 0.0 | +0.0 | zero by definition
10
+ # 2.0 | 2.5 | -0.5 | -1.25 |
11
+ # 2.0 | 3.0 | -1.0 | -1.5 |
12
+ # 2.0 | 3.5 | -1.5 | -1.75 |
13
+ # 2.0 | 4.0 | -2.0 | -2.0 |
14
+
15
+ specify ".compute_time_delta" do
16
+ aggregate_failures do
17
+ expect( described_class.compute_time_delta( 2.0, 1.0 ) ).to be_within( 0.001 ).of( +1.0 )
18
+ expect( described_class.compute_time_delta( 2.0, 1.5 ) ).to be_within( 0.001 ).of( +0.5 )
19
+ expect( described_class.compute_time_delta( 2.0, 2.0 ) ).to be_within( 0.001 ).of( 0.0 )
20
+ expect( described_class.compute_time_delta( 2.0, 2.5 ) ).to be_within( 0.001 ).of( -0.5 )
21
+ expect( described_class.compute_time_delta( 2.0, 3.0 ) ).to be_within( 0.001 ).of( -1.0 )
22
+ expect( described_class.compute_time_delta( 2.0, 3.5 ) ).to be_within( 0.001 ).of( -1.5 )
23
+ expect( described_class.compute_time_delta( 2.0, 4.0 ) ).to be_within( 0.001 ).of( -2.0 )
24
+ end
25
+ end
26
+
27
+ specify ".compute_factor" do
28
+ aggregate_failures do
29
+ expect( described_class.compute_factor( 2.0, 1.0 ) ).to be_within( 0.001 ).of( +2.0 )
30
+ expect( described_class.compute_factor( 2.0, 1.5 ) ).to be_within( 0.001 ).of( +1.333 )
31
+ expect( described_class.compute_factor( 2.0, 2.0 ) ).to be_within( 0.001 ).of( 0.0 )
32
+ expect( described_class.compute_factor( 2.0, 2.5 ) ).to be_within( 0.001 ).of( -1.25 )
33
+ expect( described_class.compute_factor( 2.0, 3.0 ) ).to be_within( 0.001 ).of( -1.5 )
34
+ expect( described_class.compute_factor( 2.0, 3.5 ) ).to be_within( 0.001 ).of( -1.75 )
35
+ expect( described_class.compute_factor( 2.0, 4.0 ) ).to be_within( 0.001 ).of( -2.0 )
36
+ end
37
+ end
38
+
39
+ def new_speedup(baseline = nil, comparison = nil, time = nil, factor = nil)
40
+ described_class.new( baseline: baseline, comparison: comparison, time: time, factor: factor )
41
+ end
42
+
43
+ it "acts like a simple model when all attributes are provided" do
44
+ x = new_speedup( 2.0, 1.0, -1.0, 2.0 )
45
+
46
+ expect( x.baseline ).to eq( +2.0 )
47
+ expect( x.comparison ).to eq( +1.0 )
48
+ expect( x.time ).to eq( -1.0 )
49
+ expect( x.factor ).to eq( +2.0 )
50
+ end
51
+
52
+ it "cheerfully tolerates missing baseline and comparison" do
53
+ x = new_speedup( nil, nil, -1.0, 2.0 )
54
+
55
+ expect( x.baseline ).to be nil
56
+ expect( x.comparison ).to be nil
57
+ expect( x.time ).to eq( -1.0 )
58
+ expect( x.factor ).to eq( +2.0 )
59
+ end
60
+
61
+ it "computes time and factor if they're missing (and it has enough data to do so)" do
62
+ x = new_speedup( 2.0, 1.0, nil, nil )
63
+
64
+ expect( x.baseline ).to eq( +2.0 )
65
+ expect( x.comparison ).to eq( +1.0 )
66
+ expect( x.time ).to eq( +1.0 )
67
+ expect( x.factor ).to eq( +2.0 )
68
+ end
69
+
70
+ it "doesn't compute time and factor if baseline is missing" do
71
+ x = new_speedup( 2.0, nil, nil, nil )
72
+
73
+ expect( x.baseline ).to eq( +2.0 )
74
+ expect( x.comparison ).to be nil
75
+ expect( x.time ).to be nil
76
+ expect( x.factor ).to be nil
77
+ end
78
+
79
+ it "doesn't compute time and factor if comparison is missing" do
80
+ x = new_speedup( nil, 2.0, nil, nil )
81
+
82
+ expect( x.baseline ).to be nil
83
+ expect( x.comparison ).to eq( +2.0 )
84
+ expect( x.time ).to be nil
85
+ expect( x.factor ).to be nil
86
+ end
87
+
88
+ it "is Comparable" do
89
+ x = new_speedup( 2.0, 1.0 )
90
+ y = new_speedup( 2.0, 2.0 )
91
+ z = new_speedup( 2.0, 3.0 )
92
+
93
+ expect( [ x, z, y ].sort ).to eq( [ z, y, x ] )
94
+ end
95
+
96
+ it "is not valid if time is nil" do
97
+ x = new_speedup( nil, nil, -1.0, 2.0 )
98
+ allow( x ).to receive( :time ).and_return nil
99
+
100
+ expect( x ).to_not be_valid
101
+ end
102
+
103
+ it "is not valid if factor is nil" do
104
+ x = new_speedup( nil, nil, -1.0, 2.0 )
105
+ allow( x ).to receive( :factor ).and_return nil
106
+
107
+ expect( x ).to_not be_valid
108
+ end
109
+
110
+ it "is valid if time and factor are present (and can't be disproved)" do
111
+ x = new_speedup( nil, nil, -1.0, 2.0 )
112
+ expect( x.time ).to be_present # precondition check
113
+ expect( x.factor ).to be_present # precondition check
114
+
115
+ expect( x ).to be_valid
116
+ end
117
+
118
+ it "is not valid if time doesn't agree with timing data" do
119
+ x = new_speedup( 2.0, 1.0, nil, nil )
120
+ allow( x ).to receive( :time ).and_return( 42 )
121
+
122
+ expect( x ).to_not be_valid
123
+ end
124
+
125
+ it "is not valid if factor doesn't agree with timing data" do
126
+ x = new_speedup( 2.0, 1.0, nil, nil )
127
+ allow( x ).to receive( :factor ).and_return( 42 )
128
+
129
+ expect( x ).to_not be_valid
130
+ end
131
+
132
+ end
133
+
@@ -0,0 +1,325 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe LabTech::Summary do
4
+ let!(:experiment) { LabTech::Experiment.create(name: "wibble", percent_enabled: 100) }
5
+ let(:summary_text) { experiment.summary.to_s }
6
+
7
+ def record_experiment(cont: "foo", cand: "foo", speedup_factor: nil, baseline: 1.0, comparison: nil)
8
+ LabTech.publish_results_in_test_mode do
9
+
10
+ LabTech.science "wibble" do |e|
11
+ e.use { cont.respond_to?(:call) ? cont.call : cont }
12
+ e.try { cand.respond_to?(:call) ? cand.call : cand }
13
+ end
14
+
15
+ #######################################
16
+
17
+ ######## ##### ###### #####
18
+ ## ## ## ## ## ## ##
19
+ ## ## ## ## ## ## ##
20
+ ## ## ## ## ## ## ##
21
+ ## ## ## ## ## ## ##
22
+ ## ## ## ## ## ## ##
23
+ ## ##### ###### #####
24
+
25
+ #######################################
26
+ # TODO: use Scientist's fabricate_durations_for_testing_purposes to make
27
+ # the below comment (and code?) unnecessary
28
+ #######################################
29
+ # Don't bother stubbing Scientist's clock; you'll get the wrong results 50%
30
+ # of the time because it runs the `try` and `use` blocks in random order,
31
+ # and then you'll be very very confused.
32
+ if speedup_factor && comparison.nil?
33
+ baseline = baseline.to_f
34
+ comparison = \
35
+ case
36
+ when speedup_factor > 0 ; +1.0 * baseline / speedup_factor
37
+ when speedup_factor == 0 ; +1.0 * baseline
38
+ else ; -1.0 * baseline * speedup_factor
39
+ end
40
+ end
41
+
42
+ if baseline && comparison && speedup_factor.nil?
43
+ speedup_factor = LabTech::Speedup.compute_factor(baseline, comparison)
44
+ end
45
+
46
+ if baseline && comparison && speedup_factor
47
+ result = experiment.results.last
48
+ result.update_attributes({
49
+ control_duration: baseline,
50
+ candidate_duration: comparison,
51
+ speedup_factor: speedup_factor,
52
+ time_delta: baseline - comparison,
53
+ })
54
+
55
+ # Technically, we only needed to update the result... but for consistency, let's update the observations too.
56
+ result.control .update_attributes duration: baseline
57
+ result.candidates.first .update_attributes duration: comparison
58
+ end
59
+
60
+ end # LabTech.publish_results_in_test_mode do
61
+ end
62
+
63
+ def wtf
64
+ puts
65
+ puts "", "Experiment" ; tp experiment
66
+ puts "", "Results" ; tp experiment.results
67
+ puts "", "Observations" ; tp experiment.observations
68
+ puts
69
+ end
70
+
71
+ context "when there are no results" do
72
+ before do
73
+ expect( experiment.results ).to be_empty # precondition check
74
+ end
75
+
76
+ it "says there are no results" do
77
+ expect( summary_text ).to match( /No results for experiment/ )
78
+ end
79
+ end
80
+
81
+ context "when the only result is a mismatch" do
82
+ before do
83
+ record_experiment cont: "foo", cand: "bar"
84
+ end
85
+
86
+ it "reports the correct counts" do
87
+ aggregate_failures do
88
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
89
+ expect( summary_text ).to include( "1 of 1 (100.00%) mismatched" )
90
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
91
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
92
+ end
93
+ end
94
+ end
95
+
96
+ context "when the only result is an error" do
97
+ before do
98
+ record_experiment cont: "foo", cand: ->{ raise "nope" }
99
+ end
100
+
101
+ it "reports the correct counts" do
102
+ aggregate_failures do
103
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
104
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
105
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
106
+ expect( summary_text ).to include( "1 of 1 (100.00%) raised errors" )
107
+ end
108
+ end
109
+ end
110
+
111
+ context "when the only result is a timeout" do
112
+ before do
113
+ record_experiment cont: "foo", cand: ->{ raise Timeout::Error, "too slow" }
114
+ end
115
+
116
+ it "reports the correct counts" do
117
+ aggregate_failures do
118
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) correct" )
119
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
120
+ expect( summary_text ).to include( "1 of 1 (100.00%) timed out" )
121
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
122
+ end
123
+ end
124
+ end
125
+
126
+ context "when there are correct results that somehow lack any timing data" do
127
+ before do
128
+ record_experiment
129
+ experiment.results.update_all time_delta: nil, speedup_factor: nil
130
+ end
131
+
132
+ it "reports the correct counts" do
133
+ aggregate_failures do
134
+ expect( summary_text ).to include( "1 of 1 (100.00%) correct" )
135
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
136
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
137
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
138
+ end
139
+ end
140
+
141
+ it "doesn't try to print the big table thingy" do
142
+ expect( summary_text ).to_not include( "Time deltas/speedups:" )
143
+ end
144
+ end
145
+
146
+ describe "when there are correct results that include timing data" do
147
+ def expect_percentile_line(percentile, *expected_strings)
148
+ line = summary_text.lines.detect { |e| e =~ /\s#{percentile.to_i}%/ }
149
+ aggregate_failures do
150
+ expected_strings.each do |string|
151
+ expect( line ).to include( string )
152
+ end
153
+ end
154
+ end
155
+
156
+ context "with a speedup factor of 0x (yawn)" do
157
+ before do
158
+ record_experiment speedup_factor: 0
159
+
160
+ # Make sure we got the math right there...
161
+ result = experiment.results.first
162
+ aggregate_failures do
163
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
164
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 1.0 )
165
+ end
166
+ end
167
+
168
+ it "reports the correct counts" do
169
+ aggregate_failures do
170
+ expect( summary_text ).to include( "1 of 1 (100.00%) correct" )
171
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) mismatched" )
172
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) timed out" )
173
+ expect( summary_text ).to_not include( "0 of 1 (0.00%) raised errors" )
174
+ end
175
+ end
176
+
177
+ it "prints the stats visualization, including the correct speedup factor" do
178
+ expect_percentile_line( 50, "+0.0x" )
179
+ end
180
+ end
181
+
182
+ context "with a speedup factor of 10x (yay!)" do
183
+ before do
184
+ record_experiment speedup_factor: 10
185
+
186
+ # Make sure we got the math right there...
187
+ result = experiment.results.first
188
+ aggregate_failures do
189
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
190
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 0.1 )
191
+ end
192
+ end
193
+
194
+ it "prints the stats visualization, including the correct speedup factor" do
195
+ expect_percentile_line( 50, "+10.0x" )
196
+ end
197
+ end
198
+
199
+ context "with a speedup factor of -10x (boo!)" do
200
+ before do
201
+ record_experiment speedup_factor: -10
202
+
203
+ # Make sure we got the math right there...
204
+ result = experiment.results.first
205
+ aggregate_failures do
206
+ expect( result.control.duration ).to be_within( 0.001 ).of( 1.0 )
207
+ expect( result.candidates.first.duration ).to be_within( 0.001 ).of( 10.0 )
208
+ end
209
+ end
210
+
211
+ it "prints the stats visualization, including the correct speedup factor" do
212
+ expect_percentile_line( 50, "-10.0x" )
213
+ end
214
+ end
215
+
216
+ context "with multiple results and different speedups" do
217
+ before do
218
+ record_experiment speedup_factor: -10
219
+ record_experiment speedup_factor: -2
220
+ record_experiment speedup_factor: 0
221
+ record_experiment speedup_factor: 2
222
+ record_experiment speedup_factor: 10
223
+ end
224
+
225
+ it "reports the correct counts" do
226
+ aggregate_failures do
227
+ expect( summary_text ).to include( "5 of 5 (100.00%) correct" )
228
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) mismatched" )
229
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) timed out" )
230
+ expect( summary_text ).to_not include( "0 of 5 (0.00%) raised errors" )
231
+ end
232
+ end
233
+
234
+ it "reports median time deltas, as well as 5th & 95th percentiles, on their own line" do
235
+ time_delta_line = summary_text.lines.detect { |e| e =~ /Median time delta/i }
236
+ expect( time_delta_line ).to be_present
237
+
238
+ expect( time_delta_line ).to include( "-9.000s" ) # 5th percentile
239
+ expect( time_delta_line ).to include( "+0.000s" ) # Median
240
+ expect( time_delta_line ).to include( "+0.900s" ) # 95th percentile
241
+ end
242
+
243
+ it "prints the stats visualization, including the correct speedup factor" do
244
+ # This is effectively acting as an integration test for the Array#percentile method we've monkeypatched in
245
+ aggregate_failures do
246
+ expect_percentile_line( 0, "-10.0x" )
247
+ expect_percentile_line( 20, "-10.0x" )
248
+
249
+ expect_percentile_line( 25, "-2.0x" )
250
+ expect_percentile_line( 40, "-2.0x" )
251
+
252
+ expect_percentile_line( 45, "+0.0x" )
253
+ expect_percentile_line( 60, "+0.0x" )
254
+
255
+ expect_percentile_line( 65, "+2.0x" )
256
+ expect_percentile_line( 80, "+2.0x" )
257
+
258
+ expect_percentile_line( 85, "+10.0x" )
259
+ expect_percentile_line(100, "+10.0x" )
260
+ end
261
+ end
262
+ end
263
+
264
+ context "real-world(ish) data that led to a scaling error" do
265
+ before do
266
+ record_experiment baseline: 1.7367, speedup_factor: 10.9099
267
+ record_experiment baseline: 0.0642, speedup_factor: -3.2183
268
+ record_experiment baseline: 0.0702, speedup_factor: -1.0906
269
+ record_experiment baseline: 0.0552, speedup_factor: 1.1123
270
+ record_experiment baseline: 0.0539, speedup_factor: 1.1808
271
+ record_experiment baseline: 0.0554, speedup_factor: -1.1269
272
+ end
273
+
274
+ it "renders properly" do
275
+ aggregate_failures do
276
+ expect_percentile_line( 0, "-3.2x" )
277
+ expect_percentile_line( 15, "-3.2x" )
278
+ expect_percentile_line( 20, "-1.1x" )
279
+ expect_percentile_line( 30, "-1.1x" )
280
+ expect_percentile_line( 35, "-1.1x" )
281
+ expect_percentile_line( 50, "-1.1x" )
282
+ expect_percentile_line( 55, "+1.1x" )
283
+ expect_percentile_line( 65, "+1.1x" )
284
+ expect_percentile_line( 70, "+1.2x" )
285
+ expect_percentile_line( 80, "+1.2x" )
286
+ expect_percentile_line( 85, "+10.9x" )
287
+ expect_percentile_line(100, "+10.9x" )
288
+ end
289
+
290
+ end
291
+ end
292
+
293
+ context "real-world(ish) data that led to a scaling error, part 2" do
294
+ before do
295
+ record_experiment baseline: 0.0030516 , comparison: 0.00306088
296
+ record_experiment baseline: 0.000261548 , comparison: 0.00220928
297
+ record_experiment baseline: 0.000781327 , comparison: 0.00279742
298
+ record_experiment baseline: 0.00201508 , comparison: 0.002386
299
+ record_experiment baseline: 0.000593603 , comparison: 0.00275979
300
+ record_experiment baseline: 0.000259521 , comparison: 0.0021131
301
+ record_experiment baseline: 0.000673067 , comparison: 0.00250636
302
+ record_experiment baseline: 0.00229586 , comparison: 0.00285059
303
+ record_experiment baseline: 0.002911 , comparison: 0.00275513
304
+ record_experiment baseline: 0.00275274 , comparison: 0.00251802
305
+ record_experiment baseline: 0.000236285 , comparison: 0.00198174
306
+ record_experiment baseline: 0.000225291 , comparison: 0.00257419
307
+ record_experiment baseline: 0.000356831 , comparison: 0.00244557
308
+ record_experiment baseline: 0.000287118 , comparison: 0.00248476
309
+ record_experiment baseline: 0.000556486 , comparison: 0.00261352
310
+ record_experiment baseline: 0.00237066 , comparison: 0.00265087
311
+ record_experiment baseline: 0.00183386 , comparison: 0.00211302
312
+ record_experiment baseline: 0.00296087 , comparison: 0.00294441
313
+ record_experiment baseline: 0.00031988 , comparison: 0.00323599
314
+ end
315
+
316
+ it "renders properly" do
317
+ aggregate_failures do
318
+ expect_percentile_line( 0, "-11.4x" )
319
+ expect_percentile_line( 50, "-3.7x" )
320
+ expect_percentile_line(100, "+1.1x" )
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end