pulse-meter 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +4 -0
- data/examples/basic.ru +23 -5
- data/examples/basic_sensor_data.rb +15 -2
- data/examples/full/server.ru +2 -3
- data/examples/minimal/server.ru +3 -2
- data/lib/pulse-meter/sensor.rb +2 -1
- data/lib/pulse-meter/sensor/configuration.rb +3 -1
- data/lib/pulse-meter/sensor/hashed_indicator.rb +22 -0
- data/lib/pulse-meter/sensor/timeline.rb +13 -1
- data/lib/pulse-meter/version.rb +1 -1
- data/lib/pulse-meter/visualize/app.rb +6 -2
- data/lib/pulse-meter/visualize/base.rb +15 -0
- data/lib/pulse-meter/visualize/dsl/base.rb +131 -0
- data/lib/pulse-meter/visualize/dsl/errors.rb +0 -6
- data/lib/pulse-meter/visualize/dsl/layout.rb +12 -41
- data/lib/pulse-meter/visualize/dsl/page.rb +15 -41
- data/lib/pulse-meter/visualize/dsl/sensor.rb +9 -10
- data/lib/pulse-meter/visualize/dsl/widget.rb +18 -65
- data/lib/pulse-meter/visualize/dsl/widget_old.rb +95 -0
- data/lib/pulse-meter/visualize/dsl/widgets/area.rb +20 -0
- data/lib/pulse-meter/visualize/dsl/widgets/gauge.rb +12 -0
- data/lib/pulse-meter/visualize/dsl/widgets/line.rb +21 -0
- data/lib/pulse-meter/visualize/dsl/widgets/pie.rb +16 -0
- data/lib/pulse-meter/visualize/dsl/widgets/table.rb +19 -0
- data/lib/pulse-meter/visualize/layout.rb +7 -16
- data/lib/pulse-meter/visualize/page.rb +5 -10
- data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
- data/lib/pulse-meter/visualize/public/js/application.coffee +156 -107
- data/lib/pulse-meter/visualize/public/js/application.js +283 -122
- data/lib/pulse-meter/visualize/sensor.rb +7 -13
- data/lib/pulse-meter/visualize/views/main.haml +8 -52
- data/lib/pulse-meter/visualize/views/widgets/area.haml +31 -0
- data/lib/pulse-meter/visualize/views/widgets/extend_options.haml +11 -0
- data/lib/pulse-meter/visualize/views/widgets/gauge.haml +13 -0
- data/lib/pulse-meter/visualize/views/widgets/line.haml +31 -0
- data/lib/pulse-meter/visualize/views/widgets/pie.haml +13 -0
- data/lib/pulse-meter/visualize/views/widgets/table.haml +23 -0
- data/lib/pulse-meter/visualize/widget.rb +17 -87
- data/lib/pulse-meter/visualize/widgets/gauge.rb +47 -0
- data/lib/pulse-meter/visualize/widgets/pie.rb +36 -0
- data/lib/pulse-meter/visualize/widgets/timeline.rb +90 -0
- data/lib/pulse-meter/visualizer.rb +18 -8
- data/perl/PulseMeter/Sensor/Base.pm +42 -0
- data/perl/PulseMeter/Sensor/Counter.pm +19 -0
- data/perl/PulseMeter/Sensor/HashedIndicator.pm +12 -0
- data/perl/PulseMeter/Sensor/Indicator.pm +17 -0
- data/perl/PulseMeter/Sensor/Timeline.pm +51 -0
- data/perl/PulseMeter/Sensor/Timelined/Average.pm +13 -0
- data/perl/PulseMeter/Sensor/Timelined/Counter.pm +12 -0
- data/perl/PulseMeter/Sensor/Timelined/HashedCounter.pm +12 -0
- data/perl/PulseMeter/Sensor/Timelined/Max.pm +18 -0
- data/perl/PulseMeter/Sensor/Timelined/Median.pm +8 -0
- data/perl/PulseMeter/Sensor/Timelined/Min.pm +18 -0
- data/perl/PulseMeter/Sensor/Timelined/Percentile.pm +17 -0
- data/perl/PulseMeter/Sensor/Timelined/UniqCounter.pm +13 -0
- data/perl/PulseMeter/Sensor/UniqCounter.pm +12 -0
- data/pulse-meter.gemspec +1 -0
- data/spec/pulse_meter/sensor/configuration_spec.rb +10 -2
- data/spec/pulse_meter/sensor/hashed_indicator_spec.rb +39 -0
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +8 -8
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +29 -42
- data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +5 -5
- data/spec/pulse_meter/visualize/dsl/widget_spec.rb +1 -122
- data/spec/pulse_meter/visualize/dsl/widgets/area_spec.rb +44 -0
- data/spec/pulse_meter/visualize/dsl/widgets/gauge_spec.rb +22 -0
- data/spec/pulse_meter/visualize/dsl/widgets/line_spec.rb +44 -0
- data/spec/pulse_meter/visualize/dsl/widgets/pie_spec.rb +35 -0
- data/spec/pulse_meter/visualize/dsl/widgets/table_spec.rb +36 -0
- data/spec/pulse_meter/visualize/layout_spec.rb +3 -3
- data/spec/pulse_meter/visualize/page_spec.rb +2 -2
- data/spec/pulse_meter/visualize/sensor_spec.rb +4 -4
- data/spec/pulse_meter/visualize/series_extractor_spec.rb +3 -3
- data/spec/pulse_meter/visualize/widgets/area_spec.rb +78 -0
- data/spec/pulse_meter/visualize/widgets/gauge_spec.rb +63 -0
- data/spec/pulse_meter/visualize/widgets/line_spec.rb +77 -0
- data/spec/pulse_meter/visualize/widgets/pie_spec.rb +73 -0
- data/spec/pulse_meter/visualize/widgets/table_spec.rb +78 -0
- data/spec/shared_examples/dsl_widget.rb +106 -0
- data/spec/shared_examples/timeline_sensor.rb +32 -2
- metadata +75 -6
- data/lib/pulse-meter/visualize/dsl.rb +0 -0
- data/spec/pulse_meter/visualize/widget_spec.rb +0 -122
data/.travis.yml
CHANGED
data/examples/basic.ru
CHANGED
|
@@ -39,11 +39,11 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
39
39
|
c.sensor :goose_count
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
p.
|
|
42
|
+
p.pie "Rhino & Lama & Gooze count comparison" do |c|
|
|
43
43
|
c.redraw_interval 5
|
|
44
44
|
c.values_label 'Count'
|
|
45
45
|
c.width 5
|
|
46
|
-
c.show_last_point
|
|
46
|
+
c.show_last_point false
|
|
47
47
|
c.timespan 1200
|
|
48
48
|
|
|
49
49
|
c.sensor :rhino_count, color: '#AAAAAA'
|
|
@@ -51,11 +51,28 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
51
51
|
c.sensor :goose_count
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
p.
|
|
54
|
+
p.gauge "CPU Usage" do |g|
|
|
55
|
+
g.redraw_interval 5
|
|
56
|
+
g.values_label '%'
|
|
57
|
+
g.width 5
|
|
58
|
+
|
|
59
|
+
g.red_from 90
|
|
60
|
+
g.red_to 100
|
|
61
|
+
g.yellow_from 75
|
|
62
|
+
g.yellow_to 90
|
|
63
|
+
g.minor_ticks 5
|
|
64
|
+
g.height 200
|
|
65
|
+
|
|
66
|
+
g.sensor :cpu
|
|
67
|
+
g.sensor :memory
|
|
68
|
+
g.sensor :temperature
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
p.table "Rhino & Lama & Goose count comparison" do |c|
|
|
55
72
|
c.redraw_interval 5
|
|
56
73
|
c.values_label 'Count'
|
|
57
|
-
c.width
|
|
58
|
-
c.show_last_point
|
|
74
|
+
c.width 10
|
|
75
|
+
c.show_last_point true
|
|
59
76
|
c.timespan 1200
|
|
60
77
|
|
|
61
78
|
c.sensor :rhino_count, color: '#AAAAAA'
|
|
@@ -63,6 +80,7 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
63
80
|
c.sensor :goose_count
|
|
64
81
|
end
|
|
65
82
|
|
|
83
|
+
|
|
66
84
|
end
|
|
67
85
|
|
|
68
86
|
l.page "Ages" do |p|
|
|
@@ -48,6 +48,15 @@ cfg = PulseMeter::Sensor::Configuration.new(
|
|
|
48
48
|
interval: 20,
|
|
49
49
|
ttl: 3600
|
|
50
50
|
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
cpu: {sensor_type: 'indicator'},
|
|
54
|
+
memory: {sensor_type: 'indicator'},
|
|
55
|
+
temperature: {
|
|
56
|
+
sensor_type: 'hashed_indicator',
|
|
57
|
+
args: {
|
|
58
|
+
annotation: 'T'
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
)
|
|
53
62
|
|
|
@@ -61,6 +70,10 @@ while true
|
|
|
61
70
|
|
|
62
71
|
10.times do
|
|
63
72
|
goose_n = Random.rand(4)
|
|
64
|
-
cfg.goose_count("
|
|
73
|
+
cfg.goose_count("g_#{goose_n}" => 1)
|
|
74
|
+
cfg.temperature("g_#{goose_n}" => Random.rand(50))
|
|
65
75
|
end
|
|
66
|
-
|
|
76
|
+
|
|
77
|
+
cfg.cpu(Random.rand(100))
|
|
78
|
+
cfg.memory(Random.rand(100))
|
|
79
|
+
end
|
data/examples/full/server.ru
CHANGED
|
@@ -14,7 +14,7 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
14
14
|
|
|
15
15
|
# Transfer some global parameters to Google Charts
|
|
16
16
|
l.gchart_options({
|
|
17
|
-
|
|
17
|
+
background_color: '#CCC'
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
# Add some pages
|
|
@@ -87,7 +87,6 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
87
87
|
w.sensor sensor
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
w.timespan 24 * 60 * 60
|
|
91
90
|
w.redraw_interval 10
|
|
92
91
|
w.show_last_point true
|
|
93
92
|
w.values_label "Request count"
|
|
@@ -102,4 +101,4 @@ layout = PulseMeter::Visualizer.draw do |l|
|
|
|
102
101
|
|
|
103
102
|
end
|
|
104
103
|
|
|
105
|
-
run layout.to_app
|
|
104
|
+
run layout.to_app
|
data/examples/minimal/server.ru
CHANGED
data/lib/pulse-meter/sensor.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
require 'pulse-meter/sensor/base'
|
|
2
2
|
require 'pulse-meter/sensor/counter'
|
|
3
|
-
require 'pulse-meter/sensor/hashed_counter'
|
|
4
3
|
require 'pulse-meter/sensor/indicator'
|
|
4
|
+
require 'pulse-meter/sensor/hashed_counter'
|
|
5
|
+
require 'pulse-meter/sensor/hashed_indicator'
|
|
5
6
|
require 'pulse-meter/sensor/remote'
|
|
6
7
|
require 'pulse-meter/sensor/uniq_counter'
|
|
7
8
|
require 'pulse-meter/sensor/timeline'
|
|
@@ -27,6 +27,8 @@ module PulseMeter
|
|
|
27
27
|
name = name.to_s
|
|
28
28
|
if @sensors.has_key?(name)
|
|
29
29
|
@sensors[name].event(*args)
|
|
30
|
+
elsif (name =~ /^(.*)_at$/) && @sensors.has_key?($1)
|
|
31
|
+
@sensors[$1].event_at(*args)
|
|
30
32
|
else
|
|
31
33
|
raise ArgumentError, "Unknown sensor: `#{name}'"
|
|
32
34
|
end
|
|
@@ -44,4 +46,4 @@ module PulseMeter
|
|
|
44
46
|
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
|
-
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module PulseMeter
|
|
2
|
+
module Sensor
|
|
3
|
+
# Static hashed indicator. In fact is is just a named hash with float value
|
|
4
|
+
class HashedIndicator < Indicator
|
|
5
|
+
|
|
6
|
+
# Sets indicator values
|
|
7
|
+
# @param value [Hash] new indicator values
|
|
8
|
+
def event(events)
|
|
9
|
+
events.each_pair {|name, value| redis.hset(value_key, name, value.to_f)}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get indicator values
|
|
13
|
+
# @return [Fixnum] indicator value or zero unless it was initialized
|
|
14
|
+
def value
|
|
15
|
+
redis.
|
|
16
|
+
hgetall(value_key).
|
|
17
|
+
inject(Hash.new(0)) {|h, (k, v)| h[k] = v.to_f; h}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -49,6 +49,7 @@ module PulseMeter
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Processes event
|
|
52
|
+
# @param value event value
|
|
52
53
|
def event(value = nil)
|
|
53
54
|
multi do
|
|
54
55
|
current_key = current_raw_data_key
|
|
@@ -57,7 +58,18 @@ module PulseMeter
|
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
# Processes event from the past
|
|
62
|
+
# @param time [Time] event time
|
|
63
|
+
# @param value event value
|
|
64
|
+
def event_at(time, value = nil)
|
|
65
|
+
multi do
|
|
66
|
+
interval_id = get_interval_id(time)
|
|
67
|
+
key = raw_data_key(interval_id)
|
|
68
|
+
aggregate_event(key, value)
|
|
69
|
+
redis.expire(key, raw_data_ttl)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
# Reduces data in given interval.
|
|
62
74
|
# @note Interval id is
|
|
63
75
|
# just unixtime of its lower bound. Ruduction is a process
|
data/lib/pulse-meter/version.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
require 'sinatra/base'
|
|
2
|
-
require 'haml'
|
|
3
1
|
require 'gon-sinatra'
|
|
2
|
+
require 'haml'
|
|
3
|
+
require 'sinatra/base'
|
|
4
|
+
require 'sinatra/partial'
|
|
4
5
|
|
|
5
6
|
module PulseMeter
|
|
6
7
|
module Visualize
|
|
7
8
|
class App < Sinatra::Base
|
|
8
9
|
include PulseMeter::Mixins::Utils
|
|
9
10
|
register Gon::Sinatra
|
|
11
|
+
register Sinatra::Partial
|
|
12
|
+
|
|
13
|
+
set :partial_template_engine, :haml
|
|
10
14
|
|
|
11
15
|
def initialize(layout)
|
|
12
16
|
@layout = layout
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module PulseMeter
|
|
2
|
+
module Visualize
|
|
3
|
+
module DSL
|
|
4
|
+
class DArray < Array; end
|
|
5
|
+
class BadDataClass < PulseMeter::Visualize::DSL::Error; end
|
|
6
|
+
class Base
|
|
7
|
+
include PulseMeter::Mixins::Utils
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@opts = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def process_args(args)
|
|
14
|
+
args.each_pair do |k, v|
|
|
15
|
+
send(k, v)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
|
|
21
|
+
def deprecated_setter(name)
|
|
22
|
+
define_method(name) do |*args|
|
|
23
|
+
STDERR.puts "DEPRECATION: #{name} DSL helper does not take any effect anymore."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def setter(name, &block)
|
|
28
|
+
define_method(name) do |val|
|
|
29
|
+
block.call(val) if block
|
|
30
|
+
@opts[name] = val
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def bool_setter(name)
|
|
35
|
+
define_method(name) do |val|
|
|
36
|
+
@opts[name] = !!val
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def string_setter(name, &block)
|
|
41
|
+
define_method(name) do |val|
|
|
42
|
+
val = val.to_s
|
|
43
|
+
block.call(val) if block
|
|
44
|
+
@opts[name] = val
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def int_setter(name, &block)
|
|
49
|
+
define_method(name) do |val|
|
|
50
|
+
val = val.to_i
|
|
51
|
+
block.call(val) if block
|
|
52
|
+
@opts[name] = val
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def hash_extender(name, &block)
|
|
57
|
+
define_method(name) do |val|
|
|
58
|
+
@opts[name] ||= {}
|
|
59
|
+
@opts[name].merge!(val)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def array_extender(name, &block)
|
|
64
|
+
define_method(name) do |val|
|
|
65
|
+
@opts[name] ||=[]
|
|
66
|
+
block.call(val) if block
|
|
67
|
+
@opts[name] << val
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def dsl_setter(name, klass)
|
|
72
|
+
define_method(name) do |*args, &block|
|
|
73
|
+
@opts[name] = create_dsl_obj(args, klass, block)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dsl_array_extender(collection_name, name, klass)
|
|
78
|
+
define_method(name) do |*args, &block|
|
|
79
|
+
@opts[collection_name] ||= DArray.new
|
|
80
|
+
@opts[collection_name] << create_dsl_obj(args, klass, block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def data_class
|
|
85
|
+
@data_class || PulseMeter::Visualize::Base
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def data_class=(klass)
|
|
89
|
+
raise BadDataClass unless klass.is_a?(Class) && klass <= PulseMeter::Visualize::Base
|
|
90
|
+
@data_class = klass
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_dsl_obj(args, klass, block)
|
|
96
|
+
params, options = extract_params(args)
|
|
97
|
+
dsl_obj = klass.new(*params)
|
|
98
|
+
dsl_obj.process_args(options)
|
|
99
|
+
block.call(dsl_obj) if block
|
|
100
|
+
dsl_obj
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def extract_params(args)
|
|
104
|
+
opts = if args.last.is_a?(Hash)
|
|
105
|
+
args.pop
|
|
106
|
+
else
|
|
107
|
+
{}
|
|
108
|
+
end
|
|
109
|
+
[args, opts]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_data
|
|
113
|
+
klass = self.class.data_class
|
|
114
|
+
args = @opts.each_with_object({}) do |(k, v), acc|
|
|
115
|
+
acc[k] = case v
|
|
116
|
+
when PulseMeter::Visualize::DSL::Base
|
|
117
|
+
v.to_data
|
|
118
|
+
when DArray
|
|
119
|
+
v.map(&:to_data)
|
|
120
|
+
else
|
|
121
|
+
v
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
klass.new(args)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
@@ -1,54 +1,25 @@
|
|
|
1
1
|
module PulseMeter
|
|
2
2
|
module Visualize
|
|
3
3
|
module DSL
|
|
4
|
-
class Layout
|
|
4
|
+
class Layout < Base
|
|
5
5
|
DEFAULT_TITLE = "Pulse Meter"
|
|
6
|
-
DEFAULT_GCHART_OPTIONS = {}
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
@pages = []
|
|
10
|
-
@title = DEFAULT_TITLE
|
|
11
|
-
@use_utc = true
|
|
12
|
-
@gchart_options = DEFAULT_GCHART_OPTIONS.dup
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def title(title)
|
|
16
|
-
@title = title
|
|
17
|
-
end
|
|
7
|
+
self.data_class = PulseMeter::Visualize::Layout
|
|
18
8
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def outlier_color(_)
|
|
24
|
-
STDERR.puts "DEPRECATION: outlier_color DSL helper does not take effect anymore"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def highchart_options(_)
|
|
28
|
-
STDERR.puts "DEPRECATION: highchart_options DSL helper does not take effect anymore, use gchart_options instead"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def gchart_options(options = {})
|
|
32
|
-
@gchart_options.merge!(options)
|
|
9
|
+
def initialize
|
|
10
|
+
super()
|
|
11
|
+
self.title(DEFAULT_TITLE)
|
|
12
|
+
self.use_utc(false)
|
|
33
13
|
end
|
|
34
14
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pages << page
|
|
39
|
-
end
|
|
15
|
+
string_setter :title
|
|
16
|
+
bool_setter :use_utc
|
|
17
|
+
hash_extender :gchart_options
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
title = @title || ''
|
|
44
|
-
PulseMeter::Visualize::Layout.new( {
|
|
45
|
-
pages: pages,
|
|
46
|
-
title: title,
|
|
47
|
-
use_utc: @use_utc,
|
|
48
|
-
gchart_options: @gchart_options
|
|
49
|
-
} )
|
|
50
|
-
end
|
|
19
|
+
deprecated_setter :outlier_color
|
|
20
|
+
deprecated_setter :highchart_options
|
|
51
21
|
|
|
22
|
+
dsl_array_extender :pages, :page, PulseMeter::Visualize::DSL::Page
|
|
52
23
|
end
|
|
53
24
|
end
|
|
54
25
|
end
|