11# frozen_string_literal: true
22
33require 'English'
4+ require_relative './hook_log'
45
56module AppMap
67 class Hook
7- LOG = ( ENV [ 'APPMAP_DEBUG' ] == 'true' || ENV [ 'DEBUG' ] == 'true' )
8- LOG_HOOK = ( ENV [ 'DEBUG_HOOK' ] == 'true' )
9-
108 OBJECT_INSTANCE_METHODS = %i[ ! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup
119 enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self ] . freeze
1210 OBJECT_STATIC_METHODS = %i[ ! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr
1311 attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self ] . freeze
14- SLOW_PACKAGE_THRESHOLD = 0.05
12+ SLOW_PACKAGE_THRESHOLD = 0.001
1513
1614 @unbound_method_arity = ::UnboundMethod . instance_method ( :arity )
1715 @method_arity = ::Method . instance_method ( :arity )
@@ -41,9 +39,9 @@ def already_hooked?(method)
4139 def qualify_method_name ( method )
4240 if method . owner . singleton_class?
4341 class_name = singleton_method_owner_name ( method )
44- [ class_name , '.' , method . name ]
42+ [ class_name , '.' , method . name ]
4543 else
46- [ method . owner . name , '#' , method . name ]
44+ [ method . owner . name , '#' , method . name ]
4745 end
4846 end
4947 end
@@ -69,19 +67,23 @@ def enable(&block)
6967 @slow_packages = Set . new
7068
7169 if ENV [ 'APPMAP_PROFILE_HOOK' ] == 'true'
70+ dump_times = lambda do
71+ @module_load_times
72+ . keys
73+ . select { |key | !@slow_packages . member? ( key ) }
74+ . each do |key |
75+ elapsed = @module_load_times [ key ]
76+ if elapsed >= SLOW_PACKAGE_THRESHOLD
77+ @slow_packages . add ( key )
78+ warn "AppMap: Package #{ key } took #{ @module_load_times [ key ] } seconds to hook"
79+ end
80+ end
81+ end
82+
83+ at_exit &dump_times
7284 Thread . new do
73- sleep 1
7485 while true
75- @module_load_times
76- . keys
77- . select { |key | !@slow_packages . member? ( key ) }
78- . each do |key |
79- elapsed = @module_load_times [ key ]
80- if elapsed >= SLOW_PACKAGE_THRESHOLD
81- @slow_packages . add ( key )
82- warn "AppMap: Package #{ key } took #{ @module_load_times [ key ] } seconds to hook"
83- end
84- end
86+ dump_times . call
8587 sleep 5
8688 end
8789 end
@@ -106,14 +108,14 @@ def hook_builtins
106108 Array ( hook . method_names ) . each do |method_name |
107109 method_name = method_name . to_sym
108110
109- warn "AppMap: Initiating hook for builtin #{ class_name } #{ method_name } " if LOG
110-
111111 begin
112112 base_cls = Object . const_get class_name
113113 rescue NameError
114114 next
115115 end
116116
117+ HookLog . builtin_begin class_name , method_name if HookLog . enabled?
118+
117119 hook_method = lambda do |entry |
118120 cls , method = entry
119121 next if config . never_hook? ( cls , method )
@@ -125,20 +127,22 @@ def hook_builtins
125127 # irb(main):001:0> Kernel.public_instance_method(:system)
126128 # (irb):1:in `public_instance_method': method `system' for module `Kernel' is private (NameError)
127129 if base_cls == Kernel
128- methods << [ base_cls , base_cls . instance_method ( method_name ) ] rescue nil
130+ methods << [ base_cls , base_cls . instance_method ( method_name ) ] rescue nil
129131 end
130- methods << [ base_cls , base_cls . public_instance_method ( method_name ) ] rescue nil
131- methods << [ base_cls , base_cls . protected_instance_method ( method_name ) ] rescue nil
132+ methods << [ base_cls , base_cls . public_instance_method ( method_name ) ] rescue nil
133+ methods << [ base_cls , base_cls . protected_instance_method ( method_name ) ] rescue nil
132134 if base_cls . respond_to? ( :singleton_class )
133- methods << [ base_cls . singleton_class , base_cls . singleton_class . public_instance_method ( method_name ) ] rescue nil
134- methods << [ base_cls . singleton_class , base_cls . singleton_class . protected_instance_method ( method_name ) ] rescue nil
135+ methods << [ base_cls . singleton_class , base_cls . singleton_class . public_instance_method ( method_name ) ] rescue nil
136+ methods << [ base_cls . singleton_class , base_cls . singleton_class . protected_instance_method ( method_name ) ] rescue nil
135137 end
136138 methods . compact!
137139 if methods . empty?
138- warn "Method #{ method_name } not found on #{ base_cls . name } " if LOG
140+ HookLog . log "Method #{ method_name } not found on #{ base_cls . name } " if HookLog . enabled?
139141 else
140142 methods . each ( &hook_method )
141143 end
144+
145+ HookLog . builtin_end class_name , method_name if HookLog . enabled?
142146 end
143147 end
144148 end
@@ -151,83 +155,93 @@ def hook_builtins
151155 protected
152156
153157 def trace_location ( trace_point )
154- [ trace_point . path , trace_point . lineno ] . join ( ':' )
158+ [ trace_point . path , trace_point . lineno ] . join ( ':' )
155159 end
156160
157161 def trace_end ( trace_point )
158162 location = trace_location ( trace_point )
159- warn "Class or module ends at location #{ location } " if Hook ::LOG || Hook ::LOG_HOOK
160- return unless @trace_locations . add? ( location )
161-
162- path = trace_point . path
163- enabled = !@notrace_paths . member? ( path ) && config . path_enabled? ( path )
164- unless enabled
165- warn 'Not hooking - path is not enabled' if Hook ::LOG || Hook ::LOG_HOOK
166- @notrace_paths << path
167- return
168- end
163+ begin
164+ HookLog . usercode_begin location if HookLog . enabled?
169165
170- cls = trace_point . self
166+ return unless @trace_locations . add? ( location )
171167
172- instance_methods = cls . public_instance_methods ( false ) + cls . protected_instance_methods ( false ) - OBJECT_INSTANCE_METHODS
173- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
174- class_methods = begin
175- if cls . respond_to? ( :singleton_class )
176- cls . singleton_class . public_instance_methods ( false ) + cls . singleton_class . protected_instance_methods ( false ) - instance_methods - OBJECT_STATIC_METHODS
177- else
178- [ ]
168+ path = trace_point . path
169+ enabled = !@notrace_paths . member? ( path ) && config . path_enabled? ( path )
170+ unless enabled
171+ HookLog . log 'Not hooking - path is not enabled' if HookLog . enabled?
172+ @notrace_paths << path
173+ return
179174 end
180- rescue NameError
181- [ ]
182- end
183175
184- hook = lambda do |hook_cls |
185- lambda do |method_id |
186- method = \
187- begin
188- hook_cls . instance_method ( method_id )
189- rescue NameError
190- warn "AppMap: Method #{ hook_cls } #{ fn } is not accessible: #{ $!} " if LOG
191- next
176+ cls = trace_point . self
177+
178+ instance_methods = cls . public_instance_methods ( false ) + cls . protected_instance_methods ( false ) - OBJECT_INSTANCE_METHODS
179+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
180+ class_methods = begin
181+ if cls . respond_to? ( :singleton_class )
182+ cls . singleton_class . public_instance_methods ( false ) + cls . singleton_class . protected_instance_methods ( false ) - instance_methods - OBJECT_STATIC_METHODS
183+ else
184+ [ ]
192185 end
186+ rescue NameError
187+ [ ]
188+ end
189+
190+ hook = lambda do |hook_cls |
191+ lambda do |method_id |
192+ method = begin
193+ hook_cls . instance_method ( method_id )
194+ rescue NameError
195+ HookLog . log "Method #{ hook_cls } #{ fn } is not accessible: #{ $!} " if HookLog . enabled?
196+ next
197+ end
193198
194- package = config . lookup_package ( hook_cls , method )
195- # doing this check first returned early in 98.7% of cases in sample_app_6th_ed
196- next unless package
199+ package = config . lookup_package ( hook_cls , method )
200+ # doing this check first returned early in 98.7% of cases in sample_app_6th_ed
201+ next unless package
197202
198- # Don't try and trace the AppMap methods or there will be
199- # a stack overflow in the defined hook method.
200- next if %w[ Marshal AppMap ActiveSupport ] . member? ( ( hook_cls &.name || '' ) . split ( '::' ) [ 0 ] )
203+ # Don't try and trace the AppMap methods or there will be
204+ # a stack overflow in the defined hook method.
205+ next if %w[ Marshal AppMap ActiveSupport ] . member? ( ( hook_cls &.name || '' ) . split ( '::' ) [ 0 ] )
201206
202- next if method_id == :call
207+ next if method_id == :call
203208
204- next if self . class . already_hooked? ( method )
209+ next if self . class . already_hooked? ( method )
205210
206- warn "AppMap: Examining #{ hook_cls } #{ method . name } " if LOG
211+ HookLog . log " Examining #{ hook_cls } #{ method . name } " if HookLog . enabled?
207212
208- disasm = RubyVM ::InstructionSequence . disasm ( method )
209- # Skip methods that have no instruction sequence, as they are either have no body or they are or native.
210- # TODO: Figure out how to tell the difference?
211- next unless disasm
213+ disasm = RubyVM ::InstructionSequence . disasm ( method )
214+ # Skip methods that have no instruction sequence, as they are either have no body or they are or native.
215+ # TODO: Figure out how to tell the difference?
216+ next unless disasm
212217
213- package . handler_class . new ( package , hook_cls , method ) . activate
218+ package . handler_class . new ( package , hook_cls , method ) . activate
219+ end
214220 end
215- end
216221
217- start = Time . now
218- instance_methods . each ( &hook . ( cls ) )
219- begin
220- # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
221- class_methods . each ( &hook . ( cls . singleton_class ) ) if cls . respond_to? ( :singleton_class )
222- rescue NameError
223- # NameError:
224- # uninitialized constant Faraday::Connection
225- warn "NameError in #{ __FILE__ } : #{ $!. message } "
226- end
227- elapsed = Time . now - start
228- if location . index ( Bundler . bundle_path . to_s ) == 0
229- package_tokens = location [ Bundler . bundle_path . to_s . length + 1 ..-1 ] . split ( '/' )
230- @module_load_times [ package_tokens [ 1 ] ] += elapsed
222+ start = Time . now
223+ instance_methods . each ( &hook . ( cls ) )
224+ begin
225+ # NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
226+ class_methods . each ( &hook . ( cls . singleton_class ) ) if cls . respond_to? ( :singleton_class )
227+ rescue NameError
228+ # NameError:
229+ # uninitialized constant Faraday::Connection
230+ warn "NameError in #{ __FILE__ } : #{ $!. message } "
231+ end
232+ elapsed = Time . now - start
233+ if location . index ( Bundler . bundle_path . to_s ) == 0
234+ package_tokens = location [ Bundler . bundle_path . to_s . length + 1 ..-1 ] . split ( '/' )
235+ @module_load_times [ package_tokens [ 1 ] ] += elapsed
236+ else
237+ file_path = location [ Dir . pwd . length + 1 ..-1 ]
238+ if file_path
239+ location = file_path . split ( '/' , 3 ) [ 0 ..1 ] . join ( '/' )
240+ @module_load_times [ location ] += elapsed
241+ end
242+ end
243+ ensure
244+ HookLog . usercode_end ( location ) if HookLog . enabled?
231245 end
232246 end
233247 end
0 commit comments