グラフがアニメーションするようにした
プログラムをきれいにしなきゃいかんなーと思いつつ、カオスな方向に突っ走ってしまいました。
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 */ };
- draw_borderは、枠線や中の罫線を書くための関数
- xmlhttpにXMLHttpRequestオブジェクトを代入します。このあたりは、激しくブラウザ依存ですがFireFox専用だよということで手を抜いています。
- update.jsなるファイルを読み込もうとしています。このファイルはファイルのように見せかけていますが、サーバで自動生成されるJavascriptプログラムです。
- 読み込みが終わったというイベントを拾います
- 読み込み終わったら結果をそのままevalします。(危ないです)
- 実際に読み込みを開始します。
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