Skip to content

Commit d9e1819

Browse files
committed
🐛 Disambiguate time usage
- `created_at` / `timestamp` is for wall clock time (for humans) - `started_at` is for monotonic / elapsed time (for computers)
1 parent 637043a commit d9e1819

File tree

8 files changed

+67
-23
lines changed

8 files changed

+67
-23
lines changed

features/step_definitions/old_coverage_json_steps.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
in_current_directory do
1616
resultset_json = File.read(RESULTSET_JSON_PATH)
1717
resultset_hash = JSON.parse(resultset_json)
18-
resultset_hash[COMMAND_NAME]["timestamp"] = SimpleCov::Timer.monotonic.truncate
18+
resultset_hash[COMMAND_NAME]["timestamp"] = SimpleCov::Timer.wall.truncate
19+
resultset_hash[COMMAND_NAME]["started_at"] = SimpleCov::Timer.monotonic.truncate
1920
File.write(RESULTSET_JSON_PATH, JSON.pretty_generate(resultset_hash))
2021
end
2122
end

lib/simplecov/combine.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Combine
1010
# Combine two coverage based on the given combiner_module.
1111
#
1212
# Combiners should always be called through this interface,
13-
# as it takes care of short-circuiting of one of the coverages is nil.
13+
# as it takes care of short-circuiting if one of the coverages is nil.
1414
#
1515
# @return [Hash]
1616
def combine(combiner_module, coverage_a, coverage_b)

lib/simplecov/result.rb

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ class Result
1515
# Returns all files that are applicable to this result (sans filters!) as instances of SimpleCov::SourceFile. Aliased as :source_files
1616
attr_reader :files
1717
alias source_files files
18-
# Explicitly set the Time this result has been created
18+
# Explicitly set the Time (Wall) this result has been created
1919
attr_writer :created_at
20+
# Explicitly set the Time (Monotonic) this result has been created for elapsed time calculations
21+
attr_writer :started_at
2022
# Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
2123
attr_writer :command_name
2224

@@ -25,11 +27,12 @@ class Result
2527

2628
# Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
2729
# coverage data)
28-
def initialize(original_result, command_name: nil, created_at: nil)
30+
def initialize(original_result, command_name: nil, created_at: nil, started_at: nil)
2931
result = original_result
3032
@original_result = result.freeze
3133
@command_name = command_name
3234
@created_at = created_at
35+
@started_at = started_at
3336
@files = SimpleCov::FileList.new(result.map do |filename, coverage|
3437
SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
3538
end.compact.sort_by(&:filename))
@@ -52,8 +55,18 @@ def format!
5255
end
5356

5457
# Defines when this result has been created. Defaults to current truncated system monotonic uptime
58+
# Wall Clock (Realtime) is for knowing what time it is; it cannot be used for elapsed time calculations.
59+
# Ref: https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
60+
# See: #started_at
5561
def created_at
56-
@created_at ||= SimpleCov::Timer.monotonic.truncate
62+
@created_at ||= SimpleCov::Timer.wall.truncate
63+
end
64+
65+
# Monotonic Clock is for calculating elapsed time accurately.
66+
# Ref: https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
67+
# See: #created_at
68+
def started_at
69+
@started_at ||= SimpleCov::Timer.monotonic.truncate
5770
end
5871

5972
# The command name that launched this result.
@@ -67,15 +80,21 @@ def to_hash
6780
{
6881
command_name => {
6982
"coverage" => coverage,
70-
"timestamp" => created_at.to_i
83+
"timestamp" => created_at.to_i,
84+
"started_at" => started_at.to_i
7185
}
7286
}
7387
end
7488

7589
# Loads a SimpleCov::Result#to_hash dump
7690
def self.from_hash(hash)
7791
hash.map do |command_name, data|
78-
new(data.fetch("coverage"), command_name: command_name, created_at: Time.at(data["timestamp"]))
92+
new(
93+
data.fetch("coverage"),
94+
command_name: command_name,
95+
created_at: Time.at(data["timestamp"]),
96+
started_at: data["started_at"]
97+
)
7998
end
8099
end
81100

lib/simplecov/result_merger.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def within_merge_timeout?(data)
8787
end
8888

8989
def time_since_result_creation(data)
90-
timestamp = data.fetch("timestamp")
91-
timer = SimpleCov::Timer.new(timestamp)
90+
started_at = data.fetch("started_at")
91+
timer = SimpleCov::Timer.new(started_at)
9292
timer.elapsed_seconds
9393
end
9494

@@ -104,7 +104,7 @@ def merge_coverage(*results)
104104
return results.first if results.size == 1
105105

106106
results.reduce do |(memo_command, memo_coverage), (command, coverage)|
107-
# timestamp is dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
107+
# timestamp & started_at are dropped here, which is intentional (we merge it, it gets a new time stamp as of now)
108108
merged_coverage = Combine.combine(Combine::ResultsCombiner, memo_coverage, coverage)
109109
merged_command = memo_command + command
110110

lib/simplecov/timer.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
# Use system uptime to calculate accurate and reliable elapsed time.
3+
# Use system uptime (monotonic time) to calculate accurate and reliable elapsed time.
44
#
55
# Question:
66
# Why not just do:
@@ -10,22 +10,35 @@
1010
# https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
1111
module SimpleCov
1212
class Timer
13-
attr_accessor :start_time
13+
# Monotonic clock: Process::CLOCK_MONOTONIC
14+
# Wall clock: Process::CLOCK_REALTIME
15+
attr_accessor :start_time, :clock
1416

1517
class << self
1618
def monotonic
17-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
19+
read(Process::CLOCK_MONOTONIC)
20+
end
21+
22+
def wall
23+
read(Process::CLOCK_REALTIME)
24+
end
25+
26+
# SimpleCov::Timer.read(Process::CLOCK_MONOTONIC) # => uptime in seconds (guaranteed directionally accurate)
27+
# SimpleCov::Timer.read(Process::CLOCK_REALTIME) # => seconds since EPOCH (may not be directionally accurate)
28+
def read(clock = Process::CLOCK_MONOTONIC)
29+
Process.clock_gettime(clock)
1830
end
1931
end
2032

2133
# Capture Time when instantiated
22-
def initialize(start_time)
23-
@start_time = start_time || self.class.monotonic
34+
def initialize(start_time, clock = Process::CLOCK_MONOTONIC)
35+
@start_time = start_time || self.class.read(clock)
36+
@clock = clock
2437
end
2538

2639
# Get Elapsed Time in Seconds
2740
def elapsed_seconds
28-
(self.class.monotonic - @start_time).truncate
41+
(self.class.read(clock) - @start_time).truncate
2942
end
3043
end
3144
end

spec/result_merger_spec.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@
175175
data = {
176176
"some command name" => {
177177
"coverage" => content,
178-
"timestamp" => SimpleCov::Timer.monotonic.truncate
178+
"timestamp" => SimpleCov::Timer.wall.truncate,
179+
"started_at" => SimpleCov::Timer.monotonic.truncate
179180
}
180181
}
181182
File.open(file_path, "w+") do |f|
@@ -286,7 +287,7 @@ def store_result(result, path:)
286287
end
287288

288289
def outdated(result)
289-
result.created_at = SimpleCov::Timer.monotonic.truncate - 172_800
290+
result.started_at = SimpleCov::Timer.monotonic.truncate - 172_800
290291
result
291292
end
292293

spec/result_spec.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@
9595
expect(dumped_result.created_at.to_i).to eq(subject.created_at.to_i)
9696
end
9797

98+
it "has the same started_at" do
99+
expect(dumped_result.started_at.to_i).to eq(subject.started_at.to_i)
100+
end
101+
98102
it "has the same command_name" do
99103
expect(dumped_result.command_name).to eq(subject.command_name)
100104
end
@@ -212,17 +216,20 @@
212216
source_fixture("sample.rb") => {"lines" => [nil, 1, 1, 1, nil, nil, 0, 0, nil, nil]}
213217
}
214218
end
215-
let(:created_at) { SimpleCov::Timer.monotonic.truncate }
219+
let(:created_at) { SimpleCov::Timer.wall.truncate }
220+
let(:started_at) { SimpleCov::Timer.monotonic.truncate }
216221

217222
it "can consume multiple commands" do
218223
input = {
219224
"rspec" => {
220225
"coverage" => original_result,
221-
"timestamp" => created_at
226+
"timestamp" => created_at,
227+
"started_at" => started_at
222228
},
223229
"cucumber" => {
224230
"coverage" => other_result,
225-
"timestamp" => created_at
231+
"timestamp" => created_at,
232+
"started_at" => started_at
226233
}
227234
}
228235

@@ -232,6 +239,7 @@
232239
sorted = result.sort_by(&:command_name)
233240
expect(sorted.map(&:command_name)).to eq %w[cucumber rspec]
234241
expect(sorted.map(&:created_at).map(&:to_i)).to eq [created_at, created_at]
242+
expect(sorted.map(&:started_at).map(&:to_i)).to eq [started_at, started_at]
235243
expect(sorted.map(&:original_result)).to eq [other_result, original_result]
236244
end
237245
end

spec/simplecov_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@
215215
end
216216

217217
let(:collated) do
218-
JSON.parse(File.read(resultset_path)).transform_values { |v| v.reject { |k| k == "timestamp" } }
218+
JSON.parse(File.read(resultset_path)).transform_values { |v| v.reject { |k| k.start_with?("timestamp", "started_at") } }
219219
end
220220

221221
context "when no files to be merged" do
@@ -299,7 +299,9 @@
299299
def create_mergeable_report(name, resultset, outdated: false)
300300
result = SimpleCov::Result.new(resultset)
301301
result.command_name = name
302-
result.created_at = SimpleCov::Timer.monotonic.truncate - 172_800 if outdated
302+
result.created_at = SimpleCov::Timer.wall.truncate
303+
result.started_at = SimpleCov::Timer.monotonic.truncate
304+
result.started_at -= 172_800 if outdated
303305
SimpleCov::ResultMerger.store_result(result)
304306
FileUtils.mv resultset_path, "#{resultset_path}#{name}.final"
305307
end

0 commit comments

Comments
 (0)