rack-mini-profiler 0.9.4 → 0.9.9
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.
Potentially problematic release.
This version of rack-mini-profiler might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/README.md +50 -25
- data/lib/html/includes.css +12 -0
- data/lib/html/includes.js +53 -18
- data/lib/html/includes.less +17 -3
- data/lib/html/profile_handler.js +1 -1
- data/lib/mini_profiler/asset_version.rb +1 -1
- data/lib/mini_profiler/config.rb +9 -4
- data/lib/mini_profiler/gc_profiler.rb +18 -42
- data/lib/mini_profiler/profiler.rb +113 -89
- data/lib/mini_profiler/profiling_methods.rb +17 -11
- data/lib/mini_profiler/storage/file_store.rb +10 -2
- data/lib/mini_profiler/timer_struct/page.rb +8 -0
- data/lib/mini_profiler/timer_struct/request.rb +8 -0
- data/lib/mini_profiler/timer_struct/sql.rb +2 -2
- data/lib/mini_profiler/version.rb +1 -1
- data/lib/mini_profiler_rails/railtie.rb +23 -5
- data/lib/patches/db/activerecord.rb +2 -0
- data/lib/patches/db/mongo.rb +16 -0
- data/lib/patches/db/neo4j.rb +14 -0
- data/lib/patches/db/nobrainer.rb +29 -0
- data/lib/patches/db/oracle_enhanced.rb +70 -0
- data/lib/patches/db/riak.rb +103 -0
- data/lib/patches/sql_patches.rb +18 -7
- data/rack-mini-profiler.gemspec +1 -1
- metadata +9 -4
@@ -3,9 +3,9 @@ module Rack
|
|
3
3
|
module ProfilingMethods
|
4
4
|
|
5
5
|
def record_sql(query, elapsed_ms)
|
6
|
-
return unless current
|
6
|
+
return unless current && current.current_timer
|
7
7
|
c = current
|
8
|
-
c.current_timer.add_sql(query, elapsed_ms, c.page_struct, c.skip_backtrace, c.full_backtrace)
|
8
|
+
c.current_timer.add_sql(query, elapsed_ms, c.page_struct, c.skip_backtrace, c.full_backtrace)
|
9
9
|
end
|
10
10
|
|
11
11
|
def start_step(name)
|
@@ -26,10 +26,9 @@ module Rack
|
|
26
26
|
def step(name, opts = nil)
|
27
27
|
if current
|
28
28
|
parent_timer = current.current_timer
|
29
|
-
result = nil
|
30
29
|
current.current_timer = current_timer = current.current_timer.add_child(name)
|
31
30
|
begin
|
32
|
-
|
31
|
+
yield if block_given?
|
33
32
|
ensure
|
34
33
|
current_timer.record_time
|
35
34
|
current.current_timer = parent_timer
|
@@ -87,34 +86,37 @@ module Rack
|
|
87
86
|
end
|
88
87
|
end
|
89
88
|
|
90
|
-
result = nil
|
91
89
|
parent_timer = Rack::MiniProfiler.current.current_timer
|
92
90
|
|
93
91
|
if type == :counter
|
94
92
|
start = Time.now
|
95
93
|
begin
|
96
|
-
|
94
|
+
self.send without_profiling, *args, &orig
|
97
95
|
ensure
|
98
96
|
duration_ms = (Time.now - start).to_f * 1000
|
99
97
|
parent_timer.add_custom(name, duration_ms, Rack::MiniProfiler.current.page_struct )
|
100
98
|
end
|
101
99
|
else
|
102
|
-
page_struct = Rack::MiniProfiler.current.page_struct
|
103
|
-
|
104
100
|
Rack::MiniProfiler.current.current_timer = current_timer = parent_timer.add_child(name)
|
105
101
|
begin
|
106
|
-
|
102
|
+
self.send without_profiling, *args, &orig
|
107
103
|
ensure
|
108
104
|
current_timer.record_time
|
109
105
|
Rack::MiniProfiler.current.current_timer = parent_timer
|
110
106
|
end
|
111
107
|
end
|
112
|
-
|
113
|
-
result
|
114
108
|
end
|
115
109
|
klass.send :alias_method, method, with_profiling
|
116
110
|
end
|
117
111
|
|
112
|
+
def profile_singleton_method(klass, method, type = :profile, &blk)
|
113
|
+
profile_method(singleton_class(klass), method, type, &blk)
|
114
|
+
end
|
115
|
+
|
116
|
+
def unprofile_singleton_method(klass, method)
|
117
|
+
unprofile_method(singleton_class(klass), method)
|
118
|
+
end
|
119
|
+
|
118
120
|
# Add a custom timing. These are displayed similar to SQL/query time in
|
119
121
|
# columns expanding to the right.
|
120
122
|
#
|
@@ -140,6 +142,10 @@ module Rack
|
|
140
142
|
|
141
143
|
private
|
142
144
|
|
145
|
+
def singleton_class(klass)
|
146
|
+
class << klass; self; end
|
147
|
+
end
|
148
|
+
|
143
149
|
def clean_method_name(method)
|
144
150
|
method.to_s.gsub(/[\?\!]/, "")
|
145
151
|
end
|
@@ -26,8 +26,14 @@ module Rack
|
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
29
|
-
|
30
|
-
|
29
|
+
if RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin|bccwin/
|
30
|
+
def path(key)
|
31
|
+
@path + "/" + @prefix + "_" + key.gsub(/:/, '_')
|
32
|
+
end
|
33
|
+
else
|
34
|
+
def path(key)
|
35
|
+
@path + "/" + @prefix + "_" + key
|
36
|
+
end
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
@@ -38,6 +44,8 @@ module Rack
|
|
38
44
|
@path = args[:path]
|
39
45
|
@expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
|
40
46
|
raise ArgumentError.new :path unless @path
|
47
|
+
FileUtils.mkdir_p(@path) unless ::File.exists?(@path)
|
48
|
+
|
41
49
|
@timer_struct_cache = FileCache.new(@path, "mp_timers")
|
42
50
|
@timer_struct_lock = Mutex.new
|
43
51
|
@user_view_cache = FileCache.new(@path, "mp_views")
|
@@ -40,10 +40,18 @@ module Rack
|
|
40
40
|
self[:root] = TimerStruct::Request.createRoot(name, self)
|
41
41
|
end
|
42
42
|
|
43
|
+
def name
|
44
|
+
@attributes[:name]
|
45
|
+
end
|
46
|
+
|
43
47
|
def duration_ms
|
44
48
|
@attributes[:root][:duration_milliseconds]
|
45
49
|
end
|
46
50
|
|
51
|
+
def duration_ms_in_sql
|
52
|
+
@attributes[:duration_milliseconds_in_sql]
|
53
|
+
end
|
54
|
+
|
47
55
|
def root
|
48
56
|
@attributes[:root]
|
49
57
|
end
|
@@ -44,10 +44,18 @@ module Rack
|
|
44
44
|
@page = page
|
45
45
|
end
|
46
46
|
|
47
|
+
def name
|
48
|
+
@attributes[:name]
|
49
|
+
end
|
50
|
+
|
47
51
|
def duration_ms
|
48
52
|
self[:duration_milliseconds]
|
49
53
|
end
|
50
54
|
|
55
|
+
def duration_ms_in_sql
|
56
|
+
@attributes[:duration_milliseconds_in_sql]
|
57
|
+
end
|
58
|
+
|
51
59
|
def start_ms
|
52
60
|
self[:start_milliseconds]
|
53
61
|
end
|
@@ -17,11 +17,11 @@ module Rack
|
|
17
17
|
(
|
18
18
|
(
|
19
19
|
Rack::MiniProfiler.config.backtrace_includes.nil? or
|
20
|
-
Rack::MiniProfiler.config.backtrace_includes.
|
20
|
+
Rack::MiniProfiler.config.backtrace_includes.any?{|regex| ln =~ regex}
|
21
21
|
) and
|
22
22
|
(
|
23
23
|
Rack::MiniProfiler.config.backtrace_ignores.nil? or
|
24
|
-
Rack::MiniProfiler.config.backtrace_ignores.
|
24
|
+
Rack::MiniProfiler.config.backtrace_ignores.none?{|regex| ln =~ regex}
|
25
25
|
)
|
26
26
|
)
|
27
27
|
stack_trace << ln << "\n"
|
@@ -4,9 +4,9 @@ module Rack::MiniProfilerRails
|
|
4
4
|
|
5
5
|
# call direct if needed to do a defer init
|
6
6
|
def self.initialize!(app)
|
7
|
-
|
7
|
+
|
8
8
|
raise "MiniProfilerRails initialized twice. Set `require: false' for rack-mini-profiler in your Gemfile" if @already_initialized
|
9
|
-
|
9
|
+
|
10
10
|
c = Rack::MiniProfiler.config
|
11
11
|
|
12
12
|
# By default, only show the MiniProfiler in development mode.
|
@@ -24,8 +24,11 @@ module Rack::MiniProfilerRails
|
|
24
24
|
|
25
25
|
c.skip_paths ||= []
|
26
26
|
|
27
|
+
if serves_static_assets?(app)
|
28
|
+
c.skip_paths << app.config.assets.prefix
|
29
|
+
end
|
30
|
+
|
27
31
|
if Rails.env.development?
|
28
|
-
c.skip_paths << app.config.assets.prefix if app.respond_to? :assets
|
29
32
|
c.skip_schema_queries = true
|
30
33
|
end
|
31
34
|
|
@@ -40,7 +43,6 @@ module Rack::MiniProfilerRails
|
|
40
43
|
# The file store is just so much less flaky
|
41
44
|
base_path = Rails.application.config.paths['tmp'].first rescue "#{Rails.root}/tmp"
|
42
45
|
tmp = base_path + '/miniprofiler'
|
43
|
-
FileUtils.mkdir_p(tmp) unless File.exists?(tmp)
|
44
46
|
|
45
47
|
c.storage_options = {:path => tmp}
|
46
48
|
c.storage = Rack::MiniProfiler::FileStore
|
@@ -60,10 +62,26 @@ module Rack::MiniProfilerRails
|
|
60
62
|
ActiveSupport.on_load(:action_view) do
|
61
63
|
::Rack::MiniProfiler.profile_method(ActionView::Template, :render) {|x,y| "Rendering: #{@virtual_path}"}
|
62
64
|
end
|
63
|
-
|
65
|
+
|
64
66
|
@already_initialized = true
|
65
67
|
end
|
66
68
|
|
69
|
+
def self.serves_static_assets?(app)
|
70
|
+
config = app.config
|
71
|
+
|
72
|
+
if !config.respond_to?(:assets) || !config.assets.respond_to?(:prefix)
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
|
76
|
+
if ::Rails.version >= "5.0.0"
|
77
|
+
::Rails.configuration.public_file_server.enabled
|
78
|
+
elsif ::Rails.version >= "4.2.0"
|
79
|
+
::Rails.configuration.serve_static_files
|
80
|
+
else
|
81
|
+
::Rails.configuration.serve_static_assets
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
67
85
|
class Railtie < ::Rails::Railtie
|
68
86
|
|
69
87
|
initializer "rack_mini_profiler.configure_rails_initialization" do |app|
|
@@ -21,6 +21,8 @@ module Rack
|
|
21
21
|
rval = log_without_miniprofiler(*args, &block)
|
22
22
|
|
23
23
|
# Don't log schema queries if the option is set
|
24
|
+
# return rval unless sql =~ /\"vms\"/
|
25
|
+
# return rval unless sql =~ /\"(vms|ext_management_systems)\"/
|
24
26
|
return rval if Rack::MiniProfiler.config.skip_schema_queries and name =~ /SCHEMA/
|
25
27
|
|
26
28
|
elapsed_time = SqlPatches.elapsed_time(start)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Mongo/Mongoid 5 patches
|
2
|
+
class Mongo::Server::Connection
|
3
|
+
def dispatch_with_timing(*args, &blk)
|
4
|
+
return dispatch_without_timing(*args, &blk) unless SqlPatches.should_measure?
|
5
|
+
|
6
|
+
result, _record = SqlPatches.record_sql(args[0][0].payload.inspect) do
|
7
|
+
dispatch_without_timing(*args, &blk)
|
8
|
+
end
|
9
|
+
return result
|
10
|
+
end
|
11
|
+
|
12
|
+
# TODO: change to Module#prepend as soon as Ruby 1.9.3 support is dropped
|
13
|
+
alias_method :dispatch_without_timing, :dispatch
|
14
|
+
alias_method :dispatch, :dispatch_with_timing
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Neo4j::Core::Query
|
2
|
+
alias_method :response_without_miniprofiler, :response
|
3
|
+
|
4
|
+
def response
|
5
|
+
return @response if @response
|
6
|
+
start = Time.now
|
7
|
+
rval = response_without_miniprofiler
|
8
|
+
elapsed_time = SqlPatches.elapsed_time(start)
|
9
|
+
Rack::MiniProfiler.record_sql(to_cypher, elapsed_time)
|
10
|
+
rval
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :response_with_miniprofiler, :response
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Rack::MiniProfiler::NoBrainerProfiler
|
2
|
+
|
3
|
+
def on_query(env)
|
4
|
+
if SqlPatches.should_measure?
|
5
|
+
not_indexed = env[:criteria] && env[:criteria].where_present? &&
|
6
|
+
!env[:criteria].where_indexed? &&
|
7
|
+
!env[:criteria].model.try(:perf_warnings_disabled)
|
8
|
+
|
9
|
+
query = ""
|
10
|
+
|
11
|
+
# per-model/query database overrides
|
12
|
+
query << "[#{env[:options][:db]}] " if env[:options][:db]
|
13
|
+
|
14
|
+
# "read", "write" prefix
|
15
|
+
# query << "(#{NoBrainer::RQL.type_of(env[:query]).to_s}) "
|
16
|
+
|
17
|
+
query << "NOT USING INDEX: " if not_indexed
|
18
|
+
query << env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ') + " "
|
19
|
+
|
20
|
+
if env[:exception]
|
21
|
+
query << "exception: #{env[:exception].class} #{env[:exception].message.split("\n").first} "
|
22
|
+
end
|
23
|
+
|
24
|
+
::Rack::MiniProfiler.record_sql query.strip, env[:duration] * 1000.0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
NoBrainer::Profiler.register self.new
|
29
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class ActiveRecord::Result
|
2
|
+
alias_method :each_without_profiling, :each
|
3
|
+
def each(&blk)
|
4
|
+
return each_without_profiling(&blk) unless @miniprofiler_sql_id
|
5
|
+
|
6
|
+
start = Time.now
|
7
|
+
result = each_without_profiling(&blk)
|
8
|
+
elapsed_time = SqlPatches.elapsed_time(start)
|
9
|
+
@miniprofiler_sql_id.report_reader_duration(elapsed_time)
|
10
|
+
|
11
|
+
result
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter
|
16
|
+
SCHEMA_QUERY_TYPES = ["Sequence", "Primary Key", "Primary Key Trigger", nil].freeze
|
17
|
+
|
18
|
+
alias_method :execute_without_profiling, :execute
|
19
|
+
def execute(sql, name = nil)
|
20
|
+
mp_profile_sql(sql, name) { execute_without_profiling(sql, name) }
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :exec_query_without_profiling, :exec_query
|
24
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
25
|
+
mp_profile_sql(sql, name) { exec_query_without_profiling(sql, name, binds) }
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :exec_insert_without_profiling, :exec_insert
|
29
|
+
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
|
30
|
+
mp_profile_sql(sql, name) { exec_insert_without_profiling(sql, name, binds, pk, sequence_name) }
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :exec_update_without_profiling, :exec_update
|
34
|
+
def exec_update(sql, name, binds)
|
35
|
+
mp_profile_sql(sql, name) { exec_update_without_profiling(sql, name, binds) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# See oracle-enhanced/lib/active_record/connection_adapters/oracle_enhanced_database_statements.rb:183
|
39
|
+
# where the exec delete method is aliased in the same way. We just have to do it again here to make sure
|
40
|
+
# the new exec_delete alias is linked to our profiling-enabled version.
|
41
|
+
alias :exec_delete :exec_update
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def mp_profile_sql(sql, name, &blk)
|
46
|
+
return yield unless mp_should_measure?(name)
|
47
|
+
|
48
|
+
start = Time.now
|
49
|
+
result = yield
|
50
|
+
elapsed_time = SqlPatches.elapsed_time(start)
|
51
|
+
record = ::Rack::MiniProfiler.record_sql(sql, elapsed_time)
|
52
|
+
|
53
|
+
# Some queries return the row count as a Fixnum and will be frozen, don't save a record
|
54
|
+
# for those.
|
55
|
+
result.instance_variable_set("@miniprofiler_sql_id", record) if (result && !result.frozen?)
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
# Only measure when profiling is enabled
|
61
|
+
# When skip_schema_queries is set to true, it will ignore any query of the types
|
62
|
+
# in the schema_query_types array
|
63
|
+
def mp_should_measure?(name)
|
64
|
+
return false unless SqlPatches.should_measure?
|
65
|
+
|
66
|
+
!(Rack::MiniProfiler.config.skip_schema_queries && SCHEMA_QUERY_TYPES.include?(name))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
SqlPatches.patched = true
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# riak-client 2.2.2 patches
|
2
|
+
class Riak::Multiget
|
3
|
+
class <<self
|
4
|
+
alias_method :get_all_without_profiling, :get_all
|
5
|
+
def get_all(client, fetch_list)
|
6
|
+
return get_all_without_profiling(client, fetch_list) unless SqlPatches.should_measure?
|
7
|
+
|
8
|
+
start = Time.now
|
9
|
+
result = get_all_without_profiling(client, fetch_list)
|
10
|
+
elapsed_time = SqlPatches.elapsed_time(start)
|
11
|
+
record = ::Rack::MiniProfiler.record_sql("get_all size=#{fetch_list.size}", elapsed_time)
|
12
|
+
|
13
|
+
result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Riak::Client
|
19
|
+
|
20
|
+
alias_method :buckets_without_profiling, :buckets
|
21
|
+
def buckets(options={}, &blk)
|
22
|
+
profile("buckets #{options}") { buckets_without_profiling(options, &blk) }
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :client_id_without_profiling, :client_id
|
26
|
+
def client_id
|
27
|
+
profile("client_id") { client_id_without_profiling }
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :delete_object_without_profiling, :delete_object
|
31
|
+
def delete_object(bucket, key, options={})
|
32
|
+
profile("delete_object bucket=#{bucket.name} key=#{key} options=#{options}") { delete_object_without_profiling(bucket, key, options) }
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :get_bucket_props_without_profiling, :get_bucket_props
|
36
|
+
def get_bucket_props(bucket, options={})
|
37
|
+
profile("get_bucket_props bucket=#{bucket.name} options=#{options}") { get_bucket_props_without_profiling(bucket, options) }
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :get_index_without_profiling, :get_index
|
41
|
+
def get_index(bucket, index, query, options={})
|
42
|
+
profile("get_index bucket=#{bucket.name} index=#{index} query=#{query} options=#{options}") { get_index_without_profiling(bucket, index, query, options) }
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :get_preflist_without_profiling, :get_preflist
|
46
|
+
def get_preflist(bucket, key, type=nil, options={})
|
47
|
+
profile("get_preflist bucket=#{bucket.name} key=#{key} type=#{type} options=#{options}") { get_preflist_without_profiling(bucket, key, type, options) }
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :get_object_without_profiling, :get_object
|
51
|
+
def get_object(bucket, key, options={})
|
52
|
+
profile("get_object bucket=#{bucket.name} key=#{key} options=#{options}") { get_object_without_profiling(bucket, key, options) }
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :list_keys_without_profiling, :list_keys
|
56
|
+
def list_keys(bucket, options={}, &block)
|
57
|
+
profile("list_keys bucket=#{bucket.name} options=#{options}") { list_keys_without_profiling(bucket, options, &block) }
|
58
|
+
end
|
59
|
+
|
60
|
+
alias_method :mapred_without_profiling, :mapred
|
61
|
+
def mapred(mr, &block)
|
62
|
+
profile("mapred") { mapred_without_profiling(mr, &block) }
|
63
|
+
end
|
64
|
+
|
65
|
+
alias_method :ping_without_profiling, :ping
|
66
|
+
def ping
|
67
|
+
profile("ping") { ping_without_profiling }
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :reload_object_without_profiling, :reload_object
|
71
|
+
def reload_object(object, options={})
|
72
|
+
profile("reload_object bucket=#{object.bucket.name} key=#{object.key} vclock=#{object.vclock} options=#{options}") { reload_object_without_profiling(object, options) }
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :set_bucket_props_without_profiling, :set_bucket_props
|
76
|
+
def set_bucket_props(bucket, properties, type=nil)
|
77
|
+
profile("set_bucket_props bucket=#{bucket.name} type=#{type}") { set_bucket_props_without_profiling(bucket, properties, type) }
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :clear_bucket_props_without_profiling, :clear_bucket_props
|
81
|
+
def clear_bucket_props(bucket, options={})
|
82
|
+
profile("clear_bucket_props bucket=#{bucket.name} options=#{options}") { clear_bucket_props_without_profiling(bucket, options) }
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :store_object_without_profiling, :store_object
|
86
|
+
def store_object(object, options={})
|
87
|
+
profile("store_object bucket=#{object.bucket.name} key=#{object.key} vclock=#{object.vclock} options=#{options}") { store_object_without_profiling(object, options) }
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def profile(request, &blk)
|
93
|
+
return yield unless SqlPatches.should_measure?
|
94
|
+
|
95
|
+
start = Time.now
|
96
|
+
result = yield
|
97
|
+
elapsed_time = SqlPatches.elapsed_time(start)
|
98
|
+
record = ::Rack::MiniProfiler.record_sql(request, elapsed_time)
|
99
|
+
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|