lab_tech 0.1.0 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|