Skip to content

Commit fdd09d0

Browse files
authored
Merge pull request #275 from elenatanasoiu/elena/add-cpu-time
Report CPU Time alongside Wall Time
2 parents b15ceeb + 4a45412 commit fdd09d0

File tree

4 files changed

+97
-12
lines changed

4 files changed

+97
-12
lines changed

README.md

+18-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Wrap a `use` block around the code's original behavior, and wrap `try` around th
2424

2525
* It decides whether or not to run the `try` block,
2626
* Randomizes the order in which `use` and `try` blocks are run,
27-
* Measures the durations of all behaviors in seconds,
27+
* Measures the wall time and cpu time of all behaviors in seconds,
2828
* Compares the result of `try` to the result of `use`,
2929
* Swallow and record exceptions raised in the `try` block when overriding `raised`, and
3030
* Publishes all this information.
@@ -121,15 +121,15 @@ class MyWidget
121121
e.try { UserService.slug_from_login login } # returns String instance or ArgumentError
122122

123123
compare_error_message_and_class = -> (control, candidate) do
124-
control.class == candidate.class &&
124+
control.class == candidate.class &&
125125
control.message == candidate.message
126126
end
127127

128128
compare_argument_errors = -> (control, candidate) do
129129
control.class == ArgumentError &&
130130
candidate.class == ArgumentError &&
131131
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")
133133
end
134134

135135
e.compare_errors do |control, candidate|
@@ -334,11 +334,18 @@ class MyExperiment
334334

335335
def publish(result)
336336

337+
# Wall time
337338
# Store the timing for the control value,
338339
$statsd.timing "science.#{name}.control", result.control.duration
339340
# for the candidate (only the first, see "Breaking the rules" below,
340341
$statsd.timing "science.#{name}.candidate", result.candidates.first.duration
341342

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+
342349
# and counts for match/ignore/mismatch:
343350
if result.matched?
344351
$statsd.increment "science.#{name}.matched"
@@ -543,17 +550,22 @@ end
543550

544551
#### Providing fake timing data
545552

546-
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.
547554

548555
```ruby
549556
science "absolutely-nothing-suspicious-happening-here" do |e|
550557
e.use { ... } # "control"
551558
e.try { ... } # "candidate"
552-
e.fabricate_durations_for_testing_purposes( "control" => 1.0, "candidate" => 0.5 )
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+
})
553563
end
554564
```
555565

556-
`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.
557569

558570
_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._
559571

lib/scientist/observation.rb

+25-3
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,32 @@ class Scientist::Observation
2020
# The Float seconds elapsed.
2121
attr_reader :duration
2222

23+
# The Float CPU time elapsed, in seconds
24+
attr_reader :cpu_time
25+
2326
def initialize(name, experiment, fabricated_duration: nil, &block)
2427
@name = name
2528
@experiment = experiment
2629

27-
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) unless fabricated_duration
30+
start_wall_time, start_cpu_time = capture_times unless fabricated_duration
31+
2832
begin
2933
@value = block.call
3034
rescue *RESCUES => e
3135
@exception = e
3236
end
3337

34-
@duration = fabricated_duration ||
35-
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) - starting
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
3649

3750
freeze
3851
end
@@ -89,4 +102,13 @@ def hash
89102
def raised?
90103
!exception.nil?
91104
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
92114
end

test/scientist/experiment_test.rb

+48-2
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ def @ex.enabled?
671671
end
672672

673673
describe "testing hooks for extending code" do
674-
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
675675
@ex.use { true }
676676
@ex.try { true }
677677
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5, "candidate" => 1.0 )
@@ -684,7 +684,28 @@ def @ex.enabled?
684684
assert_in_delta 1.0, cand.duration, 0.01
685685
end
686686

687-
it "returns actual durations if fabricated ones are omitted for some blocks" do
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
688709
@ex.use { true }
689710
@ex.try { sleep 0.1; true }
690711
@ex.fabricate_durations_for_testing_purposes( "control" => 0.5 )
@@ -696,5 +717,30 @@ def @ex.enabled?
696717
assert_in_delta 0.5, cont.duration, 0.01
697718
assert_in_delta 0.1, cand.duration, 0.01
698719
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
699745
end
700746
end

test/scientist/observation_test.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66

77
it "observes and records the execution of a block" do
88
ob = Scientist::Observation.new("test", @experiment) do
9-
sleep 0.1
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
1014
"ret"
1115
end
1216

1317
assert_equal "ret", ob.value
1418
refute ob.raised?
1519
assert_in_delta 0.1, ob.duration, 0.01
20+
assert_in_delta 0.1, ob.cpu_time, 0.01
1621
end
1722

1823
it "stashes exceptions" do

0 commit comments

Comments
 (0)