require RUBY_VERSION == "1.8.7" ? "fastercsv" : "csv" require "erb" require "tmpdir" require "securerandom" require "rich/support/core/string/colorize" require "launchy" require "stack_tracy/core_ext" require "stack_tracy/event_info" require "stack_tracy/sinatra" require "stack_tracy/version" module StackTracy extend self PRESETS = { :core => "Array #{"BasicObject " unless RUBY_VERSION == "1.8.7"}Enumerable Fixnum Float Hash IO Integer Kernel Module #{"Mutex " unless RUBY_VERSION == "1.8.7"}Numeric Object Rational String Symbol Thread Time", :active_record => "ActiveRecord::Base", :data_mapper => "DataMapper::Resource" } @options = Struct.new(:dump_dir, :dump_source_location, :limit, :threshold, :messages_only, :slows_only, :only, :exclude).new(Dir::tmpdir, false, 7500, 0.001, false, false) class Error < StandardError; end def config yield @options end def start(options = {}) opts = merge_options(options) _start mod_names(opts[:only]), mod_names(opts[:exclude]) nil end def stop _stop nil end def stack_trace @stack_trace || [] end def select(*only) [].tap do |lines| first, call_stack, only = nil, [], only.flatten.collect{|x| x.split(" ")}.flatten stack_trace.each do |event_info| next unless process?(event_info, only) first ||= event_info if event_info.call? lines << event_info.to_hash(first).merge!(:depth => call_stack.size) call_stack << [lines.size - 1, event_info] elsif event_info.return? && call_stack.last && event_info.matches?(call_stack.last.last) call_stack.pop.tap do |(line, match)| lines[line][:duration] = event_info - match end end end end end def print(*only) puts select(only).collect{ |event| line = " " * event[:depth] line << event[:call] line << " <#{"%.6f" % event[:duration]}>" if event[:duration] } end def dump(path = nil, dump_source_location = nil, *only) unless path && path.match(/\.csv$/) path = File.join [path || @options.dump_dir, "stack_events-#{SecureRandom.hex(3)}.csv"].compact end File.expand_path(path).tap do |path| bool = dump_source_location.nil? ? @options[:dump_source_location] : dump_source_location keys = [:event, (:file if bool), (:line if bool), :singleton, :object, :method, :nsec, :time, :call, :depth, :duration] (RUBY_VERSION == "1.8.7" ? FasterCSV : CSV).open(path, "w", :col_sep => ";") do |file| file << keys select(only).each do |event| file << event.values_at(*keys) end end yield path if block_given? stack_trace.clear end end def open(path = nil, use_current_stack_trace = false, options = {}) if use_current_stack_trace file = File.expand_path(path) if path else unless path && path.match(/\.csv$/) path = Dir[File.join(path || @options.dump_dir, "stack_events-*.csv")].sort_by{|f| File.mtime(f)}.last path ||= Dir[File.join(".", "stack_events-*.csv")].sort_by{|f| File.mtime(f)}.last path ||= Dir[File.join(Dir::tmpdir, "stack_events-*.csv")].sort_by{|f| File.mtime(f)}.last end if path file = File.expand_path(path) else raise Error, "Could not locate StackTracy file" end end index = ui("index.html") if use_current_stack_trace || (file && File.exists?(file)) threshold = options["threshold"] || options[:threshold] || @options[:threshold] limit = options["limit"] || options[:limit] || @options[:limit] messages_only = [options["messages_only"], options[:messages_only], @options[:messages_only]].detect{|x| !x.nil?} slows_only = [options["slows_only"], options[:slows_only], @options[:slows_only]].detect{|x| !x.nil?} events = use_current_stack_trace ? select : StackTracy::EventInfo.to_hashes(File.read(file)) erb = ERB.new File.new(ui("index.html.erb")).read File.open(index, "w"){|f| f.write erb.result(binding)} elsif path && path.match(/\.csv$/) raise Error, "Could not locate StackTracy file at #{file}" end if File.exists?(index) if RbConfig::CONFIG["host_os"].match(/(mswin|mingw)/) # I know, don't say it! `start file://#{index}` else Launchy.open("file://#{index}") end nil else raise Error, "Could not locate StackTracy file" end end private def merge_options(hash = {}) {:only => @options.only, :exclude => @options.exclude}.merge(hash.inject({}){|h, (k, v)| h.merge!(k.to_sym => v)}) end def mod_names(arg) names = PRESETS.inject([arg || []].flatten.collect(&:to_s).join(" ")){|s, (k, v)| s.gsub k.to_s, v} if names.include?("*") names.split(/\s/).collect do |name| name.include?("*") ? mods_within([constantize(name.gsub("*", ""))]).collect(&:name) : spec end.flatten else names.split(/\s/) end.sort.join(" ") end def constantize(name) name.split("::").inject(Kernel){|m, x| m.const_get x} end def mods_within(mods, initial_array = nil) (initial_array || mods).tap do |array| mods.each do |mod| mod.constants.each do |c| const = mod.const_get c if !array.include?(const) && (const.is_a?(Class) || const.is_a?(Module)) && const.name.match(/^#{mod.name}/) array << const mods_within([const], array) end end end end end def process?(event_info, only) return false if "#{event_info.object}" == "StackTracy" return true if only.empty? only.any?{|x| event_info.matches?(x)} end def ui(file) File.expand_path("../../ui/#{file}", __FILE__) end end require File.expand_path("../../ext/stack_tracy/stack_tracy", __FILE__)