# Copyright (c) 2013 AppNeta, Inc. # All rights reserved. module TraceView module API ## # Module that provides profiling of arbitrary blocks of code module Profiling ## # Public: Profile a given block of code. Detect any exceptions thrown by # the block and report errors. # # profile_name - A name used to identify the block being profiled. # report_kvs - A hash containing key/value pairs that will be reported along # with the event of this profile (optional). # with_backtrace - Boolean to indicate whether a backtrace should # be collected with this trace event. # # Example # # def computation(n) # TraceView::API.profile('fib', { :n => n }) do # fib(n) # end # end # # Returns the result of the block. def profile(profile_name, report_kvs = {}, with_backtrace = false) report_kvs[:Language] ||= :ruby report_kvs[:ProfileName] ||= profile_name report_kvs[:Backtrace] = TraceView::API.backtrace if with_backtrace TraceView::API.log(nil, 'profile_entry', report_kvs) begin yield rescue => e log_exception(nil, e) raise ensure exit_kvs = {} exit_kvs[:Language] = :ruby exit_kvs[:ProfileName] = report_kvs[:ProfileName] TraceView::API.log(nil, 'profile_exit', exit_kvs) end end ## # Public: Profile a method on a class or module. That method can be of any (accessible) # type (instance, singleton, private, protected etc.). # # klass - the class or module that has the method to profile # method - the method to profile. Can be singleton, instance, private etc... # opts - a hash specifying the one or more of the following options: # * :arguments - report the arguments passed to method on each profile (default: false) # * :result - report the return value of method on each profile (default: false) # * :backtrace - report the return value of method on each profile (default: false) # * :name - alternate name for the profile reported in the dashboard (default: method name) # extra_kvs - a hash containing any additional KVs you would like reported with the profile # # Example # # opts = {} # opts[:backtrace] = true # opts[:arguments] = false # opts[:name] = :array_sort # # TraceView::API.profile_method(Array, :sort, opts) # def profile_method(klass, method, opts = {}, extra_kvs = {}) # If we're on an unsupported platform (ahem Mac), just act # like we did something to nicely play the no-op part. return true unless TraceView.loaded if RUBY_VERSION < '1.9.3' TraceView.logger.warn '[traceview/error] profile_method: Use the legacy method profiling for Ruby versions before 1.9.3' return false elsif !klass.is_a?(Module) TraceView.logger.warn "[traceview/error] profile_method: Not sure what to do with #{klass}. Send a class or module." return false elsif !method.is_a?(Symbol) if method.is_a?(String) method = method.to_sym else TraceView.logger.warn "[traceview/error] profile_method: Not sure what to do with #{method}. Send a string or symbol for method." return false end end instance_method = klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method) class_method = klass.singleton_methods.include?(method) # Make sure the request klass::method exists if !instance_method && !class_method TraceView.logger.warn "[traceview/error] profile_method: Can't instrument #{klass}.#{method} as it doesn't seem to exist." TraceView.logger.warn "[traceview/error] #{__FILE__}:#{__LINE__}" return false end # Strip '!' or '?' from method if present safe_method_name = method.to_s.chop if method.to_s =~ /\?$|\!$/ safe_method_name ||= method without_traceview = "#{safe_method_name}_without_traceview" with_traceview = "#{safe_method_name}_with_traceview" # Check if already profiled if klass.instance_methods.include?(with_traceview.to_sym) || klass.singleton_methods.include?(with_traceview.to_sym) TraceView.logger.warn "[traceview/error] profile_method: #{klass}::#{method} already profiled." TraceView.logger.warn "[traceview/error] profile_method: #{__FILE__}:#{__LINE__}" return false end source_location = [] if instance_method ::TraceView::Util.send_include(klass, ::TraceView::MethodProfiling) source_location = klass.instance_method(method).source_location elsif class_method ::TraceView::Util.send_extend(klass, ::TraceView::MethodProfiling) source_location = klass.method(method).source_location end report_kvs = collect_profile_kvs(klass, method, opts, extra_kvs, source_location) report_kvs[:MethodName] = safe_method_name if instance_method klass.class_eval do define_method(with_traceview) do |*args, &block| profile_wrapper(without_traceview, report_kvs, opts, *args, &block) end alias_method without_traceview, "#{method}" alias_method "#{method}", with_traceview end elsif class_method klass.define_singleton_method(with_traceview) do |*args, &block| profile_wrapper(without_traceview, report_kvs, opts, *args, &block) end klass.singleton_class.class_eval do alias_method without_traceview, "#{method}" alias_method "#{method}", with_traceview end end true end private ## # Private: Helper method to aggregate KVs to report # # klass - the class or module that has the method to profile # method - the method to profile. Can be singleton, instance, private etc... # opts - a hash specifying the one or more of the following options: # * :arguments - report the arguments passed to method on each profile (default: false) # * :result - report the return value of method on each profile (default: false) # * :backtrace - report the return value of method on each profile (default: false) # * :name - alternate name for the profile reported in the dashboard (default: method name) # extra_kvs - a hash containing any additional KVs you would like reported with the profile # source_location - array returned from klass.method(:name).source_location # def collect_profile_kvs(klass, method, opts, extra_kvs, source_location) report_kvs = {} report_kvs[:Language] ||= :ruby report_kvs[:ProfileName] ||= opts[:name] ? opts[:name] : method if klass.is_a?(Class) report_kvs[:Class] = klass.to_s else report_kvs[:Module] = klass.to_s end # If this is a Rails Controller, report the KVs if defined?(::AbstractController::Base) && klass.ancestors.include?(::AbstractController::Base) report_kvs[:Controller] = klass.to_s report_kvs[:Action] = method.to_s end # We won't have access to this info for native methods (those not defined in Ruby). if source_location.is_a?(Array) && source_location.length == 2 report_kvs[:File] = source_location[0] report_kvs[:LineNumber] = source_location[1] end # Merge in any extra_kvs requested report_kvs.merge!(extra_kvs) end end end end