scientist 1.6.4 → 1.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -1
- data/README.md +32 -7
- data/doc/changelog.md +44 -0
- data/lib/scientist/experiment.rb +1 -1
- data/lib/scientist/observation.rb +25 -3
- data/lib/scientist/version.rb +1 -1
- data/test/scientist/experiment_test.rb +52 -2
- data/test/scientist/observation_test.rb +6 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 426765524d161804bb79075cb364ff3101c630da521af30c6f7f79e7e65244a8
|
4
|
+
data.tar.gz: a25d075270905c82d63eff97ae773205c3a1b7f3ddb1fc766ee822dd5ee1edfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c48b5826e463c8c1d59c147509f4ced8e371b022da15da5f8d63b038d2d9ef42945951fbf1901924a8570f24de9c95f4c1b33ca430ed47ac2e243383bde4652c
|
7
|
+
data.tar.gz: 19af496de9ceb3f2d2bdb933656d60830941489ab0702c4753247c606886184a5d1c8c7e25ceda26bc2f8ed5cdf6ec3ee833a74c44ec081a814584e0a5e8b677
|
data/.github/workflows/ci.yml
CHANGED
data/README.md
CHANGED
@@ -12,7 +12,7 @@ require "scientist"
|
|
12
12
|
class MyWidget
|
13
13
|
def allows?(user)
|
14
14
|
experiment = Scientist::Default.new "widget-permissions"
|
15
|
-
experiment.use { model.check_user
|
15
|
+
experiment.use { model.check_user(user).valid? } # old way
|
16
16
|
experiment.try { user.can?(:read, model) } # new way
|
17
17
|
|
18
18
|
experiment.run
|
@@ -24,7 +24,7 @@ Wrap a `use` block around the code's original behavior, and wrap `try` around th
|
|
24
24
|
|
25
25
|
* It decides whether or not to run the `try` block,
|
26
26
|
* Randomizes the order in which `use` and `try` blocks are run,
|
27
|
-
* Measures the
|
27
|
+
* Measures the wall time and cpu time of all behaviors in seconds,
|
28
28
|
* Compares the result of `try` to the result of `use`,
|
29
29
|
* Swallow and record exceptions raised in the `try` block when overriding `raised`, and
|
30
30
|
* Publishes all this information.
|
@@ -121,7 +121,7 @@ class MyWidget
|
|
121
121
|
e.try { UserService.slug_from_login login } # returns String instance or ArgumentError
|
122
122
|
|
123
123
|
compare_error_message_and_class = -> (control, candidate) do
|
124
|
-
control.class == candidate.class &&
|
124
|
+
control.class == candidate.class &&
|
125
125
|
control.message == candidate.message
|
126
126
|
end
|
127
127
|
|
@@ -129,7 +129,7 @@ class MyWidget
|
|
129
129
|
control.class == ArgumentError &&
|
130
130
|
candidate.class == ArgumentError &&
|
131
131
|
control.message.start_with?("Input has invalid characters") &&
|
132
|
-
candidate.message.start_with?("Invalid characters in input")
|
132
|
+
candidate.message.start_with?("Invalid characters in input")
|
133
133
|
end
|
134
134
|
|
135
135
|
e.compare_errors do |control, candidate|
|
@@ -241,6 +241,19 @@ end
|
|
241
241
|
|
242
242
|
Note that the `#clean` method will discard the previous cleaner block if you call it again. If for some reason you need to access the currently configured cleaner block, `Scientist::Experiment#cleaner` will return the block without further ado. _(This probably won't come up in normal usage, but comes in handy if you're writing, say, a custom experiment runner that provides default cleaners.)_
|
243
243
|
|
244
|
+
The `#clean` method will not be used for comparison of the results, so in the following example it is not possible to remove the `#compare` method without the experiment failing:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
def user_ids
|
248
|
+
science "user_ids" do
|
249
|
+
e.use { [1,2,3] }
|
250
|
+
e.try { [1,3,2] }
|
251
|
+
e.clean { |value| value.sort }
|
252
|
+
e.compare { |a, b| a.sort == b.sort }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
244
257
|
### Ignoring mismatches
|
245
258
|
|
246
259
|
During the early stages of an experiment, it's possible that some of your code will always generate a mismatch for reasons you know and understand but haven't yet fixed. Instead of these known cases always showing up as mismatches in your metrics or analysis, you can tell an experiment whether or not to ignore a mismatch using the `ignore` method. You may include more than one block if needed:
|
@@ -321,11 +334,18 @@ class MyExperiment
|
|
321
334
|
|
322
335
|
def publish(result)
|
323
336
|
|
337
|
+
# Wall time
|
324
338
|
# Store the timing for the control value,
|
325
339
|
$statsd.timing "science.#{name}.control", result.control.duration
|
326
340
|
# for the candidate (only the first, see "Breaking the rules" below,
|
327
341
|
$statsd.timing "science.#{name}.candidate", result.candidates.first.duration
|
328
342
|
|
343
|
+
# CPU time
|
344
|
+
# Store the timing for the control value,
|
345
|
+
$statsd.timing "science.cpu.#{name}.control", result.control.cpu_time
|
346
|
+
# for the candidate (only the first, see "Breaking the rules" below,
|
347
|
+
$statsd.timing "science.cpu.#{name}.candidate", result.candidates.first.cpu_time
|
348
|
+
|
329
349
|
# and counts for match/ignore/mismatch:
|
330
350
|
if result.matched?
|
331
351
|
$statsd.increment "science.#{name}.matched"
|
@@ -530,17 +550,22 @@ end
|
|
530
550
|
|
531
551
|
#### Providing fake timing data
|
532
552
|
|
533
|
-
If you're writing tests that depend on specific timing values, you can provide canned durations using the `fabricate_durations_for_testing_purposes` method, and Scientist will report these in `Scientist::Observation#duration` instead of the actual execution times.
|
553
|
+
If you're writing tests that depend on specific timing values, you can provide canned durations using the `fabricate_durations_for_testing_purposes` method, and Scientist will report these in `Scientist::Observation#duration` and `Scientist::Observation#cpu_time` instead of the actual execution times.
|
534
554
|
|
535
555
|
```ruby
|
536
556
|
science "absolutely-nothing-suspicious-happening-here" do |e|
|
537
557
|
e.use { ... } # "control"
|
538
558
|
e.try { ... } # "candidate"
|
539
|
-
e.fabricate_durations_for_testing_purposes(
|
559
|
+
e.fabricate_durations_for_testing_purposes({
|
560
|
+
"control" => { "duration" => 1.0, "cpu_time" => 0.9 },
|
561
|
+
"candidate" => { "duration" => 0.5, "cpu_time" => 0.4 }
|
562
|
+
})
|
540
563
|
end
|
541
564
|
```
|
542
565
|
|
543
|
-
`fabricate_durations_for_testing_purposes` takes a Hash of duration values, keyed by behavior names. (By default, Scientist uses `"control"` and `"candidate"`, but if you override these as shown in [Trying more than one thing](#trying-more-than-one-thing) or [No control, just candidates](#no-control-just-candidates), use matching names here.) If a name is not provided, the actual execution time will be reported instead.
|
566
|
+
`fabricate_durations_for_testing_purposes` takes a Hash of duration & cpu_time values, keyed by behavior names. (By default, Scientist uses `"control"` and `"candidate"`, but if you override these as shown in [Trying more than one thing](#trying-more-than-one-thing) or [No control, just candidates](#no-control-just-candidates), use matching names here.) If a name is not provided, the actual execution time will be reported instead.
|
567
|
+
|
568
|
+
We should mention these durations will be used both for the `duration` field and the `cpu_time` field.
|
544
569
|
|
545
570
|
_Like `Scientist::Experiment#cleaner`, this probably won't come up in normal usage. It's here to make it easier to test code that extends Scientist._
|
546
571
|
|
data/doc/changelog.md
CHANGED
@@ -1,5 +1,49 @@
|
|
1
1
|
# Changes
|
2
2
|
|
3
|
+
## v1.6.5 (16 December 2024)
|
4
|
+
|
5
|
+
- New: measure CPU time alongside wall time for experiments #275
|
6
|
+
|
7
|
+
## v1.6.4 (5 April 2023)
|
8
|
+
|
9
|
+
- New: GitHub Actions for CI #171
|
10
|
+
- New: add ruby 3.1 support #175
|
11
|
+
- Fix: `compare_errors` in docs #178
|
12
|
+
- Fix: remove outdated travis configs #179
|
13
|
+
- Fix: typos #191
|
14
|
+
- New: add support for `after_run` blocks #211
|
15
|
+
|
16
|
+
## v1.6.3 (9 December 2021)
|
17
|
+
|
18
|
+
- Fix: improve marshaling implementation #169
|
19
|
+
|
20
|
+
## v1.6.2 (4 November 2021)
|
21
|
+
|
22
|
+
- New: make `MismatchError` marshalable #168
|
23
|
+
|
24
|
+
## v1.6.1 (22 October 2021)
|
25
|
+
|
26
|
+
- Fix: moving supported ruby versions from <=2.3 to >=2.6 #150
|
27
|
+
- Fix: update docs to explain timeout handling #159
|
28
|
+
- New: add support for comparing errors #77
|
29
|
+
|
30
|
+
## v1.6.0 (8 March 2021)
|
31
|
+
|
32
|
+
- Fix: clarify unit for observations #124
|
33
|
+
- New: enable support for truffleruby #143
|
34
|
+
- Fix: don't default experiment when included in a module #144
|
35
|
+
|
36
|
+
## v1.5.0 (8 September 2020)
|
37
|
+
|
38
|
+
- Fix: clearer explanation of exception handling #110
|
39
|
+
- Fix: remove unused attribute from `Scientist::Observation` #119
|
40
|
+
- New: Added internal extension point for generating experinet results #121
|
41
|
+
- New: Add `Scientist::Experiment.register` helper #104
|
42
|
+
|
43
|
+
## v1.4.0 (20 September 2019)
|
44
|
+
|
45
|
+
- New: Make `MismatchError` a base `Exception` #107
|
46
|
+
|
3
47
|
## v1.3.0 (2 April 2019)
|
4
48
|
|
5
49
|
- New: Drop support for ruby <2.3
|
data/lib/scientist/experiment.rb
CHANGED
@@ -20,19 +20,32 @@ class Scientist::Observation
|
|
20
20
|
# The Float seconds elapsed.
|
21
21
|
attr_reader :duration
|
22
22
|
|
23
|
+
# The Float CPU time elapsed, in seconds
|
24
|
+
attr_reader :cpu_time
|
25
|
+
|
23
26
|
def initialize(name, experiment, fabricated_duration: nil, &block)
|
24
27
|
@name = name
|
25
28
|
@experiment = experiment
|
26
29
|
|
27
|
-
|
30
|
+
start_wall_time, start_cpu_time = capture_times unless fabricated_duration
|
31
|
+
|
28
32
|
begin
|
29
33
|
@value = block.call
|
30
34
|
rescue *RESCUES => e
|
31
35
|
@exception = e
|
32
36
|
end
|
33
37
|
|
34
|
-
|
35
|
-
|
38
|
+
if fabricated_duration.is_a?(Hash)
|
39
|
+
@duration = fabricated_duration["duration"]
|
40
|
+
@cpu_time = fabricated_duration["cpu_time"]
|
41
|
+
elsif fabricated_duration
|
42
|
+
@duration = fabricated_duration
|
43
|
+
@cpu_time = 0.0 # setting a default value
|
44
|
+
else
|
45
|
+
end_wall_time, end_cpu_time = capture_times
|
46
|
+
@duration = end_wall_time - start_wall_time
|
47
|
+
@cpu_time = end_cpu_time - start_cpu_time
|
48
|
+
end
|
36
49
|
|
37
50
|
freeze
|
38
51
|
end
|
@@ -89,4 +102,13 @@ class Scientist::Observation
|
|
89
102
|
def raised?
|
90
103
|
!exception.nil?
|
91
104
|
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def capture_times
|
109
|
+
[
|
110
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second),
|
111
|
+
Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
|
112
|
+
]
|
113
|
+
end
|
92
114
|
end
|
data/lib/scientist/version.rb
CHANGED
@@ -512,6 +512,10 @@ describe Scientist::Experiment do
|
|
512
512
|
assert_kind_of(String, Marshal.dump(mismatch))
|
513
513
|
end
|
514
514
|
|
515
|
+
it "can be marshal loaded" do
|
516
|
+
assert_kind_of(Fake, Marshal.load(Marshal.dump(@ex)))
|
517
|
+
end
|
518
|
+
|
515
519
|
describe "#raise_on_mismatches?" do
|
516
520
|
it "raises when there is a mismatch if the experiment instance's raise on mismatches is enabled" do
|
517
521
|
Fake.raise_on_mismatches = false
|
@@ -667,7 +671,7 @@ candidate:
|
|
667
671
|
end
|
668
672
|
|
669
673
|
describe "testing hooks for extending code" do
|
670
|
-
it "allows a user to provide fabricated durations for testing purposes" do
|
674
|
+
it "allows a user to provide fabricated durations for testing purposes (old version)" do
|
671
675
|
@ex.use { true }
|
672
676
|
@ex.try { true }
|
673
677
|
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5, "candidate" => 1.0 )
|
@@ -680,7 +684,28 @@ candidate:
|
|
680
684
|
assert_in_delta 1.0, cand.duration, 0.01
|
681
685
|
end
|
682
686
|
|
683
|
-
it "
|
687
|
+
it "allows a user to provide fabricated durations for testing purposes (new version)" do
|
688
|
+
@ex.use { true }
|
689
|
+
@ex.try { true }
|
690
|
+
@ex.fabricate_durations_for_testing_purposes({
|
691
|
+
"control" => { "duration" => 0.5, "cpu_time" => 0.4 },
|
692
|
+
"candidate" => { "duration" => 1.0, "cpu_time" => 0.9 }
|
693
|
+
})
|
694
|
+
@ex.run
|
695
|
+
|
696
|
+
cont = @ex.published_result.control
|
697
|
+
cand = @ex.published_result.candidates.first
|
698
|
+
|
699
|
+
# Wall Time
|
700
|
+
assert_in_delta 0.5, cont.duration, 0.01
|
701
|
+
assert_in_delta 1.0, cand.duration, 0.01
|
702
|
+
|
703
|
+
# CPU Time
|
704
|
+
assert_equal 0.4, cont.cpu_time
|
705
|
+
assert_equal 0.9, cand.cpu_time
|
706
|
+
end
|
707
|
+
|
708
|
+
it "returns actual durations if fabricated ones are omitted for some blocks (old version)" do
|
684
709
|
@ex.use { true }
|
685
710
|
@ex.try { sleep 0.1; true }
|
686
711
|
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5 )
|
@@ -692,5 +717,30 @@ candidate:
|
|
692
717
|
assert_in_delta 0.5, cont.duration, 0.01
|
693
718
|
assert_in_delta 0.1, cand.duration, 0.01
|
694
719
|
end
|
720
|
+
|
721
|
+
it "returns actual durations if fabricated ones are omitted for some blocks (new version)" do
|
722
|
+
@ex.use { true }
|
723
|
+
@ex.try do
|
724
|
+
start_time = Time.now
|
725
|
+
while Time.now - start_time < 0.1
|
726
|
+
# Perform some CPU-intensive work
|
727
|
+
(1..1000).each { |i| i * i }
|
728
|
+
end
|
729
|
+
true
|
730
|
+
end
|
731
|
+
@ex.fabricate_durations_for_testing_purposes({ "control" => { "duration" => 0.5, "cpu_time" => 0.4 }})
|
732
|
+
@ex.run
|
733
|
+
|
734
|
+
cont = @ex.published_result.control
|
735
|
+
cand = @ex.published_result.candidates.first
|
736
|
+
|
737
|
+
# Fabricated durations
|
738
|
+
assert_in_delta 0.5, cont.duration, 0.01
|
739
|
+
assert_in_delta 0.4, cont.cpu_time, 0.01
|
740
|
+
|
741
|
+
# Measured durations
|
742
|
+
assert_in_delta 0.1, cand.duration, 0.01
|
743
|
+
assert_in_delta 0.1, cand.cpu_time, 0.01
|
744
|
+
end
|
695
745
|
end
|
696
746
|
end
|
@@ -6,13 +6,18 @@ describe Scientist::Observation do
|
|
6
6
|
|
7
7
|
it "observes and records the execution of a block" do
|
8
8
|
ob = Scientist::Observation.new("test", @experiment) do
|
9
|
-
|
9
|
+
start_time = Time.now
|
10
|
+
while Time.now - start_time < 0.1
|
11
|
+
# Perform some CPU-intensive work
|
12
|
+
(1..1000).each { |i| i * i }
|
13
|
+
end
|
10
14
|
"ret"
|
11
15
|
end
|
12
16
|
|
13
17
|
assert_equal "ret", ob.value
|
14
18
|
refute ob.raised?
|
15
19
|
assert_in_delta 0.1, ob.duration, 0.01
|
20
|
+
assert_in_delta 0.1, ob.cpu_time, 0.01
|
16
21
|
end
|
17
22
|
|
18
23
|
it "stashes exceptions" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scientist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.6.
|
4
|
+
version: 1.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub Open Source
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date:
|
15
|
+
date: 2024-12-16 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: minitest
|
@@ -98,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
98
|
- !ruby/object:Gem::Version
|
99
99
|
version: '0'
|
100
100
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
101
|
+
rubygems_version: 3.5.10
|
102
102
|
signing_key:
|
103
103
|
specification_version: 4
|
104
104
|
summary: Carefully test, measure, and track refactored code.
|