Skip to content

Commit 01dbe4b

Browse files
committed
feat: Enhancing hook logging and profiling
1 parent 999b25b commit 01dbe4b

File tree

7 files changed

+221
-96
lines changed

7 files changed

+221
-96
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
/pkg/
99
/spec/reports/
1010
tmp/
11+
appmap_hook.log
1112
vendor
1213
node_modules
1314
Gemfile.lock

Diff for: lib/appmap/hook.rb

+98-84
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
# frozen_string_literal: true
22

33
require 'English'
4+
require_relative './hook_log'
45

56
module 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

Diff for: lib/appmap/hook/method.rb

+7-8
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ def initialize(hook_package, hook_class, hook_method)
3838
end
3939

4040
def activate
41-
if Hook::LOG
41+
if HookLog.enabled?
4242
msg = if method_display_name
43-
"#{method_display_name}"
44-
else
45-
"#{hook_method.name} (class resolution deferred)"
46-
end
47-
warn "AppMap: Hooking #{msg} at line #{(hook_method.source_location || []).join(':')}"
43+
"#{method_display_name}"
44+
else
45+
"#{hook_method.name} (class resolution deferred)"
46+
end
47+
HookLog.log "Hooking #{msg} at line #{(hook_method.source_location || []).join(':')}"
4848
end
4949

5050
hook_method_parameters = hook_method.parameters.dup.freeze
@@ -65,8 +65,7 @@ def activate
6565
protected
6666

6767
def defining_class(hook_class)
68-
cls = \
69-
if RUBY_MAJOR_VERSION == 2 && RUBY_MINOR_VERSION <= 5
68+
cls = if RUBY_MAJOR_VERSION == 2 && RUBY_MINOR_VERSION <= 5
7069
hook_class
7170
.ancestors
7271
.select { |cls| cls.method_defined?(hook_method.name) }

Diff for: lib/appmap/hook_log.rb

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module AppMap
2+
class HookLog
3+
LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
4+
LOG_HOOK = (ENV['DEBUG_HOOK'] == 'true' || ENV['APPMAP_LOG_HOOK'] == 'true')
5+
LOG_HOOK_FILE = (ENV['APPMAP_LOG_HOOK_FILE'] || 'appmap_hook.log')
6+
7+
def initialize
8+
@file_handle = self.class.send :open_log_file
9+
@elapsed = Hash.new { |h, k| h[k] = [] }
10+
11+
at_exit do
12+
@file_handle.puts 'Elapsed time:'
13+
@elapsed.keys.each do |k|
14+
@file_handle.puts "#{k}:\t#{@elapsed[k].sum}"
15+
end
16+
@file_handle.flush
17+
@file_handle.close
18+
end
19+
end
20+
21+
def start_time(timer)
22+
@elapsed[timer] << [Time.now.to_f]
23+
end
24+
25+
def end_time(timer)
26+
@elapsed[timer][-1] = Time.now.to_f - @elapsed[timer].last[0]
27+
end
28+
29+
class << self
30+
def enabled?
31+
LOG || LOG_HOOK
32+
end
33+
34+
def builtin_begin(class_name, method_name)
35+
log "builtin\tbegin\tInitiating hook for builtin #{class_name} #{method_name}"
36+
@hook_log.start_time :builtin
37+
end
38+
39+
def builtin_end(class_name, method_name)
40+
@hook_log.end_time :builtin
41+
log "builtin\tend\tCompleted hook for builtin #{class_name} #{method_name}"
42+
end
43+
44+
def usercode_begin(location)
45+
log "usercode\tbegin\tClass or module defined at location #{location}"
46+
@hook_log.start_time :usercode
47+
end
48+
49+
def usercode_end(location)
50+
@hook_log.end_time :usercode
51+
log "usercode\tend\tCompleted location #{location}"
52+
end
53+
54+
def log(msg)
55+
@hook_log ||= HookLog.new
56+
@hook_log.log msg
57+
end
58+
59+
protected def open_log_file
60+
if LOG_HOOK_FILE == 'stderr'
61+
$stderr
62+
else
63+
File.open(LOG_HOOK_FILE, 'w')
64+
end
65+
end
66+
end
67+
68+
def log(msg)
69+
if LOG_HOOK_FILE == 'stderr'
70+
msg = "AppMap: #{msg}"
71+
end
72+
msg = "#{Time.new.to_f}\t#{msg}"
73+
@file_handle.puts(msg)
74+
end
75+
end
76+
end

0 commit comments

Comments
 (0)