newrelic_rpm 3.6.7.159 → 3.6.8.164

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/CHANGELOG +14 -0
  2. data/lib/new_relic/agent/agent.rb +38 -35
  3. data/lib/new_relic/agent/agent_logger.rb +6 -47
  4. data/lib/new_relic/agent/beacon_configuration.rb +10 -4
  5. data/lib/new_relic/agent/browser_monitoring.rb +39 -33
  6. data/lib/new_relic/agent/commands/agent_command.rb +4 -4
  7. data/lib/new_relic/agent/commands/agent_command_router.rb +72 -10
  8. data/lib/new_relic/agent/commands/thread_profiler_session.rb +110 -0
  9. data/lib/new_relic/agent/commands/xray_session.rb +55 -0
  10. data/lib/new_relic/agent/commands/xray_session_collection.rb +158 -0
  11. data/lib/new_relic/agent/configuration/default_source.rb +61 -24
  12. data/lib/new_relic/agent/configuration/mask_defaults.rb +2 -2
  13. data/lib/new_relic/agent/configuration/server_source.rb +1 -1
  14. data/lib/new_relic/agent/instrumentation/action_controller_subscriber.rb +2 -0
  15. data/lib/new_relic/agent/instrumentation/controller_instrumentation.rb +4 -10
  16. data/lib/new_relic/agent/instrumentation/rails3/action_controller.rb +10 -11
  17. data/lib/new_relic/agent/memory_logger.rb +52 -0
  18. data/lib/new_relic/agent/new_relic_service.rb +4 -0
  19. data/lib/new_relic/agent/request_sampler.rb +32 -13
  20. data/lib/new_relic/agent/samplers/cpu_sampler.rb +6 -3
  21. data/lib/new_relic/agent/threading/agent_thread.rb +2 -1
  22. data/lib/new_relic/agent/threading/backtrace_node.rb +80 -27
  23. data/lib/new_relic/agent/threading/backtrace_service.rb +264 -0
  24. data/lib/new_relic/agent/threading/thread_profile.rb +79 -118
  25. data/lib/new_relic/agent/transaction/developer_mode_sample_buffer.rb +56 -0
  26. data/lib/new_relic/agent/transaction/force_persist_sample_buffer.rb +25 -0
  27. data/lib/new_relic/agent/transaction/slowest_sample_buffer.rb +25 -0
  28. data/lib/new_relic/agent/transaction/transaction_sample_buffer.rb +86 -0
  29. data/lib/new_relic/agent/transaction/xray_sample_buffer.rb +64 -0
  30. data/lib/new_relic/agent/transaction.rb +25 -4
  31. data/lib/new_relic/agent/transaction_sample_builder.rb +6 -10
  32. data/lib/new_relic/agent/transaction_sampler.rb +47 -202
  33. data/lib/new_relic/agent/worker_loop.rb +47 -39
  34. data/lib/new_relic/agent.rb +1 -1
  35. data/lib/new_relic/build.rb +2 -2
  36. data/lib/new_relic/coerce.rb +8 -0
  37. data/lib/new_relic/control/instance_methods.rb +1 -0
  38. data/lib/new_relic/rack/browser_monitoring.rb +15 -1
  39. data/lib/new_relic/rack/developer_mode.rb +1 -1
  40. data/lib/new_relic/transaction_sample.rb +20 -5
  41. data/lib/new_relic/version.rb +1 -1
  42. data/newrelic.yml +4 -6
  43. data/newrelic_rpm.gemspec +1 -1
  44. data/test/agent_helper.rb +11 -0
  45. data/test/environments/lib/environments/runner.rb +5 -1
  46. data/test/environments/rails21/Gemfile +2 -2
  47. data/test/environments/rails22/Gemfile +2 -2
  48. data/test/environments/rails23/Gemfile +2 -2
  49. data/test/environments/rails31/Gemfile +2 -2
  50. data/test/environments/rails32/Gemfile +2 -2
  51. data/test/multiverse/suites/agent_only/marshaling_test.rb +1 -1
  52. data/test/multiverse/suites/agent_only/testing_app.rb +6 -0
  53. data/test/multiverse/suites/agent_only/thread_profiling_test.rb +5 -5
  54. data/test/multiverse/suites/agent_only/xray_sessions_test.rb +163 -0
  55. data/test/multiverse/suites/rails/request_statistics_test.rb +2 -2
  56. data/test/multiverse/suites/rails/view_instrumentation_test.rb +20 -21
  57. data/test/new_relic/agent/agent/connect_test.rb +0 -10
  58. data/test/new_relic/agent/agent_test.rb +27 -44
  59. data/test/new_relic/agent/browser_monitoring_test.rb +0 -52
  60. data/test/new_relic/agent/commands/agent_command_router_test.rb +150 -12
  61. data/test/new_relic/agent/commands/{thread_profiler_test.rb → thread_profiler_session_test.rb} +58 -19
  62. data/test/new_relic/agent/commands/xray_session_collection_test.rb +332 -0
  63. data/test/new_relic/agent/commands/xray_session_test.rb +42 -0
  64. data/test/new_relic/agent/configuration/manager_test.rb +2 -1
  65. data/test/new_relic/agent/configuration/server_source_test.rb +10 -10
  66. data/test/new_relic/agent/cpu_sampler_test.rb +50 -0
  67. data/test/new_relic/agent/instrumentation/action_controller_subscriber_test.rb +31 -0
  68. data/test/new_relic/agent/instrumentation/queue_time_test.rb +0 -1
  69. data/test/new_relic/agent/instrumentation/sequel_test.rb +1 -1
  70. data/test/new_relic/agent/instrumentation/task_instrumentation_test.rb +0 -1
  71. data/test/new_relic/agent/memory_logger_test.rb +53 -0
  72. data/test/new_relic/agent/new_relic_service_test.rb +1 -1
  73. data/test/new_relic/agent/pipe_channel_manager_test.rb +4 -5
  74. data/test/new_relic/agent/request_sampler_test.rb +70 -20
  75. data/test/new_relic/agent/rules_engine_test.rb +6 -0
  76. data/test/new_relic/agent/threading/agent_thread_test.rb +2 -2
  77. data/test/new_relic/agent/threading/backtrace_node_test.rb +110 -17
  78. data/test/new_relic/agent/threading/backtrace_service_test.rb +567 -0
  79. data/test/new_relic/agent/threading/fake_thread.rb +4 -0
  80. data/test/new_relic/agent/threading/thread_profile_test.rb +141 -217
  81. data/test/new_relic/agent/threading/threaded_test_case.rb +3 -8
  82. data/test/new_relic/agent/transaction/developer_mode_sample_buffer_test.rb +69 -0
  83. data/test/new_relic/agent/transaction/force_persist_sample_buffer_test.rb +52 -0
  84. data/test/new_relic/agent/transaction/slowest_sample_buffer_test.rb +67 -0
  85. data/test/new_relic/agent/transaction/xray_sample_buffer_test.rb +71 -0
  86. data/test/new_relic/agent/transaction_sampler_test.rb +171 -307
  87. data/test/new_relic/agent/transaction_test.rb +33 -5
  88. data/test/new_relic/agent/worker_loop_test.rb +33 -11
  89. data/test/new_relic/coerce_test.rb +13 -0
  90. data/test/new_relic/fake_collector.rb +26 -3
  91. data/test/new_relic/multiverse_helpers.rb +2 -0
  92. data/test/new_relic/rack/browser_monitoring_test.rb +12 -0
  93. data/test/new_relic/rack/developer_mode_test.rb +2 -2
  94. data/test/new_relic/transaction_sample_test.rb +19 -2
  95. data/test/performance/lib/performance/console_reporter.rb +1 -1
  96. data/test/performance/lib/performance/test_case.rb +7 -3
  97. data/test/performance/script/runner +3 -0
  98. data/test/performance/suites/thread_profiling.rb +83 -0
  99. data/test/test_helper.rb +2 -2
  100. data.tar.gz.sig +0 -0
  101. metadata +32 -32
  102. metadata.gz.sig +1 -1
  103. data/lib/new_relic/agent/commands/thread_profiler.rb +0 -80
@@ -2,11 +2,11 @@
2
2
  # This file is distributed under New Relic's license terms.
3
3
  # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
4
 
5
+ require 'set'
5
6
  require 'new_relic/agent/worker_loop'
6
7
  require 'new_relic/agent/threading/backtrace_node'
7
8
 
8
- # Intent is for this to be a data structure for representing a thread profile
9
- # TODO: Get rid of the running/sampling in this class, externalize it elsewhere
9
+ # Data structure for representing a thread profile
10
10
 
11
11
  module NewRelic
12
12
  module Agent
@@ -14,156 +14,117 @@ module NewRelic
14
14
 
15
15
  class ThreadProfile
16
16
 
17
- attr_reader :profile_id,
18
- :traces,
19
- :profile_agent_code,
20
- :interval, :duration,
21
- :poll_count, :sample_count,
22
- :start_time, :stop_time
23
-
24
- def initialize(agent_command)
25
- arguments = agent_command.arguments
26
- @profile_id = arguments.fetch('profile_id', -1)
27
- @profile_agent_code = arguments.fetch('profile_agent_code', true)
28
-
29
- @duration = arguments.fetch('duration', 120)
30
- @worker_loop = NewRelic::Agent::WorkerLoop.new(:duration => duration)
31
- @interval = arguments.fetch('sample_period', 0.1)
17
+ attr_reader :profile_id, :traces, :sample_period,
18
+ :duration, :poll_count, :backtrace_count, :failure_count,
19
+ :created_at, :xray_id, :command_arguments, :profile_agent_code
20
+ attr_accessor :finished_at
21
+
22
+ def initialize(command_arguments={})
23
+ @command_arguments = command_arguments
24
+ @profile_id = command_arguments.fetch('profile_id', -1)
25
+ @duration = command_arguments.fetch('duration', 120)
26
+ @sample_period = command_arguments.fetch('sample_period', 0.1)
27
+ @profile_agent_code = command_arguments.fetch('profile_agent_code', false)
28
+ @xray_id = command_arguments.fetch('x_ray_id', nil)
32
29
  @finished = false
33
30
 
34
31
  @traces = {
35
- :agent => [],
36
- :background => [],
37
- :other => [],
38
- :request => []
32
+ :agent => BacktraceNode.new(nil),
33
+ :background => BacktraceNode.new(nil),
34
+ :other => BacktraceNode.new(nil),
35
+ :request => BacktraceNode.new(nil)
39
36
  }
40
- @flattened_nodes = []
41
37
 
42
38
  @poll_count = 0
43
- @sample_count = 0
39
+ @backtrace_count = 0
44
40
  @failure_count = 0
41
+ @unique_threads = []
42
+
43
+ @created_at = Time.now
45
44
  end
46
45
 
47
- def run
48
- NewRelic::Agent.logger.debug("Starting thread profile. profile_id=#{profile_id}, duration=#{duration}")
49
-
50
- Threading::AgentThread.new('Thread Profiler') do
51
- @start_time = now_in_millis
52
-
53
- @worker_loop.run(@interval) do
54
- NewRelic::Agent.instance.stats_engine.
55
- record_supportability_metric_timed("ThreadProfiler/PollingTime") do
56
-
57
- @poll_count += 1
58
- Threading::AgentThread.list.each do |t|
59
- bucket = Threading::AgentThread.bucket_thread(t, @profile_agent_code)
60
- if bucket != :ignore
61
- backtrace = Threading::AgentThread.scrub_backtrace(t, @profile_agent_code)
62
- if backtrace.nil?
63
- @failure_count += 1
64
- else
65
- @sample_count += 1
66
- aggregate(backtrace, @traces[bucket])
67
- end
68
- end
69
- end
70
- end
71
- end
72
-
73
- mark_done
74
- NewRelic::Agent.logger.debug("Finished thread profile. #{@sample_count} backtraces, #{@failure_count} failures. Will send with next harvest.")
75
- NewRelic::Agent.instance.stats_engine.
76
- record_supportability_metric_count("ThreadProfiler/BacktraceFailures", @failure_count)
77
- end
46
+ def requested_period
47
+ @sample_period
78
48
  end
79
49
 
80
- def stop
81
- @worker_loop.stop
82
- mark_done
83
- NewRelic::Agent.logger.debug("Stopping thread profile.")
50
+ def increment_poll_count
51
+ @poll_count += 1
84
52
  end
85
53
 
86
- def aggregate(trace, trees=@traces[:request], parent=nil)
87
- return nil if trace.nil? || trace.empty?
88
- node = Threading::BacktraceNode.new(trace.last)
89
- existing = trees.find {|n| n == node}
54
+ def sample_count
55
+ xray? ? @backtrace_count : @poll_count
56
+ end
90
57
 
91
- if existing.nil?
92
- existing = node
93
- @flattened_nodes << node
94
- end
58
+ def xray?
59
+ !!@xray_id
60
+ end
95
61
 
96
- if parent
97
- parent.add_child(node)
98
- else
99
- trees << node unless trees.include? node
100
- end
62
+ def empty?
63
+ @backtrace_count == 0
64
+ end
101
65
 
102
- existing.runnable_count += 1
103
- aggregate(trace[0..-2], existing.children, existing)
66
+ def unique_thread_count
67
+ return 0 if @unique_threads.nil?
68
+ @unique_threads.length
69
+ end
104
70
 
105
- existing
71
+ def aggregate(backtrace, bucket, thread)
72
+ if backtrace.nil?
73
+ @failure_count += 1
74
+ else
75
+ @backtrace_count += 1
76
+ @traces[bucket].aggregate(backtrace)
77
+ @unique_threads << thread unless @unique_threads.include?(thread)
78
+ end
106
79
  end
107
80
 
108
- def prune!(count_to_keep)
109
- @flattened_nodes.sort!(&:order_for_pruning)
81
+ def truncate_to_node_count!(count_to_keep)
82
+ all_nodes = @traces.values.map { |n| n.flatten }.flatten
110
83
 
111
84
  NewRelic::Agent.instance.stats_engine.
112
- record_supportability_metric_count("ThreadProfiler/NodeCount", @flattened_nodes.size)
85
+ record_supportability_metric_count("ThreadProfiler/NodeCount", all_nodes.size)
113
86
 
114
- mark_for_pruning(@flattened_nodes, count_to_keep)
115
-
116
- traces.each { |_, nodes| Threading::BacktraceNode.prune!(nodes) }
87
+ all_nodes.sort!
88
+ nodes_to_prune = Set.new(all_nodes[count_to_keep..-1] || [])
89
+ traces.values.each { |root| root.prune!(nodes_to_prune) }
117
90
  end
118
91
 
119
92
  THREAD_PROFILER_NODES = 20_000
120
93
 
121
94
  include NewRelic::Coerce
122
95
 
123
- def to_collector_array(encoder)
124
- prune!(THREAD_PROFILER_NODES)
125
-
126
- traces = {
127
- "OTHER" => @traces[:other].map{|t| t.to_array },
128
- "REQUEST" => @traces[:request].map{|t| t.to_array },
129
- "AGENT" => @traces[:agent].map{|t| t.to_array },
130
- "BACKGROUND" => @traces[:background].map{|t| t.to_array }
96
+ def generate_traces
97
+ truncate_to_node_count!(THREAD_PROFILER_NODES)
98
+ {
99
+ "OTHER" => @traces[:other].to_array,
100
+ "REQUEST" => @traces[:request].to_array,
101
+ "AGENT" => @traces[:agent].to_array,
102
+ "BACKGROUND" => @traces[:background].to_array
131
103
  }
132
-
133
- [[
134
- int(@profile_id),
135
- float(@start_time),
136
- float(@stop_time),
137
- int(@poll_count),
138
- string(encoder.encode(traces)),
139
- int(@sample_count),
140
- 0
141
- ]]
142
- end
143
-
144
- def now_in_millis
145
- Time.now.to_f * 1_000
146
104
  end
147
105
 
148
- def finished?
149
- @finished
106
+ def to_collector_array(encoder)
107
+ result = [
108
+ int(self.profile_id),
109
+ float(self.created_at),
110
+ float(self.finished_at),
111
+ int(self.sample_count),
112
+ string(encoder.encode(generate_traces)),
113
+ int(self.unique_thread_count),
114
+ 0 # runnable thread count, which we don't track
115
+ ]
116
+ result << int(@xray_id) if xray?
117
+ result
150
118
  end
151
119
 
152
- def mark_done
153
- @finished = true
154
- @stop_time = now_in_millis
155
- end
120
+ def to_log_description
121
+ id = if xray?
122
+ "@xray_id: #{xray_id}"
123
+ else
124
+ "@profile_id: #{profile_id}"
125
+ end
156
126
 
157
- def mark_for_pruning(nodes, count_to_keep)
158
- to_prune = nodes[count_to_keep..-1] || []
159
- to_prune.each { |n| n.to_prune = true }
160
- end
161
-
162
- def self.parse_backtrace(trace)
163
- trace.map do |line|
164
- line =~ /(.*)\:(\d+)\:in `(.*)'/
165
- { :method => $3, :line_no => $2.to_i, :file => $1 }
166
- end
127
+ "#<ThreadProfile:#{object_id} #{id} @command_arguments=#{@command_arguments.inspect}>"
167
128
  end
168
129
 
169
130
  end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ require 'new_relic/agent/transaction/transaction_sample_buffer'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ class Transaction
10
+ class DeveloperModeSampleBuffer < TransactionSampleBuffer
11
+
12
+ MAX_SAMPLES = 100
13
+
14
+ def max_samples
15
+ MAX_SAMPLES
16
+ end
17
+
18
+ def harvest_samples
19
+ NO_SAMPLES
20
+ end
21
+
22
+ def enabled?
23
+ Agent.config[:developer_mode]
24
+ end
25
+
26
+ # Truncate to the last max_samples we've received
27
+ def truncate_samples
28
+ @samples = @samples.last(max_samples)
29
+ end
30
+
31
+ # We don't hold onto previously trapped transactions on harvest
32
+ # We've already got all the traces we want, thank you!
33
+ def store_previous(*)
34
+ end
35
+
36
+ # Captures the stack trace for a segment
37
+ # This is expensive and not for production mode
38
+ def visit_segment(segment)
39
+ return unless enabled? && segment
40
+
41
+ trace = strip_newrelic_frames(caller)
42
+ trace = trace.first(40) if trace.length > 40
43
+ segment[:backtrace] = trace
44
+ end
45
+
46
+ def strip_newrelic_frames(trace)
47
+ while trace.first =~/\/lib\/new_relic\/agent\//
48
+ trace.shift
49
+ end
50
+ trace
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ require 'new_relic/agent/transaction/transaction_sample_buffer'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ class Transaction
10
+ class ForcePersistSampleBuffer < TransactionSampleBuffer
11
+
12
+ MAX_SAMPLES = 10
13
+
14
+ def max_samples
15
+ MAX_SAMPLES
16
+ end
17
+
18
+ def allow_sample?(sample)
19
+ sample.force_persist
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ require 'new_relic/agent/transaction/transaction_sample_buffer'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ class Transaction
10
+ class SlowestSampleBuffer < TransactionSampleBuffer
11
+
12
+ MAX_SAMPLES = 1
13
+
14
+ def max_samples
15
+ MAX_SAMPLES
16
+ end
17
+
18
+ def allow_sample?(sample)
19
+ sample.threshold && sample.duration >= sample.threshold
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ module NewRelic
6
+ module Agent
7
+ class Transaction
8
+ class TransactionSampleBuffer
9
+ attr_reader :samples
10
+
11
+ NO_SAMPLES = [].freeze
12
+
13
+ def initialize
14
+ @samples = []
15
+ end
16
+
17
+ def enabled?
18
+ true
19
+ end
20
+
21
+ def reset!
22
+ @samples = []
23
+ end
24
+
25
+ def harvest_samples
26
+ @samples
27
+ ensure
28
+ reset!
29
+ end
30
+
31
+ def allow_sample?(sample)
32
+ true
33
+ end
34
+
35
+ def store(sample)
36
+ return unless enabled?
37
+ if allow_sample?(sample)
38
+ add_sample(sample)
39
+ truncate_samples_if_needed
40
+ end
41
+ end
42
+
43
+ def store_previous(previous_samples)
44
+ return unless enabled?
45
+ previous_samples.each do |sample|
46
+ add_sample(sample) if allow_sample?(sample)
47
+ end
48
+ truncate_samples_if_needed
49
+ end
50
+
51
+ def truncate_samples_if_needed
52
+ truncate_samples if full?
53
+ end
54
+
55
+ def full?
56
+ @samples.length >= max_samples
57
+ end
58
+
59
+ # Our default truncation strategy is to keep max_samples worth of the
60
+ # longest samples. Override this method for alternate behavior.
61
+ #
62
+ # This doesn't use the more convenient #last and #sort_by to avoid
63
+ # additional array allocations (and abundant alliteration)
64
+ def truncate_samples
65
+ @samples.sort!{|a,b| a.duration <=> b.duration}
66
+ @samples.slice!(0..-(max_samples + 1))
67
+ end
68
+
69
+ # When pushing a scope different sample buffers potentially want to
70
+ # know about what's happening to annotate the incoming segments
71
+ def visit_segment(*)
72
+ # no-op
73
+ end
74
+
75
+ private
76
+
77
+ # If a buffer needs to modify an added sample, override this method.
78
+ # Bounds checking, allowing samples and truncation belongs elsewhere.
79
+ def add_sample(sample)
80
+ @samples << sample
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ # This file is distributed under New Relic's license terms.
3
+ # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details.
4
+
5
+ require 'new_relic/agent/transaction/transaction_sample_buffer'
6
+
7
+ module NewRelic
8
+ module Agent
9
+ class Transaction
10
+ class XraySampleBuffer < TransactionSampleBuffer
11
+
12
+ attr_writer :xray_session_collection
13
+
14
+ def initialize
15
+ super
16
+
17
+ # Memoize the config setting since this happens per request
18
+ @enabled = NewRelic::Agent.config[:'xray_session.allow_traces']
19
+ NewRelic::Agent.config.register_callback(:'xray_session.allow_traces') do |new_value|
20
+ @enabled = new_value
21
+ end
22
+
23
+ @max_samples = NewRelic::Agent.config[:'xray_session.max_samples']
24
+ NewRelic::Agent.config.register_callback(:'xray_session.max_samples') do |new_value|
25
+ @max_samples = new_value
26
+ end
27
+ end
28
+
29
+ def xray_session_collection
30
+ @xray_session_collection ||= NewRelic::Agent.instance.agent_command_router.xray_session_collection
31
+ end
32
+
33
+ def max_samples
34
+ @max_samples
35
+ end
36
+
37
+ def truncate_samples
38
+ # First in wins, so stop on allow_sample? instead of truncating
39
+ end
40
+
41
+ def allow_sample?(sample)
42
+ !full? && !lookup_session_id(sample).nil?
43
+ end
44
+
45
+ def enabled?
46
+ @enabled
47
+ end
48
+
49
+
50
+ private
51
+
52
+ def add_sample(sample)
53
+ super(sample)
54
+ sample.xray_session_id = lookup_session_id(sample)
55
+ end
56
+
57
+ def lookup_session_id(sample)
58
+ xray_session_collection.session_id_for_transaction_name(sample.transaction_name)
59
+ end
60
+
61
+ end
62
+ end
63
+ end
64
+ end