gun_dog 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 61ac60f86383d4ec2f4df7d5dba8d6ab0f503dc7
4
- data.tar.gz: 4dba94dd287a55fd391fdae2bce8168d2dc84bf9
3
+ metadata.gz: 8553b5cae7081c781faffb51279789d50c10b352
4
+ data.tar.gz: f1dae43fce48c756e6bc004a7708879d3f1e7d94
5
5
  SHA512:
6
- metadata.gz: 340f5de44210a25028553ddb84a31183b997327258eb30331c1cfb0683450f77aef5518fb2cb18492d3aa427828bcd827782d05c7bccdfe6889777dc12d60d38
7
- data.tar.gz: 595cb603c9f1c57030f5cbffa9e4ceb419ffe95869e3f085fb79aaea7fb82405cc56186d6439299e36b89527c75fc88f0f439e73a969a374294957cbef32534c
6
+ metadata.gz: bab212ec57b6f8d3b10b8fbb4adda795a932aa6b539ecd53b35b3977d628040696fe9544d49508f24be2c4827195b86100269694a8a05cb1ffd8f421c955cc70
7
+ data.tar.gz: 53117f89da46453a9e567265ab8bb781c2c6431b859b713fdefa57c502b5f62355ef9e1456001fe5c88439505bfa0704bcd7f432cd7ca4610f0155e6d71107b8
data/gun_dog.gemspec CHANGED
@@ -27,4 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "rake", "~> 10.0"
28
28
  spec.add_development_dependency "rspec", "~> 3.0"
29
29
  spec.add_development_dependency "pry-byebug"
30
+ spec.add_development_dependency "activerecord", ">= 4.2.9"
31
+ spec.add_development_dependency "sqlite3"
30
32
  end
@@ -1,30 +1,36 @@
1
1
  module GunDog
2
2
  class CallRecord
3
+ using ClassEncoding
4
+
3
5
  attr_accessor :args, :return_value, :method_name
4
6
  attr_accessor :stack
5
7
 
6
- attr_writer :internal, :cyclical
8
+ attr_writer :internal, :cyclical, :dynamic
7
9
 
8
10
  def self.from_json(json)
9
- cr = new(const_get(json['klass']),
10
- json['method_name'],
11
- class_method: json['class_method'])
11
+ cr = new(
12
+ Utilities.get_class(json['klass']),
13
+ json['method_name'],
14
+ class_method: json['class_method'],
15
+ generated: json['generated']
16
+ )
12
17
 
13
18
  cr.instance_eval do
14
19
  @internal = json['internal']
15
20
  @cyclical = json['cyclical']
16
21
  @args = json['args']
17
22
  @return_value = json['return_value']
18
- @stack = json['stack']
23
+ @stack = TraceStack.from_array(klass: @klass, stack: json['stack']) if json['stack']
19
24
  end
20
25
 
21
26
  cr
22
27
  end
23
28
 
24
- def initialize(klass, method_name, class_method: false)
29
+ def initialize(klass, method_name, class_method: false, generated: false)
25
30
  @klass = klass
26
31
  @method_name = method_name
27
32
  @class_method = class_method
33
+ @generated = generated
28
34
  end
29
35
 
30
36
  def method_location
@@ -39,31 +45,59 @@ module GunDog
39
45
  !!@cyclical
40
46
  end
41
47
 
48
+ def dynamic?
49
+ !!@dynamic
50
+ end
51
+
42
52
  def class_method?
43
53
  !!@class_method
44
54
  end
45
55
 
46
- def to_s
47
- "def #{method_name}(#{type_signatures(args)}) => #{return_value}"
56
+ def generated?
57
+ !!@generated
58
+ end
59
+
60
+ def unbound_method
61
+ if class_method?
62
+ @klass.method(method_name).unbind
63
+ else
64
+ @klass.instance_method(method_name)
65
+ end
66
+ end
67
+
68
+ def call_record_signature
69
+ "#{generated? ? "[generated] " : nil } \
70
+ def #{class_method? ? "self." : nil}#{method_name}(#{type_signatures(args)}) : #{return_value.class} \
71
+ #{ internal? ? " (internal)" : nil } \
72
+ #{ cyclical? ? " (cyclical)" : nil } \
73
+ #{ dynamic? ? " (dynamic)" : nil }".squish
48
74
  end
49
75
 
50
76
  def as_json
51
77
  {
52
- "klass" => @klass,
53
- "method_name" => method_name,
78
+ "klass" => @klass.json_encoded,
79
+ "method_name" => method_name.to_s,
54
80
  "class_method" => class_method?,
81
+ "generated" => generated?,
55
82
  "internal" => internal?,
56
83
  "cyclical" => cyclical?,
57
- "args" => args,
84
+ "dynamic" => dynamic?,
85
+ "args" => args.each_pair.with_object({}) { |(k,v), memo| memo[k.to_s] = v},
58
86
  "return_value" => return_value,
59
- "stack" => stack
87
+ "stack" => stack&.as_json
60
88
  }.reject { |_,v| v.nil? }
61
89
  end
62
90
 
63
91
  private
64
92
 
65
93
  def type_signatures(args)
66
- args.each_pair.map { |k,v| "#{k} : #{v}" }.join(', ')
94
+ if method_name == :method_missing
95
+ #TODO pass v here back through this method to show the type signatures for each method rather than
96
+ # individual arguments
97
+ args.each_pair.map { |k,v| "#{k} : #{v ? v.to_s : v.class}" }.join(', ')
98
+ else
99
+ args.each_pair.map { |k,v| "#{k} : #{v.class}" }.join(', ')
100
+ end
67
101
  end
68
102
 
69
103
  def method_separator
@@ -0,0 +1,20 @@
1
+ module GunDog
2
+ module ClassEncoding
3
+ refine Class do
4
+ def json_encoded
5
+ if singleton_class?
6
+ "#{ObjectSpace.each_object(self).first}.singleton_class"
7
+ else
8
+ name || "(anonymous subclass of #{superclass.name}"
9
+ end
10
+ end
11
+ end
12
+
13
+ refine Module do
14
+ def json_encoded
15
+ return name if name
16
+ "(anonymous module extended to an instance of #{ObjectSpace.each_object(self).first.class})"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,16 @@
1
1
  module GunDog
2
2
  class MethodOwnerStackFrame < Struct.new(:klass, :method_name)
3
+ using ClassEncoding
4
+
3
5
  def to_s
4
6
  "#{klass}##{method_name}"
5
7
  end
8
+
9
+ def as_json
10
+ {
11
+ "klass" => klass.json_encoded,
12
+ "method_name" => method_name.to_s
13
+ }
14
+ end
6
15
  end
7
16
  end
@@ -0,0 +1,57 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module GunDog
4
+ class TraceExplorer
5
+ attr_reader :trace_report
6
+
7
+ def initialize(trace_report)
8
+ @trace_report = trace_report
9
+ end
10
+
11
+ def unique_call_signatures
12
+ @trace_report.call_records.map(&:call_record_signature).uniq
13
+ end
14
+
15
+ def cyclical_methods
16
+ @trace_report.call_records.select(&:cyclical?)
17
+ end
18
+
19
+ def internal_methods
20
+ @trace_report.call_records.select(&:internal?)
21
+ end
22
+
23
+ def collaborating_local_classes
24
+ @trace_report.collaborating_classes.select { |k| Utilities.local_locations_for_class(k).any? }
25
+ end
26
+
27
+ def collaborating_classes
28
+ @trace_report.collaborating_classes
29
+ end
30
+
31
+ def trace
32
+ @trace_report
33
+ end
34
+
35
+ def [](method_location)
36
+ find_cache[method_location]
37
+ end
38
+
39
+ def methods
40
+ find_cache.keys
41
+ end
42
+
43
+ def unique_traces(doc_name)
44
+ find_cache[doc_name].map { |cr| cr.stack&.map(&:to_s) }.uniq.compact
45
+ end
46
+
47
+ def call_count
48
+ find_cache.each_pair.with_object({}) { |(k,v), memo| memo[k] = v.count }
49
+ end
50
+
51
+ private
52
+
53
+ def find_cache
54
+ find_cache ||= @trace_report.call_records.group_by(&:method_location)
55
+ end
56
+ end
57
+ end
@@ -1,11 +1,14 @@
1
+ require 'active_support/core_ext/module/anonymous'
2
+
1
3
  module GunDog
2
4
  class TraceMaker
3
- attr_reader :trace_report, :return_trace, :call_trace, :klass
5
+ attr_reader :trace_report, :return_trace, :call_trace, :klass, :complete_call_list
4
6
 
5
- def initialize(klass, &exec_block)
7
+ def initialize(klass, suppress: [], &exec_block)
6
8
  @klass = klass
7
- @trace_report = TraceReport.new(klass)
9
+ @trace_report = TraceReport.new(klass, suppression_set: build_suppression_set(suppress))
8
10
  @trace_report.stack << MethodOwnerStackFrame.new(GunDog, :trace)
11
+ @ancestor_cache = {}
9
12
  @exec_block = exec_block
10
13
  end
11
14
 
@@ -39,14 +42,36 @@ module GunDog
39
42
  def set_call_trace
40
43
  @call_trace ||= TracePoint.new(:call) do |tp|
41
44
  trace_report.stack << MethodOwnerStackFrame.new(tp.defined_class, tp.method_id)
42
- next unless tp.defined_class == klass || tp.defined_class == klass.singleton_class
43
45
 
44
46
  tp.disable
45
47
 
46
- call_record = CallRecord.new(klass, tp.method_id, class_method: tp.defined_class == klass.singleton_class)
47
- called_method = tp.self.method(tp.method_id)
48
+ binding_class = tp.binding.eval('self').class
49
+ trace_type = trace_method(binding_class, tp.defined_class)
50
+
51
+ unless trace_type
52
+ tp.enable
53
+ next
54
+ end
55
+
56
+ method_id = tp.binding.eval('__callee__') || tp.method_id
57
+
58
+ called_method = if trace_type == :meta
59
+ tp.binding.eval('self').method(method_id)
60
+ else
61
+ tp.self.method(tp.method_id)
62
+ end
63
+
64
+ # next if trace_report.suppression_set.include?(called_method.unbind)
65
+
66
+ call_record = CallRecord.new(
67
+ klass,
68
+ called_method.name,
69
+ class_method: trace_type == :eigen,
70
+ generated: trace_type == :meta
71
+ )
72
+
48
73
  call_record.args = called_method.parameters.each.with_object({}) do |p, memo|
49
- memo[p.last] = tp.binding.local_variable_get(p.last).class
74
+ memo[p.last] = tp.binding.local_variable_get(p.last)
50
75
  end
51
76
 
52
77
  trace_report.call_records << call_record
@@ -57,23 +82,62 @@ module GunDog
57
82
  end
58
83
  end
59
84
 
85
+ def trace_method(binding_class, defined_class)
86
+ return :eigen if defined_class == klass.singleton_class
87
+ return false if binding_class != klass
88
+ return :meta if klass < defined_class && (defined_class.anonymous? || after_super?(defined_class))
89
+ return :instance if defined_class == klass
90
+ false
91
+ end
92
+
93
+ def after_super?(defined_class)
94
+ return true unless klass.superclass != Object
95
+
96
+ if @ancestor_cache.has_key?(defined_class)
97
+ @ancestor_cache[defined_class]
98
+ else
99
+ ancestors = klass.ancestors
100
+ @ancestor_cache[defined_class] = ancestors.index(klass.superclass) > ancestors.index(defined_class)
101
+ end
102
+ end
103
+
60
104
  def set_method_return_trace(call_record)
61
105
  # instantiate a new return tracepoint to watch for the return of this
62
106
  # method only
63
107
  #
64
108
  TracePoint.new(:return) do |mrt|
65
- next if mrt.method_id != call_record.method_name
109
+ method_id = mrt.binding.eval('__callee__') || mrt.method_id
110
+ next if method_id != call_record.method_name
66
111
  mrt.disable
67
- call_record.return_value = mrt.return_value.class
112
+ call_record.return_value = mrt.return_value
68
113
 
69
114
  if trace_report.stack.internal_stack?
70
115
  call_record.internal = true
71
- call_record.stack = trace_report.stack.dup
116
+ call_record.stack = trace_report.stack.since_first_klass_entry
72
117
  elsif trace_report.stack.cyclical_stack?
73
118
  call_record.cyclical = true
74
- call_record.stack = trace_report.stack.dup
119
+ call_record.stack = trace_report.stack.since_first_klass_entry
120
+ end
121
+
122
+ if trace_report.stack.dynamic_stack?
123
+ call_record.dynamic = true
124
+ call_record.stack = trace_report.stack.since_first_klass_entry
75
125
  end
76
126
  end
77
127
  end
128
+
129
+ def build_suppression_set(suppression_list)
130
+ suppression_list.map { |method_id|
131
+ begin
132
+ if class_method = method_id[/self\.(.*)/,1]
133
+ klass.method(class_method).unbind
134
+ else
135
+ klass.instance_method(method_id)
136
+ end
137
+ rescue NameError
138
+ nil
139
+ end
140
+ }.uniq.to_set
141
+ end
78
142
  end
79
143
  end
@@ -1,6 +1,8 @@
1
1
  module GunDog
2
2
  class TraceReport
3
- attr_reader :klass
3
+ using ClassEncoding
4
+
5
+ attr_reader :klass, :suppression_set
4
6
 
5
7
  def self.load(filename)
6
8
  json = MultiJson.load(File.open(filename, 'r') { |f| f.read })
@@ -19,12 +21,21 @@ module GunDog
19
21
  tr
20
22
  end
21
23
 
24
+ def all_calls
25
+ @all_calls ||= []
26
+ end
27
+
28
+ def explore
29
+ GunDog::TraceExplorer.new(self)
30
+ end
31
+
22
32
  def call_records
23
33
  @call_records ||= []
24
34
  end
25
35
 
26
- def initialize(klass)
36
+ def initialize(klass, suppression_set: [])
27
37
  @klass = klass
38
+ @suppression_set = suppression_set
28
39
  end
29
40
 
30
41
  def collaborating_classes
@@ -52,21 +63,18 @@ module GunDog
52
63
 
53
64
  def as_json
54
65
  {
55
- "klass" => klass,
56
- "collaborating_classes" => collaborating_classes.to_a,
66
+ "klass" => klass.to_s,
67
+ "collaborating_classes" => collaborating_classes.map { |k| k.json_encoded },
57
68
  "call_records" => call_records.map(&:as_json)
58
69
  }
70
+
59
71
  end
60
72
 
73
+
61
74
  def to_json
62
75
  MultiJson.dump(as_json)
63
76
  end
64
77
 
65
- def find_call_record(doc_name)
66
- @find_cache ||= @call_records.group_by(&:method_location)
67
- @find_cache[doc_name]
68
- end
69
-
70
78
  def method_list
71
79
  @call_records.map(&:method_location)
72
80
  end
@@ -6,7 +6,16 @@ module GunDog
6
6
  ts = new(json['klass'])
7
7
 
8
8
  ts.instance_eval do
9
- @collaborating_classes = Set.new(json['collaborating_classes'].map { |k| Kernel.const_get(k) })
9
+ @collaborating_classes = Set.new(json['collaborating_classes'].map { |k| Utilities.get_class(k) })
10
+ end
11
+
12
+ ts
13
+ end
14
+
15
+ def self.from_array(klass:, stack: )
16
+ ts = new(klass)
17
+ stack.each do |f|
18
+ ts << GunDog::MethodOwnerStackFrame.new(Utilities.get_class(f['klass']), f['method_name'])
10
19
  end
11
20
 
12
21
  ts
@@ -18,21 +27,27 @@ module GunDog
18
27
 
19
28
  def initialize(klass)
20
29
  @klass = klass
30
+ @traced_klass_entry_points = Array.new
21
31
  @collaborating_classes = Set.new
22
32
  end
23
33
 
24
34
  def internal_stack?
25
- # if the set of the classes contained in the trace is equivalent to the
26
- # set of our own class then it is an interal stack (ie - all methods are
27
- # internal to the traced class)
28
- call_stack.classes_in_stack == self_set
35
+ # if the set of classes contained in the trace since we entered our own
36
+ # class is equivalent to the set of our own class then it is an internal
37
+ # trace
38
+ since_first_klass_entry.classes_in_stack == self_set
29
39
  end
30
40
 
31
41
  def cyclical_stack?
32
- # if the set of classes contained in the trace is a superset of the set
33
- # of our class then it is a cyclical stack (ie, it contains calls both
34
- # within and without of the class)
35
- call_stack.classes_in_stack > self_set
42
+ # if the set of classes contained in the trace since we entered our own
43
+ # class is a superset of the set of our class then it is a cyclical stack
44
+ # (ie, it contains calls both within and without of the class)
45
+ since_first_klass_entry.classes_in_stack > self_set
46
+ end
47
+
48
+ def dynamic_stack?
49
+ methods = since_first_klass_entry.map { |f| f.method_name.to_s }
50
+ methods.include?('method_missing') || methods.include?('send') || methods.include?('__send__')
36
51
  end
37
52
 
38
53
  def preceded_by_traced_klass?
@@ -44,14 +59,37 @@ module GunDog
44
59
  [klass].to_set
45
60
  end
46
61
 
62
+ def since_first_klass_entry
63
+ # the stack since the first call to a traced method excluding the current
64
+ # frame
65
+ self.slice((@traced_klass_entry_points.first || 1) .. -2) || GunDog::TraceStack.new(@klass)
66
+ end
67
+
47
68
  def call_stack
48
- # the stack excluding the sentinel (element zero) and our selves (element -1)
69
+ # the stack excluding the gundog sentinel (element zero) and ourselves (element -1)
49
70
  self.slice(1 .. -2) || TraceStack.new(klass)
50
71
  end
51
72
 
73
+ def pop
74
+ super.tap do |popped|
75
+ if length == @traced_klass_entry_points.last
76
+ @traced_klass_entry_points.pop
77
+ end
78
+ end
79
+ end
80
+
52
81
  def <<(frame)
53
82
  collaborating_classes.add(frame.klass) if preceded_by_traced_klass?
83
+ @traced_klass_entry_points << length if frame_owned_by_traced_klass?(frame)
54
84
  super(frame)
55
85
  end
86
+
87
+ def frame_owned_by_traced_klass?(frame)
88
+ frame.klass == klass || frame.klass.singleton_class == klass.singleton_class
89
+ end
90
+
91
+ def as_json
92
+ map(&:as_json)
93
+ end
56
94
  end
57
95
  end
@@ -0,0 +1,36 @@
1
+ module GunDog
2
+ module Utilities
3
+ class UnencodableClass; end
4
+
5
+ def self.get_class(str)
6
+ if str =~ /anonymous/
7
+ Class.new(UnencodableClass) do |k|
8
+ def json_encoded
9
+ k
10
+ end
11
+ end
12
+ else
13
+ begin
14
+ eval(str)
15
+ rescue => e
16
+ require 'pry'; binding.pry
17
+ e.message
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.local_locations_for_class(c)
23
+ all_locations_for_class(c)
24
+ .reject { |p| p =~ /(gem)|(rubies)|(eval)/ }
25
+ end
26
+
27
+ def self.all_locations_for_class(c)
28
+ c.instance_methods
29
+ .map { |m| c.instance_method(m) }
30
+ .select { |m| m.owner == c }
31
+ .map { |m| m.source_location&.first }
32
+ .compact
33
+ .uniq
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,3 @@
1
1
  module GunDog
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/gun_dog.rb CHANGED
@@ -1,17 +1,38 @@
1
- require "gun_dog/version"
1
+ require 'gun_dog/version'
2
2
  require 'json'
3
3
  require 'multi_json'
4
4
  require 'active_support/core_ext/hash/slice'
5
5
  require 'active_support/core_ext/string/filters'
6
+ require 'active_support/configurable'
6
7
 
7
8
  module GunDog
9
+ include ActiveSupport::Configurable
10
+
11
+ config_accessor :suppress_methods do
12
+ {}
13
+ end
14
+
8
15
  autoload :MethodOwnerStackFrame, 'gun_dog/method_owner_stack_frame'
9
16
  autoload :CallRecord, 'gun_dog/call_record'
10
17
  autoload :TraceMaker, 'gun_dog/trace_maker'
11
18
  autoload :TraceReport, 'gun_dog/trace_report'
12
19
  autoload :TraceStack, 'gun_dog/trace_stack'
20
+ autoload :TraceExplorer, 'gun_dog/trace_explorer'
21
+ autoload :ClassEncoding, 'gun_dog/class_encoding'
22
+ autoload :Utilities, 'gun_dog/utilities'
23
+
24
+ class << self
25
+ private def suppressed_methods_for_klass(klass)
26
+ config.suppress_methods.values_at(*klass.ancestors).flatten.compact
27
+ end
28
+ end
29
+
13
30
 
14
31
  def self.trace(klass, &block)
15
- TraceMaker.new(klass, &block).exec
32
+ TraceMaker.new(klass, suppress: suppressed_methods_for_klass(klass), &block).exec
33
+ end
34
+
35
+ def self.load_trace(path)
36
+ TraceReport.load(path)
16
37
  end
17
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gun_dog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Prater
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-12 00:00:00.000000000 Z
11
+ date: 2017-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 4.2.9
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 4.2.9
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: Log callsite information for a given class.
98
126
  email:
99
127
  - me@stephenprater.com
@@ -112,11 +140,14 @@ files:
112
140
  - gun_dog.gemspec
113
141
  - lib/gun_dog.rb
114
142
  - lib/gun_dog/call_record.rb
143
+ - lib/gun_dog/class_encoding.rb
115
144
  - lib/gun_dog/indexed_array.rb
116
145
  - lib/gun_dog/method_owner_stack_frame.rb
146
+ - lib/gun_dog/trace_explorer.rb
117
147
  - lib/gun_dog/trace_maker.rb
118
148
  - lib/gun_dog/trace_report.rb
119
149
  - lib/gun_dog/trace_stack.rb
150
+ - lib/gun_dog/utilities.rb
120
151
  - lib/gun_dog/version.rb
121
152
  homepage: http://github.com/stephenprater/gun_dog
122
153
  licenses: []