#---------------------------------------------------------------# # # # (C) Copyright Rubysophic Inc. 2007-2008 # # All rights reserved. # # # # Use, duplication or disclosure of the code is not permitted # # unless licensed. # # # # Last Updated: 7/09/08 # #---------------------------------------------------------------# # # # RubyRunInstrumentor__ module is responsible for performing # # instrumentation on ruby methods via metaprogramming. These # # methods belong to the candidate classes/modules discovered # # earlier in RubyRunInitializer__ plus others which are # # explicitly requested in rubyrun_opts via the use of # # INCLUDE_HASH. # # # #---------------------------------------------------------------# module RubyRunInstrumentor__ require 'logger' require 'yaml' require 'rubyrun_globals' require 'rubyrun_utils__' require 'rubyrun_monitor__' require 'rubyrun_tracer__' require 'rubyrun_dad__' require 'rubyrun_html__' require 'rubyrun_html_writer__' include RubyRunGlobals include RubyRunUtils__ include RubyRunMonitor__ include RubyRunTracer__ include RubyRunDad__ include RubyRunHTML__ # Invoked by the traps set up in rubyrun.rb. This indicates that # a file has been required/loaded such that the methods within # are being added to the process. Each method being added will # go through the following process to determine if it should be intrumented. # # 1. Through APP_PATHS a stream of class names whose methods are # identified as candiates for instrumnetation in RubyRunIntializer__. # This process forms the initial INCLUDE_HASH which states ALL methods # belonging to these classes/methods should be instrumented # 2. Through additional INCLUDE_HASH in rubyrun_opts a stream of # class => methods hash entries provide further candidates for instrumentation. # 3. Thru EXCLUDE_HASH the exclusion logic is then applied to reduce the scope # of instrumentation. # 4. Some classes and methods are never instrumented regarldess. These # are identifed in constants FIREWALL_HASH. def instrument_it?(type, klass, id) get_dad(type, klass, id) instrument_target(type, klass, id) \ if !(is_non_negotiably_excluded?(type, klass, id)) && !is_in?($rubyrun_exclude_hash, klass, id, 'strict') && is_in?($rubyrun_include_hash, klass, id, 'strict') end # Never instrument the following classes/methods to avoid recursion # 1. Exclude classes and methods that the instrumentation code uses # 2. Exclude method= # 3. Exclude method aliased by rubyrun instrumentation code # 4. Exclude method re-defined by rubyrun instrumentation code # 5. Exclude inherited instances, private, protected, and singleton methods. # The way this works is that if m is one of these non-inherited instance # methods or singleton methods then it should NOT be excluded. Otherwise # it is assumed it is an inherited one and hence excluded. def is_non_negotiably_excluded?(type, klass, id) return true if is_in?(RUBYRUN_FIREWALL_HASH, klass, id) return true if id.id2name[-1,1] == '=' if id.id2name[0, RUBYRUN_PREFIX_LENGTH] == RUBYRUN_PREFIX $rubyrun_prev_method = id.id2name return true end if ($rubyrun_prev_method ||="").include?(id.id2name) $rubyrun_prev_method = nil return true end if type == 'i' klass.instance_methods(false).each {|m| return false if m == id.id2name } klass.private_instance_methods(false).each {|m| return false if m == id.id2name } klass.protected_instance_methods(false).each {|m| return false if m == id.id2name } else klass.singleton_methods.each {|m| return false if m == id.id2name } end true end # First layer of code performing instrummentation on a class.method # The injecting code is different depending on whether the # method is an instance method, or singleton(static method of a class, # specific method added to an object). # # If this class is a Rails active controller class, create a hash # entry if it does not already exist. This hash is used to keep track # of performance metrics by action by controller. # # If we fail to instrument for whatever reason, log the # errors and leave the method alone. # # Also, create metrics hash for a RAILS controller class if it doesn't exist def instrument_target(type, klass, id) $rubyrun_logger.info "instrumenting #{klass.to_s}.#{id.id2name}." create_metrics_hash(klass) if is_rails_controller?(klass) begin case type when 'i' insert_code_to_instance_method(klass, id) when 's' insert_code_to_singleton_method(klass, id) else raise "undefined instrumentation type" end $rubyrun_logger.info "#{klass.to_s}.#{id.id2name} instrumented." rescue Exception => e $rubyrun_logger.info "Class #{klass.to_s}.#{id.id2name} failed to instrument" $rubyrun_logger.info e.to_s + "\n" + e.backtrace.join("\n") end end # To instrument an instance method of a class, a method proxy is used: # # 1. Alias the method to one with a prefix rubyrun_ (i.e., copy the method and # create another one with a new name) # 2. Create a new method with the original name. This is the method proxy. # 3. Preserve the intended visibility of the original method in the proxy # 4. This proxy method essentially adds pre and post wrapper code to # the original code. This wrapper code is embodied in collect_method_data # 5. All these must be done in the context of the class def insert_code_to_instance_method(klass, mid) klass.class_eval { alias_method "#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}", mid.id2name eval <<-CODE1 def #{mid.id2name} (*args, &blk) RubyRunInstrumentor__.collect_method_data(self, #{klass}, '#{mid}', *args) {self.send("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}", *args, &blk)} end CODE1 if klass.private_instance_methods(false).include?("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}") private mid elsif klass.protected_instance_methods(false).include?("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}") protected mid end } end # Same as insert_code_to_instance_method def insert_code_to_singleton_method(klass, mid) (class << klass; self; end).class_eval { alias_method "#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}", mid.id2name eval <<-EOF2 def #{mid.id2name} (*args, &blk) RubyRunInstrumentor__.collect_method_data(self, #{klass}, '#{mid}', *args) {self.send("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}", *args, &blk)} end EOF2 if self.private_instance_methods(false).include?("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}") private mid elsif self.protected_instance_methods(false).include?("#{RubyRunGlobals::RUBYRUN_PREFIX}_#{mid.id2name}") protected mid end } end # This is the piece of code that actually executed under the application # thread during runtime and is not executed during instrumentation time # # 1. Create a equivalent thread local storage to store request performance # metrics but only if this class is a Rails Active Controller class # 2. Trace pre and post execution of the original method which has been aliased # The original method is invoked via 'yield' # 3. When a method ends, report the timings to the response time component but # only if a thread local exists def collect_method_data(obj, klass, mid, *args) tid = get_thread_id create_thread_local(tid, obj.request, klass, mid) if is_rails_controller?(klass) rubyrun_trace = is_in?($rubyrun_trace_hash, klass, mid) if rubyrun_trace invoker = get_caller_detail enter_trace(tid, " Entry", obj, invoker, klass, mid, *args) end t1 = Time.new result = yield t2 = Time.new if rubyrun_trace (t2 - t1) >= RUBYRUN_HIGHLIGHT_THRESHOLD ? (type = "* #{sprintf("%6.2f", t2-t1)} Exit ") : (type = " #{sprintf("%6.2f", t2-t1)} Exit ") enter_trace(tid, type, nil, nil, klass, mid, nil) end report_rails_timing(klass, mid, t2, t1, tid) if $rubyrun_thread_local[tid] && (t2 - t1) > 0 result end # Instrument Thread.new by wrapping target proc with a # begin-rescue clause around the application block. # When the thread monitor shoot the thread via thr.raise # the rescue clause will catch the interrupt and collect the # stack entries in $@ and store them in a global hash, later # on printed in rubyrun log by thread id. If the thread dies # naturally, print the stack trace on the rubyrun log def instrument_thread_new (class << Thread; self; end).class_eval { alias_method "#{RubyRunGlobals::RUBYRUN_PREFIX}_new", "new" def new(*rubyrun_args, &rubyrun_apps_block) rubyrun_proc = lambda { begin rubyrun_apps_block.call(*rubyrun_args) rescue Exception => e e.message == RUBYRUN_KILL_3_STRING ? $@.each {|line| ($rubyrun_thread_stack[Thread.current] ||= []) << line} : $@.each {|line| $rubyrun_logger.info "#{line}"} end } self.send("#{RubyRunGlobals::RUBYRUN_PREFIX}_new", *rubyrun_args, &rubyrun_proc) end } end end