lab_tech 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -5
- data/app/models/lab_tech/experiment.rb +32 -34
- data/app/models/lab_tech/observation.rb +3 -0
- data/app/models/lab_tech/result.rb +11 -15
- data/app/models/lab_tech/summary.rb +6 -80
- data/app/models/lab_tech/summary/count.rb +44 -0
- data/app/models/lab_tech/summary/speedup_line.rb +84 -0
- data/db/migrate/20210205225332_add_observation_diff.rb +6 -0
- data/lib/lab_tech.rb +2 -2
- data/lib/lab_tech/version.rb +1 -1
- data/spec/dummy/config/environments/development.rb +1 -1
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +2 -1
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +1026 -0
- data/spec/dummy/log/test.log +29795 -760
- data/spec/examples.txt +76 -72
- data/spec/models/lab_tech/experiment_spec.rb +65 -2
- data/spec/models/lab_tech/summary_spec.rb +3 -17
- metadata +22 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ae9dc5a34d835d29b76b516dca2dcd6db786c44be5207f38d007104b1dda180
|
4
|
+
data.tar.gz: e47a125456c58867e841f88301a998e05c5750c7df0c74e6fb1c92907126949a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3941c70268e050eff64b8673401c66e7457a931c6f6bc906cac0218756068fdbaf8f3e1d1f50460164032f63843aefee8b9483d655514a8b47b6121936bc3592
|
7
|
+
data.tar.gz: ce7f941d62618a9de15face19a82d9fdd9bbea27a742602be37cf95efe9629e55a588fe0f974bfb15817d7f24bceb45036ebaee070d621fa9a6857f22aaba2b1
|
data/README.md
CHANGED
@@ -17,6 +17,33 @@ for accuracy and performance. (Please feel free to send those back to us in a
|
|
17
17
|
pull request; we simply haven't needed them for ourselves, so they don't
|
18
18
|
exist yet.)
|
19
19
|
|
20
|
+
## Why Scientist?
|
21
|
+
|
22
|
+
Scientist is a great tool for trying out changes where:
|
23
|
+
- comprehensive test coverage is impractical
|
24
|
+
- your test suite doesn't give you sufficient confidence in your changes
|
25
|
+
- you want detailed performance data on your proposed alternative(s)
|
26
|
+
|
27
|
+
## Why LabTech?
|
28
|
+
|
29
|
+
Scientist is amazing at **generating** data, but it assumes you'll develop your
|
30
|
+
own tools for **recording** and **analyzing** it. Scientist's README examples
|
31
|
+
show interactions with StatsD and Redis, but if you're working in a Rails app,
|
32
|
+
odds are *pretty darn good* that:
|
33
|
+
|
34
|
+
1. you already have access to a RDBMS and ActiveRecord, and
|
35
|
+
2. your throughput isn't so huge that some extra database writes will bring
|
36
|
+
said RDBMS to its knees.
|
37
|
+
|
38
|
+
If both of those assumptions are true for your application, LabTech might be a
|
39
|
+
good fit for you -- it records experimental results to the database so they're
|
40
|
+
easy to query later using ActiveRecord.
|
41
|
+
|
42
|
+
(If you're legitimately worried about the I/O load on your RDBMS, you can
|
43
|
+
always ramp up your LabTech experiments a percentage point or two at a time,
|
44
|
+
keeping an eye on your performance monitoring tools and scaling back as
|
45
|
+
needed.)
|
46
|
+
|
20
47
|
## Usage
|
21
48
|
|
22
49
|
Once you've installed the gem and run its migrations (as described in
|
@@ -257,6 +284,8 @@ mismatches by running:
|
|
257
284
|
LabTech.compare_mismatches "spiffy-search", limit: 3
|
258
285
|
```
|
259
286
|
|
287
|
+
(To view all mismatches, just leave off the `limit: 3`.)
|
288
|
+
|
260
289
|
You have the ability to customize the output of this by passing a block that
|
261
290
|
takes a "control" parameter followed by a "candidate" parameter; the return
|
262
291
|
value of that block will be printed to the console. How you do this will
|
@@ -264,6 +293,34 @@ largely depend on the kind of data you're collecting to validate your
|
|
264
293
|
experiments. There are several examples in the `lib/lab_tech.rb` file; I
|
265
294
|
encourage you to check them out.
|
266
295
|
|
296
|
+
If you have errors to inspect as well, you can view these with:
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
LabTech.summarize_errors "spiffy-search"
|
300
|
+
```
|
301
|
+
|
302
|
+
Note that the `summarize_errors` method also takes an optional `:limit` keyword
|
303
|
+
argument.
|
304
|
+
|
305
|
+
### Storing Diffs
|
306
|
+
|
307
|
+
If you're working with complex data, you might not want to recompute the diffs
|
308
|
+
from console. As such, LabTech adds a `.diff` method to the experiment. If
|
309
|
+
you call this with a block, that block will be passed the control and candidate
|
310
|
+
for each candidate, and its result will be stored on the `LabTech::Observation`
|
311
|
+
record. (See the "diff-generating behavior" spec in
|
312
|
+
`spec/models/lab_tech/experiment_spec.rb` for examples.)
|
313
|
+
|
314
|
+
### A Note About Experimental Design
|
315
|
+
|
316
|
+
Scientist supports experiments with more than one candidate at a time, and
|
317
|
+
therefore so does LabTech -- it will record as many candidates as you throw at
|
318
|
+
it. However, if you have multiple candidates, we don't have a good way to
|
319
|
+
generate performance charts to compare all of the alternatives, so LabTech just
|
320
|
+
doesn't bother printing them. **If you try this, you're on your own.** (But
|
321
|
+
do let us know how it goes, and feel free to submit a PR if you find a good
|
322
|
+
solution!)
|
323
|
+
|
267
324
|
## Installation
|
268
325
|
|
269
326
|
**NOTE: As this gem is a Rails engine, we assume you have a Rails application to
|
@@ -311,11 +368,18 @@ Once that's done, you should be good to go! See the "Usage" section, above.
|
|
311
368
|
|
312
369
|
## Contributing
|
313
370
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
371
|
+
Bug reports and pull requests are welcome on GitHub at
|
372
|
+
https://github.com/RealGeeks/lab_tech.
|
373
|
+
|
374
|
+
It's probably a good idea to open a GitHub issue to start a conversation before
|
375
|
+
undertaking any significant amount of work -- though, as always with F/OSS
|
376
|
+
code, you're perfectly welcome to fork the gem and use your modified version at
|
377
|
+
any time.
|
378
|
+
|
379
|
+
This project is intended to be a safe, welcoming space for collaboration.
|
380
|
+
While we have not yet formally adopted a code of conduct, it's probably a Very
|
381
|
+
Good Idea to act in accordance with the <a
|
382
|
+
href="https://www.contributor-covenant.org/">Contributor Covenant</a>.
|
319
383
|
|
320
384
|
## License
|
321
385
|
|
@@ -60,29 +60,19 @@ module LabTech
|
|
60
60
|
@_scientist_comparator
|
61
61
|
end
|
62
62
|
|
63
|
-
|
64
|
-
def compare_mismatches(limit: nil, io: $stdout, &block)
|
63
|
+
def compare_mismatches(limit: nil, width: 100, io: $stdout, &block)
|
65
64
|
mismatches = results.mismatched.includes(:observations)
|
66
65
|
return if mismatches.empty?
|
67
66
|
mismatches = mismatches.limit(limit) if limit
|
68
67
|
|
69
|
-
io
|
70
|
-
io.puts "=" * 100
|
71
|
-
io.puts "Comparing results for #{name}:"
|
72
|
-
io.puts
|
73
|
-
|
74
|
-
mismatches.each do |result|
|
75
|
-
io.puts
|
76
|
-
io.puts "-" * 100
|
68
|
+
display_results mismatches, label: "Comparing results for #{name}:", io: io do |result|
|
77
69
|
io.puts "Result ##{result.id}"
|
78
70
|
result.compare_observations( io: io, &block )
|
79
|
-
io.puts "-" * 100
|
80
71
|
end
|
72
|
+
end
|
81
73
|
|
82
|
-
|
83
|
-
|
84
|
-
io.puts
|
85
|
-
nil
|
74
|
+
def diff(&block)
|
75
|
+
@diff_with = block
|
86
76
|
end
|
87
77
|
|
88
78
|
def disable
|
@@ -106,7 +96,7 @@ module LabTech
|
|
106
96
|
|
107
97
|
def publish(scientist_result)
|
108
98
|
return if Rails.env.test? && !LabTech.publish_results_in_test_mode?
|
109
|
-
LabTech::Result.record_a_science( self, scientist_result )
|
99
|
+
LabTech::Result.record_a_science( self, scientist_result, diff_with: @diff_with )
|
110
100
|
end
|
111
101
|
|
112
102
|
# I don't encourage the willy-nilly destruction of experimental results...
|
@@ -125,7 +115,7 @@ module LabTech
|
|
125
115
|
n = delete_and_count.call( LabTech::Observation.where(result_id: self.result_ids) )
|
126
116
|
m = delete_and_count.call( self.results )
|
127
117
|
|
128
|
-
|
118
|
+
update(
|
129
119
|
equivalent_count: 0,
|
130
120
|
timed_out_count: 0,
|
131
121
|
other_error_count: 0,
|
@@ -140,31 +130,17 @@ module LabTech
|
|
140
130
|
super
|
141
131
|
end
|
142
132
|
|
143
|
-
|
144
|
-
def summarize_errors(limit: nil, io: $stdout)
|
133
|
+
def summarize_errors(limit: nil, width: 100, io: $stdout)
|
145
134
|
errors = results.other_error
|
146
135
|
return if errors.empty?
|
147
136
|
errors = errors.limit(limit) if limit
|
148
137
|
|
149
|
-
io
|
150
|
-
io.puts "=" * 100
|
151
|
-
io.puts "Comparing results for #{name}:"
|
152
|
-
io.puts
|
153
|
-
|
154
|
-
errors.each do |result|
|
155
|
-
io.puts
|
156
|
-
io.puts "-" * 100
|
138
|
+
display_results errors, label: "Summarizing errors for #{name}:", io: io do |result|
|
157
139
|
io.puts "Result ##{result.id}"
|
158
140
|
result.candidates.each do |observation|
|
159
|
-
puts " * " + observation.exception_class + ": " + observation.exception_message
|
141
|
+
io.puts " * " + observation.exception_class + ": " + observation.exception_message
|
160
142
|
end
|
161
|
-
io.puts "-" * 100
|
162
143
|
end
|
163
|
-
|
164
|
-
io.puts
|
165
|
-
io.puts "=" * 100
|
166
|
-
io.puts
|
167
|
-
nil
|
168
144
|
end
|
169
145
|
|
170
146
|
def summarize_results
|
@@ -186,5 +162,27 @@ module LabTech
|
|
186
162
|
return if cleaner.present?
|
187
163
|
clean { |value| LabTech::DefaultCleaner.call(value) }
|
188
164
|
end
|
165
|
+
|
166
|
+
def display_results(results, label: nil, width: 100, io: $stdout)
|
167
|
+
return if results.empty?
|
168
|
+
|
169
|
+
io.puts
|
170
|
+
io.puts "=" * width
|
171
|
+
io.puts label if label
|
172
|
+
io.puts
|
173
|
+
|
174
|
+
results.each do |result|
|
175
|
+
io.puts
|
176
|
+
io.puts "-" * width
|
177
|
+
yield result
|
178
|
+
io.puts "-" * width
|
179
|
+
end
|
180
|
+
|
181
|
+
io.puts
|
182
|
+
io.puts "=" * width
|
183
|
+
io.puts
|
184
|
+
|
185
|
+
return nil
|
186
|
+
end
|
189
187
|
end
|
190
188
|
end
|
@@ -4,6 +4,9 @@ module LabTech
|
|
4
4
|
|
5
5
|
belongs_to :result, class_name: "LabTech::Result", foreign_key: :result_id, optional: true
|
6
6
|
|
7
|
+
scope :timed_out, -> { where(exception_class: 'Timeout::Error') }
|
8
|
+
scope :other_error, -> { where.not(exception_class: 'Timeout::Error') }
|
9
|
+
|
7
10
|
serialize :value
|
8
11
|
|
9
12
|
def raised_error?
|
@@ -4,8 +4,8 @@ module LabTech
|
|
4
4
|
|
5
5
|
belongs_to :experiment, class_name: "LabTech::Experiment"
|
6
6
|
has_many :observations, class_name: "LabTech::Observation", dependent: :destroy
|
7
|
-
has_one :control, ->() {
|
8
|
-
has_many :candidates, ->() { where(
|
7
|
+
has_one :control, ->() { where(name: 'control') }, class_name: "LabTech::Observation"
|
8
|
+
has_many :candidates, ->() { where.not(name: 'control') }, class_name: "LabTech::Observation"
|
9
9
|
serialize :context
|
10
10
|
|
11
11
|
# NOTE: I don't think this accounts for the possibility that both the
|
@@ -14,23 +14,17 @@ module LabTech
|
|
14
14
|
scope :correct, -> { where( equivalent: true, raised_error: false ) }
|
15
15
|
scope :mismatched, -> { where( equivalent: false, raised_error: false ) }
|
16
16
|
scope :errored, -> { where( equivalent: false, raised_error: true ) }
|
17
|
-
|
18
|
-
|
19
|
-
operator = is_or_is_not ? "=" : "!="
|
20
|
-
value = '"Timeout::Error"'
|
21
|
-
[ col, operator, value ].join(" ")
|
22
|
-
}
|
23
|
-
scope :timed_out, -> { errored.joins(:candidates).where( is_timeout.(true) ) }
|
24
|
-
scope :other_error, -> { errored.joins(:candidates).where( is_timeout.(false) ) }
|
17
|
+
scope :timed_out, -> { errored.joins(:candidates).merge(Observation.timed_out) }
|
18
|
+
scope :other_error, -> { errored.joins(:candidates).merge(Observation.other_error) }
|
25
19
|
|
26
20
|
after_create :increment_experiment_counters
|
27
21
|
|
28
22
|
|
29
23
|
##### CLASS METHODS #####
|
30
24
|
|
31
|
-
def self.record_a_science( experiment, scientist_result )
|
25
|
+
def self.record_a_science( experiment, scientist_result, **kwargs )
|
32
26
|
self.create!(experiment: experiment) do |result|
|
33
|
-
result.record_a_science scientist_result
|
27
|
+
result.record_a_science scientist_result, **kwargs
|
34
28
|
end
|
35
29
|
end
|
36
30
|
|
@@ -56,7 +50,7 @@ module LabTech
|
|
56
50
|
return nil
|
57
51
|
end
|
58
52
|
|
59
|
-
def record_a_science(scientist_result)
|
53
|
+
def record_a_science(scientist_result, diff_with: nil)
|
60
54
|
unless scientist_result.kind_of?( Scientist::Result )
|
61
55
|
raise ArgumentError, "expected a Scientist::Result but got #{scientist_result.class}"
|
62
56
|
end
|
@@ -65,7 +59,8 @@ module LabTech
|
|
65
59
|
|
66
60
|
record_observation scientist_result.control
|
67
61
|
scientist_result.candidates.each do |candidate|
|
68
|
-
|
62
|
+
diff = diff_with&.call(scientist_result.control, candidate)
|
63
|
+
record_observation candidate, diff: diff
|
69
64
|
end
|
70
65
|
|
71
66
|
record_simple_stats scientist_result
|
@@ -99,8 +94,9 @@ module LabTech
|
|
99
94
|
end
|
100
95
|
end
|
101
96
|
|
102
|
-
def record_observation(scientist_observation)
|
97
|
+
def record_observation(scientist_observation, attrs = {})
|
103
98
|
self.observations.build do |observation|
|
99
|
+
observation.assign_attributes attrs if attrs.present?
|
104
100
|
observation.record_a_science scientist_observation
|
105
101
|
end
|
106
102
|
end
|
@@ -2,8 +2,6 @@ module LabTech
|
|
2
2
|
class Summary
|
3
3
|
TAB = " " * 4
|
4
4
|
LINE = "-" * 80
|
5
|
-
VAL = "█"
|
6
|
-
DOT = "·"
|
7
5
|
|
8
6
|
def initialize(experiment)
|
9
7
|
@experiment = experiment
|
@@ -46,10 +44,9 @@ module LabTech
|
|
46
44
|
def add_speedup_chart_to(s)
|
47
45
|
s.puts
|
48
46
|
s.puts "Speedups (by percentiles):"
|
49
|
-
speedup_magnitude = @speedup_factors.minmax.map(&:to_i).map(&:abs).max.ceil
|
50
|
-
speedup_magnitude = 25 if speedup_magnitude.zero?
|
51
47
|
(0..100).step(5) do |n|
|
52
|
-
|
48
|
+
line = SpeedupLine.new(n, @speedup_factors)
|
49
|
+
s.puts TAB + line.to_s
|
53
50
|
end
|
54
51
|
end
|
55
52
|
|
@@ -100,83 +97,12 @@ module LabTech
|
|
100
97
|
end
|
101
98
|
end
|
102
99
|
|
103
|
-
def highlight_bar(bar)
|
104
|
-
left, right = bar.split(VAL)
|
105
|
-
|
106
|
-
left = left .gsub(" ", " #{DOT}")
|
107
|
-
right = right.reverse.gsub(" ", " #{DOT}").reverse
|
108
|
-
|
109
|
-
left + VAL + right
|
110
|
-
end
|
111
|
-
|
112
|
-
def humanize(n)
|
113
|
-
width = number_helper.number_with_delimiter( @counts[:results] ).length
|
114
|
-
"%#{width}s" % number_helper.number_with_delimiter( n )
|
115
|
-
end
|
116
|
-
|
117
|
-
def pad_left(s, width)
|
118
|
-
n = [ ( width - s.length ), 0 ].max
|
119
|
-
[ " " * n , s ].join
|
120
|
-
end
|
121
|
-
|
122
|
-
def normalized_bar(x, magnitude, bar_scale: 25, highlight: false)
|
123
|
-
neg, pos = " " * bar_scale, " " * bar_scale
|
124
|
-
normalized = ( bar_scale * ( x.abs / magnitude ) ).floor
|
125
|
-
|
126
|
-
# Select an index that's as close to `normalized` as possible without generating IndexErrors
|
127
|
-
# (TODO: actually understand the math involved so I don't have to chop the ends off like an infidel)
|
128
|
-
index = [ 0, normalized ].max
|
129
|
-
index = [ index, bar_scale - 1 ].min
|
130
|
-
|
131
|
-
case
|
132
|
-
when x == 0 ; mid = VAL
|
133
|
-
when x < 0 ; mid = DOT ; neg[ index ] = VAL ; neg = neg.reverse
|
134
|
-
when x > 0 ; mid = DOT ; pos[ index ] = VAL
|
135
|
-
end
|
136
|
-
|
137
|
-
bar = "[%s%s%s]" % [ neg, mid, pos ]
|
138
|
-
bar = highlight_bar(bar) if highlight
|
139
|
-
bar
|
140
|
-
end
|
141
|
-
|
142
|
-
def number_helper
|
143
|
-
@_number_helper ||= Object.new.tap {|o| o.send :extend, ActionView::Helpers::NumberHelper }
|
144
|
-
end
|
145
|
-
|
146
|
-
def rate(n)
|
147
|
-
"%2.2f%%" % ( 100.0 * n / @counts[:results] )
|
148
|
-
end
|
149
|
-
|
150
|
-
def speedup_summary_line(n, speedup_magnitude)
|
151
|
-
highlight = n == 50
|
152
|
-
label = "%3d%%" % n
|
153
|
-
|
154
|
-
speedup_factor = LabTech::Percentile.call(n, @speedup_factors)
|
155
|
-
rel_speedup = "%+.1fx" % speedup_factor
|
156
|
-
bar = normalized_bar( speedup_factor, speedup_magnitude, highlight: highlight)
|
157
|
-
|
158
|
-
speedup_cue = pad_left( rel_speedup, speedup_width )
|
159
|
-
speedup_cue += " faster" if speedup_factor > 0
|
160
|
-
|
161
|
-
"#{label} #{bar} #{speedup_cue}"
|
162
|
-
end
|
163
|
-
|
164
|
-
def speedup_width
|
165
|
-
@_speedup_width ||= [
|
166
|
-
1, # sign
|
167
|
-
4, # digits
|
168
|
-
1, # decimal point
|
169
|
-
1, # digit after decimal point
|
170
|
-
].sum
|
171
|
-
end
|
172
|
-
|
173
100
|
def summarize_count(s, count_name, label = nil)
|
174
|
-
|
175
|
-
return if count.zero?
|
176
|
-
|
101
|
+
n = @counts[count_name]
|
177
102
|
total = @counts[:results]
|
178
|
-
|
179
|
-
|
103
|
+
count = Count.new(count_name, n, total, label)
|
104
|
+
return if count.zero?
|
105
|
+
s.puts count.to_s
|
180
106
|
end
|
181
107
|
|
182
108
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module LabTech
|
2
|
+
class Summary
|
3
|
+
|
4
|
+
class Count
|
5
|
+
attr_reader :name, :n, :total, :label
|
6
|
+
|
7
|
+
def initialize(name, n, total, label = nil)
|
8
|
+
@name = name
|
9
|
+
@n = n
|
10
|
+
@total = total
|
11
|
+
@label = label || name.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def zero?
|
15
|
+
n.zero?
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"%s of %s (%s) %s" % [
|
20
|
+
humanize( n ),
|
21
|
+
humanize( total ),
|
22
|
+
rate( n ),
|
23
|
+
label
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def humanize(n)
|
30
|
+
width = number_helper.number_with_delimiter( n ).length
|
31
|
+
"%#{width}s" % number_helper.number_with_delimiter( n )
|
32
|
+
end
|
33
|
+
|
34
|
+
def number_helper
|
35
|
+
@_number_helper ||= Object.new.tap {|o| o.send :extend, ActionView::Helpers::NumberHelper }
|
36
|
+
end
|
37
|
+
|
38
|
+
def rate(n)
|
39
|
+
"%2.2f%%" % ( 100.0 * n / total )
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|