acunote-ruby-prof 0.9.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.
Files changed (84) hide show
  1. data/CHANGES +240 -0
  2. data/LICENSE +23 -0
  3. data/README.rdoc +439 -0
  4. data/Rakefile +148 -0
  5. data/bin/ruby-prof +236 -0
  6. data/examples/empty.png +0 -0
  7. data/examples/flat.txt +55 -0
  8. data/examples/graph.dot +106 -0
  9. data/examples/graph.html +823 -0
  10. data/examples/graph.png +0 -0
  11. data/examples/graph.txt +170 -0
  12. data/examples/minus.png +0 -0
  13. data/examples/multi.flat.txt +23 -0
  14. data/examples/multi.graph.html +906 -0
  15. data/examples/multi.grind.dat +194 -0
  16. data/examples/multi.stack.html +573 -0
  17. data/examples/plus.png +0 -0
  18. data/examples/stack.html +573 -0
  19. data/ext/ruby_prof/extconf.rb +43 -0
  20. data/ext/ruby_prof/measure_allocations.h +58 -0
  21. data/ext/ruby_prof/measure_cpu_time.h +152 -0
  22. data/ext/ruby_prof/measure_gc_runs.h +76 -0
  23. data/ext/ruby_prof/measure_gc_time.h +57 -0
  24. data/ext/ruby_prof/measure_memory.h +101 -0
  25. data/ext/ruby_prof/measure_process_time.h +52 -0
  26. data/ext/ruby_prof/measure_wall_time.h +53 -0
  27. data/ext/ruby_prof/mingw/Rakefile +23 -0
  28. data/ext/ruby_prof/mingw/build.rake +38 -0
  29. data/ext/ruby_prof/ruby_prof.c +1834 -0
  30. data/ext/ruby_prof/ruby_prof.h +190 -0
  31. data/ext/ruby_prof/version.h +4 -0
  32. data/lib/ruby-prof.rb +62 -0
  33. data/lib/ruby-prof/abstract_printer.rb +41 -0
  34. data/lib/ruby-prof/aggregate_call_info.rb +68 -0
  35. data/lib/ruby-prof/call_info.rb +112 -0
  36. data/lib/ruby-prof/call_stack_printer.rb +751 -0
  37. data/lib/ruby-prof/call_tree_printer.rb +133 -0
  38. data/lib/ruby-prof/dot_printer.rb +153 -0
  39. data/lib/ruby-prof/empty.png +0 -0
  40. data/lib/ruby-prof/flat_printer.rb +78 -0
  41. data/lib/ruby-prof/flat_printer_with_line_numbers.rb +72 -0
  42. data/lib/ruby-prof/graph_html_printer.rb +278 -0
  43. data/lib/ruby-prof/graph_printer.rb +245 -0
  44. data/lib/ruby-prof/method_info.rb +131 -0
  45. data/lib/ruby-prof/minus.png +0 -0
  46. data/lib/ruby-prof/multi_printer.rb +54 -0
  47. data/lib/ruby-prof/plus.png +0 -0
  48. data/lib/ruby-prof/rack.rb +30 -0
  49. data/lib/ruby-prof/result.rb +70 -0
  50. data/lib/ruby-prof/symbol_to_proc.rb +8 -0
  51. data/lib/ruby-prof/task.rb +146 -0
  52. data/lib/ruby-prof/test.rb +148 -0
  53. data/lib/unprof.rb +8 -0
  54. data/rails/environment/profile.rb +24 -0
  55. data/rails/example/example_test.rb +9 -0
  56. data/rails/profile_test_helper.rb +21 -0
  57. data/test/aggregate_test.rb +136 -0
  58. data/test/basic_test.rb +290 -0
  59. data/test/current_failures_windows +8 -0
  60. data/test/do_nothing.rb +0 -0
  61. data/test/duplicate_names_test.rb +32 -0
  62. data/test/enumerable_test.rb +16 -0
  63. data/test/exceptions_test.rb +15 -0
  64. data/test/exclude_threads_test.rb +54 -0
  65. data/test/exec_test.rb +14 -0
  66. data/test/line_number_test.rb +73 -0
  67. data/test/measurement_test.rb +122 -0
  68. data/test/method_elimination_test.rb +74 -0
  69. data/test/module_test.rb +44 -0
  70. data/test/multi_printer_test.rb +81 -0
  71. data/test/no_method_class_test.rb +13 -0
  72. data/test/prime.rb +55 -0
  73. data/test/prime_test.rb +13 -0
  74. data/test/printers_test.rb +164 -0
  75. data/test/recursive_test.rb +236 -0
  76. data/test/ruby-prof-bin +20 -0
  77. data/test/singleton_test.rb +38 -0
  78. data/test/stack_printer_test.rb +74 -0
  79. data/test/stack_test.rb +138 -0
  80. data/test/start_stop_test.rb +112 -0
  81. data/test/test_suite.rb +32 -0
  82. data/test/thread_test.rb +173 -0
  83. data/test/unique_call_path_test.rb +225 -0
  84. metadata +185 -0
@@ -0,0 +1,70 @@
1
+ require 'set'
2
+ module RubyProf
3
+ class Result
4
+ # this method gets called internally when profiling is stopped.
5
+ # it determines for each call_info whether it is minimal: a
6
+ # call_info is minimal in a call tree if the call_info is not a
7
+ # descendant of a call_info of the same method
8
+ def compute_minimality
9
+ threads.each do |threadid, method_infos|
10
+ root_methods = method_infos.select{|mi| mi.root?}
11
+ root_methods.each do |mi|
12
+ mi.call_infos.select{|ci| ci.root?}.each do |call_info_root|
13
+ call_info_root.compute_minimality(Set.new)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # eliminate some calls from the graph by merging the information into callers.
20
+ # matchers can be a list of strings or regular expressions or the name of a file containing regexps.
21
+ def eliminate_methods!(matchers)
22
+ matchers = read_regexps_from_file(matchers) if matchers.is_a?(String)
23
+ eliminated = []
24
+ threads.each do |thread_id, methods|
25
+ matchers.each{ |matcher| eliminated.concat(eliminate_methods(methods, matcher)) }
26
+ end
27
+ compute_minimality # is this really necessary?
28
+ eliminated
29
+ end
30
+
31
+ def dump
32
+ threads.each do |thread_id, methods|
33
+ $stderr.puts "Call Info Dump for thread id #{thread_id}"
34
+ methods.each do |method_info|
35
+ $stderr.puts method_info.dump
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # read regexps from file
43
+ def read_regexps_from_file(file_name)
44
+ matchers = []
45
+ File.open(matchers).each_line do |l|
46
+ next if (l =~ /^(#.*|\s*)$/) # emtpy lines and lines starting with #
47
+ matchers << Regexp.new(l.strip)
48
+ end
49
+ end
50
+
51
+ # eliminate methods matching matcher
52
+ def eliminate_methods(methods, matcher)
53
+ eliminated = []
54
+ i = 0
55
+ while i < methods.size
56
+ method_info = methods[i]
57
+ method_name = method_info.full_name
58
+ if matcher === method_name
59
+ raise "can't eliminate root method" if method_info.root?
60
+ eliminated << methods.delete_at(i)
61
+ method_info.eliminate!
62
+ else
63
+ i += 1
64
+ end
65
+ end
66
+ eliminated
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,8 @@
1
+ unless (:a.respond_to?(:to_proc))
2
+ class Symbol
3
+ def to_proc
4
+ proc {|stuff| stuff.send(self)}
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'fileutils'
6
+
7
+ module RubyProf
8
+
9
+ # Define a task library for profiling unit tests with ruby-prof.
10
+ #
11
+ # All of the options provided by
12
+ # the Rake:TestTask are supported except the loader
13
+ # which is set to ruby-prof. For detailed information
14
+ # please refer to the Rake:TestTask documentation.
15
+ #
16
+ # ruby-prof specific options include:
17
+ #
18
+ # output_dir - For each file specified an output
19
+ # file with profile information will be
20
+ # written to the output directory.
21
+ # By default, the output directory is
22
+ # called "profile" and is created underneath
23
+ # the current working directory.
24
+ #
25
+ # printer - Specifies the output printer. Valid values include
26
+ # :flat, :graph, :graph_html and :call_tree.
27
+ #
28
+ # min_percent - Methods that take less than the specified percent
29
+ # will not be written out.
30
+ #
31
+ # Example:
32
+ #
33
+ # require 'ruby-prof/task'
34
+ #
35
+ # RubyProf::ProfileTask.new do |t|
36
+ # t.test_files = FileList['test/test*.rb']
37
+ # t.output_dir = "c:/temp"
38
+ # t.printer = :graph
39
+ # t.min_percent = 10
40
+ # end
41
+ #
42
+ # If rake is invoked with a "TEST=filename" command line option,
43
+ # then the list of test files will be overridden to include only the
44
+ # filename specified on the command line. This provides an easy way
45
+ # to run just one test.
46
+ #
47
+ # If rake is invoked with a "TESTOPTS=options" command line option,
48
+ # then the given options are passed to the test process after a
49
+ # '--'. This allows Test::Unit options to be passed to the test
50
+ # suite.
51
+ #
52
+ # Examples:
53
+ #
54
+ # rake profile # run tests normally
55
+ # rake profile TEST=just_one_file.rb # run just one test file.
56
+ # rake profile TESTOPTS="-v" # run in verbose mode
57
+ # rake profile TESTOPTS="--runner=fox" # use the fox test runner
58
+
59
+ class ProfileTask < Rake::TestTask
60
+ attr_accessor :output_dir
61
+ attr_accessor :min_percent
62
+ attr_accessor :printer
63
+
64
+ def initialize(name = :profile)
65
+ super(name)
66
+ end
67
+
68
+ # Create the tasks defined by this task lib.
69
+ def define
70
+ lib_path = @libs.join(File::PATH_SEPARATOR)
71
+ desc "Profile" + (@name==:profile ? "" : " for #{@name}")
72
+
73
+ task @name do
74
+ create_output_directory
75
+
76
+ @ruby_opts.unshift( "-I#{lib_path}" )
77
+ @ruby_opts.unshift( "-w" ) if @warning
78
+ @ruby_opts.push("-S ruby-prof")
79
+ @ruby_opts.push("--printer #{@printer}")
80
+ @ruby_opts.push("--min_percent #{@min_percent}")
81
+
82
+ file_list.each do |file_path|
83
+ run_script(file_path)
84
+ end
85
+ end
86
+ self
87
+ end
88
+
89
+ # Run script
90
+ def run_script(script_path)
91
+ run_code = ''
92
+ RakeFileUtils.verbose(@verbose) do
93
+ file_name = File.basename(script_path, File.extname(script_path))
94
+ case @printer
95
+ when :flat, :graph, :call_tree
96
+ file_name += ".txt"
97
+ when :graph_html
98
+ file_name += ".html"
99
+ else
100
+ file_name += ".txt"
101
+ end
102
+
103
+ output_file_path = File.join(output_directory, file_name)
104
+
105
+ command_line = @ruby_opts.join(" ") +
106
+ " --file=" + output_file_path +
107
+ " " + script_path
108
+
109
+ puts "ruby " + command_line
110
+ # We have to catch the exeption to continue on. However,
111
+ # the error message will have been output to STDERR
112
+ # already by the time we get here so we don't have to
113
+ # do that again
114
+ begin
115
+ ruby command_line
116
+ rescue => e
117
+ STDOUT << e << "\n"
118
+ STDOUT.flush
119
+ end
120
+ puts ""
121
+ puts ""
122
+ end
123
+ end
124
+
125
+ def output_directory
126
+ File.expand_path(@output_dir)
127
+ end
128
+
129
+ def create_output_directory
130
+ if not File.exist?(output_directory)
131
+ Dir.mkdir(output_directory)
132
+ end
133
+ end
134
+
135
+ def clean_output_directory
136
+ if File.exist?(output_directory)
137
+ files = Dir.glob(output_directory + '/*')
138
+ FileUtils.rm(files)
139
+ end
140
+ end
141
+
142
+ def option_list # :nodoc:
143
+ ENV['OPTIONS'] || @options.join(" ") || ""
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,148 @@
1
+ # Now load ruby-prof and away we go
2
+ require 'fileutils'
3
+ require 'ruby-prof'
4
+ require 'benchmark'
5
+
6
+ module RubyProf
7
+ module Test
8
+ PROFILE_OPTIONS = {
9
+ :measure_modes => [RubyProf::PROCESS_TIME],
10
+ :count => 10,
11
+ :printers => [RubyProf::FlatPrinter, RubyProf::GraphHtmlPrinter],
12
+ :min_percent => 0.05,
13
+ :output_dir => Dir.pwd }
14
+
15
+ def output_dir
16
+ PROFILE_OPTIONS[:output_dir]
17
+ end
18
+
19
+ def run(result)
20
+ return if @method_name.to_s == "default_test"
21
+
22
+ yield(self.class::STARTED, name)
23
+ @_result = result
24
+ run_warmup
25
+ PROFILE_OPTIONS[:measure_modes].each do |measure_mode|
26
+ data = run_profile(measure_mode)
27
+ report_profile(data, measure_mode)
28
+ result.add_run
29
+ end
30
+ yield(self.class::FINISHED, name)
31
+ end
32
+
33
+ def run_test
34
+ begin
35
+ setup
36
+ yield
37
+ rescue ::Test::Unit::AssertionFailedError => e
38
+ add_failure(e.message, e.backtrace)
39
+ rescue StandardError, ScriptError
40
+ add_error($!)
41
+ ensure
42
+ begin
43
+ teardown
44
+ rescue ::Test::Unit::AssertionFailedError => e
45
+ add_failure(e.message, e.backtrace)
46
+ rescue StandardError, ScriptError
47
+ add_error($!)
48
+ end
49
+ end
50
+ end
51
+
52
+ def run_warmup
53
+ print "\n#{self.class.name}##{method_name}"
54
+
55
+ run_test do
56
+ bench = Benchmark.realtime do
57
+ __send__(@method_name)
58
+ end
59
+ puts " (%.2fs warmup)" % bench
60
+ end
61
+ end
62
+
63
+ def run_profile(measure_mode)
64
+ RubyProf.measure_mode = measure_mode
65
+
66
+ print ' '
67
+ PROFILE_OPTIONS[:count].times do |i|
68
+ run_test do
69
+ begin
70
+ print '.'
71
+ $stdout.flush
72
+ GC.disable
73
+
74
+ RubyProf.resume do
75
+ __send__(@method_name)
76
+ end
77
+ ensure
78
+ GC.enable
79
+ end
80
+ end
81
+ end
82
+
83
+ data = RubyProf.stop
84
+ bench = data.threads.values.inject(0) do |total, method_infos|
85
+ top = method_infos.max
86
+ total += top.total_time
87
+ total
88
+ end
89
+
90
+ puts "\n #{measure_mode_name(measure_mode)}: #{format_profile_total(bench, measure_mode)}\n"
91
+
92
+ data
93
+ end
94
+
95
+ def format_profile_total(total, measure_mode)
96
+ case measure_mode
97
+ when RubyProf::PROCESS_TIME, RubyProf::WALL_TIME
98
+ "%.2f seconds" % total
99
+ when RubyProf::MEMORY
100
+ "%.2f kilobytes" % total
101
+ when RubyProf::ALLOCATIONS
102
+ "%d allocations" % total
103
+ else
104
+ "%.2f #{measure_mode}"
105
+ end
106
+ end
107
+
108
+ def report_profile(data, measure_mode)
109
+ PROFILE_OPTIONS[:printers].each do |printer_klass|
110
+ printer = printer_klass.new(data)
111
+
112
+ # Makes sure the output directory exits
113
+ FileUtils.mkdir_p(output_dir)
114
+
115
+ # Open the file
116
+ file_name = report_filename(printer, measure_mode)
117
+
118
+ File.open(file_name, 'wb') do |file|
119
+ printer.print(file, PROFILE_OPTIONS)
120
+ end
121
+ end
122
+ end
123
+
124
+ # The report filename is test_name + measure_mode + report_type
125
+ def report_filename(printer, measure_mode)
126
+ suffix =
127
+ case printer
128
+ when RubyProf::FlatPrinter; 'flat.txt'
129
+ when RubyProf::GraphPrinter; 'graph.txt'
130
+ when RubyProf::GraphHtmlPrinter; 'graph.html'
131
+ when RubyProf::CallTreePrinter; 'tree.txt'
132
+ else printer.to_s.downcase
133
+ end
134
+
135
+ "#{output_dir}/#{method_name}_#{measure_mode_name(measure_mode)}_#{suffix}"
136
+ end
137
+
138
+ def measure_mode_name(measure_mode)
139
+ case measure_mode
140
+ when RubyProf::PROCESS_TIME; 'process_time'
141
+ when RubyProf::WALL_TIME; 'wall_time'
142
+ when RubyProf::MEMORY; 'memory'
143
+ when RubyProf::ALLOCATIONS; 'allocations'
144
+ else "measure#{measure_mode}"
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,8 @@
1
+ require "ruby-prof"
2
+
3
+ at_exit {
4
+ result = RubyProf.stop
5
+ printer = RubyProf::FlatPrinter.new(result)
6
+ printer.print(STDOUT)
7
+ }
8
+ RubyProf.start
@@ -0,0 +1,24 @@
1
+ # Settings specified here will take precedence over those in config/environment.rb
2
+ # The profile environment should match the same settings
3
+ # as the production environment to give a reasonalbe
4
+ # approximation of performance. However, it should
5
+ # definitely not use the production databse!
6
+
7
+
8
+ # Cache classes - otherwise your code
9
+ # will run approximately 5 times slower and the
10
+ # profiling results will be overwhelmed by Rails
11
+ # dependency loading mechanism
12
+ config.cache_classes = true
13
+
14
+ # Don't check template timestamps - once again this
15
+ # is to avoid IO times overwhelming profile results
16
+ config.action_view.cache_template_loading = true
17
+
18
+ # This is debatable, but turn off action controller
19
+ # caching to see how long it really takes to run
20
+ # queries and render templates
21
+ config.action_controller.perform_caching = false
22
+
23
+ # Turn off most logging
24
+ config.log_level = :info
@@ -0,0 +1,9 @@
1
+ require File.dirname(__FILE__) + '../profile_test_helper'
2
+
3
+ class ExampleTest < Test::Unit::TestCase
4
+ include RubyProf::Test
5
+
6
+ def test_stuff
7
+ puts "Test method"
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # Load profile environment
2
+ env = ENV["RAILS_ENV"] = "profile"
3
+ require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
4
+
5
+ # Load Rails testing infrastructure
6
+ require 'test_help'
7
+
8
+ # Now we can load test_helper since we've already loaded the
9
+ # profile RAILS environment.
10
+ require File.expand_path(File.join(RAILS_ROOT, 'test', 'test_helper'))
11
+
12
+ # Reset the current environment back to profile
13
+ # since test_helper reset it to test
14
+ ENV["RAILS_ENV"] = env
15
+
16
+ # Now load ruby-prof and away we go
17
+ require 'ruby-prof'
18
+
19
+ # Setup output directory to Rails tmp directory
20
+ RubyProf::Test::PROFILE_OPTIONS[:output_dir] =
21
+ File.expand_path(File.join(RAILS_ROOT, 'tmp', 'profile'))
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'test/unit'
4
+ require 'ruby-prof'
5
+
6
+ # Test data
7
+ # A B C
8
+ # | | |
9
+ # Z A A
10
+ # | |
11
+ # Z Z
12
+
13
+ class AggClass
14
+ def z
15
+ sleep 1
16
+ end
17
+
18
+ def a
19
+ z
20
+ end
21
+
22
+ def b
23
+ a
24
+ end
25
+
26
+ def c
27
+ a
28
+ end
29
+ end
30
+
31
+ class AggregateTest < Test::Unit::TestCase
32
+ def setup
33
+ # Need to use wall time for this test due to the sleep calls
34
+ RubyProf::measure_mode = RubyProf::WALL_TIME
35
+ end
36
+
37
+ def test_all_call_infos_are_minimal_as_there_is_no_recursion
38
+ c1 = AggClass.new
39
+ result = RubyProf.profile do
40
+ c1.a
41
+ c1.b
42
+ c1.c
43
+ end
44
+ methods = result.threads.values.first.sort.reverse
45
+ methods.each do |m|
46
+ m.call_infos.each do |ci|
47
+ assert ci.minimal?, "#{ci.call_sequence} should be minimal in the call tree"
48
+ end
49
+ end
50
+ end
51
+
52
+ def test_call_infos
53
+ c1 = AggClass.new
54
+ result = RubyProf.profile do
55
+ c1.a
56
+ c1.b
57
+ c1.c
58
+ end
59
+
60
+ methods = result.threads.values.first.sort.reverse
61
+ method = methods.find {|meth| meth.full_name == 'AggClass#z'}
62
+
63
+ # Check AggClass#z
64
+ assert_equal('AggClass#z', method.full_name)
65
+ assert_equal(3, method.called)
66
+ assert_in_delta(3, method.total_time, 0.01)
67
+ assert_in_delta(0, method.wait_time, 0.01)
68
+ assert_in_delta(0, method.self_time, 0.01)
69
+ assert_in_delta(3, method.children_time, 0.01)
70
+ assert_equal(3, method.call_infos.length)
71
+
72
+ call_info = method.call_infos[0]
73
+ assert_equal('AggregateTest#test_call_infos->AggClass#a->AggClass#z', call_info.call_sequence)
74
+ assert_equal(1, call_info.children.length)
75
+
76
+ call_info = method.call_infos[1]
77
+ assert_equal('AggregateTest#test_call_infos->AggClass#b->AggClass#a->AggClass#z', call_info.call_sequence)
78
+ assert_equal(1, call_info.children.length)
79
+
80
+ call_info = method.call_infos[2]
81
+ assert_equal('AggregateTest#test_call_infos->AggClass#c->AggClass#a->AggClass#z', call_info.call_sequence)
82
+ assert_equal(1, call_info.children.length)
83
+ end
84
+
85
+ def test_aggregates_parents
86
+ c1 = AggClass.new
87
+ result = RubyProf.profile do
88
+ c1.a
89
+ c1.b
90
+ c1.c
91
+ end
92
+
93
+ methods = result.threads.values.first.sort.reverse
94
+ method = methods.find {|meth| meth.full_name == 'AggClass#z'}
95
+
96
+ # Check AggClass#z
97
+ assert_equal('AggClass#z', method.full_name)
98
+
99
+ call_infos = method.aggregate_parents
100
+ assert_equal(1, call_infos.length)
101
+
102
+ call_info = call_infos.first
103
+ assert_equal('AggClass#a', call_info.parent.target.full_name)
104
+ assert_in_delta(3, call_info.total_time, 0.05)
105
+ assert_in_delta(0, call_info.wait_time, 0.01)
106
+ assert_in_delta(0, call_info.self_time, 0.05)
107
+ assert_in_delta(3, call_info.children_time, 0.05)
108
+ assert_equal(3, call_info.called)
109
+ end
110
+
111
+ def test_aggregates_children
112
+ c1 = AggClass.new
113
+ result = RubyProf.profile do
114
+ c1.a
115
+ c1.b
116
+ c1.c
117
+ end
118
+
119
+ methods = result.threads.values.first.sort.reverse
120
+ method = methods.find {|meth| meth.full_name == 'AggClass#a'}
121
+
122
+ # Check AggClass#a
123
+ assert_equal('AggClass#a', method.full_name)
124
+
125
+ call_infos = method.aggregate_children
126
+ assert_equal(1, call_infos.length)
127
+
128
+ call_info = call_infos.first
129
+ assert_equal('AggClass#z', call_info.target.full_name)
130
+ assert_in_delta(3, call_info.total_time, 0.05)
131
+ assert_in_delta(0, call_info.wait_time, 0.01)
132
+ assert_in_delta(0, call_info.self_time, 0.05)
133
+ assert_in_delta(3, call_info.children_time, 0.05)
134
+ assert_equal(3, call_info.called)
135
+ end
136
+ end