GCのグラフを作っています

authorNariさんのGC::Profilerの結果をグラフにするプログラムを作っています。動き始めたのですが、まだまだです。
とりあえずプログラムを貼っておきます。Ver Upしたら、ここを更新していきます。
使い方は、
ruby gcgraph.rb プログラム.rb 引数

とすると、プログラム.rbを実行して、GCの状況がhttp://localhost:8088/graphで見えます。横軸がプログラムの実行開始からの経過時間(秒)で、縦軸がuse size/ total sizeのバイト数です。青・赤線がそれぞれuse sizeとtotal sizeです。線のX座標がGCが発生した時間です。

gcgraph.rbです。もうちょっと完成したらgithubに上げたいなと思います。

2008/8/21 追記
今まではグラフの巾は常に1秒でしたが、グラフの巾を1秒、10秒、100秒に切り替えられるようにしました。
8/20に書いた説明が変なので、直しました。

require 'webrick'
require 'erb'

module GCGraph
  class GraphGen
    def initialize(xsize, ysize, xscale = 1)
      @xsize = xsize
      @ysize = ysize
      @xscale = xscale
    end

    def xscale=(sc)
      @xscale = sc
      set_data_range(@ominx, @maxx, @miny, @maxy)
    end
    
    def set_data_range(minx, maxx, miny, maxy)
      @ominx = minx.to_f
      @maxx = maxx.to_f
      @miny = miny.to_f
      @maxy = maxy.to_f

      if @ominx + @xscale < @maxx then
        @minx = @maxx - @xscale
      else
        @minx = @ominx
      end
    end
    
    def position_in_graph(x, y)
      if x < @minx or @maxx < x then
        return [nil, nil]
      end
      if y < @miny or @maxy < y then
        return [nil, nil]
      end

      dx = @maxx - @minx
      posx = ((x - @minx) / dx) * @xsize
      
      dy = @maxy - @miny
      posy = ((y - @miny) / dy) * @ysize
      
      [posx, posy]
    end
  end
  
  class GraphGenCanvas<GraphGen
    NUM_VLABEL = 10
    NUM_HLABEL = 5
    
    BODRER_SCRIPT_TEMPLATE = <<EOS
    function draw_border() {
      var canctx = document.getElementById('border').getContext('2d')
      canctx.rect(0, 0, <%= @xsize%>, <%= @ysize%>);
      canctx.stroke();

      canctx.strokeStyle = 'rgb(0, 128, 128)';
      canctx.lineWidth = 0.5;

      var cnt = 0;
      while (cnt < <%= @ysize%>){
        cnt += 40;
        canctx.beginPath();
        canctx.moveTo(0,  cnt);
        canctx.lineTo(<%= @xsize%>,  cnt);
        canctx.stroke();
      }

      cnt = 0;
      while (cnt < <%= @xsize%>){
        cnt += 40;
        canctx.beginPath();
        canctx.moveTo(cnt, 0);
        canctx.lineTo(cnt, <%= @ysize%>);
        canctx.stroke();
      }
    }
EOS

    def graph_script_begin
<<EOS
    var canctx = document.getElementById('graph').getContext('2d');
EOS
    end

    def graph_script_set_line_prop(color)
<<EOS
    canctx.strokeStyle = '#{color}';
    canctx.lineWidth = 3;
EOS
    end

    def initialize(xsize, ysize)
      super
    end

    def set_data(data)
      xmin = nil
      xmax = nil
      ymin = nil
      ymax = nil
      @data = []
      data.each do |gd|
        sd = gd.sort_by {|a| a[0]}
        sdx = sd.map {|a| a[0]}
        sdy = sd.map {|a| a[1]}
        cymin = sdy.min
        if ymin == nil or cymin < ymin then
          ymin = cymin
        end
        if ymin > 0 then
          ymin = 0
        end

        cymax = 10 ** ((Math.log10(sdy.max + 1.1)).to_i + 1)
        if ymax == nil or ymax < cymax then
          ymax = cymax
        end

        cxmin = sdx.min
        if xmin == nil or cxmin < xmin then
          xmin = cxmin
        end

        cxmax = sdx.max
        if xmax == nil or xmax < cxmax then
          xmax = cxmax
        end

        @data.push sd
      end

      set_data_range(xmin, xmax, ymin, ymax)
    end

    def border_script
      ERB.new(BODRER_SCRIPT_TEMPLATE).result(binding)
    end

    GraphColor = ['Red', 'Blue']
    def graph_script
      res = "canctx.clearRect(0, 0, #{@xsize}, #{@ysize});\n" 
      @data.each_with_index do |gd, i|
        agraph = ""
        gd.each do |a|
          x, y = position_in_graph(a[0], a[1])
          if x then
            y = @ysize - y
            agraph += "canctx.moveTo(#{x}, #{@ysize});\n"
            agraph += "canctx.lineTo(#{x}, #{y});\n"
          end
        end

        res += graph_script_set_line_prop(GraphColor[i])
        res += "canctx.beginPath();\n"
        res += agraph
        res += "canctx.stroke();"
      end
      graph_script_begin + res
    end

    def vlabel_css
      res = ""
      iy = @ysize + 40
      dy = @ysize / NUM_VLABEL
      (0..NUM_VLABEL).each do |i|
        value = "left: 0px; top: #{iy  -  dy * i}px"
        res += "span#vlabel#{i} { #{value} }\n"
      end
      
      res
    end

    def vlabel_html
      res = ""
      iy = @miny
      dy = (@maxy - @miny) / NUM_VLABEL
      (0..NUM_VLABEL).each do |i|
        res += "<span id=\"vlabel#{i}\"> #{iy  +  dy * i} </span>\n"
      end
      
      res
    end

    def vlabel_script
      res = ""
      iy = @miny
      dy = (@maxy - @miny) / NUM_VLABEL
      (0..NUM_VLABEL).each do |i|
        dest = "document.getElementById(\"vlabel#{i}\").innerHTML"
        res += "#{dest} = #{iy  +  dy * i};\n"
      end
      
      res
    end

    def hlabel_css
      res = ""
      ix = 40
      dx = @xsize / NUM_HLABEL
      (0..NUM_HLABEL).each do |i|
        value = "left: #{dx * i + ix}px; top: #{@ysize + 50}px"
        res += "span#hlabel#{i} { #{value} }\n"
      end
      
      res
    end

    def hlabel_html
      res = ""
      dx = (@maxx - @minx) / NUM_HLABEL
      (0..NUM_HLABEL).each do |i|
        res += "<span id=\"hlabel#{i}\"> #{dx * i + @minx} </span>\n"
      end
      
      res
    end

    def hlabel_script
      res = ""
      dx = (@maxx - @minx) / NUM_HLABEL
      (0..NUM_HLABEL).each do |i|
        dest = "document.getElementById(\"hlabel#{i}\").innerHTML"
        res += "#{dest} =  #{dx * i + @minx};\n"
      end
      
      res
    end
  end

  class GraphServer
    include WEBrick
    class GraphServlet<HTTPServlet::AbstractServlet
      SCRIPT = <<EOS
<html>
  <style type="text/css">
    canvas {position: absolute}
    span {position: absolute}
    canvas#border { left: 40px; top: 50px }
    canvas#graph { left: 40px; top: 50px }
    div#vlabel span {text-align: right; width: 30px }
    div#hlabel span {text-align: left; width: 30px }
    <%= @graph_gen.vlabel_css %>
    <%= @graph_gen.hlabel_css %>
  </style>
    
  <script type="text/javascript" id="gscript">
   <%= @graph_gen.border_script %>
  </script>

  <script type="text/javascript" id="main">
    function setscale(scale) {
      var xmlhttp = new XMLHttpRequest();
      xmlhttp.open("GET", "setscale?SCALE=" + scale, true);
      xmlhttp.send(null);
    }

    window.onload = function() {
      draw_border();
      var xmlhttp = new XMLHttpRequest();
      xmlhttp.open("GET", "update.js", true);
      xmlhttp.onreadystatechange=function() {
         if (xmlhttp.readyState == 4) {
           eval(xmlhttp.responseText);
         }
      }
      xmlhttp.send(null);
    };

  </script>
  <body>
    <canvas id="border" width="600px" height="400px"></canvas>
    <canvas id="graph" width="600px" height="400px"></canvas>
    <div id="vlabel">
      <%= @graph_gen.vlabel_html %>
    </div>
    <div id="hlabel">
      <%= @graph_gen.hlabel_html %>
    </div>
    Time Scale: 
    <input type="radio" name="sc" onClick="setscale(this.value);" value="1"/> 1
    <input type="radio" name="sc" onClick="setscale(this.value);" value="10"/> 10
    <input type="radio" name="sc" onClick="setscale(this.value);" value="100"/> 100
  </body>
</html>
EOS

      def do_GET(req, res)
        if !defined? @graph_gen then
          @graph_gen = GraphGenCanvas.new(600, 400)
        end
        
        @graph_gen.set_data([[[0, 0], [1, 1000]]])
        res.body = ERB.new(SCRIPT).result(binding)
        res['Content-Type'] = "text/html"
      end
    end

    class GraphServlet2<HTTPServlet::AbstractServlet
      ENDSCRIPT = <<EOS
    xmlhttp.open("GET", "update.js", true);
    xmlhttp.onreadystatechange=function() {
       if (xmlhttp.readyState == 4) {
            eval(xmlhttp.responseText);
       }
    }
    xmlhttp.send(null);
EOS

      def initialize(sv, opt)
        super
        @graph_gen = opt
      end

      def do_GET(req, res)
        sleep(1)
        @graph_gen.set_data(ObjectCounter.gdata)
        script = @graph_gen.graph_script
        script += @graph_gen.vlabel_script
        script += @graph_gen.hlabel_script
        res.body = script + ENDSCRIPT
        res['Content-Type'] = "text/javascript"
      end
    end

    class GraphServletSetScale<HTTPServlet::AbstractServlet
      def initialize(sv, opt)
        super
        @graph_gen = opt
      end

      def do_GET(req, res)
        scale = req.query['SCALE'].to_i
        if scale != 0 then
          @graph_gen.xscale = scale
        end
      end
    end

    def initialize
      @graph_gen = GraphGenCanvas.new(600, 400)
      @server = HTTPServer.new(:Port => 8088, :BindAddress => "localhost")
      @server.logger.close
      trap("INT"){@server.shutdown}
      @server.mount("/graph", GraphServlet)
      @server.mount("/update.js", GraphServlet2, @graph_gen)
      @server.mount("/setscale", GraphServletSetScale, @graph_gen)
      @server.start
    end
  end

  class ObjectCounter
    def self.gdata
      duse = []
      dtotal = []
      rep = GC::Profiler.result
      rep = rep.split(/\n/)
      rep.shift
      rep.shift
      
      if rep.size < 2 then
        rep = ["0 0 0 0 0", "0, 0.1, 1, 1, 1"] + rep
      end
      rep.each do |es|
        ea = es.split(/\s+/)
        duse.push [ea[2].to_f, ea[3].to_f]
        dtotal.push [ea[2].to_f, ea[4].to_f]
      end
      [dtotal, duse]
    end
  end
end  # GCGraph

GC::Profiler.enable
GC.start
Thread.new {
  a = GCGraph::GraphServer.new
}.run

$0 = ARGV[0]
ARGV.shift
load $0