1
1
# frozen_string_literal: true
2
2
3
3
require 'English'
4
+ require_relative './hook_log'
4
5
5
6
module AppMap
6
7
class Hook
7
- LOG = ( ENV [ 'APPMAP_DEBUG' ] == 'true' || ENV [ 'DEBUG' ] == 'true' )
8
- LOG_HOOK = ( ENV [ 'DEBUG_HOOK' ] == 'true' )
9
-
10
8
OBJECT_INSTANCE_METHODS = %i[ ! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup
11
9
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
12
10
OBJECT_STATIC_METHODS = %i[ ! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr
13
11
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
15
13
16
14
@unbound_method_arity = ::UnboundMethod . instance_method ( :arity )
17
15
@method_arity = ::Method . instance_method ( :arity )
@@ -41,9 +39,9 @@ def already_hooked?(method)
41
39
def qualify_method_name ( method )
42
40
if method . owner . singleton_class?
43
41
class_name = singleton_method_owner_name ( method )
44
- [ class_name , '.' , method . name ]
42
+ [ class_name , '.' , method . name ]
45
43
else
46
- [ method . owner . name , '#' , method . name ]
44
+ [ method . owner . name , '#' , method . name ]
47
45
end
48
46
end
49
47
end
@@ -69,19 +67,23 @@ def enable(&block)
69
67
@slow_packages = Set . new
70
68
71
69
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
72
84
Thread . new do
73
- sleep 1
74
85
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
85
87
sleep 5
86
88
end
87
89
end
@@ -106,14 +108,14 @@ def hook_builtins
106
108
Array ( hook . method_names ) . each do |method_name |
107
109
method_name = method_name . to_sym
108
110
109
- warn "AppMap: Initiating hook for builtin #{ class_name } #{ method_name } " if LOG
110
-
111
111
begin
112
112
base_cls = Object . const_get class_name
113
113
rescue NameError
114
114
next
115
115
end
116
116
117
+ HookLog . builtin_begin class_name , method_name if HookLog . enabled?
118
+
117
119
hook_method = lambda do |entry |
118
120
cls , method = entry
119
121
next if config . never_hook? ( cls , method )
@@ -125,20 +127,22 @@ def hook_builtins
125
127
# irb(main):001:0> Kernel.public_instance_method(:system)
126
128
# (irb):1:in `public_instance_method': method `system' for module `Kernel' is private (NameError)
127
129
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
129
131
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
132
134
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
135
137
end
136
138
methods . compact!
137
139
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?
139
141
else
140
142
methods . each ( &hook_method )
141
143
end
144
+
145
+ HookLog . builtin_end class_name , method_name if HookLog . enabled?
142
146
end
143
147
end
144
148
end
@@ -151,83 +155,93 @@ def hook_builtins
151
155
protected
152
156
153
157
def trace_location ( trace_point )
154
- [ trace_point . path , trace_point . lineno ] . join ( ':' )
158
+ [ trace_point . path , trace_point . lineno ] . join ( ':' )
155
159
end
156
160
157
161
def trace_end ( trace_point )
158
162
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?
169
165
170
- cls = trace_point . self
166
+ return unless @trace_locations . add? ( location )
171
167
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
179
174
end
180
- rescue NameError
181
- [ ]
182
- end
183
175
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
+ [ ]
192
185
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
193
198
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
197
202
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 ] )
201
206
202
- next if method_id == :call
207
+ next if method_id == :call
203
208
204
- next if self . class . already_hooked? ( method )
209
+ next if self . class . already_hooked? ( method )
205
210
206
- warn "AppMap: Examining #{ hook_cls } #{ method . name } " if LOG
211
+ HookLog . log " Examining #{ hook_cls } #{ method . name } " if HookLog . enabled?
207
212
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
212
217
213
- package . handler_class . new ( package , hook_cls , method ) . activate
218
+ package . handler_class . new ( package , hook_cls , method ) . activate
219
+ end
214
220
end
215
- end
216
221
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?
231
245
end
232
246
end
233
247
end
0 commit comments