scout_apm 2.1.14 → 2.1.15

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
  SHA1:
3
- metadata.gz: faa58ca3a2800e8aade5ade08a31d2c5db1e5a7d
4
- data.tar.gz: ceb42f746d3b72eb84610522eeee9247fd68cf81
3
+ metadata.gz: 2432d4b3a94011da560df94d67bfda1c0e58ed80
4
+ data.tar.gz: bafd8c126d8ea5e1329d7d9e119ce24cacea9af3
5
5
  SHA512:
6
- metadata.gz: b692efc8a697ffb71657d9c2493c1c4fd2bc003f5970155339654679832d6b7fd9da95d26bc0d2b46c153980da697532376556556ff8b2ba10140cc5fb398180
7
- data.tar.gz: 3ae789a0000e598a39bce414c4955d13fa354e38f716d53ddb6041160a89fc9a500c3f1d1144095e0487722eedc2951cb6681e8db4f8917bf162adbc52112b51
6
+ metadata.gz: 766e322ec27e6ba797cb7756e67975e2b4c41b3cc5c1ad957054445e798b05c75878e28d46641f6d6fc5ff86dfb8351d4500a5264f5aa7d3f9f778715b8c7517
7
+ data.tar.gz: 62a7ce7ee17fcfed6011fba1b62fb40795ba7c5b31fc6a7bd2d20f17c9af2c84ffe3c9c3bd56a513b6561f8cb3170fe078c76e1dd6892df22a03d1facc0555e0
@@ -1,3 +1,7 @@
1
+ # 2.1.15
2
+
3
+ * Limit memory usage for very long running requests.
4
+
1
5
  # 2.1.14
2
6
 
3
7
  * Add TrackedRequest#ignore_request! to entirely ignore and stop capturing a
@@ -27,6 +27,8 @@ require 'scout_apm/version'
27
27
 
28
28
  require 'scout_apm/tracked_request'
29
29
  require 'scout_apm/layer'
30
+ require 'scout_apm/limited_layer'
31
+ require 'scout_apm/layer_children_set'
30
32
  require 'scout_apm/request_manager'
31
33
  require 'scout_apm/call_set'
32
34
 
@@ -13,13 +13,17 @@ module ScoutApm
13
13
  # instrumentation for an example of how this is useful
14
14
  attr_accessor :name
15
15
 
16
- # An array of children layers, in call order.
16
+ # An array of children layers
17
17
  # For instance, if we are in a middleware, there will likely be only a single
18
18
  # child, which is another middleware. In a Controller, we may have a handful
19
19
  # of children: [ActiveRecord, ActiveRecord, View, HTTP Call].
20
20
  #
21
21
  # This useful to get actual time spent in this layer vs. children time
22
- attr_reader :children
22
+ #
23
+ # TODO: Check callers for compatibility w/ nil to avoid making an empty array
24
+ def children
25
+ @children || LayerChildrenSet.new
26
+ end
23
27
 
24
28
  # Time objects recording the start & stop times of this layer
25
29
  attr_reader :start_time, :stop_time
@@ -38,6 +42,8 @@ module ScoutApm
38
42
  # Known Keys:
39
43
  # :record_count - The number of rows returned by an AR query (From notification instantiation.active_record)
40
44
  # :class_name - The ActiveRecord class name (From notification instantiation.active_record)
45
+ #
46
+ # If no annotations are ever set, this will return nil
41
47
  attr_reader :annotations
42
48
 
43
49
  BACKTRACE_CALLER_LIMIT = 50 # maximum number of lines to send thru for backtrace analysis
@@ -45,15 +51,18 @@ module ScoutApm
45
51
  def initialize(type, name, start_time = Time.now)
46
52
  @type = type
47
53
  @name = name
48
- @annotations = {}
49
54
  @start_time = start_time
50
55
  @allocations_start = ScoutApm::Instruments::Allocations.count
51
56
  @allocations_stop = 0
52
- @children = [] # In order of calls
57
+
58
+ # initialize these only on first use
59
+ @children = nil
60
+ @annotations = nil
53
61
  @desc = nil
54
62
  end
55
63
 
56
64
  def add_child(child)
65
+ @children ||= LayerChildrenSet.new
57
66
  @children << child
58
67
  end
59
68
 
@@ -72,6 +81,7 @@ module ScoutApm
72
81
 
73
82
  # This data is internal to ScoutApm, to add custom information, use the Context api.
74
83
  def annotate_layer(hsh)
84
+ @annotations ||= {}
75
85
  @annotations.merge!(hsh)
76
86
  end
77
87
 
@@ -144,6 +154,7 @@ module ScoutApm
144
154
  map { |child| child.total_call_time }.
145
155
  inject(0) { |sum, time| sum + time }
146
156
  end
157
+ private :child_time
147
158
 
148
159
  ######################################
149
160
  # Allocation Calculations
@@ -169,5 +180,6 @@ module ScoutApm
169
180
  map { |child| child.total_allocations }.
170
181
  inject(0) { |sum, obj| sum + obj }
171
182
  end
183
+ private :child_allocations
172
184
  end
173
185
  end
@@ -0,0 +1,72 @@
1
+ module ScoutApm
2
+ # A set of children records for any given Layer. This implements some
3
+ # rate-limiting logic.
4
+ #
5
+ # We store the first `unique_cutoff` count of each layer type. So if cutoff
6
+ # is 1000, we'd store 1000 HTTP layers, and 1000 ActiveRecord calls, and 1000
7
+ # of each other layer type. After that, make a LimitedLayer object and store
8
+ # only aggregate counts and times of future layers of that type. (So the
9
+ # 1001st an onward of ActiveRecord would get only aggregate times, and
10
+ # counts, without any detail about the SQL called)
11
+ #
12
+ # When the set of children is small, keep them unique
13
+ # When the set of children gets large enough, stop keeping details
14
+ #
15
+ # The next optimization, which is not yet implemented:
16
+ # when the set of children gets larger, attempt to merge them without data loss
17
+ class LayerChildrenSet
18
+ include Enumerable
19
+
20
+ # By default, how many unique children of a type do we store before
21
+ # flipping over to storing only aggregate info.
22
+ DEFAULT_UNIQUE_CUTOFF = 2000
23
+ attr_reader :unique_cutoff
24
+
25
+ # The Set of children objects
26
+ attr_reader :children
27
+ private :children
28
+
29
+ def initialize(unique_cutoff = DEFAULT_UNIQUE_CUTOFF)
30
+ @children = Hash.new { |hash, key| hash[key] = Set.new }
31
+ @limited_layers = nil # populated when needed
32
+ @unique_cutoff = unique_cutoff
33
+ end
34
+
35
+ # Add a new layer into this set
36
+ # Only add completed layers - otherwise this will collect up incorrect info
37
+ # into the created LimitedLayer, since it will "freeze" any current data for
38
+ # total_call_time and similar methods.
39
+ def <<(child)
40
+ metric_type = child.type
41
+ set = children[metric_type]
42
+
43
+ if set.size >= unique_cutoff
44
+ # find limited_layer
45
+ @limited_layers || init_limited_layers
46
+ @limited_layers[metric_type].absorb(child)
47
+ else
48
+ # we have space just add it
49
+ set << child
50
+ end
51
+ end
52
+
53
+ def each
54
+ children.each do |_type, set|
55
+ set.each do |child_layer|
56
+ yield child_layer
57
+ end
58
+ end
59
+
60
+ if @limited_layers
61
+ @limited_layers.each do |_type, limited_layer|
62
+ yield limited_layer
63
+ end
64
+ end
65
+ end
66
+
67
+ # hold off initializing this until we know we need it
68
+ def init_limited_layers
69
+ @limited_layers ||= Hash.new { |hash, key| hash[key] = LimitedLayer.new(key) }
70
+ end
71
+ end
72
+ end
@@ -177,7 +177,7 @@ module ScoutApm
177
177
  meta_options = make_meta_options(layer)
178
178
 
179
179
  meta = MetricMeta.new(layer.legacy_metric_name, meta_options)
180
- meta.extra.merge!(layer.annotations)
180
+ meta.extra.merge!(layer.annotations) if layer.annotations
181
181
 
182
182
  store_backtrace(layer, meta)
183
183
 
@@ -191,6 +191,11 @@ module ScoutApm
191
191
  # allocations
192
192
  stat = allocation_metric_hash[meta]
193
193
  stat.update!(layer.total_allocations, layer.total_exclusive_allocations)
194
+
195
+ if LimitedLayer === layer
196
+ metric_hash[meta].call_count = layer.count
197
+ allocation_metric_hash[meta].call_count = layer.count
198
+ end
194
199
  end
195
200
 
196
201
  # Merged Metric - no specifics, just sum up by type (ActiveRecord, View, HTTP, etc)
@@ -218,7 +223,8 @@ module ScoutApm
218
223
  # an example. We start capturing before we know if a query is cached
219
224
  # or not, and want to skip any cached queries.
220
225
  def skip_layer?(layer)
221
- return true if layer.annotations[:ignorable]
226
+ return false if layer.annotations.nil?
227
+ return true if layer.annotations[:ignorable]
222
228
  end
223
229
  end
224
230
  end
@@ -57,7 +57,7 @@ module ScoutApm
57
57
  walker.walk do |layer|
58
58
  next if layer == job_layer
59
59
  next if layer == queue_layer
60
- next if layer.annotations[:ignorable]
60
+ next if skip_layer?(layer)
61
61
 
62
62
  # we don't need to use the full metric name for scoped metrics as we
63
63
  # only display metrics aggregrated by type, just use "ActiveRecord"
@@ -21,7 +21,7 @@ module ScoutApm
21
21
  metric_hash = Hash.new
22
22
 
23
23
  walker.walk do |layer|
24
- next if layer.annotations[:ignorable]
24
+ next if skip_layer?(layer)
25
25
 
26
26
  meta_options = if layer == scope_layer # We don't scope the controller under itself
27
27
  {}
@@ -0,0 +1,122 @@
1
+ module ScoutApm
2
+ # A LimitedLayer is a lossy-compression approach to fall back on once we max out
3
+ # the number of detailed layer objects we store. See LayerChildrenSet for the
4
+ # logic on when that change over happens
5
+ #
6
+ # QUESTION: What do we do if we attempt to merge an item that has children?
7
+ class LimitedLayer
8
+ attr_reader :type
9
+
10
+ def initialize(type)
11
+ @type = type
12
+
13
+ @total_call_time = 0
14
+ @total_exclusive_time = 0
15
+ @total_allocations = 0
16
+ @total_exclusive_allocations = 0
17
+ @total_layers = 0
18
+ end
19
+
20
+ def absorb(layer)
21
+ @total_layers += 1
22
+
23
+ @total_call_time += layer.total_call_time
24
+ @total_exclusive_time += layer.total_exclusive_time
25
+
26
+ @total_allocations += layer.total_allocations
27
+ @total_exclusive_allocations += layer.total_exclusive_allocations
28
+ end
29
+
30
+ def total_call_time
31
+ @total_call_time
32
+ end
33
+
34
+ def total_exclusive_time
35
+ @total_exclusive_time
36
+ end
37
+
38
+ def total_allocations
39
+ @total_allocations
40
+ end
41
+
42
+ def total_exclusive_allocations
43
+ @total_exclusive_allocations
44
+ end
45
+
46
+ def count
47
+ @total_layers
48
+ end
49
+
50
+ # This is the old style name. This function is used for now, but should be
51
+ # removed, and the new type & name split should be enforced through the
52
+ # app.
53
+ def legacy_metric_name
54
+ "#{type}/Limited"
55
+ end
56
+
57
+ def children
58
+ Set.new
59
+ end
60
+
61
+ def annotations
62
+ nil
63
+ end
64
+
65
+ def to_s
66
+ "<LimitedLayer type=#{type} count=#{count}>"
67
+ end
68
+
69
+ ######################################################
70
+ # Stub out some methods with static default values #
71
+ ######################################################
72
+ def subscopable?
73
+ false
74
+ end
75
+
76
+ def desc
77
+ nil
78
+ end
79
+
80
+ def backtrace
81
+ nil
82
+ end
83
+
84
+
85
+ #######################################################################
86
+ # Many methods don't make any sense on a limited layer. Raise errors #
87
+ # aggressively for now to detect mistaken calls #
88
+ #######################################################################
89
+
90
+ def add_child
91
+ raise "Should never call add_child on a limited_layer"
92
+ end
93
+
94
+ def record_stop_time!(*)
95
+ raise "Should never call record_stop_time! on a limited_layer"
96
+ end
97
+
98
+ def record_allocations!
99
+ raise "Should never call record_allocations! on a limited_layer"
100
+ end
101
+
102
+ def desc=(*)
103
+ raise "Should never call desc on a limited_layer"
104
+ end
105
+
106
+ def annotate_layer(*)
107
+ raise "Should never call annotate_layer on a limited_layer"
108
+ end
109
+
110
+ def subscopable!
111
+ raise "Should never call subscopable! on a limited_layer"
112
+ end
113
+
114
+ def capture_backtrace!
115
+ raise "Should never call capture_backtrace on a limited_layer"
116
+ end
117
+
118
+ def caller_array
119
+ raise "Should never call caller_array on a limited_layer"
120
+ end
121
+ end
122
+ end
@@ -62,7 +62,6 @@ module ScoutApm
62
62
  return ignoring_start_layer if ignoring_request?
63
63
 
64
64
  start_request(layer) unless @root_layer
65
- @layers[-1].add_child(layer) if @layers.any?
66
65
  @layers.push(layer)
67
66
  end
68
67
 
@@ -86,6 +85,8 @@ module ScoutApm
86
85
  layer.record_stop_time!
87
86
  layer.record_allocations!
88
87
 
88
+ @layers[-1].add_child(layer) if @layers.any?
89
+
89
90
  # This must be called before checking if a backtrace should be collected as the call count influences our capture logic.
90
91
  # We call `#update_call_counts in stop layer to ensure the layer has a final desc. Layer#desc is updated during the AR instrumentation flow.
91
92
  update_call_counts!(layer)
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "2.1.14"
2
+ VERSION = "2.1.15"
3
3
  end
4
4
 
@@ -0,0 +1,88 @@
1
+ require 'test_helper'
2
+ require 'scout_apm/layer_children_set'
3
+
4
+ class LayerChildrenSetTest < Minitest::Test
5
+ SET = ScoutApm::LayerChildrenSet
6
+
7
+ def test_limit_default
8
+ assert_equal SET::DEFAULT_UNIQUE_CUTOFF, SET.new.unique_cutoff
9
+ end
10
+
11
+ # Add 5, make sure they're all in the children list we get back.
12
+ def test_add_layer_before_limit
13
+ s = SET.new(5)
14
+
15
+ 5.times do
16
+ s << make_layer("LayerType", "LayerName")
17
+ end
18
+
19
+ children = s.to_a
20
+ assert_equal 5, children.size
21
+
22
+ # Don't care about order
23
+ (0..4).each do |i|
24
+ assert children.include?(lookup_layer(i))
25
+ end
26
+ end
27
+
28
+ def test_add_layer_after_limit
29
+ s = SET.new(5)
30
+
31
+ 10.times do
32
+ s << make_layer("LayerType", "LayerName")
33
+ end
34
+
35
+ children = s.to_a
36
+ # 6 = 5 real ones + 1 merged.
37
+ assert_equal 6, children.size
38
+
39
+ # Don't care about order
40
+ (0..4).each do |i|
41
+ assert children.include?(lookup_layer(i))
42
+ end
43
+
44
+ # Don't care about order
45
+ (5..9).each do |i|
46
+ assert ! children.include?(lookup_layer(i))
47
+ end
48
+
49
+ limited_layer = children.last
50
+ assert_equal ScoutApm::LimitedLayer, limited_layer.class
51
+ assert_equal 5, limited_layer.count
52
+ end
53
+
54
+ def test_add_layer_with_different_type_after_limit
55
+ s = SET.new(5)
56
+
57
+ # Add 20 items
58
+ 10.times do
59
+ s << make_layer("LayerType", "LayerName")
60
+ s << make_layer("DifferentLayerType", "LayerName")
61
+ end
62
+
63
+ children = s.to_a
64
+
65
+ # Tyo types, so 2 distinct limitdlayer objects
66
+ limited_layers = children.select{ |l| ScoutApm::LimitedLayer === l }
67
+ assert_equal 2, limited_layers.length
68
+
69
+ # 5 unchanged children each for the two layer types, plus the 2 limitd when each overran their limit
70
+ assert_equal 12, children.length
71
+ limited_layers.each { |ml| assert_equal 5, ml.count }
72
+ end
73
+
74
+ #############
75
+ # Helpers #
76
+ #############
77
+
78
+ def make_layer(type, name)
79
+ @made_layers ||= []
80
+ l = ScoutApm::Layer.new(type, name)
81
+ @made_layers << l
82
+ l
83
+ end
84
+
85
+ def lookup_layer(i)
86
+ @made_layers[i]
87
+ end
88
+ end
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+ require 'ostruct'
3
+
4
+ class LimitedLayerTest < Minitest::Test
5
+
6
+ def test_counts_while_absorbing
7
+ ll = ScoutApm::LimitedLayer.new("ActiveRecord")
8
+ assert_equal 0, ll.count
9
+
10
+ ll.absorb faux_layer("ActiveRecord", "User#Find", 2, 1, 200, 100)
11
+ assert_equal 1, ll.count
12
+
13
+ ll.absorb faux_layer("ActiveRecord", "User#Find", 2, 1, 200, 100)
14
+ assert_equal 2, ll.count
15
+ end
16
+
17
+ def test_sums_values_while_absorbing
18
+ ll = ScoutApm::LimitedLayer.new("ActiveRecord")
19
+
20
+ ll.absorb faux_layer("ActiveRecord", "User#Find", 2, 1, 200, 100)
21
+ assert_equal 1, ll.total_exclusive_time
22
+ assert_equal 2, ll.total_call_time
23
+ assert_equal 100, ll.total_exclusive_allocations
24
+ assert_equal 200, ll.total_allocations
25
+
26
+
27
+ ll.absorb faux_layer("ActiveRecord", "User#Find", 4, 3, 400, 300)
28
+ assert_equal 4, ll.total_exclusive_time # 3 + 1
29
+ assert_equal 6, ll.total_call_time # 4 + 2
30
+ assert_equal 400, ll.total_exclusive_allocations # 300 + 100
31
+ assert_equal 600, ll.total_allocations # 400 + 200
32
+ end
33
+
34
+ def test_the_name
35
+ ll = ScoutApm::LimitedLayer.new("ActiveRecord")
36
+ assert_equal "ActiveRecord/Limited", ll.legacy_metric_name
37
+ end
38
+
39
+ #############
40
+ # Helpers #
41
+ #############
42
+
43
+ def faux_layer(type, name, tct, tet, a_tct, a_tet)
44
+ OpenStruct.new(
45
+ :type => type,
46
+ :name => name,
47
+ :total_call_time => tct,
48
+ :total_exclusive_time => tet,
49
+ :total_allocations => a_tct,
50
+ :total_exclusive_allocations => a_tet,
51
+ )
52
+ end
53
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.14
4
+ version: 2.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-11-01 00:00:00.000000000 Z
12
+ date: 2016-11-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -180,6 +180,7 @@ files:
180
180
  - lib/scout_apm/layaway.rb
181
181
  - lib/scout_apm/layaway_file.rb
182
182
  - lib/scout_apm/layer.rb
183
+ - lib/scout_apm/layer_children_set.rb
183
184
  - lib/scout_apm/layer_converters/allocation_metric_converter.rb
184
185
  - lib/scout_apm/layer_converters/converter_base.rb
185
186
  - lib/scout_apm/layer_converters/depth_first_walker.rb
@@ -189,6 +190,7 @@ files:
189
190
  - lib/scout_apm/layer_converters/request_queue_time_converter.rb
190
191
  - lib/scout_apm/layer_converters/slow_job_converter.rb
191
192
  - lib/scout_apm/layer_converters/slow_request_converter.rb
193
+ - lib/scout_apm/limited_layer.rb
192
194
  - lib/scout_apm/metric_meta.rb
193
195
  - lib/scout_apm/metric_set.rb
194
196
  - lib/scout_apm/metric_stats.rb
@@ -251,6 +253,8 @@ files:
251
253
  - test/unit/instruments/net_http_test.rb
252
254
  - test/unit/instruments/percentile_sampler_test.rb
253
255
  - test/unit/layaway_test.rb
256
+ - test/unit/layer_children_set_test.rb
257
+ - test/unit/limited_layer_test.rb
254
258
  - test/unit/metric_set_test.rb
255
259
  - test/unit/scored_item_set_test.rb
256
260
  - test/unit/serializers/payload_serializer_test.rb
@@ -301,6 +305,8 @@ test_files:
301
305
  - test/unit/instruments/net_http_test.rb
302
306
  - test/unit/instruments/percentile_sampler_test.rb
303
307
  - test/unit/layaway_test.rb
308
+ - test/unit/layer_children_set_test.rb
309
+ - test/unit/limited_layer_test.rb
304
310
  - test/unit/metric_set_test.rb
305
311
  - test/unit/scored_item_set_test.rb
306
312
  - test/unit/serializers/payload_serializer_test.rb