require "optparse"
require "timeout"
require "net/http"

class String
  def /(o) File.join(self, o.to_s) end
end

class RamazeBenchmark

  class BasicWriter
    def initialize
      @ljust = 24
    end

    def write(key, val)
      if key == "Name"
        puts "====== #{val} ======"
      else
        puts((key.to_s + ":").ljust(@ljust) + val.to_s)
      end
    end

    def flush
      puts
      $stdout.flush
    end

    def close; end
  end

  class CSVWriter
    def initialize
      @keys, @vals = [], []
    end

    def write(key, val)
      @keys << key
      @vals << (val =~ /^\d[\d.]+/ ? $& : val)
    end

    def flush
      unless @header
        puts FasterCSV.generate{|csv| csv << @keys }
        @header = true
      end

      puts FasterCSV.generate{|csv| csv << @vals }
      @keys, @vals = [], []
    end

    def close; end
  end

  class GruffWriter
    RPS = "Requests per second"

    def initialize
      @benchmarks = {}
      @order = []
    end

    def write(key, val)
      @benchmark ||= {}
      @benchmark[key] = (val =~ /^\d[\d.]+/ ? $&.to_f : val)
    end

    def flush
      if @benchmark["Requests per second"]
        name = @benchmark["Name"]
        @benchmarks[name] ||= []
        @benchmarks[name] << @benchmark
        @order << name unless @order.include?(name)
      end
      @benchmark = nil
    end

    def close
      g = Gruff::SideBar.new(800)
      g.title = "Ramaze Benchmark"
      setup_gruff_data(g)
      g.labels = gruff_labels
      g.sort = false
      g.hide_legend = true if graph_type == :simple
      g.x_axis_label = "requests/sec"
      g.minimum_value = 0
      g.write
    end

    private

    def gruff_labels
      table = {}
      @order.each_with_index do |bname, idx|
        table[idx] = bname
      end
      return table
    end

    def adapters
      @benchmarks.to_a[0][1].map{|bm| bm["Adapter"]}.uniq
    end

    def paths
      @benchmarks.to_a[0][1].map{|bm| bm["Path"]}.uniq
    end

    def graph_type
      case [adapters.size > 1, paths.size > 1]
      when [true , true ]; :both
      when [true , false]; :adapter
      when [false, true ]; :path
      else :simple; end
    end

    def setup_gruff_data(g)
      case graph_type
      when :simple
        g.data("", @order.map{|name| @benchmarks[name][0][RPS] }, '#6886B4')
      when :adapter
        adapters.each do |adapter|
          g.data(adapter, @order.map{|name|
                   @benchmarks[name].find{|bm| bm["Adapter"] == adapter}[RPS]
                 })
        end
      when :path
        paths.each do |path|
          g.data(path, @order.map{|name|
                   @benchmarks[name].find{|bm| bm["Path"] == path}[RPS]
                 })
        end
      when :both
        adapters.each do |adapter|
          paths.each do |path|
            g.data("#{adapter}, #{path}", @order.map{|name|
                     @benchmarks[name].find{|bm|
                       bm["Adapter"] == adapter && bm["Path"] == path
                     }[RPS]
                   })
          end
        end
      end
    end
  end

  attr_accessor :requests, :adapters, :port, :log, :display_code, :target
  attr_accessor :concurrent, :paths, :benchmarker, :informer
  attr_accessor :mode, :show_log, :formats

  def initialize()
    @adapters = [:webrick]
    @port = rand(32768-1)+32768
    @requests = 100
    @concurrent = 10
    @signal = 'SIGKILL'
    @host = "127.0.0.1"
    @paths = ["/"]
    @target = /.+/
    @informer = true
    @formats = ["text"]
    @writers = []
    @mode = :live
    yield self
  end

  def start
    # setup writers
    @formats.each do |format|
      case format
      when "csv"  ; @writers << CSVWriter.new
      when "gruff"; @writers << GruffWriter.new
      when "text" ; @writers << BasicWriter.new
      end
    end

    # benchmarks
    glob = File.expand_path('../suite/*.rb', __FILE__)
    Dir.glob(glob) do |filename|
      @adapters.each do |adapter|
        @paths.each do |path|
          benchmark(filename, adapter, path) if @target.match(filename)
        end
      end
    end

    # close writers
    @writers.each do |writer|
      writer.close
    end
  end

  # start to measure
  def benchmark(filename, adapter, path)
    # output informations
    l :Name,       filename.scan(/\/([^\/]+)\.rb/).to_s
    l :Adapter,    adapter
    l :Requests,   @requests
    l :Concurrent, @concurrent
    l :Path,       path
    l :Informer,   @informer
    if @display_code
      l :Code, "<code ruby>\n#{File.read(filename)}\n</code>\n\n"
    end

    # real benchmark
    ramaze(filename, adapter) do |pid|
      l "Mem usage before", "#{memsize(pid)}MB"
      ab(path).each do |line|
        l *line.split(/:\s*/)
      end
      l "Mem usage after", "#{memsize(pid)}MB"
    end

    # flush writers
    @writers.each do |writer|
      writer.flush
    end
  end

  private

  # memory usage size
  def memsize(pid)
    (`ps -p #{pid} -o rss=`.strip.to_f/10.24).round/100.0
  end

  # output
  def l(key, val)
    @writers.each do |writer|
      writer.write(key.to_s, val)
    end
  end

  # url of ramaze server
  def url(path)
    "http://#{@host}:#{@port}#{path}"
  end

  # apache benchmark
  def ab(path)
    re = /^(Fail|Req|Time|Total transferred|Document Length|Transfer rate)/
    `ab -c #{@concurrent} -n #{@requests} #{url(path)}`.split("\n").grep(re)
  end

  # startup
  def ramaze(filename, adapter)
    pid = fork do
      begin
        require 'ramaze'
        Ramaze.options.mode = @mode
        if @informer
          unless @show_log
            require 'ramaze/log/informer'
            Ramaze::Log.loggers = [Ramaze::Logger::Informer.new("/dev/null")]
          end
        else
          Ramaze::Log.loggers = []
        end
        require filename
        Ramaze.start :adapter => adapter, :port => @port
      rescue LoadError => ex; l :Error, ex; end
    end

    yield pid if wait_to_startup

    Process.kill(@signal, pid)
    Process.waitpid2(pid)
  end

  # wait for ramaze to start up
  def wait_to_startup
    begin
      Timeout.timeout(5) do
        loop do
          begin
            sleep 1; Net::HTTP.new(@host, @port).head("/"); return true
          rescue Errno::ECONNREFUSED; end
        end
      end
    rescue Timeout::Error
      l "Error", "failed to start benchmark script"; return false
    end
  end
end

Signal.trap(:INT, proc{exit})

RamazeBenchmark.new do |bm|
  OptionParser.new(false, 24, "  ") do |opt|
    opt.on('-a', '--adapters NAMES', '[webrick] Specify adapters') do |adapters|
      bm.adapters = adapters.split(",")
    end

    opt.on('--formats (text|csv|gruff)', '[text] Specify output formats') do |formats|
      bm.formats = formats.split(",")
      bm.formats.each do |format|
        case format
        when "csv"  ; require "fastercsv"
        when "gruff"; require "gruff"
        end
      end
    end

    opt.on('-n', '--requests NUM', '[100] Number of requests') do |n|
      bm.requests = n
    end

    opt.on('-c', '--concurrent NUM', '[10] Number of multiple requests') do |n|
      bm.concurrent = n
    end

    opt.on('--code', 'Display benchmark code') do |n|
      bm.display_code = true
    end

    opt.on('-p', '--port NUM',
           '[random(32768-65535)] Specify port number') do |n|
      bm.port = n
    end

    opt.on('--paths PATHS', '[/] Specify request paths') do |paths|
      bm.paths = paths.split(",")
    end

    opt.on('--mode MODE', '[live] Ramaze mode') do |mode|
      bm.mode = mode.to_sym
    end

    opt.on('--no-informer', 'Disable informer') do
      bm.informer = false
    end

    opt.on('--show-log', 'Show log') do
      bm.show_log = true
    end

    opt.on('--target REGEXP',
           '[/.+/] Specify benchmark scripts to measure') do |name|
      bm.target = Regexp.compile(name)
    end

    opt.on('-h', '--help', 'Show this message') do
      puts opt.help
      exit
    end

    begin
      opt.parse!(ARGV)
    rescue OptionParser::ParseError => ex
      puts "[ERROR] " + ex
      puts opt.help
      exit
    end
  end
end.start
