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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f5e6f3f09101904d4cf6fbc209768dba3f58bdb3ca9bbe157473e7d939fe124
4
- data.tar.gz: 88dceb210ec735257a5f2cf329fe132085062b39dd2ad207dae2d0ff16392993
3
+ metadata.gz: 9ae9dc5a34d835d29b76b516dca2dcd6db786c44be5207f38d007104b1dda180
4
+ data.tar.gz: e47a125456c58867e841f88301a998e05c5750c7df0c74e6fb1c92907126949a
5
5
  SHA512:
6
- metadata.gz: b52697112c48077ac53e85af052b0e20e5425f448fba77a8139344afad7bb39c4bf497dcf5987d794d5826e5a6dfd920975a0d509d44fb16779d92342d21ffbd
7
- data.tar.gz: d6178294c5066e7117435237b7ba2a93a1bbf15bba7152ead2c6f4393a808c0af1d271478126e50a5948ee133c855fa28ca2589da140a305cc5698271ce0fd81
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
- This gem was extracted just before its primary author left Real Geeks, so it's
315
- not quite clear who's going to take responsibility for the gem. It's probably
316
- a good idea to open a GitHub issue to start a conversation before undertaking
317
- any great amount of work -- though, of course, you're perfectly welcome to fork
318
- the gem and use your modified version at any time.
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
- # TODO: DRY up the io.puts structure between this and summarize_errors
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.puts
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
- io.puts
83
- io.puts "=" * 100
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
- update_attributes(
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
- # TODO: DRY up the io.puts structure between this and compare_mismatches
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.puts
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, ->() { where("name = 'control'") }, class_name: "LabTech::Observation"
8
- has_many :candidates, ->() { where("name != 'control'") }, class_name: "LabTech::Observation"
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
- is_timeout = ->(is_or_is_not) {
18
- col = LabTech::Observation.table_name + ".exception_class"
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
- record_observation candidate
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
- s.puts TAB + speedup_summary_line(n, speedup_magnitude)
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
- count = @counts[count_name]
175
- return if count.zero?
176
-
101
+ n = @counts[count_name]
177
102
  total = @counts[:results]
178
- label ||= count_name.to_s
179
- s.puts "%s of %s (%s) %s" % [ humanize( count ), humanize( total ), rate( count ), label ]
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