appmap 0.23.0 → 0.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +43 -0
- data/README.md +33 -21
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +5 -73
- data/lib/appmap.rb +61 -6
- data/lib/appmap/algorithm/prune_class_map.rb +2 -0
- data/lib/appmap/algorithm/stats.rb +4 -2
- data/lib/appmap/class_map.rb +143 -0
- data/lib/appmap/command/record.rb +8 -6
- data/lib/appmap/command/stats.rb +2 -0
- data/lib/appmap/event.rb +168 -0
- data/lib/appmap/hook.rb +152 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -21
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -13
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +247 -260
- data/lib/appmap/trace.rb +88 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +90 -92
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +7 -3
- data/spec/config_spec.rb +25 -0
- data/spec/fixtures/hook/attr_accessor.rb +5 -0
- data/spec/fixtures/hook/class_method.rb +17 -0
- data/spec/fixtures/hook/constructor.rb +7 -0
- data/spec/fixtures/hook/exception_method.rb +11 -0
- data/spec/fixtures/hook/instance_method.rb +23 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
- data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -1
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
- data/spec/fixtures/rails_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails_users_app/create_app +1 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
- data/spec/hook_spec.rb +369 -0
- data/spec/rails_spec_helper.rb +25 -16
- data/spec/railtie_spec.rb +1 -1
- data/spec/record_sql_rails_pg_spec.rb +1 -2
- data/spec/remote_recording_spec.rb +117 -0
- data/spec/spec_helper.rb +5 -0
- data/test/cli_test.rb +4 -46
- data/test/fixtures/cli_record_test/appmap.yml +2 -1
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
- data/test/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/rspec_test.rb +5 -0
- data/test/test_helper.rb +0 -42
- metadata +46 -63
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- data/lib/appmap/command/upload.rb +0 -99
- data/lib/appmap/config.rb +0 -65
- data/lib/appmap/config/directory.rb +0 -65
- data/lib/appmap/config/file.rb +0 -13
- data/lib/appmap/config/named_function.rb +0 -21
- data/lib/appmap/config/package_dir.rb +0 -52
- data/lib/appmap/config/path.rb +0 -25
- data/lib/appmap/feature.rb +0 -262
- data/lib/appmap/inspect.rb +0 -91
- data/lib/appmap/inspect/inspector.rb +0 -99
- data/lib/appmap/inspect/parse_node.rb +0 -170
- data/lib/appmap/inspect/parser.rb +0 -15
- data/lib/appmap/parser.rb +0 -60
- data/lib/appmap/rspec/parse_node.rb +0 -41
- data/lib/appmap/rspec/parser.rb +0 -15
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
- data/lib/appmap/trace/tracer.rb +0 -356
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
- data/spec/rack_handler_webrick_spec.rb +0 -59
- data/test/config_test.rb +0 -149
- data/test/explict_inspect_test.rb +0 -29
- data/test/fixtures/active_record_like/active_record.rb +0 -2
- data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
- data/test/fixtures/active_record_like/active_record/association.rb +0 -4
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
- data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
- data/test/fixtures/parse_file/defs_static_function.rb +0 -96
- data/test/fixtures/parse_file/function_within_class.rb +0 -36
- data/test/fixtures/parse_file/include_public_methods.rb +0 -127
- data/test/fixtures/parse_file/instance_function.rb +0 -17
- data/test/fixtures/parse_file/modules.rb +0 -71
- data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
- data/test/fixtures/parse_file/toplevel_class.rb +0 -13
- data/test/fixtures/parse_file/toplevel_function.rb +0 -14
- data/test/fixtures/trace_test/trace_program_1.rb +0 -44
- data/test/implicit_inspect_test.rb +0 -33
- data/test/include_exclude_test.rb +0 -48
- data/test/prerecorded_trace_test.rb +0 -76
- data/test/trace_test.rb +0 -92
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AppMap
|
2
4
|
module Algorithm
|
3
5
|
StatsStruct = Struct.new(:appmap)
|
@@ -31,7 +33,7 @@ module AppMap
|
|
31
33
|
comparator = ->(a,b) { b.count <=> a.count }
|
32
34
|
class_frequency.sort!(&comparator)
|
33
35
|
method_frequency.sort!(&comparator)
|
34
|
-
|
36
|
+
|
35
37
|
self
|
36
38
|
end
|
37
39
|
|
@@ -55,7 +57,7 @@ module AppMap
|
|
55
57
|
end
|
56
58
|
end
|
57
59
|
Frequency = Struct.new(:name, :count)
|
58
|
-
|
60
|
+
|
59
61
|
def perform(limit: nil)
|
60
62
|
events = appmap['events'] || []
|
61
63
|
frequency_calc = lambda do |key_func|
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
class ClassMap
|
7
|
+
module HasChildren
|
8
|
+
def self.included(base)
|
9
|
+
base.module_eval do
|
10
|
+
def children
|
11
|
+
@children ||= []
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Types
|
18
|
+
class Root
|
19
|
+
include HasChildren
|
20
|
+
end
|
21
|
+
|
22
|
+
Package = Struct.new(:name) do
|
23
|
+
include HasChildren
|
24
|
+
|
25
|
+
def type
|
26
|
+
'package'
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_h
|
30
|
+
{
|
31
|
+
name: name,
|
32
|
+
type: type,
|
33
|
+
children: children.map(&:to_h)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
Class = Struct.new(:name) do
|
38
|
+
include HasChildren
|
39
|
+
|
40
|
+
def type
|
41
|
+
'class'
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h
|
45
|
+
{
|
46
|
+
name: name,
|
47
|
+
type: type,
|
48
|
+
children: children.map(&:to_h)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
Function = Struct.new(:name) do
|
53
|
+
attr_accessor :static, :location
|
54
|
+
|
55
|
+
def type
|
56
|
+
'function'
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_h
|
60
|
+
{
|
61
|
+
name: name,
|
62
|
+
type: type,
|
63
|
+
location: location,
|
64
|
+
static: static
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def build_from_methods(config, methods)
|
72
|
+
root = Types::Root.new
|
73
|
+
methods.each do |method|
|
74
|
+
package = package_for_method(config.packages, method)
|
75
|
+
add_function root, package.path, method
|
76
|
+
end
|
77
|
+
root.children.map(&:to_h)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def package_for_method(packages, method)
|
83
|
+
location = method.method.source_location
|
84
|
+
location_file, = location
|
85
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
86
|
+
|
87
|
+
packages.find do |pkg|
|
88
|
+
(location_file.index(pkg.path) == 0) &&
|
89
|
+
!pkg.exclude.find { |p| location_file.index(p) }
|
90
|
+
end or raise "No package found for method #{method}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_function(root, package_name, method)
|
94
|
+
location = method.method.source_location
|
95
|
+
location_file, lineno = location
|
96
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
97
|
+
|
98
|
+
static = method.method.owner.singleton_class?
|
99
|
+
|
100
|
+
object_infos = [
|
101
|
+
{
|
102
|
+
name: package_name,
|
103
|
+
type: 'package'
|
104
|
+
}
|
105
|
+
]
|
106
|
+
object_infos += method.defined_class.split('::').map do |name|
|
107
|
+
{
|
108
|
+
name: name,
|
109
|
+
type: 'class'
|
110
|
+
}
|
111
|
+
end
|
112
|
+
object_infos << {
|
113
|
+
name: method.method.name,
|
114
|
+
type: 'function',
|
115
|
+
location: [ location_file, lineno ].join(':'),
|
116
|
+
static: static
|
117
|
+
}
|
118
|
+
parent = root
|
119
|
+
object_infos.each do |info|
|
120
|
+
parent = find_or_create parent.children, info do
|
121
|
+
Types.const_get(info[:type].classify).new(info[:name].to_s).tap do |type|
|
122
|
+
info.keys.tap do |keys|
|
123
|
+
keys.delete(:name)
|
124
|
+
keys.delete(:type)
|
125
|
+
end.each do |key|
|
126
|
+
type.send "#{key}=", info[key]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def find_or_create(list, info)
|
134
|
+
obj = list.find { |item| item.type == info[:type] && item.name == info[:name] }
|
135
|
+
return obj if obj
|
136
|
+
|
137
|
+
yield.tap do |new_obj|
|
138
|
+
list << new_obj
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AppMap
|
2
4
|
module Command
|
3
5
|
RecordStruct = Struct.new(:config, :program)
|
@@ -61,12 +63,9 @@ module AppMap
|
|
61
63
|
end
|
62
64
|
|
63
65
|
def perform(&block)
|
64
|
-
|
65
|
-
functions = features.map(&:collect_functions).flatten
|
66
|
-
|
67
|
-
require 'appmap/trace/tracer'
|
66
|
+
AppMap::Hook.hook(config)
|
68
67
|
|
69
|
-
tracer = AppMap
|
68
|
+
tracer = AppMap.tracing.trace
|
70
69
|
|
71
70
|
events = []
|
72
71
|
quit = false
|
@@ -85,7 +84,10 @@ module AppMap
|
|
85
84
|
at_exit do
|
86
85
|
quit = true
|
87
86
|
event_thread.join
|
88
|
-
yield
|
87
|
+
yield AppMap::APPMAP_FORMAT_VERSION,
|
88
|
+
self.class.detect_metadata,
|
89
|
+
AppMap.class_map(config, tracer.event_methods),
|
90
|
+
events
|
89
91
|
end
|
90
92
|
|
91
93
|
load program if program
|
data/lib/appmap/command/stats.rb
CHANGED
data/lib/appmap/event.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppMap
|
4
|
+
module Event
|
5
|
+
@@id_counter = 0
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# reset_id_counter is used by test cases to get consistent event ids.
|
9
|
+
def reset_id_counter
|
10
|
+
@@id_counter = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_id_counter
|
14
|
+
@@id_counter += 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
|
19
|
+
|
20
|
+
class MethodEvent < MethodEventStruct
|
21
|
+
LIMIT = 100
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def build_from_invocation(me, event_type, defined_class, method)
|
25
|
+
singleton = method.owner.singleton_class?
|
26
|
+
|
27
|
+
me.id = AppMap::Event.next_id_counter
|
28
|
+
me.event = event_type
|
29
|
+
me.defined_class = defined_class
|
30
|
+
me.method_id = method.name.to_s
|
31
|
+
path = method.source_location[0]
|
32
|
+
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
33
|
+
me.path = path
|
34
|
+
me.lineno = method.source_location[1]
|
35
|
+
me.static = singleton
|
36
|
+
me.thread_id = Thread.current.object_id
|
37
|
+
end
|
38
|
+
|
39
|
+
# Gets a display string for a value. This is not meant to be a machine deserializable value.
|
40
|
+
def display_string(value)
|
41
|
+
return nil unless value
|
42
|
+
|
43
|
+
last_resort_string = lambda do
|
44
|
+
warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
|
45
|
+
'*Error inspecting variable*'
|
46
|
+
end
|
47
|
+
|
48
|
+
value_string = \
|
49
|
+
begin
|
50
|
+
value.to_s
|
51
|
+
rescue NoMethodError
|
52
|
+
begin
|
53
|
+
value.inspect
|
54
|
+
rescue StandardError
|
55
|
+
last_resort_string.call
|
56
|
+
end
|
57
|
+
rescue StandardError
|
58
|
+
last_resort_string.call
|
59
|
+
end
|
60
|
+
|
61
|
+
(value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
alias static? static
|
66
|
+
end
|
67
|
+
|
68
|
+
class MethodCall < MethodEvent
|
69
|
+
attr_accessor :parameters, :receiver
|
70
|
+
|
71
|
+
class << self
|
72
|
+
def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
|
73
|
+
mc.tap do
|
74
|
+
mc.parameters = method.parameters.map.with_index do |method_param, idx|
|
75
|
+
param_type, param_name = method_param
|
76
|
+
param_name ||= 'arg'
|
77
|
+
value = arguments[idx]
|
78
|
+
{
|
79
|
+
name: param_name,
|
80
|
+
class: value.class.name,
|
81
|
+
object_id: value.__id__,
|
82
|
+
value: display_string(value),
|
83
|
+
kind: param_type
|
84
|
+
}
|
85
|
+
end
|
86
|
+
mc.receiver = {
|
87
|
+
class: receiver.class.name,
|
88
|
+
object_id: receiver.__id__,
|
89
|
+
value: display_string(receiver)
|
90
|
+
}
|
91
|
+
MethodEvent.build_from_invocation(mc, :call, defined_class, method)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_h
|
97
|
+
super.tap do |h|
|
98
|
+
h[:parameters] = parameters
|
99
|
+
h[:receiver] = receiver
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class MethodReturnIgnoreValue < MethodEvent
|
105
|
+
attr_accessor :parent_id, :elapsed
|
106
|
+
|
107
|
+
class << self
|
108
|
+
def build_from_invocation(mr = MethodReturnIgnoreValue.new, defined_class, method, parent_id, elapsed)
|
109
|
+
mr.tap do |_|
|
110
|
+
mr.parent_id = parent_id
|
111
|
+
mr.elapsed = elapsed
|
112
|
+
MethodEvent.build_from_invocation(mr, :return, defined_class, method)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_h
|
118
|
+
super.tap do |h|
|
119
|
+
h[:parent_id] = parent_id
|
120
|
+
h[:elapsed] = elapsed
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class MethodReturn < MethodReturnIgnoreValue
|
126
|
+
attr_accessor :return_value, :exceptions
|
127
|
+
|
128
|
+
class << self
|
129
|
+
def build_from_invocation(mr = MethodReturn.new, defined_class, method, parent_id, elapsed, return_value, exception)
|
130
|
+
mr.tap do |_|
|
131
|
+
if return_value
|
132
|
+
mr.return_value = {
|
133
|
+
class: return_value.class.name,
|
134
|
+
value: display_string(return_value),
|
135
|
+
object_id: return_value.__id__
|
136
|
+
}
|
137
|
+
end
|
138
|
+
if exception
|
139
|
+
next_exception = exception
|
140
|
+
exceptions = []
|
141
|
+
while next_exception
|
142
|
+
exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
|
143
|
+
exceptions << {
|
144
|
+
class: next_exception.class.name,
|
145
|
+
message: next_exception.message,
|
146
|
+
object_id: next_exception.__id__,
|
147
|
+
path: exception_backtrace&.path,
|
148
|
+
lineno: exception_backtrace&.lineno
|
149
|
+
}.compact
|
150
|
+
next_exception = next_exception.cause
|
151
|
+
end
|
152
|
+
|
153
|
+
mr.exceptions = exceptions
|
154
|
+
end
|
155
|
+
MethodReturnIgnoreValue.build_from_invocation(mr, defined_class, method, parent_id, elapsed)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_h
|
161
|
+
super.tap do |h|
|
162
|
+
h[:return_value] = return_value if return_value
|
163
|
+
h[:exceptions] = exceptions if exceptions
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/appmap/hook.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
module AppMap
|
6
|
+
class Hook
|
7
|
+
LOG = false
|
8
|
+
|
9
|
+
Package = Struct.new(:path, :exclude) do
|
10
|
+
def to_h
|
11
|
+
{
|
12
|
+
path: path,
|
13
|
+
exclude: exclude.blank? ? nil : exclude
|
14
|
+
}.compact
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Config = Struct.new(:name, :packages) do
|
19
|
+
class << self
|
20
|
+
# Loads configuration data from a file, specified by the file name.
|
21
|
+
def load_from_file(config_file_name)
|
22
|
+
require 'yaml'
|
23
|
+
load YAML.safe_load(::File.read(config_file_name))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads configuration from a Hash.
|
27
|
+
def load(config_data)
|
28
|
+
packages = (config_data['packages'] || []).map do |package|
|
29
|
+
Package.new(package['path'], package['exclude'] || [])
|
30
|
+
end
|
31
|
+
Config.new config_data['name'], packages
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(name, packages = [])
|
36
|
+
super name, packages || []
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
{
|
41
|
+
name: name,
|
42
|
+
packages: packages.map(&:to_h)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
48
|
+
|
49
|
+
class << self
|
50
|
+
# Observe class loading and hook all methods which match the config.
|
51
|
+
def hook(config = AppMap.configure)
|
52
|
+
package_include_paths = config.packages.map(&:path)
|
53
|
+
package_exclude_paths = config.packages.map do |pkg|
|
54
|
+
pkg.exclude.map do |exclude|
|
55
|
+
File.join(pkg.path, exclude)
|
56
|
+
end
|
57
|
+
end.flatten
|
58
|
+
|
59
|
+
before_hook = lambda do |defined_class, method, receiver, args|
|
60
|
+
require 'appmap/event'
|
61
|
+
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
|
62
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
|
63
|
+
[ call_event, Time.now ]
|
64
|
+
end
|
65
|
+
|
66
|
+
after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
|
67
|
+
require 'appmap/event'
|
68
|
+
elapsed = Time.now - start_time
|
69
|
+
return_event = AppMap::Event::MethodReturn.build_from_invocation \
|
70
|
+
defined_class, method, call_event.id, elapsed, return_value, exception
|
71
|
+
AppMap.tracing.record_event return_event
|
72
|
+
end
|
73
|
+
|
74
|
+
with_disabled_hook = lambda do |&fn|
|
75
|
+
# Don't record functions, such as to_s and inspect, that might be called
|
76
|
+
# by the fn. Otherwise there can be a stack oveflow.
|
77
|
+
Thread.current[HOOK_DISABLE_KEY] = true
|
78
|
+
begin
|
79
|
+
fn.call
|
80
|
+
ensure
|
81
|
+
Thread.current[HOOK_DISABLE_KEY] = false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
TracePoint.trace(:end) do |tp|
|
86
|
+
cls = tp.self
|
87
|
+
|
88
|
+
instance_methods = cls.public_instance_methods(false)
|
89
|
+
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
90
|
+
|
91
|
+
hook_method = lambda do |cls|
|
92
|
+
lambda do |method_id|
|
93
|
+
next if method_id.to_s =~ /_hooked_by_appmap$/
|
94
|
+
|
95
|
+
method = cls.public_instance_method(method_id)
|
96
|
+
location = method.source_location
|
97
|
+
location_file, = location
|
98
|
+
next unless location_file
|
99
|
+
|
100
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
101
|
+
match = package_include_paths.find { |p| location_file.index(p) == 0 }
|
102
|
+
match &&= !package_exclude_paths.find { |p| location_file.index(p) }
|
103
|
+
next unless match
|
104
|
+
|
105
|
+
disasm = RubyVM::InstructionSequence.disasm(method)
|
106
|
+
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
107
|
+
next unless disasm
|
108
|
+
|
109
|
+
defined_class, method_symbol = \
|
110
|
+
if method.owner.singleton_class?
|
111
|
+
# Singleton class name is like: #<Class:<(.*)>>
|
112
|
+
class_name = method.owner.to_s['#<Class:<'.length-1..-2]
|
113
|
+
[ class_name, '.' ]
|
114
|
+
else
|
115
|
+
[ method.owner.name, '#' ]
|
116
|
+
end
|
117
|
+
|
118
|
+
warn "AppMap: Hooking #{defined_class}#{method_symbol}#{method.name}" if LOG
|
119
|
+
|
120
|
+
cls.define_method method_id do |*args, &block|
|
121
|
+
base_method = method.bind(self).to_proc
|
122
|
+
|
123
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
124
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
125
|
+
return base_method.call(*args, &block) unless enabled
|
126
|
+
|
127
|
+
call_event, start_time = with_disabled_hook.call do
|
128
|
+
before_hook.call(defined_class, method, self, args)
|
129
|
+
end
|
130
|
+
return_value = nil
|
131
|
+
exception = nil
|
132
|
+
begin
|
133
|
+
return_value = base_method.call(*args, &block)
|
134
|
+
rescue
|
135
|
+
exception = $ERROR_INFO
|
136
|
+
raise
|
137
|
+
ensure
|
138
|
+
with_disabled_hook.call do
|
139
|
+
after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
instance_methods.each(&hook_method.call(cls))
|
147
|
+
class_methods.each(&hook_method.call(cls.singleton_class))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|