#!/usr/bin/env ruby

require 'rubygems'
require 'optparse'
require 'ostruct'

$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
require 'railsbench/gc_info'

# parse options
o = OpenStruct.new
o.title = "GC Data Plot"
o.graph_width = '1400x1050'
o.font_size = 10
o.ignored_object_types = []
o.output_file = nil
o.plot_live = true
o.plot_freed = true
o.plot_type = :both
o.engine = :gruff

parser = OptionParser.new do |opts|
  opts.banner = "Usage: perf_plot_gc [options] file1 file2 ..."

  opts.separator ""
  opts.separator "Options:"

  opts.on("-t", "--title T",
          "Specify the title for your plot") do |t|
    o.title = t
  end

  opts.on("-i", "--ignore LIST", Array,
          "Specify the object types to ignore") do |i|
    o.ignored_object_types = i.map{|t| t.upcase}
  end

  opts.on("-d", "--type TYPE", [:freed, :live, :both],
          "Select data points to plot: (live, freed, both)") do |dp|
    o.plot_type = dp
    o.plot_freed = o.plot_live = false
    case dp
    when :live  then o.plot_live = true
    when :freed then o.plot_freed = true
    when :both  then o.plot_freed = true; o.plot_live = true
    end
  end

  opts.on("-e", "--engine ENGINE", [:gruff, :gnuplot],
          "Select plotting engine: (gruff, gnuplot)") do |e|
    o.engine = e
  end

  opts.on("-f", "--font-size N", Integer,
          "Overall font size to use in the plot (points)") do |n|
    o.font_size = n
  end

  opts.on("-w", "--width W", Integer,
          "Width of the plot (pixels)") do |w|
    o.graph_width = w
  end

  opts.on("-g", "--geometry WxH", /\d+x\d+/,
          "Specify plot dimensions (pixels)") do |d|
    o.graph_width = d
  end

  opts.on("-o", "--out FILE",
          "Specify output file") do |f|
    o.output_file = f
  end

  opts.on_tail("-h", "--help", "Show this message") do
    puts opts
    exit
  end
end

# option compatibility with older versions
args=[]
ARGV.each do |arg|
  arg = arg.sub("font_size", "font-size") if arg =~ /^-font_size/
  arg = "-" + arg if arg =~ /^-(title|width|out|geometry|font-size|ignore)/
  args << arg
end

parser.parse!(args)

o.output_file ||=  if o.engine == :gruff
                     "graph.png"
                   elsif RUBY_PLATFORM =~ /darwin/
                     "graph.pdf"
                   else
                     "graph.ps"
                   end
o.files = []
o.names = []
args.each do |arg|
  o.files << File.open_or_die(arg)
  o.names[o.files.length-1] ||= File.basename(arg)
end

o.files.length > 0 or die(parser.banner)

o.gcis = []
o.files.each do |file|
  o.gcis << GCInfo.new(file)
  file.close
end

# o.object_types = %w(NODE STRING ARRAY HASH SCOPE VARMAP CLASS ICLASS REGEXP FLOAT MATCH FILE DATA MODULE OBJECT)
o.colors = %w(ff0000 00c000 0080ff c000ff 00eeee c04000 ee0000 2020c0 ffc020 008040 a080ff 804000 ff80ff 00c060 006080 c06080 008000 40ff80 306080 806000).map{|c| "#" + c}
o.object_types = GCInfo.object_types(o.gcis)
o.gc_count_max = o.gcis.map{|gci| gci.collections}.max
o.gc_max_processed = o.gcis.map{|gci| gci.processed_max}.max
o.gc_max_freed = o.gcis.map{|gci| gci.freed_max}.max
o.gc_max_live = o.gcis.map{|gci| gci.live_max}.max

o.title << " ["
o.title << "freed" if o.plot_freed
o.title << "," if o.plot_freed && o.plot_live
o.title << "live" if o.plot_live
o.title << "]"
o.title << " (ignoring #{o.ignored_object_types.join(', ')})" unless o.ignored_object_types.empty?

# for very large logs, we need to ignore some entries
N = o.gc_count_max < 100 ? 1 : o.gc_count_max / 99

class Plotter
  attr_reader :o
  def initialize(options)
    @o = options
  end

  def plot_with_gruff
    require 'gruff'

    g = Gruff::StackedBar.new(o.graph_width)

    # on OS X, ImageMagick can't find it's default font (arial) sometimes, so specify some font
    # g.font = 'Arial-Normal' if RUBY_PLATFORM =~ /darwin/
    g.font = 'Helvetica-Narrow' if RUBY_PLATFORM =~ /darwin/

    %w(#FF0000 #00FF00 #0000FF #D2FF77 #FF68C0 #D1FDFF #FFF0BD #15FFDC
    ).each do |color|
      g.add_color(color)
    end

    g.title = o.title
    g.sort = false
    g.title_font_size = o.font_size+2
    g.legend_font_size = o.font_size-2
    g.legend_box_size = o.font_size-2
    g.marker_font_size = o.font_size-2
    if o.ignored_object_types.empty?
      g.minimum_value = 0
      maximums = []
      maximums << o.gc_max_live if o.plot_live
      maximums << o.gc_max_freed if o.plot_freed
      g.maximum_value = maximums.max
    end
    label_step = 0
    label_step += 1 if o.plot_live
    label_step += 1 if o.plot_freed
    g.labels = Hash[* (0...o.gc_count_max).map{|i| [label_step*i*o.files.length, i.to_s]}.flatten ]
    # puts g.labels.inspect
    # puts object_types.inspect
    puts "ignoring #{o.ignored_object_types.join(', ')}" unless o.ignored_object_types.empty?

    (o.object_types + %w(FREELIST)).each do |ot|
      data = prepare_data(ot)
      # puts "#{ot}: #{data.inspect}"
      g.data(ot, data)
    end

    g.write(o.output_file)
  end

  def prepare_data(ot)
    return if o.ignored_object_types.include?(ot)
    data = []
    o.gc_count_max.times do |gc_index|
      next unless 0 == gc_index.modulo(N)
      for gci in o.gcis
        map_at_this_gc = gci.freed_objects[gc_index].merge('FREELIST' => gci.freelist[gc_index])
        data << ((map_at_this_gc && map_at_this_gc[ot]) || 0) if o.plot_freed
        map_at_this_gc = gci.live_objects[gc_index]
        data << ((map_at_this_gc && map_at_this_gc[ot]) || 0) if o.plot_live
      end
    end
    data
  end

  def plot_with_gnuplot
    require 'gnuplot'
    # there's a separate gnuplot binary which can read from stdin, but the gem doesn't know this
    ENV['RB_GNUPLOT'] ||= 'pgnuplot.exe' if RUBY_PLATFORM =~ /mswin/
    plot = Gnuplot::Plot.new
    if o.output_file =~ /\.pdf$/
      plot.terminal "pdf enhanced color font 'Helvetica,4'"
    else
      plot.terminal "postscript enhanced color font 'Helvetica,4'"
    end
    plot.output     o.output_file
    plot.xlabel     "Collections"
    plot.ylabel     "Objects"
    plot.title      o.title
    plot.style      "fill solid 1.0 noborder"
    plot.style      "data histogram"
    plot.style      "histogram rowstacked"
    plot.xtics      "out nomirror"
    plot.xrange     "[-1:#{(o.gc_count_max/N)*((o.plot_type == :both) ? 2 : 1)}]"
    plot.key        "outside invert reverse"
    plot.boxwidth   "0.8"
    plot.grid       "nopolar"
    plot.grid       "noxtics nomxtics ytics nomytics noztics nomztics nox2tics nomx2tics noy2tics nomy2tics nocbtics nomcbtics"
    plot.grid       "back linetype 0 linewidth 0.7"

    (o.object_types + %w(FREELIST)).each_with_index do |ot, i|
      next unless data = prepare_data(ot)
      plot.data << Gnuplot::DataSet.new([[ot.downcase] + data]) do |ds|
        ds.using = "1 title 1 lc rgb '#{ot == 'FREELIST' ? '#666666' : o.colors[i]}'"
      end
    end

    cmds = plot.to_gplot # puts cmds
    Gnuplot.open(false){|gnuplot| gnuplot << cmds}
  end

  def plot
    send "plot_with_#{o.engine}"
  end
end

Plotter.new(o).plot

__END__

#    Copyright (C) 2005-2008  Stefan Kaes
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
