scout_apm 2.1.8 → 2.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +9 -0
- data/lib/scout_apm.rb +3 -0
- data/lib/scout_apm/agent.rb +9 -6
- data/lib/scout_apm/agent/reporting.rb +5 -3
- data/lib/scout_apm/background_job_integrations/delayed_job.rb +47 -1
- data/lib/scout_apm/background_worker.rb +4 -0
- data/lib/scout_apm/config.rb +2 -1
- data/lib/scout_apm/environment.rb +1 -1
- data/lib/scout_apm/histogram.rb +11 -2
- data/lib/scout_apm/instruments/mongoid.rb +14 -1
- data/lib/scout_apm/instruments/percentile_sampler.rb +36 -19
- data/lib/scout_apm/instruments/process/process_cpu.rb +3 -2
- data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
- data/lib/scout_apm/layer_converters/converter_base.rb +193 -0
- data/lib/scout_apm/layer_converters/slow_job_converter.rb +14 -73
- data/lib/scout_apm/layer_converters/slow_request_converter.rb +13 -85
- data/lib/scout_apm/metric_set.rb +6 -0
- data/lib/scout_apm/reporter.rb +53 -15
- data/lib/scout_apm/request_histograms.rb +4 -0
- data/lib/scout_apm/scored_item_set.rb +7 -0
- data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +21 -0
- data/lib/scout_apm/serializers/payload_serializer.rb +9 -3
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +2 -1
- data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +1 -1
- data/lib/scout_apm/slow_job_record.rb +4 -1
- data/lib/scout_apm/slow_transaction.rb +18 -2
- data/lib/scout_apm/store.rb +42 -11
- data/lib/scout_apm/tracked_request.rb +1 -1
- data/lib/scout_apm/utils/gzip_helper.rb +24 -0
- data/lib/scout_apm/utils/numbers.rb +14 -0
- data/lib/scout_apm/version.rb +1 -1
- data/test/test_helper.rb +10 -0
- data/test/unit/config_test.rb +7 -9
- data/test/unit/histogram_test.rb +14 -0
- data/test/unit/instruments/percentile_sampler_test.rb +137 -0
- data/test/unit/serializers/payload_serializer_test.rb +3 -3
- data/test/unit/store_test.rb +51 -0
- data/test/unit/utils/numbers_test.rb +15 -0
- metadata +10 -4
@@ -2,9 +2,9 @@
|
|
2
2
|
module ScoutApm
|
3
3
|
module Serializers
|
4
4
|
class PayloadSerializer
|
5
|
-
def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
5
|
+
def self.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
|
6
6
|
if ScoutApm::Agent.instance.config.value("report_format") == 'json'
|
7
|
-
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
7
|
+
ScoutApm::Serializers::PayloadSerializerToJson.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
|
8
8
|
else
|
9
9
|
metadata = metadata.dup
|
10
10
|
metadata.default = nil
|
@@ -15,7 +15,13 @@ module ScoutApm
|
|
15
15
|
:metrics => metrics,
|
16
16
|
:slow_transactions => slow_transactions,
|
17
17
|
:jobs => jobs,
|
18
|
-
:slow_jobs => slow_jobs
|
18
|
+
:slow_jobs => slow_jobs,
|
19
|
+
|
20
|
+
# as_json returns a ruby object. Since it's not a simple
|
21
|
+
# array, use this to maintain compatibility with json
|
22
|
+
# payloads. At this point, the marshal code branch is
|
23
|
+
# very rarely used anyway.
|
24
|
+
:histograms => HistogramsSerializerToJson.new(histograms).as_json)
|
19
25
|
end
|
20
26
|
end
|
21
27
|
|
@@ -2,7 +2,7 @@ module ScoutApm
|
|
2
2
|
module Serializers
|
3
3
|
module PayloadSerializerToJson
|
4
4
|
class << self
|
5
|
-
def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
|
5
|
+
def serialize(metadata, metrics, slow_transactions, jobs, slow_jobs, histograms)
|
6
6
|
metadata.merge!({:payload_version => 2})
|
7
7
|
|
8
8
|
jsonify_hash({:metadata => metadata,
|
@@ -10,6 +10,7 @@ module ScoutApm
|
|
10
10
|
:slow_transactions => rearrange_the_slow_transactions(slow_transactions),
|
11
11
|
:jobs => JobsSerializerToJson.new(jobs).as_json,
|
12
12
|
:slow_jobs => SlowJobsSerializerToJson.new(slow_jobs).as_json,
|
13
|
+
:histograms => HistogramsSerializerToJson.new(histograms).as_json,
|
13
14
|
})
|
14
15
|
end
|
15
16
|
|
@@ -25,6 +25,7 @@ module ScoutApm
|
|
25
25
|
"metrics" => MetricsToJsonSerializer.new(job.metrics).as_json, # New style of metrics
|
26
26
|
"allocation_metrics" => MetricsToJsonSerializer.new(job.allocation_metrics).as_json, # New style of metrics
|
27
27
|
"context" => job.context.to_hash,
|
28
|
+
"truncated_metrics" => job.truncated_metrics,
|
28
29
|
|
29
30
|
"score" => job.score,
|
30
31
|
}
|
@@ -33,4 +34,3 @@ module ScoutApm
|
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
36
|
-
|
@@ -21,8 +21,9 @@ module ScoutApm
|
|
21
21
|
attr_reader :seconds_since_startup
|
22
22
|
attr_reader :score
|
23
23
|
attr_reader :git_sha
|
24
|
+
attr_reader :truncated_metrics
|
24
25
|
|
25
|
-
def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score)
|
26
|
+
def initialize(queue_name, job_name, time, total_time, exclusive_time, context, metrics, allocation_metrics, mem_delta, allocations, score, truncated_metrics)
|
26
27
|
@queue_name = queue_name
|
27
28
|
@job_name = job_name
|
28
29
|
@time = time
|
@@ -37,6 +38,8 @@ module ScoutApm
|
|
37
38
|
@hostname = ScoutApm::Environment.instance.hostname
|
38
39
|
@git_sha = ScoutApm::Environment.instance.git_revision.sha
|
39
40
|
@score = score
|
41
|
+
@truncated_metrics = truncated_metrics
|
42
|
+
|
40
43
|
ScoutApm::Agent.instance.logger.debug { "Slow Job [#{metric_name}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta}"}
|
41
44
|
end
|
42
45
|
|
@@ -17,7 +17,9 @@ module ScoutApm
|
|
17
17
|
attr_accessor :seconds_since_startup # hack - we need to reset these server side.
|
18
18
|
attr_accessor :git_sha # hack - we need to reset these server side.
|
19
19
|
|
20
|
-
|
20
|
+
attr_reader :truncated_metrics # True/False that says if we had to truncate the metrics of this trace
|
21
|
+
|
22
|
+
def initialize(uri, metric_name, total_call_time, metrics, allocation_metrics, context, time, raw_stackprof, mem_delta, allocations, score, truncated_metrics)
|
21
23
|
@uri = uri
|
22
24
|
@metric_name = metric_name
|
23
25
|
@total_call_time = total_call_time
|
@@ -32,6 +34,8 @@ module ScoutApm
|
|
32
34
|
@hostname = ScoutApm::Environment.instance.hostname
|
33
35
|
@score = score
|
34
36
|
@git_sha = ScoutApm::Environment.instance.git_revision.sha
|
37
|
+
@truncated_metrics = truncated_metrics
|
38
|
+
|
35
39
|
ScoutApm::Agent.instance.logger.debug { "Slow Request [#{uri}] - Call Time: #{total_call_time} Mem Delta: #{mem_delta} Score: #{score}"}
|
36
40
|
end
|
37
41
|
|
@@ -46,7 +50,19 @@ module ScoutApm
|
|
46
50
|
end
|
47
51
|
|
48
52
|
def as_json
|
49
|
-
json_attributes = [:key,
|
53
|
+
json_attributes = [:key,
|
54
|
+
:time,
|
55
|
+
:total_call_time,
|
56
|
+
:uri,
|
57
|
+
[:context, :context_hash],
|
58
|
+
:score,
|
59
|
+
:prof,
|
60
|
+
:mem_delta,
|
61
|
+
:allocations,
|
62
|
+
:seconds_since_startup,
|
63
|
+
:hostname,
|
64
|
+
:git_sha,
|
65
|
+
:truncated_metrics]
|
50
66
|
ScoutApm::AttributeArranger.call(self, json_attributes)
|
51
67
|
end
|
52
68
|
|
data/lib/scout_apm/store.rb
CHANGED
@@ -26,7 +26,23 @@ module ScoutApm
|
|
26
26
|
# Save newly collected metrics
|
27
27
|
def track!(metrics, options={})
|
28
28
|
@mutex.synchronize {
|
29
|
-
|
29
|
+
period = if options[:timestamp]
|
30
|
+
@reporting_periods[options[:timestamp]]
|
31
|
+
else
|
32
|
+
current_period
|
33
|
+
end
|
34
|
+
period.absorb_metrics!(metrics)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def track_histograms!(histograms, options={})
|
39
|
+
@mutex.synchronize {
|
40
|
+
period = if options[:timestamp]
|
41
|
+
@reporting_periods[options[:timestamp]]
|
42
|
+
else
|
43
|
+
current_period
|
44
|
+
end
|
45
|
+
period.merge_histograms!(histograms)
|
30
46
|
}
|
31
47
|
end
|
32
48
|
|
@@ -67,15 +83,15 @@ module ScoutApm
|
|
67
83
|
def write_to_layaway(layaway, force=false)
|
68
84
|
ScoutApm::Agent.instance.logger.debug("Writing to layaway#{" (Forced)" if force}")
|
69
85
|
|
70
|
-
|
71
|
-
|
86
|
+
reporting_periods.select { |time, rp| force || time.timestamp < current_timestamp.timestamp }.
|
87
|
+
each { |time, rp| collect_samplers(rp) }.
|
72
88
|
each { |time, rp| write_reporting_period(layaway, time, rp) }
|
73
|
-
}
|
74
89
|
end
|
75
90
|
|
76
91
|
def write_reporting_period(layaway, time, rp)
|
77
|
-
|
78
|
-
|
92
|
+
@mutex.synchronize {
|
93
|
+
layaway.write_reporting_period(rp)
|
94
|
+
}
|
79
95
|
rescue => e
|
80
96
|
ScoutApm::Agent.instance.logger.warn("Failed writing data to layaway file: #{e.message} / #{e.backtrace}")
|
81
97
|
ensure
|
@@ -91,12 +107,10 @@ module ScoutApm
|
|
91
107
|
def collect_samplers(rp)
|
92
108
|
@samplers.each do |sampler|
|
93
109
|
begin
|
94
|
-
|
95
|
-
rp.absorb_metrics!(metrics)
|
110
|
+
sampler.metrics(rp.timestamp, self)
|
96
111
|
rescue => e
|
97
112
|
ScoutApm::Agent.instance.logger.info "Error reading #{sampler.human_name} for period: #{rp}"
|
98
|
-
ScoutApm::Agent.instance.logger.debug e.message
|
99
|
-
ScoutApm::Agent.instance.logger.debug e.backtrace.join("\n")
|
113
|
+
ScoutApm::Agent.instance.logger.debug "#{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
100
114
|
end
|
101
115
|
end
|
102
116
|
end
|
@@ -159,6 +173,9 @@ module ScoutApm
|
|
159
173
|
# A ScoredItemSet holding the "best" traces for the period
|
160
174
|
attr_reader :job_traces
|
161
175
|
|
176
|
+
# An Array of HistogramsReport
|
177
|
+
attr_reader :histograms
|
178
|
+
|
162
179
|
# A StoreReportingPeriodTimestamp representing the time that this
|
163
180
|
# collection of metrics is for
|
164
181
|
attr_reader :timestamp
|
@@ -171,6 +188,8 @@ module ScoutApm
|
|
171
188
|
@request_traces = ScoredItemSet.new
|
172
189
|
@job_traces = ScoredItemSet.new
|
173
190
|
|
191
|
+
@histograms = []
|
192
|
+
|
174
193
|
@metric_set = MetricSet.new
|
175
194
|
@jobs = Hash.new
|
176
195
|
end
|
@@ -181,7 +200,8 @@ module ScoutApm
|
|
181
200
|
merge_metrics!(other.metric_set).
|
182
201
|
merge_slow_transactions!(other.slow_transactions_payload).
|
183
202
|
merge_jobs!(other.jobs).
|
184
|
-
merge_slow_jobs!(other.slow_jobs_payload)
|
203
|
+
merge_slow_jobs!(other.slow_jobs_payload).
|
204
|
+
merge_histograms!(other.histograms)
|
185
205
|
self
|
186
206
|
end
|
187
207
|
|
@@ -230,6 +250,17 @@ module ScoutApm
|
|
230
250
|
self
|
231
251
|
end
|
232
252
|
|
253
|
+
def merge_histograms!(new_histograms)
|
254
|
+
new_histograms = Array(new_histograms)
|
255
|
+
@histograms = (histograms + new_histograms).
|
256
|
+
group_by { |histo| histo.name }.
|
257
|
+
map { |(_, histos)|
|
258
|
+
histos.inject { |merged, histo| merged.combine!(histo) }
|
259
|
+
}
|
260
|
+
|
261
|
+
self
|
262
|
+
end
|
263
|
+
|
233
264
|
#################################
|
234
265
|
# Retrieve Metrics for reporting
|
235
266
|
#################################
|
@@ -252,7 +252,7 @@ module ScoutApm
|
|
252
252
|
# If there's an instant_key, it means we need to report this right away
|
253
253
|
if instant?
|
254
254
|
trace = slow_converter.call
|
255
|
-
ScoutApm::InstantReporting.new(trace, instant_key).call
|
255
|
+
ScoutApm::InstantReporting.new(trace, instant_key).call
|
256
256
|
end
|
257
257
|
end
|
258
258
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ScoutApm
|
2
|
+
module Utils
|
3
|
+
# A simple wrapper around Ruby's built-in gzip support.
|
4
|
+
class GzipHelper
|
5
|
+
DEFAULT_GZIP_LEVEL = 5
|
6
|
+
|
7
|
+
attr_reader :level
|
8
|
+
|
9
|
+
def initialize(level = DEFAULT_GZIP_LEVEL)
|
10
|
+
@level = level
|
11
|
+
end
|
12
|
+
|
13
|
+
def deflate(str)
|
14
|
+
strio = StringIO.new
|
15
|
+
|
16
|
+
gz = Zlib::GzipWriter.new(strio, level)
|
17
|
+
gz.write str
|
18
|
+
gz.close
|
19
|
+
|
20
|
+
strio.string
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/scout_apm/version.rb
CHANGED
data/test/test_helper.rb
CHANGED
data/test/unit/config_test.rb
CHANGED
@@ -7,24 +7,24 @@ class ConfigTest < Minitest::Test
|
|
7
7
|
conf = ScoutApm::Config.without_file
|
8
8
|
|
9
9
|
# nil for random keys
|
10
|
-
assert_nil conf.value(
|
10
|
+
assert_nil conf.value('log_file_path')
|
11
11
|
|
12
12
|
# but has values for defaulted keys
|
13
|
-
assert conf.value(
|
13
|
+
assert conf.value('host')
|
14
14
|
|
15
15
|
# and still reads from ENV
|
16
16
|
ENV['SCOUT_CONFIG_TEST_KEY'] = 'testval'
|
17
|
-
assert_equal 'testval', conf.value(
|
17
|
+
assert_equal 'testval', conf.value('config_test_key')
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_loading_a_file
|
21
|
-
set_rack_env(
|
21
|
+
set_rack_env('production')
|
22
22
|
|
23
|
-
conf_file = File.expand_path(
|
23
|
+
conf_file = File.expand_path('../../data/config_test_1.yml', __FILE__)
|
24
24
|
conf = ScoutApm::Config.with_file(conf_file)
|
25
25
|
|
26
|
-
assert_equal
|
27
|
-
assert_equal
|
26
|
+
assert_equal 'debug', conf.value('log_level')
|
27
|
+
assert_equal 'APM Test Conf (Production)', conf.value('name')
|
28
28
|
end
|
29
29
|
|
30
30
|
def test_loading_file_without_env_in_file
|
@@ -68,5 +68,3 @@ class ConfigTest < Minitest::Test
|
|
68
68
|
assert_equal ["a"], coercion.coerce(["a"])
|
69
69
|
end
|
70
70
|
end
|
71
|
-
|
72
|
-
|
data/test/unit/histogram_test.rb
CHANGED
@@ -79,6 +79,20 @@ class HistogramTest < Minitest::Test
|
|
79
79
|
assert combined.quantile(0) < combined.quantile(100)
|
80
80
|
end
|
81
81
|
|
82
|
+
def test_combine_dedups_identicals
|
83
|
+
hist1 = ScoutApm::NumericHistogram.new(5)
|
84
|
+
hist2 = ScoutApm::NumericHistogram.new(5)
|
85
|
+
hist1.add(1)
|
86
|
+
hist1.add(2)
|
87
|
+
hist2.add(2)
|
88
|
+
hist2.add(3)
|
89
|
+
|
90
|
+
combined = hist1.combine!(hist2)
|
91
|
+
assert_equal 4, combined.total
|
92
|
+
assert_equal [[1, 1], [2, 2], [1, 3]],
|
93
|
+
combined.bins.map{|bin| [bin.count, bin.value.to_i] }
|
94
|
+
end
|
95
|
+
|
82
96
|
def test_mean
|
83
97
|
hist = ScoutApm::NumericHistogram.new(5)
|
84
98
|
10.times {
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/instruments/percentile_sampler'
|
4
|
+
|
5
|
+
class PercentileSamplerTest < Minitest::Test
|
6
|
+
PercentileSampler = ScoutApm::Instruments::PercentileSampler
|
7
|
+
HistogramReport = ScoutApm::Instruments::HistogramReport
|
8
|
+
|
9
|
+
attr_reader :subject
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@subject = PercentileSampler.new(logger, histograms)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def test_initialize_with_logger_and_histogram_set
|
17
|
+
assert_equal subject.logger, logger
|
18
|
+
assert_equal subject.histograms, histograms
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_implements_instrument_interface
|
22
|
+
assert subject.respond_to?(:human_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_percentiles_returns_one_percentile_per_endpoint_at_time
|
26
|
+
histograms[time].add("foo", 10)
|
27
|
+
histograms[time].add("bar", 15)
|
28
|
+
histograms[time2].add("baz", 15)
|
29
|
+
|
30
|
+
assert_equal subject.percentiles(time).length, 2
|
31
|
+
assert_equal subject.percentiles(time2).length, 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_percentiles_clears_time_from_hash
|
35
|
+
histograms[time].add("foo", 10)
|
36
|
+
histograms[time2].add("baz", 15)
|
37
|
+
|
38
|
+
subject.percentiles(time)
|
39
|
+
|
40
|
+
assert_false histograms.key?(time)
|
41
|
+
assert histograms.key?(time + 10)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_percentiles_returns_histogram_reports
|
45
|
+
histograms[time].add("foo", 10)
|
46
|
+
|
47
|
+
assert subject.percentiles(time).
|
48
|
+
all?{ |item| item.is_a?(HistogramReport) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_percentiles_returns_correct_histogram_report
|
52
|
+
histograms[time].add("foo", 100)
|
53
|
+
histograms[time].add("foo", 200)
|
54
|
+
histograms[time].add("foo", 100)
|
55
|
+
histograms[time].add("foo", 300)
|
56
|
+
|
57
|
+
report = subject.percentiles(time).first
|
58
|
+
histogram = report.histogram
|
59
|
+
|
60
|
+
assert_equal "foo", report.name
|
61
|
+
assert_equal 4, histogram.total
|
62
|
+
assert_equal [[2, 100], [1, 200], [1, 300]],
|
63
|
+
histogram.bins.map{|bin| [bin.count, bin.value] }
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_metrics_saves_histogram_to_store
|
67
|
+
store = mock
|
68
|
+
store.expects(:track_histograms!)
|
69
|
+
subject.metrics(ScoutApm::StoreReportingPeriodTimestamp.new(time), store)
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
################################################################################
|
74
|
+
# HistogramReport Test
|
75
|
+
################################################################################
|
76
|
+
def test_histogram_report_combine_refuses_to_combine_mismatched_name
|
77
|
+
assert_raises { HistogramReport.new("foo", histogram).combine!(HistogramReport.new("bar", histogram)) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_histogram_report_merge_keeps_name
|
81
|
+
report1 = HistogramReport.new("foo", histogram)
|
82
|
+
report2 = HistogramReport.new("foo", histogram)
|
83
|
+
combined = report1.combine!(report2)
|
84
|
+
|
85
|
+
assert "foo", combined.name
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_histogram_report_combine_merges_histograms
|
89
|
+
histogram1 = histogram
|
90
|
+
histogram2 = histogram
|
91
|
+
histogram1.add(1)
|
92
|
+
histogram1.add(2)
|
93
|
+
histogram2.add(2)
|
94
|
+
histogram2.add(3)
|
95
|
+
|
96
|
+
report1 = HistogramReport.new("foo", histogram1)
|
97
|
+
report2 = HistogramReport.new("foo", histogram2)
|
98
|
+
combined = report1.combine!(report2)
|
99
|
+
|
100
|
+
assert_equal 4, combined.histogram.total
|
101
|
+
assert_equal [[1, 1], [2, 2], [1, 3]],
|
102
|
+
combined.histogram.bins.map{|bin| [bin.count, bin.value.to_i] }
|
103
|
+
end
|
104
|
+
|
105
|
+
################################################################################
|
106
|
+
# Test Helpers
|
107
|
+
################################################################################
|
108
|
+
def logger
|
109
|
+
@logger ||= begin
|
110
|
+
@logger_io = StringIO.new
|
111
|
+
Logger.new(@logger_io)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def histograms
|
116
|
+
@histograms ||= begin
|
117
|
+
@request_histograms_by_time = Hash.new { |hash, key|
|
118
|
+
hash[key] = ScoutApm::RequestHistograms.new
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def histogram
|
124
|
+
max_bins = 20
|
125
|
+
ScoutApm::NumericHistogram.new(max_bins)
|
126
|
+
end
|
127
|
+
|
128
|
+
# An arbitrary time
|
129
|
+
def time
|
130
|
+
@time ||= Time.now
|
131
|
+
end
|
132
|
+
|
133
|
+
def time2
|
134
|
+
time + 10
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|