coverband 6.0.1 → 6.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 036f9cdceb426dd675d4e6d78af901a6618dee5062fc5572b47ce90518ffbb1f
4
- data.tar.gz: d1d1d8f6c1cf4123d34ae630cd92767d673e1e04e4ef0f18660327c481b309f0
3
+ metadata.gz: 0e834afa9139c4fc6b0e17a151476a7e9769f082ff435d93a58f3d1a8c44ff1a
4
+ data.tar.gz: 935efdfe633f5ccafc3d43d4d5237558b8032545526f0a7320b11ba64a04c7c1
5
5
  SHA512:
6
- metadata.gz: a10c5a56f2ef1cb15e42f5f52f4636432fa0bb779276e44dd719b06474d51da7529b03334d8d0a383fe25b38af932bc0d32c67e3c527aaa568bd9ab35466eb9e
7
- data.tar.gz: 9be31a11af5c7d490e2f965a8e91f1fc916668efa3ec384afc88f78225a880056d3b3ae5dc4e1193be87e63a0d2cb07f35c72c4268018cf01e357c7d9835d3ae
6
+ metadata.gz: de37c02bb694b58b25dad7b31f8fd9bbe09f0c963d779f5094e1db8db6e971b4df4501722657e00a7772a3e0a1622d9538e75b8e4625996ce4e60e3faadf27a3
7
+ data.tar.gz: 8ce7d73882bbc4e501dedde77f58358165e16143e59794d779e7568ec2b15bbbe0dc5f286f995808283c1c8616b7b03704137ec22a28392cff0df95c0fc97d35
data/README.md CHANGED
@@ -179,11 +179,6 @@ Coverband.configure do |config|
179
179
  # default false. button at the top of the web interface which clears all data
180
180
  config.web_enable_clear = true
181
181
 
182
- # default false. Experimental support for tracking view layer tracking.
183
- # Does not track line-level usage, only indicates if an entire file
184
- # is used or not.
185
- config.track_views = true
186
-
187
182
  # default false. Experimental support for routes usage tracking.
188
183
  config.track_routes = true
189
184
  end
@@ -221,9 +216,9 @@ config.ignore += ['config/application.rb',
221
216
 
222
217
  Coverband allows an optional feature to track all view files that are used by an application.
223
218
 
224
- To opt-in to this feature... enable the feature in your Coverband config.
219
+ This feature is enabled by default. To stop this feature, disable the feature in your Coverband config.
225
220
 
226
- `config.track_views = true`
221
+ `config.track_views = false`
227
222
 
228
223
  ![image](https://raw.github.com/danmayer/coverband/master/docs/coverband_view_tracker.png)
229
224
 
@@ -252,12 +247,25 @@ end
252
247
 
253
248
  ### Avoiding Cache Stampede
254
249
 
255
- If you have many servers and they all hit Redis at the same time you can see spikes in your Redis CPU, and memory. This is due to a concept called [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede). It is better to spread out the reporting across your servers. A simple way to do this is to add a random wiggle on your background reporting. This configuration option allows a wiggle. The right amount of wiggle depends on the number of servers you have and how willing you are to have delays in your coverage reporting. I would recommend at least 1 second per server. Note, the default wiggle is set to 30 seconds.
250
+ If you have many servers and they all hit Redis at the same time you can see spikes in your Redis CPU, and memory. This is due to a concept called [cache stampede](https://en.wikipedia.org/wiki/Cache_stampede).
251
+
252
+ It is better to spread out the reporting across your servers. A simple way to do this is to add a random wiggle on your background reporting. This configuration option allows a wiggle. The right amount of wiggle depends on the number of servers you have and how willing you are to have delays in your coverage reporting. I would recommend at least 1 second per server. Note, the default wiggle is set to 30 seconds.
256
253
 
257
254
  Add a wiggle (in seconds) to the background thread to avoid all your servers reporting at the same time:
258
255
 
259
256
  `config.reporting_wiggle = 30`
260
257
 
258
+ Another way to avoid cache stampede is to omit some reporting on starting servers. Coverband stores the results of eager_loading to Redis at server startup. The eager_loading results are the same for all servers, so there is no need to save all results. By configuring the eager_loading results of some servers to be stored in Redis, we can reduce the load on Redis during deployment.
259
+
260
+ ```ruby
261
+ # To omit reporting on starting servers, need to defer saving eager_loading data
262
+ config.defer_eager_loading_data = true
263
+ # Store eager_loading data on 5% of servers
264
+ config.send_deferred_eager_loading_data = rand(100) < 5
265
+ # Store eager_loading data on servers with the environment variable
266
+ config.send_deferred_eager_loading_data = ENV.fetch('ENABLE_EAGER_LOADING_COVERAGE', false)
267
+ ```
268
+
261
269
  ### Redis Hash Store
262
270
 
263
271
  Coverband on very high volume sites with many server processes reporting can have a race condition which can cause hit counts to be inaccurate. To resolve the race condition and reduce Ruby memory overhead we have introduced a new Redis storage option. This moves the some of the work from the Ruby processes to Redis. It is worth noting because of this, it has larger demands on the Redis server. So adjust your Redis instance accordingly. To help reduce the extra redis load you can also change the background reporting frequency.
@@ -267,6 +275,11 @@ Coverband on very high volume sites with many server processes reporting can hav
267
275
 
268
276
  See more discussion [here](https://github.com/danmayer/coverband/issues/384).
269
277
 
278
+ Please note that with the Redis Hash Store, everytime you load the full report, Coverband will execute `HGETALL` queries in your Redis server twice for every file in the project (once for runtime coverage and once for eager loading coverage). This shouldn't have a big impact in small to medium projects, but can be quite a hassle if your project has a few thousand files.
279
+ To help reduce the extra redis load when getting the coverage report, you can enable `get_coverage_cache` (but note that when doing that, you will always get a previous version of the report, while a cache is re-populated with a newer version).
280
+
281
+ - Use Hash Redis Store with _get coverage cache_: `config.store = Coverband::Adapters::HashRedisStore.new(redis, get_coverage_cache: true)`
282
+
270
283
  ### Clear Coverage
271
284
 
272
285
  Now that Coverband uses MD5 hashes there should be no reason to manually clear coverage unless one is testing, changing versions, or possibly debugging Coverband itself.
data/changes.md CHANGED
@@ -1,3 +1,14 @@
1
+ ### Coverband 6.0.2
2
+
3
+ * thanks makicamel for improved deferred eager loading
4
+ * thanks Drowze for two performance improvements to reporting coverage
5
+
6
+
7
+ ### Coverband 6.0.1
8
+
9
+ * [fix on reload routes](https://github.com/danmayer/coverband/commit/f7a81c9499c01a7c027e5f8bc127815bf29a5cb7)
10
+
11
+
1
12
  ### Coverband 6.0.0
2
13
 
3
14
  __NOTE: I ended up having 5.2.6 in various RCs for a long time, mostly because I had some breaking changes that were related to dropping support for old versions of Ruby and Rails__
@@ -5,6 +5,84 @@ require "securerandom"
5
5
  module Coverband
6
6
  module Adapters
7
7
  class HashRedisStore < Base
8
+ class GetCoverageNullCacheStore
9
+ def self.clear!(*_local_types)
10
+ end
11
+
12
+ def self.fetch(_local_type)
13
+ yield(0)
14
+ end
15
+ end
16
+
17
+ class GetCoverageRedisCacheStore
18
+ LOCK_LIMIT = 60 * 30 # 30 minutes
19
+
20
+ def initialize(redis, key_prefix)
21
+ @redis = redis
22
+ @key_prefix = [key_prefix, "get-coverage"].join(".")
23
+ end
24
+
25
+ def fetch(local_type)
26
+ cached_result = get(local_type)
27
+
28
+ # if no cache available, block the call and populate the cache
29
+ # if cache is available, return it and start re-populating it (with a lock)
30
+ if cached_result.nil?
31
+ value = yield(0)
32
+ result = set(local_type, JSON.generate(value))
33
+ value
34
+ else
35
+ if lock!(local_type)
36
+ Thread.new do
37
+ begin
38
+ result = yield(deferred_time)
39
+ set(local_type, JSON.generate(result))
40
+ ensure
41
+ unlock!(local_type)
42
+ end
43
+ end
44
+ end
45
+ JSON.parse(cached_result)
46
+ end
47
+ end
48
+
49
+ def clear!(local_types = Coverband::TYPES)
50
+ Array(local_types).each do |local_type|
51
+ del(local_type)
52
+ unlock!(local_type)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # sleep in between to avoid holding other redis commands..
59
+ # with a small random offset so runtime and eager types can be processed "at the same time"
60
+ def deferred_time
61
+ rand(3.0..4.0)
62
+ end
63
+
64
+ def del(local_type)
65
+ @redis.del("#{@key_prefix}.cache.#{local_type}")
66
+ end
67
+
68
+ def get(local_type)
69
+ @redis.get("#{@key_prefix}.cache.#{local_type}")
70
+ end
71
+
72
+ def set(local_type, value)
73
+ @redis.set("#{@key_prefix}.cache.#{local_type}", value)
74
+ end
75
+
76
+ # lock for at most 60 minutes
77
+ def lock!(local_type)
78
+ @redis.set("#{@key_prefix}.lock.#{local_type}", "1", nx: true, ex: LOCK_LIMIT)
79
+ end
80
+
81
+ def unlock!(local_type)
82
+ @redis.del("#{@key_prefix}.lock.#{local_type}")
83
+ end
84
+ end
85
+
8
86
  FILE_KEY = "file"
9
87
  FILE_LENGTH_KEY = "file_length"
10
88
  META_DATA_KEYS = [DATA_KEY, FIRST_UPDATED_KEY, LAST_UPDATED_KEY, FILE_HASH].freeze
@@ -17,7 +95,7 @@ module Coverband
17
95
 
18
96
  JSON_PAYLOAD_EXPIRATION = 5 * 60
19
97
 
20
- attr_reader :redis_namespace
98
+ attr_reader :redis_namespace, :get_coverage_cache
21
99
 
22
100
  def initialize(redis, opts = {})
23
101
  super()
@@ -29,6 +107,13 @@ module Coverband
29
107
 
30
108
  @ttl = opts[:ttl]
31
109
  @relative_file_converter = opts[:relative_file_converter] || Utils::RelativeFileConverter
110
+
111
+ @get_coverage_cache = if opts[:get_coverage_cache]
112
+ key_prefix = [REDIS_STORAGE_FORMAT_VERSION, @redis_namespace].compact.join(".")
113
+ GetCoverageRedisCacheStore.new(redis, key_prefix)
114
+ else
115
+ GetCoverageNullCacheStore
116
+ end
32
117
  end
33
118
 
34
119
  def supported?
@@ -45,6 +130,7 @@ module Coverband
45
130
  file_keys = files_set
46
131
  @redis.del(*file_keys) if file_keys.any?
47
132
  @redis.del(files_key)
133
+ @get_coverage_cache.clear!(type)
48
134
  end
49
135
  self.type = old_type
50
136
  end
@@ -54,6 +140,7 @@ module Coverband
54
140
  relative_path_file = @relative_file_converter.convert(file)
55
141
  Coverband::TYPES.each do |type|
56
142
  @redis.del(key(relative_path_file, type, file_hash: file_hash))
143
+ @get_coverage_cache.clear!(type)
57
144
  end
58
145
  @redis.srem(files_key, relative_path_file)
59
146
  end
@@ -87,12 +174,21 @@ module Coverband
87
174
  end
88
175
 
89
176
  def coverage(local_type = nil)
90
- files_set = files_set(local_type)
91
- @redis.pipelined { |pipeline|
92
- files_set.each do |key|
93
- pipeline.hgetall(key)
177
+ cached_results = @get_coverage_cache.fetch(local_type || type) do |sleep_time|
178
+ files_set = files_set(local_type)
179
+
180
+ # use batches with a sleep in between to avoid overloading redis
181
+ files_set.each_slice(250).flat_map do |key_batch|
182
+ sleep sleep_time
183
+ @redis.pipelined do |pipeline|
184
+ key_batch.each do |key|
185
+ pipeline.hgetall(key)
186
+ end
187
+ end
94
188
  end
95
- }.each_with_object({}) do |data_from_redis, hash|
189
+ end
190
+
191
+ cached_results.each_with_object({}) do |data_from_redis, hash|
96
192
  add_coverage_for_file(data_from_redis, hash)
97
193
  end
98
194
  end
@@ -64,7 +64,7 @@ module Coverband
64
64
  else
65
65
  if @deferred_eager_loading_data && Coverband.configuration.defer_eager_loading_data?
66
66
  toggle_eager_loading do
67
- @store.save_report(@deferred_eager_loading_data)
67
+ @store.save_report(@deferred_eager_loading_data) if Coverband.configuration.send_deferred_eager_loading_data?
68
68
  @deferred_eager_loading_data = nil
69
69
  end
70
70
  end
@@ -16,7 +16,8 @@ module Coverband
16
16
  attr_writer :logger, :s3_region, :s3_bucket, :s3_access_key_id,
17
17
  :s3_secret_access_key, :password, :api_key, :service_url, :coverband_timeout, :service_dev_mode,
18
18
  :service_test_mode, :process_type, :track_views, :redis_url,
19
- :background_reporting_sleep_seconds, :reporting_wiggle
19
+ :background_reporting_sleep_seconds, :reporting_wiggle,
20
+ :send_deferred_eager_loading_data
20
21
 
21
22
  attr_reader :track_gems, :ignore, :use_oneshot_lines_coverage
22
23
 
@@ -67,6 +68,7 @@ module Coverband
67
68
  @background_reporting_enabled = true
68
69
  @background_reporting_sleep_seconds = nil
69
70
  @defer_eager_loading_data = false
71
+ @send_deferred_eager_loading_data = true
70
72
  @test_env = nil
71
73
  @web_enable_clear = false
72
74
  @track_views = true
@@ -287,6 +289,10 @@ module Coverband
287
289
  @defer_eager_loading_data
288
290
  end
289
291
 
292
+ def send_deferred_eager_loading_data?
293
+ @send_deferred_eager_loading_data
294
+ end
295
+
290
296
  def service_disabled_dev_test_env?
291
297
  return false unless service?
292
298
 
@@ -17,7 +17,11 @@ module Coverband
17
17
  def file_with_type(source_file, results_type)
18
18
  return unless get_results(results_type)
19
19
 
20
- get_results(results_type).source_files.find { |file| file.filename == source_file.filename }
20
+ @files_with_type ||= {}
21
+ @files_with_type[results_type] ||= get_results(results_type).source_files.map do |source_file|
22
+ [source_file.filename, source_file]
23
+ end.to_h
24
+ @files_with_type[results_type][source_file.filename]
21
25
  end
22
26
 
23
27
  def runtime_relevant_coverage(source_file)
@@ -48,7 +52,11 @@ module Coverband
48
52
  def file_from_path_with_type(full_path, results_type = :merged)
49
53
  return unless get_results(results_type)
50
54
 
51
- get_results(results_type).source_files.find { |file| file.filename == full_path }
55
+ @files_from_path_with_type ||= {}
56
+ @files_from_path_with_type[results_type] ||= get_results(results_type).source_files.map do |source_file|
57
+ [source_file.filename, source_file]
58
+ end.to_h
59
+ @files_from_path_with_type[results_type][full_path]
52
60
  end
53
61
 
54
62
  def method_missing(method, *args)
@@ -70,11 +78,11 @@ module Coverband
70
78
  private
71
79
 
72
80
  def get_eager_file(source_file)
73
- eager_loading_coverage.source_files.find { |file| file.filename == source_file.filename }
81
+ file_with_type(source_file, Coverband::EAGER_TYPE)
74
82
  end
75
83
 
76
84
  def get_runtime_file(source_file)
77
- runtime_coverage.source_files.find { |file| file.filename == source_file.filename }
85
+ file_with_type(source_file, Coverband::RUNTIME_TYPE)
78
86
  end
79
87
 
80
88
  def eager_loading_coverage
@@ -93,11 +101,7 @@ module Coverband
93
101
  def get_results(type)
94
102
  return nil unless Coverband::ALL_TYPES.include?(type)
95
103
 
96
- if @results.key?(type)
97
- @results[type]
98
- else
99
- @results[type] = Coverband::Utils::Result.new(report[type])
100
- end
104
+ @results[type] ||= Coverband::Utils::Result.new(report[type])
101
105
  end
102
106
  end
103
107
  end
@@ -5,5 +5,5 @@
5
5
  # use format "4.2.1.rc.1" ~> 4.2.1.rc to prerelease versions like v4.2.1.rc.2 and v4.2.1.rc.3
6
6
  ###
7
7
  module Coverband
8
- VERSION = "6.0.1"
8
+ VERSION = "6.0.2"
9
9
  end
@@ -192,4 +192,52 @@ class HashRedisStoreTest < Minitest::Test
192
192
  @store.clear_file!("app_path/dog.rb")
193
193
  assert_nil @store.get_coverage_report[:merged]["./dog.rb"]
194
194
  end
195
+
196
+ def test_get_coverage_cache
197
+ @store = Coverband::Adapters::HashRedisStore.new(
198
+ @redis,
199
+ redis_namespace: "coverband_test",
200
+ relative_file_converter: MockRelativeFileConverter,
201
+ get_coverage_cache: true
202
+ )
203
+ @store.get_coverage_cache.stubs(:deferred_time).returns(0)
204
+ @store.get_coverage_cache.clear!
205
+ mock_file_hash
206
+ yesterday = DateTime.now.prev_day.to_time
207
+ mock_time(yesterday)
208
+ @store.save_report(
209
+ "app_path/dog.rb" => [0, 1, 2]
210
+ )
211
+ assert_equal(
212
+ {
213
+ "first_updated_at" => yesterday.to_i,
214
+ "last_updated_at" => yesterday.to_i,
215
+ "file_hash" => "abcd",
216
+ "data" => [0, 1, 2]
217
+ },
218
+ @store.coverage["./dog.rb"]
219
+ )
220
+ @store.save_report(
221
+ "app_path/dog.rb" => [0, 1, 2]
222
+ )
223
+ assert_equal(
224
+ {
225
+ "first_updated_at" => yesterday.to_i,
226
+ "last_updated_at" => yesterday.to_i,
227
+ "file_hash" => "abcd",
228
+ "data" => [0, 1, 2]
229
+ },
230
+ @store.coverage["./dog.rb"]
231
+ )
232
+ sleep 0.1 # wait caching thread finish
233
+ assert_equal(
234
+ {
235
+ "first_updated_at" => yesterday.to_i,
236
+ "last_updated_at" => yesterday.to_i,
237
+ "file_hash" => "abcd",
238
+ "data" => [0, 2, 4]
239
+ },
240
+ @store.coverage["./dog.rb"]
241
+ )
242
+ end
195
243
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path("../test_helper", File.dirname(__FILE__))
4
+ require "rack"
5
+
6
+ class FullStackSendDeferredEagerTest < Minitest::Test
7
+ REDIS_STORAGE_FORMAT_VERSION = Coverband::Adapters::RedisStore::REDIS_STORAGE_FORMAT_VERSION
8
+ TEST_RACK_APP = "../fake_app/basic_rack.rb"
9
+
10
+ def setup
11
+ super
12
+ Coverband::Collectors::Coverage.instance.reset_instance
13
+ Coverband.configure do |config|
14
+ config.background_reporting_enabled = false
15
+ config.track_gems = true
16
+ config.defer_eager_loading_data = true
17
+ config.send_deferred_eager_loading_data = false
18
+ end
19
+ Coverband.start
20
+ Coverband::Collectors::Coverage.instance.eager_loading!
21
+ @rack_file = require_unique_file "fake_app/basic_rack.rb"
22
+ Coverband.report_coverage
23
+ Coverband::Collectors::Coverage.instance.runtime!
24
+ end
25
+
26
+ test "call app" do
27
+ # eager loaded class coverage starts empty
28
+ Coverband.eager_loading_coverage!
29
+ expected = {}
30
+ assert_equal expected, Coverband.configuration.store.coverage
31
+
32
+ Coverband::Collectors::Coverage.instance.runtime!
33
+ request = Rack::MockRequest.env_for("/anything.json")
34
+ middleware = Coverband::BackgroundMiddleware.new(fake_app_with_lines)
35
+ results = middleware.call(request)
36
+ assert_equal "Hello Rack!", results.last
37
+ Coverband.report_coverage
38
+ expected = [nil, nil, 0, nil, 0, 0, 1, nil, nil]
39
+ assert_equal expected, Coverband.configuration.store.coverage[@rack_file]["data"]
40
+
41
+ # eager loaded class coverage is skipped at first normal coverage report
42
+ Coverband.eager_loading_coverage!
43
+ expected = {}
44
+ assert_equal expected, Coverband.configuration.store.coverage
45
+ end
46
+
47
+ private
48
+
49
+ def fake_app_with_lines
50
+ @fake_app_with_lines ||= ::HelloWorld.new
51
+ end
52
+ end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coverband
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.1
4
+ version: 6.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Mayer
8
8
  - Karl Baum
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-11-14 00:00:00.000000000 Z
12
+ date: 2024-01-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: benchmark-ips
@@ -418,6 +418,7 @@ files:
418
418
  - test/forked/rails_route_tracker_stack_test.rb
419
419
  - test/forked/rails_view_tracker_stack_test.rb
420
420
  - test/integration/full_stack_deferred_eager_test.rb
421
+ - test/integration/full_stack_send_deferred_eager_test.rb
421
422
  - test/integration/full_stack_test.rb
422
423
  - test/jruby_check.rb
423
424
  - test/rails4_dummy/Rakefile
@@ -498,7 +499,7 @@ metadata:
498
499
  documentation_uri: https://github.com/danmayer/coverband
499
500
  changelog_uri: https://github.com/danmayer/coverband/blob/main/changes.md
500
501
  source_code_uri: https://github.com/danmayer/coverband
501
- post_install_message:
502
+ post_install_message:
502
503
  rdoc_options: []
503
504
  require_paths:
504
505
  - lib
@@ -513,8 +514,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
513
514
  - !ruby/object:Gem::Version
514
515
  version: '0'
515
516
  requirements: []
516
- rubygems_version: 3.2.32
517
- signing_key:
517
+ rubygems_version: 3.4.10
518
+ signing_key:
518
519
  specification_version: 4
519
520
  summary: Rack middleware to measure production code usage (LOC runtime usage)
520
521
  test_files:
@@ -579,6 +580,7 @@ test_files:
579
580
  - test/forked/rails_route_tracker_stack_test.rb
580
581
  - test/forked/rails_view_tracker_stack_test.rb
581
582
  - test/integration/full_stack_deferred_eager_test.rb
583
+ - test/integration/full_stack_send_deferred_eager_test.rb
582
584
  - test/integration/full_stack_test.rb
583
585
  - test/jruby_check.rb
584
586
  - test/rails4_dummy/Rakefile