グラフがアニメーションするようにした

プログラムをきれいにしなきゃいかんなーと思いつつ、カオスな方向に突っ走ってしまいました。
Cometもどきを使ってグラフを動かすようにしました。
原理を簡単に説明します。
まず、ttp://localhostt:8088/graphでアクセスすると、グラフの枠とグラフを書くためのCanvas等を含むHTMLデータが返されます。このHTMLはonLoadイベントで次のような、コードが実行されます。

     window.onload = function() {
      draw_border();                                   /*  1 */
      var xmlhttp = new XMLHttpRequest();              /*  2 */
      xmlhttp.open("GET", "update.js", true);          /*  3 */
      xmlhttp.onreadystatechange=function() {          /*  4 */
         if (xmlhttp.readyState == 4) {                /*   */
           eval(xmlhttp.responseText);                 /*  5 */
         }
      }
      xmlhttp.send(null);                              /* 6 */
    };
  1. draw_borderは、枠線や中の罫線を書くための関数
  2. xmlhttpにXMLHttpRequestオブジェクトを代入します。このあたりは、激しくブラウザ依存ですがFireFox専用だよということで手を抜いています。
  3. update.jsなるファイルを読み込もうとしています。このファイルはファイルのように見せかけていますが、サーバで自動生成されるJavascriptプログラムです。
  4. 読み込みが終わったというイベントを拾います
  5. 読み込み終わったら結果をそのままevalします。(危ないです)
  6. 実際に読み込みを開始します。

update.jsはこんな感じです。細かいパラメータとかが変わります。

    var canctx = document.getElementById('graph').getContext('2d');
    canctx.strokeStyle = 'rgb(0, 0, 0)';
    canctx.lineWidth = 0.5;
canctx.clearRect(0, 0, 600, 400);
canctx.beginPath();
canctx.moveTo(0.0, 400.0);
canctx.lineTo(85.7142857142857, 400.0);
canctx.lineTo(171.428571428571, 400.0);
canctx.lineTo(257.142857142857, 400.0);
canctx.lineTo(342.857142857143, 400.0);
canctx.lineTo(428.571428571429, 400.0);
canctx.lineTo(600.0, 309.784);
    canctx.stroke();
document.getElementById("vlabel0").innerHTML = 0.0;
document.getElementById("vlabel1").innerHTML = 10000.0;
document.getElementById("vlabel2").innerHTML = 20000.0;
document.getElementById("vlabel3").innerHTML = 30000.0;
document.getElementById("vlabel4").innerHTML = 40000.0;
document.getElementById("vlabel5").innerHTML = 50000.0;
document.getElementById("vlabel6").innerHTML = 60000.0;
document.getElementById("vlabel7").innerHTML = 70000.0;
document.getElementById("vlabel8").innerHTML = 80000.0;
document.getElementById("vlabel9").innerHTML = 90000.0;
document.getElementById("vlabel10").innerHTML = 100000.0;
document.getElementById("hlabel0").innerHTML =  1.0;
document.getElementById("hlabel1").innerHTML =  2.4;
document.getElementById("hlabel2").innerHTML =  3.8;
document.getElementById("hlabel3").innerHTML =  5.2;
document.getElementById("hlabel4").innerHTML =  6.6;
document.getElementById("hlabel5").innerHTML =  8.0;
    xmlhttp.open("GET", "update.js", true);
    xmlhttp.onreadystatechange=function() {
       if (xmlhttp.readyState == 4) {
            eval(xmlhttp.responseText);
       }
    }
    xmlhttp.send(null);

最後に先ほど説明した読み込みコードが入っているので、またサーバーからupdate.jsを読み込もうとします。

サーバーではしばらく待ってレスポンスを返すということをやっています。一杯クライアントがあると大変ですが、プロファイルモニタ用と割り切っているので、クライアントは1つなのが前提です。ここのコードはこんな感じです。ここは、Rubyで書いてあります。

  def do_GET(req, res)
    if !defined? @graph_gen then
      @graph_gen = GraphGenCanvas.new(600, 400)
    end

    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

Content-Typeをtext/javascriptにしています。

サンプルとして、ObjectSpace.count_objectsの結果のうち:FREEをデータとして使うようになっています。本当は、authorNariさんのGC Profilerをグラフ化したいのですが使い慣れてないのでとりあえず見送りました。あと、パッチ無しで試せるということで、ObjectSpace.count_objectsのデータを使いました。
グラフにして、ObjectSpace.count_objects[:FREE]を見守っていると、単調に数が減っていき、あるときぴょこっと増える(多分GC起動)、また単調に数が減っていくというパターンです。あまり面白くないですが、とりあえずグラフが動きますよということで。

ソースリストです。

require 'webrick'
require 'erb'

include WEBrick

class GraphGen
  def initialize(xsize, ysize)
    @xsize = xsize
    @ysize = ysize
  end

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

  def position_in_graph(x, y)
    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

  GRAPH_SCRIPT_BEGIN = <<EOS
    var canctx = document.getElementById('graph').getContext('2d');
    canctx.strokeStyle = 'rgb(0, 0, 0)';
    canctx.lineWidth = 0.5;
EOS

  GRAPH_SCRIPT_END = <<EOS
    canctx.stroke();
EOS

  def initialize(xsize, ysize)
    super
  end

  def set_data(data)
    @data = []
    data.each do |gd|
      sd = gd.sort_by {|a| a[0]}
      sdx = sd.map {|a| a[0]}
      sdy = sd.map {|a| a[1]}
      ymin = sdy.min
      if ymin > 0 then
        ymin = 0
      end
      ymax = 10 ** ((Math.log10(sdy.max)).to_i + 1)
      set_data_range(sdx.min, sdx.max, ymin, ymax)
      @data.push sd
    end
  end

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

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

      res += "canctx.beginPath();\n"
      res += agraph
    end

    GRAPH_SCRIPT_BEGIN + res + GRAPH_SCRIPT_END
  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 ObjectCounter
  @@time = 7
  @@hash = Hash.new
  @@hist = [[0, 0], [1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0]]
  def self.gdata
    @@hist.shift
    @@time += 1
    ObjectSpace.count_objects(@@hash)
    @@hist.push [@@time, @@hash[:FREE]]
    @@hist
  end
end

s = HTTPServer.new(:Port => 8088, :BindAddress => "localhost")
trap("INT"){s.shutdown}

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">
    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>
  </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 do_GET(req, res)
    if !defined? @graph_gen then
      @graph_gen = GraphGenCanvas.new(600, 400)
    end

    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

s.mount("/graph", GraphServlet)
s.mount("/update.js", GraphServlet2)

s.start