gun_dog 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []