Class: ADSL::Extract::Instrumenter

Inherits:
Object
  • Object
show all
Defined in:
lib/adsl/extract/instrumenter.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(instrument_domain = Dir.pwd) ⇒ Instrumenter

Returns a new instance of Instrumenter.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/adsl/extract/instrumenter.rb', line 84

def initialize(instrument_domain = Dir.pwd)
  @instrument_domain = instrument_domain
  @replacers = []
  @stack_depth = 0
  @method_locals_stack = []

  # mark the instrumentation
  replace :defn, :defs do |sexp|
    mark_sexp_instrumented sexp
  end

  # make sure the instrumentation propagates through calls
  replace :call do |sexp|
    # expected format: s(:call, object, method_name, *args)
    # replaced with Extract::Instrumenter.e(instrumenter_id, object, method_name, *args)
    original_object = sexp.sexp_body[0] || s(:self)
    original_method_name = sexp.sexp_body[1]
    original_args = sexp.sexp_body[2..-1]

    next sexp if [s(:self), nil].include? original_object and Kernel.respond_to? original_method_name

    s(:call, nil, :ins_call, original_object, s(:lit, original_method_name), *original_args)
  end
end

Instance Attribute Details

#instrumentation_filtersObject

Returns the value of attribute instrumentation_filters.



20
21
22
# File 'lib/adsl/extract/instrumenter.rb', line 20

def instrumentation_filters
  @instrumentation_filters
end

#method_locals_stackObject (readonly)

Returns the value of attribute method_locals_stack.



19
20
21
# File 'lib/adsl/extract/instrumenter.rb', line 19

def method_locals_stack
  @method_locals_stack
end

#stack_depthObject (readonly)

Returns the value of attribute stack_depth.



19
20
21
# File 'lib/adsl/extract/instrumenter.rb', line 19

def stack_depth
  @stack_depth
end

Class Method Details

.get_instanceObject



25
26
27
# File 'lib/adsl/extract/instrumenter.rb', line 25

def self.get_instance()
  @instance
end

.instrumentedObject



33
34
35
# File 'lib/adsl/extract/instrumenter.rb', line 33

def self.instrumented()
  # a dummy method injected into the AST
end

Instance Method Details

#convert_root_defs_into_defn(sexp) ⇒ Object



152
153
154
# File 'lib/adsl/extract/instrumenter.rb', line 152

def convert_root_defs_into_defn(sexp)
  sexp.sexp_type == :defs ? s(:defn, *sexp[2..-1]) : sexp
end

#exec_withinObject



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/adsl/extract/instrumenter.rb', line 49

def exec_within
  Instrumenter.instance_variable_set(:@instance, self) if @stack_depth == 0
  @stack_depth += 1
  @method_locals_stack << create_locals if respond_to? :create_locals

  return yield(self)
ensure
  @stack_depth -= 1
  @method_locals_stack.pop
  Instrumenter.instance_variable_set(:@instance, nil) if @stack_depth == 0
end

#execute_instrumented(object, method_name, *args, &block) ⇒ Object



145
146
147
148
149
150
# File 'lib/adsl/extract/instrumenter.rb', line 145

def execute_instrumented(object, method_name, *args, &block)
  self.exec_within do
    instrument object, method_name
    return object.send method_name, *args, &block
  end
end

#instrument(object, method_name) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/adsl/extract/instrumenter.rb', line 166

def instrument(object, method_name)
  if should_instrument? object, method_name
    begin
      # this is complex because I want to avoid using .method on non-class objects
      # because they might implement method themselves
      method = object.singleton_class.instance_method method_name
      
      source = method.source
      
      # Ruby 2.0.0 support is in development as of writing this
      sexp = ruby_parser.process source

      unless sexp.nil?
        sexp = convert_root_defs_into_defn sexp

        instrumented_sexp = instrument_sexp sexp
        
        new_code = Ruby2Ruby.new.process instrumented_sexp

        object.replace_method method_name, new_code
        
        new_code
      else
        source
      end
    rescue MethodSource::SourceNotFoundError
    end
  end
end

#instrument_sexp(sexp) ⇒ Object



196
197
198
199
200
201
202
# File 'lib/adsl/extract/instrumenter.rb', line 196

def instrument_sexp(sexp)
  return nil if sexp.nil?
  @replacers.reverse_each do |types, block, options|
    sexp = sexp.block_replace *types, options, &block
  end
  sexp
end

#instrument_string(source) ⇒ Object



156
157
158
159
160
161
162
163
164
# File 'lib/adsl/extract/instrumenter.rb', line 156

def instrument_string(source)
  sexp = ruby_parser.process source
  unless sexp.nil?
    instrumented_sexp = instrument_sexp sexp
    new_code = Ruby2Ruby.new.process instrumented_sexp
  else
    source
  end
end

#mark_sexp_instrumented(sexp) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/adsl/extract/instrumenter.rb', line 61

def mark_sexp_instrumented(sexp)
  raise 'Already instrumented' if sexp_instrumented? sexp
  
  first_stmt = sexp[3]

  if first_stmt[0] != :call or
      first_stmt[1] != Instrumenter.to_sexp or
      first_stmt[2] != :instrumented
    new_stmt = s(:call, Instrumenter.to_sexp, :instrumented)
    sexp.insert 3, new_stmt
  end
  sexp
end

#method_localsObject



37
38
39
# File 'lib/adsl/extract/instrumenter.rb', line 37

def method_locals
  @method_locals_stack.last
end

#previous_localsObject



41
42
43
# File 'lib/adsl/extract/instrumenter.rb', line 41

def previous_locals
  @method_locals_stack[-2]
end

#replace(*types, &block) ⇒ Object



133
134
135
136
# File 'lib/adsl/extract/instrumenter.rb', line 133

def replace(*types, &block)
  options = types.last.is_a?(Hash) ? types.pop : {}
  @replacers << [types, block, options]
end

#root_localsObject



45
46
47
# File 'lib/adsl/extract/instrumenter.rb', line 45

def root_locals
  @method_locals_stack.first
end

#ruby_parserObject



29
30
31
# File 'lib/adsl/extract/instrumenter.rb', line 29

def ruby_parser
  RUBY_VERSION >= '2' ? Ruby19Parser.new : RubyParser.for_current_ruby
end

#sexp_instrumented?(sexp) ⇒ Boolean

Returns:

  • (Boolean)


75
76
77
78
79
80
81
82
# File 'lib/adsl/extract/instrumenter.rb', line 75

def sexp_instrumented?(sexp)
  first_stmt = sexp[3]
  return (first_stmt[0] == :call and
      first_stmt[1] == Instrumenter.to_sexp and
      first_stmt[2] == :instrumented)
rescue MethodSource::SourceNotFoundError
  return
end

#should_instrument?(object, method_name) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/adsl/extract/instrumenter.rb', line 109

def should_instrument?(object, method_name)
  return false if object.is_a?(Fixnum) or object.is_a?(Symbol)
 
  method = object.singleton_class.instance_method method_name

  return false if method.source_location.nil?
  return false if method.owner == Kernel
  return false if @instrument_domain && !(method.source_location.first =~ /^#{@instrument_domain}.*$/)

  (instrumentation_filters || []).each do |filter|
    return false unless filter.allow_instrumentation? object, method_name
  end
  
  source = method.source
  sexp = ruby_parser.process source
  !sexp_instrumented? sexp
rescue MethodSource::SourceNotFoundError
  # sometimes this happens because the method_source gem bugs out with evals etc
  return false
rescue NameError => e
  # ghost method with no available source
  return false
end

#with_replace(*types, replacer) ⇒ Object



138
139
140
141
142
143
# File 'lib/adsl/extract/instrumenter.rb', line 138

def with_replace(*types, replacer)
  replace *types, replacer
  yield
ensure
  @replacers.pop
end